Rokos Replicant Bombs Landed — Reversing Challenge — HackTheBox Writeup | 0x00nier

0x00nier

writeups/

xp/

whoami/

Bombs Landed — Reversing Challenge — HackTheBox Writeup

Greetings everyone. In this write-up, I’ll be tackling the Bombs Landed challenge — my first time dealing with a medium-level (50 points) reversing challenge. I’m excited to share what I’ve learned and fill in the gaps since there are no detailed write-ups available. However, I won’t be giving away the flag directly. Instead, I’ll do my best to provide a comprehensive understanding of the challenge while still keeping things engaging.

Here’s the challenge prompt -

We’re on the hunt for the hidden password in the binary. Let’s begin.

Initial Analysis

Let’s try to run this program.

/mnt/H/Misc > ./BombsLanded                                                                    
Bad luck dude.
/mnt/H/Misc > ./BombsLanded 1                                                                  
Bad luck dude.
/mnt/H/Misc > ./BombsLanded 1 2                                                                
Bad luck dude.
/mnt/H/Misc > ./BombsLanded 1 2 3                                                              
input password: itworked 
Bad luck dude.

Running the binary as is only results in the message Bad luck dude. being displayed, with no opportunity for input. However, after some experimentation, we discover that the binary requires at least three arguments. This will become clearer in the decompilation process. When we run the binary with the correct number of arguments, it prompts for a password but, as expected, our initial attempt is met with Bad luck dude.

/mnt/H/Misc > file BombsLanded                                                                 
BombsLanded: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.6.32, BuildID[sha1]=53d6c985990fd9b8ed4f1caf10ce7d64e14b2121, dynamically linked, interpreter /lib/ld-linux.so.2, no section header

The file command reveals that this binary is a 32-bit, dynamically linked executable with no section headers. This makes things tricky, as the lack of clear boundaries between the .data, .text, and .rodata sections complicate understanding the binary’s structure and functions. And, with symbols and code sections resolved during runtime, reverse engineering the binary is going to require a little extra effort.

/mnt/H/Misc > readelf -s BombsLanded                                                           

Dynamic symbol information is not available for displaying symbols.
/mnt/H/Misc > readelf --dynamic BombsLanded                                                    

Dynamic section at offset 0x1010 contains 29 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libdl.so.2]
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x0000000c (INIT)                       0x8048670
 0x0000000d (FINI)                       0x8048c54
 0x00000019 (INIT_ARRAY)                 0x8049000
 0x0000001b (INIT_ARRAYSZ)               8 (bytes)
 0x0000001a (FINI_ARRAY)                 0x8049008
 0x0000001c (FINI_ARRAYSZ)               4 (bytes)
 0x6ffffef5 (GNU_HASH)                   0x804818c
 0x00000005 (STRTAB)                     0x8048318
 0x00000006 (SYMTAB)                     0x80481b8
 0x0000000a (STRSZ)                      283 (bytes)
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000015 (DEBUG)                      0x0
 0x00000003 (PLTGOT)                     0x8049130
 0x00000002 (PLTRELSZ)                   104 (bytes)
 0x00000014 (PLTREL)                     REL
 0x00000017 (JMPREL)                     0x8048608
 0x00000011 (REL)                        0x80484c0
 0x00000012 (RELSZ)                      328 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
 0x00000016 (TEXTREL)                    0x0
 0x0000001e (FLAGS)                      TEXTREL
 0x6ffffffb (FLAGS_1)                    Flags: PIE
 0x6ffffffe (VERNEED)                    0x8048460
 0x6fffffff (VERNEEDNUM)                 2
 0x6ffffff0 (VERSYM)                     0x8048434
 0x6ffffffa (RELCOUNT)                   18
 0x00000000 (NULL)                       0x0

As we can see, the only information we can retrieve is about the dynamic section of the binary. The dynamic section shows that the binary has an initialization and finalization function, as the INIT and FINI entries have absolute addresses at the very least.

/mnt/H/Misc > checksec BombsLanded                                                             
[!] Did not find any GOT entries
[*] '/mnt/HackingUnixBinariesStuff/Misc/BombsLanded'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

Another helpful aspect is that the binary doesn’t have Position Independent Executable (PIE) enabled. This is evident from the use of absolute addresses in the dynamic section of the binary. This makes our reverse engineering journey a bit smoother, as we don’t have to worry about dealing with address relocations when setting breakpoints.

/mnt/H/Misc > strings BombsLanded                                                       1m 49s 
/lib/ld-linux.so.2
libdl.so.2
_ITM_deregisterTMCloneTable
__gmon_start__
_Jv_RegisterClasses
_ITM_registerTMCloneTable
dlsym
libc.so.6
_IO_stdin_used
exit
strncmp
__isoc99_scanf
puts
printf
mmap
strlen
memset
getchar
malloc
ptrace
__cxa_finalize
__libc_start_main
free
GLIBC_2.0
GLIBC_2.7
GLIBC_2.1.3
fPf1
fPfP
WVSQ
Y[^_]
UWVS
t$,U
[^_]
input password: 
Correct password:
Bad luck dude.
strncmp
;*2$"
cccc
g r c
cccc
g j c

When we run the strings command on the binary, we see some interesting strings such as mmap, malloc, strncmp, and dlsym. These strings indicate that the symbols and other sections requiring resolving are resolved dynamically, as the presence of dlsym explicitly confirms.

We also see the strings input password:, Correct password:, and Bad luck dude., which are important for our understanding of the binary. Note that the colon against Correct password is peculiar, and we will explore this soon. Additionally, the presence of scanf when getchar is already present seems odd for a simple program that just uses stdin once.

On the other hand, there is a problem that a lot of solvers apparently encounter.

According to elfparser, the binary has ptrace detection enabled.

Here, we can see that the function main is both defined and a global function. In my solution, I did not face any issues with this, but for those who might, here’s a way to bypass it:

Here, we can see that the function ptrace is defined as an external import in Ghidra (you can use tools like IDA Pro, radare2 and Binary Ninja if you want).

On the function itself, right-click and select Show references to ptrace in the References tab.

You will see that, there is an unconditional call to ptrace in this case. Double-click and take a look at the disassembly.

Here, originally this instruction was JNZ, and now we attempt to reassemble it as a JZ instruction instead. If you are debugging, the value of EAX after the ptrace call will most certainly be 0xffffffff or -1. Thus, the CMP instruction will be passed as a check and you will pass the overall ptrace check.

There are many other ways to bypass this ptrace check. You could try removing the call to ptrace altogether by replacing it with NOPs either in the ptrace function itself or the call function. You can even set the value of EAX accordingly to pass the check while debugging. But it’s easier to just patch the binary instead of doing that every debug session.

Decompilation And Disassembly

We open up the binary in Ghidra to take a look at the disassembly and its decompilation side by side.

undefined4 main(int param_1)

{
  int iVar1;
  undefined4 *__s;
  uint uVar2;
  undefined4 *puVar3;
  undefined4 *puVar4;
  byte bVar5;
  uint local_24;
  
  bVar5 = 0;
  _DAT_10091348 = &__DT_PLTGOT;
  _DAT_1009134c = _DAT_1009134c + 1;
  _DAT_10091338 = _DAT_10091338 + -1;
  _DAT_1009133c = _DAT_1009133c + 1;
  _DAT_10091340 = _DAT_10091340 + -1;
  if ((_DAT_1009133c < _DAT_1009134c) && (param_1 < 5)) {
    __s = (undefined4 *)mmap((void *)0x0,0x1000,7,0x22,-1,0);
    memset(__s,0xc3,0x1000);
    *__s = _DAT_100911a0;
    *(undefined4 *)((int)__s + 0x193) = _DAT_10091333;
    iVar1 = (int)__s - (int)(undefined4 *)((uint)(__s + 1) & 0xfffffffc);
    puVar3 = (undefined4 *)(&DAT_100911a0 + -iVar1);
    puVar4 = (undefined4 *)((uint)(__s + 1) & 0xfffffffc);
    for (uVar2 = iVar1 + 0x197U >> 2; uVar2 != 0; uVar2 = uVar2 - 1) {
      *puVar4 = *puVar3;
      puVar3 = puVar3 + (uint)bVar5 * -2 + 1;
      puVar4 = puVar4 + (uint)bVar5 * -2 + 1;
    }
    for (local_24 = 0; local_24 < 0x197; local_24 = local_24 + 1) {
      *(byte *)((int)__s + local_24) = *(byte *)((int)__s + local_24) ^ 99;
    }
    (*(code *)__s)();
    memset(__s,0,0x197);
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  if (3 < param_1) {
    printf((char *)0x10090c70); //input password:
    iVar1 = getchar();
    if ((char)iVar1 == 'X') {
      (*(code *)0xc3)();
      __isoc99_scanf(0x10090c81); //Correct password:
    }
  }
  puts((char *)0x10090c93); //Bad luck dude.
  return 0;
}

This is the main function as decompiled by Ghidra. Here is where the weirdness happens. As you can see, it’s not an ordinary password checker. It has something else going on altogether before it. Even the password checking part is odd. Furthermore, none of the variables above are used in the password checker and is completely separate.

First, it prints out input password: and takes input via getchar. It checks whether the first character of the input we give is X or not. If it is, a function with the address c3 is called and Correct password: is passed as an argument to scanf. This makes no sense and there is no way to retrieve the password here. I spent at least 4–5 hours tinkering with this section of main and got nowhere. It is the most interesting rabbit hole I have seen in my short reverse-engineering journey.

What to do then?

Well, we can try to focus on the code above then since there is no other function with code that may be relevant. To do that, we must first pass the if condition if ((_DAT_1009133c < _DAT_1009134c) && (param_1 < 5)). In my experience, it has always been safer to just switch up the conditional jumps to their opposite counterparts. Which is exactly what we will do. Alternatively, we can try to change the values of the registers in gdb-pwndbg or patch the binary to always fulfill the condition. Or we can remove the condition altogether.

080489b8 39 c1           CMP        ECX,EAX
080489ba 77 05           JA         LAB_080489c1
080489bc 83 3a 04        CMP        dword ptr [EDX]=>param_1,0x4
080489bf 7f 57           JG         LAB_08048a18

This part of the assembly code concerns with the check that needs to be passed. To do this, we will switch from JA to JBE and from JG to JLE. This should work because the values in the registers before the CMP instruction always seem to be the same, so patching the code would allow it to pass the checks.

This is the patched code. Now, let’s jump to the CMP directly by breaking at 0x80489b8.

We break at the CMP instruction successfully.

Stepping through the code, we successfully bypass the check as seen above. After bypassing the check, we step through the code until we see this loop -

   0x8048a9a    movzx  edx, byte ptr [edx]
   0x8048a9d    xor    edx, 0x63
   0x8048aa0    mov    byte ptr [eax], dl
   0x8048aa2    add    dword ptr [ebp - 0x1c], 1
   0x8048aa6    mov    eax, dword ptr [ebp - 0x1c]
  0x8048aa9    cmp    eax, 0x196
   0x8048aae    jbe    0x8048a8a                     <0x8048a8a>

At the jbe instruction, we will loop a couple of times. So, to end the loop, we will use x/3i $eip to set a breakpoint on the address after `jbe.

As you can see that worked perfectly.

And then, three instructions after JBE, we’ll see a CALL EAX. This is where the flag is hidden. To step into this, we’ll use the si (step into) command.

As you can see, there are multiple MOV instructions done with a couple of hex characters. There is a high likelihood of this having something to do with our flag.

   0xf7fc000a    mov    dword ptr [ebp - 0x14], 0x6c333374
   0xf7fc0011    mov    dword ptr [ebp - 0x5c], 0x647f6573
   0xf7fc0018    mov    dword ptr [ebp - 0x58], 0x786f7c6f
   0xf7fc001f    mov    dword ptr [ebp - 0x54], 0x6463656d
   0xf7fc0026    mov    dword ptr [ebp - 0x50], 0x6c657e6d
   0xf7fc002d    mov    dword ptr [ebp - 0x4c], 0x676e6463
   0xf7fc003a    mov    dword ptr [ebp - 0x6d], 0x61647a7d
   0xf7fc0041    mov    dword ptr [ebp - 0x69], 0x75643460
   0xf7fc0048    mov    dword ptr [ebp - 0x65], 0x7b636767
   0xf7fc004f    mov    dword ptr [ebp - 0x61], 0x342e7066

These are all the instructions featuring a series of hex strings.

>>> import pwn
>>> l=[0x6c333374,0x647f6573,0x786f7c6f,0x6463656d,0x6c657e6d,0x676e6463,0x61647a7d,0x75643460,0x7b636767,0x342e7066]
>>> print(''.join([pwn.p32(i).decode() for i in l]))
t33lsedo|oxmecdm~elcdng}zda`4duggc{fp.4
>>>

Using pwntools, we try to decrypt these hex characters into strings and we get a row of nonsense.

As it turns out, this string (in little-endian) is looped through and eventually produces the string input password:

We can see that happening above here.

00:0000 esp 0xffffceb0 —▸ 0xffffcedb ◂— 'input passw{fp.4'
01:0004     0xffffceb4 ◂— 0x3055e4
02:0008     0xffffceb8 —▸ 0xf7fc1958 —▸ 0xf7ffdbac —▸ 0xf7fc1a94 —▸ 0xf7ffda40 ◂— ...
03:000c     0xffffcebc —▸ 0xffffcf30 —▸ 0xf7d77ad6 (__memset_sse2_rep+294) ◂— add ebx, 0xa551e
04:0010     0xffffcec0 —▸ 0xffffcf34 ◂— 0x6c333374 ('t33l')
05:0014     0xffffcec4 —▸ 0xf7fd2fce (_dl_lookup_symbol_x+302) ◂— add esp, 0x30
06:0018     0xffffcec8 —▸ 0xf7c09a54 ◂— 0x61ab
07:001c     0xffffcecc —▸ 0xffffcf34 ◂— 0x6c333374 ('t33l')

In the stack above, we also see remnants of the string we converted using python. Thus, it’s possible that when the instruction strncmp is run against our input, a similar decryption loop occurs and thus we will be able to see the flag.

When you reach this part of the execution, if EAX is not equal to 0x0, the program eventually exits. However, when debugging, we can set EAX to 0x0 by using the command set $eax=0x0. Once this is done, you will be greeted with the result.

We are on the right track, but we are skipping a certain part of the code.

This is the only instruction that looked suspicious enough before the checking (and printing you win if you get it right). On stepping in a few instructions inside the CALL EAX, we see the following -

Now, we know we are on the right track since we see that strncmp is now resolved using dlsym. Normally, in gdb-pwndbg you can see the arguments fed into strncmp but here we can’t since this is just a dynamically linked symbol within the CALL EAX. This tells me that the CALL EAX done is actually running strncmp. Thus, let’s step over the call dlsym and see what happens.

After stepping in with ni a couple of times, we start to see interesting things. Take a look at $esp+4. We see a y. Let’s step in some more.

We are starting to see something interesting. After stepping enough times we find the password.

00:0000 esp 0xffffce80 —▸ 0xffffcf02 ◂— 'asdasdadsasda'
01:0004     0xffffce84 —▸ 0x804a9c0 ◂— 'younexxxxxxxx'
02:0008     0xffffce88 ◂— 0x15
03:000c     0xffffce8c —▸ 0xf7c53b25 (__isoc99_scanf+37) ◂— add esp, 0x1c
04:0010     0xffffce90 —▸ 0xf7e1d620 (_IO_2_1_stdin_) ◂— 0xfbad2288
05:0014     0xffffce94 —▸ 0x804a9c0 ◂— 'younexxxxxxxx'
06:0018     0xffffce98 —▸ 0xf7d8a1e0 (__strncmp_sse4_2) ◂— push ebp
07:001c     0xffffce9c ◂— 0x15

Here, I’ve censored the password, but you can see it in the stack if you try this method out for yourself.

The final flag is HTB{<the password you find on the stack>}.

I still couldn’t figure out where this piece of code came from. I tried searching for cross-references of functions like dlsym, strncmp, and strlen, but all I found was a reference to an unknown code unit. This means that the code we just looked at was created at runtime and can’t be found in static analysis. Extremely interesting puzzle and I learned a lot about anti-reversing/debugging and how difficult it can be to bypass such measures.

I hope you learned something from this blog :)

Thank you for reading.