My kids and I are playing through the Quest For Glory games. I’m pretty sure QFG qualifies as my third parent – I spent a lot of time playing these games when I was a kid! I remember all the nooks and crannies well enough, my kids come up and ask random questions and I can rattle off the answers. My hope is that they grow up just like I did (because I turned out mostly alright, didn’t I?).

I always like to max out my stats before moving on to the next game. You can get up to n * 100 in each skill for game n. I’m not perfect at it – some skills are harder to max out than others (I’m looking at you luck!). But while grinding in QFG3, the boys and I started talking about hacking it, and one thing lead to another…

You can see in the screenshot below that my skills are doing alright. Not great – I have to work a bit on the key paladin skills (strength, agility, vitality, etc.). Some of these will never improve, though, because there isn’t a good place in the game to grind them. What to do?

Before

The question arises, though: what is the quick way? My first thought was to edit the save file. I found a couple fragments online suggesting that the save files might just be tractable binary files. Maybe I can find the stats in there, change them with a hex editor, and be done. But after looking through it, it seems that the save files are pretty complex (not like the character-import files between games, which is another animal!):

Hexdump save file

After looking through for obvious values that match my stats, I can’t find them. I could maybe reverse engineer the game, find the save/load logic, and piece my way through it. But shouldn’t there be a quicker way? After ruminating, it occurred to me that the stats are probably stored in memory somewhere. I’d imagine they’d look something like this:

struct player {
  int strength;
  int intelligence;
  ...
};

Or maybe this:

#define STRENGTH    0
#define INTELLIGENCE 1
...

int player[NUM_STATS];

As long as Sierra didn’t decide to put the stats in some crazy linked list or tree, scattered all over the heap, it might not be too hard to find the stats in memory. Another google search found me this poor soul, who doesn’t know what endianness is, but suggests that the game uses a short to represent the stats, and not an int. That makes sense – it would take a lot of sequels to breach the 65,535 skill barrier!

That means, if I’m lucky, my skills should look something like this in memory:

:) josh@atlantis $ pry
[1] pry(main)> [230, 170, 250].pack("s*").unpack("H*")
=> ["e600aa00fa00"]
[2] pry(main)> 

For a quick check, I’ll just make a core dump and see if I find that byte sequence somewhere in memory. First, find the target process:

:) josh@atlantis $ ps -ef | grep dosbox
josh      7082 25328  2 Jan01 pts/5    00:21:37 dosbox
josh     16718 28889 50 09:25 pts/6    00:07:00 dosbox
josh     17693 17443  0 09:39 pts/8    00:00:00 grep dosbox

Now, attach with gdb, and make a core dump:

:) josh@atlantis $ gdb -p 16718
GNU gdb (GDB) Fedora 7.11.1-86.fc24
Copyright (C) 2016 Free Software Foundation, Inc.
. . .
Attaching to process 16718
[New LWP 16719]
[New LWP 16721]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
0x000055631860a0d9 in CPU_Core_Normal_Run() ()
Missing separate debuginfos, use: dnf debuginfo-install dosbox-0.74-18.fc24.x86_64
(gdb) generate-core-file 
Saved corefile core.16718
(gdb)

I found a handy tool for binary grepping (bgrep) on github, and that makes quick work of the core dump. Things look pretty promising – there seem to be two places where I can see the strength, intelligence, and agility stats consecutive in memory (it could have been harder… what if they were stored in another order?):

:) josh@atlantis $ tools/bgrep/bgrep e600aa00fa00 core.16718 
core.16718:0x3AD0E6E
core.16718:0x3AD0EBE
:) josh@atlantis $

And indeed, when I check with a hexdump, I see what definitely looks like the character stats. Interestingly, the stat order is a little different from shown in the game, but the first few are all in order as shown in the left column on the character screen.

View in memory dump

So this is what I need to change, but the offset in the core file isn’t very useful. There are a couple ways I could go about this – try to destructure the core file and find the address of these bytes, or hop into the process with a debugger and find the data in memory (slower, but easy since I know for sure that the data is there). I chose the second option arbitrarily.

I hop into the process with radare2, and end up in some place where DOSBox is emulating the program. Who knows where I am, but I don’t really care:

:) josh@atlantis $ r2 -d 16718
= attach 16718 16718
bin.baddr 0x5563185c6000
Using 0x5563185c6000
asm.bits 64
 -- I love gradients.
[0x7fbd3c97e49d]>

From here, I need to find the space in memory where QFG3 is running. I checked htop, and noted that DOSBox has allocated something like 590MB of space (not all resident, but memory searches might try it all…), so I want to be clever about this. There’s a command in r2 for listing the memory maps:

[0x7fbd3c97e49d]> dm | head -n 20
[0x7fbd3c97e49d]> 0x0000000041996000 - 0x0000000041998000 - sys     8K s . . .
0x00000000419aa000 - 0x0000000041a4b000 - usr   644K s rw- unk0 unk0 ; m . . .
0x00005563185c6000 - 0x000055631882c000 - usr   2.4M s r-x /usr/bin/dosb . . .
0x0000556318a2b000 - 0x0000556318a31000 - usr    24K s r-- /usr/bin/dosb . . .
0x0000556318a31000 - 0x0000556318a5b000 - usr   168K s rw- /usr/bin/dosb . . .
0x0000556318a5b000 - 0x000055631b878000 - usr  46.1M s rw- unk1 unk1 ; m . . .
0x000055631cef5000 - 0x000055631daaf000 - usr  11.7M s rw- [heap] [heap] . . .
0x00007fbd1a7f6000 - 0x00007fbd1e7f7000 - sys    64M s rw- /dev/shm/puls . . .
0x00007fbd1e7fe000 - 0x00007fbd227ff000 - sys    64M s rw- /dev/shm/puls . . .
0x00007fbd227ff000 - 0x00007fbd24000000 - usr    24M s rw- unk2 unk2 ; m . . .
0x00007fbd24000000 - 0x00007fbd24021000 - usr   132K s rw- unk3 unk3 ; m . . .
0x00007fbd24021000 - 0x00007fbd28000000 - usr  63.9M s --- unk4 unk4 ; m . . .
0x00007fbd2893e000 - 0x00007fbd28d3e000 - sys     4M s rw- /dev/nvidiact . . .
0x00007fbd28d3e000 - 0x00007fbd2913e000 - sys     4M s rw- /dev/nvidiact . . .
0x00007fbd2913e000 - 0x00007fbd2953e000 - sys     4M s rw- /dev/nvidiact . . .
0x00007fbd29545000 - 0x00007fbd29546000 - usr     4K s --- unk5 unk5 ; m . . .
0x00007fbd29546000 - 0x00007fbd29d46000 - usr     8M s rw- unk6 unk6 ; m . . .
0x00007fbd29fb3000 - 0x00007fbd2a1e6000 - usr   2.2M s rw- unk7 unk7 ; m . . .
0x00007fbd2a1e6000 - 0x00007fbd2a3e6000 - sys     2M s rw- /dev/nvidiact . . .
0x00007fbd2a3e6000 - 0x00007fbd2a7e6000 - sys     4M s rw- /dev/nvidiact . . .

It might not be immediately obvious, but one of those memory maps stands out to me right away. I configured DOSBox to emulate a machine with 24MB of RAM (that’s how I rolled in the 90s), and one memory map is exactly that size:

. . . 
0x00007fbd1e7fe000 - 0x00007fbd227ff000 - sys    64M s rw- /dev/shm/puls . . .
0x00007fbd227ff000 - 0x00007fbd24000000 - usr    24M s rw- unk2 unk2 ; m . . .
0x00007fbd24000000 - 0x00007fbd24021000 - usr   132K s rw- unk3 unk3 ; m . . .
. . . 

So I’m guessing that’s where it’s going to be. In r2, an easy way to do this is just to seek to that location and search in the current map for a byte string. Something like this:

[0x7fbd3c97e49d]> s 0x00007fbd227ff000
[0x7fbd227ff000]> e search.in=dbg.map
[0x7fbd227ff000]> /x e600aa00fa00
Searching 6 bytes in [0x7fbd227ff000-0x7fbd24000000]
hits: 2
0x7fbd2282094a hit0_0 e600aa00fa00
0x7fbd2282099a hit0_1 e600aa00fa00
[0x7fbd227ff000]> 

And once again, there seem to be two copies. Spoiler alert, it’s the second one that I want (the first one seems to be a second copy, maybe used to compare against the current copy so that the character screen can highlight the ones that have changed in red?). I’ll seek there, and hop to visual mode and take a look:

Found in memory

Yeah, that’s it. All my stats, in their 16-bit integer glory, exactly what I’m questing for! We can use the r2 command wx to write some bytes to this space. There are 15 stats I want to change, and I want each to be 300, which comes out to 2c01, hex. Something like this should work:

:> wx 2c012c012c012c012c012c012c012c012c012c012c012c012c012c012c01

Et voila, it’s updated in memory:

Changed in memory

A quick q to exit visual mode, and dc to continue execution of DOSBox, and I can check to see if it’s updated…

Hacked character

Conclusions

This was a fun little excursion. It’s also a poignant reminder that my kids (10 and 12 year-old boys) don’t really appreciate that their father is a hacker until he does something tangible. By hacking stats in an RPG that they are currently struggling through, they can feel the power that comes with knowledge and control of the computer. One of these days, I’ll make hackers out of them!

Another note, I’m trying to get into radare2. I had avoided it for years because it seemed so hard to use. But once I realized it’s basically the vi of debuggers, it’s grown on me. I did try to experiment with using gdb to do this instead, but it turned out much less suited to this kind of activity.

Finally, if you’re a fan of the game – yes, my Paladin had magic and climbing skills (I usually add those in QFG3 on import). And also, yes, I added thief skills in this hack. Never know when those might come in handy. Makes me wonder in QFG4, if I use my thief skills, will it decimate my honor and paladin points? We’ll see!