00 - Defeating A Serial & Username Check

Here we will be reversing a binary that has 2 checks that validates if a username entered is correct and then the serial number entered, can we read the assembly and defeat/find both checks?

Purpose & Outcome

Many pieces of software try and implement anti cracking to protect their intellectual property and keep their software safe against pirating and or sold on black markets and abused etc. Today the binary we will reverse is fairly simple but showcases a small glimpse of checks some software providers use to protect the software, however we will try to defeat the username and serial check.

Source Code

It is important to note the functions called below in the source code of the binary we do not have their actual source code to read, so we are forced to read the assembly! The functions that we do not have the source code to are the validate_name and valid_serial, init_wargame functions.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void main() 
{
    init_wargame(); //Initializes the wargame function and sets up what it needs
    
    char buffer[64] = {}; //Sets up a char buffer variable which can hold 64 bytes
    unsigned int serial; //Sets up a serial unsigned integer variable

    printf("------------------------------------------------------------\n");
    printf("--[ Reverse Engineering Level #1 - 2-EZ Crackme             \n");
    printf("------------------------------------------------------------\n");

    // prompt the user for their name
    printf("Enter your name: ");
    fgets(buffer, sizeof(buffer), stdin); // stores name in buffer variable
    buffer[strcspn(buffer, "\n")] = 0;     // strip newline

    // if no name was given, abort
    if(strlen(buffer) == 0) // checks if the length is 0 of the supplied username
    {
        printf("Invalid name...\n");
        exit(1); // kills the binary and exits it if the user input is nothing
    }

    if(!validate_name(buffer)) // validate the name input that got stored in buffer
    {
        printf("Unrecognized Name...\n"); // if the function is false we exit
        exit(1);
    } // if the name is validated we can then pass it onto the next function

    // prompt the user for a valid serial number
    printf("Enter serial number: ");
    scanf("%u", &serial); // store the serial input at the address of serial variable
    printf("------------------------------------------------------------\n");

    // check the serial number for correctness
    if(!valid_serial(buffer, serial)) // if the serial doesnt pass the check we exit
    {
        printf("Invalid serial number...\n");
        exit(1);
    } 
    // if it passed the check we print it accepeted and get the flag yay    
    printf("Serial number accepted!\n");
    system("cat flag");
    
}

Figuring Out A Valid Username

Below we go ahead and run the binary and quickly get met with "Enter your name:", the program is prompting us to enter the name and this could be a check that typically wants to see if of course the username you enter is valid. Usually pieces of software which are more mature will of course perform these validations server side, but you never know and many developers hard code and make mistakes on the client side! With that said lets go ahead and try to figure out the username.

Great so after doing some source code analysis and figuring out that we cant read the source of the function which validates this username check which is validate_name, we now must spin up the GDB debugger and get to doing some debugging and reading the functions assembly code!

//Validate_Name functions assembly
0x4009c7:  push    rbp
0x4009c8:  mov     rbp, rsp
0x4009cb:  sub     rsp, 0x10
0x4009cf:  mov     qword [rbp-0x8], rdi
0x4009d3:  mov     rax, qword [rbp-0x8]
0x4009d7:  mov     rsi, rax
0x4009da:  mov     edi, 0x400cc8
0x4009df:  call    strcmp
0x4009e4:  test    eax, eax
0x4009e6:  je      0x400a27
0x4009e8:  mov     rax, qword [rbp-0x8]
0x4009ec:  mov     rsi, rax
0x4009ef:  mov     edi, 0x400ccc {"Alice"
0x4009f4:  call    strcmp
0x4009f9:  test    eax, eax
0x4009fb:  je      0x400a27
0x4009fd:  mov     rax, qword [rbp-0x8]
0x400a01:  mov     rsi, rax
0x400a04:  mov     edi, 0x400cd2
0x400a09:  call    strcmp
0x400a0e:  test    eax, eax
0x400a10:  je      0x400a27
0x400a12:  mov     rax, qword [rbp-0x8]
0x400a16:  mov     rsi, rax
0x400a19:  mov     edi, 0x400cd6 {"Mallory"
0x400a1e:  call    strcmp
0x400a23:  test    eax, eax
0x400a25:  jne     0x400a2e
0x400a27:  mov     eax, 0x1
0x400a2c:  jmp     0x400a33
0x400a2e:  mov     eax, 0x0
0x400a33:  leave   
0x400a34:  retn    

Reversing The Assembly Of Validate_Name Function

I have seen longer lines of assembly and this function is pretty straight forward but just like any big task we need to piece it up. To begin with we will break down the functions assembly piece by piece and try and understand the bigger picture as we go! Here below is the first chunk:

0x4009c7:  push    rbp //Push old functions base pointer onto the stack
0x4009c8:  mov     rbp, rsp //Set the new base pointer equal to the top
0x4009cb:  sub     rsp, 0x10 //Allocate 16 bytes for the stack
0x4009cf:  mov     qword [rbp-0x8], rdi //Move value from rdi to address offset
0x4009d3:  mov     rax, qword [rbp-0x8] //Moving value at offset into RAX register
0x4009d7:  mov     rsi, rax //Moving value in RAX register into RSI register
0x4009da:  mov     edi, 0x400cc8 //Moving 4197576 into the edi register
0x4009df:  call    strcmp //Calling string compare on the RDI/RSI register
0x4009e4:  test    eax, eax //Checking if ZF flag is 1 which means equal
0x4009e6:  je      0x400a27 //If strings are equal jump to address 0x400a27

What this first chunk of the assembly is doing is the following: It sets up the stack and then allocates 16 bytes, after doing so it then moves the value from within the RDI register into the offset of RBP-0x8 which holds a memory address and the value is then placed there. After doing so that same offset moves the value again into the RAX register. Great so now we have a decent idea of what we may need to figure out. Once again the value in the rax register is moved into the RSI register and then the value at the address 0x400cc8 is moved into the EDI register and then strcmp which is a function that compares strings is called. Think of strcmp as the following, if the strings compared are equal then 0 is returned and if not it returns 1. Great now after that, we called a test because the return value of most functions is usually stored in the RAX register but we test EAX which is the 32 bit aspect of the RAX register. Test performs a AND operation basically so if we had the strings not equal the EAX register would be 1 and then when it is 1, now the ZF flag is set to 0 which tells our CPU to basically not jump to the address specified if equal. However if the eax register is set to 0 and we test 0 AND 0 we then set the XF flag to 1 which now means the string comparison we did was equal and we can proceed to jumping to the address at 0x400a27. Great but to add one more thing when calling the strcmp function we are comparing the values in the RDI and RSI registers. So what does this conclude? We should go ahead and figure out what is in both of these registers maybe this is where the values of our input and a username is compared? Mhm.

Okay so to check if we theoretically our idea is correct we will set a breakpoint and the following addresses within to the program to stop the execution flow and dump the registers: 0x4009cf and then 0x4009da. Lets see if we are right here below is a small snippet of what is going on...............:

Great so now as we can see the RDI register at the start holds the value of the name we input at the console of the program (which was hello world this time) and then it moved it towards the end a little to the RSI register and moved the value stored at 0x400cc8 into EDI which is the 32 bit of the RDI register thus now our name input is stored at RSI and the value which is compared it to is the value which we dumped was "Bob". Hmmmm so can we now check and see maybe if "Bob" can get us passed the first check as a name? Lets check that out below and see if we are right or not:

NICE! Bob was a valid name, and if you check before you can see that upon entering a incorrect username we could not pass the functions check and the program got terminated and we failed :(, so we now know "Bob" is a valid username. But if you look closely at the assembly it also gives us information which you may be wondering about such as "Alice" and "Mallory" and more maybe we can pull out but we will focus on these now and see if maybe these can be valid usernames or not?

Okay great! We now know that we have 3 valid names we can use to pass the validate_name function and check which are "Bob, Alice, Mallory"! Now we need to enter the correct serial key can we find it? We do know that the function that checks the key is valid_key.

Extracting The Serial Key

So now that we know we have three different and valid usernames we need to enter the correct serial key and get that to work, how can we do so considering we have no access to that source codes function? The same way we did with the other function we are forced to be able to read the assembly and extract the key!!! Below is an image of the assembly code and commented each line!

0x400957:  push    rbp                      ; pushing rbp of the old function onto the stack which is 8 bytes
0x400958:  mov     rbp, rsp                 ; setting the base pointer of the stack to the current or equal to the rsp address
0x40095b:  push    rbx                      ; pushing rbx which is 8 bytes onto the stack
0x40095c:  sub     rsp, 0x28                ; allocating 40 bytes onto the stack for variables etc
0x400960:  mov     qword [rbp-0x28], rdi    ; Bob
0x400964:  mov     dword [rbp-0x2c], esi    ; 0
0x400967:  mov     dword [rbp-0x18], 0x4141 ; 16705
0x40096e:  mov     dword [rbp-0x14], 0x0    ; 0
0x400975:  jmp     0x4009ab                 ; jump to address 0x4009ab
0x400977:  mov     eax, dword [rbp-0x14]    ; move 0 into the eax register
0x40097a:  movsxd  rbx, eax                 ; moving a signed 0 from a 32 bit register to a 64 bit while maintaining its signedess and filling the upper 32 bits of the rax register with 0s
0x40097d:  mov     rax, qword [rbp-0x28]    ; moving the address and offset which holds the name which I put Bob
0x400981:  mov     rdi, rax                 ; copying over the address and offset or value of the name entered into the rdi register
0x400984:  call    strlen                   ; calling strlen on the name entered to get the length of the string
0x400989:  mov     rcx, rax                 ; moving the length of the name into the rcx register
0x40098c:  mov     rax, rbx                 ; move 0 into the rax register essentially overwriting the name length
0x40098f:  mov     edx, 0x0                 ; move 0 into the edx register which is the lower 32 bit of rdx
0x400994:  div     rcx                      ; here we are dividing 0 from the rax register by rcx which is 3 or the length of the name entered
0x400997:  mov     rax, qword [rbp-0x28]    ; we are then now moving the address of the name entered and storing that in rax
0x40099b:  add     rax, rdx                 ; here we are now adding 0 which was stored from the previous divison into rdx and adding it to rax which is holding the memory address of the name entered.
0x40099e:  movzx   eax, byte [rax]          ; here we are moving the first byte of the name entered
0x4009a1:  movzx   eax, al                  ; here we are moving the byte from al which is 1 byte into the eax register again and I dont know why although we already called it into here
0x4009a4:  add     dword [rbp-0x18], eax    ; here we add the value of 16705 to eax which is 0x42 and 66
0x4009a7:  add     dword [rbp-0x14], 0x1    ; this basically adds 1 to whatever the offset at rbp-0x14 is and basically this keeps looping until below condition is met.
0x4009ab:  cmp     dword [rbp-0x14], 0x4ff  ; compare 1279 with 0
0x4009b2:  jle     0x400977                 ; jump if less than or equal to 1279 to 0x400977
0x4009b4:  mov     eax, dword [rbp-0x18]    ; here we are moving the address at the offset rbp-0x18 which holds the serial key into eax the register
0x4009b7:  cmp     eax, dword [rbp-0x2c]    ; here the serial key is stored at the offset rbp-0x2c and then compared to the eax registers value which we copied over from rbp-0x18
0x4009ba:  sete    al                       ; here we are setting the value of the al register to 1 if the serial keys are equal and to 0 if they arent based on the ZF flag after the cmp instruction sets the ZF flag 
0x4009bd:  movzx   eax, al                  ;  here we are moving the one byte or 8 bits which is al and is either 1 or 0 and then storing it in eax and zeroing out the other 24 bits.
0x4009c0:  add     rsp, 0x28                ; here we are adding 0x28 which is 40 to the top of the stack pointer
0x4009c4:  pop     rbx                      ;  here we are popping a value of 0 into the rbx register when I checked
0x4009c5:  pop     rbp                      ; here we are popping the base address most likely back again into the rbp before returning
0x4009c6:  retn                             ; here we are returning to the main function weeeeeeeee

Great so what we can see from this is the following, the serial key we entered is stored at RBP-0x2c and it is compared with the key which is in the EAX register and we will see if it is valid or not. We will set a breakpoint at the address 0x4009b7 and enter a random key inside to see! Here it is below:

Okay so we entered the following as the serial code which was "1234567" and then we set a breakpoint below where the comparison is being made at 0x4009b7 and we found out if you see the small video above that our input is stored at RBP-0x2c and then being compared against the value in the EAX register, mhmmmm so we dumped the eax register and the number we got was "134032" which may just be the serial code.... lets check and see if we are correct below!:

So our theory was correct, and we quickly defeated both the checks in the program!

Conclusion

So to conclude, this was a simple program with 2 checks asking for a correct username and a correct serial code number. Many software that has been developed has some types of checks as so above and although this was quite simple to defeat many do have checks of this sort and make the mistake of hardcoding values, usernames, and more in the program and we can take advantage of that easily. I will end this off by saying, THE ASSEMBLY NEVER LIES ALWAYS!

Last updated