Download as pdf or txt
Download as pdf or txt
You are on page 1of 13

Lotus 1-2-3 For Linux

by Tavis Ormandy

Intro
Background
Progress
Lotus 1-2-3 for UNIX
A Mystery File
Hacking
GNU objcopy
Porting Problems
System Calls
Relocations
Incompatible Functions
Termios
Licensing
Result

Intro
It’s an exciting time
in the Lotus 1-2-3
enthusiast community –
that was a joke, there
is no enthusiast

😆
community, it’s just
me!

It really is an
exciting time though –
that part isn’t a
joke!

There have been some major developments in the last


few weeks, and I guess that’s pretty unusual for 30
year old abandonware.

I’ll cut to the chase; through a combination of


unlikely discoveries, crazy hacks and the 90s BBS
warez scene I’ve been able to port Lotus 1-2-3
natively to Linux – an operating system that literally
didn’t exist when 1-2-3 was released!1

If you want to hear how a proprietary application


could be ported to new operating systems 30 years
after release, read on.

Background
I really like
Lotus 1-2-3, I
even maintain a
driver to make
sure it works
well on modern
systems. I had to
reverse engineer
the driver api to
make that happen,
but it works
beautifully.

Getting that driver working was quite an adventure,


but there is still one piece of the puzzle missing:
add-ins. 1-2-3 was designed to be extensible with
plugins (or “add-ins”) – in theory you could add
support for modern spreadsheet functions or integrate
with Google Finance or something!

The problem is add-ins had to be written in a special


language called LPL, and unfortunately the compiler
and SDK have been lost. This is not really surprising,
this was a niche product and Lotus didn’t just give
the SDK away – they charged $395 for it. I can’t
imagine many people have a copy just lying around.

There were lots of


commercial plugins
available in the heyday,
some were really
impressive. I emailed a
few developers who
worked on some to see if
they had any old
backups, the answer was
always the same – 1-2-3
was the biggest name in
software, nobody thought it was going anywhere, so why
would they keep backups?
There were also two third-party books written about
LPL, I managed to track them down from old libraries.
I like the cover on this one, very serious business.
It’s so frustating, I can see screenshots of the
debugger, compiler output and sample add-ins - but
just can’t do anything with it without the SDK!

Progress
Fast forward a year
or two, and I found
someone who used to
be a sysop in the
’90s BBS scene.
Check out his
website, he still
has a catalogue of
NFOs and a telnet
BBS online if you
want to see some
rad ANSI art!

He had kept tape backups from old BBS systems, and was
able to recover a warez copy of the SDK –
unbelievable! It actually worked, I was able to build
a few sample plugins!
You can download the ADK right here, and here is a
sample LPL program.

This would already have been enough to keep me


entertained for a while, but it doesn’t stop there. It
turns out that the BBS also had a warez copy of Lotus
1-2-3 for UNIX. This was widely thought to be lost –
I’m told it couldn’t compete with a more popular UNIX
office suite called SCO Professional, so there were
not many copies sold.

I didn’t really have any use for it, but I’m


definitely curious enough to poke around on the
installation media to see what’s in it!

Lotus 1-2-3 for UNIX


The warez
release was
a bunch of
TD0 files,
that’s a
format I’ve never seen before – apparently it’s an old
compressed disk image format from the 80s. I found
this page which recommends using samdisk to convert it
to a raw disk image.

$ ls
123UNIX1.TD0 123UNIX2.TD0 123UNIX3.TD0
123UNIX4.TD0 123UNIX5.TD0 LEGAL.NFO
WHATITIS
$ file *.TD0
123UNIX1.TD0: floppy image data (TeleDisk)
123UNIX2.TD0: floppy image data (TeleDisk)
123UNIX3.TD0: floppy image data (TeleDisk)
123UNIX4.TD0: floppy image data (TeleDisk)
123UNIX5.TD0: floppy image data (TeleDisk)

That seemed promising, samdisk builds and seems to


work! I’ve uploaded the full images onto the Internet
Archive in case anyone else is curious enough to take
a look.

$ samdisk info 123UNIX1.TD0


[123UNIX1.TD0]
Type: TD0
Size: 80 Cyls, 2 Heads

created : 1991-06-22 20:24:04


Lotus 1-2-3 for UNIX System V
$ for i in *.TD0; do samdisk copy ${i} ${i/.TD0
/.RAW}; done
Wrote 80 cyls, 2 heads, 18 sectors, 512
bytes/sector = 1474560 bytes
Wrote 80 cyls, 2 heads, 18 sectors, 512
bytes/sector = 1474560 bytes
Wrote 80 cyls, 2 heads, 18 sectors, 512
bytes/sector = 1474560 bytes
Wrote 80 cyls, 2 heads, 18 sectors, 512
bytes/sector = 1474560 bytes
Wrote 80 cyls, 2 heads, 18 sectors, 512
bytes/sector = 1474560 bytes

$ file *.RAW
123UNIX1.RAW: tar archive
123UNIX2.RAW: ASCII cpio archive (pre-SVR4 or
odc)
123UNIX3.RAW: ASCII cpio archive (pre-SVR4 or
odc)
123UNIX4.RAW: ASCII cpio archive (pre-SVR4 or
odc)
123UNIX5.RAW: ASCII cpio archive (pre-SVR4 or
odc)

I know that UNIX


software was
usually
distributed as
raw archives,
and you were
expected to
insert the
diskette and run
something like
tar -C / -xvf
/dev/fd0, so these files look right. I think this is
smart, why waste precious bytes on a filesystem?

$ tar xf 123UNIX1.RAW
$ for i in 123UNIX{2..5}.RAW; do cpio -id < $i; done
1555 blocks
2606 blocks
2510 blocks
2481 blocks

Full Directory Listing


A Mystery File
While poking around, this one directory caught my eye,
what exactly is this?

$ ls -l
total 2.0M
-rw-r--r-- 1 taviso taviso 1.2M May 20 13:53
123.o.z_1
-rw-r--r-- 1 taviso taviso 717K May 20 13:53
123.o.z_2
-rw------- 1 taviso taviso 913 May 20 13:53
dl_init.o.z
-rwx------ 1 taviso taviso 77K May 20 13:53 ld.z*
-rwx------ 1 taviso taviso 14K May 20 13:53
mkdlobj.z*
-rw------- 1 taviso taviso 649 May 20 13:53 stub.o
-rw------- 1 taviso taviso 328 May 20 13:53 tail.o
-rw-r--r-- 1 taviso taviso 1.4K May 20 13:53
wyse50-lts123

That 123.o file is huge, even compressed it had to be


split across two disks. Let’s take a closer look…

$ cat 123.o.z_? | gzip -d > 123.o


$ file 123.o
123.o: Intel 80386 COFF object file, not stripped, 5
sections, symbol offset=0x1efbdc, 19755
symbols, optional header size 28

Yikes - it’s an original unstripped object file from


1-2-3. There are nearly 20,000 symbols including
private symbols and debug information.

Why would Lotus ship this? It’s so big it must have


required them to phyiscally ship an extra disk to
every customer? Could it have been a mistake,
accidentally left on the final release image?

I had so many questions, but I’m not old enough to


have any experience with SysV, so I asked the
greybeards on alt.folklore.computers if they had seen
this before and why this might have happened.

The answer was that this is probably deliberate -


dlopen() was not widely available on UNIX in the early
90s, so there was no easy way to load native plugins
or extensions. To solve this, vendors would ship a
bunch of partially linked object files with a script
to relink them with your extensions – Clever!

Hacking
I can’t tell you how useful this discovery was – the
debug information answered so many questions I had
about Lotus 1-2-3 internals! This was a direct source
port from DOS, so it mostly worked the same way but
now I had debugging data. For example, I really wanted
to hook into the rasterizer in my driver so that I
could improve the appearance of graphs in the
terminal… but it was just too complex to understand
without documentation.

I now know that the rasterizer dynamically generates


little bytecode programs that that are interpreted by
the graphics engine. Now that I know what the opcodes
are I can disassemble and change them in my driver to
improve the output!

GNU objcopy
Okay… but there is one more big question, I know that
objcopy can convert COFF object files to ELF, the
format used by Linux. It seems like a long shot, but

🐧
is it possible I could link this into a native Linux
program?

$ objcopy -I coff-i386 -O elf32-i386 123.o 123elf.o


$ file 123elf.o
123elf.o: ELF 32-bit LSB relocatable, Intel 80386,
version 1 (SYSV), not stripped

Hilariously, the first version of Linux hadn’t even


been released when this object file was compiled – but
I think this is possible! If you want to hear about
the technical challenges, read on!

$ objdump -p 123.o | grep Date


Time/Date Sat Sep 8 06:23:50 1990

Porting Problems

System Calls
The first problem is that Linux and UNIX do not use a
compatible system call interface. UNIX uses the lcall7
interface, so we need to find those calls and fix them
up. Here is how this object file calls open():

$ objdump -M intel --disassemble=open 123elf.o

123elf.o: file format elf32-i386

Disassembly of section .text:

000e20d4 <open>:
e20d4: b8 05 00 00 00 mov eax,0x5
e20d9: 9a 00 00 00 00 07 00 call 0x7:0x0
e20e0: 0f 82 c6 01 00 00 jb e22ac
<_cerror>
e20e6: c3 ret
e20e7: 90 nop

That call instruction is what’s known as a callgate,


which isn’t supported on Linux2, it will just crash. I
want to remove this and route all calls through glibc
instead. My first thought was just to mark these
symbols as undefined, and then let the linker fix that
up by importing a replacement symbol from glibc.

Relocations

Nothing is ever easy, it turns out that won’t work! If


we try, objcopy will simply refuse:
$ objcopy -I coff-i386 -O elf32-i386 --strip-symbol
open 123.o 123elf.o
objcopy: not stripping symbol `open' because it is
named in a relocation

What is objcopy trying to tell us here?

This is a relocatable object file, which simply means


it can be loaded at any address and still work. That’s
possible because it contains all the necessary
information – the relocations – to adjust it.

Relocations are really simple, the compiler just


records the name of the symbol and the references to
it. Now the linker can just walk through and patch
each reference to point to the new location – easy.

So objcopy is saying that you can’t remove this


symbol, because the linker won’t know what to patch in
when it moves it. Fair enough – but, just because
objcopy won’t do it doesn’t mean it’s impossible! We
could just fix the relocations too, right?

I don’t know of any tool that can do that, but COFF is


not a complicated format – I’ll write one!

Introducing coffsyrup, a tiny little tool that will


remove those pesky COFF symbols even if objcopy
refuses!

$ coffsyrup 123.o 123new.o open


MATCH open
RELOC rel open @0x180fa ~0xc9ede
RELOC rel open @0x4c9a1 ~0x95637
RELOC rel open @0x4d348 ~0x94c90
RELOC rel open @0x4ec13 ~0x933c5

Incompatible Functions

Now that we can reroute functions, we have to worry


about incompatible functions.

Lots of standard UNIX functions are source but not


binary compatible, this is because nobody promises
that structures are the same size or layout across
UNIX versions. The obvious example is struct stat.
For example, this code is likely to work on any UNIX-
like system you can compile it on:

struct stat sb;

if (stat("/etc/passwd", &sb) == 0) {
printf("Size: %u\n", sb.st_size);
}

However, the resulting object file is not likely to


work on any other system. That’s because the size of
struct statand the offset of st_size will be different
– it will probably just corrupt your stack and crash!

Luckily there are not really that many functions like


this in UNIX. In fact, the number is small enough that
I can probably write wrappers to translate them. The
important ones are stat(), times(), uname(), fcntl(),
ioctl() and so on.

All I have to do is rename those symbols with


objcopy, then mark them undefined with coffsyrup. Now
I can write a little wrapper that translates a Linux
struct stat to a UNIX struct stat and it should work!

Termios

Well…I said “little” wrappers – but there are some big


incompatabilities in places. One big nightmare was
termios. Go ahead and take a look at the termios(3)
man page, pretty complex right? Well, everything here
works differently in subtle, incompatible, and
difficult to debug ways on every UNIX system.

Licensing

Incredibly,
after a bunch of
hacking it
actually runs
without
crashing!

…and refuses to
work without a
license, damn!
Well, I am a
legitimate licensed 1-2-3 owner
with a boxed copy
of 1-2-3, and this
is 32 year old
abandonware. I
think Mitch Kapor
will forgive me
for bypassing this
check.

I can see from breaking on exit() that there is an


internal symbol called lic_init() responsible
for
checking for a valid license. I looked at the code in
IDA, and figured out the logic.

It is simply looking for a file called LICENSE.000,


which contains an expiry date, username and


systemname. If that all matches what the system
reports, the check passes!

Result
That’s it, Lotus 1-2-3 has been ported to a new
operating system. There are a few kinks that need to
be ironed out, and I need to port over my terminal
driver, but it is 100% usable. At the moment, the DOS
version running under emulation looks better - but
this can be fixed!

I hope you enjoyed reading about this, in the highly


unlikely event you actually want to try it yourself –
all of my code is on github.

1. I’m specifically talking about the classic R3,


there were releases until 2002.↩

2. Linux did have lcall7 and lcall27 compatability


support at one point, but alas no more.↩

HOME • ABOUT • CONTACT

You might also like