
A study of the defense of the game Limbo. Keygen
- Tutorial

Hello. Many people know about this wonderful game - LIMBO ! You even probably bought it in Steam , or downloaded it from torrents ...
I also bought it once ( which I advise you too ! ), And went through). But, as always, this was not enough for me, and, out of sports interest, I decided to study its protection. So there was a keygen to the game LIMBO .
In this article I will tell and show you how I did it.
Before you begin, remember: all the actions you perform at your own peril and risk. Respect the work of game developers.
Stage One: Examination of the Patient
The full installer of the game can be downloaded here . After installing the game, first of all, as usual, we find out what the main executable file is written on. I will use for this ExeInfo PE .

We see: Visual Studio 2008 . IDA Pro does a great job of it, so we'll send it there. I will use a bunch of IDA Pro + HexRays , i.e. with a decompiler - to speed up the work.
Stage Two: What Are We Looking For?
First of all, let's give Idea to analyze limbo.exe - the main executable file of the game.
Next, you need to determine what exactly we, in fact, want to find here. Run the game:

We see the magic inscription " UNLOCK FULL GAME ". Click on it. Next, we are expected by an unexpected person (at least, when I first selected this menu item, I expected to see the input field on the graphic engine of the game, or something like that, but it turned out to be much simpler ...):

Yes Yes! It is an ordinary window! It’s easier for us. Let's try to enter something, and press Unlock . Something like this:

Well , let's look at the text in the IDA so that we can build on it and find a place to check. And then a bummer was waiting for me ...
According to the text of the message in the error window, Ida did not find anything for me! Content search through Total Commander told me the same thing . The message may be encrypted. You can try to find a window call based on a MessageBoxA / W call . But, I went the other way, which, for some reason, is described in few articles.
Stage Three: Click Me
We will proceed as follows. We’ll open any resource editor that is convenient for you, drag an exe-worker into it, find the key input dialog box, and in it the Unlock button . No sooner said than done:

On the screen, I highlighted the ID of our button. By it we will search exactly where the click is processed. Open Ida , press Alt + I ( Search -> immediate value ...), enter the number 203 (without 0x , because decimal), and see what happens. And this was found:

See those lines that Ida marked as ; nIDDlgItem ? We’ll start with them. Double-click on the first of these results:

With a green arrow, I designated the place that Ida pointed to , and a little lower ( habit: scroll above / below the desired location ) - the arrow indicates the place of calling one interesting API function: GetDlgItemTextA . Judging
Why didn’t I immediately search by input field ID ? Of course, it was possible to do so. But, you never know what happens after the button is pressed, even before the text is subtracted from the field.
So, let’s track where the received serial goes. We scroll through the listing to see the whole place the API function was called:

My “ soapy ” look tells me that the received buffer ( Ida designated it as var_134 ) is passed directly to the function following the call to GetDlgItemTextA , which returns a zero or non-zero value in al (similar to the result of checking the key). Let's check the guess ...
Stage Four: Decompilation
We go into the function. We see there a jump to another address - go to it. We see the normal code, so feel free to click F5 there (call HexRays Decompiler ).
Decompilation result
bool __cdecl sub_48D410(int a1)
{
int v1; // esi@7
char *v2; // ebp@7
unsigned int v3; // edi@7
int v4; // ST28_4@9
int v5; // edx@9
int v6; // eax@12
bool result; // al@12
char v8; // [sp+4h] [bp-44h]@8
char v9; // [sp+Ch] [bp-3Ch]@12
char v10; // [sp+1Ch] [bp-2Ch]@7
char v11; // [sp+3Ch] [bp-Ch]@12
if ( strlen((const char *)a1) != 37
|| *(_BYTE *)(a1 + 5) != 45
|| *(_BYTE *)(a1 + 11) != 45
|| *(_BYTE *)(a1 + 17) != 45
|| *(_BYTE *)(a1 + 23) != 45
|| *(_BYTE *)(a1 + 30) != 45 )
{
result = 0;
}
else
{
v1 = 0;
v2 = &v10;
v3 = 0;
do
{
v8 = *(_BYTE *)(v3 + a1);
if ( v8 != 45 )
{
v4 = (char)sub_412EBD(v8);
v1 += v4 << 5 * (3 - v5);
if ( v5 == 3 )
{
v2 += sprintf(v2, "%05lx", v1);
v1 = 0;
}
}
++v3;
}
while ( v3 < 0x25 );
v6 = sub_40C48C(&v10, 32);
sprintf(&v9, "%08x", v6);
result = strcmp(&v9, &v11) == 0;
}
return result;
}
Now you can try to bring this code to a more adequate one.
First of all, we notice that the input parameter is of type int , which is not entirely true. Let's designate it as " char * ". To do this, stand in the name of the function and press the Y ( Set item type ) key there . We fix the type and name of the input parameter (I called it as key ).
Next ... We see the line:
if ( strlen(key) != 37 || key[5] != 45 || key[11] != 45 || key[17] != 45 || key[23] != 45 || key[30] != 45 )
Because our input parameter is a string, let's fix it in the places where the characters of the key are compared with numbers, and compare them with the characters. To do this, on each of these numbers, press R ( Char ). Already better:
if ( strlen(key) != 37 || key[5] != '-' || key[11] != '-' || key[17] != '-' || key[23] != '-' || key[30] != '-' )
Now the loop:
Cycle number 1
v1 = 0;
v2 = &v10;
v3 = 0;
do
{
v8 = key[v3];
if ( v8 != 45 )
{
v4 = (char)sub_412EBD(v8);
v1 += v4 << 5 * (3 - v5);
if ( v5 == 3 )
{
v2 += sprintf(v2, "%05lx", v1);
v1 = 0;
}
}
++v3;
}
while ( v3 < 0x25 );
For clarity, we give v3 the name i , because It looks like it is being used as an iterator. Rename by pressing the name key N ( Name ).
We notice that in the loop, each character is taken from the key, and transferred to a function that is still unknown to us. I propose to find out what this function is. Double click on it. We see there a call to another function, go there. And, here it is - processing a single character! ( There is a lot of work for the R key here , but I will only show the result of processing immediately ).
Convert_char function
char __cdecl convert_char(char C)
{
char _C; // al@1
_C = C;
if ( C < '0' )
return -1;
if ( C <= '9' )
return C - '0';
if ( C < 'A' )
return -1;
if ( C <= 'Z' )
{
if ( C != 'I' && C != 'L' && C != 'O' && C != 'U' )
{
if ( C >= 'U' )
_C = C - 1;
if ( _C >= 'O' )
--_C;
if ( _C >= 'L' )
--_C;
if ( _C >= 'I' )
--_C;
return _C - '7';
}
return -1;
}
if ( C < 'a' || C > 'z' || C == 'i' || C == 'l' || C == 'o' || C == 'u' )
return -1;
if ( C >= 'u' )
_C = C - 1;
if ( _C >= 'o' )
--_C;
if ( _C >= 'l' )
--_C;
if ( _C >= 'i' )
--_C;
return _C - 'W';
}
Perfectly! Now we go back with the Esc key to the main function. We notice that the IDA itself redefined for us the type of result returned by the symbol processing function. We name it further, designate types, and get the following loop code:
Cycle number 2
sum = 0;
x5buf = v10;
i = 0;
do
{
C = key[i];
if ( C != '-' )
{
new_c = j_convert_char(C);
sum += new_c << 5 * (3 - itr);
if ( itr == 3 )
{
x5buf += sprintf(x5buf, "%05lx", sum);
sum = 0;
}
}
++i;
}
while ( i < 0x25 );
If you notice, then there is one interesting decompiler bug. We see that the variable designated by me as itr is not incremented at all. To find out what actually happens, click RMB -> Copy to assembly , and see where our itr is used . We find out: it is incremented directly in this cycle (which was to be expected), and before the cycle it is reset. We will take this into account when writing keygen.
Now the second part of the key verification function ... We still have one unexplored function, which, by the way, is very similar to the CRC32 counting function . The result of processing (albeit in a hurry, but readable):
crc32
int __cdecl calc_crc32(char *my_key, int len)
{
int i; // ebp@1
unsigned int _xor; // ecx@1
char *_my_key; // edi@2
char C; // al@3
signed int mask; // edx@3
int B; // esi@3
bool bit; // al@4
int crc32; // eax@10
signed int _len; // edx@10
i = len;
_xor = 0xFFFFFFFF;
if ( len )
{
_my_key = my_key;
do
{
C = *_my_key;
--i;
++_my_key;
mask = 1;
B = (unsigned __int8)C;
do
{
bit = (_xor & 0x80000000) == 0x80000000;
_xor *= 2;
if ( B & mask )
bit = bit == 0;
if ( bit )
_xor ^= 0x4C11DB7u;
mask *= 2;
}
while ( (_BYTE)mask );
}
while ( i );
}
crc32 = _xor & 1;
_len = 0x1F;
do
{
_xor >>= 1;
crc32 = _xor & 1 | 2 * crc32;
--_len;
}
while ( _len );
return ~crc32;
}
Remaining piece (converted):
crc32 = j_calc_crc32(my_key, 32);
sprintf(crc32_, "%08x", crc32);
result = strcmp(crc32_, &my_key[32]) == 0;
Stage five: writing keygen
Objective: to determine what exactly happened with the key in order to write the inverse function. I will write, contrary to common sense and issuing HexRays , in Delphi , and you can write in a language that is easier for you.
By debugging, we find out what happened:
- The game needs a 32- character key without hyphens ( 37 with hyphens ).
- Taken over the four symbols of the key (not counted hyphens). Each of them is passed through the convert_char function and summed by the formula: sum + = new_c << 5 * (3 - itr) ;
- Each such amount is converted to a lower-case hex string ( 5 characters ) and glued to the existing one (total 40 characters );
- Taken CRC32 from the first 32 -x symbols and the resulting string is compared with the remaining eight symbols received in the previous paragraph line;
- If the lines do not match, our key is incorrect.
Reverse Thinking:
- Write a function that converts the 40- character hash back to the key;
- Generate a 32- character hash;
- Count from it 8 - character CRC32;
- Glue the lines obtained in steps ( 2 ) and ( 3 );
- Pass to function ( 1 ) - we get the desired key.
Thoughts on transformative function:
- Because the input hash was obtained from eight 5- character hash chunks, we will process it the same way, according to the " fives ";
- Each " five " was obtained from four key characters ;
- Because during each calculation of the " five ", it shifted 5 bits to the left , it turns out that for each character of the key there is 5 bits ;
- A careful examination of the convert_char function code leads us to the idea that the character set of the key is limited only to the characters of the set " 0123456789ABCDEFGHJKMNPQRSTVWXYZ ";
- Total: 32 hash characters are generated from the " fives ". 32% 5 = 24 integer characters and 2 in the remainder - i.e. two characters we just have to regenerate.
The final version of the hash generating function (Delphi)
function GenHash(len: byte): string;
var
i, k: byte;
sum: integer;
begin
Randomize;
Result := '';
sum := 0;
k := 0;
for i := 1 to len do
begin
sum := sum + (Random(Length(alphabet)) shl ((3 - k) * 5));
Inc(k);
if k = 4 then
begin
Result := Result + AnsiLowerCase(Int2Hex(sum, 5));
sum := 0;
k := 0;
end;
end;
Result := Result + 'a0'; // Решил не баловаться, а оставить два случайных хекс символа
end;
Next, consider the CRC32 from the hash:
var
key, hash, crc32: string;
begin
hash := GenHash(24);
crc32 := Crc32b(hash); // нашел первый попавшийся модуль, реализующий данный хэш-алгоритм
Transformer function code:
Hash to code converter function
function Hash2Code(const Hash: string): string;
var
s: string;
five: integer;
begin
Result := '';
s := Hash;
while Length(s) > 0 do
begin
five := Hex2Int(Copy(s, 1, 5));
Delete(s, 1, 5);
Result := Result + alphabet[(five and $F8000 shr 15) + 1] +
alphabet[(five and $07C00 shr 10) + 1] +
alphabet[(five and $003E0 shr 05) + 1] +
alphabet[(five and $0001F shr 00) + 1];
end;
end;
And finally, the resulting license key retrieval:
key := Hash2Code(hash + crc32);
lic_code := Format('%s-%s-%s-%s-%s-%s', [Copy(key, 1, 5),
Copy(key, 6, 5),
Copy(key, 11, 5),
Copy(key, 16, 5),
Copy(key, 21, 6),
Copy(key, 27, 6)
]);
We check, and ... Entering the generated key activated the game, the activation point disappeared!
Summary
The main thing when writing keygen is to be able to think back. Those. be able to write such an algorithm that will be the reverse of the one you have. This is not an easy task, but it can be solved in most cases.
PS
Perhaps the article was too messy, I don’t know. The main idea that I wanted to convey: keygen is not such a difficult thing, if you have brains, and desire along with perseverance.
PPS
In the next article I will describe how I wrote keygen to another game - Unepic .