Registration Code (Part 1): w%kQ6
Registration Code (Part 2): b<#$1[*(cw~
In order to register on this forum, you must use the codes above. Combine them into one code (copy paste).

Dino Crisis Timer Information

Programming topics that relate to the C/C++ languages.
Post Reply
User avatar
atom0s
Site Admin
Posts: 408
Joined: Sun Jan 04, 2015 11:23 pm
Location: 127.0.0.1
Contact:

Dino Crisis Timer Information

Post by atom0s » Fri Jan 09, 2015 9:53 am

I got asked to help with another timer project for a speed run thing for the PC game 'Dino Crisis'. So here is the information on figuring out the timer data for this game.

1. Save Game Analysis
Because searching for unknown values that constantly change is a pain in the ass and annoying, I choose to use other means to locate data such as the timer. In my case, I use the save game files because it tends to be a very helpful location for finding information based on your character and game data.

To start, we play the game through until we get to the first Maintenance Room. Here, we can save the game every time we try to exit the room. So to start, we save once. The file is stored at:
C:\Program Files (x86)\CAPCOM\DINOCRISIS
- SAVENO1.dno

Now that we have 1 save, leave the room, enter it and then exit again allowing us to resave. This time save in the 2nd save slot making the new file:
- SAAVENO2.dno

Keep note of the time shown on the screen for each save spot.
For example my two saves were:
- #1: 29:22
- #2: 31:29

Next we compare the save files to each other. And find the differences between the two of them. In this games case there are a handful of differences since we have to move and such while we are trying to save between games. So another step we can take is converting the timers we have to their storage type.

Now, this step will not work for every game, this is just a guess and in our case it worked out for this game. (CAPCOM seems to do the same for almost all their games) so it works out well for us. So we take the timer we have, for example:
29:22

And we convert this back to the base seconds timer.
  1. timer = original tick count from the game
  2.  
  3. abilityTimer = timer / 60;
  4. hour = floor( abilityTimer / ( 60 * 60 ) );
  5. mins = floor( abilityTimer / 60 - hour * 60 );
  6. secs = floor( abilityTimer - ( mins + hour * 60 ) * 60 );
This is how the timestamp is created for CAPCOM games. So we can do the reverse of this to get our value back. Doing so will give us a value in the range of:
105700 - 105800

So we can check the differences of the two save games and locate this value.
Image

Which we find is:
105724

2. Memory Analysis
Next after we find the value from the save game file, we can restart the game and go to the load menu. Once there, scan for the value we found in the save game file with a memory scanner such as Cheat Engine.

After searching (in my case only found 1 result) add this item to the cheat list then use Cheat Engines 'Find What Accesses This Address' feature.

Now we will find that the following is reading the data for the load screen:
  1. 0041D93C - 8B 48 0C              - mov ecx,[eax+0C]
If you look at the overall function you can see it is calculating the timer from the save game file:
  1. 0041D939 - 8B 45 08              - mov eax,[ebp+08]
  2. 0041D93C - 8B 48 0C              - mov ecx,[eax+0C]
  3. 0041D93F - 89 4D E4              - mov [ebp-1C],ecx
  4. 0041D942 - 8B 45 E4              - mov eax,[ebp-1C]
  5. 0041D945 - 33 D2                 - xor edx,edx
  6. 0041D947 - B9 C04B0300           - mov ecx,00034BC0
  7. 0041D94C - F7 F1                 - div ecx
  8. 0041D94E - 89 45 E0              - mov [ebp-20],eax
  9. 0041D951 - 8B 55 E0              - mov edx,[ebp-20]
  10. 0041D954 - 69 D2 C04B0300        - imul edx,edx,00034BC0
  11. 0041D95A - 8B 45 E4              - mov eax,[ebp-1C]
  12. 0041D95D - 2B C2                 - sub eax,edx
  13. 0041D95F - 89 45 E4              - mov [ebp-1C],eax
  14. 0041D962 - 8B 45 E4              - mov eax,[ebp-1C]
  15. 0041D965 - 33 D2                 - xor edx,edx
  16. 0041D967 - B9 100E0000           - mov ecx,00000E10
  17. 0041D96C - F7 F1                 - div ecx
  18. 0041D96E - 89 45 F8              - mov [ebp-08],eax
  19. 0041D971 - 8B 55 F8              - mov edx,[ebp-08]
  20. 0041D974 - 69 D2 100E0000        - imul edx,edx,00000E10
  21. 0041D97A - 8B 45 E4              - mov eax,[ebp-1C]
  22. 0041D97D - 2B C2                 - sub eax,edx
  23. 0041D97F - 89 45 E4              - mov [ebp-1C],eax
  24. 0041D982 - 8B 45 E4              - mov eax,[ebp-1C]
  25. 0041D985 - 33 D2                 - xor edx,edx
  26. 0041D987 - B9 3C000000           - mov ecx,0000003C
  27. 0041D98C - F7 F1                 - div ecx
So know we know that the format we are using is correct, we can use part of this function to locate other parts of the game that make use of it. (Such as the save screen.)

To do that we take a chunk of the data here and scan for it as an array of bytes. In my case I choose this chunk:
33 D2 B9 10 0E 00 00 F7 F1
  1. 0041D945 - 33 D2                 - xor edx,edx
  2. 0041D947 - B9 C04B0300           - mov ecx,00034BC0
  3. 0041D94C - F7 F1                 - div ecx
Now we found 4 results, so we take a look at each one a bit more in depth.
The first one is actually the one we are after luckily.

Inside the first function result, we find:
  1. 0041D380 - 55                    - push ebp
  2. 0041D381 - 8B EC                 - mov ebp,esp
  3. 0041D383 - 81 EC 8C000000        - sub esp,0000008C
  4. 0041D389 - C7 45 D8 0000801F     - mov [ebp-28],1F800000
  5. 0041D390 - 81 7D D8 0000801F     - cmp [ebp-28],1F800000
  6. 0041D397 - 72 09                 - jb 0041D3A2
  7. 0041D399 - 81 7D D8 0004801F     - cmp [ebp-28],1F800400
  8. 0041D3A0 - 72 08                 - jb 0041D3AA
  9. 0041D3A2 - 8B 45 D8              - mov eax,[ebp-28]
  10. 0041D3A5 - 89 45 D4              - mov [ebp-2C],eax
  11. 0041D3A8 - EB 13                 - jmp 0041D3BD
  12. 0041D3AA - 8B 4D D8              - mov ecx,[ebp-28]
  13. 0041D3AD - 8B 15 08E16700        - mov edx,[0067E108] : [022C6040]
  14. 0041D3B3 - 8D 84 0A 000080E0     - lea eax,[edx+ecx-1F800000]
  15. 0041D3BA - 89 45 D4              - mov [ebp-2C],eax
  16. 0041D3BD - 8B 4D D4              - mov ecx,[ebp-2C]
  17. 0041D3C0 - 8B 11                 - mov edx,[ecx]
  18. 0041D3C2 - 8B 82 B49A0000        - mov eax,[edx+00009AB4]
  19. 0041D3C8 - 89 45 E0              - mov [ebp-20],eax
  20. 0041D3CB - 8B 45 E0              - mov eax,[ebp-20]
  21. 0041D3CE - 33 D2                 - xor edx,edx
  22. 0041D3D0 - B9 C04B0300           - mov ecx,00034BC0
  23. 0041D3D5 - F7 F1                 - div ecx
So we can see its using the same formatting to calculate the timer. However, it is not pulling the same value as the load screens was. Instead this one is reading the value from:
  1. 0041D3AD - 8B 15 08E16700        - mov edx,[0067E108] : [022C6040]
  2. 0041D3B3 - 8D 84 0A 000080E0     - lea eax,[edx+ecx-1F800000]
  3. 0041D3BA - 89 45 D4              - mov [ebp-2C],eax
  4. 0041D3BD - 8B 4D D4              - mov ecx,[ebp-2C]
  5. 0041D3C0 - 8B 11                 - mov edx,[ecx]
  6. 0041D3C2 - 8B 82 B49A0000        - mov eax,[edx+00009AB4]
  7. 0041D3C8 - 89 45 E0              - mov [ebp-20],eax
  8. 0041D3CB - 8B 45 E0              - mov eax,[ebp-20]
So we can see the base is: 0067E108
Then it is read into, then stored, and read into from the base plus: 00009AB4

So we see that ultimately, the game is reading:
[[[0067E108]+0]+09AB4]

So we follow this and find that its the current in-game timer. :)
Image

3. Confirming Our Work
To double check and make sure things are correct, we can reset the game and go to the load screen again. With one of our save games we know the timer in the save game file is:
00:31:29 (113368)

From the load screen we scan for our value of: 113368
So we find this value at: dino.exe+2C66AC (6C66AC)

Now select the game, but before selecting a costume, scan for our value again: 113368

We now find more than one address of this value, now run the game as normal. One of those addresses will now be constantly incrementing which is our in-game timer value.

Source Code
Here is some source code to read and display the timer over the game window using simple GDI:
  1. /**
  2.  * Dino Crisis (PC) - Timer Trainer
  3.  * (c) 2014 atom0s [atom0s@live.com]
  4.  */
  5.  
  6. #include <Windows.h>
  7. #include <iostream>
  8. #include <TlHelp32.h>
  9.  
  10. /**
  11.  * @brief Game Executable Names
  12.  */
  13. const char g_GameNames[][255] =
  14. {
  15.     { "DINO.exe" },
  16.     { NULL }
  17. };
  18.  
  19. /**
  20.  * @brief Data structure passed to our window enumeration callback.
  21.  */
  22. typedef struct _WINDOW_DATA
  23. {
  24.     DWORD   ProcessId;
  25.     HWND    WindowHandle;
  26. } WINDOW_DATA;
  27.  
  28. /**
  29.  * @brief Enumerates the running windows on the system.
  30.  *
  31.  * @Param hWnd      The current window handle.
  32.  * @Param lParam    The custom data passed to this callback.
  33.  *
  34.  * @returN True to keep searching, false otherwise.
  35.  */
  36. BOOL __stdcall EnumWindowsProc(HWND hWnd, LPARAM lParam)
  37. {
  38.     auto procId = (DWORD)0;
  39.     auto wd = (WINDOW_DATA*)lParam;
  40.  
  41.     ::GetWindowThreadProcessId(hWnd, &procId);
  42.     if (procId == wd->ProcessId)
  43.     {
  44.         wd->WindowHandle = hWnd;
  45.         return FALSE;
  46.     }
  47.     return TRUE;
  48. }
  49.  
  50. /**
  51.  * @brief Obtains the main window hwnd of the given process id.
  52.  *
  53.  * @Param dwProcId          The process id of our game.
  54.  *
  55.  * @returN The window handle if found, NULL otherwise.
  56.  */
  57. HWND getResidentEvilWindowHandle(DWORD dwProcId)
  58. {
  59.     auto wd = new WINDOW_DATA();
  60.     wd->ProcessId = dwProcId;
  61.  
  62.     ::EnumWindows(EnumWindowsProc, (LPARAM)wd);
  63.  
  64.     auto hWnd = wd->WindowHandle;
  65.     delete wd;
  66.  
  67.     return hWnd;
  68. }
  69.  
  70. /**
  71.  * @brief Obtains the process id of our target.
  72.  * @returN The process id if found, 0 otherwise.
  73.  */
  74. DWORD getGameProcessId(void)
  75. {
  76.     PROCESSENTRY32 pe32{ sizeof(PROCESSENTRY32) };
  77.  
  78.     auto handle = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  79.     if (handle == INVALID_HANDLE_VALUE)
  80.         return 0;
  81.  
  82.     if (!::Process32First(handle, &pe32))
  83.     {
  84.         ::CloseHandle(handle);
  85.         return 0;
  86.     }
  87.  
  88.     do
  89.     {
  90.         for (auto x = 0; x < _countof(g_GameNames); x++)
  91.         {
  92.             if (g_GameNames[x][0] != NULL && _strnicmp(pe32.szExeFile, g_GameNames[x], strlen(g_GameNames[x])) == 0)
  93.             {
  94.                 ::CloseHandle(handle);
  95.                 return pe32.th32ProcessID;
  96.             }
  97.         }
  98.     } while (::Process32Next(handle, &pe32));
  99.  
  100.     ::CloseHandle(handle);
  101.     return 0;
  102. }
  103.  
  104. /**
  105.  * @brief Obtains the process base address of the target process.
  106.  *
  107.  * @Param dwProcId  The process id to obtain the base address of.
  108.  * @returN The process base address if found, 0 otherwise.
  109.  */
  110. DWORD getGameProcessBaseAddress(DWORD dwProcId)
  111. {
  112.     MODULEENTRY32 me32{ sizeof(MODULEENTRY32) };
  113.  
  114.     auto handle = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcId);
  115.     if (handle == INVALID_HANDLE_VALUE)
  116.         return 0;
  117.  
  118.     if (!::Module32First(handle, &me32))
  119.     {
  120.         ::CloseHandle(handle);
  121.         return 0;
  122.     }
  123.  
  124.     ::CloseHandle(handle);
  125.     return (DWORD)me32.modBaseAddr;
  126. }
  127.  
  128. /**
  129.  * @brief Obtains the process base size of the target process.
  130.  *
  131.  * @Param dwProcId  The process id to obtain the base address of.
  132.  * @returN The process base size if found, 0 otherwise.
  133.  */
  134. DWORD getGameProcessBaseSize(DWORD dwProcId)
  135. {
  136.     MODULEENTRY32 me32{ sizeof(MODULEENTRY32) };
  137.  
  138.     auto handle = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcId);
  139.     if (handle == INVALID_HANDLE_VALUE)
  140.         return 0;
  141.  
  142.     if (!::Module32First(handle, &me32))
  143.     {
  144.         ::CloseHandle(handle);
  145.         return 0;
  146.     }
  147.  
  148.     ::CloseHandle(handle);
  149.     return me32.modBaseSize;
  150. }
  151.  
  152. /**
  153.  * @brief Compares the given memory data against the given pattern using the desired mask.
  154.  *
  155.  * @Param lpData            Pointer to the actual memory to be compared again.
  156.  * @Param lpPattern         Pointer to the pattern to scan the memory for.
  157.  * @Param pszMask           String containing the mask for the pattern being compared against.
  158.  *
  159.  * @returN True when the pattern is found, false otherwise.
  160.  */
  161. static bool __stdcall MaskCompare(const unsigned char* lpData, const unsigned char* lpPattern, const char* pszMask)
  162. {
  163.     for (; *pszMask; ++pszMask, ++lpData, ++lpPattern)
  164.         if (*pszMask == 'x' && *lpData != *lpPattern)
  165.             return false;
  166.     return (*pszMask) == NULL;
  167. }
  168.  
  169. /**
  170.  * @brief Locates the given pattern inside the given data.
  171.  *
  172.  * @Param lpData            The data to scan for our pattern within.
  173.  * @Param nDataSize         The size of the data block to scan within.
  174.  * @Param lpPattern         The pattern to compare the memory against.
  175.  * @Param pszMask           String containing the mask for the pattern being compared against.
  176.  *
  177.  * @returN Location of where the pattern was found, 0 otherwise.
  178.  */
  179. static DWORD __stdcall FindPattern(const unsigned char* lpData, unsigned int nDataSize, const unsigned char* lpPattern, const char* pszMask)
  180. {
  181.     for (unsigned int x = 0; x < nDataSize; x++)
  182.         if (MaskCompare(lpData + x, lpPattern, pszMask))
  183.             return (DWORD)(lpData + x);
  184.     return 0;
  185. }
  186.  
  187. /**
  188.  * @brief The main application entry point.
  189.  *
  190.  * @Param argc  The number of arguments passed to this program.
  191.  * @Param argv  The arguments passed to this program.
  192.  *
  193.  * @returN Error code on succss of application.
  194.  */
  195. int __cdecl main(int argc, char* argv[])
  196. {
  197.     std::cout << "===============================================" << std::endl;
  198.     std::cout << "Dino Crisis (PC) Game Timer" << std::endl;
  199.     std::cout << "by atom0s (c) 2014 [atom0s@live.com]" << std::endl;
  200.     std::cout << "===============================================" << std::endl << std::endl;
  201.  
  202.     // Get the process id..
  203.     auto procId = getGameProcessId();
  204.     if (procId == 0)
  205.         return -1;
  206.     std::cout << "[*] Found game process!" << std::endl;
  207.  
  208.     // Get the process base address..
  209.     auto procBase = getGameProcessBaseAddress(procId);
  210.     if (procBase == 0)
  211.         return -1;
  212.     std::cout << "[*] Found game process base!" << std::endl;
  213.  
  214.     // Get the process base size..
  215.     auto procSize = getGameProcessBaseSize(procId);
  216.     if (procSize == 0)
  217.         return -1;
  218.     std::cout << "[*] Obtained process base size!" << std::endl;
  219.  
  220.     // Get the window handle..
  221.     auto procHwnd = getResidentEvilWindowHandle(procId);
  222.  
  223.     // Open the process for reading..
  224.     auto handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_READ, FALSE, procId);
  225.     if (handle == INVALID_HANDLE_VALUE)
  226.         return -1;
  227.     std::cout << "[*] Obtained process handle for memory reading!" << std::endl;
  228.  
  229.     // Dump the process memory..
  230.     auto procDump = new unsigned char[procSize + 1];
  231.     ::ReadProcessMemory(handle, (LPCVOID)procBase, procDump, procSize, NULL);
  232.  
  233.     // Scan for our signature..
  234.     auto timerPtr = FindPattern(procDump, procSize, (BYTE*)"\x8B\x15\xFF\xFF\xFF\xFF\x8D\x84\x0A\xFF\xFF\xFF\xFF\x89\x45\xD4\x8B\x4D\xD4\x8B\x11\x8B\x82\xFF\xFF\xFF\xFF\x89\x45\xE0\x8B\x45\xE0", "xx????xxx????xxxxxxxxxx????xxxxxx");
  235.     if (timerPtr == NULL)
  236.     {
  237.         ::CloseHandle(handle);
  238.         return 0;
  239.     }
  240.  
  241.     // Readjust the offsets..
  242.     timerPtr -= (DWORD)procDump;
  243.     std::cout << "[*] Found signatures for timer offsets!" << std::endl << std::endl;
  244.     std::cout << "[*] Game time should now be rendering on the game window!" << std::endl;
  245.     std::cout << "[*] Hold shift and press escape to close this application!" << std::endl;
  246.  
  247.     // Loop until we want to escape..
  248.     while (true)
  249.     {
  250.         // Allow escape to leave the loop and close the app..
  251.         if ((::GetAsyncKeyState(VK_ESCAPE) & 1) && ::GetAsyncKeyState(VK_SHIFT))
  252.             break;
  253.  
  254.         // Obtain the main timer value..
  255.         DWORD dwTimerValue1 = 0;
  256.         DWORD dwTimerValue2 = 0;
  257.         ::ReadProcessMemory(handle, (LPCVOID)(procBase + timerPtr + 2), &dwTimerValue1, 4, NULL);
  258.         ::ReadProcessMemory(handle, (LPCVOID)(procBase + timerPtr + 23), &dwTimerValue2, 4, NULL);
  259.         ::ReadProcessMemory(handle, (LPCVOID)(dwTimerValue1), &dwTimerValue1, 4, NULL);
  260.         ::ReadProcessMemory(handle, (LPCVOID)(dwTimerValue1), &dwTimerValue1, 4, NULL);
  261.         ::ReadProcessMemory(handle, (LPCVOID)(dwTimerValue1 + dwTimerValue2), &dwTimerValue1, 4, NULL);
  262.  
  263.         // Convert the time into a timestamp..
  264.         auto timer = dwTimerValue1;
  265.         timer /= 60;
  266.  
  267.         auto hour = (int)floor(timer / (60 * 60));
  268.         auto mins = (int)floor(timer / 60 - hour * 60);
  269.         auto secs = (int)floor(timer - (mins + hour * 60) * 60);
  270.  
  271.         char szBuffer[1024] = { 0 };
  272.         sprintf_s(szBuffer, "%02i:%02i:%02i\r\n", hour, mins, secs);
  273.  
  274.         // Get the window dc..
  275.         auto dc = ::GetDC(procHwnd);
  276.  
  277.         // Get the default UI font..
  278.         LOGFONT logfont = { 0 };
  279.         auto font = ::GetStockObject(DEFAULT_GUI_FONT);
  280.         ::GetObject(font, sizeof(LOGFONT), &logfont);
  281.  
  282.         // Adjust the font size..
  283.         logfont.lfHeight = -MulDiv(15, ::GetDeviceCaps(dc, LOGPIXELSY), 72);
  284.  
  285.         // Create the new font..
  286.         font = ::CreateFontIndirect(&logfont);
  287.  
  288.         // Change the dc font..
  289.         auto oldfont = ::SelectObject(dc, font);
  290.  
  291.         // Draw our text..
  292.         RECT rectText = { 5, 5, 0, 0 };
  293.         ::SetTextColor(dc, 0x0000FF);
  294.         ::SetBkMode(dc, TRANSPARENT);
  295.         ::DrawText(dc, szBuffer, strlen(szBuffer), &rectText, DT_CALCRECT);
  296.         ::DrawText(dc, szBuffer, strlen(szBuffer), &rectText, 0);
  297.  
  298.         // Cleanup the font..
  299.         ::SelectObject(dc, oldfont);
  300.         ::DeleteObject(font);
  301.         ::DeleteDC(dc);
  302.  
  303.         ::Sleep(1);
  304.     }
  305.  
  306.     // Cleanup..
  307.     ::CloseHandle(handle);
  308.     return ERROR_SUCCESS;
  309. }
Image
Derp~
Need a great web host? Check out: AnHonestHost.com


Donations can be made via Paypal:
https://www.paypal.me/atom0s
Post Reply

Who is online

Users browsing this forum: No registered users and 1 guest