Contents

Reversing ELFs on TryHackMe: Part 2

Contents

Safe-space

Please don’t ever execute any random ELF/PE binary you find on the web directly on your workstation. Even if it comes from a trusted source like HTB or THM, we should always work in a segmented environment. Since these challenges are all ELFs, I spun up a Kali machine (you can literally use any OS to do these, I just had a VM ready to go) to run through these. Once you’ve done that, you can just log in and download each of the challenge files.

Crackme5

A lot of people want to jump right into using fancy GUI based programs for performing initial reversing, but I’d like to take the opportunity here to introduce yet another commandline utility we can leverage for simple tasks. Up to this point we’ve been executing our RE binaries directly from the terminal. But what happens if we proxy that execution through a program that can trace behavior along the way? Allow me to introduce… ltrace (a library call tracer). If we run this program, we’ll actually see something really interesting…

/images/thm/reversing/reversingelf/ltrace.png

Now, obviously our strcmp is showing the actual password but let’s pause for a second to appreciate some additional program behavior we can identify. Notice how when using the input test, this binary calls strlen("test") several times - 5 times. Why is it doing that? Well we need to try a few more cases to see if there’s a method to the madness.

/images/thm/reversing/reversingelf/ltrace2.png

Based off this, it looks like strlen(<input>) will execute input.length() + 1 times. Little tidbits like this help us figure out program flow and logic (despite not necessarily being immediately usefull to this challenge, this is a good habit to build).

Crackme6

This is where things start to get fun, and where I’m going to introduce GUI-based tools. For the screenshots in this writeup, I’ll be using BinaryNinja but Ghidra or IDA Pro also have similar Graph visualization (I’m just showing my bias, and also shamelessly plugging a fantastic project that I think everyone should support). So let’s open up our binary, and navigate to the entry point to visualize the program’s flow a little.

/images/thm/reversing/reversingelf/binja_main.png

From this main function graph, we can see two distinct branches - one that outputs the usage syntax, and the other calling a new function compare_pwd. Basically, if we use the program incorrectly (like not passing in an argument) we’ll get the usage messasge, and if we do correctly pass in a value that the program is expecting, we’ll call compare_pwd. So let’s drilldown into that function to see what it’s doing (simply double click the function name).

/images/thm/reversing/reversingelf/binja_compare.png

Again we’re presented with two distinct branches - one if the input is correct and one if it isn’t. But before we branch, we call a function my_secure_test… so another drilldown is in order!

/images/thm/reversing/reversingelf/binja_secure.png

Well… that doesn’t look fun. Instead of trying to track down all these different conditions, lets start with a small chunk and see if there’s any pattern we can discern.

/images/thm/reversing/reversingelf/binja_chunk1.png

Let’s break this down line by line. If we remember back to the graph of compare_pwd above, we can see that we’re calling the following functions before going into my_secure_test:

mov     qword [rbp-0x8 {var_10}], rdi
mov     rax, qword [rbp-0x8 {var_10}]
mov     rdi, rax
call    my_secure_test

So what’s happening here? To understand that, we need to know what these mov instructions are doing - which we can learn here. In short, The mov instruction copies the data item referred to by its second operand (i.e. register contents, memory contents, or a constant value) into the location referred to by its first operand (i.e. a register or memory). So in the context of these instructions, we’re taking the value stored on rdi (our input) and storing it in a variable called var_10. After this, we’re taking var_10 and moving it to rax and then finally moving the contents of rax to rdi. We won’t go into why it needs to do all this, because that is going into computer architecture that isn’t really relevant for understanding the program flow for our purposes. All we need to know is that var_10 holds our input… sorry for taking the roundabout way of arriving at that conclusion.

Going back to the my_secure_test function in the screenshot above, we can see references to var_10. What I want to focus on is this logic:

mov     rax, qword [rbp-0x8 {var_10}]
movzx   eax, byte [rax]
test    al, al

What’s happening here, is we’re taking var_10 and storing it into rax, but then we’re taking a single byte from rax and storing it into eax. Following this, we’re running test al al which just used to set a flag prior to our last instruction je 0x40059b which points to the instruction mov eax,0xffffffff. I know… your brain is probably melting trying to take that in but in simple terms, we’re taking a single byte (oh hey don’t characters in C have a size of one byte?) and then testing it. If the test fails, we load 0xffffffff into eax and move on. Even simpler, we’re taking our input string and comparing it against the target string character by character, and if there’s no match we return a 1.

The reason I wanted to use Binary Ninja for this is because we can switch this graph view now to see a Pseudo-C representation.

/images/thm/reversing/reversingelf/binja_pseudo.png

Let’s use this representation to extract all the values we’re comparing against

  • if ((*(int8_t*)arg1 == 0 || (*(int8_t*)arg1 != 0 && *(int8_t*)arg1 != 0x31))) : 0x31
  • if ((arg1[1] == 0 || (arg1[1] != 0 && arg1[1] != 0x33))) : 0x33
  • if ((arg1[2] == 0 || (arg1[2] != 0 && arg1[2] != 0x33))) : 0x33
  • if ((arg1[3] == 0 || (arg1[3] != 0 && arg1[3] != 0x37))) : 0x37
  • if ((arg1[4] == 0 || (arg1[4] != 0 && arg1[4] != 0x5f))) : 0x5f
  • if ((arg1[5] == 0 || (arg1[5] != 0 && arg1[5] != 0x70))) : 0x70
  • if ((arg1[6] == 0 || (arg1[6] != 0 && arg1[6] != 0x77))) : 0x77
  • if ((arg1[7] == 0 || (arg1[7] != 0 && arg1[7] != 0x64))) : 0x64
  • if (arg1[8] == 0) : 0 (null terminator for c string)

By going through this flow, we can see that we have a 9-character string (including the null terminator) of the following values: 0x31, 0x33, 0x33, 0x37, 0x5f, 0x70, 0x77, 0x64, 0x0. So what happens when we convert this hex to say ASCII? Luckily, it’s trivial to copy paste hex into CyberChef or even just use echo

/images/thm/reversing/reversingelf/echo_1337.png

We could have also done this directly in Binary Ninja/Ghidra/IDA by having the software resolve the Hex values for us:

/images/thm/reversing/reversingelf/displayas.png

/images/thm/reversing/reversingelf/decoded.png

Part 3 Coming Soon

Today’s writeup was pretty dense, so rather than continue on and overload anyone reading, we’ll stop for the day. I hope this writeup resulted in you learning something new!