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