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

Resident Evil 3 Timer Information

Topics regarding the Resident Evil game series.
Post Reply
User avatar
atom0s
Site Admin
Posts: 397
Joined: Sun Jan 04, 2015 11:23 pm
Location: 127.0.0.1
Contact:

Resident Evil 3 Timer Information

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

About a month or so ago someone had asked for help with finding the timer information for Resident Evil 3 (Also known as BioShock 3 in Japan) for speed runs since it seems to be a popular thing for these games. Since I was bored I decided to help out, this is the result of reversing the game for the timer information.

1. Getting a general idea of time.
The first step was to get an idea of how the game stores time. I used the save games to do this. Standing next to a save spot, I saved once, copied and backed up that save, waited about 10 seconds then saved again and compared the files. Easily, this gave me a good start.

The difference chunk looked like:
  1. D2 5E 01 00 00 00 00 00 01 00 00 00 85 10 A8 B1
  2. 00 00 53 B4 46 00 00 00 02 00 00 03 00 00 04 00
  3. FF FF FF FF FF FF 00 00 00 00 05 00 01 00 03 00
  4. 00 00 00 00 00 00 01 00 00 00 00 00 08 00 00 00
  5. 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  6. 17 00 01 00 14 01 02 00 00 00 4E CE 10 01 00 00
  7. 00 00 C8 00 01 00 BC 02 00 00 00 00 00 00 08 00
  8. 00 00 03 00 00 00 00 00 94 02 90 87 00 80 00 04
The first four bytes appeared to be the timer.
The second row where the 00 02 00 is, that is the save count.

So now we can scan for this when we are in-game. Load up the game, scan for the time in the saved file before loading the game. (Sit at the load menu with the game hovered but not actually in the game.)

I found a few results. Using breakpoints lead me to this function:
  1. 0043AE63 - C7 05 F8C3A700 00000000 - mov [00A7C3F8],00000000
  2. 0043AE6D - EB 27                 - jmp 0043AE96
  3. 0043AE6F - 6A FF                 - push -01
  4. 0043AE71 - E8 AAA50D00           - call 00515420
  5. 0043AE76 - 8B 15 E46DA700        - mov edx,[00A76DE4] : [00001993]
  6. 0043AE7C - 8B C8                 - mov ecx,eax
  7. 0043AE7E - 2B CA                 - sub ecx,edx
  8. 0043AE80 - 8B 15 F8C3A700        - mov edx,[00A7C3F8] : [00018396] ; Time currently in our saved file.
  9. 0043AE86 - 83 C4 04              - add esp,04
  10. 0043AE89 - 03 D1                 - add edx,ecx
  11. 0043AE8B - 89 15 F8C3A700        - mov [00A7C3F8],edx
  12. 0043AE91 - A3 E46DA700           - mov [00A76DE4],eax
The call being:
  1. 00515420 - 8B 0D 44CC5400        - mov ecx,[0054CC44] : [04AE8890]
  2. 00515426 - 8B 54 24 04           - mov edx,[esp+04]
  3. 0051542A - 8B 01                 - mov eax,[ecx]
  4. 0051542C - 52                    - push edx ; our current timer from the saved file...
  5. 0051542D - FF 50 78              - call dword ptr [eax+78]
Following this call takes us to: [[[0054CC44]]+78]
  1. 00518630 - 8B 44 24 04           - mov eax,[esp+04]
  2. 00518634 - 85 C0                 - test eax,eax
  3. 00518636 - 56                    - push esi
  4. 00518637 - 8B F1                 - mov esi,ecx
  5. ...
Reading through this, we will see:
  1. 00518643 - 8B 86 AC050000        - mov eax,[esi+000005AC]
This is using the class pointer (this) +0x5AC which points to the increased time since you last saved.

So from the looks of it:
ResidentEvil3.exe+67C3F8 = The time that was last saved.
[0054CC44]+5AC = The time since you last saved.

2 Analyzing the function call.
Next I looked at the function in IDA/HexView and found it to be:
  1. int __thiscall sub_518630(int this, signed int a2)
  2. {
  3.   int v2; // esi@1
  4.   __int64 v3; // qax@2
  5.   int v4; // eax@4
  6.   int v5; // ecx@5
  7.   int v6; // ecx@8
  8.   int v7; // edi@13
  9.   int v8; // eax@14
  10.   int v9; // ecx@14
  11.   int v10; // edi@17
  12.   int v11; // esi@17
  13.   double v12; // st7@17
  14.   int v13; // edi@17
  15.   int v14; // ecx@18
  16.  
  17.   v2 = this;
  18.   if ( a2 < 0 )
  19.   {
  20.     (*(void (**)(void))(**(_DWORD **)(this + 4) + 16))();
  21.     LODWORD(v3) = *(_DWORD *)(v2 + 1452);
  22.     return v3;
  23.   }
  24.   if ( !a2 )
  25.   {
  26.     v4 = *(_DWORD *)(this + 4);
  27.     if ( v4 )
  28.       v5 = *(_DWORD *)(v4 + 44);
  29.     else
  30.       v5 = 0;
  31.     if ( (unsigned int)(*(int (**)(void))(*(_DWORD *)v5 + 8))() <= *(_DWORD *)(v2 + 1460) )
  32.     {
  33.       (*(void (__thiscall **)(int))(*(_DWORD *)v2 + 176))(v2);
  34.       (*(void (**)(void))(**(_DWORD **)(v2 + 4) + 16))();
  35.     }
  36.     else
  37.     {
  38.       v6 = *(_DWORD *)(v2 + 4);
  39.       ++*(_DWORD *)(v2 + 0x5B8);
  40.       (*(void (**)(void))(*(_DWORD *)v6 + 16))();
  41.     }
  42. LABEL_16:
  43.     *(_DWORD *)(v2 + 0x5B0) = *(_DWORD *)(v2 + 0x5AC);
  44.     goto LABEL_17;
  45.   }
  46.   if ( a2 == 1 )
  47.   {
  48.     (*(void (**)(void))(**(_DWORD **)(this + 4) + 16))();
  49.   }
  50.   else
  51.   {
  52.     if ( a2 > 1 )
  53.     {
  54.       v7 = a2 + *(_DWORD *)(this + 1456);
  55.       if ( *(_DWORD *)(this + 1452) < (unsigned int)v7 )
  56.       {
  57.         do
  58.         {
  59.           (*(void (__thiscall **)(int))(*(_DWORD *)v2 + 176))(v2);
  60.           (*(void (**)(void))(**(_DWORD **)(v2 + 4) + 16))();
  61.         }
  62.         while ( *(_DWORD *)(v2 + 1452) < (unsigned int)v7 );
  63.       }
  64.       else
  65.       {
  66.         v8 = *(_DWORD *)(this + 1464);
  67.         v9 = *(_DWORD *)(this + 4);
  68.         *(_DWORD *)(v2 + 1464) = v8 + 1;
  69.         (*(void (**)(void))(*(_DWORD *)v9 + 16))();
  70.       }
  71.       goto LABEL_16;
  72.     }
  73.   }
  74. LABEL_17:
  75.   v12 = (double)*(unsigned int *)(v2 + 1456);
  76.   v13 = *(_DWORD *)(v2 + 1448);
  77.   v11 = *(_DWORD *)(v2 + 4);
  78.   v10 = v13 - (unsigned __int64)(v12 * -16.66666666666667);
  79.   if ( v11 )
  80.     v14 = *(_DWORD *)(v11 + 44);
  81.   else
  82.     v14 = 0;
  83.   return (unsigned __int64)((double)(unsigned int)((*(int (**)(void))(*(_DWORD *)v14 + 8))() - v10) * 15.78);
  84. }
The important part here for us was:
  1. if ( a2 < 0 )
  2.   {
  3.     (*(void (**)(void))(**(_DWORD **)(this + 4) + 0x10))();
  4.     LODWORD(v3) = *(_DWORD *)(v2 + 0x5AC);
  5.     return v3;
  6.   }
When you save the game, the 2nd param (a2) is always -1, so this first chunk is whats important to us.
So now with that info we have this:
  1. //
  2. // Call the timer function I pasted above..
  3. //
  4. 0043AE6F - 6A FF                 - push -01
  5. 0043AE71 - E8 AAA50D00           - call 00515420
  6.  
  7. //
  8. // Calculate the difference in time..
  9. //
  10. 0043AE76 - 8B 15 E46DA700        - mov edx,[00A76DE4] : [00000593]
  11. 0043AE7C - 8B C8                 - mov ecx,eax
  12. 0043AE7E - 2B CA                 - sub ecx,edx
  13. 0043AE80 - 8B 15 F8C3A700        - mov edx,[00A7C3F8] : [00016F96]
  14. 0043AE86 - 83 C4 04              - add esp,04
  15. 0043AE89 - 03 D1                 - add edx,ecx
  16.  
  17. //
  18. // Store the new time offsets..
  19. //
  20. 0043AE8B - 89 15 F8C3A700        - mov [00A7C3F8],edx
  21. 0043AE91 - A3 E46DA700           - mov [00A76DE4],eax
Which looks like this: (pseudo)
  1. v0 = sub_515420(-1); // Call to the timer function..
  2.       dword_A7C3F8 += v0 - dword_A76DE4;
  3.       dword_A76DE4 = v0;
And in the end we have:
A7C3F8 += [0054CC44]+05AC - A76DE4

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