Greetings
ArtMoney itself was zaigengen me a long time ago. This is not the first time I've been trying to start writing an article about how this program was cached, but it has always stopped somewhere. This time, I decided to finish everything to the end! Plus, this article can be considered a continuation of a series of articles on cracking for beginners.
So, in this article you will learn how I wrote the keygen to
ArtMoney (version
7.45.1 will be described
here ).
Stage One: Analysis of the executable file
I installed the English version of the program so that
IDA and other utilities look for the text normally.
First of all, you need to find out what
AM is written / packed with. Open it (am745.exe
file ) in my favorite
ExeInfo PE :
')
We are told that this is
Aspack v2.24 - 2.34 . Well, seemingly not a complicated packer. I will remove it by the first automatic unpacker (for the article is not about unpacking).
Stage Two: Analyzes Again
We look again at
ExeInfo PE , and we see that the program is written in
Borland Delphi . Perfectly! Let's use the super program for analyzing
Delphi programs:
IDR ( Interactive Delphi Reconstructor ) . By the way, she also has
open source . The IDE version is there and defined.
Download all available databases and put in the directory with the
IDR . We are dragging our unpacked test subject into the reconstructor, waiting for the end of the analysis.
I will do the whole research process in
IDA Pro (plus
Hex Rays ), so let's generate an
IDC script that tells her about all the names of forms, methods, classes, etc. Click
Tools → IDC Generator in the
IDR menu. We are waiting for the script to be created.
Next, open the
IDA , and also shove our program into it. Let's wait for the end of the analysis. Then apply the generated
IDC script: in
IDA Pro, click
File → Script File ... , select the script. We are waiting for the application.
In order for
IDA to properly decompile
Delphi code, it needs to say that we are dealing with a
Delphi compiler, and
__fastcall calls. Unfortunately, the generated
IDC , like the
IDA itself, doesn’t say / know about it.
Go to the settings of the
IDA compiler:
Options →
Compiler ... :
Next, click to re-analyze the program:
Options →
General →
Analysis →
Reanalyze program and
OK .
Not everything ... :) The
IDC- script for some reason did not mark the boundaries of library functions. Because of what the process of disassembling and decompiling does not become easier. We'll have to mark up and specify the types (
Y key). We take
call on the known function and we look at its borders. If the function does not start where the
call leads, correct it. Go to the instruction that is above the beginning of the function (most often, it is
retn ), and press
E (specify the address of the end of the function) there. That's better.
Now you need to specify the type of function. Take for example
@LStrClr . The caring
IDR pointed out in the comments that this function takes one argument - the address of the string, which means we denote the function's prototype as (remember that in
Delphi the fastcall call
convention ):
void __fastcall LStrClr(char*)
I hope it is clear why. Well,
void because the
procedure .
And so we repeat with many and many functions ... When specifying prototypes, we use these simple rules:
- Arguments are passed via eax , edx , ecx , stack (from left to right). This will help if the decompiler stumbles upon a " positive sp value ";
- In any StrCatN functions, if ArgCnt: Integer is specified, then this is the vararg function, and the prototype will contain .... In the decompiler, you need to get at each place you call such functions, and press + / - on the Numpad- keyboard, adding / removing arguments. The amount can be viewed in disasm-listing. Prototype example: " void LStrCatN (char *, char *, ...) ";
- If the function, judging by the Delphi prototype, returns a pointer to a string, then, most often, this is an implicit output argument in the prototype, and it should be added as the last argument too.
Well, now you can start searching for the registration procedure ...
Stage three: Where are you, my beloved, where?
We turn to
IDR to the forms, and (I’ll say right away, open the
Form28 form), switch to the visual viewing. We see the registration window. Scroll through the window and find the three outlines of the buttons. The left one is the
OK button, the registration application:
Stage four: Analyze onclick (). Surface
Right-click on it and go to the
OKClick handler. In
IDA, go to the address of the beginning of this procedure (button
G ).
First, we point out that this is a
bp-based function (i.e., addressing local variables goes through the
EBP register minus offset), otherwise local variables are not recognized. We press
Alt + P (or PKM by function,
Edit function ... ), and set the
BP-based frame .
Let's try to decompile ... If everything went well, we will see a terrible pseudocode decompiler, otherwise fix the prototypes:
We press
Y on each call to the library / self-writing function, and make sure that the prototypes are with
__fastcall , and the arguments are correctly specified.
The first cycle, judging by the
IDR , is checking the key for alphabetic belonging of characters, and gluing them into a global variable. Call it
g_LicKey .
Next comes the call to an unknown function so far, and, apparently, this is actually the key verification function itself ...
Stage Five: Key Verification (Side View)
This function takes two arguments: the first is the
out parameter, it will contain some error code, and the second is the letter ('
A ' is in the English version, or '
B ' is in Russian).
Decompiling ...
The code is quite large, I agree, but if you approach it correctly, slowly, and without shying away from the abundance of code, you can successfully figure out what is happening here.
Very often you can come across similar code that issued a decompiler (variable names, of course, may differ):
v2 = g_LicKey; if ( g_LicKey ) v2 = (char *)*((_DWORD *)g_LicKey - 1);
Here it is worth saying that
Delphi has its own special string type, and in the form of a structure it can be described as follows:
d_str struc ; (sizeof=0x8, mappedto_243, variable size) _top dd ? length dd ? string db 0 dup(?) ; string(C) delphi_string ends
The first dvd is always
0xFFFFFFFF , the second is the length of the string, and then the string itself goes. Therefore, similar code constructs only get the length of the string.
Another important note:
Delphi row indices because of this come with
1 , so you will often see subtraction 1 from the index in the decompiler / disassembler (to match the actual arrangement of characters in memory).
Next comes the length check:
> = 70 and
<= 500 .
Further, we see the verification of the first key symbol for equality to the second argument of the function, i.e. '
A ' or '
B ', and setting the flag depending on the character. I called this flag
rus_ver .
Agree, it looks cumbersome:
LStrFromChar((char *)&v193, (char *)(unsigned __int8)g_LicKey[2]); gvar_006F58C0 = Pos(v193, *(char **)_abcdefghijklmnopqrstuvwxyzABCDEFGHIJ1234567890KLMNOPQRSTUVWXYZ_); gvar_006F58C8 = *(_BYTE *)(*(_DWORD *)_abcdefghijklmnopqrstuvwxyzABCDEFGHIJ1234567890KLMNOPQRSTUVWXYZ_ + (unsigned __int8)gvar_006F58C0 - 3); gvar_006F58C9 = *(_BYTE *)(*(_DWORD *)_abcdefghijklmnopqrstuvwxyzABCDEFGHIJ1234567890KLMNOPQRSTUVWXYZ_ + (unsigned __int8)gvar_006F58C0 - 4); LStrFromChar((char *)&v192, (char *)(unsigned __int8)g_LicKey[1]); v242 = Pos(v192, *(char **)_abcdefghijklmnopqrstuvwxyzABCDEFGHIJ1234567890KLMNOPQRSTUVWXYZ_);
And so much better:
LStrFromChar((char *)&lic_key, g_LicKey[2]); g_PosChar2 = Pos(lic_key, str_EngAlpha); g_AlphaChar1 = str_EngAlpha[g_PosChar2 - 3]; g_AlphaChar2 = str_EngAlpha[g_PosChar2 - 4]; LStrFromChar((char *)&key_char_1, g_LicKey[1]); key_char1_pos = Pos(key_char_1, str_EngAlpha);
Next - check
key_char1_pos for equality
12 .
Now there is a summation of the key symbols, except for the last two, and, as long as the sum is greater than
0xFF , division by 2, and increment by 1:
idx = key_len_minus_2 - 2; if ( key_len_minus_2 - 2 > 0 ) { key_idx = 1; do { key_sum += (unsigned __int8)g_LicKey[key_idx++ - 1]; --idx; } while ( idx ); } for ( ; key_sum > 0xFF; key_sum = (key_sum >> 1) + 1 ) ;
We convert the resulting amount to a hex-string, and then to
lowercase .
Now we get the last two characters of the key, transfer each of them to some function, and at the output we get by the transformed character. Let's take this function ...
Stage Six: Character Conversion
In general, the character conversion function looks like this:
LStrFromChar((char *)&inChar_2, inChar); v3 = Pos(inChar_2, str_EngAlpha); if ( v3 > 0 ) { if ( g_PosChar2 > 0xAu ) { for ( i = 1; i < g_PosChar2 - 5; i += 2 ) { if ( i == v3 ) { v3 = i + 1; } else if ( v3 == i + 1 ) { v3 = i; } } } for ( j = g_PosChar2 + 1; j < 60; j += 2 ) { if ( j == v3 ) { v3 = j + 1; } else if ( v3 == j + 1 ) { v3 = j; } } v6 = v3 - g_PosChar2 + ((char)(v3 - g_PosChar2) < 0 ? 0x3E : 0); if ( flag_1 ) v2 = str_RusAlpha1[v6 - 1]; else v2 = str_EngAlpha1[v6 - 1]; } return v2;
It seems simple. I have to say, we have to invert it.
Let's call it
DecodeChar .
As a result, the last two characters of the key are converted by this function, and compared with the previously obtained hex-sum of all the other key characters.
ToRevert (with this word I will mark the important points in the reverse of the key verification function ): At the very end, when the key is ready, we consider the sum of its characters, convert to hex ( 0xXX ), then each nibbl is inverted DecodeChar , and paste to the key.
We see further that the third character of the key is converted, translated from hex to
int , and the bits are checked. So let's call it all:
v202 = meffi_DecodeChar(g_LicKey[3], 0); PStrCpy(&v132, str_Hex); v134 = v202; v133 = 1; PStrNCat(&v132, &v133, 2); LStrFromString((char *)&v129, &v132); key_char_3 = StrToInt(v129); k3_flag1 = (key_char_3 & 1) != 0; k3_flag2 = (key_char_3 & 2) != 0; k3_flag3 = (key_char_3 & 4) != 0; k3_flag4 = (key_char_3 & 8) != 0;
While we do not know the purpose of the bits, we therefore give them at least some meaningful names.
Again a function that we do not know, and one of the flags is passed to it:
key_idx = 5; sub_66408C(&key_idx, k3_flag1, &v128);
The last parameter seems to be the output string, since passed on to
Trim () and used later.
Immediately give this function the corresponding prototype:
void __fastcall sub_66408C(int idx, bool flag, char *output)
Stage seven: Reading a string from the key
Yes, this is exactly what this function does. That shows debugging, and a quick overview of the code. But first things first.
while ( g_LicKey[*(_DWORD *)idx_1 - 1] != g_AlphaChar1 && LStrLen(g_LicKey) >= *(_DWORD *)idx_1 )
Immediately, we note that the first parameter is used as a pointer to dword (int), so we change its type to
int * (in
Delphi, this is called
var arguments).
After all type conversions, and adjustment of arguments, our function takes the form:
while ( g_LicKey[*idx_1 - 1] != g_AlphaChar1 && LStrLen(g_LicKey) >= *idx_1 ) { c1 = g_LicKey[*idx_1 - 1]; if ( c1 == g_AlphaChar2 ) { LStrFromChar((char *)&c1_str, c1); c1_str_ = c1_str; LStrFromChar((char *)&c2_str, g_LicKey[*idx_1]); c2_str_ = c2_str; LStrFromChar((char *)&c3_str, g_LicKey[*idx_1 + 1]); LStrCatN(c3_str, c2_str_, c1_str_, gvar_0070DF4C); PStrCpy(&str_hex, str_Hex_0); cc[1] = meffi_DecodeChar(g_LicKey[*idx_1], 0); cc[0] = 1; PStrNCat(&str_hex, cc, 2); PStrCpy(&hexVal, &str_hex); cc[1] = meffi_DecodeChar(g_LicKey[*idx_1 + 1], 0); cc[0] = 1; PStrNCat(&hexVal, cc, 3); LStrFromString((char *)&hexVal_1, &hexVal); value = ValLong(hexVal_1, &outCode); LStrFromChar((char *)&value_1, value); LStrCat((char *)&output_2, value_1); *idx_1 += 2; } else { LStrFromChar((char *)&keyChar, g_LicKey[*idx_1 - 1]); LStrCat((char *)&gvar_0070DF4C, keyChar); c = meffi_DecodeChar(g_LicKey[*idx_1 - 1], flag_1); LStrFromChar((char *)&c_str, c); LStrCat((char *)&output_2, c_str); } ++*idx_1; } ++*idx_1;
We see that the key characters are being read, until
g_AlphaChar1 is encountered . If the symbol is not equal to
g_AlphaChar2 , we glue it into a global variable, and
glue the symbol converted with
DecodeChar () into the output buffer.
If we got the
g_AlphaChar2 character, we read the two characters following it, transform them, convert them to a number, and paste them to the output buffer. In the non-converted form, we add the same two symbols together with
g_AlphaChar2 to the global variable.
Let's call it
g_stringFromKey1 .
Apparently, this function can be called
DecodeString .
ToRevert : The string that we want to encode into a key will have to be converted using the inverse function for DecodeString () .
PS On this, the first part of the article about the keying of
ArtMoney I probably finish. In the second part, we will continue to decompile the key verification code, faced with new difficulties, and stupid-looking code. But, does it stop us?
PPS Give us more interesting articles on reverse!