THE

SPRAWL

  •  
  •  
  •  
  • Crackmes.de has a nice collection of crackmes, fun and educational challenges useful for honing your reversing skills. Looking at the latest submissions section there was a recently published Crackme by nlxx rated at difficulty 2. In this guide I will go over the static analysis based solution to this crackme and explain how to write a key generator.

    The Challenge

    The crackme consists of a single executable crackMe.exe. Running the executable pops up a text console asking for a password. When you enter a random string (e.g. 12345) the application responds with the Fail :( message after a brief delay:

    Enter the password: 12345
    
    You typed "12345", checking....
    Fail :(
    

    A quick look at the binary itself with PEBear did not reveal any signs of packing as all of the standard segments (.text, .data, etc.) were present and contained unobfuscated instructions and data.

    Static Analysis

    I have loaded the executable in IDA which was able to immediately locate the main() function. The Signatures subview indicated a successfully applied GCC (mingw/cygwin) v3.4 runtime signature which explains why main() was detected so easily and will simplify further analysis. Furthermore, the crackme was developed in C++ and as a result many of the function names have the exact names and parameter types encoded in them. Here is an example call from main() to an interesting function:

    .text:00401CD5 mov     dword ptr [esp], offset aEnterThePasswo ; "Enter the password: "
    .text:00401CDC call    _printf
    .text:00401CE1 lea     eax, [ebp+input_string]
    .text:00401CE7 mov     [esp], eax      ; char *
    .text:00401CEA call    _gets
    ...
    .text:00401D68 mov     [esp], eax      ; char_string
    .text:00401D6B call    __Z9checkPassPc ; checkPass(char *)
    

    In the disassembly above, the crackme queries a user to enter a password and passes the collected string [ebp+intput_string] to checkPass(). Here you can see an example of C++ function mangling (__Z9checkPassPc) which reveals a single char * parameter to the function checkPass. In case your disassembler does not perform demangling automatically, you could use c++filt to achieve the same result:

    $ c++filt -_ __Z9checkPassPc
    checkPass(char*)
    

    Continuing with the analysis, It appears all of the logic is contained inside checkPass() so let's look at it in depth to figure out the exact algorithm used to generate valid passwords.

    NOTE: In the interest of saving space I will skip disassembly irrelevant to the algorithm; however, I encourage you to look at the function yourself in more detail.

    .text:004013C2 ; int __cdecl checkPass(char *password)
    .text:004013C2 public __Z9checkPassPc
    .text:004013C2 __Z9checkPassPc proc near
    ...
    .text:004013C2 password= dword ptr  8
    .text:004013C2
    .text:004013C2 push    ebp
    .text:004013C3 mov     ebp, esp
    .text:004013C5 push    edi
    .text:004013C6 push    esi
    .text:004013C7 push    ebx
    .text:004013C8 sub     esp, 14Ch       ; allocate 332 bytes on the stack
    

    The function starts with a standard prologue, preserves registers that are going to be modified on the stack, and allocates 332 bytes on the stack for the local variables. Notice that I have renamed the first argument arg0 to password for the sake of clarity. I would highly recommend that you rename the variables as you go along to make the analysis process easier.

    After some initialization junk included by the compiler we have the first real test:

    .text:004014DF mov     eax, [ebp+password]
    .text:004014E2 mov     [esp], eax      ; char *
    .text:004014E5 mov     [ebp+var_E8], 0Ah
    .text:004014EF call    _strrev         ; reverse password in place
    .text:004014F4 mov     [esp+4], eax    ; char *
    .text:004014F8 lea     eax, [ebp+rev_password]
    .text:004014FB mov     [esp], eax      ; int
    .text:004014FE call    __ZNSsaSEPKc    ; std::operator=()
    .text:004014FE                        ; rev_password = strrev(password)
    .text:00401503 mov     eax, [ebp+password]
    .text:00401506 mov     [esp], eax      ; char *
    .text:00401509 call    _strrev         ; reverse password back to normal
    .text:0040150E mov     [esp+4], eax    ; char *
    .text:00401512 lea     eax, [ebp+rev_password]
    .text:00401515 mov     [esp], eax      ; int
    .text:00401518 call    __ZSteqIcSt11char_traitsIcESaIcEEbRKSbIT_T0_T1_EPKS3_ ; password == rev_reverse
    .text:0040151D test    al, al
    .text:0040151F jz      short loc_40154D; if (password != rev_reverse)
    

    The provided password string is reversed in place using strrev(). Next the reversed value is preserved in the [ebp+rev_password] variable, using the assignment operator operator=(), and compare it against the original password after reversing it back to the original. If the two strings are equal we are redirected to a block which prints "Close, but wrong.\n" and exists. I guess the author wants to make sure strings with all of the same characters (e.g. aaaaaaaaa) fail early with a friendly error message.

    In case where the two strings are not equal we proceed to the next test:

    .text:0040154D loc_40154D:
    .text:0040154D mov     eax, [ebp+password]
    .text:00401550 mov     [esp], eax      ; char *
    .text:00401553 call    _strlen
    .text:00401558 mov     [ebp+var_4C], eax
    .text:0040155B cmp     [ebp+var_4C], 0Ah 
    .text:0040155F jnz     loc_401B9B ; if (strlen(char_string) == 10)
    

    The above simply runs strlen() on the original input string and makes sure that the string is exactly 10 characters long. If jnz succeedes (indicating the string length is other than 10), then we are redirected to the fail() function and exit.

    Let's proceed to the next challenge:

    .text:00401565 lea     edx, [ebp+substr1]
    .text:00401568 mov     dword ptr [esp+0Ch], 5 ; pos
    .text:00401570 mov     dword ptr [esp+8], 1 ; count
    .text:00401578 lea     eax, [ebp+rev_password]
    .text:0040157B mov     [esp+4], eax
    .text:0040157F mov     [esp], edx
    .text:00401582 mov     [ebp+var_E8], 0Ah
    .text:0040158C call    __ZNKSs6substrEjj ; std::string::substr(uint,uint)
    .text:0040158C                         ; substr1 = rev_password.substr(1, 5)
    .text:00401591 sub     esp, 4
    .text:00401594 lea     edx, [ebp+substr2]
    .text:00401597 mov     dword ptr [esp+0Ch], 9 ; pos
    .text:0040159F mov     dword ptr [esp+8], 6 ; count
    .text:004015A7 lea     eax, [ebp+rev_password]
    .text:004015AA mov     [esp+4], eax
    .text:004015AE mov     [esp], edx
    .text:004015B1 mov     [ebp+var_E8], 9
    .text:004015BB call    __ZNKSs6substrEjj ; std::string::substr(uint,uint)
    .text:004015BB                         ; substr2 = rev_password.substr(6, 9)
    .text:004015C0 sub     esp, 4
    .text:004015C3 lea     edx, [ebp+substr3]
    .text:004015C6 mov     dword ptr [esp+0Ch], 4 ; pos
    .text:004015CE mov     dword ptr [esp+8], 7 ; count
    .text:004015D6 lea     eax, [ebp+rev_password]
    .text:004015D9 mov     [esp+4], eax
    .text:004015DD mov     [esp], edx
    .text:004015E0 mov     [ebp+var_E8], 8
    .text:004015EA call    __ZNKSs6substrEjj ; std::string::substr(uint,uint)
    .text:004015EA                         ; substr3 = rev_password.substr(7, 4)
    .text:004015EF sub     esp, 4
    

    Now the crackme begins doing something interesting to the reversed password string, rev_password. Using the std::string::substr() function it creates three substrings. Here is a pseudo-code equivalent to the above:

    substr1 = rev_password.substr(1, 5);
    substr2 = rev_password.substr(6, 9);
    substr3 = rev_password.substr(7, 4);
    

    Notice that a lot of the slices exceed the range of the input string. For example rev_password.substr(6, 9) will attempt to extract 9 characters starting at position 6; however, because the total length is only 10 characters, substr2 will only received the last 4 characters. At this point, I found it useful to enumerate the exact password indices that will be present in each substring. Remember that we are dealing with the reverse version of the original password.

    rev_password = [9] [8] [7] [6] [5] [4] [3] [2] [1] [0]
    substr1 = [8] [7] [6] [5] [4]
    substr2 = [3] [2] [1] [0]
    substr3 = [2] [1] [0]
    

    With the substrings clearly defined let's see how they are used:

    .text:004015F2 lea     edx, [ebp+concat2]
    .text:004015F8 mov     [ebp+concat2_ptr], edx
    .text:004015FE lea     eax, [ebp+concat1]
    .text:00401604 lea     edx, [ebp+substr2]
    .text:00401607 mov     [esp+8], edx
    .text:0040160B lea     edx, [ebp+substr3]
    .text:0040160E mov     [esp+4], edx
    .text:00401612 mov     [esp], eax
    .text:00401615 mov     [ebp+var_E8], 7
    .text:0040161F call    __ZStplIcSt11char_traitsIcESaIcEESbIT_T0_T1_ERKS6_S8_ ;
    .text:0040161F                         ; std::operator+()
    .text:0040161F                         ; concat1 = substr3 + substr2
    .text:00401624 sub     esp, 4
    .text:00401627 lea     edx, [ebp+concat1]
    .text:0040162D lea     eax, [ebp+substr1]
    .text:00401630 mov     [esp+8], eax
    .text:00401634 mov     [esp+4], edx
    .text:00401638 mov     eax, [ebp+concat2_ptr]
    .text:0040163E mov     [esp], eax
    .text:00401641 mov     [ebp+var_E8], 6
    .text:0040164B call    __ZStplIcSt11char_traitsIcESaIcEESbIT_T0_T1_ERKS6_S8_ ;
    .text:0040164B                         ; std::operator+()
    .text:0040164B                         ; concat2 = concat1 + substr1
    .text:00401650 sub     esp, 4
    

    In the above disassembly the three substrings are concatenated using std::operator+() as follows:

    concat1 = substr3 + substr2
    concat2 = concat1 + substr1
    

    So if we write up the exact indices from the reversed password it is going to look as follows:

    concat1 = [2] [1] [0] [3] [2] [1] [0]
    concat2 = [2] [1] [0] [3] [2] [1] [0] [8] [7] [6] [5] [4]
    

    Finally the result of the second concatenation is assigned to the original rev_password variable:

    .text:00401653 lea     eax, [ebp+concat2]
    .text:00401659 mov     [esp+4], eax
    .text:0040165D lea     eax, [ebp+rev_password]
    .text:00401660 mov     [esp], eax
    .text:00401663 mov     [ebp+var_E8], 5
    .text:0040166D call    __ZNSsaSERKSs   ; std::operator=()
    .text:0040166D                         ; rev_password = concat2
    .text:00401672 jmp     short loc_4016A6
    

    Now that the original password string is reversed and re-arranged, the crackme starts to check for specific values:

    .text:0040170D mov     [ebp+flag_0], 0 ; flag0 = 0
    .text:00401714 lea     edx, [ebp+strtest1]
    .text:0040171A mov     dword ptr [esp+0Ch], 1
    .text:00401722 mov     dword ptr [esp+8], 0
    .text:0040172A lea     eax, [ebp+rev_password]
    .text:0040172D mov     [esp+4], eax
    .text:00401731 mov     [esp], edx
    .text:00401734 call    __ZNKSs6substrEjj ; std::string::substr(uint,uint)
    .text:00401734                         ; strtest1 = rev_password.substr(0, 1)
    .text:00401739 sub     esp, 4
    .text:0040173C mov     [ebp+flag_1], 0
    .text:00401746 mov     [ebp+flag_2], 0
    .text:00401750 mov     [ebp+flag_3], 0
    .text:0040175A lea     eax, [ebp+strtest1]
    .text:00401760 mov     dword ptr [esp+4], offset asc_44301C ; "x"
    .text:00401768 mov     [esp], eax      ; int
    .text:0040176B mov     [ebp+var_E8], 1
    .text:00401775 call    __ZSteqIcSt11char_traitsIcESaIcEEbRKSbIT_T0_T1_EPKS3_ ;
    .text:00401775                         ; std::operator==()
    .text:00401775                         ; if (strtest1 == "x")
    .text:0040177A test    al, al
    .text:0040177C jz      loc_401970
    

    In the above disassembly the very first character from the rev_password string (remember it is now the result of two concatenations) is extracted and compared against the character "x". Thus we know that whatever index is stored at the very beginning of the heavily manipulated rev_password must hold the character x as follows:

    'x'             'x'
    [2] [1] [0] [3] [2] [1] [0] [8] [7] [6] [5] [4]
    

    Subsequent disassembly blocks follow the exact same pattern and allow us to deduce the expected content of rev_password:

    'x'         'b' 'x'         'z' 'z' 'h' 'a' 'x'   
    [2] [1] [0] [3] [2] [1] [0] [8] [7] [6] [5] [4]
    

    If all of the above conditions are satisfied, we are branched into the coveted success block:

    .text:00401A99 cmp     [ebp+flag_0], 0  ; flag0 is set once all tests are passed
    .text:00401AA0 jz      short loc_401AB8 ; if (flag0)
    ...
    .text:00401AA2 mov     dword ptr [esp+4], offset aSuccess ; "Success!\n"
    .text:00401AAA mov     dword ptr [esp], offset __ZSt4cout ; int
    .text:00401AB1 call    __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc ; 
    .text:00401AB1                         ; std::operator<<()
    .text:00401AB6 jmp     short loc_401AF8
    

    This should give us a enough information about the algorithm necessary to develop the solution and a key generator.

    Keygen

    With the information learned from the static analysis, we can figure out the required mask for valid passwords:

            'x' 'b' 'x' 'a' 'h' 'z' 'z'
    [0] [1] [2] [3] [4] [5] [6] [7] [8] [9]
    

    Thus a password such as xxxbxahzzz should work:

    Enter the password: xxxbxahzzz
    
    You typed "xxxbxahzzz", checking....
    Success!
    
    The password is xxxbxxxzzhax
    

    Notice the final string displays the password in its transformed form as per the password mangling algorithm above.

    You can cook up a simple python one-liner to generate valid passwords for this crackme:

    >>> import string, random
    >>> print "%s%sxbxahzz%s" % tuple([random.choice(string.printable[:-6]) for i in range(3)])
    {[xbxahzz`
    >>> print "%s%sxbxahzz%s" % tuple([random.choice(string.printable[:-6]) for i in range(3)])
    bCxbxahzza
    >>> print "%s%sxbxahzz%s" % tuple([random.choice(string.printable[:-6]) for i in range(3)])
    %)xbxahzzF
    

    External Links and References

    Special Note

    Thanks Crackmes.de for maintaining the great collection and nlxx for a fun crackme. I am hooked ;-).

    Published on January 30th, 2014 by iphelix

    sprawlsimilar

    open security training - introduction to re - bomb lab secret phase

    A walkthrough for the Secret Phase of the Bomb Lab covered in Open Security Training's Introduction to Reverse Engineering class. Read more.

    open security training - introduction to software exploits - uninitialized variable overflow

    Open Security Training's Introduction to Software Exploits course has a number of vulnerability examples designed to illustrate unconventional exploitation techniques. One such example is an uninitialized variable condition which may be exploitable under certain conditions. The following walkthrough goes into the exact exploitation steps for this class of vulnerabilities. Read more.

    open security training - introduction to software exploits - off-by-one

    A walkthrough for the Off-by-One exploit in the Open Security Training - Introduction to Software Exploits class. Read more.

    open security training - introductory x86 - buffer overflow mystery box

    A walkthrough for the Mystery Box Buffer Overflow challenge in the Open Security Training - Introductory x86 class. Read more.


    sprawlcomments

    All original content on this site is copyright protected and licensed under Creative Commons - Attribution, NonCommercial, ShareAlike 4.0 International.

    π
    ///\oo/\\\