skip to content
Site header image Minh Ton

crackmesone — indulgeo_crackme_v2

An educational keygenme challenge written in pure Assembly, with a difficulty rating of 3.5. Working through this really deepened my understanding of Assembly code, and I picked up an encryption algorithm along the way!


Crackme Exploration

According to the description, this is a KeygenMe challenge written in pure x86 Assembly.

Upon launching the executable file, I’m quite impressed with how the author could create such a cool user interface like this with just Assembly! I was just expecting a TUI-based challenge, but seeing this GUI just feels like a great start.

Entering some random strings into the Name and Serial text boxes yielded an error message box, indicating an incorrect serial key.

In previous write-ups, I’ve never used the Detect It Easy tool for analyzing an executable for a challenge prior to solving. Since the crackme’s description hinted that the executable is packed, let’s open it up in Detect It Easy to see some preliminary information.

And yes, the crackme is packed using UPX.

Also, I discovered that we can actually press the YARA button and use YARA rules to find additional information that can help us with solving the challenge.

From the YARA rules, seems like there are some cryptography things inside the crackme. We’re not sure about their actual usage, but they might be helpful later.

Unpacking The Crackme

I’ve never dealt with a UPX-packed executable before, so I went to Google and came across this article explaining how to unpack one:

I’m going to follow this article just for unpacking this crackme, then I’ll definitely look into this in more detail later!

Finding The OEP

In Binary Ninja, I quickly set a breakpoint at the jmp right after the popad instruction at the end of _start():

I then stepped into the jmp and it took me to sub_401000.

Just by looking at some strings, this function seems to be the program’s main logic. Here, the jmp took me to the OEP (Original Entry Point) of the binary, so we get 00401000 as the OEP of the crackme!

Let’s extract the unpacked executable from this OEP.

Dumping The Unpacked PE

To do this, I used a tool called Scylla. I’m going to run the crackme first, and choose the crackme process in the Attach to an active process dropdown.

And… our OEP isn’t correct at all. Let’s fix that to 00401000. Then click on IAT Autosearch to scan for imports.

When this dialog pops up, click on Yes.

And press on Get Imports once Scylla finishes.

Finally, click on Dump to extract the unpacked binary. Then click on Fix Dump and choose the recently dumped binary to fix its IAT.

The executable ending with _SCY.exe is our unpacked binary!

Now we can open the unpacked executable in a disassembler!

Cracking Time

Let’s open the unpacked binary in Binary Ninja. In the _start function, there are quite some debugger checks and probably some anti-patching there.

Scrolling a bit down to the end of the _start() function… But wait, where is the serial key validation logic?

I opened the Strings view and look for the strings for the message boxes that we saw earlier. So somehow this part isn’t disassembled yet.

Then I decided to do a pretty dumb thing. I NOP-ed all the code of some debugger checks, starting from 00401035 to 0040113c. I’m not sure what really happened but Binary Ninja just decided to disassemble the main logic of the program!

To be honest I was quite tempted to just decompile this section into pseudocode because I was too lazy to read raw Assembly. But I decided to stay true to the author’s intent and analyze the entire logic directly through the disassembly.

Serial Construction Logic

Since the Name and Serial are entered through EditBoxes, we’ll focus on the GetDlgItemTextA calls. These appear at addresses 00401211, 00401280, and 00401308, which correspond to the parts of the code that read input from the text boxes, generate the key from the name, and compare it to the entered serial.

004011ff  6800020000         push    0x200
00401204  681de14000         push    0x40e11d
00401209  68f3030000         push    0x3f3
0040120e  ff7508             push    dword [ebp+0x8 {arg1}]
00401211  e840080000         call    GetDlgItemTextA
00401216  681de14000         push    0x40e11d
0040121b  e8de080000         call    lstrlenA
00401220  a31ded4000         mov     dword [data_40ed1d], eax
00401225  69c039050000       imul    eax, eax, 0x539
0040122b  50                 push    eax
0040122c  6809c94000         push    0x40c909
00401231  681de54000         push    0x40e51d
00401236  e8f1070000         call    wsprintfA
0040123b  681de34000         push    0x40e31d
00401240  ff351ded4000       push    dword [data_40ed1d]
00401246  681de14000         push    0x40e11d
0040124b  e890800000         call    sub_4092e0
00401250  681de34000         push    0x40e31d
00401255  681de74000         push    0x40e71d
0040125a  e899080000         call    lstrcpyA
0040125f  681de54000         push    0x40e51d
00401264  681de74000         push    0x40e71d
00401269  e87e080000         call    lstrcatA
0040126e  6800020000         push    0x200
00401273  6821f14000         push    0x40f121
00401278  680dc94000         push    0x40c90d  {"KPCR tapz"}
0040127d  ff7508             push    dword [ebp+0x8 {arg1}]
00401280  e8d1070000         call    GetDlgItemTextA
00401285  50                 push    eax
00401286  6821f14000         push    0x40f121
0040128b  e880620000         call    sub_407510
00401290  6821ed4000         push    0x40ed21
00401295  681de74000         push    0x40e71d
0040129a  e849630000         call    sub_4075e8
0040129f  6821ef4000         push    0x40ef21
004012a4  ff351ded4000       push    dword [data_40ed1d]
004012aa  6821ed4000         push    0x40ed21
004012af  e82c800000         call    sub_4092e0
004012b4  8bf8               mov     edi, eax
004012b6  e8b97e0000         call    sub_409174
004012bb  57                 push    edi
004012bc  6821ef4000         push    0x40ef21
004012c1  e80a7f0000         call    sub_4091d0
004012c6  e8797f0000         call    sub_409244
004012cb  681deb4000         push    0x40eb1d
004012d0  6a20               push    0x20
004012d2  50                 push    eax  {data_4176c8}
004012d3  e808800000         call    sub_4092e0
004012d8  681deb4000         push    0x40eb1d
004012dd  681de74000         push    0x40e71d
004012e2  e811080000         call    lstrcpyA
004012e7  6817c94000         push    0x40c917  {"-OMGWTFBBQ"}
004012ec  681de74000         push    0x40e71d
004012f1  e8f6070000         call    lstrcatA
004012f6  6800020000         push    0x200
004012fb  6829f34000         push    0x40f329
00401300  68f7030000         push    0x3f7
00401305  ff7508             push    dword [ebp+0x8 {arg1}]
00401308  e849070000         call    GetDlgItemTextA
0040130d  681de74000         push    0x40e71d
00401312  6829f34000         push    0x40f329
00401317  e8d6070000         call    lstrcmpiA
0040131c  0bc0               or      eax, eax
0040131e  7416               je      0x401336

00401320  6a10               push    0x10
00401322  6894cf4000         push    0x40cf94  {"BarbecueMe"}
00401327  6860cf4000         push    0x40cf60  {"OMG  Sausages burning\r\nYour serial is not correct"}
0040132c  ff7508             push    dword [ebp+0x8 {arg1}]
0040132f  e83a070000         call    MessageBoxA
00401334  eb5f               jmp     0x401395

00401336  6a40               push    0x40
00401338  68c4cf4000         push    0x40cfc4  {"BarbecueMe"}
0040133d  68a0cf4000         push    0x40cfa0  {"w00t, w00t\r\nYour serial is correct"}
00401342  ff7508             push    dword [ebp+0x8 {arg1}]
00401345  e824070000         call    MessageBoxA
0040134a  c9                 leave    {__saved_ebp}
0040134b  c21000             retn    0x10 {__return_addr}
Click to expand

Let’s start analyzing it.

Hex-ifying Variables

First, the program retrieves the value for the Name field through GetDlgItemTextA and save it to a variable at the address 0x40e11d. According to the documentation for GetDlgItemTextA at MicrosoftLearn jwmsft GetDlgItemTextA function (winuser.h) - Win32 apps, the third argument is the buffer to receive the text, and that corresponds to the variable at 0x40e11d.

004011ff  6800020000         push    0x200
00401204  681de14000         push    0x40e11d
00401209  68f3030000         push    0x3f3
0040120e  ff7508             push    dword [ebp+0x8]
00401211  e840080000         call    GetDlgItemTextA
Click to expand

Then the length of the Name text is calculated at 0x40e11d and saved to eax, then eax is multiplied by 0x539.

00401216  681de14000         push    0x40e11d
0040121b  e8de080000         call    lstrlenA
00401220  a31ded4000         mov     dword [data_40ed1d], eax
00401225  69c039050000       imul    eax, eax, 0x539
Click to expand

The value of eax is now converted to a hexadecimal string using wsprintfA and saved into a variable at 0x40e51d.

The wsprintfA function takes three argument, a buffer to store the output, a format string that defines how to format the text, and a variable list of values to insert into that format. Here, the variable at 0x40c909 holds the format string and has a value of %.X. From the Remarks section at MicrosoftLearn jwmsft wsprintfA function (winuser.h) - Win32 apps, the resulting string of this wsprintfA call will be an uppercase hexadecimal string.

0040122b  50                 push    eax
0040122c  6809c94000         push    0x40c909
00401231  681de54000         push    0x40e51d
00401236  e8f1070000         call    wsprintfA
Click to expand

There’s this sub_4092e0 function which takes the Name string at 0x40e11d, Name string length at 0x40e11d, and a variable at 0x40e31d.

0040123b  681de34000         push    0x40e31d
00401240  ff351ded4000       push    dword [data_40ed1d]
00401246  681de14000         push    0x40e11d
0040124b  e890800000         call    sub_4092e0
Click to expand

This function is actually for converting raw bytes to an uppercase hex string, and write the result to a variable at 0x40e31d.

004092e0    void* __fastcall sub_4092e0(int32_t arg1, int32_t arg2, char* arg3, int32_t arg4, void* arg5)

004092e0  55                 push    ebp {__saved_ebp}
004092e1  8bec               mov     ebp, esp {__saved_ebp}
004092e3  57                 push    edi {__saved_edi}
004092e4  56                 push    esi {__saved_esi}
004092e5  53                 push    ebx {__saved_ebx}
004092e6  8b5d0c             mov     ebx, dword [ebp+0xc {arg4}]
004092e9  8b7d10             mov     edi, dword [ebp+0x10 {arg5}]
004092ec  85db               test    ebx, ebx
004092ee  8b7508             mov     esi, dword [ebp+0x8 {arg3}]
004092f1  7436               je      0x409329

004092f3  0fb606             movzx   eax, byte [esi]
004092f6  8bc8               mov     ecx, eax
004092f8  83c702             add     edi, 0x2
004092fb  c1e904             shr     ecx, 0x4
004092fe  83e00f             and     eax, 0xf
00409301  83e10f             and     ecx, 0xf
00409304  83f80a             cmp     eax, 0xa
00409307  1bd2               sbb     edx, edx
00409309  83d000             adc     eax, 0x0
0040930c  8d44d037           lea     eax, [eax+edx*8+0x37]
00409310  83f90a             cmp     ecx, 0xa
00409313  1bd2               sbb     edx, edx
00409315  83d100             adc     ecx, 0x0
00409318  c1e008             shl     eax, 0x8
0040931b  8d4cd137           lea     ecx, [ecx+edx*8+0x37]
0040931f  0bc1               or      eax, ecx
00409321  46                 inc     esi
00409322  668947fe           mov     word [edi-0x2], ax
00409326  4b                 dec     ebx
00409327  75ca               jne     0x4092f3

00409329  8bc7               mov     eax, edi
0040932b  c60700             mov     byte [edi], 0x0
0040932e  2b4510             sub     eax, dword [ebp+0x10 {arg5}]
00409331  5b                 pop     ebx {__saved_ebx}
00409332  5e                 pop     esi {__saved_esi}
00409333  5f                 pop     edi {__saved_edi}
00409334  c9                 leave    {__saved_ebp}
00409335  c20c00             retn    0xc {__return_addr}
Click to expand

We then encountered some calls to lstrcpyA and lstrcatA. These are just copying the hexadecimal Name string to 0x40e71d, and concatenate it with the hexadecimal Name string length at 0x40e51d.

00401250  681de34000         push    0x40e31d
00401255  681de74000         push    0x40e71d
0040125a  e899080000         call    lstrcpyA
0040125f  681de54000         push    0x40e51d
00401264  681de74000         push    0x40e71d
00401269  e87e080000         call    lstrcatA
Click to expand

Let’s sum all of this up a little bit. Here’s what we’ve got:

  • The Name EditBox value is retrieved and saved into 0x40e11d.
  • The string length of the Name value is calculated, multiplied by 0x539, and converted into an uppercase hexadecimal string. We’ll refer to this as hex_name_length.
  • The Name value is converted into a hexadecimal string as well, and then gets concatenated with hex_name_length. We’ll call the concatenated string hex_name_and_length.

An Encryption Algorithm?

I really don’t know what the second GetDlgItemTextA is doing here though. It stores the retrieved value into a variable at 0x40F121, which is then passed to sub_407510, but function doesn’t appear to do anything related to our serial construction. So I’ll just skip this.

00407510    void __stdcall sub_407510(int32_t arg1, int32_t arg2)

00407510  55                 push    ebp {__saved_ebp}
00407511  8bec               mov     ebp, esp {__saved_ebp}
00407513  56                 push    esi {__saved_esi}
00407514  57                 push    edi {__saved_edi}
00407515  53                 push    ebx {__saved_ebx}
00407516  bf78664100         mov     edi, data_416678
0040751b  bef8d04000         mov     esi, 0x40d0f8
00407520  b900040000         mov     ecx, 0x400
00407525  f3a5               rep movsd dword [edi], [esi]  {0x0}
00407527  8b5d0c             mov     ebx, dword [ebp+0xc {arg2}]
0040752a  8b7508             mov     esi, dword [ebp+0x8 {arg1}]
0040752d  83fb04             cmp     ebx, 0x4
00407530  0f82a9000000       jb      0x4075df

00407536  83fb38             cmp     ebx, 0x38
00407539  0f87a0000000       ja      0x4075df

0040753f  33c9               xor     ecx, ecx  {0x0}
00407541  33ff               xor     edi, edi  {0x0}

00407543  33d2               xor     edx, edx  {0x0}
00407545  33c0               xor     eax, eax  {0x0}

00407547  c1e008             shl     eax, 0x8
0040754a  0a0437             or      al, byte [edi+esi]
0040754d  47                 inc     edi
0040754e  3bfb               cmp     edi, ebx
00407550  7202               jb      0x407554

00407552  33ff               xor     edi, edi  {0x0}

00407554  42                 inc     edx
00407555  83fa04             cmp     edx, 0x4
00407558  75ed               jne     0x407547

0040755a  33048db0d04000     xor     eax, dword [ecx*4+0x40d0b0]
00407561  89048d30664100     mov     dword [ecx*4+0x416630], eax
00407568  41                 inc     ecx
00407569  83f912             cmp     ecx, 0x12
0040756c  75d5               jne     0x407543

0040756e  33db               xor     ebx, ebx  {0x0}
00407570  891d78764100       mov     dword [data_417678], ebx  {0x0}
00407576  891d7c764100       mov     dword [data_41767c], ebx  {0x0}

0040757c  6878764100         push    data_417678
00407581  6878764100         push    data_417678
00407586  e85d000000         call    sub_4075e8
0040758b  0fc8               bswap   eax
0040758d  0fca               bswap   edx
0040758f  8904dd30664100     mov     dword [ebx*8+0x416630], eax
00407596  8914dd34664100     mov     dword [ebx*8+0x416634], edx
0040759d  43                 inc     ebx
0040759e  83fb09             cmp     ebx, 0x9
004075a1  75d9               jne     0x40757c  {"hxvA"}

004075a3  33ff               xor     edi, edi  {0x0}

004075a5  33db               xor     ebx, ebx  {0x0}

004075a7  6878764100         push    data_417678
004075ac  6878764100         push    data_417678
004075b1  e832000000         call    sub_4075e8
004075b6  0fc8               bswap   eax
004075b8  0fca               bswap   edx
004075ba  8984df78664100     mov     dword [edi+ebx*8+0x416678], eax
004075c1  8994df7c664100     mov     dword [edi+ebx*8+0x41667c], edx
004075c8  43                 inc     ebx
004075c9  81fb80000000       cmp     ebx, 0x80
004075cf  75d6               jne     0x4075a7  {"hxvA"}

004075d1  81c700040000       add     edi, 0x400
004075d7  81ff00100000       cmp     edi, 0x1000
004075dd  75c6               jne     0x4075a5

004075df  5b                 pop     ebx {__saved_ebx}
004075e0  5f                 pop     edi {__saved_edi}
004075e1  5e                 pop     esi {__saved_esi}
004075e2  c9                 leave    {__saved_ebp}
004075e3  c20800             retn    0x8 {__return_addr}
Click to expand

Next, the hex_name_and_length string is passed to sub_4075e8 as arg1. At first glance I couldn’t figure out what this function is doing, so I look it up and it turns out that this is actually the Blowfish algorithm!

004075e8    int32_t __stdcall sub_4075e8(int32_t* arg1, int32_t* arg2)

004075e8  55                 push    ebp {__saved_ebp}
004075e9  8bec               mov     ebp, esp {__saved_ebp}
004075eb  56                 push    esi {__saved_esi}
004075ec  57                 push    edi {__saved_edi}
004075ed  53                 push    ebx {__saved_ebx}
004075ee  8b7508             mov     esi, dword [ebp+0x8 {arg1}]
004075f1  33c9               xor     ecx, ecx  {0x0}
004075f3  8b06               mov     eax, dword [esi]
004075f5  8b5604             mov     edx, dword [esi+0x4]
004075f8  0fc8               bswap   eax
004075fa  0fca               bswap   edx

004075fc  33048d30664100     xor     eax, dword [ecx*4+0x416630]
00407603  8bf8               mov     edi, eax
00407605  8bf0               mov     esi, eax
00407607  c1ef18             shr     edi, 0x18
0040760a  c1ee10             shr     esi, 0x10
0040760d  81e6ff000000       and     esi, 0xff
00407613  8b1cbd78664100     mov     ebx, dword [edi*4+0x416678]
0040761a  031cb5786a4100     add     ebx, dword [esi*4+0x416a78]
00407621  0fb6fc             movzx   edi, ah
00407624  0fb6f0             movzx   esi, al
00407627  331cbd786e4100     xor     ebx, dword [edi*4+0x416e78]
0040762e  031cb578724100     add     ebx, dword [esi*4+0x417278]
00407635  33da               xor     ebx, edx
00407637  41                 inc     ecx
00407638  8bd0               mov     edx, eax
0040763a  83f910             cmp     ecx, 0x10
0040763d  8bc3               mov     eax, ebx
0040763f  75bb               jne     0x4075fc

00407641  92                 xchg    edx, eax
00407642  331570664100       xor     edx, dword [data_416670]
00407648  330574664100       xor     eax, dword [data_416674]
0040764e  8b750c             mov     esi, dword [ebp+0xc {arg2}]
00407651  0fc8               bswap   eax
00407653  0fca               bswap   edx
00407655  8906               mov     dword [esi], eax
00407657  895604             mov     dword [esi+0x4], edx
0040765a  5b                 pop     ebx {__saved_ebx}
0040765b  5f                 pop     edi {__saved_edi}
0040765c  5e                 pop     esi {__saved_esi}
0040765d  c9                 leave    {__saved_ebp}
0040765e  c20800             retn    0x8 {__return_addr}
Click to expand

I don’t have any prior cryptography knowledge, but I’ll try to understand the algorithm.

In this first part, the function initialized a counter variable ecx. The function takes 8 bytes of input (arg1), or hex_name_and_length in this case, and splits this input into two halves of 4 bytes and store them into eax and edx.

push    ebp {__saved_ebp}
mov     ebp, esp {__saved_ebp}
push    esi {__saved_esi}
push    edi {__saved_edi}
push    ebx {__saved_ebx}
mov     esi, dword [ebp+0x8 {arg1}]
xor     ecx, ecx             ; ecx = 0, used as a loop counter
mov     eax, [esi]           ; load first 4 bytes of input into eax
mov     edx, [esi+4]         ; load next 4 bytes of input into edx
bswap   eax                  ; reverse byte order of eax
bswap   edx                  ; reverse byte order of edx
Click to expand

Here comes the loop of 16 iterations.

xor     eax, [ecx*4+0x416630] ; XOR eax with a 4-byte (32-bit) value
mov     edi, eax              ; copy eax into edi
mov     esi, eax              ; copy eax into esi again
shr     edi, 24               ; extract the highest byte of eax into edi
shr     esi, 16               ; extract the next-highest byte of eax into esi
and     esi, 0xff             ; keep only the lower 8 bits of esi
mov     ebx, [edi*4+0x416678] ; look up a 4-byte value using that high byte
add     ebx, [esi*4+0x416A78] ; add another value based on the next byte
movzx   edi, ah               ; extract the third byte of eax
movzx   esi, al               ; extract the lowest byte of eax
xor     ebx, [edi*4+0x416E78] ; XOR ebx with another value
add     ebx, [esi*4+0x417278] ; add yet another value
xor     ebx, edx              ; XOR ebx with edx
inc     ecx                   ; increase loop counter
mov     edx, eax
cmp     ecx, 0x10             ; check if 16 iterations are done
mov     eax, ebx
jne     0x4075fc              ; if not done, repeat the loop
Click to expand

After the loop finishes, the function do another swap and a couple of XOR operations with some constants at 0x416670 and 0x416674.

xchg    edx, eax              ; swap eax and edx
xor     edx, [0x416670]       ; XOR edx with another constant
xor     eax, [0x416674]       ; XOR eax with another constant
Click to expand

Finally, the results are written into the output buffer at arg2.

mov     esi, [ebp+0xC]        ; esi = arg2 (pointer to output)
bswap   eax                   ; reverse byte order of eax
bswap   edx                   ; reverse byte order of edx
mov     [esi], eax            ; write first 4 bytes to output
mov     [esi+4], edx          ; write next 4 bytes to output
pop     ebx
pop     edi
pop     esi
leave
retn    8
Click to expand

If you look at how the Blowfish algorithm is implemented in other places, you can see that it uses a P-array consisting of 18 4-byte subkeys (18-entry P-array) and four 4-byte S-boxes with 256 entries (256-entry S-boxes). The values in the P-array and the S-boxes are used for lots of operations inside the 16-iteration loop.

You may notice several addresses inside the loop from the above disassembly, such as [ecx*4+0x416630], [edi*4+0x416678], [esi*4+0x416A78], [edi*4+0x416E78], and [esi*4+0x417278]. Upon comparing the offsets, you'll notice that the difference between the first two is 0x48 (or 72 bytes), whereas the difference between the subsequent offsets is 0x400 (or 1024 bytes). This aligns perfectly with the size of the Blowfish P-array and S-boxes: the P-array is 184=7218 * 4 = 72 bytes, while each S-box is 2564=1024256 * 4 = 1024 bytes.

After the loop, there are some addresses as well. 0x416670 and 0x416674 were in fact the last two entries of the P-array (at indices 16 and 17). We know that by doing some simple offset calculations from the base address of the P-array at 0x416630: 0x416630 + (16 * 4) = 0x416670 and 0x416630 + (17 * 4) = 0x416674.

But these arrays only contain zeroes from their initialization!

Wait, the 0x416678 address looks familiar. I’ve seen it somewhere previously. Yes, it’s in the sub_407510 function (that I skipped earlier LOL) where it attempted to copy over the S-boxes at 0x40d0f8 to 0x416678:

00407516  bf78664100         mov     edi, data_416678
0040751b  bef8d04000         mov     esi, 0x40d0f8
00407520  b900040000         mov     ecx, 0x400
00407525  f3a5               rep movsd dword [edi], [esi]  {0x0}
Click to expand

We know this code snippet is copying over 4 S-boxes because REP MOVSD moves 4 bytes (or a dword) per iteration, and mov ecx, 0x400 sets the repeat count to 1024 (0x400) dwords. So it’s basically moving 4096 (0x1000) bytes, which is equivalent to the total size of 4 S-boxes that we’ve seen earlier. If we look at 0x40d0f8, these are the exact values of the S-boxes!

You can compare them with some implementations of Blowfish on the internet:

But what about the P-array? Yes, there is some copying to 0x416630 in sub_407510 as well:

0040755a  33048db0d04000     xor     eax, dword [ecx*4+0x40d0b0]
00407561  89048d30664100     mov     dword [ecx*4+0x416630], eax
Click to expand

But then if we look closely at the function, the values in the P-array are set to the hardcoded values at 0x40d0b0 XOR-ed with some bytes only if arg2 ≥ 0x4 && a2 ≤ 0x38.

0040752d  83fb04             cmp     ebx, 0x4
00407530  0f82a9000000       jb      0x4075df

00407536  83fb38             cmp     ebx, 0x38
00407539  0f87a0000000       ja      0x4075df
Click to expand

0x40d0b0 definitely contains the exact 18 entries of the P-array. So if those conditions are met, we would have to deal with a XOR-ed P-array.

Does the values at 0x416630 even set? Tracing back to the _start() function, something is very strange about the second GetDlgItemTextA that we’ve seen earlier:

0040126e  6800020000         push    0x200
00401273  6821f14000         push    0x40f121
00401278  680dc94000         push    0x40c90d  {"KPCR tapz"}
0040127d  ff7508             push    dword [ebp+0x8 {arg1}]
00401280  e8d1070000         call    GetDlgItemTextA
00401285  50                 push    eax
00401286  6821f14000         push    0x40f121
0040128b  e880620000         call    sub_407510
Click to expand

If you look at the second argument of GetDlgItemTextA which corresponds to the ID of the text field, which should be a hardcoded integer, not a memory offset like 0x40c90d. If we treat 0x40c90d as an integer, its value would be over 4 million, far too large for a typical dialog control ID. So this GetDlgItemTextA should fail, no text is retrieved, and the return length is 0. As a result, arg2 of sub_407510 is 0, so the P-array remains all zeroes! So our Blowfish algorithm has no impact from the P-array XORs, and we can easily omit the P-array when we write the keygen!

Let’s get back to the _start() function. The Blowfish-encrypted hex_name_and_length that sub_4075e8 stores at 0x40ed21 is converted into an uppercase hexadecimal string using sub_4092e0 that we’ve analyzed above. The converted string that we’ll be refering as hex_blowfish is then stored in a variable at 0x40ef21.

0040129f  6821ef4000         push    0x40ef21
004012a4  ff351ded4000       push    dword [data_40ed1d]
004012aa  6821ed4000         push    0x40ed21
004012af  e82c800000         call    sub_4092e0
004012b4  8bf8               mov     edi, eax
Click to expand

At this point, I noticed something strange: sub_4092e0 takes two arguments — the encrypted hex_name_and_length value and the length of the Name string (which comes from the dialog’s Name field). The encrypted hex_name_and_length (let’s call it blowfish_output) is always 8 bytes long, but the function is given name_len (the length of the Name string) as if that were the size of the data. What if name_len is greater than 8?

If you take a closer look at sub_4092e0, you'll notice that there’s no bounds checking. The function just reads past the real data starting from 0x40ed21 and convert whatever bytes are in memory there. The variable at 0x40ed21 is initialized with 512 bytes (defined as db 3 dup(0) + dd 7Fh dup(0) + db 0, which totals to 512 bytes: 3+4127+13 + 4 * 127 + 1). As a result, the function appends zero bytes to the end of the hexadecimal string as a form of padding.

Hashing, And Serial Found!

The next 3 functions performs SHA-256 hashing of the hex_blowfish string at 0x40ef21. At this point I’m too lazy to explain the details inside these functions, but anyways here’s the basic idea:

  • sub_409174: Resets everything and sets the SHA-256 starting values.
  • sub_4091d0: Adds the input data from the hex_blowfish string to the hash and processes it in 64-byte chunks.
  • sub_409244: Finishes the hash, adds padding, swaps bytes, and returns the 32-byte result.

Then sub_4092e0 is called to turn those 32 bytes into a 64-character hex string at 0x40eb1d.

004012b6  e8b97e0000         call    sub_409174
004012bb  57                 push    edi
004012bc  6821ef4000         push    0x40ef21
004012c1  e80a7f0000         call    sub_4091d0
004012c6  e8797f0000         call    sub_409244
004012cb  681deb4000         push    0x40eb1d
004012d0  6a20               push    0x20
004012d2  50                 push    eax  {data_4176c8}
004012d3  e808800000         call    sub_4092e0
Click to expand

And finally, the hex string is appended -OMGWTFBBQ at the end, and that’s the final serial key!

004012d8  681deb4000         push    0x40eb1d
004012dd  681de74000         push    0x40e71d
004012e2  e811080000         call    lstrcpyA
004012e7  6817c94000         push    0x40c917  {"-OMGWTFBBQ"}
004012ec  681de74000         push    0x40e71d
004012f1  e8f6070000         call    lstrcatA
Click to expand

Once again, let’s sum all of this up (with an example):

Steps Example
1. The Name EditBox value is retrieved. name = crackmes_are_fun
2. The length of the Name value (name_len) is calculated, multiplied by 0x539, and converted into an uppercase hexadecimal string (hex_name_length). name_len = 16
name_len_multiplied = 16 * 0x539 = 0x5390
hex_name_length = 5390
3. The Name value is converted into a hexadecimal string, then concatenated with hex_name_length to form hex_name_and_length. hex_name = 637261636B6D65735F6172655F66756E
hex_name_and_length = 637261636B6D65735F6172655F66756E5390
4. Blowfish encryption is applied to the hex_name_and_length string, resulting in the blowfish_output. blowfish_output = 0x2D8CC2A1AEA88C10
5. The blowfish_output is converted into a hexadecimal string, hex_blowfish. If name_len > 8, zero-padding is added to the end of the string. hex_blowfish = 2D8CC2A1AEA88C10
Since nameLen > 8, add zero-paddings:
hex_blowfish = 2D8CC2A1AEA88C100000000000000000.
6. The hex_blowfish string is then processed through the SHA-256 hashing function, and the string OMGWTFBBQ is appended to produce the final serial key. hashed_blowfish = 013C488CD032A96B4E9BEA7A68897B92D1EFC7404B13E8252CE9C09A7EBC175C
serial = 013C488CD032A96B4E9BEA7A68897B92D1EFC7404B13E8252CE9C09A7EBC175C-OMGWTFBBQ

After verifying that the logic is correct, we can create a keygen for any input name!

Let’s Create A Keygen

I think turning the above logic into a keygen in Python should be straighforward. I’ve written a part of it here, using variable names and comments similar to my above analysis:

import hashlib

def blowfish_encrypt(input_string):
    block = input_string[:8]               # take only first 8 bytes
    eax = int.from_bytes(block[:4], 'big') # load first 4 bytes of input into eax
    edx = int.from_bytes(block[4:], 'big') # load next 4 bytes of input into edx

    for _ in range(16):
        edi = (eax >> 24) & 0xff           # extract the highest byte of eax into edi
        esi = (eax >> 16) & 0xff           # extract the next-highest byte of eax into esi

        ebx = S0[edi]                      # look up S0[edi] into ebx
        ebx += S1[esi]                     # add S1[esi] to ebx
        ebx &= 0xFFFFFFFF

        edi = (eax >> 8) & 0xff            # extract the next byte of eax into edi
        esi = eax & 0xff                   # extract the lowest byte of eax into esi

        ebx ^= S2[edi]                     # XOR S2[edi] with ebx
        ebx &= 0xFFFFFFFF

        ebx += S3[esi]                     # add S3[esi] to ebx
        ebx &= 0xFFFFFFFF

        ebx ^= edx                         # XOR edx with ebx
        edx = eax & 0xFFFFFFFF
        eax = ebx & 0xFFFFFFFF

    eax, edx = edx, eax                    # swap eax and edx

    # write back the result
    return eax.to_bytes(4, 'big') + edx.to_bytes(4, 'big')

def generate_serial(name):
    name = name.encode('ascii')
    name_len = len(name)
    name_len_multiplied = name_len * 1337
    hex_name_length = f"{name_len_multiplied:X}"   # convert to hex string

    hex_name = ''.join(f"{b:02X}" for b in name)
    hex_name_and_length = hex_name + hex_name_length

    blowfish_output = blowfish_encrypt(hex_name_and_length.encode('ascii'))
    blowfish_output = blowfish_output.ljust(name_len, b'\x00')    # this is the padding part
    hex_blowfish = ''.join(f"{b:02X}" for b in blowfish_output)

    hashed_blowfish = hashlib.sha256(hex_blowfish.encode('ascii')).hexdigest().upper()
    serial = hashed_blowfish + "-OMGWTFBBQ"

    return serial

print(generate_serial("crackmes_are_fun"))
Click to expand

The script can’t be run yet, since it’s missing the S-boxes in S0, S1, S2, and S3. Let’s just straight up copying the S-boxes from this Blowfish implementation:

The keygen is now completed!

import hashlib

S0 = [
0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e,
0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e,
0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a,
0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032,
0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0,
0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe,
0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7,
0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09,
0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8,
0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573,
0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8,
0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c,
0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9,
0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af,
0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915,
0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a
]

S1 = [
0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e,
0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1,
0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd,
0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331,
0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87,
0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd,
0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3,
0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960,
0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84,
0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e,
0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281,
0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, 0xcdb30aeb, 0x532e3054, 0x8d948e4e, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73,
0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250,
0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061,
0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc,
0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7
]

S2 = [
0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840,
0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb,
0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b,
0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b,
0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc,
0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115,
0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e,
0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b,
0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c,
0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe,
0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61,
0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c,
0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169,
0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62,
0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc,
0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0
]

S3 = [
0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4,
0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22,
0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59,
0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c,
0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd,
0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f,
0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166,
0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47,
0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048,
0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7,
0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525,
0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e,
0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299,
0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a,
0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060,
0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6
]
    
def blowfish_encrypt(input_string):
    block = input_string[:8]               # take only first 8 bytes
    eax = int.from_bytes(block[:4], 'big') # load first 4 bytes of input into eax
    edx = int.from_bytes(block[4:], 'big') # load next 4 bytes of input into edx

    for _ in range(16):
        edi = (eax >> 24) & 0xff           # extract the highest byte of eax into edi
        esi = (eax >> 16) & 0xff           # extract the next-highest byte of eax into esi

        ebx = S0[edi]                      # look up S0[edi] into ebx
        ebx += S1[esi]                     # add S1[esi] to ebx
        ebx &= 0xFFFFFFFF

        edi = (eax >> 8) & 0xff            # extract the next byte of eax into edi
        esi = eax & 0xff                   # extract the lowest byte of eax into esi

        ebx ^= S2[edi]                     # XOR S2[edi] with ebx
        ebx &= 0xFFFFFFFF

        ebx += S3[esi]                     # add S3[esi] to ebx
        ebx &= 0xFFFFFFFF

        ebx ^= edx                         # XOR edx with ebx
        edx = eax & 0xFFFFFFFF
        eax = ebx & 0xFFFFFFFF

    eax, edx = edx, eax                    # swap eax and edx

    # write back the result
    return eax.to_bytes(4, 'big') + edx.to_bytes(4, 'big')

def generate_serial(name):
    name = name.encode('ascii')
    name_len = len(name)
    name_len_multiplied = name_len * 1337
    hex_name_length = f"{name_len_multiplied:X}"   # convert to hex string

    hex_name = ''.join(f"{b:02X}" for b in name)
    hex_name_and_length = hex_name + hex_name_length

    blowfish_output = blowfish_encrypt(hex_name_and_length.encode('ascii'))
    blowfish_output = blowfish_output.ljust(name_len, b'\x00')    # this is the padding part
    hex_blowfish = ''.join(f"{b:02X}" for b in blowfish_output)

    hashed_blowfish = hashlib.sha256(hex_blowfish.encode('ascii')).hexdigest().upper()
    serial = hashed_blowfish + "-OMGWTFBBQ"

    return serial

print(generate_serial("crackmes_are_fun"))
Click to expand

Collisions?

Well this is actually an unintended part LOL. I just discovered (by accident) that the key construction algorithm is susceptible to collisions right while I was writing the explanation for the Blowfish implementation!

The idea is that our Blowfish encryption above only processes the first 8 bytes of the hex_name_and_length string, which correspond to the hexadecimal representation of the first 4 characters of the input name. Also, the padding and hex conversion for the SHA-256 input depend just on the name's length. So if two names start with the same 4 characters and have the same total length, they'll generate the same serial number.

Try running the script with names like overwrite_instruction_pointer and overcome_assembly_obfuscation. They should yield the same serial!

Thoughts

I've been relying on decompilers when I'm working through problems, and honestly, raw Assembly still makes me uncomfortable. So I figured this crackme was a good opportunity to force myself to actually understand it.
I ended up spending hours going through the functions line by line. But you know what? Writing this whole thing up actually helped. Breaking it down and explaining it made Assembly feel way less intimidating than it used to. I'm starting to think I might actually get this now!

Kudos to the author for hand-crafting such an educational crackme!