📜 ⬆️ ⬇️

Reverse development of a commercial program: keygen for Zuma Deluxe




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:

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."

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.

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

  // Public RSA stuff BigInt n("D99BC76AB7B2578738E606F7"); BigInt e("11"); BigInt aHash = HashData(aFileData, aFileDataPos, 94); delete aFileData; BigInt aSignature(aSigStr); BigInt aHashTest = aSignature.ModPow(e, n); 

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:
  1. calculate MD5 from the NAME OF USER, 0Ah, ZUMA
  2. throw out the last DWORD, and write down the remaining byte order
  3. WORD-ovo walk through the result, and apply (chit .: kopipastit to keygen) shift; see 0x41D280
  4. change the byte order again
  5. calculate the function ModPow (D, N), where D = 0x3AE5465C52D0C4C0A8FE303D, N = 0x42BF94023BBA6D040C8B81D9, E = 0x11
  6. 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.

Source: https://habr.com/ru/post/135861/


All Articles