Search This Blog

Thursday 28 July 2016

WinDbg and PC games

I’ve always loved computer games. The first PC I played with was a 286 running on 20MHz in the early ‘90s. It’s interesting to recall how we referred to PCs back then. We used to say things like “I have a 286” or “mine is a 486 DX2”. All that mattered was the CPU. Other parts of the computer were practically irrelevant. Starting a game was a real challenge in some cases in MS-DOS, especially when you needed sound, mouse and cd-rom support. As conventional memory was limited to 640K, you had to carefully choose which drivers to load. Not to mention the tweaks you had to make in autoexec.bat and config.sys files e.g. to force DOS to be loaded to the Upper Memory Block. (DOS=High, UMB - Anyone?)
Then came EMS, XMS, Windows 3.1 (95, 98) and all of a sudden it became much easier to run your games. No more tweaks, yaaaaay!

WinDbg

WinDbg is a powerful debugger and disassembler. You can use it to debug processes or analyze dump files. You can even debug the Windows kernel with it. But what does it have to do with games? Well, it’s a great tool for cheating! :-)
But why cheating? Isn’t it more exciting to beat a game w/o it? Well, most of the time it is. But there are special cases. One of them is when the computer is also cheating to beat you. It happened to me too many times to be pure coincidence.
The game "Need For Speed: Most Wanted" is a good example. It’s a racing simulator from the mid 2000’s and it really pissed me off recently. No matter how accurately you drive, the game sometimes tries to compensate the clumsiness of your opponents and starts moving them with ridiculously high speeds (~900 km/h) on the map. Within moments your hope to win usually falls to pieces. Of course this usually happens in the last lap, so that you have the feeling that you’ve almost won. Very cheeky.
Another pesky situation is when cars in traffic decide to suddenly maneuver to block your way and usually in the most critical situations when you are absolutely not allowed to make any mistakes.
And my "favourite" one: opponents / police cars can actually maneuver while being airborne. Should you try it however, you end up bouncing / rolling / crashing, i.e. losing precious seconds.
Being a programmer I just couldn’t accept this. The odds should be balanced properly. So I decided to do something about it.

The cheat begins

Okay, so the game is about beating the top 15 most wanted street racers in order to be nr #1. In order to challenge one, you need to collect a certain amount of bounty. However, this can be very difficult to achieve because of the abovementioned reasons. So I started off with opening the appropriate save game file with a hex editor. You should find it in %USERPROFILE%\Documents\NFS Most Wanted.
You can have several cars in the game. Each has a bounty on its own. I had 3 cars, so I chose the one with the most unique bounty value, which was 3051800 (0x2E9118). Uniqueness is very important, as it makes it easier to find the right value that needs to be changed. I was lucky as there was only a single occurrence of this value in the save game file.
Note: 0x2E9118 is a 32-bit value. In order to find it, you’ll have to play a bit with the order of the hex digit pairs. 0x2E9118 = 0x00 2E 91 18 –> 18 91 2E 00 <—this is how you can find it in your favourite hex editor. The reason behind it is that multi-byte values are stored in memory differently than on disk. For 16-bit values, like 0xABCD = 0xAB CD –> CD AB should be used. This rule does not apply to searching for a value in memory.
Warning: always backup your files before editing. You don't wanna lose your progress in the game if something goes wrong, do you?
I found the value so I changed it to 4000000, i.e. from 18 91 2E 00  --> 00 09 3D 00. Saved the file, started the game, loaded my profile and….BAAANG!!! "Your profile appears to be damaged and cannot be used". Great. It seems the save game contains some kind of hash (like a sha-1 message digest) that should be updated to reflect the changes I've made. A typical protection from that era. Unfortunately, I had no idea what hash algorithm was used and on what input data. And I needed a quick solution. Conclusion: this approach was a dead end.
Hm… now what?
We need a different approach. How about trying to edit this value in memory after loading the original save game? This is where WinDbg comes into the picture. I restored the save game from the backup, loaded my game, started the 32-bit version of WinDbg - as the game was also 32-bit - and attached to speed.exe.
So how to find 0x2E9118 in memory? As it is a 32-bit process, the full address space of it can be scanned through very quickly.

0:000> !address -c:"s -d %1 %2 2E9118"

This command iterates over all memory regions of the virtual address space of the process and scans for the value in them. %1 and %2 are placeholders that are replaced with the base address and end address+1 of the actual memory region.
Note: for an x64 process you might want to use some filters to search only the heap regions for instance. That would be:

0:000> !address –f:Heap -c:"s -d %1 %2 2E9118"

For me the output was:

03430274  002e9118 0000000e 00020000 000b0000  ................

The address in bold is the one we need, but let's verify it once again with the display memory command (dd):

0:000> dd 03430274  L1
03430274  002e9118

The dd command displays 32-bit values starting at the address we provide it with.
Now let's convert 4000000 to hexadecimal:

0:000> ? 0n4000000
Evaluate expression: 4000000 = 003d0900

Okay, now we know the memory address to change and the desired value. So let's run the edit memory command (ed):

0:000> ed 03430274  003d0900

Verify our success:

0:000> dd 03430274  L1
03430274  003d0900

And that's it! You can now safely detach from the process and save your game to persist the changes. There's only one thing left: to enjoy the results. Who's smarter now, Mr. Game? ;)

The story continues

A few days later, the game challenged me once again. In order to race against the top 5 black list members, you need to pass several criteria, one of which is about hitting a certain number of milestones. Milestones are usually things like escaping from police chases, evading roadblocks and causing trouble in the public. The longer the chase the more bounty you get. Collecting several hundreds of thousands of bounty can be quite a challenge. It can be very annoying when you get busted by the police after a looong chase. So why not start a chase and give ourselves a nice large bounty ASAP?
Beware game, WinDbg is coming to aid me again. :)
Sooo, I ran the game, loaded my profile and started a chase. It's very easy to avoid getting busted for 1-2 minutes, so I did that to make sure my bounty was unique enough to be sought in memory.
Then I switched to WinDbg, pressed Ctrl-Break to break into the debugger and tried to find the value (4000):

0:000> ? 0n4000
Evaluate expression: 4000 = 00000fa0
0:000> !address -c:"s -d %1 %2 00000fa0"
00010228  00000fa0 00000000 00000000 00000000  ................
0023014c  00000fa0 037c0048 00000080 00000001  ....H.|.........
0028014c  00000fa0 00000000 00000080 00000001  ................
002f0228  00000fa0 00000000 00000000 00000000  ................
002fc498  00000fa0 00000000 0000000a 00000008  ................
<stripped ~340 occurrences for brevity>

Whoaaa! A whole bunch of occurrences. I had to resort to a neat trick. I resumed the game and escaped for another 10 seconds, i.e. until the bounty value changed. The new value was 4500.

0:000> !address -c:"s -d %1 %2 0n4500"
003064e8  00001194 00000000 00000008 00000008  ................
006289e0  00001194 283daa74 74000023 34bc3da3  ....t.=(#..t.=.4
00dfa3cc  00001194 9ee9006a 3b000000 0000888e  ....j......;....
00e10110  00001194 a3e85653 8300013e 50e910c4  ....SV..>......P
0120a298  00001194 00000000 00000000 40005008  .............P.@...
<stripped ~80 occurrences for brevity>

The intersection of the sets created from the 1st columns of these outputs gives us a set with a single memory address: 0bed3ba4. So let’s boost our bounty to 65536 for testing purposes:

0:000> ed 0bed3ba4  10000
0:000> g

Testing, and…. it doesn’t work. Hmm… is the value we set still intact?

0:000> dd 0bed3ba4  L1
0bed3ba4  00001194

What?! Something altered my value. Dang! Let’s create a breakpoint that fires when someone tries to write the memory address in question.

0:000> ed 0bed3ba4  10000
0:000> ba w4 0bed3ba4
0:000> g

Very soon, I got this:

Breakpoint 0 hit
eax=00001194 ebx=02d44f48 ecx=0bed3b68 edx=00892988 esi=0bed3b68 edi=008a2428
eip=00568eac esp=0018fda0 ebp=0bf375f8 iopl=0         nv up ei pl nz ac pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00200216
speed+0x168eac:

Great, now let’s check the surrounding assembly code (near cs:Eip) by leveraging the "u" (disassemble) command.

0:000> u eip-0x30 L16
speed+0x168e7c:
00568e7c cc              int     3
00568e7d cc              int     3
00568e7e cc              int     3
00568e7f cc              int     3
00568e80 8b442404        mov     eax,dword ptr [esp+4]
00568e84 394134          cmp     dword ptr [ecx+34h],eax
00568e87 7403            je      speed+0x168e8c (00568e8c)
00568e89 894134          mov     dword ptr [ecx+34h],eax
00568e8c c20400          ret     4
00568e8f cc              int     3
00568e90 8a442404        mov     al,byte ptr [esp+4]
00568e94 384138          cmp     byte ptr [ecx+38h],al
00568e97 7403            je      speed+0x168e9c (00568e9c)
00568e99 884138          mov     byte ptr [ecx+38h],al
00568e9c c20400          ret     4
00568e9f cc              int     3 
00568ea0 8b442404        mov     eax,dword ptr [esp+4]
00568ea4 39413c          cmp     dword ptr [ecx+3Ch],eax 
00568ea7 7403            je      speed+0x168eac (00568eac)
00568ea9 89413c          mov     dword ptr [ecx+3Ch],eax
00568eac c20400          ret     4
00568eaf cc              int     3

I have to admit, you’ll need some assembly language knowledge here. The code in red tells me, that the top of stack+4 contains a value that is compared with the contents of our memory address and rewrites it if necessary. Ooookay, there are several options to circumvent this, so let’s choose one: find the code that pushes the reference value to the stack. The "gu" command runs the code until the next return statement is executed.

0:000> gu
eax=00001194 ebx=02d44f48 ecx=0bed3b68 edx=00892988 esi=0bed3b68 edi=008a2428
eip=006f19c4 esp=0018fda8 ebp=0bf375f8 iopl=0         nv up ei pl nz ac pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00200216
speed+0x2f19c4:
006f19c4 8b6c2418        mov     ebp,dword ptr [esp+18h] ss:002b:0018fdc0=0bf375f8

Let’s check around cs:Eip again:

0:000> u eip-0x30 L16
speed+0x2f1994:
006f1994 57              push    edi
006f1995 088b44242c8b    or      byte ptr [ebx-74D3DBBCh],cl
006f199b 16              push    ss
006f199c 50              push    eax
006f199d 8bce            mov     ecx,esi
006f199f ff520c          call    dword ptr [edx+0Ch]
006f19a2 8b5500          mov     edx,dword ptr [ebp]
006f19a5 8b3e            mov     edi,dword ptr [esi]
006f19a7 8bcd            mov     ecx,ebp
006f19a9 ff5244          call    dword ptr [edx+44h]
006f19ac 8b5500          mov     edx,dword ptr [ebp]
006f19af 8bcd            mov     ecx,ebp
006f19b1 89442440        mov     dword ptr [esp+40h],eax
006f19b5 ff5240          call    dword ptr [edx+40h]
006f19b8 8b4c2440        mov     ecx,dword ptr [esp+40h]
006f19bc 03c8            add     ecx,eax
006f19be 51              push    ecx
006f19bf 8bce            mov     ecx,esi
006f19c1 ff573c          call    dword ptr [edi+3Ch]
006f19c4 8b6c2418        mov     ebp,dword ptr [esp+18h]
006f19c8 8b4c2414        mov     ecx,dword ptr [esp+14h]
006f19cc 8b4974          mov     ecx,dword ptr [ecx+74h]

Ahha! The code in red pushes the reference value to the stack. So why not create a breakpoint for the address of the push statement (006f19be) and alter the Ecx register beforehand?

0:000> bd *
0:000> bp 006f19be "r ecx=0x00010000;g;"

The "bd *" command disables all previous breakpoints. The "bp" command sets our new breakpoint and a command that runs automatically when it is hit.

And that was it. This hack resulted in a bounty value of 65536 (while in the chase), the only thing left was to successfully escape from the cops. Pretty neat, huh?

No comments:

Post a Comment