Pwn - Sigreturn Oriented Programming (SROP) Technique
Background
In recently ctf (tamuctf 2022), I solve a challenge called void (writeup).
This challenge only contains a few line of assembly code, with no libc and NX enabled.
The only thing we can utilize is a buffer overflow and some syscall gadget.
It seems impossible to do. However, there is a technique call SROP - Sigreturn Oriented Programming that can help us to pwn this binary.
Theory
The original paper is here paper, slides
Check it out if you want to.
I'll brief explain how SROP works.
Before
Introduce to syscall - rt_sigreturn
Here is a picture showing how linux kernel handle signaling.
rt_sigreturn is a syscall the will be called when program come back from signal handler.
Since signal handler may change registers, before the program going to signal handler, the program will save current (which is called Signal Frame) state including all the register on the stack.
Then, after the program come back from signal handler, progrma will use syscall rt_sigreturn to recover register and continue running.
That said, if we can fake Signal Frame on the stack, then call rt_sigreturn. we can set register to what ever value we want.
And here is what signal frame looks like in linux x86-64. (detail here)
How to trigger sigreturn in the first place.
In order to trigger sigreturn and do a srop, the binary should satisfy following criteria
- Knowing the address of
syscall; ret
- A big enough buffer overflow or something that allow us to write signal frame on the stack
- some how control the value rax
first and second criteria are easy to spot or identify. For setting the value in rax, there are multiple way to do that.
- using
pop rax; ret
- using function a function return value (rax is used for function return)
- using syscall read (syscall read will return how many bytes read)
Getting a shell using SROP
To get a shell, we need to execute execve('/bin/sh',0,0)
by calling syscall
So, despite all the requirements describe above. We also need an address for /bin/sh
.
If we have a buffer overflow or something, we either write /bin/sh
into stack, or we can contruct a rop chain that write /bin/sh
some where in the memory.
Pretty Straightforward
Example - Void
Take the example from tamuctf 2022, my writeup here.
Analyze
Examining the code, the program only contains following codes. Basicly, the program call main
and read 2000 bytes to the stack, then exit.
1 | ┌ 27: int main (int argc, char **argv, char **envp); |
And there is no writable memory page except the stack.
1 | 0x0000000000400000 - 0x0000000000401000 - usr 4K s r-- void void ; segment.ehdr |
So, how to use SROP to get a shell? Lets think reversely.
In the end, we want to get a shell, so we must use syscall(59,'/bin/sh',0,0)
. But '/bin/sh' is not in the memory. So, we need to write '/bin/sh' into the memory.
We can simply write '/bin/sh' on the stack, however, there is no way we are able to know the stack address. (In this case, rdi never gonna be 1, so we can't use write(1,addr,0x7df)
to get stack address)
Here is another method, first, we do a mprotect
to make a memory page writable using sigreturn. After sigreturn, rsp (stack pointer) will set to the address now is writable.
Then we return to main
function and write '/bin/sh' and signal frame for calling syscall(59,'/bin/sh',0,0)
there. In that case, we know the address of '/bin/sh'.
Now, how to trigger sigreturn? thats pretty straightforward, Since we have a write syscall in main, we just write 15 bytes to the stack and return to syscall gadget. That will set rax to 15 and trigger sigreturn.
There one more thing. Since we are using the gadget syscall; ret
. After we execute mprotect
, the program will ret to a address in the rsp.
1 | Before sigreturn |
Its okay if we end after one sigreturn. But in this case, we need to do two sigreturn, so it is important to return back to main
. Therefore, we need find some where in the memory that contains an pointer to main
, so that when ret
is called, it will go back to main
and we can do another SROP there.
Luckily, in 0x004020b8
, there is pointer that point to main
. So we can happily make whole 0x00402000-0x00403000
page writable and use this address as our new rsp.
Summary
- write signal frame to the stack with rax=10 (mprotect), rdi = 0x00402000, rsi = 0x1000, rdx = 7 (rwx), rsp = 0x004020b8, rip = syscall addr. Then return to main
- write 15 bytes to trigger sigreturn
- syscall
mprotect(0x00402000,0x1000,7)
- return to main, write signal frame for calling execve. Then return to main
- write 15 bytes to trigger sigreturn
- syscall
execve('/bin/sh',0,0)
to get a shell
Exploit
1 | from pwn import * |