Blessing - HackTheBox Challenge Writeup

Solving an easy challenge on HackTheBox platform.

Overview

Today we will work on HackTheBox’s “Blessing” challenge. It’s a very easy challenge if we are only looking to exploit, but we can find a bit more unintended information about the actual code and the complete range of inputs possible to exploit if we are curious, post exploitation. This challenge focused on exploiting malloc() by abusing pointer arithmetic and how mmap() function works. So let’s begin!

Fuzzing

As usual, let’s simply run the application and see what we are given on the front-end.

In the ancient realm of Eldoria, a roaming bard grants you good luck and offers you a gift!

Please accept this:

[Bard]: Now, I want something in return...

How about a song?

Give me the song's length: 2

[Bard]: Excellent! Now tell me the song: A
A
[Bard]: Your song was not as good as expected...

So it expects a number as length and asks for a “song”, and entering a humongous length causes a segmentation fault”. Apart from that, we are given a big hex value at “Please accept this: “ which looked like a memory pointer. So let’s analyze it further with ghidra.

Analysis

Ghidra Disassembly

undefined8 main(void)

{
  long in_FS_OFFSET;
  size_t local_30;
  ulong local_28;
  long *local_20;
  void *local_18;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  setup();
  banner();
  local_30 = 0;
  local_20 = (long *)malloc(0x30000);
  *local_20 = 1;
  printstr("In the ancient realm of Eldoria, a roaming bard grants you good luck and offers you a gif t!\n\nPlease accept this: ");
  printf("%p",local_20);
  sleep(1);
  for (local_28 = 0; local_28 < 0xe; local_28 = local_28 + 1) {
    printf("\b \b");
    usleep(60000);
  }
  puts("\n");
  printf("%s[%sBard%s]: Now, I want something in return...\n\nHow about a song?\n\nGive me the song\ 's length: "
         ,&DAT_00102063,&DAT_00102643,&DAT_00102063);
  __isoc99_scanf(&DAT_001026b1,&local_30);
  local_18 = malloc(local_30);
  printf("\n%s[%sBard%s]: Excellent! Now tell me the song: ",&DAT_00102063,&DAT_00102643,
         &DAT_00102063);
  read(0,local_18,local_30);
  *(undefined8 *)((long)local_18 + (local_30 - 1)) = 0;
  write(1,local_18,local_30);
  if (*local_20 == 0) {
    read_flag();
  }
  else {
    printf("\n%s[%sBard%s]: Your song was not as good as expected...\n\n",&DAT_001026e9,
           &DAT_00102643,&DAT_001026e9);
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

This code confirms the fact that we were given a memory pointer stored in local_20. So the flow of code is as follows:

  1. Allocate around 196KB of memory and store its pointer in local_20.
  2. Set the initialized memory to 1.
  3. Ask the user for a long unsigned integer in local_30 and allocate a new space of memory with that size, with the new location pointer stored in local_18.
  4. Perform pointer arithmetic on the created memory and initialize the new calculated memory location with 0.
  5. Check if our initial memory location local_20 contains 0 or not. If it does, provide the flag, otherwise exit normally.

So we have to find a way to change the *local_20 value to 0

Exploit

Theory

Just before the if statement we have an interesting line of code:

*(undefined8 *)((long)local_18 + (local_30 - 1)) = 0;

This line is performing arithmetic on some values and dereferencing it to store 0 at the pointer result. Is there a way we can control the values to store this 0 at *local_20? Because remember, we are given the location of this malloc’d memory, and we can try to match the LHS to this address. So here’s what we know and controls regarding the variables:

size_t local_30;
void *local_18;
local_30 = 0;
__isoc99_scanf(&DAT_001026b1,&local_30);
local_18 = malloc(local_30);
*(undefined8 *)((long)local_18 + (local_30 - 1)) = 0;

size_t is a platform-dependant integer type that can store upto 8 bytes of unsigned integer (upto 18 quintillion). It is initialized to 0 and we enter a value into this variable local_30 when providing the “song length”. It uses the value to allocate that amount of storage to local_18. The key to exploit the arithmetic is to understand that if we supply a very big value as “song length” in local_30, malloc(local_30) will fail and return NULL and 0x0 will be stored in local_18. Thus the equation will reduce to:

*(undefined8 *)((local_30 - 1)) = 0;

And if we enter 1 + the address in local_20 as “song length” to local_30, which is provided to us briefly and which stores the value 1, the equation will finally become:

*(undefined8 *)(local_20)) = 0;

And this should inadvertently change *local_20 to 0, giving us the flag!

Testing exploit

We can test directly in GDB and put a breakpoint right after it provides the address. Then we can convert the address to decimal and add 1, and provide that as the song length.

Image

And with this we exploited the binary to provide us the flag!

Beyond the Flag

Beyond the exploit, there were a couple of things of interest for me.

MMAP()

What we took for granted was the fact that the malloc’d address we were given at the beginning was a very high memory address. And it was only due to this high address, that the second malloc() failed and returned NULL. This was due to the fact that when we request a small amount of space through malloc() (generally, anything less than 128KB), malloc utilizes brk() to provide memory space from heap, placed lower in memory. But when requesting more than that, it uses mmap() to request the memory space from an anonymous mapped memory space, which is always placed higher in memory.

So even though we did not require 0x3000 bytes of data to store the “song length”, if the code used a smaller size, heap would’ve been utilized and returned a lower address, making our further attack impossible.

Other Solution(s)?

Are there other solutions apart from local_20 + 1? Yes! So if we focus again on the pointer arithmetic, assuming a large “song length” for local_18 to be NULL again:

*(undefined8 *)(local_30 - 1) = 0;

Based on the size of the undefined8 data type, the range of valid inputs changes, since the write operation spans multiple bytes. For example, if the type were 2 bytes (like short), the write would only affect [x, x+1], but with an 8-byte type (like long), it could span [x-6, x+1] due to how pointer arithmetic works.

In this case upon checking manually:

\[local\_30 \in [local\_20 - 6, local\_20+1] \cap \mathbb{R}\]

And since the range of possible entries is 8 bytes, we can come to the conclusion that the undefined8 is of long integer data type for a 64-bit system! You can refer the below image to visualize what is happening.

Image

This just further helps us understand the actual code and the complete possibilities of inputs we could use to exploit the vulnerability.