Crackme 5: ARM, basic
https://www.root-me.org/en/Challenges/Cracking/ELF-ARM-basic-crackme (binary)
First ARM binary! We won’t be able to run it in Docker either, so let’s dive in with Hopper.
Again, let’s follow the string “Checking %s for password…\n” and look at the logic around the code that references it:
In this first block, fp
is the frame pointer, the base address for local variables. The one at offset 0x18 is a string, passed to printf
as the value for the “%s” entry mentionned above. For function calls with four or fewer 32-bit parameters, registers r0
to r3
are used to pass in the values. After a call, r0
contains the return value. Here the format string is placed into r0
via r3
and the string at fp
- 0x18 is referenced by r1
. This is our input. bl
is Branch-with-Link, similar to call
in x86 assembly.
ldr r3, aCheckingSForPa ; dword_8658,"Checking %s for password...\\n"
mov r0, r3 ; argument "__format" for method printf@PLT
ldr r1, [fp, #-0x18]
bl printf@PLT ; printf
After the call to printf
, we compute the input’s length, store it in a local variable at fp
- 0x1c, and compare it to 6. We Branch-if-Equal (beq
) to loc_84f8
. If the length is not 6, we print an error message.
ldr r0, [fp, #-0x18] ; argument "__s" for method strlen@PLT
bl strlen@PLT ; strlen
mov r3, r0
str r3, [fp, #-0x1c]
ldr r3, [fp, #-0x1c]
cmp r3, #0x6
beq loc_84f8
The password length is 6! Let’s look at the remaining validation code. The dump being pretty verbose, we’ll look at some of the assembly to get a general idea of the language before switching to Hopper’s pseudo-code view.
In the first block of this function, we had set the variable at fp
- 0x10 (let’s call it var_10
) to 6:
mov r3, #0x6
str r3, [fp, #-0x10]
Now that we’ve checked the length, we use this local variable again and copy its value to r4
before calling strlen
again and saving that into r3
:
loc_84f8:
ldr r4, [fp, #-0x10] ; CODE XREF=sub_8470+116
ldr r0, [fp, #-0x18] ; argument "__s" for method strlen@PLT
bl strlen@PLT ; strlen
mov r3, r0
r3
now has the length, and r4
is 6. We compute r3 = r3 - r4
and put this back in var_10
:
rsb r3, r3, r4
str r3, [fp, #-0x10]
So we started with var_10
= 6, subtracted the string length from it – which we know to be 6 – var_10
is therefore zero at this point.
We then load the first byte of our input into r2
:
ldr r3, [fp, #-0x18]
ldrb r2, [r3]
And the fifth byte of our input into r3
(load address into r3
, add 5, dereference into r3
):
ldr r3, [fp, #-0x18]
add r3, r3, #0x5
ldrb r3, [r3]
Compare r2
and r3
:
cmp r2, r3
beq loc_8538
if they are equal, we jump over a block. This block contains the following code, which is simply incrementing var_10
:
ldr r3, [fp, var_10]
add r3, r3, #0x1
str r3, [fp, var_10
In effect, we ran:
var_10 = 0;
if (input[0] != input[5]) {
var_10++;
}
There are several more similar comparisons following:
They decode to:
if (input[0] + 1 != input[1]) {
var_10++;
}
if (input[3] + 1 != input[0]) {
var_10++;
}
if (input[2] + 4 != input[5]) {
var_10++;
}
if (input[4] + 2 != input[2]) {
var_10++;
}
The next block is different. We start by loading input[3]
into r3
:
loc_85f0:
ldr r3, [fp, #-0x18] ; CODE XREF=sub_8470+368
add r3, r3, #0x3
ldrb r3, [r3]
We XOR r3
with 0x72, AND with 0xff, and store the result back into r3
:
eor r3, r3, #0x72
and r3, r3, #0xff
We then load var_10
into r2
, add r2
and r3
, and put this whole thing back into var_10
:
ldr r2, [fp, #-0x10]
add r3, r2, r3
str r3, [fp, #-0x10]
We load input[6]
(which we know is a null byte) into r3
, var_10
into r2
, add the two together and put the sum back into var_10
:
ldr r3, [fp, #-0x18]
add r3, r3, #0x6
ldrb r3, [r3]
ldr r2, [fp, #-0x10]
add r3, r2, r3
str r3, [fp, #-0x10]
We then compare var_10
to zero and print the success message if they are equal:
ldr r3, [fp, #-0x10]
cmp r3, #0x0
bne loc_8644
Now that we have gone through this process manually, let’s examine the pseudo-code that Hopper generates for it. Click on the pseudo-code button, open the right frame and tick the “R11 based frame” check box. Navigate back to CFG mode and again to pseudo-code, which now displays:
int sub_8470(int arg0, int arg1) {
var_24 = arg1;
if (arg0 != 0x2) {
puts("Please input password");
r0 = exit(0x1);
}
else {
var_18 = *(var_24 + 0x4);
printf("Checking %s for password...\n", var_18);
var_1C = strlen(var_18);
if (var_1C != 0x6) {
puts("Loser...");
r0 = exit(var_1C);
}
else {
var_10 = 0x6 - strlen(var_18);
if (*(int8_t *)var_18 != *(int8_t *)(var_18 + 0x5)) {
var_10 = var_10 + 0x1;
}
if (*(int8_t *)var_18 + 0x1 != *(int8_t *)(var_18 + 0x1)) {
var_10 = var_10 + 0x1;
}
if (*(int8_t *)(var_18 + 0x3) + 0x1 != *(int8_t *)var_18) {
var_10 = var_10 + 0x1;
}
if (*(int8_t *)(var_18 + 0x2) + 0x4 != *(int8_t *)(var_18 + 0x5)) {
var_10 = var_10 + 0x1;
}
if (*(int8_t *)(var_18 + 0x4) + 0x2 != *(int8_t *)(var_18 + 0x2)) {
var_10 = var_10 + 0x1;
}
var_10 = var_10 + ((*(int8_t *)(var_18 + 0x3) ^ 0x72) & 0xff);
var_10 = var_10 + *(int8_t *)(var_18 + 0x6);
if (var_10 == 0x0) {
puts("Success, you rocks!");
r0 = exit(0x0);
}
else {
puts("Loser...");
r0 = exit(var_10);
}
}
}
return r0;
}
This is readable and matches our analysis. We now have a full set of constraints that we can use to generate a valid input; since we know that var_10
must always stay at zero, all the conditions need to match accordingly.
- The length must be 6.
input[0]
==input[5]
input[0]
+ 1 ==input[1]
input[3]
+ 1 ==input[0]
input[2]
+ 4 ==input[5]
input[4]
+ 2 ==input[2]
- (
input[3]
^ 0x72) & 0xff == 0 input[6]
== 0
Starting from input[3]
, the only matching value that would be zero when XORed with 0x72 is 0x72 itself, which encodes the character ‘r’. From this we can deduce all the other values. The resulting password is the word “storms”.