SigReturn-Oriented Programming (SROP)
A special technique in ROP to set all registers only using a syscall
Last updated
A special technique in ROP to set all registers only using a syscall
Last updated
To understand the basics of SROP, read the following pages:
The above example has the requirement of a "/bin/sh"
string already being present in the binary. Still, with a bit more complexity you can eliminate this requirement and execute your own shellcode. This technique is taken from the following writeup:
SROP stands for SigReturn-Oriented Programming. This may sound like a completely different technique, but if you already know the basics of Return-Oriented Programming (ROP), it's simply a helpful gadget that exists in most binaries.
It utilizes the rt_sigreturn
syscall, normally reserved for returning from the signal handler. It allows the program to reset the state of all registers, taking a sigreturn frame of around 300 bytes from the stack containing all the values. The idea of SROP is to abuse this and write our own sigreturn frame on the stack since we have control over it, and then only call the rt_sigreturn
syscall to pop all the values into the registers. This way no more pop; ret
gadgets are needed for your registers, as you can set them all at once.
SROP bypasses ASLR (Address Space Layout Randomization) because it does not use any GOT, PLT, or libraries. But it does not bypass PIE (Position Independent Executable) because we require the gadgets in the executable code
Of course, PwnTools already has a class that can generate these frames for us, so we don't have to remember the whole layout. On it, we can simply set registers as attributes and unfilled registers will default to 0:
When the rt_sigreturn
syscall is called, these bytes on the top of the stack will represent the sigreturn frame. So in a simple ROP chain where you control the rax
register to choose the syscall you want to perform, simply set it to 15 (source) and jump to a syscall
instruction gadget.
This will cover a generic scenario, where there aren't many gadgets available for a ROP chain, so it can be applied in multiple situations of SROP. This will assume
sigreturn
The first thing required for SROP is of course being able to perform its syscall. This is done in two steps:
Set the rax
register to 15 (0xf
)
Jump to syscall
After that is set, you just need to jump to a syscall
. This gadget does not even need a ret
instruction afterward, because we keep controlling the RIP anyways due to setting all the registers with sigreturn. You can easily find such a gadget with ropper or ROPgadget:
PwnTools can also find these:
Then simply combine these into the first ROP chain:
When we trigger this right now, it will do the sigreturn
syscall, and try to get the sigreturn frame from the stack, but we haven't provided one yet. So it will just set all the registers to random values on the stack. The next step is providing these values ourselves to control all registers.
PwnTools makes it easy to create a sigreturn frame and control all registers. Now the question remains, what do we set them to? One idea is to do another syscall, but with arguments now that we have control over the rdi
, rsi
, etc. registers. In the lucky case that there is already a "/bin/sh" string contained in your binary at a known address, you can simply do execve("/bin/sh", 0, 0)
to start a shell. See the example SigreturnFrame()
above.
More often than not, this string is absent in the binary. This does not mean however that it is impossible to get a shell, as there are many other ways. We could write our own "/bin/sh" string into the memory somehow, or use shellcode instead. We'll go with shellcode here as it doesn't rely on any other gadgets.
Shellcode needs to be written somewhere and then executed in place. This means we need some section in the binary that is writable, and executable. This is not often the case in a binary when NX (Non-eXecutable Stack) is enabled. Because of this, we need to be a bit more clever. Luckily we can already control all the registers and perform arbitrary syscalls, so let's just create a writable and executable piece of memory ourselves using mprotect()
! This is the plan:
Use a sigreturn frame to set registers for a mprotect(start, len, prot)
syscall, creating a big region of rwx (read-write-execute) memory
Write shellcode to the rwx location
Jump to the written shellcode, and get a shell
There are a few questions left, but let's just start with step one. We want an easy-to-access location where we will make the memory rwx. The call convention for x64 is (rdi, rsi, rdx)
(source) and the syscall is determined by rax
. We can use the PwnTools for the syscall, and then choose the start of the binary as a simple address to modify. We'll take a big size, because why not, and set the permissions to 111 meaning everything (rwx).
This will set the registers, but after setting the registers we want to execute the mprotect()
syscall we created. Luckily we set all registers, including rip
, so we can just set it to where we want to jump: to the SYSCALL
gadget again:
Executing this payload, we can step through slowly in GDB to see what happens. The first trigger of the syscall will perform our sigreturn, and right after the registers are set up for another syscall of mprotect()
. When the second syscall happened, we can see the result by looking at the permissions of the memory with vmmap
:
Here we see the first few sections were modified to have rwx permissions like we wanted.
Now that we made the binary vulnerable, we need to write shellcode at this location, and later jump to it to get a shell. We don't just have a write-anywhere gadget laying around, so we need to be a bit clever again. But we can write something to some memory, using the vulnerable read()
function that received the input in the first place. That stores the input on the stack, which we might be able to use.
Another problem is the fact that our binary just crashes right after the second syscall
. This is because every register is set, even rsp
. We did not explicitly give it a value, so it defaulted to 0. This is obviously not a valid address, so the program does not know where the stack is. When it needs something from the stack, like at the ret
instruction right after, it segfaults. So we'll need to give this register a value too if we want to keep the program running.
The value of rsp
is a pointer to some address in memory, and the top-most value there will be used as the rip
after the ret
instruction. So we actually need to set it to a pointer to a pointer to the code we want to execute. This is a bit tricky because where do we find such a value?
It turns out, if we look hard enough we can find a pointer to the address of the program entrypoint:
This perfectly fits our needs. We set the rsp to one of these, and then the ret
instruction will pop one value from it, jumping to the entrypoint, and restarting the program. We will also again be able to perform the buffer overflow again to regain control over the Instruction Pointer. The only difference is that now the start of the program (0x400000-0x410000) have rwx permissions, and the stack pointer is at 0x400018 if we choose the first address (this address is close to unmapped memory, so consider choosing the second address here).
In this case, we can kill two birds with one stone. The stack pointer is now pointing to rwx memory, so any input we provide will be stored in that region! We also know the address of our input because we just set the rsp ourselves. Our plan from here on out will be:
Set the rsp
to 0x400018 to let the program return back to the start of the program
When we reach the buffer overflow a second time, include shellcode in the input to write it to the stack (rwx memory now)
Finally, overwrite the return address as well to return to our just written shellcode
We can simply add the address to our sigreturn frame:
This will restart the program from the entrypoint, and eventually end up at the buffer overflow again. Then we will put some shellcode in the input, writing it to the stack. In the end we need to jump to the start of our shellcode, but instead of calculating exactly where this will end up, we'll just write it and search for it later:
Running this payload in GDB, we can analyze exactly where the shellcode ends up. A simple way is to just grep
for the hex values again:
Then we step through the program in GDB, until it tries to ret
to the 0xdeadbeef
address. This is the time where we would need the location of the shellcode, so let's look for it (searching the hex start, with big endianness):
Perfect, it's stored at 0x4010f8, our rwx region. Now we just need to jump to it in our exploit by replacing the 0xdeadbeef:
This will jump to the shellcode, and execute from there. Then we will have an interactive shell:
There are many ways to set rax=15
, the easiest being a pop rax; ret
gadget where we simply provide the value 15 on the stack in our input. See for more complex ideas.