Reverse commercial development: keygen for Zuma Deluxe




Introduction

Hello, Habralumi.
Judging by the latest Assembler blog posts , the keygen theme is becoming very popular here. Well, I'll bring in my five cents.
Our experimental subject today is the Zuma Deluxe game, which I couldn’t google keygen for myself (don’t think that I am a gamer: I was inspired by Comrade k_d with his self- game for Zuma for all this research ). And immediately the disclaimer: this hack from the beginning to the end was done for educational purposes and does not intend to incur losses of the company PopCap Games.
So, google the Zuma distribution , download it, put OllyDBG on alert , and begin the analysis.
Yes, I’ll make a reservation in advance - for some time now I shut myself up with a Linuxoid, 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: insidious flash

In general, we launch 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:



Ok, select “Buy Now”, close the popped-up browser window with an offer to buy such a game for a miserable 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 gibberish, as expected we get “Please enter a valid key”, and go to figure out what's what. The first thing that warns with a thoughtful examination is the presence in the folder, right next to the binar of the game, two files hinting at the use of Flash technology in the program: in fact, Flash.ocx and drm.swf ... Apparently, you could take a while to close the browser. Okay, open this very drm.swf - and what do we see:



The whole glamorous shell for registering the key, as it turned out, is executed in the same .SWF file. Maybe the verification code itself is in the same place? Let's get a look. We take any ActionScript decompiler (for example, I used Flare ) and take the source code from drm.swf.
We look at what we have compiled over there. Sooner or later, we stumble upon this interesting line:

    gFrameLabels[4] = 'RegFailed';

We search by “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 (it simply no longer allows you to enter the text field itself) and forces validate_regkey () to return True . You can ignore the fact that in the same block of code initialization of such "scary" values ​​as gRegFailedMessage takes place , because here, regardless of them, from the flash object through fscommand () data is transferred to the parent process.
Now it's time to get into the validate_regkey () function itself . Here it is in its entirety:

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 !';
              returntrue;
            }
          } else {
            result = 'Unauthorized character ' + char;
            returnfalse;
          }
          ++k;
        }
      } else {
        result = 'Error in delimiters';
        returnfalse;
      }
    }

Well, the central audit is an unambiguous masterpiece. You should probably post it on govnokod.ru , but oh well, we are not going to get some fun here. The main thing is that this function gave us the structure of the license key:

##### - ##### - ##### - #####
where # is the symbol from the alphabet "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-  ".
Exactly 23 characters.

Looking ahead again, I will say that the set of correct characters inside the program itself will be slightly reduced. But for now, it makes no difference to us. Launch OllyDBG and load our experimental into it. We launch it (F9) and wait until the main window is drawn.
Where to dig further?
Remember the fscommand () call we foundwith the first parameter equal to the string "Register"?
Therefore, open the Memory Map (Alt + M) and look for the entry of this line (Ctrl + B). And here she is, lying to herself at 0x4417D0 :



We put a breakpoint on it on it (selection → Shift + F3). Next, in the verification window, click on the link below “Already purchased this game?” → “Enter the Registration Key Manually”, and enter any alphanumeric rubbish in the key field, separated by hyphens into 4 blocks of 5 characters. Click "Register".


Part 2: we need to go deeper

So we finally got inside the verification algorithm. We go to the list of control points for memory and delete 0x4417D0 (Alt + Y → Del), execute (Ctrl + F9) until the end of the function, because there is nothing interesting for us there, only a comparison cycle, then we return to the calling function (F8), and we see there a check of the returned result. Again, we skip everything to 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, put (F2) a control point at 0x04066CA and look around. A little lower ( 0x406757 and 0x4067A8 ) you can see function calls with very interesting string parameters “RegSucceeded” and “RegFailed”. A higher ( 0x406748) there is a branch that transfers control to the desired function. This branch is tied to comparing ( 0x40672D ) 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, let's check our guess: we will change the comparison so that it turns out to be true with incorrect source data. Point the selection at 0x406748 and press the spacebar. The Assemble window opens. Replace the transition by equality, JE, by the transition by inequality - JNE. Run it (F9) ...



Hooray, the first bastion is taken!
But now we are writing not just a crack, but a very keygen. And this means that it is too early to stop at what has been achieved, and you have to restart the program (Ctrl + F2) and delve into the bowels of the 0x404260 function .

Our task now is to understand how the AL and BL registers behave inside this function, and where exactly the piece of code is located that is responsible for their equality or inequality.
We’ll go to the “tail” of the function, closer to the exit point, RETN, and turn on the backlight of the BL register (context menu → Highlight register → EBX).



As you can see, the contents of the AL register are stored in BL for almost the entire “tail”, and are copied back just before the exit, 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.
We go inside this function - and what we see:



This function has only two possible output values ​​- 0 and 1. The first one is generated when the byte string, the pointer to the structure with which was passed as a function parameter, does not coincide with the string whose pointer lies along address [ECX + 8]. The second (the one that we need) is in the opposite situation, i.e. when the strings are identical.


Part 3: MD5, RSA and all-all-all

We return to the parent function and take a look at where the values ​​from [ECX + 8] and [ARG.1 + 8] come from.
To do this, put (selection → Shift + F5) the “hardware” breakpoints on the two addresses in the stack where these lines should be placed. For different machine configurations and operating systems, these addresses are likely to vary. So, in my case, these are 0x33E624 and 0x33E660 (in general, I recommend getting an additional window on my side showing the state of the stack, independent of the position of its top, ESP, as in the picture ).
These control points must be hardware-based, as other types of control points, when put on the stack, will either lead to a “crash” of the program or will not be saved between starts. For now, these points need to be made inactive (context menu → Breakpoint → Hardware disable).

Now restart the program and stop at the entrance to our main scan function ( 0x404260 ). Put a breakpoint there and start tracing the function line by line (F8), monitoring the state of our two hardware breakpoints. Tracing shows that until line 0x404546 both values ​​remain unchanged. But then it’s more interesting.

A function directly called from 0x404546 is the “springboard” for starting the function0x41E320 , so there is nothing interesting in it. Set a breakpoint at 0x41E320 and press F9.
At the moment, in the stack you can see a line consisting of strange, but nonetheless printed characters (for example, I have this A ..... 6..O6NBBO .... E4GXF3O0 ..), a line feed character and postfix ZUMA. We trace further, and find out on 0x41E37F :



So-so-so ... yes these are 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 : data initialization 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 link to the result
  • 0x41E3C5 - 0x41E408 : a cycle that rearranges bytes as a result backwards
  • 0x41E40A - 0x41E424 : a check that, due to the fact that the fourth parameter of the function is fixed (0x5E), always turns out to be true
  • 0x41E426 - 0x41E474 : code that, by virtue of the previous check, is never executed
  • 0x41E476 - 0x41E4B5 : "trim" functions of a 128-bit MD5 hash to 96 bits; exit function

Now let's look at the first of our hardware control points:



Such a structure of five values, as I said, will subsequently be called the “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 later)
  • The third DWORD is the address in memory where the byte string is stored
  • The fourth DWORD sets its “effective” length in WORDs (that is, how many WORDs from a string will be used in further calculations)
  • The fifth DWORD sets its “total” length in WORDs (that is, how many WORDs were originally allocated to place the line)

So, after performing the procedure just traced by us, the first of the frames is already filled. That is, we already have one of the lines, let's call it conditionally “reference”, which will be compared with the one that we have yet to calculate in 0x40454F - 0x40458C .

Now let's take a closer look at the 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 string of a registration key entered by us, and recounts it in number.
  • 0x41E5A0 - 0x41E608 : initialization, allocation of memory, installation of an exception handler
  • 0x41E60B - 0x41E6EF : a cycle that converts the letters of a string to uppercase and replaces the characters "1" with "L", and "O" and "0" with "Q"
  • 0x41E6F5 - 0x41E70A : preparing for the second cycle
  • 0x41E710 - 0x41E7E4 : the second cycle building a number according to the principle of converting it from a 28-decimal number system (the first character is the least significant, the last is the highest), with the character set "234679ACDEFGHJKLMNPQRTUVWXYZ", ignoring hyphens, and throwing an error if there is no current character in the set
  • 0x41E7EA - 0x41E844 : copy result, free temporary buffers, exit
  • 0x41E845 - 0x41E874 : "tail" that executes if the second cycle throws an error

Great, the string is converted to a number, and now we are doing something with it.

Until the cherished moment, when we can say that the reverse was done, there was only one function left, 0x41E100 (call from 0x40458C ).

Well, here I’m not going to bother you with reading and decoding assembler listings, since one good friend, by the time I just started to disassemble it, threw me a piece of the PopCap’s source code that had leaked to the Network very early on, containing, if not the implementation of the specified functions, but at least its name. In general, the drum roll ... the function that we are going to launch a frontal attack on is called aSignature.ModPow (e, n) .
Those interested can follow the line 00069 and find a striking similarity between the bool function SexyApp :: Validate () (SexyApp - for some reason; I'm crap without button accordion, gracious gentlemen) with ours, which has already become so dear, 0x404260 .

Also, I recommend paying attention to e and n themselves :

BigInt n("42BF94023BBA6D040C8B81D9");
    BigInt e("11");

or, in the assembler representation,



as the name suggests, ModPow () means exponentiation modulo.
A comment on line 00478

// Public RSA stuffBigInt 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 raw data to start writing keygen. The only thing left to do is factor the public module 0x42BF94023BBA6D040C8B81D9 and calculate the private exponent. Well, take MSieve + TMG RSA Tool and get 0x03AE5465C52D0C4C0A8FE303D at the output .
All that remains is to write (or erase the finished, hehe) implementation of long arithmetic. We have the algorithm for generating the key itself:
  1. count MD5 from the string USERNAME, 0Ah, ZUMA
  2. throw out the last DWORD, and write the remaining byte order
  3. in WORD-ovo, go through the result, and apply (read: copy-paste to keygen) shift; see 0x41D280
  4. reverse byte order again
  5. calculate from the received function ModPow (D, N), where D = 0x3AE5465C52D0C4C0A8FE303D, N = 0x42BF94023BBA6D040C8B81D9, E = 0x11
  6. dividing by 28 by sequentially substituting the residuals according to the table “234679ACDEFGHJKLMNPQRTUVWXYZ”, reveal the license key from the calculated

Here, I deliberately omitted several hours of research on what the treasured line “A ..... 6..O6NBBO .... E4GXF3O0 ..” is, which I took for granted as part of the analysis, and in the above algorithm designated as USERNAME. It turns out that it is generated on the basis of computer hardware, 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 drug addicts. Here, for example, is the real situation: at any given time, my computer can have three or four network adapters ( lo return , Ethernet interface eth0 , WiFi interface wlan0 , as well as a mobile phone connected via USB port and playing the role of a GPRS modem , ppp0) As soon as I connect the 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, will simply fly off.

In general, based on the foregoing, the code that generates this dirty trick, I decided not to copy paste into keygen, but to trivially steal an already finished line from the game’s memory using ReadProcessMemory () . As a little hooliganism, I also added the ability to write something of my own in the name string (using WriteProcessMemory () , as you might guess). But, unfortunately, this trick works (that is, preserves the validity of registration) only on WINE, but not on "real" Windows.

For the rest - please love and favor:Zuma keygen, proof-of-concept .
The writing language is assembler. The MD5 algorithm is copied from the game binar, and is slightly modified with a file. 96-bit arithmetic - authentic =)

Keygen 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 username differed, the license key itself approached them all, which testifies to the invariability of the algorithm from 2003 onwards.


Afterword and references used

This article would not have been possible without the help of several literate guys from WASM.RU Forum who guided me on the right path in this thread .
Also, the RSA Tool 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 bowels of the game.
If someone is interested, I spread the .UDD file for OllyDBG with all the comments and control points to the heap .

PS If someone can advise a more reliable file hosting 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 will unzip the real bin with Zuma named popcapgame1.exe into its own folder using the license key ...

[UPDATE: ] transferred pictures to Habrastorage.

Also popular now: