Recently, I decided to put my reversing skills to a test by solving some of the public crackmes and writing down my solutions. This is the second post of the series.
For work, I do some very basic Android, TEE, RASP and IoT reversing, but most of those tasks are too shallow and repetitive. You can’t get much depth when reversing is an “if there’s time to waste” part of an engagement with overall length of a week and bottomless scope, so this series is about a monkey learning to dive deeper.
I decided to source my first batch of challenges from Crackmes.One. Second challenge is of difficulty level 2 and is targeting ARM/Linux.
Running it
First, figure out what the binary is:
$ file diff2_bin
diff2_bin: ELF 32-bit LSB executable, ARM, EABI4 version 1 (SYSV), statically linked, for GNU/Linux 2.6.16, with debug_info, not stripped
$ readelf -a diff2_bin
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: ARM
Version: 0x1
Entry point address: 0x8150
Start of program headers: 52 (bytes into file)
Start of section headers: 496100 (bytes into file)
Flags: 0x4000002, Version4 EABI, <unknown>
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 7
Size of section headers: 40 (bytes)
Number of section headers: 34
Section header string table index: 31
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .note.ABI-tag NOTE 00008114 000114 000020 00 A 0 0 4
[ 2] .init PROGBITS 00008134 000134 000014 00 AX 0 0 4
[ 3] .text PROGBITS 00008150 000150 05d3a8 00 AX 0 0 16
[ 4] __libc_freeres_fn PROGBITS 000654f8 05d4f8 000cac 00 AX 0 0 4
[ 5] .fini PROGBITS 000661a4 05e1a4 000010 00 AX 0 0 4
[ 6] .rodata PROGBITS 000661b8 05e1b8 0144e0 00 A 0 0 8
[ 7] __libc_atexit PROGBITS 0007a698 072698 000004 00 A 0 0 4
[ 8] __libc_subfreeres PROGBITS 0007a69c 07269c 00002c 00 A 0 0 4
[ 9] .ARM.extab PROGBITS 0007a6c8 0726c8 00030c 00 A 0 0 4
[10] .ARM.exidx ARM_EXIDX 0007a9d4 0729d4 000768 00 AL 3 0 4
[11] .eh_frame PROGBITS 0007b13c 07313c 000080 00 A 0 0 4
[12] .tdata PROGBITS 00083fb4 073fb4 000010 00 WAT 0 0 4
[13] .tbss NOBITS 00083fc4 073fc4 000018 00 WAT 0 0 4
[14] .init_array INIT_ARRAY 00083fc4 073fc4 000004 00 WA 0 0 4
[15] .fini_array FINI_ARRAY 00083fc8 073fc8 000008 00 WA 0 0 4
[16] .jcr PROGBITS 00083fd0 073fd0 000004 00 WA 0 0 4
[17] .data.rel.ro PROGBITS 00083fd4 073fd4 00002c 00 WA 0 0 4
[18] .got PROGBITS 00084000 074000 00006c 04 WA 0 0 4
[19] .data PROGBITS 00084070 074070 0006e4 00 WA 0 0 8
[20] .bss NOBITS 00084758 074754 001880 00 WA 0 0 8
[21] __libc_freeres_pt NOBITS 00085fd8 074754 000014 00 WA 0 0 4
[22] .comment PROGBITS 00000000 074754 004472 00 0 0 1
[23] .debug_aranges PROGBITS 00000000 078bc8 000058 00 0 0 8
[24] .debug_pubnames PROGBITS 00000000 078c20 000025 00 0 0 1
[25] .debug_info PROGBITS 00000000 078c45 00017b 00 0 0 1
[26] .debug_abbrev PROGBITS 00000000 078dc0 00006f 00 0 0 1
[27] .debug_line PROGBITS 00000000 078e2f 000130 00 0 0 1
[28] .debug_str PROGBITS 00000000 078f5f 0000a4 01 MS 0 0 1
[29] .debug_ranges PROGBITS 00000000 079008 000048 00 0 0 8
[30] .ARM.attributes ARM_ATTRIBUTES 00000000 079050 00002d 00 0 0 1
[31] .shstrtab STRTAB 00000000 07907d 000167 00 0 0 1
[32] .symtab SYMTAB 00000000 079734 00bf00 10 33 1925 4
[33] .strtab STRTAB 00000000 085634 006394 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
y (purecode), p (processor specific)
There are no section groups in this file.
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
EXIDX 0x0729d4 0x0007a9d4 0x0007a9d4 0x00768 0x00768 R 0x4
LOAD 0x000000 0x00008000 0x00008000 0x731bc 0x731bc R E 0x8000
LOAD 0x073fb4 0x00083fb4 0x00083fb4 0x007a0 0x02038 RW 0x8000
NOTE 0x000114 0x00008114 0x00008114 0x00020 0x00020 R 0x4
TLS 0x073fb4 0x00083fb4 0x00083fb4 0x00010 0x00028 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
GNU_RELRO 0x073fb4 0x00083fb4 0x00083fb4 0x0004c 0x0004c R 0x1
Section to Segment mapping:
Segment Sections...
00 .ARM.exidx
01 .note.ABI-tag .init .text __libc_freeres_fn .fini .rodata __libc_atexit __libc_subfreeres .ARM.extab .ARM.exidx .eh_frame
02 .tdata .init_array .fini_array .jcr .data.rel.ro .got .data .bss __libc_freeres_ptrs
03 .note.ABI-tag
04 .tdata .tbss
05
06 .tdata .init_array .fini_array .jcr .data.rel.ro
There is no dynamic section in this file.
There are no relocations in this file.
Unwind section '.ARM.exidx' at offset 0x729d4 contains 237 entries:
...skip...
Symbol table '.symtab' contains 3056 entries:
...skip...
No version information found in this file.
Displaying notes found in: .note.ABI-tag
Owner Data size Description
GNU 0x00000010 NT_GNU_ABI_TAG (ABI version tag)
OS: Linux, ABI: 2.6.16
Attribute Section: aeabi
File Attributes
Tag_CPU_name: "ARM10TDMI"
Tag_CPU_arch: v5TE
Tag_ARM_ISA_use: Yes
Tag_ABI_PCS_wchar_t: 4
Tag_ABI_FP_denormal: Needed
Tag_ABI_FP_exceptions: Needed
Tag_ABI_FP_number_model: IEEE 754
Tag_ABI_align_needed: 8-byte
Tag_ABI_align_preserved: 8-byte, except leaf SP
Tag_ABI_enum_size: int
This is a 32-bit ARM v5TE binary, built for Linux Kernel 2.6.16. Once again, I didn’t have a compatible system handy, so I used the same QEMU image as in part 1. ARMv7-A is backwards compatible with ARMv5TE for my purposes, so this will work.
This time, the binary has a lot of symbols, which could mean that it will be easier to reverse or that they were added by an obfuscator. After all, it’s in excess of 3000.
Upon running the binary, it exits with code 255
:
user@debian-armhf:~$ ./diff2_bin
user@debian-armhf:~$ echo $?
255
This could indicate further issues with my QEMU setup or it could be by design. The code will reveal which case it is.
Step 1 complete: get the binary to run.
Reversing
At a glance, it seems that this binary has everything it needs statically compiled into it. Symbols include a lot of standard library functions and there are no imports. At 559KB compared to 5.5KB of the previous challenge, this is quite believable.
Inside main()
, I see this theory confirmed:
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // r1
int v7; // [sp+14h] [bp-20h]
int v8; // [sp+18h] [bp-1Ch]
int v9; // [sp+18h] [bp-1Ch]
int i; // [sp+18h] [bp-1Ch]
signed int v11; // [sp+1Ch] [bp-18h]
if ( argc <= 1 )
return -1;
v8 = 0;
v7 = xmalloc(32, argv, envp);
while ( v8 != 8 )
{
*(v7 + 4 * v8) = xmalloc(32, v3, 4 * v8);
memset(*(v7 + 4 * v8++), 10, 32);
}
*(v7 + 32) = 0;
v9 = 0;
v11 = 65;
while ( v9 != 31 )
*(*(v7 + 12) + v9++) = v11++;
*(*(v7 + 12) + 31) = 0;
for ( i = 0; argv[1][i]; ++i )
{
if ( argv[1][i] != *(*(v7 + 12) + i) )
return -1;
}
return 1337;
}
One thing that sticks out is the usage of xmalloc()
function, which is not in standard C library. I did a quick search and found the following references:
Looks like a) publib may be involved and b) the only thing special about the function is that it exits if memory allocation fails. IDA’s decompilation output seems wrong, because the signature to expect is void *xmalloc(size_t bytes)
. Upon closer inspection of functions called from within xmalloc()
, it seems that I can safely fix that up in IDA by editing the function signature.
It looks like signatures for some standard library function definitions have not been picked up by IDA either. I don’t know of an automated way to get the definitions in, so I just edit these in IDA manually whenever the need arises.
In this function, I see that it expects some arguments:
if ( argc <= 1 )
return -1;
The way argc
and argv
work is best represented by the following snippet from documentation:
Invocation: ./a.out indatafile outdatafile
argc = 3
argv[0] --> ./a.out
argv[1] --> indatafile
argv[2] --> outdatafile
argv[argc] = (nil)
Therefore, if the condition is argc > 1
, it means there has to be at least 1 argument:
Invocation: ./a.out indatafile
argc = 2
argv[0] --> ./a.out
argv[1] --> indatafile
argv[argc] = (nil)
Fail cases all return -1
exit code, which will show up in the terminal as 255
, so my QEMU setup is correct.
After the check in the binary, a 32
byte buffer is allocated. Next, it is filled with 8 more pointers to buffers of 32
bytes, each of which gets initialized with 0xA
times 32
, so I created a corresponding structure in IDA:
idx1 = 0;
buf1 = xmalloc(0x20u);
while ( idx1 != 8 )
{
*(&buf1->field_0 + idx1) = xmalloc(0x20u);
memset(*(&buf1->field_0 + idx1++), 10, 0x20u);
}
buf1[1].field_0 = 0;
The first line after the loop doesn’t make sense though: buf1[1].field_0
points beyond the end of the allocated buffer, which could cause memory corruption. Time to check correctness of this line in assembly:
loop_exit ; r3 has idx
LDR R3, [R11,#idx1]
MOV R2, R3,LSL#2 ; r2 has idx << 2, which evals to 8<<2, meaning 32
LDR R3, [R11,#buf1] ; retrieve a ptr to buf1
ADD R2, R3, R2 ; r2 = r3+32; r2 = buf1+32;
MOV R3, #0 ; [buf1+32] = 0
STR R3, [R2] ; Store to Memory
MOV R3, #0 ; Rd = Op2
STR R3, [R11,#idx1] ; idx=0
MOV R3, #0x41 ; 'A' ; Rd = Op2
STR R3, [R11,#Achar] ; Store to Memory
B loc_839C ; Branch
Indeed, it looks like there is an off-by-one bug here and 4
bytes extra after the 32
byte structure will be overwritten with nulls.
Either way, the next few lines look very simple and promising:
idx2 = 0;
Achar = 'A';
while ( idx2 != 31 )
*(buf1->field_C + idx2++) = Achar++;
*(buf1->field_C + 31) = 0;
for ( idx3 = 0; argv[1][idx3]; ++idx3 )
{
if ( argv[1][idx3] != *(buf1->field_C + idx3) )
return -1;
}
return 1337;
A field in the struct is filled with 31 characters following A
, and a null-byte terminator is added after that.
Second loop then compares bytes in the first argument to bytes at corresponding index in this string inside a struct. It seems that the solution code should be:
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char const *argv[])
{
char Achar = 'A';
void * field_C = malloc(32);
int idx2 = 0;
while (idx2 != 31) {
*((char *)field_C + idx2++) = Achar++;
}
*((char *)field_C + 31) = 0;
printf("%s\n", field_C);
return 1337;
}
And the answer is:
user@debian-armhf:~$ ./diff2_bin "ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_"
user@debian-armhf:~$ echo $?
57
The off-by-one memory corruption bug was not needed after all :)