IceCTF 2018 - Fermat string Writeup
Does size matter?
The challenge description gives us some hint about Format Strings attacks, and the ability to exploit their phenomenal powers…in a Itty-Bitty living space :) […_a margin of paper_]
Some Analysis
Which kind of beast are we facing? A run of checksec
and file
:
With PIE disabled, and the binary statically linked, we basically have
every address we desire carved in a stone. Running multiple times the binary in target machine, with gdb set disable-randomization off
, also confirms that stack addresses are stable during different runs.
This time, we do have the source code to analyze. Let’s take a look.
1 |
|
We can provide two kinds of inputs:
input
, which will be delivered as argv[1] via command line.USER
, via environment variable.
About input
1 | int i, cnt = -1; |
It is clear that the protection mechanism will be triggered with at least two uses of ‘%’ character. So we have only one format specifier to shoot.
1 | /* Make sure input isn't too long */ |
Here it is, our tiny margin of space. Length of the string cannot seemingly exceed 7 characters.
1 | memset (buf, 0, sizeof(buf)); |
At last, a format string vulnerability!
About USER
1 | char buf[1000 + (user != NULL ? strlen(user) : 0)]; |
The buffer grows together with the length of our env input. This means we can stretch it quite a lot :)
Planning The Attack
NX enabled, PIE disabled, binary is statically linked. We have a way to write over memory (format strings) and a way to fill the stack with arbitrary input (USER
env variable copied in buf). We have static address for pretty much everything.
I have a clear goal in mind:
- Place in
USER
a payload composed of:- A ROPchain calling
mprotect()
to enable RWX permissions on a stack page; - A shellcode to
setreuid(1337, 1337)
&execve("/bin/sh", NULL, NULL)
- A ROPchain calling
- Exploit the format string vulnerability to jump to my previously placed payload.
NOTE
The reason I need setreuid(1337, 1337)
lays in how Linux and bash handle permissions. fermat
binary is owned by target
user, with setuid bit enabled. This means that every user running the binary will run it with owner permissions, which we need to read the flag.
However, in our target machine, /bin/sh
links to /bin/bash
, and for security reasons the latter drops suid privileges when executed.
The solution to this is to call setreuid()
, which sets our real_id to be equal to our current effective_id. Linux manual is there for more informations on the argument :)
So, back to the business…I need a reliable way to start my ROPchain.
In technical words, this means that ESP
register must point to the beginning of the fake stack I have injected via USER
variable,
and EIP
register must point to a ret
instruction.
To keep things short, I’ll explain here how I managed to obtain the above, together with the exploit. For your information, getting to that solution required a lot of trial and error, and searching thru the stack and text section with GDB.
The Exploit
Let’s see how to use our format string vulnerability.
This is a snapshot of memory layout when hitting the vulnerable printf()
:
Looking at output from bt
and stack
we can examine the how stack frames where formed.
We are in function payload()
, and there on the stack we have:
- Saved EBP and Saved EIP (pointing to
dispatch + 40
) to restoredispatch
; - Saved EBP and Saved EIP (pointing to
main + 42
) to restoremain
;
We will use a stack pivoting technique to make ESP
point to our fake stack.
The idea is to overwrite Saved EBP in dispatch
frame, at address 0xffffd5a8
, to point to our payload injected in welcome()
.
How? We have 7 characters.
The following input: %14$hn
writes 0 in the two bytes starting at the address pointed by the 14th word after the format string.
In practice:
- Looking back at the image, the 14th word after the format string is at address
0xffffd588
; - This address points to
0xffffd5a8
; - So, our format string will write 0x0 into
0xffffd5a8
and0xffffd5a9
- Previously,
0xffffd5a8
contained value0xffffd5c8
; - After
printf()
, it will contain value0xffff0000
.
Now, remember the welcome()
function? Thanks to that function, we can write a huge payload into memory, and buffer will grow (in stack, so towards lower addresses), so that we can make the area now written in Saved BP (0xffff0000
) pointing to our payload.
Now I needed to find a way to write that value into ESP
and trigger the ROPChain.
The trick to trigger stack pivoting is using the two leave
instruction in dispatch
and main
:
- The first
leave
movesESP
to currentEBP
, then copies our overwritten SBP intoEBP
; - The second
leave
movesESP
to currentEBP
- which we injected in the previous frame - then pops the SBP into EBP, but this doesn’t matter since ESP points to our payload now :)
I slightly modified this approach because of these instructions in main
:
Following leave
,
the value in memory pointed by ecx - 4
is moved into esp
. Luckily, we can control the content of ecx
, since it is loaded in:mov ecx, DWORD PTR [ebp - 0x4]
, and ebp
is already pointing to our fake stack.
The rest of the work has just been writing a ROPchain which didn’t contain NULL bytes (that couldn’t reside in env
) – but since the binary is statically linked, I have a whole load of ROP gadgets already linked to binary, found with ropper
– and carefully computing addresses inside and outside gdb. Here, the script to generate the payload, which I used in target machine with:
USER=$(python exploit.py) ./fermat "%14\$hn"
1 | import struct |
IceCTF{s1ze_matt3rs_n0t}