Dragon CTF 2020 - no-eeeeeeeeeeeemoji Writeup
Variation on the theme of 0ctf’s eeemoji.This challenge is running on Ubuntu 18.04.nc noemoji.hackable.software 1337
Unfortunately, we didn’t have the time to play this really awesome CTF but I got some time to have a look at the PWN challenges at the beginning of the CTF and this one really got my attention. I tried playing with it whenever I had time during the CTF but I got stuck and couldn’t solve it before the CTF ended. I joined the IRC, knew what the intended solution was and here’s me trying to understand and reproduce it:
Checking the given binary and its security flags
Running the binary we get the memory mappings of the process then we’re presented with a menu
Static Analysis
Throwing the binary to IDA and jumping right into the main
function
|
|
There are four function used inside the main
which are:
sub_17B9
sub_1259
sub_16A7
sub_14F7
Given that we already know the options from the runtime, we can see that the cow option c
is useless so we’re left with both beer and horse options.
sub_17B9
simply prints the menu we saw at the runtime
|
|
sub_1259
reads bytes with a given size
|
|
Before jumping into the beer and horse functions lets look at a function in the _init_array
|
|
It simply initialize the buffers, calls sub_1306
then calls alarm
with 300 seconds that will result in a call to sub_13F1
Looking at sub_1306
, it’s the function printing the memory mappings.
|
|
After 300 seconds sub_13F1
is called which simply calls exit
|
|
Diving into the functions we’ll focus on. Lets start with sub_14F7
(the beer function) first:
|
|
It generates a random base address greater than 0xFFFF
then maps 0x1000
bytes at that address with RWX permissions and stores the pointer to the allocated memory in the global variable qword_40F0
. It then sets the global variable dword_40F8
to 1. Finally, it prints qword_40F0
.
Finally, lets have a look at sub_16A7
(the horse function)
|
|
It first checks the dword_40F8
to make sure we used the beer function first. Then it does the following:
- Sets the 4096 bytes in
qword_40F0
to ‘A’ - Takes input of 4096 bytes into
qword_40F0
- Copies a shellcode from
loc_15B1
intoqword_40F0 + 1024
- Sets 256 bytes to ‘A’ starting from
qword_40F0 + 256
- Sets 254 bytes to ‘A’ starting from
qword_40F0 + 514
- Copies another shellcode from
loc_1668
intoqword_40F0 + 514
- Calls the shellcode at
qword_40F0 + 1024
Before jumping into these shellcodes let’s first simplify how qword_40F0
will look like after all these operations:
|
|
Lets dig into the shellcodes starting with the one at loc_15B1
as it’s executed first
|
|
As we can see it fills the stack with a 0x10000
‘A’ starting from rsp-0x8000
then it moves 0xdeadbeefdeadbeef
into all registers and finally jumps backwards (to the 2 bytes before the second shellcode).
The other shellcode which is at loc_1668
prints the first 38 bytes in qword_40F0
then terminates the program.
|
|
Dynamic Analysis
So far we know that we have 2 bytes that will be executed before the second shellcode but lets confirm that using the following script:
|
|
As a matter of fact our analysis was correct and we have 2 executable bytes that we control
The first thing that came to mind at this point was that we need to find a way to jump forwards or backwards to reach a bigger part of the allocated memory that we control. To approach this we thought of three ways:
- A short jump but unfortunately that only works for a maximum jump of 127 bytes forwards or backwards but we need at least a 256 bytes jump to reach our input.
- A near or far jumps padded with the
nops
that are already present. - Bruteforcing the two bytes with the rest of our input as
\xcc
and look forSIGTRAP
But non of the above worked so I got stuck at this point and couldn’t think of anything more. After the CTF, I joined the IRC and many was asking about the solution as the challenge had only 1 solve and what I understood from the ongoing conversation:
Use a
sysenter
. The kernel considerssysenter
is called only in a 32bit context, so returning from asysenter
system call withsysexit
, it returns back to to a 32bit address stored in a register which points to the middle of vdsovsyscall
(in vdso 32-bit version)
Right after reading I decided to read some more before I try it out to understand why it all happens. Using this reference, I understood the following:
- Usually when the
sysenter
is called from a userland program it is called via__kernel_vsyscall
which is available in the 32bit version of vdso. As a result the kernel downgrades the userland context into 32bit. - When
sysexit
tries to return to the userland it calculates the address to return to in the__kernel_vsyscall
which will be a 32bit address.
I’m still not sure if I understand the whole thing in a correct way so if I’m stating anything incorrectly please reach out to me so I can fix it. I adjusted my script to try out this new trick I learned:
It works like a charm but notice that rsp
also has changed as we’ll need that later on.
Exploitation
Now we have all the parts we need to start crafting our exploit. The steps of exploitation will be as follows:
- Bruteforce the vdso till the lower 32bit are achievable by our beer function.
- Bruteforce the beer function until we get an address equal to the lower 32bit of the vdso.
- Find the part that we return to in our input and replace it with a 32bit shellcode.
First, to bruteforce the vdso we have to remember the snippet responsible for generates the random address LODWORD(addr) = rand() % 1000 << 12;
. Given that, we need the lower 32bit of vdso to be smaller than 1000 << 12
which is 0x3e8000
. With that in mind we wrote the following script that does all three steps:
|
|
We actually hit a our input at the offset 0xb5a
which is at the last part of our input.
Now, lets add a 32bit shellcode -as we’re now downgraded to 32bit- instead of the breakpoints but before the shellcode we need to adjust the stack pointer to point to a valid writable memory location which I chose to be mmap + 512
|
|
Then we switch to the remote server instead of locally by changing
io = process('main', level='error')
to
io = remote('noemoji.hackable.software', 1337, level='error')
and WE GET THE FLAG!
I really loved this challenge and the new stuff I got to learn by attempting to solve it. It actually made me regret even more not having time to play the CTF but at least I got to learn a new thing ¯\_(ツ)_/¯
DrgnS{H0p3_y0u_d1dn7_jUsT_brUt3_y0ur_sOlu710n}