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!
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,
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!
We start by running
file on the binary as we always do!
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
And the code that produced the binary:
msg: db "Hello, World!",0xa
len: equ $-msg
mov eax, 4
mov ebx, 1
mov ecx, msg
mov edx, len
mov al, 1
mov ebx, 0
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.
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
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
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.
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.
After changing the data type, it looks a lot cleaner!
What's even better is the disassembly is also updated.
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.
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
EAX = 4) with the file descriptor of
EBX = 1), the buffer is "Lucky Numbers:" (
ECX = "Lucky Numbers"), and the amount of bytes to print is
0xf). With these registers set, this is equivalent to calling
printf("Lucky Numbers: "). Alright, let's continue our analysis.
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
user_input and continue our analysis.
We first see the first index
user_input gets stored in the
AL register and then
0x30 is subtracted. So,
AL = user_input - 0x30. We then see the second index of our user_input (DAT_0804a025) is stored in the
BL register. Similarly,
BL is subtracted by
BL = user_input - 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 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
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
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 😂.
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:
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:
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:
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!
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 isn't equal to
0x16 we jump to
LAB_08049000. Let's take a look at what this code segment does.
As you can see, if
AL is not equal to
0x16 then we fail. The next comparison between
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!
And there you have it! The most challenging part of this challenge was understanding the
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! ✌🏾