Practical case: Crack Me 0x01

The challenge in this practical case is taken from the Pen.Test CTF 2018 of the Platform Voor Informatiebeveiliging (Platform For Information Security), which took place on the 11th of October 2018. The challenge has been created by Jeffrey Jansen from Access42. During the CTF, I solved this challenge together with my team mate Exploiteer.

The challenge can be downloaded here.

In this practical case, the challenge will be analysed in the usual step-by-step approach. The first time, the analysis will be done using x64dbg. The second analysis will be done with Ghidra. In both cases, the complete challenge will be solved, meaning no knowledge between the two solutions is shared. The reason to include two solutions, is to show multiple methods how one can solve such a challenge: dynamically, with a debugger, or statically, by only looking at the code. This is by no means a definitive list on how the challenge can be solved.

Observations regarding the challenge

For the challenge, two things are given. Firstly, a single Windows command-line interface binary is given. In the interface, an e-mail address and serial number are required. If the combination of the e-mail address and serial number combination is correct, an unknown action is taken.

In the case of an incorrect combination, a message stating such is shown. Additionally, the e-mail address should be between 3 and 15 characters long, or the program states that the length of the e-mail address should be between 3 and 15 characters. Regardless of the outcome of the combination check, one has to wait a couple of seconds before one can try again.

Secondly, a URL is given. The URL is an API endpoint, with which one can interact. If the API is accessed via a GET request, 42 e-mail addresses are given. Every 20 seconds, these 42 e-mail addresses change. To solve the challenge, one needs to send a POST request to the API, where the body contains the current 42 e-mail addresses and their respective serial number. Upon doing so, the API will return the flag.

Note that in this article, only the analysis of the binary is done, together with the creation of a serial number generation script. The generation script is also referred to as a keygen.

Dynamic analysis

Upon inspecting the binary with the GNU file tool, one can observe that the application is based on the x86 architecture.

./challenge.exe: PE32 executable (console) Intel 80386 (stripped to external PDB), for MS Windows

This means that one has to use the 32-bit variant of x64dbg. After starting x64dbg, one can open the binary. Upon pressing run, one can stop at the binary’s entry point. This is, however, not the function where the user input is requested. Simply holding F8 (step over) until the program wont execute further, will make one reach the desired function. This works because the program waits on user input, as described in the observed behaviour above. One can place a breakpoint at the function prologue (which is located at 00401390) to quickly get back to this location at a later point in time. This is useful when one has to restart the binary.

In this example, the e-mail address input will equal regName and the serial number input will equal serialNumber. This way, one can quickly see where the input is saved and when it is accessed.

The e-mail address is then saved in the address where EBP-78 points to. The first line in the code below, is the information x64dbg provides during the runtime, whereas the second line is the instruction at which point the runtime information can be seen. The pointer towards the e-mail address is stored in EAX using the LEA (Load Effective Address) instruction.

dword ptr [ebp-78]=[0064FEB0 "regName"]=4E676572
00401452 | 8D45 88                  | lea eax,dword ptr ss:[ebp-78]           |

After that, the pointer towards the provided e-mail address is pushed on the stack and the strlen function is called.

00401455 | 890424                   | mov dword ptr ss:[esp],eax              |
00401458 | E8 33F40000              | call <JMP.&strlen>                      |

This function returns the length of the string (the e-mail address’ length) into EAX, which is stored in the memory location of EBP-1D0.

0040145D | 8985 30FEFFFF            | mov dword ptr ss:[ebp-1D0],eax          |

The e-mail address length is then compared to 0xE (or 14 in decimal).

00401463 | 83BD 30FEFFFF 0E         | cmp dword ptr ss:[ebp-1D0],E            |

If the length of the e-mail address is greater than 0xE, a jump is taken.

0040146A | 0F8F 2C010000            | jg challenge.40159C                     |

If the jump is not taken, the length is compared to 2.

00401470 | 83BD 30FEFFFF 02         | cmp dword ptr ss:[ebp-1D0],2            |

If the length of the e-mail address is less than (or equal to) 2, the jump is taken.

00401477 | 0F8E 1F010000            | jle challenge.40159C                    |

Both jumps point towards the same address, which starts with the following instruction.

0040159C | C74424 04 FC014400       | mov dword ptr ss:[esp+4],challenge.4401 | 4401FC:"Sorry, name must be between 3-15 charictars!"

Based on the instructions before the comparisons, this makes sense. As such, the input should be within the given boundaries. Based on the earlier made observations, the provided user input in this example is within those boundaries.

A variable at EBP-1D4 is set to zero, which will be used later on.

0040147D | C785 2CFEFFFF 00000000   | mov dword ptr ss:[ebp-1D4],0            |

The variable which was set to zero earlier on, is moved into EAX.

00401487 | 8B85 2CFEFFFF            | mov eax,dword ptr ss:[ebp-1D4]          |

The variable is then compared to the length of the provided e-mail address (which resides at EBP-1D0).

0040148D | 3B85 30FEFFFF            | cmp eax,dword ptr ss:[ebp-1D0]          |

If the unnamed variable is greater than (or equal to) the length of the e-mail address, a jump downwards in the code will be made. This indicates the for-loop, which is also visible in the GUI of x64dbg. As such, the variable at EBP-1D0 is similar to i in a for-loop, and can be named as such.

00401493 | 7D 2A                    | jge challenge.4014BF                    |

The next four instructions need to be read at once. At first, the pointer that resides at EBP-8 is loaded into EAX. Rephrasing that to match what the instruction says: the effective address that resides at the value of EBP-8 is loaded into EAX.

After that, the value that resides at EBP-1D4 is added to EAX. At last, 0x70 is subtracted from EAX. At this point, EAX points towards the serial number that the user provided. At last, a single byte is loaded into EAX, based on the value that EAX points towards.

00401495 | 8D45 F8                  | lea eax,dword ptr ss:[ebp-8]            |
00401498 | 0385 2CFEFFFF            | add eax,dword ptr ss:[ebp-1D4]          |
0040149E | 83E8 70                  | sub eax,70                              |
004014A1 | 0FBE00                   | movsx eax,byte ptr ds:[eax]             |

The serial number input was originally stored at EBP-78, making EBP-8 the end of the array. The variable i is then added, after which the size of the complete array is subtracted. In the example below, this will be demonstrated.

If the character array LibraAnalysis is used to iterate through in a for-loop, where i is used to keep track of the count, the assembly code above functions as follows:

The size of the character array is 0xD (13 in decimal). In the first instruction, the index of the character array is 13. Assume that the loop is in the third iteration, making i equal to 3. Then 13+3 equals 16. The next step is to remove the complete size of the character array from the index, which is 3 (the result of 16 minus 13). In the case above, the same calculation happens, but with a memory address instead of an index. This is, indirectly, the same.

In the end, it comes down to getting the current character from the character array, which is commonly written as:

characterArray[i]

The variable that resides at EBP-1CC is set to 0 in the beginning of the function as can be seen below.

004013ED | C785 34FEFFFF 00000000   | mov dword ptr ss:[ebp-1CC],0            |
004013F7 | C785 34FEFFFF 00000000   | mov dword ptr ss:[ebp-1CC],0            |

Back in the for-loop, the value that resides at EBP-1CC is added to EAX, after which the total is decreased with 3C7F.

004014A4 | 0385 34FEFFFF            | add eax,dword ptr ss:[ebp-1CC]          |
004014AA | 2D 7F3C0000              | sub eax,3C7F                            |

The result of that is then stored at EBP-1CC. During each iteration of the loop, the value that resides at the pointer that EBP-1CC contains, is altered.

004014AF | 8985 34FEFFFF            | mov dword ptr ss:[ebp-1CC],eax          |

In pseudo code, the code so far is given below. The variable that resides at EBP-1CC is referred to as sum.

sum = characterArray[i] + sum - 0x3C7F

The pointer to the variable named i is then stored in EAX, after which the value is increased with one. After that, the jump upwards is taken, to check if another iteration is required.

004014B5 | 8D85 2CFEFFFF            | lea eax,dword ptr ss:[ebp-1D4]          |
004014BB | FF00                     | inc dword ptr ds:[eax]                  |
004014BD | EB C8                    | jmp challenge.401487                    |

After the last iteration, the calculated value that resides at EBP-1CC is stored in EAX, after which it is placed on the stack at ESP+C. Since the application’s architecture is x86 and function arguments are passed via the stack with a size of four bytes each, this is the fourth argument.

004014BF | 8B85 34FEFFFF            | mov eax,dword ptr ss:[ebp-1CC]          |
004014C5 | 894424 0C                | mov dword ptr ss:[esp+C],eax            |

The variable that is located at EBP-1C8 is assigned before that in the code, as can be seen below.

004013C3 | A1 00004400              | mov eax,dword ptr ds:[440000]           | eax:"regName", 00440000:"A42"
004013C8 | 8985 38FEFFFF            | mov dword ptr ss:[ebp-1C8],eax          |

This means that the variable that resides at EBP-1C8 is equal to A42. This variable is also pushed on the stack.

004014C9 | 8D85 38FEFFFF            | lea eax,dword ptr ss:[ebp-1C8]          |
004014CF | 894424 08                | mov dword ptr ss:[esp+8],eax            |

Another argument, a string, is pushed on the stack. The string is equal to %s-%d.

004014D3 | C74424 04 8D004400       | mov dword ptr ss:[esp+4],challenge.4400 | 44008D:"%s-%d"

The last variable, a pointer which is located at EBP-158, is stored in EAX, after which it pushed onto the stack.

004014DB | 8D85 A8FEFFFF            | lea eax,dword ptr ss:[ebp-158]          |
004014E1 | 890424                   | mov dword ptr ss:[esp],eax              |

The function wsprintfA is then called. The function writes the formatted string into the given buffer. The argument which is pushed on the stack the last (the pointer that resides in EBP-158) will contain a string that is formatted as follows: %s-%d. The string equals A42 and the decimal value equals the calculation that is made based upon the provided e-mail address.

004014E4 | E8 C7F50000              | call <JMP.&wsprintfA>                   |

The serial number that was entered by the user is stored at the address that resides at EBP-E8, as can be seen below.

dword ptr [ebp-E8]=[0064FE40 "serialNumber"]=69726573

This pointer is then moved into EAX.

004014E9 | 8D85 18FFFFFF            | lea eax,dword ptr ss:[ebp-E8]           |

The pointer at EBP-158 contains the computed serial number, as can be seen below.

dword ptr [ebp-158]=[0064FDD0 "A42--107706"]=2D323441

Both the user provided serial number and the computed serial number are then stored on the stack.

004014EF | 8D95 A8FEFFFF            | lea edx,dword ptr ss:[ebp-158]          |
004014F5 | 894424 04                | mov dword ptr ss:[esp+4],eax            |
004014F9 | 891424                   | mov dword ptr ss:[esp],edx              |

The two serial numbers are then compared, as pointers towards both are saved on the stack. The function named strcmp returns an integer. If the first string is less than the second string, the returned value is less than zero. If the strings are equal, the value zero is returned. If the second string is greater than the first string, a value greater than zero is returned.

The return value, which is passed via EAX, is stored in EBP-1D4.

004014FC | E8 7FF30000              | call <JMP.&strcmp>                      |
00401501 | 8985 2CFEFFFF            | mov dword ptr ss:[ebp-1D4],eax          |

The return value is then compared to zero. The return value is only zero if the compared values are the same. If the compared values are not equal, the jump is taken.

00401507 | 83BD 2CFEFFFF 00         | cmp dword ptr ss:[ebp-1D4],0            |
0040150E | 75 6A                    | jne challenge.40157A                    |

If the jump is taken, the screen is cleared and the failure message is shown to the user.

0040157A | C70424 93004400          | mov dword ptr ss:[esp],challenge.440093 | 440093:"cls"
00401581 | E8 EAF20000              | call <JMP.&system>                      |
00401586 | C74424 04 A8014400       | mov dword ptr ss:[esp+4],challenge.4401 | 4401A8:"Registation failed!\nBut sinse ime a nice guy ime going to let you have another go!\n"

If the jump is not taken, that means that the entered serial number is equal to the one that is computed. The screen is cleared, after which the success message is shown.

00401510 | C70424 93004400          | mov dword ptr ss:[esp],challenge.440093 | 440093:"cls"
00401517 | E8 54F30000              | call <JMP.&system>                      |
0040151C | C74424 04 98004400       | mov dword ptr ss:[esp+4],challenge.4400 | 440098:"Registration sucseeded!\n\nYour serial was:  "

The complete function has been analysed and the algorithm has been found. The algorithm is given in pseudo code above. After the static analysis part, the keygen is created and tested.

Static analysis

Static code analysis does not involve executing the sample, as only the code is analysed. In this case, Ghidra is used to decompile the code and refactor the variables.

Ghidra’s analysis settings
The version of Ghidra that is used in this article is the latest at the time of writing: 9.0.4. The default analysis settings are used, with only a single change. The analysis option WindowsPE x86 Propagate External Parameters is also enabled. In short, all but the experimental analysis modules are used to analyse the binary.

Additionally, the decompiler will be used to quickly get an overview of the function. To refactor a variable in the decompiler, one can use the default hotkey: l (a lowercase L).

Finding the entry point
After the analysis is complete, multiple functions can be found in the binary. The binary contains two functions that start with _main, both of which look rather interesting:

  • _mainCRTStartup
  • _main

The decompilation of _mainCRTStartup is given below.

/* WARNING: Exceeded maximum restarts with more pending */
 
void _mainCRTStartup(void)
{
  MSVCRT.DLL::__set_app_type(1);
  ___mingw_CRTStartup();
  MSVCRT.DLL::__set_app_type(2);
  ___mingw_CRTStartup();
                    /* WARNING: Could not recover jumptable at 0x0040126a. Too many branches */
                    /* WARNING: Treating indirect jump as call */
  MSVCRT.DLL::atexit();
  return;
}

In the code above, it is obvious that the code does not perform the behaviour that is seen during the described observation. The MSVCRT.DLL that is referenced in the code, stands for Microsoft Visual C Runtime. The function named ___mingw_CRTStartup refers to the MinGW Common Runtime Startup. This function refers to the main set-up of the Common Runtime, and is not interesting during this challenge.

The other function, _main, is the following function that seems interesting. The decompiled output of the function is given exactly as Ghidra provides it, aside from a single change: the e-mail address of the challenge author has been redacted to avoid automated spam bots finding his e-mail address.

int __cdecl _main(int _Argc,char **_Argv,char **_Env)
{
  size_t sVar1;
  int iVar2;
  int *piVar3;
  int local_1d8;
  int local_1d0;
  undefined4 local_1cc;
  undefined local_1c8 [108];
  CHAR local_15c [112];
  char local_ec [112];
  char local_7c [120];
 
  FUN_0040d270();
  FUN_0040ceb0();
  local_1cc = 0x323441;
  memset(local_1c8,0,0x5f);
  do {
    local_1d0 = 0;
    .text$_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
              ((int *)&DAT_004433c0,"Registration name: ");
    .text$_ZStrsIcSt11char_traitsIcEERSt13basic_istreamIT_T0_ES6_PS3_((int *)&DAT_00443460,local_7c)
    ;
    .text$_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
              ((int *)&DAT_004433c0,"Registartion serial: ");
    .text$_ZStrsIcSt11char_traitsIcEERSt13basic_istreamIT_T0_ES6_PS3_((int *)&DAT_00443460,local_ec)
    ;
    sVar1 = strlen(local_7c);
    if (((int)sVar1 < 0xf) && (2 < (int)sVar1)) {
      local_1d8 = 0;
      while (local_1d8 < (int)sVar1) {
        local_1d0 = (int)local_7c[local_1d8] + local_1d0 + -0x3c7f;
        local_1d8 = local_1d8 + 1;
      }
      wsprintfA(local_15c,"%s-%d",&local_1cc,local_1d0);
      iVar2 = strcmp(local_15c,local_ec);
      if (iVar2 == 0) {
        system("cls");
        piVar3 = .text$_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
                           ((int *)&DAT_004433c0,"Registration sucseeded!\n\nYour serial was:  ");
        .text$_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc(piVar3,local_15c);
        .text$_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
                  ((int *)&DAT_004433c0,"\nAnd if you think your really good write a keygen!\n");
        .text$_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
                  ((int *)&DAT_004433c0,
 
                   "The keygen should be able to solve 1000 random registration names in less than10 seconds in the PvIB platform. Copy of source code? Email me at:[redacted]..!\n"
                  );
        system("pause");
      }
      else {
        system("cls");
        .text$_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
                  ((int *)&DAT_004433c0,
 
                   "Registation failed!\nBut sinse ime a nice guy ime going to let you have anothergo!\n"
                  );
      }
    }
    else {
      .text$_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
                ((int *)&DAT_004433c0,"Sorry, name must be between 3-15 charictars!");
    }
    Sleep(5000);
    system("cls");
  } while( true );
}

There are a couple of rather long function names in the output, but the gist of the function itself becomes clear: the messages that are shown during the execution of the program are visible, making this the starting point of the challenge. Below, this function will be analysed in the usual step-by-step method.

Firstly, the function that is named .text$_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc refers to the basic_ostream. This functions handles the output stream. The function is given below.

int * __cdecl .text$_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc(int *param_1,char *param_2)
{
  int *piVar1;
  size_t sVar2;
  uint uVar3;
  int iVar4;
  char acStack128 [12];
  char *local_74;
  size_t local_70;
  size_t local_6c;
  int local_68;
  int *local_64;
  LPVOID local_60;
  undefined4 local_5c;
  undefined *local_48;
  undefined *local_44;
  undefined *local_40;
  undefined *local_3c;
  undefined *local_38;
  char local_2c [4];
  int *local_28;
  undefined local_1c [12];
 
  local_40 = local_1c;
  local_38 = &stack0xffffff64;
  local_48 = &___gxx_personality_sj0;
  local_64 = param_1;
  local_44 = &DAT_0043df0c;
  local_3c = &DAT_0043c49f;
  FUN_0040d3b0(&local_60);
  local_5c = 0xffffffff;
  .text$_ZNSo6sentryC1ERSo(local_2c,local_64);
  if (local_2c[0] == '\0') {
    if (param_2 != NULL) goto LAB_0043c3c8;
  }
  else {
    if (param_2 != NULL) {
      local_68 = (int)local_64 + *(int *)(*local_64 + -0xc);
      local_6c = *(size_t *)(local_68 + 8);
      local_70 = strlen(param_2);
      if ((int)local_70 < (int)local_6c) {
        FUN_0040d270();
        local_74 = acStack128;
        if (*(char *)(local_68 + 0x75) == '\0') {
          local_5c = 2;
          iVar4 = .text$_ZNKSt9basic_iosIcSt11char_traitsIcEE5widenEc(local_68,0x20);
          *(undefined *)(local_68 + 0x74) = (char)iVar4;
          *(undefined *)(local_68 + 0x75) = 1;
        }
        local_5c = 2;
        FUN_004387d0((int)local_64 + *(int *)(*local_64 + -0xc),*(char *)(local_68 + 0x74),local_74,
                     param_2,local_6c,local_70,'\0');
        local_70 = local_6c;
      }
      local_5c = 2;
      sVar2 = (**(code **)(**(int **)((int)local_64 + *(int *)(*local_64 + -0xc) + 0x78) + 0x30))();
      if (sVar2 != local_70) {
        iVar4 = (int)local_64 + *(int *)(*local_64 + -0xc);
        .text$_ZNSt9basic_iosIcSt11char_traitsIcEE5clearESt12_Ios_Iostate
                  (iVar4,*(uint *)(iVar4 + 0x14) | 1);
      }
      *(undefined4 *)((int)local_64 + *(int *)(*local_64 + -0xc) + 8) = 0;
      goto LAB_0043c3c8;
    }
  }
  iVar4 = (int)local_64 + *(int *)(*local_64 + -0xc);
  local_5c = 3;
  .text$_ZNSt9basic_iosIcSt11char_traitsIcEE5clearESt12_Ios_Iostate
            (iVar4,*(uint *)(iVar4 + 0x14) | 1);
LAB_0043c3c8:
  if ((*(byte *)((int)local_28 + *(int *)(*local_28 + -0xc) + 0xd) & 0x20) != 0) {
    uVar3 = __ZSt18uncaught_exceptionv();
    if (((char)uVar3 == '\0') &&
       (piVar1 = *(int **)((int)local_28 + *(int *)(*local_28 + -0xc) + 0x78), piVar1 != NULL)) {
      local_5c = 0xffffffff;
      iVar4 = (**(code **)(*piVar1 + 0x18))();
      if (iVar4 == -1) {
        iVar4 = (int)local_28 + *(int *)(*local_28 + -0xc);
        .text$_ZNSt9basic_iosIcSt11char_traitsIcEE5clearESt12_Ios_Iostate
                  (iVar4,*(uint *)(iVar4 + 0x14) | 1);
      }
    }
  }
  FUN_0040d490(&local_60);
  return local_64;
}

Based upon the function’s arguments, one can predict the function’s behaviour. The first occurence of this function within _main, is given below.

.text$_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc((int *)&DAT_004433c0,"Registration name: ");

A message is printed and a buffer is provided, based on which one can conclude that the function behaves like printf. The message that is shown to the user is given as the second argument, whereas the first argument is the output stream that is used to print the result. As such, this function can be renamed to printf. The global variable DAT_004433c0 can be renamed to outputObject. This change cleaned the code, making it way more readable, as can be seen below.

int __cdecl _main(int _Argc,char **_Argv,char **_Env)
{
  size_t sVar1;
  int iVar2;
  int *piVar3;
  int local_1d8;
  int local_1d0;
  undefined4 local_1cc;
  undefined local_1c8 [108];
  CHAR local_15c [112];
  char local_ec [112];
  char local_7c [120];
 
  FUN_0040d270();
  FUN_0040ceb0();
  local_1cc = 0x323441;
  memset(local_1c8,0,0x5f);
  do {
    local_1d0 = 0;
    printf((int *)&outputObject,"Registration name: ");
    .text$_ZStrsIcSt11char_traitsIcEERSt13basic_istreamIT_T0_ES6_PS3_((int *)&DAT_00443460,local_7c)
    ;
    printf((int *)&outputObject,"Registartion serial: ");
    .text$_ZStrsIcSt11char_traitsIcEERSt13basic_istreamIT_T0_ES6_PS3_((int *)&DAT_00443460,local_ec)
    ;
    sVar1 = strlen(local_7c);
    if (((int)sVar1 < 0xf) && (2 < (int)sVar1)) {
      local_1d8 = 0;
      while (local_1d8 < (int)sVar1) {
        local_1d0 = (int)local_7c[local_1d8] + local_1d0 + -0x3c7f;
        local_1d8 = local_1d8 + 1;
      }
      wsprintfA(local_15c,"%s-%d",&local_1cc,local_1d0);
      iVar2 = strcmp(local_15c,local_ec);
      if (iVar2 == 0) {
        system("cls");
        piVar3 = printf((int *)&outputObject,"Registration sucseeded!\n\nYour serial was:  ");
        printf(piVar3,local_15c);
        printf((int *)&outputObject,"\nAnd if you think your really good write a keygen!\n");
        printf((int *)&outputObject,
 
              "The keygen should be able to solve 1000 random registration names in less than 10seconds in the PvIB platform. Copy of source code? Email me at:[redacted]..!\n"
             );
        system("pause");
      }
      else {
        system("cls");
        printf((int *)&outputObject,
 
              "Registation failed!\nBut sinse ime a nice guy ime going to let you have anothergo!\n"
             );
      }
    }
    else {
      printf((int *)&outputObject,"Sorry, name must be between 3-15 charictars!");
    }
    Sleep(5000);
    system("cls");
  } while( true );
}

Around halfway through the code, the following line of code can be observed:

wsprintfA(local_15c,"%s-%d",&local_1cc,local_1d0);

The function signature can be found here, which provides information regarding each of the arguments.

The first argument, local_15c is the buffer where the formatted string will be saved in. The second argument, %s-%d, sets the string format to be a string and a decimal with a dash in between. The two variables after that are, respectively, the string (local_1cc) and the decimal (local_1d0). The buffer can be refactored to wsprintfBuffer, whilst the two literals can be named stringLiteral and integerLiteral.

At the top of the function, the variable stringLiteral is set equal to 0x323441. The ASCII value of this equals 24A. Due to the fact that Windows uses the little-endian notation, the string should read from right to left, making it A42. This makes sense, as the challenge author’s company is named Access42.

The fact that this string is found, leads to the conclusion that the serial number is written as %s-%d. Currently, half of the serial number is known. To increase the readability in the code, the variable names can be refactored again: stringLiteral becomes serialNumberPrefix and integerLiteral becomes serialNumberPostfix. At last, the wsprintfBuffer contains the complete serial number, and can be refactored into serialNumber.

The line below the wsprintfA call, compares the serial number to the variable local_ec, which is presumably also the serial number. The code is given below.

iVar2 = strcmp(serialNumber,local_ec);

The variable local_ec is only used in the function that is called directly after the serial number is requested via the scanf-like function, as can be seen below. Note that the name of the function refers to the basic_istream, which is the basic input stream.

.text$_ZStrsIcSt11char_traitsIcEERSt13basic_istreamIT_T0_ES6_PS3_((int *)&DAT_00443460,local_ec);

One can therefore assume that the variable local_ec contains the user input as provided by the input stream object that is located at DAT_00443460. The input stream object can be renamed to inputObject. This also happens two lines above that, when requesting the e-mail address from the user.

The variable local_7c is therefore likely to contain the user provided e-mail address. The function named .text$_ZStrsIcSt11char_traitsIcEERSt13basic_istreamIT_T0_ES6_PS3_ can be refactored into scanf, as the exact inner workings are unknown, but the gist of the function’s behaviour is known. The variable local_ec can be renamed to userInputSerialNumber. Similarly, the variable local_7c can be renamed to userInputEmail.

Below the scanf function, an if-statement and while-loop are present. With the renamed variables, this part of the code looks like it is responsible for the serial number generation. The code is given below.

sVar1 = strlen(userInputEmail);
if (((int)sVar1 < 0xf) && (2 < (int)sVar1)) {
  local_1d8 = 0;
  while (local_1d8 < (int)sVar1) {
	serialNumberPostfix = (int)userInputEmail[local_1d8] + serialNumberPostfix + -0x3c7f;
	local_1d8 = local_1d8 + 1;
  }

The variable sVar1 is equal to the length of the e-mail address that the user provides. After that, the length of the provided e-mail address is compared. The length should be less than 0xf (which equals 15 in decimal) and should be more than 2.

As stated in the program’s behaviour description, the e-mail address length should be between 3 and 15 characters in length. The variable sVar1 can be renamed to emailAdressLength.

Within the if-statement, the variable local_1d8 is present. After that, a while-loop is looped through as long as the local_1d8 variable is less than the emailAdressLength and it is incremented with 1 at the end of each iteration of the while-loop. Therefore, the variable local_1d8 can be renamed to i, as it serves to keep track of the amount of iterations that have been computed.

The while-loop with the refactored names is given below.

i = 0;
while (i < (int)emailAdressLength) {
  serialNumberPostfix = (int)userInputEmail[i] + serialNumberPostfix + -0x3c7f;
  i = i + 1;
}

The while loop takes a character from the provided e-mail address as an integer, which is added to the total sum, after which the value 0x3c7f is subtracted. When the complete e-mail address has been parsed in this way, the serialNumberPostfix has a certain value, which is its final value.

At last, the two serial numbers (the one that is calculated and the one that the user provided) are compared. If they are equal, the win message is displayed. If they are not equal, the failure message is displayed. The simplified code is given below.

iVar1 = strcmp(serialNumber,userInputSerialNumber);
if (iVar1 == 0) {
  //Display win condition
} else {
  //Display failure condition
}

Note that the variable iVar1 can be refactored into serialNumberComparisonResult to increase the readability.

To provide a clear overview of the code, the complete function is given below.

int __cdecl _main(int _Argc,char **_Argv,char **_Env)
{
  size_t emailAdressLength;
  int serialNumberComparisonResult;
  int *piVar1;
  int i;
  int serialNumberPostfix;
  undefined4 serialNumberPrefix;
  undefined local_1c8 [108];
  CHAR serialNumber [112];
  char userInputSerialNumber [112];
  char userInputEmail [120];
 
  FUN_0040d270();
  FUN_0040ceb0();
  serialNumberPrefix = 0x323441;
  memset(local_1c8,0,0x5f);
  do {
    serialNumberPostfix = 0;
    printf((int *)&outputObject,"Registration name: ");
    scanf((int *)&inputObject,userInputEmail);
    printf((int *)&outputObject,"Registartion serial: ");
    scanf((int *)&inputObject,userInputSerialNumber);
    emailAdressLength = strlen(userInputEmail);
    if (((int)emailAdressLength < 0xf) && (2 < (int)emailAdressLength)) {
      i = 0;
      while (i < (int)emailAdressLength) {
        serialNumberPostfix = (int)userInputEmail[i] + serialNumberPostfix + -0x3c7f;
        i = i + 1;
      }
      wsprintfA(serialNumber,"%s-%d",&serialNumberPrefix,serialNumberPostfix);
      serialNumberComparisonResult = strcmp(serialNumber,userInputSerialNumber);
      if (serialNumberComparisonResult == 0) {
        system("cls");
        piVar1 = printf((int *)&outputObject,"Registration sucseeded!\n\nYour serial was:  ");
        printf(piVar1,serialNumber);
        printf((int *)&outputObject,"\nAnd if you think your really good write a keygen!\n");
        printf((int *)&outputObject,
 
              "The keygen should be able to solve 1000 random registration names in less than 10seconds in the PvIB platform. Copy of source code? Email me at:[redacted]..!\n"
             );
        system("pause");
      }
      else {
        system("cls");
        printf((int *)&outputObject,
 
              "Registation failed!\nBut sinse ime a nice guy ime going to let you have anothergo!\n"
             );
      }
    }
    else {
      printf((int *)&outputObject,"Sorry, name must be between 3-15 charictars!");
    }
    Sleep(5000);
    system("cls");
  } while( true );
}

Creating the keygen

Now that the generation algorithm is known, one can create a keygen to calculate the serial number of a given e-mail address. In the Python3 script below, there is no length check, as the API part of the challenge is left out.

#Written by Max 'Libra' Kersten (@LibraAnalysis) and Exploiteer at the 11th of October 2018 during the Pen.Test CTF of Platform Voor Informatiebeveiliging
 
#!/usr/bin/env python3
 
input = 'alis2108@pvib.' #Results in A42--215647
for email in input.splitlines():
    #Initialise the variable to store the serial number in
    serialNumber = 0
    #Loop through all characters of the provided e-mail address
    for i in range(0, len(email)):
        currentCharacter = ord(email[i])
        currentCharacter += serialNumber
        currentCharacter -= 0x3c7f
        serialNumber = currentCharacter
    #The serial always starts with 'A42-', after which the generated number is added
    serial = 'A42-%d' % serialNumber
    #Print the serial number
    print(serial)

Note that the top level domain of the e-mail address is removed due to the length constraint. When running the script with the e-mail address alis2108@pvib., the calculated serial number equals A42–215647. When entering this in the challenge, the success message is observed:

Registration sucseeded!

Your serial was:  A42--215647
And if you think your really good write a keygen!
The keygen should be able to solve 1000 random registration names in less than 10seconds in the PvIB platform. Copy of source code? Email me at:[redacted]..!
Press any key to continue . . .

To contact me, you can e-mail me at [info][at][maxkersten][dot][nl], send me a PM on Reddit or DM me on Twitter @LibraAnalysis.