Binary Exploitation

tea-cash

CyLab (picoCTF)

picoCTF - tea-cash Write-Up

Informasi Challenge

Atribut Detail
Nama Challenge tea-cash
Kategori Binary Exploitation
Tingkat Kesulitan Medium
Poin 100
Files heapedit (binary), Makefile.share, libc.so.6, heapedit.c (source code)

Deskripsi Challenge

Deskripsi tantangannya:

You've stumbled upon a mysterious cash register that doesn't keep
money — it keeps secrets in memory. Traverse the free list and find
all the free chunks to get to the flag.

Dari deskripsi ini terlihat jelas bahwa fokus challenge adalah menyusuri (traverse) free list pada heap untuk menemukan seluruh chunk yang sudah di-free, yang pada akhirnya mengarah ke flag.

Dua hal yang perlu diperhatikan dari source code: 1. Program mengalokasikan beberapa chunk berukuran sama, mengisi salah satu chunk dengan isi flag.txt, lalu membebaskan (free) seluruh chunk tersebut. 2. Setelah itu, program meminta user memasukkan alamat-alamat chunk satu per satu untuk "memverifikasi" struktur tcache free list.

Kedua poin ini mengarah langsung ke pemahaman tentang struktur tcache (thread-local cache) pada GLIBC dan bagaimana ia menyimpan chunk yang sudah di-free.

Memahami Kerentanannya (Tcache Free List Traversal)

Source code heapedit.c menyiapkan skenario berikut:

  • Program mengalokasikan 6 chunk (CHUNK_COUNT 6), masing-masing berukuran 128 byte (CHUNK_SIZE 0x80).
  • Isi flag.txt disalin ke dalam chunk terakhir (chunk index 5) dengan offset 8 byte (FLAG_OFFSET), sehingga flag "disembunyikan" di dalam salah satu chunk heap.
  • Program mencetak alamat chunk pertama (chunk 0) dan menyebutnya sebagai tcache head.
  • Seluruh chunk kemudian di-free() dalam urutan terbalik, mulai dari chunk 5 hingga chunk 0.

Poin penting terkait GLIBC tcache: - Binary ini dikompilasi pada lingkungan Ubuntu 18.04 dengan GLIBC 2.27 (berdasarkan string yang ditemukan pada binary maupun library yang disertakan). - Pada versi GLIBC ini, chunk berukuran 0x80 yang di-free akan masuk ke dalam tcache bin, yang beroperasi sebagai singly linked list dengan sifat LIFO (Last-In, First-Out). - Karena chunk dibebaskan dengan urutan terbalik (5, 4, 3, 2, 1, 0), sifat LIFO ini justru menghasilkan linked list yang berurutan maju: chunk 0 menjadi head dari tcache, lalu pointer next-nya mengarah ke chunk 1, yang mengarah ke chunk 2, dan seterusnya hingga chunk 5. - Untuk berhasil melewati verifikasi program (dan pada akhirnya mendapatkan flag), kita harus memasukkan alamat-alamat chunk tersebut secara berurutan sesuai urutan link list yang sebenarnya di memori.

Analisis

Struktur chunk pada heap GLIBC 64-bit:

[ prev_size  8 byte ]
[ size       8 byte ]   <- header chunk (total 16 byte / 0x10)
[ data     0x80 byte ]  <- data yang diminta user (CHUNK_SIZE)

Karena setiap alokasi berukuran 0x80 byte memerlukan header sebesar 0x10 byte, maka ukuran chunk aktual di memori menjadi 0x90 byte (144 byte).

Karena keenam chunk dialokasikan secara berurutan (sequential allocation) tanpa ada alokasi lain di antaranya, maka setiap chunk berikutnya akan berada tepat 0x90 byte setelah chunk sebelumnya:

Chunk Alamat (relatif terhadap base)
Chunk 0 (head) base + 0x00
Chunk 1 base + 0x90
Chunk 2 base + 0x120
Chunk 3 base + 0x1B0
Chunk 4 base + 0x240
Chunk 5 base + 0x2D0

Contoh konkret, jika program mencetak:

tcache head (start of free list) -> 0x555555759260

maka alamat-alamat chunk yang harus dimasukkan adalah:

  • Chunk 1: 0x555555759260 (base address)
  • Chunk 2: 0x5555557592f0 (base + 0x90)
  • Chunk 3: 0x555555759380 (base + 0x120)
  • Chunk 4: 0x555555759410 (base + 0x1B0)
  • Chunk 5: 0x5555557594a0 (base + 0x240)
  • Chunk 6: 0x555555759530 (base + 0x2D0)

Setelah keenam alamat ini dimasukkan dengan benar dan berurutan, program akan mengonfirmasi bahwa traversal berhasil, lalu mencetak flag yang sebelumnya sudah disalin ke dalam salah satu chunk.

Solusi

Exploit dengan pwntools

Menghitung alamat heksadesimal secara manual sebelum koneksi remote timeout cukup rawan salah, sehingga proses ini diotomatisasi dengan script berikut:

from pwn import *

# Terhubung ke instance challenge
conn = remote('candy-mountain.picoctf.net', 56725)

# Membaca alamat tcache head
conn.recvuntil(b"tcache head (start of free list) -> ")
base_address_str = conn.recvline().strip().decode()
base_address = int(base_address_str, 16)

log.info(f"Base Address: {hex(base_address)}")

# Mengirimkan alamat-alamat chunk yang telah dihitung
for i in range(6):
    # Menghitung alamat dengan menambahkan 0x90 untuk setiap chunk berurutan
    chunk_address = base_address + (i * 0x90)

    conn.recvuntil(f"Chunk {i+1} address: ".encode())
    conn.sendline(hex(chunk_address).encode())
    log.info(f"Sent Chunk {i+1}: {hex(chunk_address)}")

# Menerima dan mencetak flag
conn.recvuntil(b"Correct traversal! ")
flag = conn.recvline().decode().strip()
log.success(flag)

Jalankan dengan:

python3 solver.py

Logika script secara ringkas: 1. Terhubung ke server remote menggunakan pwntools. 2. Membaca baris output yang berisi alamat tcache head dan mengonversinya dari string heksadesimal menjadi integer. 3. Melakukan iterasi sebanyak 6 kali, menghitung alamat setiap chunk berikutnya dengan menambahkan kelipatan 0x90 dari base address. 4. Mengirimkan setiap alamat chunk sesuai urutan prompt yang diminta program. 5. Setelah keenam alamat berhasil dikirim dan diverifikasi, membaca dan menampilkan flag yang dikembalikan oleh program.

Verifikasi

Sebelum dijalankan ke server remote, penting untuk memastikan asumsi berikut benar-benar sesuai dengan binary target: - Ukuran chunk aktual di memori (CHUNK_SIZE + header) memang 0x90 byte, bukan ukuran lain — ini bisa dicek ulang lewat disassembly malloc call pada source/binary atau lewat debugging dengan gdb + plugin seperti pwndbg/gef untuk melihat langsung layout heap. - Urutan free() di dalam source code memang terbalik (chunk 5 → chunk 0), yang menjadi dasar asumsi bahwa tcache head adalah chunk 0 dan list berjalan maju secara berurutan. - Tidak ada alokasi heap lain di antara keenam chunk yang dapat mengganggu asumsi jarak 0x90 byte antar chunk (misalnya alokasi buffer sementara oleh printf/scanf di antara proses alokasi).

Setelah asumsi-asumsi ini diuji dan terbukti konsisten pada binary lokal, script yang sama dapat dijalankan langsung terhadap instance remote.

Flag

picoCTF{38703001eec3ec525642dfaf3281ba7a}

Kesimpulan (Key Takeaways)

  1. Tcache pada GLIBC beroperasi sebagai singly linked list LIFO — chunk yang terakhir di-free akan menjadi chunk pertama yang dikembalikan pada malloc berikutnya, dan menjadi head dari free list.
  2. Urutan pembebasan chunk menentukan struktur linked list — dengan membebaskan chunk secara terbalik, pola link list yang dihasilkan justru menjadi berurutan maju, sehingga bisa diprediksi.
  3. Header chunk menambah ukuran nyata di memori — permintaan alokasi sebesar 0x80 byte tetap membutuhkan 0x10 byte tambahan untuk header (prev_size + size), sehingga jarak antar chunk sequensial adalah 0x90 byte, bukan 0x80 byte.
  4. Mengetahui base address saja sudah cukup untuk memetakan seluruh struktur — karena alokasi bersifat sequential dan ukurannya tetap, seluruh alamat chunk lain bisa dihitung sebagai kelipatan offset dari base address.
  5. Pemahaman terhadap internal heap allocator sangat penting dalam heap exploitation — soal seperti ini mengasah kemampuan membaca struktur data internal GLIBC (chunk header, tcache bin, free list) yang menjadi dasar untuk teknik eksploitasi heap yang lebih kompleks seperti tcache poisoning atau double free.