TL/DR: I hacked PalmOS simulator from the inside, giving it new capabilities to run real ARM code -- something it could never do.


Palm's PalmOS Simulator was a development tool to assist in developing for PalmOS 5 device (and PalmOS 6 as well FWIW). PalmOS devices ran on ARM processors, while previous PalmOs verdions used Motorolla's 68K series. This PalmOS 5 has a built-in MC68K emulator called PACE.This was fast enough but for native speed, PalmOS supported chunks of native code that could be run. Initially they were called ARMlets but were then renamed to PNOlets. My theory is that Palm was considering architectures other than ARM as well, thus the rename. At the time, PocketPC devices used ARM, MIPS, and SH3 architectures, so those would be my guesses. Palm Simulator ran under MS Windows on intel x86 processors. IT did not include ARM emulation, instead developers were expected to compile their native code to a DLL using Visual Studio, place it inside the simulator directory, and run native code this way. This also can be seen as evidence that they considered multiple architectures, thus the drive for not-only-ARM code.

How do native calls work on PalmOS 5? The 68k code calls the OsCall PceNativeCall(), passing the pointer to the native code to run as the first param, and the pointer to pass to that code as the second. The native code's return value is returned back to the 68k code. For ARM devices, the code pointer was usually a locked code resource on the storage heap. For the simulator, instead, it was a string containing the name of the DLL, a NULL byte, and then the entrypoint name to call, with the stdcall calling convention. 

The actual native code entrypoint has a prototype that looks like this: UInt32 NativeFunc(EmulStateRef *ref, void *userData, UInt32 (*callout)(EmulStateRef, UInt32 funcPtrOrOscallNo, void *params, UInt32 paramsLenAndFlags)). The "ref" is an opaque pointer to the PACE emulator's emulated 68k state. Its actual insides are partly understood. The second param -- userData -- is the parameter passed from the 68k code. The third is a callback back into PACE that can be used by the native code to call back into 68k code, or to call OsCalls, since there is no official way to call OsCalls from native code directly. Instead, one must marshal params into memory in 68k byte order and padding, and then pass the pointer to that set of params on the fake stack to the callout. For x86 it works the same way, except in x86 instead of ARM.

Now we need to look at some PACE insides. What does that PACE EmulState have? At offset 0 is a word that is not often used and can be overwritten anytime. It is primarily used to record invalid instructions before crashing due to them. At offset 4 it has the 8 D-registers of the emulated 68k processor, at offset 0x24, it has the 8 A-registers, the last of which is the stack pointer. Since, technically, multiple 68k contexts may exist in the system, more than one such EmulState may exist. But somewhere in the system is the first one created, the main one. All others (and it itself indeed) have a pointer to that main one at 0xCC. and at offset 0x16C is a pointer to the handlers of every OsCall that 68k code can make. As this table is very large, no copies of it are made, instead, only the main EmulState has it, all others have that pointer set to NULL. This makes sense, since there are about 700 OsCalls. Inside PACE, there are handlers for each that will unmarshal the parameters from the emulated 68k stack, slice and dice them as needed to match what the undocumented native ARM OsCalls expect, call them, and then place the return value where needed. These handlers all have the same prototype: void osCallHandlerFunc(EmulStateRef *ref, UInt16 osCallIdx). And, of course, they are called in native code for whatever the host architecture is (ARM or x86).

Thus, the plan of attack is simple: replace the native handler for PceNativeCall with my own, that can emulate ARM and properly call out of there, into there as needed. There are actually some more intricacies involved but let's get to them first. First of all, how do we find the table of handlers to modify. Well, in PalmOS 5.x, the pointer to it is simply exported in a feature ('a68k', 1), in Palm OS 6.x, this is not the case, so this approach would not work there. Finding the table by using an EmulStateRef is the better approach since it would work everywhere. So how do we find our own EmulStateRef from 68k code? We'll need to leak it somehow. Recall that the only native code we can call are named entrypoints in a DLL. How do we exploit that? Well, consider _lrotl() exported by msvcrt.dll. This function will rotate its first integer parameter right by a number of bits given by its second integer parameter. Recall that native code we call has a prototype UInt32 NativeFunc(EmulStateRef *ref, void *userData, [..]callout). We control the second param and we wish to leak the first. So ... we just call PceNativeCall("msvcrt.dll\0_lrotl", NULL) and we get back a pointer to our own EmulStateRef, rotated right 0 bits, so ... the unmolested value. Cool! We have the EmulStateRef.

Sadly this was the easy part. The code to call out back into the OS or into 68k code, the thing that "callout" param points to is very complex and we'll need a pointer to it so that our emulator can do all that. So, we now need to find a way to leak the third param using only exported entrypoints from known DLLs. I spent a lot of time finding a way to do this, and I only came up with a rather complex one. My solution centers on wsprintfA() from user32.dll. EmulStateRef will be the destination buffer, user-provided param is the format string, and our secret-in-need-of-being-leaked will be the first param to print. On return from the native code, PceNativeCall handler will overwrite the 68k's D0 and A0 registers with the return value of the native call. And the first word of EmulStateRef is not at all readable in 68k, so, as a result, we can only see bytes 8 and on of the resulting string. So we call it twice. First with the format string "________%08x" and then with the format string "____%08x". We then read D1 which contains the 4 characters produces by wsprintfA() at output offsets 8 to 11. From there we can calculate the third parameter successfully and thus have the callout function pointer recovered.

We are now ready to replace the native handler for PceNativeCall with our own! We get a pointer to the ARM code, and we just emulate it, one instruction at a time. Not too hard, even. But what do we pass to it as its "callout" pointer? We cannot pass in a x86 function pointer, since we'll not know when it tried to jump there and we'll try to emulate the bytes there are ARM. Same for completion -- how would we know when the emulation is complete and it is time to return? The answer is simple -- magic values. On entry we set ARM's LR register to 0xffffc004 and we pass 0xffffc00c as "callout". Now, if during emulation we see PC being set to either of those, we know the respective event happened. If we see 0xffffc004, we can read out emulated R0 and return that -- it is the return value. If we see 0xffffc00c, we collect the values of the R0, R1, R2, R3 and pass them as params to the real x86 callout. When it returns, we set emulated R0 to the returned value and jump back into emulation. This all works only because x86 and ARM are both little endian, ILP32, and thus pointers and structures may be passed freely between them. Does it work? It does!

There is another complication to consider -- the other way to enter native code on some PalmOS5+ devices - Audio Streams. Not all devices support sampled audio output, so not all have this issue, but it needs to be handled properly. When creating an Audio Stream, one passes a pointer to the function which will provide samples to play. This callback may be 68k code or native code. If it is 68k code, it will be emulated by PACE and that is boring for us. But if it needs to call native code, we need to emulate! How do we get in the middle of this? We need to intercept SndStreamCreate() and SndStreamCreateExtended(), replace their callbacks with our own, saving the original one elsewhere so we know what to emulate. In order to free this state, we'll need to know when a stream is destroyed, forcing us to intercept SndStreamDelete() as well. All these pieces of state must be stored in some common structure so we cna lookup by stream ID, as that is all we have at the time of the call to SndStreamDelete(). There are more minutiae involved in making this work, but more or less, this is it! This works and audio works now.

For debugging, I wanted to be able to show a dialog box on Windows, but how? My x86 code exists at an unknown-to-me virtual address which may differ every boot. I also have no way to import or export any DLLs as my code is not a DLL, it is just bytes I cause jumps to. What to do? Luckily, this problem is long-solved by malware writers. Effectively, what we have is shellcode, and how to go from that to calling named exports from DLLs is known. The PEB is at fs:0x30 in Windows, from there one may find the loaded modules list, which will contain kernel32.dll for all Win32 processes. In there, one may locate LoadLibraryA and GetProcAddress entry points by name, which allows loading any DLL by name and calling an entrypoint in there. From there, it is only a matter or some code to cobble together a debugLog(const char *fmt, ...) by using ntdll.dll's _vsnprintf() and user32.dll's MessageBoxA(). Why all this work instead of just making a DLL as Palm had intended? A DLL would not need to leak EmulStateRef, and could easily get the "callout" value too. Two reasons: I did not want to depend on old Visual Studio versions for build, preferring modern gcc simply due to availability, but also, I like the single-PRC-file install method. One self-contained file. Works in every version of the PalmOS Simulator! How cool is that?

It is fast enough on my old Win2K machine that games work with audio and MP3 players work in the official simulator now! How do you use it? Install the PRC into the simulator and either soft reset it or run the app once. From then on ARM applications & games will just work. Sources included. Enjoy!




