- .rdata:0000000180628C74 00000005 C game
Some tools you will want to have while doing this, this is what I use:
- x64dbg
- IDA (or similar disassembler)
Preface
The CryEngine platform allows you to encrypt and zip the content and data of your game using their in-house Zip engine that makes use of RSA encryption to attempt to prevent people from accessing the data files. While compiling the game, developers can alter the RSA key used for this process making the unpacking process unique to their game. This will be a short guide on how to find this key in Wolcen.
Understanding CryEngine
Before mindlessly poking about the game, it is best to get yourself familiar with the engine. CryEngine is open source therefore you can find information regarding it fairly easily. You can also find how the PAK system works entirely through the source code, where the RSA key is used and how its stored etc. Wolcen uses CryEngine 3, which is a bit more effort to obtain a full copy of the source code but it is out there.
First, we find the RSA key stored in the following file:
\GameSDK\GameDll\GameStartup.cpp
Which looks like this:
- static unsigned char g_rsa_public_key_data[] =
- {
- 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
- 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
- 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
- 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
- 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
- 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
- 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
- };
- const uint8* CGameStartup::GetRSAKey(uint32 *pKeySize) const
- {
- *pKeySize = sizeof(g_rsa_public_key_data);
- return g_rsa_public_key_data;
- }
So we know a call to GetRSAKey will return the data stored in this buffer. We know the buffer is just a static block of memory as well.
If we trace back further to where this is used, we can find the following in the code:
CryEngine\CrySystem\CryPak.cpp
- CCryPak::CCryPak(IMiniLog* pLog, PakVars* pPakVars, const bool bLvlRes, const IGameStartup *pGameStartup ):
- m_pLog (pLog),
- m_eRecordFileOpenList(RFOM_Disabled),
- m_pPakVars (pPakVars?pPakVars:&g_cvars.pakVars),
- m_fFileAcessTime(0.f),
- m_bLvlRes(bLvlRes),
- m_renderThreadId(0),
- m_pWidget(NULL)
- {
- #if defined(LINUX) || defined(APPLE)
- m_HandleSource = 0;
- #endif
- m_pITimer = gEnv->pTimer;
- m_bGameFolderWritable = true;
- m_disableRuntimeFileAccess[0] = m_disableRuntimeFileAccess[1] = false;
- // Default game folder
- m_strDataRoot = "game";
- m_strDataRootWithSlash = m_strDataRoot + (char)g_cNativeSlash;
- m_pEngineStartupResourceList = new CResourceList;
- m_pLevelResourceList = new CResourceList;
- m_pNextLevelResourceList = new CNextLevelResourceList;
- m_arrAliases.reserve(16);
- m_arrArchives.reserve(64);
- #if defined(XENON) || defined(PS3)
- m_bInstalledToHDD = false;
- #else
- m_bInstalledToHDD = true;
- #endif
- m_mainThreadId = GetCurrentThreadId();
- gEnv->pSystem->GetISystemEventDispatcher()->RegisterListener(this);
- #ifdef INCLUDE_LIBTOMCRYPT
- if(pGameStartup)
- {
- uint32 keyLen;
- const uint8* pKeyData = pGameStartup->GetRSAKey(&keyLen);
- if(pKeyData && keyLen > 0)
- {
- ZipEncrypt::Init(pKeyData, keyLen);
- }
- }
- #endif
- }
This is the important function to us. We want to find this function in the game while its starting up. Because of this preparing the ZipEncrypt system with the RSA key, this is what we want to find and set a breakpoint on in order to dump the key.
So for now we know the following about what we need:
- The code we want would be compiled inside of: CrySystem.dll (This is where CryPak.cpp is from.)
- The function we are looking for (CCryPak::CCryPak) has a string reference we can look for which is the word: "game"
- The function we are looking for (CCryPak::CCryPak) has a common API reference we can look for which is: GetCurrentThreadId
This is what we can use to find this function a bit easier. There is other data here we can use as well such as the .reserve calls using 16 and 64 as values, but for now the "game" string is plenty.
Finding The String Reference
Next, we will open up CrySystem.dll inside of IDA. You can find this dll in your Wolcen install folder such as:
<Path To Steam>\steamapps\common\Wolcen\win_x64\CrySystem.dll
Keep in mind this is a 64bit game so you need a 64bit disassembler or similar!
Once this DLL is fully loaded, we can then open the string references found within it. In IDA, you can do this via:
View -> Open Subviews -> Strings
Once this loads, use the search bar at the bottom (Ctrl+F to open it) and enter the word 'game' without quotes. You will get a lot of results, so sort the strings and find the lower-case version of the word game as its written in the code. You should find a reference like this:
Double-click this entry. Now you should see it in in the main IDA view. You should see the word "game" to the right and a named variable such as 'aGame' next to it to the left.
- .rdata:0000000180628C74 aGame db 'game',0 ; DATA XREF: sub_180137010+2D0↑o
- .rdata:0000000180628C79 align 20h
Here we will right-click the aGame variable and choose 'Jump to xref to operand'. In the new window there should be only 1 result. Double-click that and we are taken the function that references this. Once the function in raw assembly loads, I recommend switching to Text-View for easier to read code. To do that right-click in the grey areas of the window and choose 'Text View'. You should see something like this:
- .text:00000001801372D4 call sub_1800176AC
- .text:00000001801372D9 mov [r14+388h], rax
- .text:00000001801372E0 lea rdx, aGame ; "game"
- .text:00000001801372E7 mov [r14+398h], rbp
If we scroll a little bit down we will also see the call to GetCurrentThreadId.
- .text:00000001801376AC mov byte ptr [r14+208h], 1
- .text:00000001801376B4 call cs:__imp_GetCurrentThreadId
- .text:00000001801376BA mov [r14+2ACh], eax
So now we can find the call that handled the key here. We know it was near the bottom of the function so we can assume where it is at with this information. Because it came after GetCurrentThreadId we know it will be below it. (Keep in mind depending on certain compilers this is not always true, optimizations can reorganize code that will alter the order they are in, so keep this in mind!)
Here is the last part of the function we will be working with:
- .text:00000001801376BA mov [r14+2ACh], eax
- .text:00000001801376C1 mov rax, cs:qword_1807421F8
- .text:00000001801376C8 mov rcx, [rax+0C0h]
- .text:00000001801376CF mov rax, [rcx]
- .text:00000001801376D2 call qword ptr [rax+230h]
- .text:00000001801376D8 lea rdx, [r14+8]
- .text:00000001801376DC mov rcx, rax
- .text:00000001801376DF mov r8, [rax]
- .text:00000001801376E2 call qword ptr [r8+8]
- .text:00000001801376E6 mov rcx, [rsp+78h+arg_20]
- .text:00000001801376EE test rcx, rcx
- .text:00000001801376F1 jz short loc_180137719
- .text:00000001801376F3 mov rax, [rcx]
- .text:00000001801376F6 lea rdx, [rsp+78h+arg_18]
- .text:00000001801376FE call qword ptr [rax+40h]
- .text:0000000180137701 test rax, rax
- .text:0000000180137704 jz short loc_180137719
- .text:0000000180137706 mov edx, [rsp+78h+arg_18]
- .text:000000018013770D test edx, edx
- .text:000000018013770F jz short loc_180137719
- .text:0000000180137711 mov rcx, rax
- .text:0000000180137714 call sub_18001B806
We know that the last part of the function to use the key was the following call:
- ZipEncrypt::Init(pKeyData, keyLen);
So here we can follow the last call and see if this takes us to the same thing.
Double click on the sub_18001B806 here then follow the jump table again that shows up.
In this function we will see a string shortly into the function like this:
- .text:00000001800C86D5 lea rcx, off_180655D60 ; Buf2
- .text:00000001800C86DC call sub_180023B96
- .text:00000001800C86E1 lea rcx, aYarrow ; "yarrow"
Perfect, if we follow the ZipEncrypt::Init function in the actual code, we will see this:
- void ZipEncrypt::Init(const uint8* pKeyData, uint32 keyLen)
- {
- ltc_mp = ltm_desc;
- register_hash (&sha1_desc);
- register_hash (&sha256_desc);
- register_cipher (&twofish_desc);
- int prng_idx = register_prng(&yarrow_desc) != -1;
- assert( prng_idx != -1 );
- rng_make_prng(128, find_prng("yarrow"), &g_yarrow_prng_state, NULL);
Which does include the 'yarrow' string. We now know where we need to set a breakpoint and dump some information.
Since IDA loads the DLL as a static file at a pre-defined base address we need the offset inside of it where this call was at. So from our information above we have:
- .text:0000000180137714 call sub_18001B806
Then we need the base address IDA is using for this. We can find this by simply scrolling up to the top of the IDA View and see this:
- .text:0000000180001000 ; Input SHA256 : DE8FF68F112C36246A031375A34179CB18E0EFD381D5914358B6D7DC22011916
- .text:0000000180001000 ; Input MD5 : 0D88EC6FBECBDA7CDFB9983B9ABDD9E1
- .text:0000000180001000 ; Input CRC32 : 009FF1D1
- .text:0000000180001000
- .text:0000000180001000 ; File Name : D:\Games\Steam\steamapps\common\Wolcen\win_x64\CrySystem.dll
- .text:0000000180001000 ; Format : Portable executable for AMD64 (PE)
- .text:0000000180001000 ; Imagebase : 180000000
- .text:0000000180001000 ; Timestamp : 5AD84A1D (Thu Apr 19 07:49:49 2018)
- .text:0000000180001000 ; Section 1. (virtual address 00001000)
- .text:0000000180001000 ; Virtual size : 006160CD (6381773.)
- .text:0000000180001000 ; Section size in file : 00616200 (6382080.)
- .text:0000000180001000 ; Offset to raw data for section: 00000400
- .text:0000000180001000 ; Flags 60000020: Text Executable Readable
- .text:0000000180001000 ; Alignment : default
- .text:0000000180001000 ; PDB File Name : c:\Wolcen\Umbra\BinTemp\win_x64_release\Code\CryEngine\CrySystem\CrySystem.pdb
- .text:0000000180001000
Imagebase is what we're after. So we have:
0000000180137714 - The address of the call to ZipEncrypt::Init
180000000 - The base address of the CrySystem.dll inside of IDA.
We take these two values and subtract them from each other to get our offset. Which is:
0000000180137714 - 180000000 = 137714 (keep in mind these values are all in hexadecimal!)
Next step is to debug the game.
Debugging Wolcen / Dumping The Key
Next, load up x64dbg and load up Wolcen.exe into it. Before we go further we want to force x64dbg to stop each time a DLL is loaded. This is important because we want it to stop as soon as CrySystem.dll is loaded before it calls the ZipEncrypt::Init function.
So in x64dbg go to: Options -> Preferences -> Events -> Break On - make sure DLL Load is checked.
We will also want to ignore exceptions that happen while its loading. So we can ignore all of them by opening:
Options -> Preferences -> Exceptions -> Add Range -> and enter:
Start: 0
End: FFFFFFFF
Click OK -> Save.
Now, press the -> (Run - F9) button to continue stepping through the loading of Wolcen until you see it has loaded CrySystem.dll. x64dbg will keep pausing each time it loads a DLL which you can see what was loaded in the bottom status bar next to the yellow 'Puased' word.
Once you see it say it loaded CrySystem.dll stop pressing the play button.
Now that it is loaded we want to go to our offset to set a breakpoint. To do this we do the following:
- In x64dbg, click on the 'Memory Map' tab.
- Scroll down til you find 'crysystem.dll' in the Info column.
- Right-click and choose 'Follow in Disassembler'.
- In the Disassembler, right-click the first line that has the bytes '4D 5A' and choose Copy -> Address.
- Press CTRL+G to open the Go To window.
- Paste in the address you just copied then also add: + 137714 which is our offset we calculated from before.
- Your Goto should look like this: 00007FF9707A0000 + 137714 (With your address differing from mine of course.)
- Click OK to go to this address. it should land on a call like this:
- 00007FF9708D7714 | E8 ED 40 EE FF | call crysystem.7FF9707BB806 |
Press F2 now to set a breakpoint here. Next we can remove the option to stop each time a DLL is loaded. So to remove that we do:
Options -> Preferences -> Events -> Break On - make sure DLL Load is unchecked now.
Then click Save and press the -> (Run - F9) button again to let the game continue to load until we hit our breakpoint here.
Once the breakpoint is hit, we can dump the key now.
Looking at the registers on the right side of x64dbg, we can see the following:
- RAX holds a pointer to the RSA key.
- RDX holds the size of the RSA key.
Right-click on the RAX value and choose 'Follow in Dump'.
Here is the RSA key.

Copy the size of the key that matches RDX's value, currently it is 8C bytes long, again which is hex so you want to copy 140 bytes (decimal) long of data to get the full key.
- 00007FF96C5D1790 30 81 89 02 81 81 00 DD 6A 61 1E 84 79 D7 AE 91 0......Ýja..y×®.
- 00007FF96C5D17A0 60 50 6A 7E 9A 99 74 95 27 28 06 29 4F 1E 4D DC `Pj~..t.'(.)O.MÜ
- 00007FF96C5D17B0 C7 52 46 1F 5F 63 11 7B 61 E9 06 50 42 15 97 FD ÇRF._c.{aé.PB..ý
- 00007FF96C5D17C0 03 04 B1 BE 89 2B C3 4E DA D1 7E E7 5C 18 E4 21 ..±¾.+ÃNÚÑ~ç\.ä!
- 00007FF96C5D17D0 69 03 E2 7D 7A F5 83 A6 50 5E 72 00 EF CA E5 07 i.â}zõ.¦P^r.ïÊå.
- 00007FF96C5D17E0 67 5C BB 77 7A E7 74 FE C0 F9 99 FA 41 1E 80 7C g\»wzçtþÀù.úA..|
- 00007FF96C5D17F0 54 BB 18 CB FC 86 48 35 BC DD C8 6F 34 97 6C B8 T».Ëü.H5¼ÝÈo4.l¸
- 00007FF96C5D1800 82 4B 76 94 76 34 BF 2D 70 52 2B BA 15 35 36 6E .Kv.v4¿-pR+º.56n
- 00007FF96C5D1810 06 D9 88 D4 00 70 9D 02 03 01 00 01 .Ù.Ô.p......
And that's it, you now have the needed RSA key to decrypt the PAK files of Wolcen.
Closing..
One last note for this, this should technically work for any CryEngine game as long as they haven't heavily modified things to where this information is invalid. But, in general, you should be able to dump the RSA key of any CryEngine game using this method.
For more advanced users, you can just create a pattern to find and dump the key as well through other means, but I wont go into detail for that. It's simple enough for those that know how to do it, this was more of a tutorial aimed at getting it manually for those that wanted a tutorial from me.
Enjoy!