Skip to content

Modern Binary Exploitation Labs: Reversing crackme challenges (levels 0-3)

Introduction

Modern Binary Exploitation is an undergraduate course released for free by RPISec. The course content covers basic and intermediate topics, and describes itself as follows:

Modern Binary Exploitation will focus on teaching practical offensive security skills in binary exploitation and reverse engineering. Through a combination of interactive lectures, hands on labs, and guest speakers from industry, the course will offer students a rare opportunity to explore some of the most technically involved and fascinating subjects in the rapidly evolving field of security.

The course will start off by covering basic x86 reverse engineering, vulnerability analysis, and classical forms of Linux-based userland binary exploitation. It will then transition into protections found on modern systems (Canaries, DEP, ASLR, RELRO, Fortify Source, etc) and the techniques used to defeat them. Time permitting, the course will also cover other subjects in exploitation including kernel-land and Windows based exploitation.

The first set of labs involve “Tools and Basic Reverse Engineering”, and include a set of crackme exercises. In the rest of this article I’ll be walking through the process of reversing the files in order to determine the solution.

Tools

Radare2

Radare is a unix-like reverse engineering framework and commandline tools http://www.radare.org/

GDB (with pwndbg)

GDB is a standard tool for debugging/exploit development, originally I was using it with the PEDA toolkit. Recently I discovered that pwndbg actually provides a lot more useful functionality and a consistent interface, so it’s a worthy addition to any RE toolkit.

Snowman

Snowman is a native code to C/C++ decompiler. Although it’s not perfect, it can provide some useful analysis of control flow at the least https://derevenets.com.

Crackme0x00a

First binary. When executed, we’re greeted with the following prompt:

Enter password: hi
Wrong!
Enter password: yes
Wrong!
Enter password: no
Wrong!
Enter password: password
Wrong!

So we need to figure out what the password is somehow. Let’s load this one into radare2 and see what kind of output comes out. Some useful functions for radare2 are ‘aa’ (analyze all) and ‘pdf’ (print decompiled function). Let’s take a look at the main function:

I’m going to go into the most detail on the first few crackmes, with progressively less of the basics as we go through. With some basic inspection it’s possible to see what this binary is doing.

Knowing how function calls work we can see that this code is using the scanf() function with the parameter %d, for instance.
Given that we’re looking for a password, and this code contains a call to strcmp (the standard C library to compare two strings) it seems likely that this is how the password is being checked against our input. There’s now a few ways of figuring out this password, each of which I’ll walk through as this illustrates the process nicely. Here’s the disassembled call to strcmp.

Method 1: Look through objdump

Looking at the disassembled instructions for strcmp it’s evidence the symbols are still present, and the password looks like it’s stored in sym.pass.1685. Running objdump -D on crackme0x00a and looking for pass.1685 turns up this:

0804a024 :
 804a024:       67 30 30                xor    %dh,(%bx,%si)
 804a027:       64 4a                   fs dec %edx
 804a029:       30 42 21                xor    %al,0x21(%edx)
 804a02c:       00 00                   add    %al,(%eax)

The second column is the data, and seems to be a null-terminated 8 character string (two hex digits = one byte) consisting of 67 30 30 64 4a 30 42 21. Mapped to decimal digits these become 103, 48, 48, 100, 74, 48, 66, 33, which are the ASCII codes for g 0 0 d J 0 B !. Which turns out to be the password!

Method 2: Check output of ‘strings’

`strings` is a small system utility that prints strings stored in binaries. Since we only want to see what’s in the data section the -d switch is helpful, resulting in this output:

$ strings -d ./crackme0x00a 
/lib/ld-linux.so.2
__gmon_start__
libc.so.6
_IO_stdin_used
__isoc99_scanf
puts
__stack_chk_fail
printf
strcmp
__libc_start_main
GLIBC_2.7
GLIBC_2.4
GLIBC_2.0
PTRh
D$,1
T$,e3
UWVS
[^_]
Enter password: 
Congrats!
Wrong!
;*2$"
g00dJ0B!

Method 3: Set a breakpoint at strcmp and examine in GDB

GDB makes it very easy to view code as it’s happening. Breakpoints can be set in the code, which are the “interesting” parts where we want to stop and examine what’s going on. A really useful breakpoint might be the strcmp function, because then we can see the two arguments being passed (our string and the target string). Starting up GDB for crackme0x00a and using the `break *0x0804852a` command, we can now execute `run` and should see something like this:

The lower third contains a visualization of the stack, and the top two items? Our comparison string and the password.

crackme0x00b

Let’s check the main() function for this one with the same commands as before: ‘aa’ followed by ‘pdf’:

Being able to map out the pseudocode for disassembled instructions in your head can be useful, since this crackme is similar to the last let’s start out with the pseudocode for the one we already solved (note, I’m not an expert at this by any means, I’m learning as I go):

main() {
  printf("Enter password: ");
  password = scanf("%s");
  while (strcmp(password, stored_password) != 0) {
    puts "Wrong!"
    password = scanf("%s");
  }
  puts "Congrats!"
}

But in our new crackme, the call is instead to wcscmp, and the scanf function uses %ls instead of %s. A little bit of research suggests that wcscmp is strcmp but for “wide” characters, and that %ls reads a “Pointer to wchar_t string”. So we’re dealing with something really similar to the first example but for wide characters. So we’re looking at:

main() {
  printf("Enter password: ");
  password = scanf("%ls");
  while (wcscmp(password, stored_password) != 0) {
    puts "Wrong!"
    password = scanf("%ls");
  }
  puts "Congrats!"
}

Running `strings` on the file doesn’t particularly help either. But method #1 from the previous example can work. Finding the relevant section in the objdump yields the following:

0804a040 :
 804a040:       77 00                   ja     804a042 
 804a042:       00 00                   add    %al,(%eax)
 804a044:       30 00                   xor    %al,(%eax)
 804a046:       00 00                   add    %al,(%eax)
 804a048:       77 00                   ja     804a04a 
 804a04a:       00 00                   add    %al,(%eax)
 804a04c:       67 00 00                add    %al,(%bx,%si)
 804a04f:       00 72 00                add    %dh,0x0(%edx)
 804a052:       00 00                   add    %al,(%eax)
 804a054:       65 00 00                add    %al,%gs:(%eax)
 804a057:       00 61 00                add    %ah,0x0(%ecx)
 804a05a:       00 00                   add    %al,(%eax)
 804a05c:       74 00                   je     804a05e 
 804a05e:       00 00                   add    %al,(%eax)
 804a060:       00 00                   add    %al,(%eax)

The wikipedia page for wchar_t states that “the width of wchar_t is compiler-specific and can be as small as 8 bits”. It looks here like they’re 32bits (reading down the second column). I did a quick test on my own system using the following program:

#include "wchar.h"

int main() {
  printf("%d", sizeof(wchar_t));
}

which outputs 4 (bytes), and so 32 bits. The bytes end up being 77 30 77 67 72 65 61 74, which translated into ASCII codes gives: “w0wgreat”. And this is the password!

crackme0x01

Our disassembled main() function:

The important part of this program is here:

Giving us a proposed code structure of something like the following:

main() {
  printf("IOLI Crackme Level 0x01\n");
  printf("Password: ");
  password = scanf("%d");
  if (password == 5247) {
    printf("Password OK!");
  } else {
    printf("Invalid Password");
  }
  return 0;
}

I mentioned the snowman decompiler in the intro, running crackme0x01 through the ‘nocode’ tool it supplies gives us the following (I’ve found nocode useful so far for mapping out the structure of a binary and figuring out the control flow):

int32_t main() {
    void* v1;
    void* v2;
    int32_t v3;

    fun_804831c("IOLI Crackme Level 0x01\n", v1);
    fun_804831c("Password: ", v2);
    fun_804830c("%d", reinterpret_cast(__zero_stack_offset()) - 4 - 4);
    if (v3 == 0x149a) {
        fun_804831c("Password OK :)\n", reinterpret_cast(__zero_stack_offset()) - 4 - 4);
    } else {
        fun_804831c("Invalid Password!\n", reinterpret_cast(__zero_stack_offset()) - 4 - 4);
    }
    return 0;
}

The scanf reads in a single decimal digit (%d), and then performs a cmp against some value (0x149a), jumping to “Password OK” if it’s equal. Therefore if we try the number 5274 it should work!

crackme0x02

Starting out with the radare2 output for the main function again:

Much of the structure is similar to the previous exercises, the part that’s different is the important segment with the imul operation here:

What might the pseudocode for this look like? Remember that the layout of the stack frame in IA32 architecture keeps local variables at locations below the stack pointer, so the definitions up the top are referencing two local variables at ebp-0x8 and ebp-0xc and assigning the values 0x5a (90) and 0x1ec (492) respectively. I’ve annotated the disassembly with what operations the parts of the code correspond to:

0x0804842b    c745f85a000. mov dword [ebp-0x8], 0x5a ; a = 90
0x08048432    c745f4ec010. mov dword [ebp-0xc], 0x1ec ; b = 492
0x08048439    8b55f4       mov edx, [ebp-0xc]
0x0804843c    8d45f8       lea eax, [ebp-0x8]
0x0804843f    0110         add [eax], edx ; a = a + b
0x08048441    8b45f8       mov eax, [ebp-0x8]
0x08048444    0faf45f8     imul eax, [ebp-0x8] ; a = a * a
0x08048448    8945f4       mov [ebp-0xc], eax
0x0804844b    8b45fc       mov eax, [ebp-0x4]
0x0804844e    3b45f4       cmp eax, [ebp-0xc]  ; if (password == a)
0x08048451    750e         jnz 0x8048461

The password is compared with the value of ((90 + 492) * (90 + 492)) which results in 338724, which is the password!.
Interestingly, running this code through snowman resulted in some of the excess operations being folded and optimised out. The function ends up being:

int32_t main() {
    void* v1;
    void* v2;
    int32_t v3;

    fun_804831c("IOLI Crackme Level 0x02\n", v1);
    fun_804831c("Password: ", v2);
    fun_804830c("%d", reinterpret_cast(__zero_stack_offset()) - 4 - 4);
    if (v3 != 0x52b24) {
        fun_804831c("Invalid Password!\n", reinterpret_cast(__zero_stack_offset()) - 4 - 4);
    } else {
        fun_804831c("Password OK :)\n", reinterpret_cast(__zero_stack_offset()) - 4 - 4);
    }
    return 0;
}

Spoiler alert! The hex value 0x52b24 corresponds to the decimal value of 338724.

crackme0x03

This one is extremely similar to the last. In fact, the solution is the same. When disassembling the binary with radare2 you might notice a slight difference. Now, instead of directly comparing the password against the stored password in the main() function, it calls a test() function that looks like this:

The structure of this code shouldn’t really be too difficult to understand, here’s what it might look like in pseudocode:

test(a, b) {
  if (a == b) {
    shift("SdvvzrugRN")
  } else {
    shift("LqydolgSdvvzrug")
  }
}

The fact that the function is named “shift” and that it contains what seems like random letters should be a clue, as is the structure of the test() function. I suspect this is using a simple caesar cipher and rotating the letters. I was looking for a quick way to test this theory, so I hacked up some ruby to rotate the characters in the string and see if I got anything interesting.

require 'pp'

str = "LqydolgSdvvzrug"

alpha = ('a'..'z').to_a + ('A'..'Z').to_a

1.upto(26) do |i|
  pp str.split("").map { |s| (s.ord - i).chr }.join
end

The resulting output is:

"KpxcnkfRcuuyqtf"
"JowbmjeQbttxpse"
"InvalidPassword"
"Hmu`khcO`rrvnqc"
"Glt_jgbN_qqumpb"
[..truncated..]

So we’re looking at strings rotated 3 places to the left. None of this was really necessary, but there you go. I hope you enjoyed this walkthrough, I’ve split the overall guide into several sections else they can get pretty long, so look forward to more soon!.

Published inReverse EngineeringWalkthroughs

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *