Home > Game Hacking, General x86, Programming, Reverse Engineering > Bypassing Product Key Authentication (1/2)

Bypassing Product Key Authentication (1/2)

This post will cover the topic of product authentication in applications and how it can be bypassed. It aims to serve as a detailed walkthrough of how to locate these functions in a target application and methods in which an application can be modified to allow it to accept invalid product keys. The post will focus on a concrete application and will involve reverse engineering the code which is responsible for performing authentication. At this time, only the calling code will be investigated — this will not be a post about reverse engineering the actual algorithm itself, although that may come at a later date.

Tools

Not much is needed here outside of the standard tools. Below is what was used when creating this post:

  • Cheat Engine for memory scanning
  • x64dbg for dynamic analysis
  • Installer executable (SETUP.EXE) SHA1 Hash: AC9241F632FFB0D845E404FC06C3A204D2EE1B99 that comes with the Age of Mythology CD

The Target

The target for this post will be Age of Mythology. The game requires a valid product key as part of the installation process. The verification is done entirely within the executable itself; there is no online activation required, which would greatly complicate the process.

The installation process requires the input of a valid 25-character product key that is located on the physical CD case. Failure to provide this product key results in an error dialog saying that the product key is invalid and prevents the user from continuing the installation process.

The goal then is to bypass this process and be able to install the game without having a valid product key. This will involve finding the code responsible for calling the authentication function(s), reverse engineering it to understand how it works, and then finding a way to modify it so that it is possible to proceed in the installation process without having a valid key.

Finding the Function

As mentioned above, the natural starting point is to find where the product key is being verified. This can be accomplished in multiple ways, each one having its own benefits and drawbacks. For this example, the approach I took involved finding the key in memory and seeing where it was accessed. This was done by inputting a key into the box and then searching for the string in the process memory using Cheat Engine.

Doing this resulted in one address. Finding out what writes to this address provided additional information to investigate

At this point it is time to attach a debugger and begin stepping through some of this code. Starting at the top of the list of addresses, the ones in the 0x757621… range looked interesting. Given the high address, it can be concluded that these likely reside in a Windows core DLL. Navigating to the first address in the debugger reveals that it is part of the lstrcpyA function in kernel32.dll.

757621B0 | 6A 08                    | push 8
757621B2 | 68 B8 F5 7C 75           | push kernel32.757CF5B8
757621B7 | E8 E0 85 00 00           | call kernel32.7576A79C
757621BC | 83 65 FC 00              | and dword ptr ss:[ebp-4], 0
757621C0 | 8B 55 0C                 | mov edx, dword ptr ss:[ebp+C]
757621C3 | 8B 45 08                 | mov eax, dword ptr ss:[ebp+8]
757621C6 | 8B F0                    | mov esi, eax
757621C8 | 2B F2                    | sub esi, edx
757621CA | 8A 0A                    | mov cl, byte ptr ds:[edx]
757621CC | 88 0C 16                 | mov byte ptr ds:[esi+edx], cl
757621CF | 42                       | inc edx
757621D0 | 84 C9                    | test cl, cl
757621D2 | 75 F6                    | jne kernel32.757621CA
757621D4 | C7 45 FC FE FF FF FF     | mov dword ptr ss:[ebp-4], FFFFFFFE
757621DB | E8 01 86 00 00           | call kernel32.7576A7E1
757621E0 | C2 08 00                 | ret 8

There’s nothing surprising here, the two arguments are passed in [EBP+0x8] and [EBP+0xC]. The contents of the source argument are copied, one byte at a time, into the destination argument in a loop which terminates when a null terminator is found in the source parameter. Setting a breakpoint on this function shows that it is being hit multiple times. It is initially hit five times for the five different parts of the key. Afterwards it is hit with the entire key. For the first five parts, the call stack shows the call coming from the following:

0040CD06 | 50                       | push eax                                         |
0040CD07 | 68 D0 D8 47 00           | push ebud71f.47D8D0                              | 47D8D0:"11111"
0040CD0C | FF D6                    | call esi                                         | esi:lstrcpyA
0040CD0E | 8B 0D A0 CC 47 00        | mov ecx,dword ptr ds:[47CCA0]                    |
0040CD14 | 8B 11                    | mov edx,dword ptr ds:[ecx]                       |
0040CD16 | FF 92 90 00 00 00        | call dword ptr ds:[edx+90]                       |
0040CD1C | 50                       | push eax                                         |
0040CD1D | 68 D4 D9 47 00           | push ebud71f.47D9D4                              | 47D9D4:"22222"
0040CD22 | FF D6                    | call esi                                         | esi:lstrcpyA
0040CD24 | 8B 0D A4 CC 47 00        | mov ecx,dword ptr ds:[47CCA4]                    |
0040CD2A | 8B 01                    | mov eax,dword ptr ds:[ecx]                       |
0040CD2C | FF 90 90 00 00 00        | call dword ptr ds:[eax+90]                       |
0040CD32 | 50                       | push eax                                         |
0040CD33 | 68 D8 DA 47 00           | push ebud71f.47DAD8                              | 47DAD8:"33333"
0040CD38 | FF D6                    | call esi                                         | esi:lstrcpyA
0040CD3A | 8B 0D A8 CC 47 00        | mov ecx,dword ptr ds:[47CCA8]                    |
0040CD40 | 8B 11                    | mov edx,dword ptr ds:[ecx]                       |
0040CD42 | FF 92 90 00 00 00        | call dword ptr ds:[edx+90]                       |
0040CD48 | 50                       | push eax                                         |
0040CD49 | 68 DC DB 47 00           | push ebud71f.47DBDC                              | 47DBDC:"44444"
0040CD4E | FF D6                    | call esi                                         | esi:lstrcpyA
0040CD50 | 8B 0D AC CC 47 00        | mov ecx,dword ptr ds:[47CCAC]                    |
0040CD56 | 8B 01                    | mov eax,dword ptr ds:[ecx]                       |
0040CD58 | FF 90 90 00 00 00        | call dword ptr ds:[eax+90]                       |
0040CD5E | 50                       | push eax                                         |
0040CD5F | 68 E0 DC 47 00           | push ebud71f.47DCE0                              | 47DCE0:"55555"
0040CD64 | FF D6                    | call esi                                         | esi:lstrcpyA

Navigating around the code a bit doesn’t show immediately useful being done with the results of these calls. The next hit gives the following call stack:

Moving down the frame to the return to 0x0040D035 brings us to a rather large function starting at 0x0040CFD0. This will be the target of investigation.

Analyzing the Function

The function starts out with the following code:

0040CFE0 | 8D 44 24 2C                | lea eax,dword ptr ss:[esp+2C]                    | [esp+2C]:lstrcpyA
0040CFE4 | 6A 06                      | push 6                                           |
0040CFE6 | 50                         | push eax                                         |
0040CFE7 | BE 01 00 00 00             | mov esi,1                                        |
0040CFEC | 33 DB                      | xor ebx,ebx                                      |
0040CFEE | 68 39 01 00 00             | push 139                                         |
0040CFF3 | 51                         | push ecx                                         |
0040CFF4 | 89 74 24 20                | mov dword ptr ss:[esp+20],esi                    | [esp+20]:WaitForSingleObject
0040CFF8 | 89 5C 24 30                | mov dword ptr ss:[esp+30],ebx                    |
0040CFFC | 89 5C 24 2C                | mov dword ptr ss:[esp+2C],ebx                    | [esp+2C]:lstrcpyA
0040D000 | 89 5C 24 38                | mov dword ptr ss:[esp+38],ebx                    |
0040D004 | E8 37 3D 00 00             | call ebu9e7d.410D40                              |
0040D009 | A1 04 D1 47 00             | mov dword ptr ds:[47D104]                        |
0040D00E | 8D 54 24 44                | lea edx,dword ptr ss:[esp+44]                    |
0040D012 | 6A 0A                      | push A                                           |
0040D014 | 52                         | push edx                                         |
0040D015 | 68 83 01 00 00             | push 183                                         |
0040D01A | 50                         | push eax                                         |
0040D01B | E8 20 3D 00 00             | call ebu9e7d.410D40                              |

Stepping into the function at 0x004100D40 this shows that it is responsible for loading some resources from the executable.

00410D45 | 55                         | push ebp                                         |
00410D46 | 8B 6C 24 18                | mov ebp,dword ptr ss:[esp+18]                    |
...
00410D4C | 8D 44 2D 00                | lea eax,dword ptr ss:[ebp+ebp]                   |
00410D50 | 33 FF                      | xor edi,edi                                      |
00410D52 | 50                         | push eax                                         |
00410D53 | C6 03 00                   | mov byte ptr ds:[ebx],0                          |
00410D56 | E8 EF 36 04 00             | call ebu9e7d.45444A                              | malloc
00410D5B | 8B F0                      | mov esi,eax                                      |
...
00410D77 | 8B 4C 24 18                | mov ecx,dword ptr ss:[esp+18]                    | [esp+18]:lstrcpyA
00410D7B | 8B 54 24 14                | mov edx,dword ptr ss:[esp+14]                    |
00410D7F | 55                         | push ebp                                         |
00410D80 | 56                         | push esi                                         |
00410D81 | 51                         | push ecx                                         |
00410D82 | 52                         | push edx                                         |
00410D83 | FF 15 5C 64 46 00          | call dword ptr ds:[<&LoadStringA>]               |

The definition of LoadStringA shows that the function takes in the HINSTANCE of the executable to load the resource from, a resource identifier, an output buffer to receive the resource data, and the length of the output buffer. Three of the four parameters are those that are passed into 0x004100D40. The parameter that isn’t passed in is the output buffer parameter. This buffer parameter is created via a call to a malloc wrapper that creates a buffer of (2 * nBufferMax) size. After the resource is successfully loaded (the call succeeds), the loaded resource is copied into the third parameter of 0x004100D40 and is null-terminated.

00410D8D | 8B CD                      | mov ecx, ebp
00410D8F | 8B FB                      | mov edi, ebx
00410D91 | 8B C1                      | mov eax, ecx
00410D93 | C1 E9 02                   | shr ecx, 2
00410D96 | F3 A5                      | rep movsd dword ptr es:[edi], dword ptr ds:[esi]
00410D98 | 8B C8                      | mov ecx, eax
00410D9A | 83 E1 03                   | and ecx, 3
00410D9D | F3 A4                      | rep movsb byte ptr es:[edi], byte ptr ds:[esi]
00410D9F | C6 44 2B FF 00             | mov byte ptr ds:[ebx+ebp-1], 0
00410DA4 | 8A 03                      | mov al, byte ptr ds:[ebx]
00410DA6 | 84 C0                      | test al, al
00410DA8 | 8B F3                      | mov esi, ebx

This is done through some pretty clever assembly involving some shifts and rep moves. The function continues on perform some checks for code pages, presumably because the resource data can have an ANSI code page or DBCS code page. The function wraps up by calling two other functions at 0x00410200 and 0x00454361, which at a quick glance are responsible for verifying some information about the resource. The function wraps up by returning the length of the loaded resource.

Stepping through these calls in the original function at 0x0040D035 reveals that the loaded resources are:

  • “69405” at resource index 0x139
  • “Z09-00001” at resource index 0x183
  • A missing resource at index 0x185

Continuing on, the function then proceeds to try to load a DLL and get the address of a function. The executed code is colored below:

0040D07B | 68 A8 D3 46 00             | push ebu9e7d.46D3A8                              | 46D3A8:"%SETUPEXEDIR"
0040D080 | F3 AB                      | rep stosd dword ptr es:[edi],eax                 | edi:"\\PidGen.dll"
0040D082 | 8D 4C 24 64                | lea ecx,dword ptr ss:[esp+64]                    |
0040D086 | 88 5C 24 44                | mov byte ptr ss:[esp+44],bl                      |
0040D08A | 51                         | push ecx                                         |
0040D08B | C7 84 24 70 02 00 00 00 01 | mov dword ptr ss:[esp+270],100                   |
0040D096 | 66 AB                      | stosw word ptr es:[edi],ax                       | edi:"\\PidGen.dll"
0040D098 | FF D5                      | call ebp                                         | ebp:lstrcpyA
0040D09A | 8D 54 24 60                | lea edx,dword ptr ss:[esp+60]                    |
0040D09E | 68 04 01 00 00             | push 104                                         |
0040D0A3 | 52                         | push edx                                         |
0040D0A4 | E8 57 31 00 00             | call ebu9e7d.410200                              |
0040D0A9 | 8D 44 24 68                | lea eax,dword ptr ss:[esp+68]                    |
0040D0AD | 50                         | push eax                                         |
0040D0AE | E8 FD 4E 00 00             | call ebu9e7d.411FB0                              |
0040D0B3 | 8B 35 20 63 46 00          | mov esi,dword ptr ds:[<&lstrcat>]                |
0040D0B9 | 83 C4 0C                   | add esp,C                                        |
0040D0BC | 8D 4C 24 60                | lea ecx,dword ptr ss:[esp+60]                    |
0040D0C0 | 68 2C D8 46 00             | push ebu9e7d.46D82C                              | 46D82C:"PidGen.dll"
0040D0C5 | 51                         | push ecx                                         |
0040D0C6 | FF D6                      | call esi                                         |
0040D0C8 | 8D 54 24 60                | lea edx,dword ptr ss:[esp+60]                    |
0040D0CC | 52                         | push edx                                         |
0040D0CD | E8 AE 25 00 00             | call ebu9e7d.40F680                              |
0040D0D2 | 83 C4 04                   | add esp,4                                        |
0040D0D5 | 85 C0                      | test eax,eax                                     |
0040D0D7 | 75 46                      | jne ebu9e7d.40D11F                               |
0040D0D9 | 8D 44 24 60                | lea eax,dword ptr ss:[esp+60]                    |
0040D0DD | 50                         | push eax                                         |
0040D0DE | 68 04 01 00 00             | push 104                                         |
0040D0E3 | FF 15 00 63 46 00          | call dword ptr ds:[<&GetTempPathA>]              |
0040D0E9 | 8D 4C 24 60                | lea ecx,dword ptr ss:[esp+60]                    |
0040D0ED | 51                         | push ecx                                         |
0040D0EE | E8 BD 4E 00 00             | call ebu9e7d.411FB0                              |
0040D0F3 | 83 C4 04                   | add esp,4                                        |
0040D0F6 | 8D 54 24 60                | lea edx,dword ptr ss:[esp+60]                    |
0040D0FA | 68 2C D8 46 00             | push ebu9e7d.46D82C                              | 46D82C:"PidGen.dll"
0040D0FF | 52                         | push edx                                         |
0040D100 | FF D6                      | call esi                                         |
0040D102 | 8D 44 24 60                | lea eax,dword ptr ss:[esp+60]                    |
0040D106 | 50                         | push eax                                         |
0040D107 | E8 74 25 00 00             | call ebu9e7d.40F680                              |
0040D10C | 83 C4 04                   | add esp,4                                        |
0040D10F | 85 C0                      | test eax,eax                                     |
0040D111 | 75 0C                      | jne ebu9e7d.40D11F                               |
0040D113 | 8D 4C 24 60                | lea ecx,dword ptr ss:[esp+60]                    |
0040D117 | 68 20 BB 47 00             | push ebu9e7d.47BB20                              |
0040D11C | 51                         | push ecx                                         |
0040D11D | FF D5                      | call ebp                                         | ebp:lstrcpyA
0040D11F | 38 5C 24 60                | cmp byte ptr ss:[esp+60],bl                      |
0040D123 | 0F 84 51 01 00 00          | je ebu9e7d.40D27A                                |
0040D129 | 8D 54 24 60                | lea edx,dword ptr ss:[esp+60]                    |
0040D12D | 52                         | push edx                                         |
0040D12E | FF 15 98 61 46 00          | call dword ptr ds:[<&LoadLibraryA>]              |
0040D134 | 8B F0                      | mov esi,eax                                      |
0040D136 | 3B F3                      | cmp esi,ebx                                      |
0040D138 | 89 74 24 24                | mov dword ptr ss:[esp+24],esi                    |
0040D13C | 0F 84 38 01 00 00          | je ebu9e7d.40D27A                                |
0040D142 | 68 20 D8 46 00             | push ebu9e7d.46D820                              | 46D820:"PIDGenSimpA"
0040D147 | 56                         | push esi                                         |
0040D148 | FF 15 E4 62 46 00          | call dword ptr ds:[4662E4]                       |
0040D14E | 3B C3                      | cmp eax,ebx                                      |

The first part in orange is responsible for building the path string of the PidGen.dll file. There are other functions that are called above which are responsible for making sure that the file exists and that it has the correct file attributes. Once the path string is built, the PidGen.dll file is loaded with a call to LoadLibrary and the address of the PIDGenSimpA function is retrieved via a call to GetProcAddress (teal).

The Verification Call

The call to PIDGenSimpA shows that it takes nine parameters

0040D156 | 8D 4C 24 28                | lea ecx,dword ptr ss:[esp+28]                    |
0040D15A | 8D 54 24 20                | lea edx,dword ptr ss:[esp+20]                    |
0040D15E | 51                         | push ecx                                         |
0040D15F | 8D 8C 24 6C 02 00 00       | lea ecx,dword ptr ss:[esp+26C]                   |
0040D166 | 52                         | push edx                                         |
0040D167 | 51                         | push ecx                                         |
0040D168 | 8B 4C 24 28                | mov ecx,dword ptr ss:[esp+28]                    |
0040D16C | 8D 54 24 4C                | lea edx,dword ptr ss:[esp+4C]                    |
0040D170 | 52                         | push edx                                         |
0040D171 | 51                         | push ecx                                         |
0040D172 | 8D 54 24 28                | lea edx,dword ptr ss:[esp+28]                    |
0040D176 | 8D 4C 24 48                | lea ecx,dword ptr ss:[esp+48]                    |
0040D17A | 52                         | push edx                                         |
0040D17B | 51                         | push ecx                                         |
0040D17C | 8B 8C 24 88 03 00 00       | mov ecx,dword ptr ss:[esp+388]                   |
0040D183 | 8D 54 24 48                | lea edx,dword ptr ss:[esp+48]                    |
0040D187 | 52                         | push edx                                         |
0040D188 | 51                         | push ecx                                         |
0040D189 | FF D0                      | call eax                                         |

At the time of the call, the stack looks like the following:

0496F23C  0496F5E0  "1111122222333334444455555"
0496F240  0496F28C  "69405"
0496F244  0496F294  "Z09-00001"
0496F248  0496F274  
0496F24C  00000000  
0496F250  0496F2A0  
0496F254  0496F4C8  
0496F258  0496F280  
0496F25C  0496F288  

The first three parameters are plainly obvious; they are the input product key, and the two resources that were loaded from the executable earlier in the function. The next six are a bit harder to pin down however. Stepping back through the function and looking at the origin of these addresses shows that:

  • 0x0496F274 comes from the third string resource at index 0x185 that is attempted to be loaded. Since the resource does not exist, this string remains empty.
  • The value of 0 is initialized at the start of the function. It is possible to have it set to 1 if the call to 0x004112D0 returns 1, which it doesn’t in this case.
  • 0x0496F2A0 is not initialized anywhere, so it is likely an optional or output parameter
  • 0x0496F4C8 is written into at 0x0040D08B, which writes in 0x100 (256). Following 0x0496F4C8 in the memory dump shows that it contains the bytes 00 01 00 00, which is the little endian representation of 0x100 (256).
  • 0x0496F280 is not initialized anywhere, so it is likely an optional or output parameter
  • 0x0496F228 is not initialized anywhere, so it is likely an optional or output parameter

Since an invalid key is being entered, it will be useful to study the failure case and see what conditions bring up the “Invalid Product Key” popup. The call to PIDGenSimpA returns with a value of 1 when an invalid product key is entered. The following instructions are then executed:

0040D18B | 3B C3                      | cmp eax,ebx                                      |
0040D18D | 75 64                      | jne ebu9e7d.40D1F3                               |
...
0040D1F3 | 8B 0D 04 D1 47 00          | mov ecx,dword ptr ds:[47D104]                    |
0040D1F9 | 8D 84 24 64 01 00 00       | lea eax,dword ptr ss:[esp+164]                   |
0040D200 | 68 04 01 00 00             | push 104                                         |
0040D205 | 50                         | push eax                                         | eax:"Invalid Product Key"
0040D206 | 68 A3 00 00 00             | push A3                                          |
0040D20B | 51                         | push ecx                                         |
0040D20C | E8 2F 3B 00 00             | call ebu9e7d.410D40                              |
0040D211 | A1 0C D1 47 00             | mov dword ptr ds:[47D10C]                        |
0040D216 | 83 C4 10                   | add esp,10                                       |
0040D219 | 8D 94 24 64 01 00 00       | lea edx,dword ptr ss:[esp+164]                   |
0040D220 | 52                         | push edx                                         | edx:"Invalid Product Key"
0040D221 | 6A 30                      | push 30                                          |
0040D223 | 50                         | push eax                                         |
0040D224 | EB 32                      | jmp ebu9e7d.40D258                               |
...
0040D258 | E8 23 26 00 00             | call ebu9e7d.40F880                              |

The “Invalid Product Key” popup comes after stepping over the call at 0x0040D258, which calls 0x0040F8880. This concludes the analysis of the error-case. The next part will cover what happens in the success case and how to properly get the application to respond to the success case with an invalid product key.

Thanks for reading and follow on Twitter for more updates.

  1. No comments yet.
  1. No trackbacks yet.