Codegate CTF 2019 Quals - cg_casino
Table of Contents
0x00. Introduction
)
Concept
)
)
)
)
)
)
>
The challenge implements three casino games along with put voucher and merge voucher functionalities.
0x01. Vulnerability
Stack Overflow
void __fastcall __noreturn
When putting or merging a voucher, the voucher name is received through read_401108():
unsigned __int64 __fastcall
The function reads input byte by byte until encountering \n, which causes an overflow all the way to the end of the stack.
However, since main() calls exit() directly without returning, gaining RIP control appears difficult.
Stack Leak
I stumbled upon this during dynamic analysis - there’s a leak of uninitialized data in lotto_4011A7():
unsigned __int64
The game generates 6 random numbers between 0-44, stores them, and asks you to guess them by filling the guess array.
Here’s the issue: if you input something that doesn’t match the %u format (like a), scanf fails and prints the existing value in guess:
===================
| | | | | | |
===================
File Copy
This is actually a feature rather than a vulnerability, but merge voucher calls this function:
unsigned __int64 __fastcall
By specifying a filename in stack’s 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
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 merge voucher to move it to /home/cg_casino/voucher/, then proceed to the next step…
That’s when I found this in the /proc/self/environ file:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binHOSTNAME=3197b44a52
1aERASER2=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
The 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.
When I actually overwrote the environment variables til the end of the stack, I confirmed that /proc/self/environ changed:
So we can manipulate stack values to leave data into a file at /proc/self/environ.
Stack Overflow & File Copy
There are three conditions to write libc data to /proc/self/environ and bring it to /home/cg_casino/voucher:
newin main must be the filename to save in/home/cg_casino/voucheroldin main must be 32 bytes long and point to/proc/self/environ- libc data must be written to the environment variable space at the end of the stack
So I structured the payload as follows:
= b
+= b *
+=
+= b *
During this process, since input is received through 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:
=
=
This needs to be added to the payload.
Small Libc
Another issue here is that the environment variable area has limits, restricting the length of data that can be brought via merge voucher:
I needed a libc smaller than 3432 bytes, which I created by compiling this source code on Ubuntu 16.04:
// gcc -w -znorelro -s -fPIC -shared -nostdlib -o mylib.so mylib.c
void
Interestingly, Ubuntu 22.04 produces a much larger file size with the same compilation options - compiler version can make such a significant difference:
|
Stack Leak
As confirmed earlier, inputting non-%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:
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 $rsp+0x30. Calling up_down_40139E() followed by lotto_4011A7() works:
Since guess is integer, inputting ‘a’ for the 3rd and 4th inputs leaks 4 bytes each:
=
=
=
= << 32 | + 0x40
Envp Overwrite
Now that the libc file executing the shell is in /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.
Since I defined on_unload() in the libc, I needed to verify two things:
- Does
exit(0);triggeron_unload()? - Does modifying environment variables after execution apply
LD_PRELOAD?
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 slot_401477() uses system("/usr/bin/clear");, and during dynamic analysis this message appeared:
||
=========================
| |
| | | | | | | |
| || || || |
=========================
||
The current process cg_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],rdx
Following the memory:
The structure is **envp(0x7fffffffdf28) -> *envp(0x7fffffffe0a8) -> first env(0x7fffffffe276)`.
So we need to follow this structure, making sure to include null at the end of *envp:
= b *
+=
+= b *
+=
+= b *
+= b
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 0xXX0, suggesting success rate of 1/256.
0x03. Payload
= False
=
=
=
= f
=
return
return
return
return
=
=
=
=
=
=
=
=
=
= << 32 | + 0x40
= + 0x1796
= + 0x326
= + 0x158
= + 0xe8
= b
+= b *
+=
+= b *
= b *
+=
+= b *
+=
+= b *
+= b