back to homepage

Reverse Engineering IOLI Crackmes

A crackme is an executable program that people reverse engineer for fun. The executable usually prompts the user for a password, and the game is figuring out what that password is by reverse engineering the executable. IOLI crackmes are a series of 10 introductory crackmes we'll be reverse engineering today. They look like this

$ ./crackme0x00
IOLI Crackme Level 0x00
Password: 346234
Invalid Password!
$ ./crackme0x00
IOLI Crackme Level 0x00
Password: 250382
Password OK :)

Our goal is to get the Password OK :) message for every program. You can download the crackmes at IOLI-crackme.tar.gz.

crackme0x00

The first one is trivial, we can use one of radare2's tools to get the strings of the program

$ rabin2 -z ./crackme0x00
[Strings]
nth paddr      vaddr      len size section type  string
―――――――――――――――――――――――――――――――――――――――――――――――――――――――
0   0x00000568 0x08048568 24  25   .rodata ascii IOLI Crackme Level 0x00\n
1   0x00000581 0x08048581 10  11   .rodata ascii Password:
2   0x0000058f 0x0804858f 6   7    .rodata ascii 250382
3   0x00000596 0x08048596 18  19   .rodata ascii Invalid Password!\n
4   0x000005a9 0x080485a9 15  16   .rodata ascii Password OK :)\n

We find the password is just 250382. If you don't have rabin2 you could've also used GNU strings.

$ ./crackme0x00
IOLI Crackme Level 0x00
Password: 250382
Password OK :)

crackme0x01

Taking a look at the disassembly shows that the only cmp instruction in the main function is comparing the constant 0x149a with a local variable on the stack.

$ objdump -d ./crackme0x01
...
080483e4 <main>:
 80483e4:        55                           push   %ebp
 80483e5:        89 e5                        mov    %esp,%ebp
 80483e7:        83 ec 18                     sub    $0x18,%esp
 80483ea:        83 e4 f0                     and    $0xfffffff0,%esp
 80483ed:        b8 00 00 00 00               mov    $0x0,%eax
 80483f2:        83 c0 0f                     add    $0xf,%eax
 80483f5:        83 c0 0f                     add    $0xf,%eax
 80483f8:        c1 e8 04                     shr    $0x4,%eax
 80483fb:        c1 e0 04                     shl    $0x4,%eax
 80483fe:        29 c4                        sub    %eax,%esp
 8048400:        c7 04 24 28 85 04 08         movl   $0x8048528,(%esp)
 8048407:        e8 10 ff ff ff               call   804831c <printf@plt>
 804840c:        c7 04 24 41 85 04 08         movl   $0x8048541,(%esp)
 8048413:        e8 04 ff ff ff               call   804831c <printf@plt>
 8048418:        8d 45 fc                     lea    -0x4(%ebp),%eax
 804841b:        89 44 24 04                  mov    %eax,0x4(%esp)
 804841f:        c7 04 24 4c 85 04 08         movl   $0x804854c,(%esp)
 8048426:        e8 e1 fe ff ff               call   804830c <scanf@plt>
 804842b:        81 7d fc 9a 14 00 00         cmpl   $0x149a,-0x4(%ebp)
 8048432:        74 0e                        je     8048442 <main+0x5e>
 8048434:        c7 04 24 4f 85 04 08         movl   $0x804854f,(%esp)
 804843b:        e8 dc fe ff ff               call   804831c <printf@plt>
 8048440:        eb 0c                        jmp    804844e <main+0x6a>
 8048442:        c7 04 24 62 85 04 08         movl   $0x8048562,(%esp)
 8048449:        e8 ce fe ff ff               call   804831c <printf@plt>
 804844e:        b8 00 00 00 00               mov    $0x0,%eax
 8048453:        c9                           leave
 8048454:        c3                           ret
 8048455:        90                           nop
...

0x149a in decimal is 5274 and that's the password.

crackme0x02

If you look at the disassembly again like last time you'll notice that it's doing some additions and multiplications on local variables to sort of obfuscate what the password is. It's only a few simple instructions, you could work through them manually and figure out what the password is but I think it's much easier to just use gdb. Jumping to the part where the comparison is taking place, we can print the value of the local variable it's comparing our input against.

Reading symbols from crackme0x02...
(No debugging symbols found in crackme0x02)
(gdb) start main
Temporary breakpoint 1 at 0x80483ea
Starting program: /home/kjc/IOLI/crackme0x02 main
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".

Temporary breakpoint 1, 0x080483ea in main ()
(gdb) disass main
Dump of assembler code for function main:
   0x080483e4 <+0>:    push   %ebp
   0x080483e5 <+1>:    mov    %esp,%ebp
   0x080483e7 <+3>:    sub    $0x18,%esp
=> 0x080483ea <+6>:    and    $0xfffffff0,%esp
   0x080483ed <+9>:    mov    $0x0,%eax
   0x080483f2 <+14>:   add    $0xf,%eax
   0x080483f5 <+17>:   add    $0xf,%eax
   0x080483f8 <+20>:   shr    $0x4,%eax
   0x080483fb <+23>:   shl    $0x4,%eax
   0x080483fe <+26>:   sub    %eax,%esp
   0x08048400 <+28>:   movl   $0x8048548,(%esp)
   0x08048407 <+35>:   call   0x804831c <printf@plt>
   0x0804840c <+40>:   movl   $0x8048561,(%esp)
   0x08048413 <+47>:   call   0x804831c <printf@plt>
   0x08048418 <+52>:   lea    -0x4(%ebp),%eax
   0x0804841b <+55>:   mov    %eax,0x4(%esp)
   0x0804841f <+59>:   movl   $0x804856c,(%esp)
   0x08048426 <+66>:   call   0x804830c <scanf@plt>
   0x0804842b <+71>:   movl   $0x5a,-0x8(%ebp)
   0x08048432 <+78>:   movl   $0x1ec,-0xc(%ebp)
   0x08048439 <+85>:   mov    -0xc(%ebp),%edx
   0x0804843c <+88>:   lea    -0x8(%ebp),%eax
   0x0804843f <+91>:   add    %edx,(%eax)
   0x08048441 <+93>:   mov    -0x8(%ebp),%eax
   0x08048444 <+96>:   imul   -0x8(%ebp),%eax
   0x08048448 <+100>:  mov    %eax,-0xc(%ebp)
   0x0804844b <+103>:  mov    -0x4(%ebp),%eax
   0x0804844e <+106>:  cmp    -0xc(%ebp),%eax
   0x08048451 <+109>:  jne    0x8048461 <main+125>
   0x08048453 <+111>:  movl   $0x804856f,(%esp)
   0x0804845a <+118>:  call   0x804831c <printf@plt>
   0x0804845f <+123>:  jmp    0x804846d <main+137>
   0x08048461 <+125>:  movl   $0x804857f,(%esp)
   0x08048468 <+132>:  call   0x804831c <printf@plt>
   0x0804846d <+137>:  mov    $0x0,%eax
   0x08048472 <+142>:  leave
   0x08048473 <+143>:  ret
End of assembler dump.
(gdb) b *0x0804844e
Breakpoint 2 at 0x804844e
(gdb) c
Continuing.
IOLI Crackme Level 0x02
Password: 636

Breakpoint 2, 0x0804844e in main ()
(gdb) x/d $ebp-0xc
0xffffd72c:    338724
(gdb)

So the password is 338724.

crackme0x03

Take a look at the disassembly

$ objdump -d ./crackme0x03
...
0804846e <test>:
 804846e:        55                           push   %ebp
 804846f:        89 e5                        mov    %esp,%ebp
 8048471:        83 ec 08                     sub    $0x8,%esp
 8048474:        8b 45 08                     mov    0x8(%ebp),%eax
 8048477:        3b 45 0c                     cmp    0xc(%ebp),%eax
 804847a:        74 0e                        je     804848a <test+0x1c>
 804847c:        c7 04 24 ec 85 04 08         movl   $0x80485ec,(%esp)
 8048483:        e8 8c ff ff ff               call   8048414 <shift>
 8048488:        eb 0c                        jmp    8048496 <test+0x28>
 804848a:        c7 04 24 fe 85 04 08         movl   $0x80485fe,(%esp)
 8048491:        e8 7e ff ff ff               call   8048414 <shift>
 8048496:        c9                           leave
 8048497:        c3                           ret

08048498 <main>:
 8048498:        55                           push   %ebp
 8048499:        89 e5                        mov    %esp,%ebp
 804849b:        83 ec 18                     sub    $0x18,%esp
 804849e:        83 e4 f0                     and    $0xfffffff0,%esp
 80484a1:        b8 00 00 00 00               mov    $0x0,%eax
 80484a6:        83 c0 0f                     add    $0xf,%eax
 80484a9:        83 c0 0f                     add    $0xf,%eax
 80484ac:        c1 e8 04                     shr    $0x4,%eax
 80484af:        c1 e0 04                     shl    $0x4,%eax
 80484b2:        29 c4                        sub    %eax,%esp
 80484b4:        c7 04 24 10 86 04 08         movl   $0x8048610,(%esp)
 80484bb:        e8 90 fe ff ff               call   8048350 <printf@plt>
 80484c0:        c7 04 24 29 86 04 08         movl   $0x8048629,(%esp)
 80484c7:        e8 84 fe ff ff               call   8048350 <printf@plt>
 80484cc:        8d 45 fc                     lea    -0x4(%ebp),%eax
 80484cf:        89 44 24 04                  mov    %eax,0x4(%esp)
 80484d3:        c7 04 24 34 86 04 08         movl   $0x8048634,(%esp)
 80484da:        e8 51 fe ff ff               call   8048330 <scanf@plt>
 80484df:        c7 45 f8 5a 00 00 00         movl   $0x5a,-0x8(%ebp)
 80484e6:        c7 45 f4 ec 01 00 00         movl   $0x1ec,-0xc(%ebp)
 80484ed:        8b 55 f4                     mov    -0xc(%ebp),%edx
 80484f0:        8d 45 f8                     lea    -0x8(%ebp),%eax
 80484f3:        01 10                        add    %edx,(%eax)
 80484f5:        8b 45 f8                     mov    -0x8(%ebp),%eax
 80484f8:        0f af 45 f8                  imul   -0x8(%ebp),%eax
 80484fc:        89 45 f4                     mov    %eax,-0xc(%ebp)
 80484ff:        8b 45 f4                     mov    -0xc(%ebp),%eax
 8048502:        89 44 24 04                  mov    %eax,0x4(%esp)
 8048506:        8b 45 fc                     mov    -0x4(%ebp),%eax
 8048509:        89 04 24                     mov    %eax,(%esp)
 804850c:        e8 5d ff ff ff               call   804846e <test>
 8048511:        b8 00 00 00 00               mov    $0x0,%eax
 8048516:        c9                           leave
 8048517:        c3                           ret
 8048518:        90                           nop
...

Clearly the test function is comparing its two arguments and printing something depending on whether their equal or not. If we look at the main function we see that it's calling the test function against its local variables -0xc(%ebp) and -0x4(%ebp). We can use gdb like last time to print the two arguments and one of them will be our password. Doing that yields 338724, which is the password and the same password as the last one.

crackme0x04

Looking at the disassembly we see that the main function is passing an argument to a check function. Most likely, that argument is our input and the password is calculated on the fly in the check function. Just looking at it, it appears to be using a quite a lot of loops and conditionals and it would be annoying and burdensome to puzzle everything together in assembly (at least for me, I'm not exactly experienced with it) so we can open up Ghidra—a very nice reverse engineering tool authored by the NSA—and look at the C decompilation it gives of the check function

void check(char *param_1)

{
  size_t sVar1;
  char local_11;
  uint local_10;
  int local_c;
  int local_8;
  
  local_c = 0;
  local_10 = 0;
  while( true ) {
    sVar1 = strlen(param_1);
    if (sVar1 <= local_10) {
      printf("Password Incorrect!\n");
      return;
    }
    local_11 = param_1[local_10];
    sscanf(&local_11,"%d",&local_8);
    local_c = local_c + local_8;
    if (local_c == 0xf) break;
    local_10 = local_10 + 1;
  }
  printf("Password OK!\n");
                    /* WARNING: Subroutine does not return */
  exit(0);
}

From this it's clear that we need to get local_c to equal 0xf, where local_c appears to be the sum of the digits of check's argument, our input. That is to say the password is just a number whose digits sum to 15—like 96, which is a valid password.

crackme0x05

If we look at the decompilation by Ghidra again, we see much of the same except it's calling another function parell if the digits sum to 0xf.

void parell(char *param_1)

{
  uint local_8;
  
  sscanf(param_1,"%d",&local_8);
  if ((local_8 & 1) == 0) {
    printf("Password OK!\n");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  return;
}

This function checks if its argument's first bit is not set, in other words that the number is even. In short, the password must be an even number whose digits sum to 16, like 88.

crackme0x06

This one is similar to the last one in structure, but check and parell now take two arguments: the input and the third argument to main

undefined4 main(undefined4 param_1,undefined4 param_2,undefined4 param_3)

{
  undefined local_7c [120];
  
  printf("IOLI Crackme Level 0x06\n");
  printf("Password: ");
  scanf("%s",local_7c);
  check(local_7c,param_3);
  return 0;
}

I suspected that param_3 had something to do with environmental variables and searching it up followed by some experimentation confirmed my suspicions . param_3 is char **envp, a null-terminated array of environmental variables represented as strings in the form of VAR=VAL. The check function hasn't changed much, but here's what the parell function looks like

void parell(char *input,char **envp)

{
  int iVar1;
  int local_c;
  uint local_8;
  
  sscanf(input,"%d",&local_8);
  iVar1 = dummy(local_8,envp);
  if (iVar1 != 0) {
    for (local_c = 0; local_c < 10; local_c = local_c + 1) {
      if ((local_8 & 1) == 0) {
        printf("Password OK!\n");
                    /* WARNING: Subroutine does not return */
        exit(0);
      }
    }
  }
  return;
}

The for loop is a red herring, the function does the same thing as it did last time but this time we must ensure that the dummy function returns a nonzero value. The dummy function looks like

undefined4 dummy(uint input,char **envp)

{
  int iVar1;
  int i;
  
  i = 0;
  do {
    if (envp[i] == (char *)0x0) {
      return 0;
    }
    iVar1 = strncmp(envp[i],"LOLO",3);
    i = i + 1;
  } while (iVar1 != 0);
  return 1;
}

It returns 1 if any of the environmental variables in envp starts with "LOL"—not necessarily "LOLO" because of the 3 in strncmp. We want it to return 1 so we just pass an environmental variable that begins with "LOL" and give it the same input as last time. It looks like this

$ LOL= ./crackme0x06
IOLI Crackme Level 0x06
Password: 88
Password OK!

crackme0x07

The first thing I did was input the exact same password as the last crackme

$ LOL= ./crackme0x07
IOLI Crackme Level 0x07
Password: 88
Password OK!

And it works. But let's reverse engineer it anyway and figure out how it works. The first thing we notice is the symbols have been stripped

$ file ./crackme0x07
./crackme0x07: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.9, stripped

If we run objdump -d we just get a huge blob of instructions in .text. No worries, from the top we see

08048400 <.text>:
 8048400:        31 ed                        xor    %ebp,%ebp
 8048402:        5e                           pop    %esi
 8048403:        89 e1                        mov    %esp,%ecx
 8048405:        83 e4 f0                     and    $0xfffffff0,%esp
 8048408:        50                           push   %eax
 8048409:        54                           push   %esp
 804840a:        52                           push   %edx
 804840b:        68 50 87 04 08               push   $0x8048750
 8048410:        68 e0 86 04 08               push   $0x80486e0
 8048415:        51                           push   %ecx
 8048416:        56                           push   %esi
 8048417:        68 7d 86 04 08               push   $0x804867d
 804841c:        e8 67 ff ff ff               call   8048388 <__libc_start_main@plt>
 8048421:        f4                           hlt
 8048422:        90                           nop
 8048423:        90                           nop
...

This asks __libc_start_main to start the main function. The address it pushes right before calling the function is where our main function is

...
 804867d:        55                           push   %ebp
 804867e:        89 e5                        mov    %esp,%ebp
 8048680:        81 ec 88 00 00 00            sub    $0x88,%esp
 8048686:        83 e4 f0                     and    $0xfffffff0,%esp
 8048689:        b8 00 00 00 00               mov    $0x0,%eax
 804868e:        83 c0 0f                     add    $0xf,%eax
 8048691:        83 c0 0f                     add    $0xf,%eax
 8048694:        c1 e8 04                     shr    $0x4,%eax
 8048697:        c1 e0 04                     shl    $0x4,%eax
 804869a:        29 c4                        sub    %eax,%esp
 804869c:        c7 04 24 d9 87 04 08         movl   $0x80487d9,(%esp)
 80486a3:        e8 10 fd ff ff               call   80483b8 <printf@plt>
 80486a8:        c7 04 24 f2 87 04 08         movl   $0x80487f2,(%esp)
 80486af:        e8 04 fd ff ff               call   80483b8 <printf@plt>
 80486b4:        8d 45 88                     lea    -0x78(%ebp),%eax
 80486b7:        89 44 24 04                  mov    %eax,0x4(%esp)
 80486bb:        c7 04 24 fd 87 04 08         movl   $0x80487fd,(%esp)
 80486c2:        e8 d1 fc ff ff               call   8048398 <scanf@plt>
 80486c7:        8b 45 10                     mov    0x10(%ebp),%eax
 80486ca:        89 44 24 04                  mov    %eax,0x4(%esp)
 80486ce:        8d 45 88                     lea    -0x78(%ebp),%eax
 80486d1:        89 04 24                     mov    %eax,(%esp)
 80486d4:        e8 e0 fe ff ff               call   80485b9 <exit@plt+0x1d1>
 80486d9:        b8 00 00 00 00               mov    $0x0,%eax
 80486de:        c9                           leave
 80486df:        c3                           ret
...

It does what we've seen before and it calls some check function again at 80485b9 <exit@plt+0x1d1>. I'll use Ghidra from now on. The check function looks like

void check(char *input,char **envp)

{
  size_t sVar1;
  int iVar2;
  char local_11;
  uint local_10;
  int local_c;
  uint local_8;
  
  local_c = 0;
  local_10 = 0;
  while( true ) {
    sVar1 = strlen(input);
    if (sVar1 <= local_10) break;
    local_11 = input[local_10];
    sscanf(&local_11,"%d",&local_8);
    local_c = local_c + local_8;
    if (local_c == 0x10) {
      parell(input,envp);
    }
    local_10 = local_10 + 1;
  }
  print_password_incorrect();
  iVar2 = dummy(local_8,envp);
  if (iVar2 != 0) {
    for (local_10 = 0; (int)local_10 < 10; local_10 = local_10 + 1) {
      if ((local_8 & 1) == 0) {
        printf("wtf?\n");
                    /* WARNING: Subroutine does not return */
        exit(0);
      }
    }
  }
  return;
}

The parell function looks similar to the one in the last crackme but in our check function there's an unreachable bit of code that prints "wtf?"

crackme0x08

Once again the password is the same as the last crackme.

$ LOL= ./crackme0x08
IOLI Crackme Level 0x08
Password: 88
Password OK!

Just taking a look at the disassembly, it looks very much like every other one we've cracked, maybe a new function or two.

crackme0x09

Once again the password is the same as the last crackme.

$ LOL= ./crackme0x09
IOLI Crackme Level 0x09
Password: 88
Password OK!

Once again the binary is stripped but it's not fun reverse engineering something when you've already got the answer. Maybe the author should've changed the passwords, these last couple crackmes have been almost disappointing.

Conclusion

These were really easy and simple. It was an underwhelming ending, crackme0x00 to crackme0x06 were all fun and original but crackme0x07 to crackme0x09 just reused the same answers.

I'll do more crackmes in the future, these are pretty fun. crackmes.one has many good crackmes. I have an account on that website, my username is kejcao.