Sieberrsec 4.0
This is a challenge from Sieberrsec 4.0. I thought it was a pretty good challenge that taught me how to properly use gdb for pwn.
Description:
I built a simple program to simulate Among Us.
- This challenge is quite hard (relative to the others)
- That’s why we’re giving prizes to the first person to solve this
- Server runs libc 2.31 (Dockerfile provided)
- This is not a heap challenge
For this challenge, source and Dockerfile was given. Since I had the Dockerfile,
the first thing I did was to locate and copy the libc to my host. I wanted to
patch the binary using pwninit
so that my environment was as similar to remote
as possible, but the patcher strangely couldn’t detect the libc version, which
means our linker couldn’t be automatically resolved and downloaded.
Ultimately, libc wasn’t really that important for this challenge so it’s all good.
Analysis
On first reading through the source, I immediately spotted the format string vuln that is only triggered if a crewmate is killed. It passes my name (by default “Impostor”) into printf directly. However, there’s no built-in way to control this value.
The program’s behaviour is as such:
- Read up to 19 characters (using
fgets
) intocrewmateName
, a buffer with 20 bytes allocated. This part is safe since we can’t overflowcrewmateName
. - Concatenate the contents of
crewmateName
into a global variableinput
. This global variable is of size 50 and is initialized to “Dummy input”. Note that instrncat
, the trailing null byte ofinput
is replaced by the first character ofcrewmateName
. - Iterate through the global array
crewmates
and check if any of the items are found ininput
.- If true, the vulnerable
printf
is called and the first character ofinput
is set to a null byte (effectively clearing it out)
- If true, the vulnerable
From this I identified another vuln - strncat
doesn’t check the length of
input
! This allows me to overflow out of input
, into name
. Hence, this is
how I’ll control name
to exploit the FSB.
Exploit
It all seems very simple, until I realized - it’s full RELRO…
For most FSB challs, I’ve usually seen either 1 of these:
- Partial RELRO - overwrite GOT
- Full RELRO with buffer overflow - leak canary, PIE and libc base, then ret2win
So I thought it probably was no. 2.
But this one is kinda different for 3 reasons:
- There’s no stack-based buffer overflow! So there’s no way I could control RIP with an overflow.
- My input is partially out of stack (in .bss region). This makes format string
writes a little harder since I can’t use
fmtstr_payload
to generate my payload. - My input (in some sense) has a limit of 17 characters, which really restricts
the length of my payload, again forcing me not to use
fmtstr_payload
.
Since I probably needed to leak some values first, I set a breakpoint at the
vulnerable printf
to examine the stack at the point of FSB:
gef➤ telescope 0x007fffffffe450 |
- 0x007fffffffe488 - This is the address I want to write to. When disassembling
main
, I can see that this is the next instruction after the call tovuln
. So if I were to overwrite this, exitingvuln
would allow me to control the program flow. Also, this address will allow me to leak PIE. - 0x007fffffffe480 - This address points to 0x007fffffffe4a0, which is always a fixed offset from the address I want to control (0x007fffffffe4a0 - 0x007fffffffe488 = 0x18).
So, the plan is to leak these 2 addresses via FSB, and then on a second write, control the value at the 1st address which would give me my RCE. :)
(ps. actually, leaking the 1st address isn’t necessary - the author made it such
that the 2nd last byte of main
and vent
are always the same regardless of
PIE, and PIE doesn’t affect the last 3 nibbles. So PIE is irrelevant in this
case.)
While experimenting with the binary, I also figured out how to fill up the
input
buffer so that I would land exactly at the start of name
. However, for
subsequent overflows after the first time we call printf
, we need 11 more
characters since “Dummy input” is no longer there.
# this works only for the first overflow - subsequently, we need to add these 2 lines: |
Back to the FSB - how can I determine the offsets to leak values from? Well, looking at the stack, “pink” should be at offset 8, and $rbp (the value we want to leak) at offset 12. I confirmed this by testing out offset 12, and true enough the expected value is there.
The below script will leak the value and calculate the address of our target:
setup_buffer(p) |
To execute the write, we can use %n
, which writes the number of characters
printed so far into the address at the specified offset. For example, if my
payload was as such:
aaaa%8$n
and the value at offset 8 is 0xdeadbeef, then 0x4 would be written to 0xdeadbeef. Note that 0xdeadbeef is the start of the write - %n will write to the following 8 bytes as well, which sets everything to 0x00 except 0xdeadbeef, which is set to 0x04.
But like I said earlier, I don’t want to control the entire address - just the
last byte! Hence, I can use %hhn
which only writes 1 byte instead of 8 bytes.
As I identified earlier, the offset of my input (“pink” in the earlier case) is 8. Controlling this value will allow me to control the address I’m writing to, so I’ll need to set it to the leaked address of $rip.
I also need to trigger the write together with my write address. But since there are null bytes at the start of $rip, I’ll need to shift the address to the end, changing my offset to 9 instead.
setup_buffer(p) |
(if you’re confused about why I write to the start of $rip when I actually want to control the last byte, do read up on little endian)
As a PoC, here’s the stack before my write:
gef➤ telescope 0x007fffb9f9a340 |
and after my write:
gef➤ telescope 0x007fffb9f9a340 |
Afterwards, I can just exit vuln
to jump to vent
and cat the flag. :)
[*] '/home/samuzora/ctf/writeups/sieberrsec-4.0/sus-machine/dist/chal' |