Multiplan for Xenix 3.0 on the Lisa solved.

From: Ray Arachelian <ray_at_email.domain.hidden>
Date: Tue, 7 Mar 2017 21:36:17 -0500

This is based on the multiplan.dc42 disk image up on bitsavers, if there are others floating out there, it might not work.


To patch the serial number check, run this from bash/zsh, on Linux, *BSD, or Mac OS X, cygwin, etc:

  cp multiplan.dc42 multiplan-hax.dc42

  (echo 102; echo 'editsector 0x184 70 01') | lisafsh-tool multiplan-hax.dc42

alternatively, you can change the 2nd line to do this instead:

  (echo 102; echo 'editsector 0x184 70 02') | lisafsh-tool multiplan-hax.dc42

Then write multiplan-hax.dc42 image to a physical disk and insert it into the machine, and then, as root on Lisa Xenix 3.0, run:

  cd /

  tar xpvf /dev/fd

  cd /once; rm brand

To start multiplan just type in mp and hit enter.

I'm not sure what the difference is between 01 and 02. The serial # check routine comes back with either 0 for not registered, or 1 or 2.

Perhaps 1 or 2 are different somehow - maybe one of them allows for more users or unlocks extra features. Maybe one of them indicates a serial number from an older version that's accepted for upgrades, maybe one unlocks "god mode" etc. Not sure at this time, if you guys experiment and find out the difference, pls reply in this thread.

      About MultiPlan:

Multiplan was a spreadsheet that Microsoft written many years ago by Douglas Klunder (or perhaps just the p-code interpreter?) during the Charles Simonyi era, and ported to many systems such as CP/M, the C64, the TRS/80, the Mac and since they wrote Xenix, of course they made a port to the Xenix on the Lisa.

Since there was such a huge plethora of machines in the 80s with all sorts of unique architectures and CPUs, it was difficult to port code from one system to another, so many companies, Microsoft included, used what today would be called a virtual machine (in the JVM sense), but back then it was called a p-code machine, most famously after the UCSD Pascal System. To port a whole collection of software from one machine to the other just required you to rewrite the p-code interpreter.

(Microsoft said that the "p" stood for "packed" rather than Pascal)


      Details of the crack for those interested:

Like most Xenix software, Multiplan comes on a floppy, but in tar format. So you don't get to mount the floppy, but rather just do tar xvf /dev/fd to extract it. Somewhere in /etc there is an installer script that does this for you. Software distributed this way also dumps a few install scripts inside the/once directory, which as its name implies is intended to run once.

In this case there is a binary executable called /once/brand, and a shell script called/once/ and a binarymkpath (which makes paths)

brand -s serial path-to-binary runs some checks on the provided serial number, then looks for a string starting with SCOSN inside the binaryto "brand" it with a serial number, is a bit annoying, it's a wrapper around brand, but if you enter the wrong serial # it will then uninstall all of multiplan making you start over, which is of course really slow on a Lisa. If successful, it does offer the useful feature of letting you move /usr/lib/mp to another disk, which if you're on a 5MB ProFile, would be good.

brand also takes some other command line options like-q for quiet (which invokes it with) but also-m and -n which probably tell you where the identify string lives on a serialized copy of the binary, but running against the unserialized one, it says "brand: identifier string found at D in Memory fault - core dumped" and of course you get a core. Against/dev/null, it spits out "brand: identify string not found in /dev/null"

The target of brand is /usr/lib/mp/inter - so why inter? and how does inter run? There is a script called mp that sets some variables and then runs /usr/lib/mp/inter /usr/lib/mp. Remember the p-code interpreter in the history section above? That's what that guy is!

The string it looks for starts with SCOSN - this is found inside brand 4 times (when using the strings command) (why?) It is found in/usr/lib/mp/inter just once.
Here's what strings on brand looks like:

%s: serial number checkdigits are not valid
%s: cannot open %s
%s: cannot create stream for %s
%s: location set with multiple flag
%s: location %D is incorrect for %s
%s: identify string not found in %s
%s:ident string multiply found in %s
%s: identifier string found at %D in %s
%s: serial number is wrong length in %s
%s: usage: %s [-s serialnum][-l location][-n][-m] files

             *(((((                  H*

The interesting thing about inter is that it's not a generic p-code interpreter, well, it might be, but it has specific multiplan related strings such as a product ID. If you run it, it wants a path to a pair of files, one ending in .cod and the other in .dat. The serial number check in inter is wholly in inter, and not the interpreted code. I know this because I touched */usr/lib/inter/mp/foo.cod* and passed it as a parameter to inter. It then complained about/usr/lib/inter/mp/foo.dat missing (so it needs both), which I then touched, and again it complained about an invalid serial number, but it does print a serial number of NUL-000000 Ok, good stuff.

Now, the target string inside of inter is this guy: *SCOSNMFO9999990000*

Ok, that's kinda weird, because somehow inter says it's NUL-000000. So what if we modify inter and overwrite the strings. Say maybe we put in SCOSNAAA000000. Well now, inter spits out the serial number is invalid, and it's ZZZ-999999. Interesting. So if you swap A for Z and B for Y and continue like that, MFO (that's not a zero) is NUL. Ok, another clue. And if you now try other numbers you see that 0 becomes 9 and 1 becomes 8, etc. Nice obfuscation there. :)

But the complaint is in the "check digits" whatever those are. Jason tried to brute force them without luck. Those are the extra 4 zeros at the end of the serial #.

Note that last string after (null) with all the spaces and the open parens? I highly suspect it's the IV (initialization vector) for the serial number and it's check digits. I'm not 100% sure, but it's a good guess. Maybe it's true.

Ok, that was day 1. Got some details but not very far. Meanwhile Jason wrote a script that generates serial numbers like SCO-000000 and counts up - the brute force attempt. That didn't quite work. So there's more to it.

      Day 2

The next attempt was to try to use a debugger against brand. Great idea, and w00t! Oh what wonderful luck, someone forgot to strip the brand binary of its symbols! If we runnm brand we can see all the tasty symbols! Very useful. The debugger Xenix has is adb. Hmm, I used that guy before in Solaris. It's really old it turns out. It's been a while so I google adb man page. D'oh all the results I get are for android's adb. Grrrr argh. Well, this is because modern unixes such as Solaris have replaced adb with mdb (or gdb/ldb). I eventually find an ancient adb man page and go to town.

So how do you use adb? I won't give you a whole lesson on it, just a few things.
If you type in a symbol name, it takes you to that address. If you type in ? and hit enter it will disassemble the instruction at that address. If you hit enter again, the next opcode is displayed and so on. If you type in ?x you get a 2 byte (word) hex dump. Cool.

If you type in:b you can set a breakpoint and $b will print them out and $r will show you the registers.
If you type in :r -s SCO-123456 /usr/lib/mp/inter it will run the program being debugged and it will pass those arguments to it, and since I've set a breakpoint, it will break at that location. Yeay! But, if you try to step through the program one instruction at a time, instead, it continues to run the whole program! D'oh!

Still, it's very useful though it's got some issues. Being lazy, I decided to disassemble all of brand, so I wrote a small script that calls adb and types in the right stuff for me like _main to start, then ? and then keeps hitting enter. I piped the output of this through my friend tee and wrote it to a file. But.... how to get it out of Xenix? Oh, sure enough, I dd the file onto a floppy and then run strings against it. Yeay! I now possess the fully disassembled copy of brand.

I do the same thing with inter, but ofc there's no symbols, so no names.

There's some interesting stuff here. As usual 68K C functions start with a link instruction and end with an RTS, and calls start by pushing stuff to the stack and then releasing them off the stack pointer (A7). Good stuff. So since inter doesn't have symbols I have no idea what's what. So I just start to look for RTS and LINK and break up the file into individual functions.

Now looking back at the structure of brand, first comes the magic _start code, then _main, then the program's functions, then the linked in libc code, then strings and other data. Ok, cool. But... there was an optimization, since this is ancient unix (well, Xenix), and all binaries are statically linked, (dynamic libs weren't invented yet), and disk space was tight, the linker would only link in the libc.a functions that were actually used. Good news, so there's less code to look at when trying to reverse engineer it.

So there's interesting stuff like long division, remainder of division, fdopen, seek, printf, read/write, etc. All what you'd expect. The hint is that the serial number has some sort of division and long integer math - maybe 32 bit, maybe 64 bit, whatever. And there's interesting code at a function called _verifys and at _untrp (untrap?). So I spend some time there until my eyes glaze over trying to figure out the format of the serial number and the check digits, but no clue as to what the 3 letters should be and how the two parts of the serial number work.

      Day 3

Ok, let's try something else. I don't know what crazy math was involved, and it's soooo booooring. So I compare the disassembly of brand which has symbols to that of inter which does not. I eventually figure out the few libc functions that brand and inter have in common, but it's enough. The big one I care about is _printf. That's the baby that spits out the serial number and more importantly "Invalid Serial Number" So how do I find out what does that?

Well, going back to adb against inter, I look at the end of the inter binary where strings live and I tediously use ?x until I find what I'm looking for - the address offset to "Invalid Serial Number" adb says it lives at 0x86BC. and _print (or _printf) is at 0x8282 - but the printf in inter is actually a call to trap 0 unlike the one in brand, but that's a story I'll skip over telling you how I found out because it's long and boring and involves setting random breakpoints. But ok, now I know the address for the pointer to the "Invalid Serial Number\0" and I know something somewhere calls that and then it's game over. So I look for that pointer. I find it. Here's the routine:

      0x90E:      link  a6,#0x0
      0x912:      tstb  -144.(a7)
      0x916:      moveml  #<>,-(a7)
      0x91A:      moveq #0x18,d0
      0x91C:      movl  d0,-(a7) push 0x18 whatever that is to stack
      0x91E:      movl  #0x86BC,-(a7) <<< push pointer of "Invalid
    Serial Number\0" to stack
      0x924:      moveq #0x2,d0  probably stderr? since that's file
    descriptor 2 always
      0x926:      movl  d0,-(a7)
      0x928:      jsr 0x8282                      print
      0x92E:      addw  #0xC,a7
      0x932:      moveq #0x1,d0
      0x934:      movl  d0,-(a7)
      0x936:      jsr 0x79C8
      0x93C:      addql #0x4,a7
      0x93E:      bra 0x942
      0x942:      unlk  a6
      0x944:      rts

Oh yeah, if you know 68K you'll notice the compiler used to generate this is retarded. It inserted an extra branch before the unlink for some retarded reason. There's a lot of that here, but we'll ignore it.

Ok, great, so we know anything that calls 0x90E means it failed the serial # check and we need to find that, and avoid it. But, there's no direct call to this! Instead there's this beauty and it's right after _main:


    0x50:     link  a6,#0xFFFFFFE8
    0x54:     tstb  -146.(a7)
    0x58:     moveml  #<>,-(a7)
    0x5C:     movl  #0x1,-4.(a6)
    0x64:     clrl  -8.(a6)
    0x68:     movl  #0x2,-12.(a6)
    0x70:     jsr 0x6394
    *0x76:     movl  #0x90E,-(a7) routine for invalid serial#?*
    0x7C:     movl  #0x8F0,-(a7) routine for something
    0x82:     movl  #0x8D2,-(a7) routine for something else
    0x88:     jsr 0x6516
    0x8E:     addw  #0xC,a7  12 bytes off

Interesting, that's the only reference to this. And weird. Well, let's see what the function at 0x6516 does.... (I'd guess 0x6394 spits out the banner announcing it's multiplan and prints out the serial number and such, don't know, didn't look there.)

And here are those other two something functions:

  0x8D2:      link  a6,#0x0
  0x8D6:      tstb  -132.(a7)
  0x8DA:      moveml  #<>,-(a7)
  0x8DE:      movl  #0x8000,0x8694
  0x8E8:      bra 0x8EC
  0x8EC:      unlk  a6
  0x8EE:      rts
  0x8F0:      link  a6,#0x0
  0x8F4:      tstb  -132.(a7)
  0x8F8:      moveml  #<>,-(a7)
  0x8FC:      movl  #0x1A0D,0x8694
  0x906:      bra 0x90A
  0x90A:      unlk  a6
  0x90C:      rts

They both just write some value at 8694. One writes 1A0D, the other 8000. Ok, whatever. Both have the retarded extra useless branch before the unlink, but whatever. Maybe those values are a clue for what the 01 or 02 mean in terms of the serial number.

So let's see what's at 6516 which gets called from 0x88 right after the address of a function that says invalid serial number (and two others) are pushed to the stack.

    0x6516:     link  a6,#0x0
    0x651A:     tstb  -132.(a7)
    0x651E:     moveml  #<>,-(a7)
    0x6522:     jsr 0x60C6        some function call is done here,
    likely the SN check
    0x6528:     bra 0x6550
    0x652C:     movl  8.(a6),0x93A6  one of the function calls
    0x6534:     bra 0x656A
    0x6538:     movl  12.(a6),0x93A6 another of the function calls
    0x6540:     bra 0x656A
    0x6544:     movl  16.(a6),0x93A6 and the (3/tu)rd - the "Invalid
    Serial Number" one.
    0x654C:     bra 0x656A
    0x6550:     tstl  d0          whoa! a decision is made here!
    0x6552:     beq 0x6544        is it 0?
    0x6556:     cmpl  #0x1,d0     is it 1?
    0x655C:     beq 0x6538
    0x6560:     cmpl  #0x2,d0     is it 2?
    0x6566:     beq 0x652C
    0x656A:     bra 0x656E
    0x656E:     unlk  a6
    0x6570:     rts

Interesting. So it's a selector function that writes a function pointer that got passed to it earlier to a 0x93A6. Could be something. And for sure something else will call this address, but...

First, that TST looks interesting. Let's set a breakpoint there and run it with adb. What do I get back in d0? Why I get 0. This is the value for an unserialized system.
Ok, so for shizzle we don't want a zero there.

(Fast forward past some really boring hex searches to find the location for that exact bit of code on the multiplan disk image - why it's at sector 102, and that TST.L is at offset 0x184 in that sector and MOVEQ.L to D0 is 0x70 and the value to move to register D0 follows it.)

Let's try to overwrite that TST.L D0 with MOVEQ.L #1,D0 just to see what happens. That way it will always return the same value.

And now we have a winner. Multiplan came up. (That's what the 70 01 in the command at the top was: MOVEQ.L #1,D0. If you want 2, we use 70 02.)

I then send the magic lisafsh-tool commands to Jason and ask him to test both 01 and 02 to see what they do different. He reports both work and for multiuser (I'm assuming over serial port for the 2nd tty session), so all is good with both.

At this point I lose interest in trying to figure out the rest because I have a working MultiPlan that Jason can demo at VCF, so I break out the 25 year old Auchentoshan, get Schwifty, and declare victory. Wabba Lubba Dub Dub!

      Day 5 (or 4 or whatever)

I decide to vomit out this text like one of Unity's hosts - I thought I'd share the knowledge of this incase anyone else wants to play or has similar situations with Xenix.
If you got this far and are not comatose, you're insane like me, congrats! Your mission is to find out the difference between 01 and 02 and what 1A0D vs 8000 mean.

The bigger mission for you and your Morty to fully reverse engineer inter and figure out the mp.cod/mp.dat and port it to a modern system so you can run multiplan on OS X and Linux natively.

If you gave up at the top, no worries, I'm still happy I was able to help you get some Multiplan nostalgia.

You received this message because you are a member of the LisaList group.
The group FAQ is at
To post to this group, send email to lisalist_at_email.domain.hidden
To leave this group, send email to lisalist+unsubscribe_at_email.domain.hidden
For more options, visit this group at
You received this message because you are subscribed to the Google Groups "LisaList" group.
To unsubscribe from this group and stop receiving emails from it, send an email to
For more options, visit

Received on 2017-03-07 21:37:39

This archive was generated by hypermail 2.4.0 : 2020-01-13 12:15:16 EST