The Lost Mind of Dr. Brain

Background
Recently, there was a need to fire up the vintage Inspiron 7000 to use Adobe Reader. While rocking Win2k on the ol’ Pentium II (400 MHz!), I realized it had been quite some time since The Lost Mind of Dr. Brain had been played. Dr. Brain is a puzzle game that saw fairly frequent play time during ’95 and ’96; that is, until the N64 (with the likes of Golden Eye) staged a hostile takeover. A couple of years ago, both nostalgia and the need for a decent puzzle game led to a well spent $10 on eBay and a few weeks of intense Dr. Brain activity. Back to the present… the old laptop was out, powered on, and working. Having just suffered through the Adobe Reader upgrade process, it was time to indulge in a little Dr. Brain time. Apparently my copy was thrown out of an airplane window, as it was nowhere to be found. Searches for an ‘alternative source’ of the software turned out to be successful, that is, until the software was executed:

Dr. Brain Error Dialog

Google didn’t provide anything all that helpful, which isn’t all that surprising… Dr. Brain wasn’t exactly popular in ’95, let alone now. All hope was not lost though, Sierra was helpful enough to include a sound card test which passed. There might be a chance that the check in Dr. Brain itself was bogus. If only it could be skipped…

The search for The String

Not expecting success, everything was half-heartedly moved over to a Linux box to make use of familiar tools. The first action taken was searching for part of the error text using good ol’ grep:

jason@marathon:~/projects/dr-brain/The Lost Mind Of Dr. Brain$ grep "11 kHz" DRBRAIN3.EXE
Binary file DRBRAIN3.EXE matches

So far so good… next was a combination of xxd and less. After fiddling with the search a bit, the string was found in the middle of a bunch of error messages:


0012d80: 7461 6c6c 6564 2073 6f75 6e64 2064 7269 talled sound dri
0012d90: 7665 722e 0000 2573 2072 6571 7569 7265 ver...%s require
0012da0: 7320 6120 736f 756e 6420 6472 6976 6572 s a sound driver
0012db0: 2074 6861 7420 616c 6c6f 7773 2066 6f72 that allows for
0012dc0: 2061 7379 6e63 6872 6f6e 6f75 7320 706c asynchronous pl
0012dd0: 6179 6261 636b 2e00 2573 2072 6571 7569 ayback..%s requi
0012de0: 7265 7320 6120 736f 756e 6420 6361 7264 res a sound card
0012df0: 2074 6861 7420 7375 7070 6f72 7473 2031 that supports 1
0012e00: 3120 6b48 7a20 6f72 2062 6574 7465 7220 1 kHz or better
0012e10: 706c 6179 6261 636b 2072 6174 6573 2e00 playback rates..
0012e20: 2573 2072 6571 7569 7265 7320 6120 7363 %s requires a sc
0012e30: 7265 656e 2072 6573 6f6c 7574 696f 6e20 reen resolution
0012e40: 6f66 2036 3430 7834 3830 206f 7220 6c61 of 640x480 or la
0012e50: 7267 6572 2e00 2573 2072 6571 7569 7265 rger..%s require
0012e60: 7320 7375 7070 6f72 7420 666f 7220 3235 s support for 25
0012e70: 3620 636f 6c6f 7273 2e00 416c 7468 6f75 6 colors..Althou

Taking apart the executable

At this point, I thought it might actually be possible to skip over the problematic check. All that I need to do is find where the error string is referenced in the executable and disable the part that throws up the error dialog. The easiest way to do this would be to look at a disassembly of the executable. Objdump can disassemble some binary formats, but it was not much help here:

jason@marathon:~/projects/dr-brain/The Lost Mind Of Dr. Brain$ objdump -a DRBRAIN3.EXE
objdump: DRBRAIN3.EXE: File format not recognized

Fortunately, the file command gave a bit of direction:

jason@marathon:~/projects/dr-brain/The Lost Mind Of Dr. Brain$ file DRBRAIN3.EXE
DRBRAIN3.EXE: MS-DOS executable, NE for MS Windows 3.x

Here, Google and Wikipedia were most helpful, leading me to the file format as described by MicroSoft. After digging through the document, it seemed like the segment information was probably the place to start looking. Windows 3.1 appeared to use a segmented memory model, where addresses were specified using a 16-bit offset into a segment identified by a 16-bit address. The NE file format has the executable divided into chunks at most 64 kb in size (i.e. the max a 16-bit offset can reference). So, if the segment in the executable that contains the string can be located, references to it in the executable can be found. A quick and dirty parser for the NE format yielded what I was looking for:


SEGMENT NUMBER: 2
Logical sector offset to segment: 0x5be (0x5be0 bytes)
Segment length: 61095 (0xeea7)
Flags: CODE MOVABLE PRELOAD RELOCINFO
Minimum allocation size: 61095 (0xeea7)

Segment 2 starts at offset 0x5be0 in the executable and is 61095 bytes long which means it ends at offset 0x14a87, and therefore contains the error string (which is at 0x12dd8). The segment is extracted using a little bash and dd magic:

jason@marathon:~/projects/dr-brain/The Lost Mind Of Dr. Brain$ dd if=DRBRAIN3.EXE of=segment2 bs=1 skip=$((0x5be0)) count=61095
61095+0 records in
61095+0 records out
61095 bytes (61 kB) copied, 0.469996 s, 130 kB/s

Finding the code

Since the segment contains code, it seemed like a reasonable place to start looking for references to the string. The first step was to disassemble the segment. The Netwide Disassembler, ndisasm, was employed as it’s a bit more flexible than objdump (i.e. it doesn’t really care about the format, it’d disassemble your letter to grandma if you wanted). Grep was then used to try to find references to the string, which is at offset 0xd1f8 from the beginning of the segment (0x12dd8 – 0x5be0):

jason@marathon:~/projects/dr-brain/The Lost Mind Of Dr. Brain$ ndisasm -b 16 segment2 > segment2.s
jason@marathon:~/projects/dr-brain/The Lost Mind Of Dr. Brain$ grep -i "d1f8" segment2.s
0000CF1C  68F8D1            push word 0xd1f8
0000D191  7265              jc 0xd1f8

The push word at 0xcf1c looks especially promising; perhaps the string is being pushed onto the stack as a parameter to a call? Here’s a bit of the surrounding disassembly:

0000CECF  0BC0              or ax,ax
0000CED1  751A              jnz 0xceed
0000CED3  33F6              xor si,si
0000CED5  FF7608            push word [bp+0x8]
0000CED8  57                push di
0000CED9  68FACE            push word 0xcefa
0000CEDC  688ED1            push word 0xd18e
0000CEDF  8D86D2FB          lea ax,[bp-0x42e]
0000CEE3  16                push ss
0000CEE4  50                push ax
0000CEE5  9A06CF0000        call word 0x0:0xcf06
0000CEEA  83C40C            add sp,byte +0xc
0000CEED  F646E610          test byte [bp-0x1a],0x10
0000CEF1  741A              jz 0xcf0d
0000CEF3  33F6              xor si,si
0000CEF5  FF7608            push word [bp+0x8]
0000CEF8  57                push di
0000CEF9  681ACF            push word 0xcf1a
0000CEFC  68B6D1            push word 0xd1b6
0000CEFF  8D86D2FB          lea ax,[bp-0x42e]
0000CF03  16                push ss
0000CF04  50                push ax
0000CF05  9A26CF0000        call word 0x0:0xcf26
0000CF0A  83C40C            add sp,byte +0xc
0000CF0D  F646E001          test byte [bp-0x20],0x1
0000CF11  751A              jnz 0xcf2d
0000CF13  33F6              xor si,si
0000CF15  FF7608            push word [bp+0x8]
0000CF18  57                push di
0000CF19  685ACF            push word 0xcf5a
0000CF1C  68F8D1            push word 0xd1f8
0000CF1F  8D86D2FB          lea ax,[bp-0x42e]
0000CF23  16                push ss
0000CF24  50                push ax
0000CF25  9A66CF0000        call word 0x0:0xcf66
0000CF2A  83C40C            add sp,byte +0xc
0000CF2D  6A00              push byte +0x0
0000CF2F  9AA02E0000        call word 0x0:0x2ea0

There appears to be a series of tests: the conditional jumps at 0xced1, 0xcef1, 0xcf11 all jump over calls to the same routine at 0xcee5, 0xcf05, and 0xcf25 respectively. Each routine call is passed via the stack one of the error strings, which makes it highly likely that routine is responsible for handling error conditions (i.e. displaying the error dialog and quitting the program). This pattern continues past the end of the above listing.

(You may have noticed that the above calls have different addresses which would seem to indicate that they are calling different routines. However, what’s going on here (I believe) is those addresses are really forming a linked list used by a runtime linker to insert the actual runtime address of that routine. Each address points to the next place that needs to be updated. At 0xcee5 the address is 0xcf06, which is the address used by the call at 0xcf05. At 0xcf05 the address is 0xcf26 which is the address used by the call at 0xcf25, etc.)

Skipping the test

Now it’s simply a matter of having the code believe that the test passed regardless of what actually happened. Changing the conditional jump-if-not-zero (JNZ) at 0xcf11 to an unconditional JMP should get the job done. Consulting the Intel x86 documentation (Vol. 2A), the JNZ is using opcode 0x75, which is the 8-bit immediate offset form. The equivalent JMP has an opcode of 0xEB.

Using tweak, a binary/hex editor, the executable itself is edited with the updated opcode. The offset to update with the new opcode is 0x12af1. (0x5be0 + 0xcf11, i.e. segment offset + JNZ offset)

Success!

After making the ONE BYTE CHANGE, the updated executable is copied over to the old laptop. With fingers appropriately crossed, the updated executable is run. In place of the error dialog is the Sierra startup logo with audio! This is the most fun I’ve had in front of a computer in quite some time. Actually playing Dr. Brain afterwards was not nearly as interesting. Oh well.

9 Comments

  1. Kendall
    Posted August 11, 2009 at 9:33 am | Permalink

    For being borderline edutainment Dr. Brain has some addictive puzzles. Though I think the whole thing could be packaged up as a flash game at this point. Good enough for an afternoon of slacking for sure! Also this comment will maybe bring the comments feed to life?

  2. jachrispens
    Posted August 11, 2009 at 9:43 am | Permalink

    The comment feed lives! Also, comment moderation is heading west on Annoying BLVD. Time to see about turning that off until it becomes a problem (if ever).

  3. jachrispens
    Posted August 11, 2009 at 9:49 am | Permalink

    Indeed… definitely would be a dozen different flash games today.

  4. htimsnivek
    Posted August 11, 2009 at 12:17 pm | Permalink

    As always Jason kicks some ass. Now that you have that figured out are you going to move on to the world of n0 cd cr@ck$.

    I would have given up at the sound card error. Way to be.

  5. htimsnivek
    Posted August 11, 2009 at 12:18 pm | Permalink

    Am I the only one to laugh at the idea of this being posted to a blog entitled “not so clever”?

  6. jachrispens
    Posted August 11, 2009 at 12:51 pm | Permalink

    Normally I think I would’ve given up also. For whatever reason I was annoyed into action by this not working when I know it had in the past.

    The game studios haven’t given up on the CD-in-the-drive checks yet? 🙂

    The ‘no-so-clever’ title is intended to be slightly tongue in cheek, but only slightly. 🙂

  7. Kevin Larsen
    Posted August 13, 2009 at 10:21 pm | Permalink

    Oh Jason…. I just can’t help but think that the world could be using this attention to detail, desire to crack code to do pretty awesome stuff (not that cracking Dr. Brain isn’t freaking cool) perhaps you could write a program to teach congress to spend only what they have in their bank, or maybe optimize the power grid or something…

    So I guess the question is how do we make the worlds problems fun to solve? Perhaps an app entitled World Vexations 2.0?

    I look forward to more info packed blogging.

  8. Kendall
    Posted August 13, 2009 at 11:34 pm | Permalink

    “ERROR 11 – to display the solution for world hunger your display must be set to 256 colors.”

  9. htimsnivek
    Posted August 14, 2009 at 1:31 am | Permalink

    Thanks for the late night laugh Kenny.

Post a Comment

Your email is never shared.