Infosec Scribbles

August 3, 2019

Crackmes Series: ARM, part 1

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 first of many to come.

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. First challenge is of difficulty level 1 and is targeting ARM/Linux.

Running it

First, figure out what the binary is:

$ file diff1_crack 
diff1_crack: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.6.16, BuildID[sha1]=30a939bb626d5ec53b08ccc7ad6745b93bcce5cf, stripped
$ readelf -a diff1_crack 
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:               0x8591
  Start of program headers:          52 (bytes into file)
  Start of section headers:          4512 (bytes into file)
  Flags:                             0x5000002, Version5 EABI, <unknown>
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         9
  Size of section headers:           40 (bytes)
  Number of section headers:         27
  Section header string table index: 26

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        00008154 000154 000013 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            00008168 000168 000020 00   A  0   0  4
  [ 3] .note.gnu.build-i NOTE            00008188 000188 000024 00   A  0   0  4
  [ 4] .gnu.hash         GNU_HASH        000081ac 0001ac 000068 04   A  5   0  4
  [ 5] .dynsym           DYNSYM          00008214 000214 000110 10   A  6   1  4
  [ 6] .dynstr           STRTAB          00008324 000324 0000b3 00   A  0   0  1
  [ 7] .gnu.version      VERSYM          000083d8 0003d8 000022 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED         000083fc 0003fc 000040 00   A  6   2  4
  [ 9] .rel.dyn          REL             0000843c 00043c 000018 08   A  5   0  4
  [10] .rel.plt          REL             00008454 000454 000070 08   A  5  12  4
  [11] .init             PROGBITS        000084c4 0004c4 00000a 00  AX  0   0  4
  [12] .plt              PROGBITS        000084d0 0004d0 0000c0 04  AX  0   0  4
  [13] .text             PROGBITS        00008590 000590 0004d4 00  AX  0   0  4
  [14] .fini             PROGBITS        00008a64 000a64 000006 00  AX  0   0  4
  [15] .rodata           PROGBITS        00008a6c 000a6c 0000e4 00   A  0   0  4
  [16] .ARM.exidx        ARM_EXIDX       00008b50 000b50 000008 00  AL 13   0  4
  [17] .init_array       INIT_ARRAY      00010f04 000f04 000004 00  WA  0   0  4
  [18] .fini_array       FINI_ARRAY      00010f08 000f08 000004 00  WA  0   0  4
  [19] .jcr              PROGBITS        00010f0c 000f0c 000004 00  WA  0   0  4
  [20] .dynamic          DYNAMIC         00010f10 000f10 0000f0 08  WA  6   0  4
  [21] .got              PROGBITS        00011000 001000 000048 04  WA  0   0  4
  [22] .data             PROGBITS        00011048 001048 00000c 00  WA  0   0  4
  [23] .bss              NOBITS          00011058 001054 000010 00  WA  0   0  8
  [24] .comment          PROGBITS        00000000 001054 00002a 01  MS  0   0  1
  [25] .ARM.attributes   ARM_ATTRIBUTES  00000000 00107e 000031 00      0   0  1
  [26] .shstrtab         STRTAB          00000000 0010af 0000f0 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          0x000b50 0x00008b50 0x00008b50 0x00008 0x00008 R   0x4
  PHDR           0x000034 0x00008034 0x00008034 0x00120 0x00120 R E 0x4
  INTERP         0x000154 0x00008154 0x00008154 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.3]
  LOAD           0x000000 0x00008000 0x00008000 0x00b58 0x00b58 R E 0x8000
  LOAD           0x000f04 0x00010f04 0x00010f04 0x00150 0x00164 RW  0x8000
  DYNAMIC        0x000f10 0x00010f10 0x00010f10 0x000f0 0x000f0 RW  0x4
  NOTE           0x000168 0x00008168 0x00008168 0x00044 0x00044 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4
  GNU_RELRO      0x000f04 0x00010f04 0x00010f04 0x000fc 0x000fc R   0x1

 Section to Segment mapping:
  Segment Sections...
   00     .ARM.exidx 
   01     
   02     .interp 
   03     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .ARM.exidx 
   04     .init_array .fini_array .jcr .dynamic .got .data .bss 
   05     .dynamic 
   06     .note.ABI-tag .note.gnu.build-id 
   07     
   08     .init_array .fini_array .jcr .dynamic 

Dynamic section at offset 0xf10 contains 25 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x00000001 (NEEDED)                     Shared library: [ld-linux.so.3]
 0x0000000c (INIT)                       0x84c5
 0x0000000d (FINI)                       0x8a65
 0x00000019 (INIT_ARRAY)                 0x10f04
 0x0000001b (INIT_ARRAYSZ)               4 (bytes)
 0x0000001a (FINI_ARRAY)                 0x10f08
 0x0000001c (FINI_ARRAYSZ)               4 (bytes)
 0x6ffffef5 (GNU_HASH)                   0x81ac
 0x00000005 (STRTAB)                     0x8324
 0x00000006 (SYMTAB)                     0x8214
 0x0000000a (STRSZ)                      179 (bytes)
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000015 (DEBUG)                      0x0
 0x00000003 (PLTGOT)                     0x11000
 0x00000002 (PLTRELSZ)                   112 (bytes)
 0x00000014 (PLTREL)                     REL
 0x00000017 (JMPREL)                     0x8454
 0x00000011 (REL)                        0x843c
 0x00000012 (RELSZ)                      24 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
 0x6ffffffe (VERNEED)                    0x83fc
 0x6fffffff (VERNEEDNUM)                 2
 0x6ffffff0 (VERSYM)                     0x83d8
 0x00000000 (NULL)                       0x0

Relocation section '.rel.dyn' at offset 0x43c contains 3 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00011044  00000115 R_ARM_GLOB_DAT    00000000   __gmon_start__
00011058  00000514 R_ARM_COPY        00011058   __stack_chk_guard@GLIBC_2.4
00011060  00000b14 R_ARM_COPY        00011060   stdin@GLIBC_2.4

Relocation section '.rel.plt' at offset 0x454 contains 14 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0001100c  00000716 R_ARM_JUMP_SLOT   000084e4   abort@GLIBC_2.4
00011010  00000d16 R_ARM_JUMP_SLOT   000084f0   __libc_start_main@GLIBC_2.4
00011014  00000816 R_ARM_JUMP_SLOT   000084fc   signal@GLIBC_2.4
00011018  00000116 R_ARM_JUMP_SLOT   00000000   __gmon_start__
0001101c  00000216 R_ARM_JUMP_SLOT   00008518   ptrace@GLIBC_2.4
00011020  00000316 R_ARM_JUMP_SLOT   00008524   fgets@GLIBC_2.4
00011024  00000e16 R_ARM_JUMP_SLOT   00008530   system@GLIBC_2.4
00011028  00000f16 R_ARM_JUMP_SLOT   0000853c   memcpy@GLIBC_2.4
0001102c  00000416 R_ARM_JUMP_SLOT   00008548   alarm@GLIBC_2.4
00011030  00000916 R_ARM_JUMP_SLOT   00008554   printf@GLIBC_2.4
00011034  00000a16 R_ARM_JUMP_SLOT   00008560   malloc@GLIBC_2.4
00011038  00001016 R_ARM_JUMP_SLOT   0000856c   __stack_chk_fail@GLIBC_2.4
0001103c  00000616 R_ARM_JUMP_SLOT   00008578   puts@GLIBC_2.4
00011040  00000c16 R_ARM_JUMP_SLOT   00008584   exit@GLIBC_2.4

Unwind section '.ARM.exidx' at offset 0xb50 contains 1 entry:

0x8590: 0x1 [cantunwind]


Symbol table '.dynsym' contains 17 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     2: 00008518     0 FUNC    GLOBAL DEFAULT  UND ptrace@GLIBC_2.4 (2)
     3: 00008524     0 FUNC    GLOBAL DEFAULT  UND fgets@GLIBC_2.4 (2)
     4: 00008548     0 FUNC    GLOBAL DEFAULT  UND alarm@GLIBC_2.4 (2)
     5: 00011058     4 OBJECT  GLOBAL DEFAULT   23 __stack_chk_guard@GLIBC_2.4 (3)
     6: 00008578     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.4 (2)
     7: 000084e4     0 FUNC    GLOBAL DEFAULT  UND abort@GLIBC_2.4 (2)
     8: 000084fc     0 FUNC    GLOBAL DEFAULT  UND signal@GLIBC_2.4 (2)
     9: 00008554     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.4 (2)
    10: 00008560     0 FUNC    GLOBAL DEFAULT  UND malloc@GLIBC_2.4 (2)
    11: 00011060     4 OBJECT  GLOBAL DEFAULT   23 stdin@GLIBC_2.4 (2)
    12: 00008584     0 FUNC    GLOBAL DEFAULT  UND exit@GLIBC_2.4 (2)
    13: 000084f0     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.4 (2)
    14: 00008530     0 FUNC    GLOBAL DEFAULT  UND system@GLIBC_2.4 (2)
    15: 0000853c     0 FUNC    GLOBAL DEFAULT  UND memcpy@GLIBC_2.4 (2)
    16: 0000856c     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@GLIBC_2.4 (2)

Histogram for `.gnu.hash' bucket list length (total of 3 buckets):
 Length  Number     % of total  Coverage
      0  0          (  0.0%)
      1  0          (  0.0%)      0.0%
      2  0          (  0.0%)      0.0%
      3  0          (  0.0%)      0.0%
      4  1          ( 33.3%)     26.7%
      5  1          ( 33.3%)     60.0%
      6  1          ( 33.3%)    100.0%

Version symbols section '.gnu.version' contains 17 entries:
 Addr: 00000000000083d8  Offset: 0x0003d8  Link: 5 (.dynsym)
  000:   0 (*local*)       0 (*local*)       2 (GLIBC_2.4)     2 (GLIBC_2.4)  
  004:   2 (GLIBC_2.4)     3 (GLIBC_2.4)     2 (GLIBC_2.4)     2 (GLIBC_2.4)  
  008:   2 (GLIBC_2.4)     2 (GLIBC_2.4)     2 (GLIBC_2.4)     2 (GLIBC_2.4)  
  00c:   2 (GLIBC_2.4)     2 (GLIBC_2.4)     2 (GLIBC_2.4)     2 (GLIBC_2.4)  
  010:   2 (GLIBC_2.4)  

Version needs section '.gnu.version_r' contains 2 entries:
 Addr: 0x00000000000083fc  Offset: 0x0003fc  Link: 6 (.dynstr)
  000000: Version: 1  File: ld-linux.so.3  Cnt: 1
  0x0010:   Name: GLIBC_2.4  Flags: none  Version: 3
  0x0020: Version: 1  File: libc.so.6  Cnt: 1
  0x0030:   Name: GLIBC_2.4  Flags: none  Version: 2

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

Displaying notes found in: .note.gnu.build-id
  Owner                 Data size   Description
  GNU                  0x00000014   NT_GNU_BUILD_ID (unique build ID bitstring)
    Build ID: 30a939bb626d5ec53b08ccc7ad6745b93bcce5cf
Attribute Section: aeabi
File Attributes
  Tag_CPU_name: "7-A"
  Tag_CPU_arch: v7
  Tag_CPU_arch_profile: Application
  Tag_ARM_ISA_use: Yes
  Tag_THUMB_ISA_use: Thumb-2
  Tag_FP_arch: VFPv3-D16
  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
  Tag_ABI_HardFP_use: Deprecated
  Tag_DIV_use: Not allowed

This is a 32-bit ARM v7-A binary, built for Linux Kernel 2.6.16. I didn’t have a compatible system handy, so I did some studying and found this gem: https://people.debian.org/~aurel32/qemu/armhf/.

It’s a plug-n-play ARMv7 QEMU image. But when I tried to run it, I got this:

user@debian-armhf:~$ ./diff1_crack 
-bash: ./diff1_crack: No such file or directory

I’ve seen it before, and usually it happens when the system doesn’t have a compatible loader/dynamic linker. Looking at output of readelf again, I saw ld-linux.so.3 mentioned and searched for it on the QEMU system:

user@debian-armhf:~$ ls -al /lib/ld-linux*
lrwxrwxrwx 1 root root 30 Dec 30  2012 /lib/ld-linux-armhf.so.3 -> arm-linux-gnueabihf/ld-2.13.so
user@debian-armhf:~$ ls -al /lib/arm-linux-gnueabihf/
total 5892
drwxr-xr-x  4 root root  12288 Dec 16  2013 .
drwxr-xr-x 13 root root   4096 Aug  3 09:41 ..
-rwxr-xr-x  1 root root  93472 Dec 30  2012 ld-2.13.so
lrwxrwxrwx  1 root root     10 Dec 30  2012 ld-linux-armhf.so.3 -> ld-2.13.so
lrwxrwxrwx  1 root root     19 Dec 30  2012 ld-linux.so.3 -> ld-linux-armhf.so.3

Looks like it’s just missing a symlink. Let’s try that:

root@debian-armhf:~# ln -s /lib/arm-linux-gnueabihf/ld-linux.so.3 /lib/ld-linux.so.3
user@debian-armhf:~$ ./diff1_crack 
WELCOME TO ARM CRACK ME LEVEL 1 or MAYBE IT WAS 2????? GOOD LUCK
 NO PATCHING OR MODIFYING WHATSOEVER

The program then blocks for input and exits silently upon receiving any.

Step 1 complete: get the binary to run.

Entry point

At the entry point, I see a typical entry point for a binary compiled from C. There is a short setup step that will execute functions from .init_array section before entering main().

; Segment type: Pure code
AREA .text, CODE
; ORG 0x8590
CODE16


; Attributes: noreturn

EXPORT start
start
MOV.W           R11, #0 ; Rd = Op2
MOV.W           LR, #0  ; Rd = Op2
POP.W           {R1}    ; argc
MOV             R2, SP  ; ubp_av
PUSH.W          {R2}    ; stack_end
PUSH.W          {R0}    ; rtld_fini
LDR.W           R12, =(nullsub_1+1) ; Load from Memory
PUSH.W          {R12}   ; fini
LDR             R0, =(libc_main+1) ; main
LDR             R3, =(libc_init+1) ; init
BLX             __libc_start_main ; Branch with Link and Exchange (immediate address)
BLX             abort   ; Branch with Link and Exchange (immediate address)
; End of function start

At this point I invoke Hex-Rays to get the lay of the land.

int libc_main()
{
  malloc(12u);
  buf1_ptr = malloc(12u);
  buf2_ptr = malloc(12u);
  puts("WELCOME TO ARM CRACK ME LEVEL 1 or MAYBE IT WAS 2????? GOOD LUCK\n NO PATCHING OR MODIFYING WHATSOEVER");
  sub_8688(12, buf1_ptr, buf2_ptr, "`TU_KU_KRXMS");
  dest = malloc(12u);
  s = malloc(12u);
  memcpy(dest, "`TU_KU_KRXMS", 12u);
  v0 = memcpy(buf1_ptr, "`TU_KU_KRXMS", 12u);
  sub_862C(v0);
  fgets(s, 13, stdin);    // this is where we are. Read up to 12 chars.
  if ( sub_8738(s) != 12 )
    exit(1);
  if ( sub_86A8(dest, s) == 1 )
  {
    printf("YOU Sol... OOOPS YOU DIDNT");
    system("yes FAIL");
  }
  for ( i = 0; i <= 11; ++i )
    dest[i] = sub_877C(dest[i], 12, 12, 12);
  memcpy(buf2_ptr, s, 12u);
  v1 = sub_8874(buf1_ptr, buf2_ptr, "`TU_KU_KRXMS", dest);
  if ( v1 == 1 )
    puts("WAY TO GO SOLVED IT!");
  return v1;
}

From the looks of it, sub_8738 does a length check. Let’s verify that.

int __fastcall sub_8738(char *a1)
{
  int next_byte; // ST08_4
  int loop_ctr; // [sp+Ch] [bp+Ch]

  for ( loop_ctr = 0; a1[loop_ctr]; next_byte = a1[loop_ctr] + 0xF )
    ++loop_ctr;
  return loop_ctr;
}

Condition for loop continuation is a1[loop_ctr]. Remember, that this doesn’t have to be an explicit comparison, it just has to evaluate to true. Generally, such checks in assembly are implemented as a comparison of some value to zero. So a1[loop_ctr] evaluating to zero at some point will terminate the loop. A zero is also a null-byte. A null-byte is often used as a string terminator. So this loop goes through the supplied string one byte at a time, until it encounters a zero - it counts characters. However, there is still this weird action that is done on every iteration: next_byte = a1[loop_ctr] + 0xF. It doesn’t make sense, so could this be a decompiler issue?

When in doubt, I examine the disassembly.

In the first basic block, a loop counter is initialized to 0:

PUSH            {R7}    ; Push registers
SUB             SP, SP, #0x14 ; Rd = Op1 - Op2
ADD             R7, SP, #0 ; Rd = Op1 + Op2
STR             R0, [R7,#0x14+a1] ; Store to Memory
MOV.W           R3, #0  ; Rd = Op2
STR             R3, [R7,#0x14+loop_ctr] ; Store to Memory
B               loc_8762 ; Branch

Next, a character is loaded into r3 and checked against a null:

loc_8762
LDR             R3, [R7,#0x14+loop_ctr] ; Load from Memory
LDR             R2, [R7,#0x14+a1] ; Load from Memory
ADDS            R3, R2, R3 ; Rd = Op1 + Op2
LDRB            R3, [R3] ; loads a character into r3
CMP             R3, #0  ; checks if it's a null, proceeds to epilogue if yes
BNE             not_null_loop ; Branch

From here, there are two branches. The non-null one increments the counter and performs some more nonsense that never gets used again:

not_null_loop
LDR             R3, [R7,#0x14+loop_ctr] ; Load from Memory
ADD.W           R3, R3, #1 ; increments loop counter
STR             R3, [R7,#0x14+loop_ctr] ; puts it back
LDR             R3, [R7,#0x14+loop_ctr] ; loads it again (redundant, as well as the rest of this BB)
LDR             R2, [R7,#0x14+a1] ; loads a1 (which is a char *)
ADDS            R3, R2, R3 ; r3 = a1 + loop_ctr
LDRB            R3, [R3] ; load next byte from char *
STR             R3, [R7,#0x14+next_byte] ; redundant store-load op
LDR             R3, [R7,#0x14+next_byte] ; Load from Memory
ADD.W           R3, R3, #0xF ; adds 0xF to this byte
STR             R3, [R7,#0x14+next_byte] ; stores the resulting value overwriting what was there before

This is why the decompilation output in IDA was confusing. It doesn’t get used indeed, so for my use case, I can ignore what happens here after incrementing the loop counter.

The epilogue of the function pulls a value from a memory location into r3, moves it into r0, restores the registers and returns:

epilogue_867E
LDR             R3, [R7,#0x14+loop_ctr] ; Load from Memory
MOV             R0, R3  ; puts current counter value in r0, restores the registers and returns
ADD.W           R7, R7, #0x14 ; Rd = Op1 + Op2
MOV             SP, R7  ; Rd = Op2
POP             {R7}    ; Pop registers
BX              LR      ; Branch to/from Thumb mode
; End of function sub_8738

For some reason, the value was not read directly into r0, which has to be due to a convention or some limitation of ARM architecture. I decided not to follow up. Either way, the return value is in r0, which is standard for ARM/Linux binaries.

Back to main(). So this crackme expects precisely 12 characters to be given to it and exits if that is not the case:

fgets(s, 13, stdin);    // this is where we are. Read up to 12 chars.
  if ( strlen(s) != 12 )
    exit(1);

Next, it sends my input and a buffer filled out earlier in the function to sub_86A8. If that returns 1, the crackme will prank me by executing yes FAIL as a shell command:

  if ( sub_86A8(dest, s) == 1 )    // remember, dest has || `TU_KU_KRXMS ||
  {
    printf("YOU Sol... OOOPS YOU DIDNT");
    system("yes FAIL");
  }

Upon closer examination of sub_86A8, I see that it returns 1 if both strings passed to it are equal and 12 bytes long:

signed int __fastcall sub_86A8(char *other_str, char *input_str)
{
  signed int i; // [sp+Ch] [bp+Ch]

  for ( i = 0; ; ++i )
  {
    if ( !other_str[i] && !input_str[i] )       // both strings have next byte as 0 -> 1
      return 1;
    if ( !other_str[i] )                        // first string has next byte as 0 -> -1
      return -1;
    if ( !input_str[i] )                        // input string has next byte as 0 -> -2
      return -2;
    if ( other_str[i] != input_str[i] )
      return 9;
    if ( i > 10 )
      break;
  }
  return 1;
}

Therefore, entering `TU_KU_KRXMS into the prompt will trigger the prank.

Next, dest is overwritten with output of sub_877C, buf2 is overwritten with my input and sub_8874 performs the crackme solution check:

  for ( i = 0; i <= 11; ++i )
    dest[i] = sub_877C(dest[i], 12, 12);
  memcpy(buf2_ptr, s, 12u);
  v0 = sub_8874(buf1_ptr, buf2_ptr, "`TU_KU_KRXMS", dest);
  if ( v0 == 1 )
    puts("WAY TO GO SOLVED IT!");
  return v0;
}

First, I decided to look at the solution check.

int __fastcall sub_8874(void *buf1, char *my_input, char *static_key, char *dest)
{
  signed int cmp_static_dest; // [sp+10h] [bp+10h]
  char *_buf1; // [sp+18h] [bp+18h]
  signed int cmp_dest_myinput; // [sp+1Ch] [bp+1Ch]

  _buf1 = memcpy(buf1, dest, 12u);
  cmp_dest_myinput = custom_compare_strings(_buf1, my_input);
  cmp_static_dest = custom_compare_strings(static_key, dest);
  while ( cmp_dest_myinput <= 7 )
    cmp_dest_myinput += custom_compare_strings(_buf1, my_input);
  return cmp_static_dest - cmp_dest_myinput;
}                                               // buf1_ptr, buf2_ptr, "`TU_KU_KRXMS", dest

The equation to solve is: cmp_static_dest - cmp_dest_myinput = 1.

cmp_static_dest is computed based on hardcoded values in sub_877C. It is safe to assume that a modified copy of the hardcoded value will not be equal to that value, so this comparison is likely to result in the case of non-equal strings without me even looking at sub_877C. A quick look at the comparison function earlier shows that this would result in a return value of 9.

The equation becomes: 9 - cmp_dest_myinput = 1.

cmp_dest_myinput is a value based on my input and needs to be 8 for me to solve this challenge. There is another loop here that means it will always be 8 after the loop, as long as I provide the input this code expects.

However, my string here will be compared to a value based on computations in sub_877C. Looks like the author wants me to calculate that value, provide it to the binary and that will be the solution.

This is what that function does:

int __fastcall sub_877C(char a1, int a2, int a3)
{
  if ( a1 > 0xA )
    a3 = a1;
  return a3 + 20;
}

For character codes above 0x0A (newline), it will return that character value incremented by 20. 0x0A is a character code smaller than that of any printable character, so for my purposes, I can ignore this condition and just increment all characters in the static string by 20. Solution code:

#include <stdio.h>

int main(int argc, char const *argv[])
{
    const char base[12] = "`TU_KU_KRXMS";
    char solution[12];

    for (int i = 0; i < 12; i++) {
        solution[i] = base[i] + 20;
    }

    printf("Solution: %s\n", solution);

    return 0;
}

And the answer is:

$ ./solution1 
Solution: this_is_flag
user@debian-armhf:~$ ./diff1_crack
WELCOME TO ARM CRACK ME LEVEL 1 or MAYBE IT WAS 2????? GOOD LUCK
 NO PATCHING OR MODIFYING WHATSOEVER
this_is_flag
WAY TO GO SOLVED IT!