As you might already be aware that ARM powers a variety of low-powered devices around us, including but not limited to, phones, routers, IoT devices, etc. Therefore, it is only logical to dig into this architecture and understand how it differs from x86 and x64 architectures. For this blog post, we will focus on 64bit ARM CPU as currently it is most commonly used. Our setup includes Ubuntu 16.04 on ARM Cortex-A53 CPU, which supports both 32bit and 64bit instruction set.
Compile the program:
$ gcc crack_me.c -o crack_me
Now let’s fire up GDB with the binary and start analysing. Note that I am using GEF (https://github.com/hugsy/gef) with GDB, so that, my prompt will look like gef> and not gdb>. Let’s start by disassembling the main function.
$ gdb ./crack_me
gef> disas main
Our attention goes directly to <check_pass> function at <main+64>, but before going there, you may want to take a moment and understand what these instructions mean. You can read up more about these on ARM’s documentation (https://developer.arm.com/docs/100069/latest/a64-general-instructions).
The following are some instructions, which are of importance to our analysis.
b – branch to label, like jmp statement
bl – branch with link to label, like call statement
b.ne – branch to label if not equal, like jne statement
b.eq – branch to label if equal, like je statement
Let’s dive into the assembly code. The numbering refers to the sections highlighted in the GDB disassembly output.
- In address 0x4007bc, <main+4>, the stack pointer (sp) register is mov’ed to register x29. Then we notice the main function arguments being accessed from x29 register. Note that offset 28 of x29 register contains argc and offset 16 contains argv (which is our input password). Upon comparing the argc value, if it is equal to 0x2, we branch (b.eq – branch if equal) to <main+52>.
- The next three lines <main+52>, <main+56>, and <main+60> expand the length of the size of argv string from 16 to 24 (16+0x8=24) and is referenced by x0 register.
- Then we call (bl – branch with link) to <check_pass> function.Let’s disassemble the <check_pass> function.
gef> disas check_pass
- In the address 0x400738, <check_pass+8>, the newly sized argv string is copied from x0 register to x29 register with offset 24. Then we see some stack canary operations, from <check_pass+12> till <check_pass+24>. Something is being stored on x29 register with offset 56 from address 0x411048, which is later compared at the end of the function from <check_pass+96> onwards till <check_pass+124>.
- Coming back to the body of <check_pass> function, we see that from <check_pass+32> onwards, something is being accessed from 0x4008d0 and stored into x29 register with offset 0x28 (40), probably the secret password?
- Then from <check_pass+60> onwards, x1 register points to the newly copied data from 0x4008d0 and x0 register points to the argv string in register x29 with an offset 24, followed by a call to strcmp (string compare between x0 & x1) function. The return value of strcmp function is stored in 16bit general purpose w0 register. If the strings are equal, then the w0 is set to 0x0, else it is set to 0x1.Coming back to the <main> function…
- The return value of <check_pass> function is stored in w0 register, which is copied to x29 register with an offset 44. Then at <main+76> we compare the value of w0 register to see if it is equal to 0x1. If not, we jump (b.ne – branch if not equal) to <main+100>, which will lead us to a success message, and finally exit the program.
We will now start the program with a wrong password. But before that, we must add breakpoint, at the compare statement at <main+76>.
gef> break *0x400804
gef> run pass123
We have hit the breakpoint at the compare statement at 0x400804, <main+76>. Also, observe that the value of x0 register is 0x1. Since, x0 pointer is nothing but w0 register + 32 extra bits, x0 contains the return value of <check_pass> function. But based on hypothesis from analysis, this value should be 0x0 for the program to display us a success message.
Let us change its value…
gef> set $x0=0x0
Now let us continue with the execution.
Turns out our hypothesis was correct. Changing the value of x0 from 0x1 to 0x0 did the trick. Which means it will always check if w0 is set to 0x1 to display the incorrect message. We know this from the source code of our program. Therefore, going back to <check_pass> function, we had noticed something being copied from address 0x4008d0. Let’s inspect that for a bit.
This does not look like any valid assembly instruction, but the repetitions of 53 is suspicious, also 41 is ‘A’ in hexadecimal. This looks like a constant string. Let’s look deeper. Dumping 10 more lines from our address 0x4008d0…
Looking at 0x4008d0 and 0x4008d4, we can make out that it is a little-endian 8-bit string. Let’s try to decode it…
So, here we have the original password “PASSWORD”.
This was just another example of analysing binaries with GDB, but in a different architecture. Going ahead, we will deal with more complex programs, uncommon architectures, and weirder binaries.
Arannya Mukherjee is a Security Analyst, whose primary interests are Reverse Engineering, Network based Attacks, and Threat Hunting, among many other.