
Introduction
Hello, Habraludi.
Judging by the latest articles in the
Assembler blog, the topic of keygens is becoming very popular here. Well, I'll bring in my five kopecks.
Our today's test subject is the Zuma Deluxe game, which I couldn’t go on trying to get rid of my keygen (don’t think that I’m a gamer: Comrade
k_d inspired me with his
self-playing game for Zuma ). And immediately a disclaimer: this hacking from the beginning to the end is done for educational purposes and is not intended to incur losses for PopCap Games.
So, google
the Zuma distribution , download it, bring
OllyDBG into battle, and start parsing.
Yes, I will make a reservation in advance - for some time I have become a Linux user, so all this joy will be launched from under WINE. However, looking ahead, I note that this task has its advantages, such as, for example, the ease of editing records and tracking changes in the WINE registry, due to its storage in a regular text file.
Part 1: The Cunning Flash
In general, we start the game, play longer than expected (or immediately climb into the HKLM / Software / PopCap / Zuma registry branch and set zeros in the TimesExecuted and TimesPlayed keys) - and voila:
')

Great, choose “Buy Now”, close the pop-up window of the browser with an offer to buy
such a game for a measly 16.99 euros, and click “Enter the Registration Key Manually”.

So-s, input field. Already from something you can dance. We try to enter some kind of abracadabra, expectedly we get “Please enter a valid key”, and go to figure out what's what. The first thing that is alarming during a thoughtful inspection is the presence in the folder, right next to the game binaries, two files hinting at the use of Flash technology in the program: Actually, Flash.ocx and drm.swf ... Apparently, it was possible to delay the closure of the browser. Okay, open this very drm.swf - and what we see:

The whole glamorous shell for registering the key, as it turned out, is executed in that very .SWF file. Maybe the verification code itself is in the same place? Let's get a look. We take any ActionScript decompiler (I, for example, used
Flare ) and remove the source code from drm.swf.
We look, that at us there it was compiled. Is it too early, is it too late to stumble upon such an interesting line:
gFrameLabels[4] = 'RegFailed';
We look for “RegFailed” and exit to this block of code:
if (_root.RegCodeEdit.text.length >= 23 && _root.validate_regkey(_root.RegCodeEdit.text)) { _root.APError.text = ''; gRegFailedHeader = gHeader_RegFail; gRegFailedMessage = gMessage_RegFail; gRegFailedRetryLocation = 'APScreen'; fscommand('Register', _root.RegCodeEdit.text); }
Here it is. The correct key is 23 characters long (no longer simply allows you to enter the text field itself) and causes
validate_regkey () to return
True . The fact that in the same block of code initialization of such “scary” values ​​as
gRegFailedMessage occurs can be ignored, since here, regardless of them, data from the flash object is transferred to the parent process via the
fscommand () .
Now it's time to do the actual function
validate_regkey () . Here it is:
function validate_regkey(string) { if (string.substr(5, 1) == '-' && string.substr(11, 1) == '-' && string.substr(17, 1) == '-') { char = new Array(); k = 0; while (k <= string.length - 1) { char = string.slice(k, k + 1); if (char == '0' || char == '1' || char == '2' || char == '3' || char == '4' || char == '5' || char == '6' || char == '7' || char == '8' || char == '9' || char == 'A' || char == 'B' || char == 'C' || char == 'D' || char == 'E' || char == 'F' || char == 'G' || char == 'H' || char == 'I' || char == 'J' || char == 'K' || char == 'L' || char == 'M' || char == 'N' || char == 'O' || char == 'P' || char == 'Q' || char == 'R' || char == 'S' || char == 'T' || char == 'U' || char == 'V' || char == 'W' || char == 'X' || char == 'Y' || char == 'Z' || char == 'a' || char == 'b' || char == 'c' || char == 'd' || char == 'e' || char == 'f' || char == 'g' || char == 'h' || char == 'i' || char == 'j' || char == 'k' || char == 'l' || char == 'm' || char == 'n' || char == 'o' || char == 'p' || char == 'q' || char == 'r' || char == 's' || char == 't' || char == 'u' || char == 'v' || char == 'w' || char == 'x' || char == 'y' || char == 'z' || char == '-' || char == ' ') { if (k == string.length - 1) { result = 'Thank you for submitting !'; return true; } } else { result = 'Unauthorized character ' + char; return false; } ++k; } } else { result = 'Error in delimiters'; return false; } }
Well, the central check is an unequivocal masterpiece. It is necessary, probably, to post it on
govnokod.ru , but oh well, we have not gathered a bit of a bitch here. The main thing is that this function gave us the structure of the license key:
##### - ##### - ##### - #####
where # is a character from the alphabet "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-
".
Exactly 23 characters.
Again, looking ahead, I will say that the set of correct characters within the program itself will be somewhat reduced. But so far we have no difference. We launch OllyDBG and load our experimental into it. Run (F9) and wait for the main window to be drawn.
Where to dig further?
Do you remember the
fscommand () call we found with the first parameter equal to the string “Register”?
Therefore, open the Memory Map (Alt + M) and look for the entry of this string (Ctrl + B). And here it is, it lies to itself at the address
0x4417D0 :

Put a break point on it on reading (selection → Shift + F3). Next, in the verification window, go to the link below “Already purchased this game?” → “Enter the Registration Key Manually”, and enter in the field for the key any alphanumeric stub, divided by 4 hyphens in 5 characters. Click "Register".
Part 2: we need to go deeper
So we finally got inside the verification algorithm. Go to the list of control points for memory and delete
0x4417D0 (Alt + Y → Del), execute (Ctrl + F9) until the end of the function, since there is nothing interesting for us, only the comparison cycle, then we return to the calling function (F8), and we see there the check of the returned result. Again, we skip everything until the end of the function and return to the caller:

Aha And now we are in the heart of the license key analyzer. In order not to forget this place, set (F2) the control point at
0x04066CA and look around. Just below (
0x406757 and
0x4067A8 ) function calls with very interesting string parameters “RegSucceeded” and “RegFailed” are visible. And higher (
0x406748 ) is a branch, which transfers control to the desired function. This branch is tied to a comparison (
0x40672D ) of the AL and BL registers. It seems that the function
0x404260 , called by two more commands earlier, is exactly what we were looking for, i.e. The Most Important Check Function.
First, check your guess: change the comparison so that it turns out to be true with incorrect source data. Bring the selection to
0x406748 and
press the spacebar. The “Assemble” window will open. Replace the transition by equality, JE, to the transition by inequality - JNE. Run (F9) ...

Hurray, the first bastion is taken!
But now we are writing not just a crack, but a very keygen. And this means that it’s too early to stop at what has been accomplished, and you have to restart the program (Ctrl + F2) and delve into the depths of the
0x404260 function.
Now our task is to understand how the AL and BL registers behave inside this function, and where exactly is the piece of code responsible for their equality or inequality.
Go to the “tail” of the function, closer to the exit point, RETN, and turn on the BL register highlight (context menu → Highlight register → EBX).

As you can see, the contents of the AL register for almost the entire “tail” are stored in BL, and just before the output is copied back, with further restoration of the original EBX value from the stack.
Moreover, the AL value itself is taken from the function in the picture above marked with a selection.
Go inside this function - and what we see:

This function has only two possible output values ​​- 0 and 1. The first is generated when the string is bytes, the pointer to the structure with which is passed as a function parameter, does not match the string whose pointer is at [ECX + 8]. The second (the one we need) - in the opposite situation, i.e. when the strings are identical.
Part 3: MD5, RSA and all-all
Let's go back to the parent function and see where the values ​​from [ECX + 8] and [ARG.1 + 8] come from.
To do this, put (selection → Shift + F5) “hardware” checkpoint points for two addresses in the stack where these lines should be placed. For different configurations of machines and operating systems, these addresses are likely to differ. So, in my case, this is
0x33E624 and
0x33E660 (in general, I recommend having an additional window on my side that shows the state of the stack, independent of the position of its top, ESP,
as in the picture ).
These checkpoints must be hardware, because other types of control points, being stacked, will either lead to a program crash, or will not be saved between runs. So far, these points need to be made inactive (context menu → Breakpoint → Hardware disable).
Now restart the program and stop at the entrance to our main check function (
0x404260 ). Put a checkpoint there and start tracing the function line by line (F8), following the state of our two hardware breakpoints. Tracing indicates that before line
0x404546 both values ​​remain unchanged. But further already curious.
A function that is directly called from
0x404546 is a “springboard” for starting the function
0x41E320 , so there is nothing interesting in it. Put a breakpoint at
0x41E320 and hit F9.
At the moment, in the stack you can see a line consisting of strange, but nevertheless printable characters (for example, I have A ..... 6..O6NBBO .... E4GXF3O0 ..), a line feed and postfix zuma. We trace further, and we
get on
0x41E37F :

So-so-so ... yes these are the same initialization vectors for
the MD5 algorithm !
MD5. That's better. Now the analysis of the rest of the function code is easy and carefree:
- 0x41E320 - 0x41E397 : initialization of data and memory allocation for the result
- 0x41E39B - 0x41E3C3 : calculation of the MD5 hash from the above line and preparation of the structure (hereinafter, I will call it a “frame”), which will contain a reference to the result
- 0x41E3C5 - 0x41E408 : a loop that rebuilds bytes as a result backwards
- 0x41E40A - 0x41E424 : a check that, by virtue of the fixedness of the fourth parameter of the function (0x5E), always turns out to be true
- 0x41E426 - 0x41E474 : code that, by virtue of a previous check, is never executed
- 0x41E476 - 0x41E4B5 : functions of “trimming” 128-bit MD5 hash to 96 bits; exit function
Now let's look at the first of our hardware checkpoints:

Such a structure of five values, as I said, will later be called a "frame."
- The first DWORD never changes, and is always 0x44543C
- The second DWORD has an unclear purpose (yes, in general, it is not particularly important, as practice will show further)
- The third DWORD is the address in memory where the byte string is stored.
- The fourth DWORD sets its “effective” length in WORD `s (i.e. how many WORD` s from the string will be used in further calculations)
- The fifth DWORD sets its “total” length in WORD `s (i.e. how many WORD` s were initially allocated to accommodate the string)
So, after the procedure we have just traced, the first of the frames is already filled. That is, we already have one of the strings, let's call it conditionally “reference”, which will undergo a comparison with the one that we have yet to calculate at
0x40454F -
0x40458C .
Now let's take a
closer look at line
0x404560 , from where the function
0x41E5A0 is
called .
The function is very long and very scary, however, we are here to make the long and scary simple and understandable. This function processes the registration key string we entered, and recalculates it into a number.
- 0x41E5A0 - 0x41E608 : initialization, memory allocation, installation of an exception handler
- 0x41E60B - 0x41E6EF : a loop that converts the letters of a string to uppercase, and replaces the characters "1" with "L", and "O" and "0" - with "Q"
- 0x41E6F5 - 0x41E70A : preparation for the second cycle
- 0x41E710 - 0x41E7E4 : the second cycle that builds a number from a string based on the principle of converting it from a 28-digit number system (the first character is a low-order bit, the last is a high-order number), with the character set “234679ACDEFGHJKLMNPQRTUVWXYZ” ignoring hyphens, and giving an error when there is no current character in set
- 0x41E7EA - 0x41E844 : copy result, free temporary buffers, exit
- 0x41E845 - 0x41E874 : “tail”, executed if the second cycle gave an error
Great, the string is recalculated into a number, and now we are doing something with it.
Until the cherished moment, when it can be said that the reverse is done, there is only one function left,
0x41E100 (call from
0x40458C ).
Well, here I will not torture you by reading and decoding assembly lists, because one good friend, by the time I just started to disassemble it, threw a piece of the source code PopCap ʻovskogo framework, which contains in itself if not the implementation specified functions, but at least its name. In general, the
drum roll ... the function that we are going to launch into a frontal attack is called
aSignature.ModPow (e, n) .
Those who are interested can proceed to line 00069 and find out the striking similarity of the
bool SexyApp :: Validate () function (SexyApp - they called it; I'm fucking off without a button accordion, gracious gentlemen) with ours, who has already become so dear,
0x404260 .
Also, I recommend to pay attention to
e and
n themselves:
BigInt n("42BF94023BBA6D040C8B81D9"); BigInt e("11");
or, in an assembly representation,

As the name suggests,
ModPow () means exponentiation modulo.
And the comment on line 00478
finally clarifies the situation: the algorithm we are dealing with is
RSA .
Part 4: Key Generator
Well, we have almost all the source data to start writing keygen. The only thing left to do is to factor the public module
0x42BF94023BBA6D040C8B81D9 and calculate the private exponent. Well, we take into the hands of the
MSieve +
TMG RSA Tool , and get at the output
0x03AE5465C52D0C4C0A8FE303D .
Everything, it remains to write (or stole the finished, he-he) implementation of long arithmetic. We have the algorithm for key generation:
- calculate MD5 from the NAME OF USER, 0Ah, ZUMA
- throw out the last DWORD, and write down the remaining byte order
- WORD-ovo walk through the result, and apply (chit .: kopipastit to keygen) shift; see 0x41D280
- change the byte order again
- calculate the function ModPow (D, N), where D = 0x3AE5465C52D0C4C0A8FE303D, N = 0x42BF94023BBA6D040C8B81D9, E = 0x11
- by dividing by 28, substituting the residuals according to the table “234679ACDEFGHJKLMNPQRTUVWXYZ”, reveal from the calculated license key
Here I deliberately dropped a few hours of research on what the most cherished line “A ..... 6..O6NBBO .... E4GXF3O0 ..” is, which I took for granted as an analysis, and in the above algorithm designated as a user name. It turns out that it is generated on the basis of the computer's iron, in particular, the number of network adapters on the computer is responsible for its length.
The code of this generation, in my opinion, was written by some paranoid addicts. Here, for example, is the real situation: at any given time in my computer there may be three or four network adapters (
lo back-up,
eth0 Ethernet interface,
wlan0 WiFi interface, as well as a mobile phone connected via a USB port and playing the role of a GPRS modem ,
ppp0 ). As soon as I connect a mobile phone, they become 4. As I disconnect - 3. These two states, according to the generator, correspond to different lines. Therefore, in one of them, registration Zuma, bought for, sorry, € 16.99, just fly.
In general, based on the foregoing, the code that generates this dirty trick, I decided not to copy-paste keygen, but it’s banal to steal a ready-made line from the memory of the game using
ReadProcessMemory () . As a small hooliganism, I also added the ability to write something of my
own in the name string (using
WriteProcessMemory () , as you can easily guess). But, unfortunately, such a trick only works on WINE (that is, it retains the validity of registration), but not on “real” Windows.
The rest - please love and favor:
Zuma keygen, proof-of-concept .
The writing language is assembler. The MD5 algorithm is copied from the game's binary, and slightly modified by the file. 96-bit arithmetic - authentic =)
Keigen was checked not only on the version of Zuma, which is discussed in the article, but also on others, earlier or later (I did not understand). Despite the fact that the addresses with the user name have differed, the license key itself has been suitable for all of them, which indicates that the algorithm has not changed since 2003.
Afterword and references used
This article would have been impossible without the help of several literate guys from the
WASM.RU Forum , who had guided me on the
right path
in this topic .
Also, the
RSA Tool utility from the TMG hacker team and the online RSA calculator
http://nmichaels.org/rsa.py helped me a lot.
The Wikipedia articles on
RSA and
MD5 also gave me a lot to understand the essence of what is happening in the depths of the game.
If someone is interested, I post the
.UDD file for OllyDBG with all comments and control points.
PS If someone can advise a more reliable file sharing service from which files are not deleted after 30 days of inactivity - I will be extremely grateful.
PPS What was my surprise when, after hacking, I discovered that
Zuma.exe from the package in
question is just a wrapper, an archive that unpacks the real binary from Zuma, called
popcapgame1.exe , using its license key ...
[UPDATE:] transferred images to Habrastorage.