Reverse Engineering Challenge | Lucky Numbers

Reverse Engineering Challenge | Lucky Numbers

Introduction

Hello and welcome back yet again! Today we have a simple crackme written in Assembler. Someone I've been helping recently sent me the binary asking for help. When I originally looked at the binary I was confused because I was expecting a C/C++ binary. Only to learn later that it was written in Assembler which changed how I looked at the binary. The challenge honestly is not very difficult but I did learn a few interesting instructions so I wanted to share what I learned. Let's take a look shall we? Of course you can find the video walkthrough on my YouTube channel!

Video Walkthrough for Lucky Numbers

Optional Materials to Follow Along

As always, you can follow along by using my VM which has all of the tools you'll need to solve this challenge. You'll find the binary in /home/kali/reverse_engineering/crackmes/lucky_numbers. If you don't want to use my VM, you'll need to download the binary here.

Finally, you'll need a disassembler. I recommend IDA or Ghidra. I'll be using Ghidra throughout this tutorial. If you're using my VM, ghidraRun and ida64, are in the path so you can execute them from anywhere. Alright with all of that out of the way, let's get started!

Initial Triage

We start by running file on the binary as we always do!

File output
File output

We see the binary is 32-bits and the author stripped the binary of symbols. If you're curious, as I was, here are the symbols that would be present in a simple hello_world.asm program.

Symbols for a simple hello world program
Symbols for a simple hello world program

And the code that produced the binary:

global _start
section .data
   msg: db "Hello, World!",0xa
   len: equ $-msg
section .text
_start:
   mov eax, 4
   mov ebx, 1
   mov ecx, msg
   mov edx, len
   int 0x80
   mov al, 1
   mov ebx, 0
   int 0x80

Interestingly, I've never actually compiled a binary written in ASM nor have I ever analyzed one so there are quite a few firsts for me here! Anyway, let's get back on track and take a look at the strings in our target binary.

Strings output
Strings output

We see the normal stuff we are used to seeing. A prompt and a success and error message. Well, let's go ahead and open this binary in Ghidra!

Static Analysis in Ghidra

Disassembling the binary's entry point
Disassembling the binary's entry point

This is the majority of the binary. There are some conditional jump instructions that we'll have to analyze once we get there. But first, let's go over the first 5 lines in the binary. We see 4, 1, and 0xf are loaded in the EAX, EBX, and EDX register respectively. Further, we also see a data string get loaded into the ECX register. Before we talk about that int 0x80 instruction, let's take a look at what is stored in this data string.

Data section
Data section

We see there were a few strings in this binary. However, this representation isn't very intuitive, so let's tell Ghidra to interpret these as strings. You'll need to highlight the entire section you want Ghidra to turn into a string, then Right-Click -> Data -> String. This is demonstrated in the image below.

Changing the data type to a string
Changing the data type to a string

After changing the data type, it looks a lot cleaner!

Updated strings
Updated strings

What's even better is the disassembly is also updated.

Updated disassembly
Updated disassembly

Alright great! Now, what's up with that int 0x80 instruction? int stands for interrupt. What's happening in this section is the program is performing a system call. The parameters for the system call are placed in various registers. Let's take a look at the 32-bit system call table to figure out what system call is being executed and the parameters.

32-bit System Call Table
32-bit System Call Table

If you have trouble seeing the image above you can find a list of system calls here. It's important to remember that this is a 32-bit binary so we have to look at the system call for a 32-bit machine. There are slight differences between the system call tables. If you're curious here is the 64-bit system call table. Analyzing the table above, we see we are calling sys_write (EAX = 4) with the file descriptor of 1 (EBX = 1), the buffer is "Lucky Numbers:" (ECX = "Lucky Numbers"), and the amount of bytes to print is 0xf (EDX = 0xf). With these registers set, this is equivalent to calling printf("Lucky Numbers: "). Alright, let's continue our analysis.

Another System Call
Another System Call

We see we have another syscall to decipher. The EAX register is being loaded with 0x3 which means this is a read. Oddly enough, the file descriptor being used here is stderr when we expect stdin. Now, I thought this was VERY odd and that there might be a problem, but I have to give a shoutout to a friend of mine from the Discord channel crackmes.one. He pointed me to the following stack overflow answer that says using 0/1/2 for input, output, and error is only a convention! You can actually use the file descriptors however you see fit. Pretty crazy right!? So, because the syscall being used is a read, let's rename DAT_0804a024 to user_input and continue our analysis.

Last part of main function
Last part of main function

We first see the first index user_input gets stored in the AL register and then 0x30 is subtracted. So, AL = user_input[0] - 0x30. We then see the second index of our user_input (DAT_0804a025) is stored in the BL register. Similarly, BL is subtracted by 0x30. So BL = user_input[1] - 0x30. Then we see an instruction we haven't encountered before ADC. This stands for Add with Carry. This instruction simply adds the operand register, the carry flag, and the second operand to each other. So this instruction looks like this ADC -> (operand1 + CF) + operand2. The carry flag could be set in either of the two subtraction operations from before so it's important to keep that in mind when developing our solution. The next instruction, DAA, is also novel to us. This instruction confused me for a bit, but I was able to find a great tutorial that I’ll link later on. The instruction stands for Decimal Adjust AL after Addition. I know that's a mouthful!

It's important to note that this instruction ONLY acts on the AL register. No other register is affected. I know that might be obvious given the name, but I thought it necessary to explicitly point this out.

There's an excellent explanation of this instruction here but I will try to explain it here. The beginning of the video is a little hard to hear but it gets really good around the 4:34 mark.

DAA Instruction Explained

This is going to be a little meaty (giggity) so I think it deserves it's own section. This instruction aims to bridge the gap between the hexadecimal numbering system used in computers and the decimal system we use in real life. What does that mean? Suppose, you are working behind a cash register and you add the numbers 88 and 88. We expect the result to be 176, however, in the computer these numbers will be treated as hexadecimal, so it will be 0x88 + 0x88 which is 0x110 (272 in decimal). Obviously, this is not the number we expect so we need a way to convert it back to decimal. That's where binary coded decimals come into play and the purpose of the DAA instruction. In the example I gave earlier, we can split up the two number (8 and 8) into two nibbles (4 bits). The higher 4 bits are called the higher nibble and the lower 4 bits are called the lower nibble. So, in this example, the higher and lower nibbles happen to be the same. But, if the number was 0x9C the higher nibble would be 9 and the lower nibble would be C. Why does this matter? Well, there are a few rules for converting these numbers back to decimal numbers. I'll explain each of these rules with practical examples!

Lower Nibble Example

The first rule I'll discuss tells us to add 06 if the lower nibble is greater than 9 OR the auxiliary carry bit is set? If you don't know what the auxiliary carry bit is, don't worry I'll be sure to explain. I never heard of it before diving deep (giggity) into this instruction. You might be wondering why do we need to add 6. Think about the problem we are trying to solve. In the decimal space, only 0–9 are valid numbers but in hexadecimal there are 5 extra characters (A, B, C, D, E, and F). You want to get the hex number back into the space of decimal digits. Since there are 5 characters we can add 6 to any of these numbers and we will fall back into the decimal digit space. Alright let's first take a look at what happens when the lower nibble is greater than 9 AFTER an addition. Let's add the hexadecimal numbers 0x14 and 0x16. First, 6 + 4 = A (10 in hexadecimal). Then, we add 1 + 1 = 2. So, the answer comes out to be 0x2A. But, remember the point of this is to convert these numbers to decimal. Since the lower nibble is greater then 9, we have to add 6 which gives us 10. The 1 gets added to 2 which gives us a final answer of 30. Which is what we would expect when we add 14 and 16 in decimal!

What The Heck is Auxiliary Carry

The other case that will cause us to add 6 to the lower nibble is if the auxiliary carry flag is set but what in the world is that!? If you already know before continuing that's impressive! Let's take the following example, 0x18 + 0x18. Hopefully the image below helps illustrate auxiliary carry. Please forgive the chicken scratch 😂.

Auxiliary Carry Bit Example
Auxiliary Carry Bit Example

This would be similar if we were adding any two numbers whose sum is greater than or equal to 16. So, since the auxiliary carry flag would be set in this example, we add 6 to the lower nibble so our result is 36. Which is what we expect if we were to add 18 + 18 in decimal.

Higher Nibble Example

What happens if the higher nibble is greater than 9? Well in that case, we add 6 to the higher nibble. Let's look at another example: 0x52 + 0x52. Similar to what we did earlier, we add the nibbles together. So, 0x2 + 0x2 = 4. However, 0x5 + 0x5 = 0xA.  Thus, making the final sum: 0xA4. Since 0xA is greater than 9, we have to add 6 to the higher nibble. 0xA + 6 = 0x10. So, the answer converted to decimal would be 104. The other case we have to deal with is if the carry flag is set as a result of the addition. Consider the following example:

Carry bit example
Carry bit example

Here, we see the carry bit is the result of adding two numbers whose sum is greater than or equal to 16 in the higher bit. So, we add 6 to the higher nibble and we get 164 which is what we expect by adding 82 and 82.

Last DAA Example

Now, what happens if both the auxiliary flag AND the carry flag were set? Well, we'd add 6 to both the high and low nibbles. Let's look at one last example:

Both Auxiliary and Carry Bit Flags Set
Both Auxiliary and Carry Bit Flags Set

As you can see, because both the auxiliary and carry bits were set, we had to add 6 to both the lower nibble and the higher nibble in that order to get our final answer. And that's it for this instruction. I hope it made sense because it will play an important part in how we develop the solution. If not, feel free to ask me any questions. My contact information is at the bottom of this blog post right before my Patreon information😉. Alright, let's FINALLY get back to our code!

Back to the Disassembly!

Returning to the disassembly
Returning to the disassembly

Alright, nothing has changed here except we now have a deeper understanding of each of the instructions. So, after the DAA instruction executes, we add 0x30 to the BL register. Next, we compare AL with 0x16. If AL isn't equal to 0x16 we jump to LAB_08049000. Let's take a look at what this code segment does.

Bad answer code segment
Bad answer code segment

As you can see, if AL is not equal to 0x16 then we fail. The next comparison between BL and 0x38 tells us that BL should be equal to 0x38 otherwise we fail. Let's think a little what this code is doing. It takes the higher nibble and lower nibble of our input and subtracts 0x30 from both. We've seen this behavior in past challenges. The author is trying to extract the decimal number from the ASCII represented number. Remember, if we type in the number 66, the higher and lower nibbles would be represented as 0x36 which is 6 in ASCII. So, it appears the author wants the actual decimal value. Then, the author performs an add with carry of the two nibbles, performs the adjust of the higher nibble, and finally adds 0x30 to the lower nibble essentially turning it back into an ASCII represented number. So, how do we go about solving this? Well, the lower nibble never really changes right? Sure it gets subtracted by 0x30 but as we saw, 0x30 gets added back. So, the lower nibble has to be 8 otherwise BL will never be equal to 0x38. So, how do we find AL? Well, we know that eventually, it has to be equal to 0x16 after the addition and the adjustment. That means we can essentially treat 0x16 as the decimal 16. That's what the DAA instruction does after all. Converts hexadecimal numbers to binary coded decimals. Let's apply some simple algebra to solve for AL. So, our equation is AL + BL = 16 which becomes AL = 16 - BL. We know that BL has to be 8 so AL must also be 8! See, Math is useful outside of the classroom 😂. So, our solution should be 88! Let's test this out!

We got it!
We got it!

Conclusion

And there you have it! The most challenging part of this challenge was understanding the DAA and ADC instructions! Although it was a simple challenge I learned a lot and I hope you did as well! We learned that we can mix and match file descriptors. Additionally, we learned about binary coded decimals! As always if you have any questions feel free to hit me up on Twitter, Instagram, or Discord: jaybailey216#6540. If you have a challenge you want me to try next, let me know and I'll give it a shot! I'll see you all next time!

Peace out! ✌🏾

Show Comments