Exploring the Pippin ROM(s), part 8: Cracking open the Pippin (without the boys)

The Bandai Pippin has finally been cracked.

They say a picture is worth a thousand words. So I put together a fun proof-of-concept demo, and made a video to summarize these last few months.

For an in-depth technical explanation of what’s happening here, here’s some further reading:

Exploring the Pippin ROM(s), part 7: A lot to digest

Apple’s public key for verifying the authentication data on a Pippin boot volume is:
E0 E0 27 5C AB 60 C8 86 A3 FA C2 98 21 79 54 A8 9F D1 B9 DC 8A BA 84 EF B1 E7 C9 E2 1B F7 DD D7 DC F0 E4 4A BB 79 51 0E 7C EB 80 B1 1D

How did I find this? Strap in and let’s go for a ride. 🙂

The What

Quick recap: I want to unlock homebrew on the Pippin. Every time a Pippin tries to boot from an HFS volume on CD-ROM, it loads the ‘rvpr’ resource with ID 0 from ROM and executes it as code, passing as arguments that volume’s ID and two blocks of data found elsewhere in ROM. ‘rvpr’ 0 locates and reads an RSA-signed “PippinAuthenticationFile” from the volume. It contains a 128-bit digest for each 128K block in the volume, along with a 45-byte signature at the end of the file. If the signature cannot be verified or any of the digests don’t match what ‘rvpr’ 0 calculates from the volume in its main loop, the Pippin (r)ejects the disc.

“Feed me, Seymour!”

Last time I looked at ‘rvpr’ 0, I examined and broke down what the main loop does at a high level. Outside of main, there are ten non-library function symbols in the resource, six of which I’ve identified their usage. Reading through these, I determined the format of the PippinAuthenticationFile and how its data is fed to the rest of the main loop. The remaining four functions—VerifyDigestInfo, VerifySignature, CreateDigest, and InitRSAAlgorithmChooser—form what I conjectured to be the “meat” of the authentication process. I elected to pore over these at a later time.

‘rvpr’ 0 is over 35K in size, with almost 34K of that comprised of 68K assembly code. Unfortunately, what I looked at in part 6 only touches about 3K of that—not even 10% of the whole. What’s worse is that the remaining 31K/90+% of code is almost completely lacking symbols, save for an occasional call to T_malloc, T_memset, T_memcpy, or T_free. Without human-readable symbols to guide what the remaining memory locations, values, and functions mean in the context of ‘rvpr’ 0’s greater purpose, I would be “flying blind” without a safety net. If I was to attempt to grok this code to the same degree as I currently understand main and its (named, mind you) auxiliary functions, I would have a long road ahead of me especially if I used the same static analysis technique of stepping through the code offline on paper.

The Why

I decided that the best way to figure out the rest of this code was to use dynamic analysis; that is, to examine it while it’s running. There were just too many subroutines and too much data being pushed around to keep it all straight in my head. I needed a computer to help. I don’t have any hardware debuggers for the Pippin that would allow me to step the CPU and examine the contents of RAM, and no working software emulators exist for the Pippin (yet). What I found does exist is a suite of 68K assembly tools—a code editor, binary editor, and, crucially: a simulator—called EASy68K. If I wanted to look at ‘rvpr’ 0 in something that even somehow resembled a debugger, I’d have to build a working version of ‘rvpr’ 0 that could be run outside of a Pippin, without first understanding how the code works in the first place.

EASy68K’s simulator provides a hypothetical computer system featuring a 68K CPU, 16MB of RAM, and rudimentary I/O facilities. Luckily, ‘rvpr’ 0 is pretty self-contained, which allowed me to quickly “port” it to EASy68K. I was correct in that this technique significantly accelerated my understanding of the digesting and verification process, but as I hope to elucidate later in this post, that pursuit required very little actual parsing of code. 🙂

The How

The first step to building an ‘rvpr’ 0 replica in EASy68K was to adopt the syntax EASy68K likes. I prefer FDisasm for disassembly because it’s part of the Mini vMac project and as such, it’s at least a tiny bit aware of the classic Mac OS API, known as the Toolbox. FDisasm can replace raw A-traps (two-byte 68K instructions beginning with the hex digit $A, which typically map to commonly-used subroutines) with their corresponding human-readable names according to official Mac documentation, which is a nice time-saver especially in large blocks of 68K Mac code. I also like FDisasm’s output formatting, which is the basis of how I list 68K assembly in these blog posts.

I admit it ain’t neat, but it’s handy.

Cloning ‘rvpr’ 0 in EASy68K serves two purposes. First, I can step through it using a real signed Pippin CD, observe what its code does, and document it. After I reverse-engineer the authentication process though, this functional copy will serve a second purpose: to verify that my own authentication files are crafted properly. Since we know from part 2 and part 6 that the main loop will return zero in register D0 if the verification process succeeds, we should be able to observe that in the simulator. Using our own ‘rvpr’ 0 binary that’s as close as possible to what’s in ROM on an actual Pippin should assuage doubt as to whether a proof-of-concept will pass the console’s tests or not. Plus, since it’s all simulated in software, it saves me from having to burn a ton of test CDs. 😛

Converting my (annotated) disassembly from FDisasm’s syntax to EASy68K’s was easy—regular expressions to the rescue. Assembling the result produces code identical to what’s in the original resource—yay, the assembler works, and we have a byte-for-byte clone of what’s in the Pippin ROM. But making this new replica functional required a little bit of creativity.

On a real Pippin, ‘rvpr’ 0 is loaded from the ROM’s resource map into an area on the system heap in RAM. The relocation code at the beginning of ‘rvpr’ 0 patches each subroutine jump by offsetting them relative to where the code resides in RAM (discussed in part 6). It keeps track of whether this is done by storing this offset in a global when relocation is complete. Recall from part 3 that this global has an initial value of zero when ‘rvpr’ 0 is first run. If this code is executed a second time, it subtracts this global from its base address in RAM and, if the result is zero, it doesn’t need to do relocation again since the jumps already have valid destination addresses.

The simulator comes with no bootloader at all but starts up fully initialized, so in essence the contents of ‘rvpr’ 0 form the “ROM” of our virtual computer. We thus boot directly to ‘rvpr’ 0’s entry point, at the start of the simulator’s memory space. But since ‘rvpr’ 0 now always starts at address 0, the difference between that initial global and our base address… is zero. So the relocation code never runs in the simulator; it doesn’t have to because those unpatched jumps are already relative to zero. 😉

By the time ‘rvpr’ 0 executes in a real Pippin’s boot process, many subsystems on the console have been readied: the QuickDraw API for graphics, the Device Manager for device I/O, and the HFS filesystem package to name a few. These APIs, having been designed and built by Apple for the Mac, only exist on a Mac-based system and therefore naturally aren’t present in the fantasy system we get from EASy68K. We are in luck though in that ‘rvpr’ 0 only makes calls to a grand total of nine Toolbox APIs. Four of these calls are used in the “prologue” code discussed earlier that relocate all the jumps before main is even called. Since that relocation code doesn’t run in our simulator, that leaves five Toolbox APIs essential to the main loop: _Read, _Random, _NewPtr, _DisposePtr, and _BlockMoveData. We need equivalents to these routines if we are to expect ‘rvpr’ 0 to work properly.

Oh yes I would. And I did.

_BlockMoveData is an easy one. It copies D0 bytes from (A0) to (A1):

8DCA   12D8    Move.B    (A0)+, (A1)+
8DCC   5380    SubQ.L    #1, D0
8DCE   6CFA    BGE.B     _myBlockMoveData

I took a shortcut with _Random: my implementation simply returns a constant value. I did this partially because I’m lazy but also because _Random is only called once, albeit in a loop: to determine which 128K chunks to digest. By controlling the values returned, I can selectively and deterministically test chunk hashing.

There’s an XKCD for everything, even cracking Pippins.

I took similar liberties with _NewPtr and _DisposePtr: I keep a global pointing to the next unused block, and _NewPtr simply returns the value of that global and then advances it by the requested size. _DisposePtr is implemented as a no-op. Why did I do this? Well, again, part of it is because I’m lazy and didn’t want to write a proper heap allocator for this, but also because it affords me the ability to inspect memory blocks used even after they’ve been “freed.” I don’t care about memory leaks in this case—in fact, here they’re a feature! 🙂 Since ‘rvpr’ 0 is roughly 36K, I set aside the first 64K of memory for it (and any additional supporting code/data I add, like these replacement Toolbox routines). With register A7 initially pointing to the top of memory for use by the stack, the rest of RAM starting at $10000 I designate for my “heap.”

Finally we come to _Read. EASy68K may be pretty bare-bones, but it does come with some niceties allowing for basic interactions with its host PC. In this case, I needed a way for my “virtual Pippin” to have random-access readability from a virtual CD-ROM drive. Fortunately, EASy68K provides this in the form of the Trap #15 instruction. My version of _Read only does the bare minimum of what ‘rvpr’ 0’s main loop requires: it opens an HFS disk image on the host PC, seeks to the offset specified in the ParamBlockRec passed on the stack, reads the requested amount of bytes into the specified buffer, then closes the file.

8DF0=  6361 6E64 ...     DC.B      'candidate.hfs', 0

8DFE   43F9 0000 8DF0    Lea.L     _myFilename, A1
8E04   303C 0033         Move      #51, D0
8E08   4E4F              Trap      #15

8E0A   2428 002E         Move.L    $46(A0), D2
8E0E   303C 0037         Move      #55, D0
8E12   4E4F              Trap      #15

8E14   2268 0020         Move.L    $32(A0), A1
8E18   2428 0024         Move.L    $36(A0), D2
8E1C   303C 0035         Move      #53, D0
8E20   4E4F              Trap      #15

8E22  303C 0032          Move      #50, D0
8E26  4E4F               Trap      #15

8E28  303C 0000          Move      #0, D0

Now that we’ve got functional replacements for the necessary Toolbox routines, how do we refit the rest of the code so that our versions are called instead of Apple’s, which don’t exist? I already had the Toolbox API names substituted in my listing, thanks to FDisasm, so I could simply create macros with those names that execute a tiny bit of code in place of those calls. The easiest way, and the method I tried first, is to invoke each replacement with a Jsr instruction, which is short for “Jump to SubRoutine.” This was really straightforward to do and assembled without issue, but upon loading and running in the simulator, I quickly discovered why this approach wouldn’t work. Jsr is a four, sometimes six byte instruction, whereas the original A-traps they were to replace use only two bytes. Since these larger instructions are inserted in the main loop near the beginning of the code, this throws off hardcoded addresses used later. Needless to say, when I ran in the simulator, a hardcoded Jsr landed instead in an unexpected area of code and I crashed almost instantly.

However I was going to invoke my faux-Toolbox calls, they had to be done in only two bytes. I thought for a second about how I could write an A-trap exception handler and leave Apple’s original A-trap instructions as-is, but I didn’t do that either because 1) laziness and 2) I thought of an easier way. Remember the Trap #15 instruction I used to implement _Read?

On a 68K processor, the two-byte Trap instruction provides a way to jump to any of 16 different addresses stored in a predefined area of memory, known as “vectors.” These 32-bit vectors are all stored consecutively in a block of memory that always starts at address $80. ‘rvpr’ 0 normally executes code at address $80, but that’s part of the address relocation done only on a real Pippin, not in our simulator. It is therefore safe for us to replace that block of code with the addresses of our replacement Toolbox routines, starting with Trap #0 and ending with Trap #3. Recall that I’ve implemented _DisposePtr as a no-op—which is the two-byte opcode $4E71—so I don’t need to set aside a trap vector for it. EASy68K only sets aside trap 15 for itself, leaving traps 0-14 for us to use however we wish. The code we do care about executing in the simulator doesn’t start until after address $100, so our entire trap table easily fits inside this block of unused code. How lucky can you get? 🙂

My very own Pippin “emulator”

With my cobbled-together Pippin “emulator” now up and running, finally I could take a look at the before and after of the remaining functions called by the main loop. I decided to start with CreateDigest, as I had already figured out the inner workings of CompareDigests, so this seemed like a simple starting point. CreateDigest starts by creating a context object for itself, and initializes one of its buffers with a curious but predictable pattern of 16 bytes: 67 45 23 01 EF CD AB 89 98 BA DC FE 10 32 54 76. Along the way it checks to see if any of this setup fails, and if so, the entire authentication check is written off as a failure. But assuming everything is fine, it enters a loop which digests our 128K input chunk up to 16K at a time, by passing each 16K “window” to an unnamed subroutine. Each time we iterate through this loop, the aforementioned buffer which was initially filled with a predictable pattern now instead contains a brand new seemingly random jumble of numbers. I suspected that this 16-byte buffer was used as a sort of working space for whatever Apple chose as a hashing algorithm, since its size matched that of the digests in the PippinAuthenticationFile.

764   B883              Cmp.L     D3, D4               ; chunk size > 16K?
766   6C02              BGE.B     dontResetD3          ; if so, use 16K for window size
768   2604              Move.L    D4, D3               ; otherwise we have <16K left, so use what's left as window size instead
76A   4878 0000         Pea.L     ($0)                 ; push zero
76E   2F03              Move.L    D3, -(A7)            ; push window size (typically 16K until the end)
770   2F0A              Move.L    A2, -(A7)            ; push working chunk buffer ptr
772   2F2E FFFC         Move.L    -$4(A6), -(A7)       ; push ptr to our hash buffer
776   4EB9 0000 6516    Jsr       Anon217              ; patched, creates hash? digest?
77C   2A00              Move.L    D0, D5
77E   4A85              Tst.L     D5
780   4FEF 0010         Lea.L     $10(A7), A7          ; cleanup stack
784   6628              BNE.B     createDigestCleanup  ; if Anon217 failed, bail
786   9883              Sub.L     D3, D4               ; subtract window size from chunk size
788   D5C3              AddA.L    D3, A2               ; advance working chunk buffer to next 16K-ish window
78A   4A84              Tst.L     D4                   ; still more data to hash/digest?
78C   6ED6              BGT.B     createDigestLoop     ; hash/digest it

78E   4878 0000         Pea.L     ($0)                 ; push zero
792   4878 0010         Pea.L     ($10)                ; push 16
796   486E FFF8         Pea.L     -$8(A6)              ; push address of local longword
79A   2F2E 0010         Move.L    $10(A6), -(A7)       ; push out buffer ptr
79E   2F2E FFFC         Move.L    -$4(A6), -(A7)       ; push ptr to our hash buffer
7A2   4EB9 0000 654C    Jsr       Anon218              ; patched, copies hash out?
7A8   2A00              Move.L    D0, D5
7AA   4FEF 0014         Lea.L     $14(A7), A7          ; cleanup stack

Finally, CreateDigest makes one more call to an unnamed subroutine, passing it among other things the number 16 (presumably the digest size in bytes), a pointer to its context object, and a pointer to a 16-byte area on the stack filled with $FF bytes. After this call, the working buffer is once again reset to its initial pattern, and the area on the stack is filled with what looks like it could be a digest.

Wait a minute, upon closer examination...


... the output hash matches what's in the PippinAuthenticationFile! This makes sense, because all CreateDigest does after this is tear down and dispose of its context object. It then returns to the main loop, where the computed digest is passed along for CompareDigests to, well, compare. So clearly those two unnamed subroutines play a vital role in computing the digest, however that's done.

I dove right in to the routine called in the loop. It starts by doing several integrity checks of the data structures it's about to use, then goes right into a confusing routine that appears to add the amount of bits in our up-to-16K input window to a counter of some sort, optionally increasing the next byte if the counter rolls over. I suspected this was used for a 40-bit counter of bits hashed, its purpose not obvious yet. It then enters a loop of its own, dividing our input window into yet another sliding window, this time of 64 bytes in size. Each iteration of this loop, it passes these 64 bytes and CreateDigest's 16-byte working buffer to an unnamed subroutine with some very interesting behavior.

7186   2612              Move.L    (A2), D3
7188   282A 0004         Move.L    $4(A2), D4
718C   2A2A 0008         Move.L    $8(A2), D5
7190   2C2A 000C         Move.L    $C(A2), D6
7194   4878 0040         Pea.L     ($40)
7198   2F2E 000C         Move.L    $C(A6), -(A7)
719C   486E FFC0         Pea.L     -$40(A6)
71A0   4EB9 0000 7C58    Jsr       ($7C58).L
71A6   2004              Move.L    D4, D0
71A8   4680              Not.L     D0
71AA   C086              And.L     D6, D0
71AC   2204              Move.L    D4, D1
71AE   C285              And.L     D5, D1
71B0   8280              Or.L      D0, D1
71B2   D2AE FFC0         Add.L     -$40(A6), D1
71B6   0681 D76A A478    AddI.L    #$D76AA478, D1
71BC   D681              Add.L     D1, D3
71BE   2003              Move.L    D3, D0
71C0   7219              MoveQ.L   #$19, D1
71C2   E2A8              LsR.L     D1, D0
71C4   2203              Move.L    D3, D1
71C6   EF89              LsL.L     #$7, D1
71C8   8280              Or.L      D0, D1
71CA   2601              Move.L    D1, D3
71CC   D684              Add.L     D4, D3

Looking at this new subroutine, I was fairly convinced that it does the actual hashing. It is an unrolled loop that does a number of bitwise operations before adding a longword (32-bit quantity) from our input window along with what appear to be magic numbers to one of four 32-bit registers. At the end of this function, the contents of these registers are concatenated and added to the existing contents of CreateDigest's 16-byte working buffer. In order to maybe recognize a pattern to what this hash function was doing and perhaps identify the algorithm, I converted this assembly back to C code, and then verified that my C version produced identical output. Unfortunately, the algorithm didn't look familiar to me at all—I assumed it was something Apple invented specifically for the Pippin. I feared the initial "salt" might not be constant and could change depending on where the input chunk exists in the volume. Perhaps I merely found one hash function, but the Pippin could switch between different hash functions depending on some heuristic? It would require more disassembly and careful analysis to verify whether or not this was the case and why. 🙁

A Fun Side Story

Last Friday was 4/26, known informally among fans as Alien Day. I’m a big fan of the Alien universe, and this year happens to be the 40th anniversary of the 1979 Ridley Scott classic. So on Friday I had a number of friends over to watch both Alien films. 😉 There was pizza, chips, my homemade queso (half a box of Velveeta + a can of Rotel chiles—nuke it in the microwave for five minutes, stirring occasionally), and everybody had a good time.

One of the folks who dropped by was my friend Allison, who wanted to leave me with a disc of early Xbox demos her wife Erica found for me. I’m interested in investigating the contents of this disc in case there’s anything of historical value on it, but Xbox discs cannot be mounted or copied using a run-of-the-mill DVD-ROM drive. I remember years ago burning one or two (homebrew, ahem) Xbox DVDs with my PC, so I know writing Xbox discs is possible, but I was curious why reading them posed such an obstacle.

All roads lead back to Presto.

After some Googling, I found that the Xbox employs its own scheme to verify and “unlock” a boot disc candidate (described by none other than Multimedia Mike—an intrepid hacker whose blog I recommend). As I read, I learned that the Xbox’s disc verification involves the host (in most cases, an Xbox) answering an encrypted series of challenges at the drive level. This process, which is unique to each Xbox disc, uses SHA-1 hashes and RC4 encryption. This is a pretty cool and fascinating way to hide Xbox game data from non-Xboxes—it’s definitely worth checking out the details.

As one does on a Friday evening, I found myself clicking through to the Wikipedia entry on SHA-1. Not much time later, I was deep in the Wikipedia rabbit hole, ultimately landing on the page describing the MD5 message digest algorithm. Those of you reading this who have at least a passing familiarity with cryptography might recognize where this is going based on my description of CreateDigest's behavior above. I did not. 😛


According to Wikipedia, MD5 was designed in 1991 by Ronald Rivest, one of the inventors of the RSA cryptosigning algorithm used by the Pippin. MD5 was designed to replace an earlier version, MD4, which traded better security for increased performance. At a basic level, MD5 takes a bitstring of arbitrary length—the "message"—and generates a 128-bit string that uniquely identifies this input, called a "digest." The input string is padded to a multiple of 512 bits by adding a 1 bit, a number of zero bits, and finally the size of the original message in bits, stored as a little-endian 64-bit value. The padded message is finally split into chunks of 16 longwords, and these 64-byte chunks are then passed into MD5's core hash function to be added to a final 16-byte digest. If that sounds confusing, here's a summary: MD5 takes an arbitrary-sized message and turns it into a unique fixed-size message. The same input will result in the same output, but no two distinct inputs will result in the same output (this isn't 100% true, but for the purpose of this discussion we'll pretend it is).

On the surface, MD5 sounded like it might be what Apple adopted as the Pippin's digest algorithm, but I knew I'd hit paydirt when I instantly recognized the "magic numbers" used in its reference implementation. What's more, the Transform function looked almost exactly the same as the C code I derived from that unnamed subroutine in 'rvpr' 0, and the MD5Update function likewise performs the same steps as the 68K routine that calls into Transform. I am confident that Apple licensed this particular implementation for use in the Pippin. It follows the MD5 specification to the letter, even going so far as endian-swapping the input longwords from the Pippin's native big-endianness.

See? They match!

Armed with the knowledge that MD5 is the message digest algorithm used in the Pippin authentication process, it is clear that the digests computed in CreateDigest, and the digests read from the PippinAuthenticationFile used in CompareDigests, are themselves not signed with RSA. In fact, RSA is not involved with verifying chunks of the disc at all. This tells me that the only thing RSA is used for in the authentication process is for verifying the signature at the end of the PippinAuthenticationFile.

The signature, to recap, is 45 bytes long and lives at the end of the PippinAuthenticationFile. Before entering the main loop, 'rvpr' 0 makes a call to VerifyDigestInfo, which in turn makes a call to VerifySignature. VerifySignature calls upon MD5 to digest the "message" portion of the PippinAuthenticationFile—everything but the signature. It then must use RSA to decrypt and verify the signature against that MD5 digest. If it does, we know the chunk hashes therein can be trusted, so RSA is no longer needed. Otherwise, we know the PippinAuthenticationFile has been tampered with in some way.

Diagram credit: Tommy Yune
This diagram took way too long to make.

Let's say for illustration's sake that the PippinAuthenticationFile is 64K and the last 1K is the signature. When the signature is decrypted, it should contain a digest of that first 63K. If we digest that first 63K ourselves and the two match, we're verified. The whole process is... pretty modern, actually, when you consider this was 1995. 🙂

Using the "Macintosh on Pippin" CD (a.k.a. "Tuscon") as a test case, I stepped through VerifySignature to obtain the MD5 digest of the authentication file's "message:" AE 1A EC AE A4 C5 11 68 2E 38 7D D1 48 F0 55 C2. With this in mind, I set out to test my hypothesis and hopefully find our computed MD5 digest of the message portion somewhere in memory. If I could find this, I could work backwards and reveal how the signature is decrypted. Apple indicated in a Pippin technote that RSA was licensed for their authentication software library. Whether this means Apple used the library as-is, or licensed the code to augment for their own needs, I wanted to verify this one way or the other. VerifySignature makes two unnamed calls before cleaning up and returning a result:

668   B883              Cmp.L     D3, D4          ; is the remaining bytes to hash greater than 16K
66A   6C02              BGE.B     (* + $4)        ; then hash another 16K
66C   2604              Move.L    D4, D3          ; else hash the remaining bytes (which will be < 16K)
66E   4878 0000         Pea.L     ($0)
672   2F03              Move.L    D3, -(A7)       ; push size as bytes to hash (typically 16K until the last chunk)
674   2F0A              Move.L    A2, -(A7)       ; push ptr to start of chunk to hash
676   2F2E FFFC         Move.L    -$4(A6), -(A7)  ; -$4(A6) -> hash object
67A   4EB9 0000 816C    Jsr       ($816C).L       ; create digest of 16K chunk in hash object
680   2A00              Move.L    D0, D5          ; (don't know what is returned in D0, I think a size?)
682   9883              Sub.L     D3, D4          ; remaining bytes -= how many bytes we just hashed (typically 16K)
684   D5C3              AddA.L    D3, A2          ; A2 -> next chunk to hash
686   4FEF 0010         Lea.L     $10(A7), A7     ; cleanup stack
68A   4A84              Tst.L     D4              ; are there any remaining bytes left?
68C   6EDA              BGT.B     (* + -$24)      ; keep hashing
68E   4878 0000         Pea.L     ($0)
692   4878 0000         Pea.L     ($0)
696   2F2E 001C         Move.L    $1C(A6), -(A7)  ; $1C(A6) == $2D (size of signature?)
69A   2F2E 0018         Move.L    $18(A6), -(A7)  ; $18(A6) -> second longword in data block after hashes in auth file (start of signature?)
69E   2F2E FFFC         Move.L    -$4(A6), -(A7)  ; -$4(A6) -> hash object
6A2   4EB9 0000 81A2    Jsr       ($81A2).L       ; probably decrypt the signature?
6A8   2A00              Move.L    D0, D5
6AA   4FEF 0014         Lea.L     $14(A7), A7     ; cleanup stack

I knew that the first call at $816C computes the MD5 digest of the PippinAuthenticationFile. I intuited that in order to determine whether verification succeeds, whatever occurs in the second call at $81A2 must accomplish that. Therefore, the public key must exist in memory at some point during $81A2's execution. In addition, the signature bytes must exist in memory at the same time. If I drill down into $81A2 until I find the signature bytes in RAM, I should find clues as to what data is used to decrypt it based on the proximity of what's changed to what hasn't, thanks to how I implemented my "heap."

$81A2 eventually makes its way to a subroutine at $1B0E, wherein I found the following:

1B58   2F0B              Move.L    A3, -(A7)    ; push nullptr?
1B5A   4878 0000         Pea.L     ($0)         ; push 0
1B5E   2F04              Move.L    D4, -(A7)    ; push size of signature?
1B60   2F05              Move.L    D5, -(A7)    ; push address of signature?
1B62   42A7              Clr.L     -(A7)        ; push 0
1B64   486E FF78         Pea.L     -$88(A6)     ; push ptr to area on stack
1B68   4878 0000         Pea.L     ($0)         ; push 0
1B6C   486A 0028         Pea.L     $28(A2)      ; push ptr to $10F0 in hash object?
1B70   4EB9 0000 12E0    Jsr       ($12E0).L    ; jump somewhere that copies the signature to a working buffer
1B76   2600              Move.L    D0, D3       ; D0 == result code
1B78   4FEF 0020         Lea.L     $20(A7), A7
1B7C   6600 00AE         BNE       (* + $B0)    ; if it's nonzero, bail
1B80   2F0B              Move.L    A3, -(A7)    ; push nullptr?
1B82   4878 0000         Pea.L     ($0)         ; push 0
1B86   4878 0040         Pea.L     ($40)        ; push 64
1B8A   486E FF98         Pea.L     -$68(A6)     ; push ptr to somewhere on stack
1B8E   486E FFA8         Pea.L     -$58(A6)     ; push ptr to somewhere else on stack
1B92   486A 0028         Pea.L     $28(A2)      ; push ptr to $10F0 in hash object?
1B96   4EB9 0000 13D8    Jsr       ($13D8).L    ; jump somewhere, get processed data we care about on stack

$12E0 copies our signature from the PippinAuthenticationFile into a working buffer shortly after a copy of part of the data block in ROM passed to 'rvpr' 0 upon initial invocation. Could the "processed data" coming from the subroutine at $13D8 be our decrypted signature? I took a look at the memory before and after the call, and...

Do you see it?

Look at memory location $20451.

When I saw it, I gasped. There it is. There's our decrypted digest.

I wasn't as lucky with the RSA code as I was with MD5—neither the reference implementations 1.0 nor 2.0 of RSA have portions that appear in this code, but they do answer the question of the signature format. The bytes appearing before our decrypted digest are a header consisting mostly of magic numbers and some $FF padding bytes, but with a lonely "05" byte at address $2044C, or offset 24 into our decrypted signature. This byte's value indicates that the digest is an MD5 digest, just like the reference implementation specifies.

That completed my understanding of the format of the PippinAuthenticationFile, leaving only one final piece of the puzzle: what and where the public key is. The public key must come from somewhere, but at this point I hadn't yet determined the purpose of the data passed in from ROM to 'rvpr' 0...

RSA (in which I dust off my math minor)

The RSA algorithm for cryptosecurity, invented in 1977 by Ronald Rivest, Adi Shamir, and Leonard Adleman, is built upon the notion that factoring large semiprime numbers is considered a hard problem. Not impossible, but very hard. Finding these primes can take a computer or cluster of computers a significant amount of time, proportional to the size of the semiprime to factor. Mathematically, it involves a few steps but can be implemented using basic algebraic concepts.

First, find two numbers P and Q such that they are both prime, meaning that both P and Q can only be divided neatly by themselves and one. Let's use P = 19 and Q = 17 as examples.

Next, compute P \cdot Q. 19 \cdot 17 = 323 in our example case. Call this N.

Now we need to calculate \lambda. We do this by computing (P - 1) \cdot (Q - 1). (19 - 1) \cdot (17 - 1) = 18 \cdot 16 = 288.

We need to choose a value for e such that e and \lambda are coprime; that is, \lambda is not neatly divisible by e. The smallest value that works here is 5, so we'll use that in our example case, but typically a larger value is used with a small amount of 1 bits in its binary representation.

Finally we need to find the value of D. D can be found by solving the equation D \cdot e \equiv 1 \pmod \lambda. We do this by using the extended Euclidean algorithm.

All we do here is integer divide \lambda by e and also take \lambda \mod e; that is, we divide \lambda by e with remainder. Then repeat this process with the results until the remainder equals one: divide the divisor by the remainder from the previous calculation. For example, start with \lambda \div e:

\begin{aligned}  288 \div 5 &= 57\text{ remainder }3 \\  288 &= (57 \cdot 5) + 3  \end{aligned}

Take the divisor from that and divide by the remainder:

\begin{aligned}  5 \div 3 &= 1\text{ remainder }2 \\  5 &= (1 \cdot 3) + 2  \end{aligned}

One more time:

\begin{aligned}  3 \div 2 &= 1\text{ remainder }1 \\  3 &= (1 \cdot 2) + 1  \end{aligned}

Once we have a remainder of one, we've found the greatest common divisor and so we need to build back up to find our value for D. Do so by substituting our results until we have a linear combination of \lambda and e (288 and 5, respectively):

\begin{aligned}  1 &= 3 - (1 \cdot 2) \\  &= 3 - 1 \cdot (5 - (1 \cdot 3)) \\  &= 3 - 5 + 3 \\  &= 2 \cdot 3 - 1 \cdot 5 \\  &= 2 \cdot (288 - (57 \cdot 5)) - 1 \cdot 5 \\  &= 2 \cdot 288 - 114 \cdot 5 - 5 \\  &= 2 \cdot 288 - 115 \cdot 5  \end{aligned}

Since we need to satisfy D \cdot e \equiv 1 \pmod \lambda, we ignore \lambda's coefficient here, leaving e with a coefficient of -115. \lambda equals 288 (as we calculated earlier), so -115 \mod 288 is 173, giving us our value for D.

We now have everything we need to sign and verify messages. Our "private key" is our values for D and N—messages signed with the private key can only be verified by someone with our "public key". Our "public key" is our values for e and N—only our public key can verify messages signed by our private key, assuring our recipient that the signed message indeed comes from us and can be trusted.

Let's say we want to send someone the answer to the ultimate question, but we want to sign it first in case our message gets intercepted by Vogons. 😛 Call the original message M with the value 42, and here we'll calculate the signature S. We do so using our values for D and N:

\begin{aligned}  S &= M^{D} \mod N \\  S &= 42^{173} \mod 323 \\  &= 111  \end{aligned}

When our recipient receives our message, they will need to verify our signature in order to be sure it can be trusted and that it has not been tampered with by any poetic aliens. 😉 They do so using our values for e and N:

\begin{aligned}  M &= S^{e} \mod N \\  M &= 111^{5} \mod 323 \\  &= 42  \end{aligned}

If the signature matches the original message (as it does here in our example), the message arrived safely intact.

Notice that the values for P, Q, e, \lambda, N, and D are all relatively small—the largest of these, N, only needs nine bits for its binary representation. You could therefore say that we used a 9-bit key in our above example. Furthermore, notice that our message 42—and its signature 111—also fit inside of nine bits. This is a property of RSA: a key of bit length X can only operate on a message with a maximum bit length also of X.

The signature size as defined in the PippinAuthenticationFile is 45 bytes, suggesting that the Pippin's public RSA key is at least 360 bits long (45 * 8 bits per byte). Recall from earlier that although the RSA public key is still unknown, it must come from somewhere in the ROM, and it is still at this point unclear what purpose the blocks of data passed to 'rvpr' 0 serve.

I found part of one of the blocks in RAM near where the decrypted signature is: 45 bytes, same as the signature. I also found a nearby value of 0x10001, or 65537, which seems to be a popular choice for the value of e. Hmm. Interesting.

I found another block of memory also nearby containing the same data, but reversed by 16-bit words. Hmm. Interesting.

Wonder what the odds are... 😉

It was reasonable to hypothesize that one of these blocks contained the public key. There are plenty of web pages out there that explain RSA using examples, some with implementations in JavaScript allowing someone to plug in their own keys, messages, and signatures. I tried the nearest 45-byte "key" in one such website with the raw 45-byte signature from the PippinAuthenticationFile and...

It didn't work. The "decrypted" signature didn't match at all. Garbage in, garbage out. Cue sigh of disappointment.

I had one data block left, and with little hope remaining...


I'd found it.

Apple's public key for verifying the signature on a PippinAuthenticationFile is:
E0 E0 27 5C AB 60 C8 86 A3 FA C2 98 21 79 54 A8 9F D1 B9 DC 8A BA 84 EF B1 E7 C9 E2 1B F7 DD D7 DC F0 E4 4A BB 79 51 0E 7C EB 80 B1 1D

... and I didn't even have to look at very much code. 😀

Cracking RSA

I just have to crack RSA now, right?

Fortunately, available tools make that a much less daunting prospect than popular media contemporary with the Pippin suggested. There was even an ongoing RSA Factoring Challenge for a while until 2007. Back then though, it was a different story. The Open Source Initiative had yet to be founded. Prime factoring was done mostly in isolation by dedicated teams with access to massive amounts of computing power (for the time). A 364-bit decimal number took a two-person team and a supercomputer about a month to factor in 1992.

But this isn't 1992 anymore. The computers on our desks and in our pockets have more than enough number-crunching power to factor the Pippin's public key. Today, with some freely available open source software and a typical desktop PC, a 360-bit key can be factored in a matter of hours. And thanks to the efforts of several open source projects within recent years, we have a little tool to help us called msieve. 😀

msieve is very user-friendly. 😉 You pass the number you want to factor as its only command line argument and it just goes. It even saves its progress to disk, just in case it's a Really Big Number and something terrible happens like a power outage or something.

msieve took 18 hours, 34 minutes, and 4 seconds on my i7 Intel NUC to find two prime factors P and Q of the Pippin's public key:

Hard part's over. 😀

Let's plug these into our RSA formulas from above and find Apple's private key, shall we?

P = 0F 2D 25 BF 3C 5B 70 28 72 6E 49 75 3F D5 62 67 11 37 38 94 51 EF D7
Q = 0E D1 47 5D E1 92 41 28 59 2C 4B 3E 47 4E 5F C1 23 1F 1B AF A0 D8 2B
e = 0x10001
N = P \cdot Q = E0 E0 27 5C AB 60 C8 86 A3 FA C2 98 21 79 54 A8 9F D1 B9 DC 8A BA 84 EF B1 E7 C9 E2 1B F7 DD D7 DC F0 E4 4A BB 79 51 0E 7C EB 80 B1 1D (this is the public key—we know this from stepping through 'rvpr' 0 and examining memory)
\lambda = (P - 1) \cdot (Q - 1) = E0 E0 27 5C AB 60 C8 86 A3 FA C2 98 21 79 54 A8 9F D1 B9 DC 8A BA 66 F1 44 CA AB F4 6A A7 12 3D 48 3D 5D 26 F9 51 1C B8 28 A7 8D E9 1C
D \equiv e^{-1} \pmod \lambda = 01 1C D3 AD E7 99 86 67 D6 E9 E2 17 11 DB EC 33 07 B6 0E 4D 6D 03 26 20 77 5D DB 9B 3B 64 CF 22 B2 0E 4A F3 2F 07 40 EE B0 6F 85 F2 A0 1D

That last value for D should be Apple's private signing key. Now let's verify, using the same JavaScript RSA calculator I found on the Web. As a test case, let's again use the PippinAuthenticationFile from the "Macintosh on Pippin" CD (a.k.a. "Tuscon").

Its first four bytes indicate a message size of $FD4F, or 64847 bytes. The MD5 digest of those first 64847 bytes is AE 1A EC AE A4 C5 11 68 2E 38 7D D1 48 F0 55 C2.

It has the following signature:
5A 90 36 69 DD 06 F5 15 EF 7A A2 04 5D 24 C2 CA 3C DD 2E C3 85 7D BB B8 9C 53 78 24 65 CC F0 0A 52 09 20 76 E1 9D F7 CC B3 C6 6D 7E AF

When decrypted, we get:
00 01 FF FF FF FF FF FF FF FF 00 30 20 30 0C 06 08 2A 86 48 86 F7 0D 02 05 05 00 04 10 AE 1A EC AE A4 C5 11 68 2E 38 7D D1 48 F0 55 C2

Notice that the last 16 bytes match our computed MD5 digest.

Finally, if we take the decrypted signature and re-sign it using what we think is Apple's private key, we get:
5A 90 36 69 DD 06 F5 15 EF 7A A2 04 5D 24 C2 CA 3C DD 2E C3 85 7D BB B8 9C 53 78 24 65 CC F0 0A 52 09 20 76 E1 9D F7 CC B3 C6 6D 7E AF

... which matches the original signature found in the PippinAuthenticationFile.

Mr. Hammond, I think we're in business. 😀
The RSA keys used in the signing and verification of a PippinAuthenticationFile are 360 bits long.

Apple’s public key for verifying the PippinAuthenticationFile is:
E0 E0 27 5C AB 60 C8 86 A3 FA C2 98 21 79 54 A8 9F D1 B9 DC 8A BA 84 EF B1 E7 C9 E2 1B F7 DD D7 DC F0 E4 4A BB 79 51 0E 7C EB 80 B1 1D

Apple’s private key for signing a PippinAuthenticationFile is:
01 1C D3 AD E7 99 86 67 D6 E9 E2 17 11 DB EC 33 07 B6 0E 4D 6D 03 26 20 77 5D DB 9B 3B 64 CF 22 B2 0E 4A F3 2F 07 40 EE B0 6F 85 F2 A0 1D

There you go, Internet. We now have all the information we need to sign and boot our own Pippin media.

Exploring the Pippin ROM(s), part 6: Back in the ‘rvpr’

(How’s that for a punny subtitle?)

It’s been a while. I haven’t lost interest, I’ve just been busy with work and other things. Life has a funny way of sneaking up on you. 😉

The wait is worth it, though. Buckle up; this one’s a doozy.

Apple hasn’t documented the Pippin’s authentication process beyond what developers needed to know. There exists a technote that was distributed via the SDK(s) that gives an overview of what developers were expected to do to get their discs signed before final mastering and duplication. The Pippin’s authenticated boot process hinges upon the presence of a specially-crafted, RSA-signed file unique to each disc called the “PippinAuthenticationFile.” Since the Pippin platform was abandoned and subsequently cancelled in 1998, Apple no longer signs Pippin discs nor have they made available the means for third parties to do so. To my knowledge, most of the specifics of how the PippinAuthenticationFile plays a role in the Pippin’s boot process have never been documented outside of Apple.

That changes today.

This post is pretty dense, so I highly recommend (re-)reading parts 1 through 4 for some background before getting too deep. Otherwise, here’s a quick recap: during every boot, a retail Pippin console locates a potential boot volume on CD, loads an ‘rvpr’ 0 resource from ROM, then calls the code therein in order to verify that the target volume passes an authentication check allowing it to boot the system. (An aside: Previously, I asserted that while I found identical copies of ‘rvpr’ 0 in the 1.2 and 1.3 ROMs, I couldn’t find an entry for it in the resource map, therefore it must either be dead code or called some other way. This conclusion turned out to be incorrect—the resource map is not contiguous in ROMs 1.2 and 1.3, which made manually searching it more difficult, but it does indeed contain an entry for ‘rvpr’ 0. The authentication process is therefore identical between ROM 1.0 and 1.2.) When I last looked at ‘rvpr’ 0, I was stymied by a routine called upon entry which, absent of any symbols to help point me toward its purpose, I conjectured used a complex block of data at the end of the resource to “decrypt” the code therein. After taking a closer look a few days ago, I was delighted to find that its purpose is much simpler—it exists to patch the absolute memory locations in the code so they are relative to the buffer where ‘rvpr’ 0 is loaded. Without these patches, the code would crash the Pippin on boot practically every time!

The way this routine accomplishes this is kind of elegant. We initialize a cursor pointer to the beginning of our buffer where ‘rvpr’ 0 is loaded. The offset table starting at offset $8A47 from the start of ‘rvpr’ 0 begins with a 32-bit longword defining the size of the table. Then, the table itself is compressed: a byte with bit 7 set means it’s a relative sign-extended 7-bit offset from our cursor position, a byte with bit 6 set means it along with the next byte form a sign-extended 14-bit offset from our cursor position, but if both bit 6 and 7 are clear, then combine the next three bytes to form a 30-bit absolute cursor position. Multiply these offsets by two before applying them (because 68K opcodes are always at least two bytes), add the address of our ‘rvpr’ 0 buffer to the 32-bit longword pointed to by our cursor, then repeat the process until we’ve exhausted the offset table. Easy peasy.

5E   1218         Move.B   (A0)+, D1   ; grab the next byte into D1, we'll call it the command byte
60   1001         Move.B   D1, D0
62   0240 0080    AndI     #$80, D0    ; is bit 7 set?
66   670C         BEQ.B    @checkBit6  ; then handle the bit 6 case

; command byte bit 7 is set, so
; D2 += signExtend(D1 * 2) as a byte (* 2 because alignment)
68   D201         Add.B    D1, D1
6A   1001         Move.B   D1, D0
6C   4880         Ext      D0
6E   48C0         Ext.L    D0
70   D480         Add.L    D0, D2
72   6028         Bra.B    @gotOffset

; else, command byte bit 7 not set...
74   1E81         Move.B   D1, (A7)         ; put D1 into the highest byte of temp
76   1F58 0001    Move.B   (A0)+, $1(A7)    ; grab the next byte into the 2nd byte of temp
7A   1001         Move.B   D1, D0
7C   0240 0040    AndI     #$40, D0         ; is bit 6 of D1 set?
80   670C         BEQ.B    @get32BitOffset  ; yes? then

; command byte bit 6 is set, so
; our address offset is only 14 bits
82   3017         Move     (A7), D0    ; grab the new temp into D0
84   E548         LsL      #2, D0      ; D0 <<= 2
86   E240         AsR      #1, D0      ; D0 /= 2
88   48C0         Ext.L    D0          ; sign extend it
8A   D480         Add.L    D0, D2      ; D0 is the found offset * 2 (because alignment), add to our current offset
8C   600E         Bra.B    @gotOffset  ; apply it

; bit 6 not set...
8E   1F58 0002    Move.B   (A0)+, $2(A7)  ; grab the next byte into the 3rd byte of temp
92   1F58 0003    Move.B   (A0)+, $3(A7)  ; grab the next byte into the 4th byte of temp
96   2417         Move.L   (A7), D2       ; D2 is a brand new offset!
98   E58A         LsL.L    #2, D2         ; D2 <<= 2
9A   E282         AsR.L    #1, D2         ; D2 /= 2

; D2 == the offset we want to apply to argument 2
; D6 == the offset we want to apply to the longword found there (typically @start)
9C   DDB1 2800    Add.L    D6, $0(A1,D2.L)  ; add D6 to the longword at (@start + D2)
A0   5385         SubQ.L   #1, D5
A2   4A85         Tst.L    D5               ; are we out of longs to patch?
A4   6EB8         BGT.B    @loopBody

Now that we've "unpacked" the code of 'rvpr' 0, let's dig into it. 🙂

main starts off initializing a number of globals, first by calling InitRSAAlgorithmChooser and then a handful of other subroutines. It then initializes some local variables on the stack: previous A4, values related to the PippinAuthenticationFile, and a ParamBlockRec for calls to _Read. In addition, among those locals is a 16-byte temporary buffer for digests created during the main loop.

Recall from part 2 that the Pippin ROM passes as input to 'rvpr' 0 the following arguments: two pointers to some as-of-yet-unknown data in ROM shortly preceding the callsite, the ID of the boot volume candidate, and the refNum of the candidate's disk driver. After we've initialized our variables, we hit the ground running by calling GetVolAuthFileInfo to fetch the offset to and size of the PippinAuthenticationFile. Note that if at any point during 'rvpr' 0 one of its internal subroutines fails, the entire process is reported as having failed the authentication check.

24C   41EE FFAA         Lea.L     -$56(A6), A0    ; A0 -> temp var for created digests
250   2E08              Move.L    A0, D7          ; D7 == ptr to temp digest
252   486E FFCA         Pea.L     -$36(A6)        ; pass size out address
256   486E FFBE         Pea.L     -$42(A6)        ; pass offset out address
25A   3F05              Move      D5, -(A7)       ; $10(A6) is dqDrive passed in from ROM
25C   3F2E 0012         Move      $12(A6), -(A7)  ; $12(A6) is dqRefNum passed in from ROM
260   4EB9 0000 03E6    Jsr       GetVolAuthFileInfo
266   3600              Move      D0, D3
268   4A43              Tst       D3
26A   4FEF 000C         Lea.L     $C(A7), A7
26E   6600 0142         BNE       @mainCleanup    ; if GetVolAuthFileInfo returns nonzero, fail
272   202E FFBE         Move.L    -$42(A6), D0    ; D0 = offset from start of HFS volume to PippinAuthenticationFile in allocation blocks
276   7209              MoveQ.L   #9, D1
278   E3A8              LsL.L     D1, D0          ; D0 = offset from start of HFS volume to PippinAuthenticationFile in bytes
27A   2D40 FFC6         Move.L    D0, -$3A(A6)    ; save the offset into -$3A(A6)
27E   202E FFCA         Move.L    -$36(A6), D0    ; D0 = size of the PippinAuthenticationFile in bytes
282   A11E              _NewPtr
284   2648              MoveA.L   A0, A3
286   200B              Move.L    A3, D0
288   4A80              Tst.L     D0
28A   660E              BNE.B     @gotAuthBuffer  ; if _NewPtr returns null, clean up the stack, and fail
28C   554F              SubQ      #2, A7
28E   3EB8 0220         Move      (MemErr), (A7)
292   301F              Move      (A7)+, D0
294   3600              Move      D0, D3
296   6000 011A         Bra       @mainCleanup

29A   3D6E 0012 FFE6    Move      $12(A6), ioRefNum(A6)
2A0   3D45 FFE4         Move      D5, ioVRefNum(A6)         ; dqDrive
2A4   2D4B FFEE         Move.L    A3, ioBuffer(A6)
2A8   2D6E FFCA FFF2    Move.L    -$36(A6), ioReqCount(A6)  ; size of the PippinAuthenticationFile
2AE   3D7C 0001 FFFA    Move      #fsFromStar, ioPosMode(A6)
2B4   202E FFBE         Move.L    -$42(A6), D0              ; offset from start of HFS volume to PippinAuthenticationFile in device blocks
2B8   7209              MoveQ.L   #9, D1
2BA   E3A8              LsL.L     D1, D0                    ; get the offset in bytes by multiplying by device block size (512 bytes)
2BC   2D40 FFFC         Move.L    D0, ioPosOffset(A6)
2C0   41EE FFCE         Lea.L     -$32(A6), A0
2C4   A002              _Read
2C6   3600              Move      D0, D3
2C8   4A43              Tst       D3
2CA   6600 00E6         BNE       @mainCleanup              ; if _Read returns something other than noErr, fail

Following this call, we allocate enough space for the file by calling _NewPtr. We then call _Read with our local ParamBlockRec filled with the disk driver refNum, the volume refNum, the pointer to the buffer we just allocated, our buffer's size, and the byte offset to the authentication file on our candidate. Armed with the contents of the PippinAuthenticationFile, we then pass them to VerifyDigestInfo to verify the signature contained therein. If that succeeds, we're clear to start verifying the candidate's contents against this file, so we allocate temp space large enough to load a single "chunk" of data from the candidate to be hashed and verified.

Every Pippin except the KMP 2000 shipped with a built-in 4x speed CD-ROM drive. A 1x CD-ROM drive can read data at a rate of 150KB/sec, which is the speed necessary for smooth playback of audio CDs. A 2x drive doubles that rate to 300KB/sec, a 4x drive quadruples it to 600KB/sec, and so on. At 600KB/sec, it would take a Pippin almost a full minute to read just over 35MB, and nearly 20 minutes to read the entire contents of a 700MB CD-ROM. Even the KMP 2000 with its 8x drive would take almost 10 minutes to do the same. Hashing the entire contents of a CD during every boot would be unacceptable at this speed, and since the Pippin only takes a couple seconds to verify a disc at startup, it's clearly not verifying the whole thing. So what does the Pippin do?

320   4A86               Tst.L     D6
322   6604               BNE.B     @pickRandomChunk
324   7800               MoveQ.L   #0, D4
326   6016               Bra.B     @readChunk

328    202B 004C         Move.L    $4C(A3), D0  ; longword after the 128K size field at $48, appears to be number of entries in table
32C    5380              SubQ.L    #1, D0
32E    2F00              Move.L    D0, -(A7)    ; upper bound == total number of chunks
330    4878 0001         Pea.L     ($1)         ; lower bound == 1
334    4EB9 0000 0814    Jsr       RangedRand   ; patched
33A    2800              Move.L    D0, D4       ; D4 == pseudorandom integer between [1, <total number of chunks>]?
33C    504F              AddQ      #8, A7       ; clean up stack

33E    2004              Move.L    D4, D0               ; D0 == pseudorandom integer in the lowword, probably
340    2205              Move.L    D5, D1               ; D1 == 128K? 0 x 0002 0000
342    4EB9 0000 0116    Jsr       _D0timesD1           ; patched, does some weird multiplication, returns in D0
348    2D40 FFFC         Move.L    D0, ioPosOffset(A6)  ; D0 == the pseudorandom integer * 128K? D0 == offset to random 128K chunk in disc?
34C    41EE FFCE         Lea.L     -$32(A6), A0
350    A002              _Read
352    3600              Move      D0, D3
354    4A43              Tst       D3
356    665A              BNE.B     @mainCleanup

Put simply, the Pippin randomly spot-checks the candidate volume's contents every boot. The PippinAuthenticationFile isn't just a key, it isn't just a single hash—it is in fact a collection of hashes corresponding to as many 128K chunks of data that make up the boot volume. main enters a loop that iterates six times: the first check, it loads the first 128K of the volume containing important metadata about the HFS filesystem into our temporary buffer, and then verifies that data against its corresponding digested hash previously loaded from the PippinAuthenticationFile. The remaining five checks, it does the same, but on randomly selected other 128K chunks of the volume. This way, the Pippin only has to load and verify 768K—a process that takes less than a couple seconds on its 4x CD-ROM drive. But because this loop selects five of those six input chunks at random each run-through, the PippinAuthenticationFile still needs digests of the entire volume. For it's not known ahead of time which five chunks will be verified and furthermore, they rarely will be the same five chunks.

Examining several PippinAuthenticationFile examples with this code in mind quickly reveals how this file is structured. Both the chunk size and the total number of chunks in the volume are stored in a common header. This loop uses that information to determine the upper bound of which chunks to select at random and how large. Following these two fields is a table of digested 128-bit hashes corresponding, in sequential order, to the chunks in the volume. Finally, there is a signature near the end of the file, which gets verified in the call to VerifyDigestInfo before entering the loop. The process by which a PippinAuthenticationFile is created, therefore, is essentially as follows:

  1. Get the size of the target volume.
  2. Integer divide this size into 128K chunks. Call the total number of chunks N.
  3. Allocate 80 bytes for a file header.
  4. Multiply N chunks by the 16-byte size of each digest (N * 16). Call this table size T. Allocate T bytes for digests of each 128K chunk.
  5. Allocate 16 bytes for the signature size S.
  6. Pad the signature size until it is a multiple of 16 bytes (((S + 16) % 16) *16). Call this padded size P. Allocate P bytes for the signature itself.
  7. Pad additional bytes until the total file size is the next multiple of the device block size (512 bytes). The total file size therefore should be ((80 + T + 16 + P + 512) % 512) * 512.
  8. Preallocate a blank version of this file on the target volume.
  9. Starting at offset 80, compute and store a 16-byte (128-bit) digest for each of the N sequential 128K chunks. Note that to compute the digests for the entire finalized volume correctly, this file must already exist in the filesystem. It is therefore necessary to compute the size of this file in advance, preallocate a "dummy" version of it on the target volume, then compute the digests and overwrite the file in-place.
  10. At offset 80 + T + 15, store the signature size S as a byte (only one byte at the end of this space is actually used, the rest are zeroes).
  11. At offset 80 + T + 16 + 3, store the signature itself. The signature always seems to be 45 bytes long, placed such that it ends on a 16-byte boundary, explaining the extra 3-byte offset.
  12. Fill in the file header at the beginning of the file:
    • offset 0 (4 bytes): offset to signature size byte (80 + T + 15)
    • offset 4 (4 bytes): longword equal to zero (version?)
    • offset 8 (64 bytes): copyright notice (60 bytes, zero-padded right)
    • offset 72 (4 bytes): chunk size longword equal to 128K, or $20000
    • offset 76 (4 bytes): chunk count longword equal to N

Apple probably provided a tool that automated this process for stamping houses. Said tool presumably would have named the aforementioned file "PippinAuthenticationFile" with type/creator 'PpnV'/'PpnA' and saved it to the filesystem root. I imagine that this same tool likely would have filled the file's contents in-place with the signed version received from Apple. However, I have never seen such a tool in the wild so this is pure speculation on my part.

Incidentally, the name, placement within the folder hierarchy, and type/creator codes of the authentication file itself are inconsequential. The Pippin makes no HFS calls to locate the PippinAuthenticationFile—it could technically be buried within a nest of folders or named "FoobarAuthenticationFile." The verification code does not care. Instead, it fetches the Master Directory Block—512 bytes located at byte offset 1024 from the start of the boot volume. The "logical" MDB is a data structure 161 bytes in size and found immediately at the start of this "physical" MDB. However, that leaves 351 bytes unaccounted for. For Pippin CD-ROMs, Apple chose to set aside two 32-bit longwords at the end of the physical MDB for the purpose of locating the PippinAuthenticationFile at the block level. The first of these longwords defines the offset, in 512-byte blocks from the start of the volume, to the contents of the authentication file. The second of these longwords define the authentication file's size in bytes.

(As an aside, this mechanism is one reason why deleting the PippinAuthenticationFile and naively replacing it with a new version at the filesystem level is not likely to work. The new file would likely reside starting at a different allocation block in the volume; the offset in the MDB would still point to where the deleted file was/is, and HFS wouldn't know to patch it up—why should it?)

One important component of creating the authentication file, and verifying against it, is the concept of chunk "cleansing." Once the loop selects and loads a chunk, it passes it to CleanseInputChunk to optionally "cleanse" it. What does that mean in this context?

358   2F2E FFCA         Move.L    -$36(A6), -(A7)         ; size of the PippinAuthenticationFile in bytes
35C   2F2E FFC6         Move.L    -$3A(A6), -(A7)         ; offset from start of HFS volume to PippinAuthenticationFile in bytes
360   2F05              Move.L    D5, -(A7)               ; 128K
362   2F2E FFFC         Move.L    ioPosOffset(A6), -(A7)  ; the computed offset
366   2F0A              Move.L    A2, -(A7)               ; working chunk buffer
368   4EB9 0000 049A    Jsr       CleanseInputChunk       ; patched, remove the auth file from this chunk if we happened to land on it

The digested hashes contained within the authentication file do not include hashing the authentication file itself, for obvious reasons. Similarly, certain fields in the MDB should not be hashed because they change upon writing the final signed authentication file to the volume. "Cleansing" solves this problem by zeroing out these areas before hashing and digesting them. While creating an authentication file, the blocks of the volume containing the file itself should be zeroed out when hashing, and likewise most of the MDB. Upon loading a chunk, the verification loop checks its offset to see whether the chunk overlaps the MDB or the authentication file. If so, it zeroes out that data so that the digested hash matches the corresponding one stored in the authentication file.

I was going to recreate this diagram in Illustrator, but then I remembered I suck at Illustrator.

Finally, the loop passes the chunk to CreateDigest, and compares its result byte-for-byte with the digest in the authentication file by calling CompareDigests. If all six digested chunks match what's found in the authentication file, we pass the check (and can boot from this volume! Yay!). Otherwise, we return -1 to indicate failure.

36E   2F07              Move.L    D7, -(A7)       ; D7 -> out buffer for temp digest
370   2F05              Move.L    D5, -(A7)       ; chunk size (128K)
372   2F0A              Move.L    A2, -(A7)       ; cleansed working chunk buffer
374   4EB9 0000 06EE    Jsr       CreateDigest    ; patched
37A   3600              Move      D0, D3
37C   4A43              Tst       D3
37E   4FEF 0020         Lea.L     $20(A7), A7     ; clean up stack
382   662E              BNE.B     mainCleanup
384   2F07              Move.L    D7, -(A7)       ; D7 -> the digest we just created
386   2004              Move.L    D4, D0          ; D4 == which chunk this is
388   E988              LsL.L     #4, D0          ; D4 == chunk * 16 bytes (128 bit hash per chunk)
38A   206E FFC2         MoveA.L   -$3E(A6), A0    ; A0 -> chunk hashes
38E   D1C0              AddA.L    D0, A0          ; A0 -> hash of this chunk
390   4850              Pea.L     (A0)
392   4EB9 0000 07D4    Jsr       CompareDigests  ; patched, returns zero in D0 if digests match
398   7200              MoveQ.L   #0, D1
39A   1200              Move.B    D0, D1
39C   3601              Move      D1, D3
39E   4A43              Tst       D3
3A0   504F              AddQ      #8, A7          ; clean up stack
3A2   6704              BEQ.B     @nextMainForLoopIteration
3A4   76FF              MoveQ.L   #-1, D3         ; D3 == -1, fail
3A6   600A              Bra.B     mainCleanup

3A8    5286              AddQ.L    #1, D6

3AA    7006              MoveQ.L   #6, D0
3AC    BC80              Cmp.L     D0, D6
3AE    6D00 FF70         BLT       @topOfMainForLoop

So what's left?

Of the named functions I found in part 2, only four of them I have yet to step through and understand:

main done
GetVolAuthFileInfo done
CleanseInputChunk done
VerifyDigestInfo in progress
VerifySignature to do
CreateDigest in progress
CompareDigests done
RangedRand done
CleanseVCB done
GetVAFileInfoGivenMDB done
InitRSAAlgorithmChooser in progress

Astute readers may notice that I have provided sparse details about the functions related to dealing with digests and signatures so far. Those are next on my list but will also be the hardest to grok because I'll be more or less "flying solo" without any symbols whatsoever to guide me. Fortunately I have a passing familiarity with the RSA algorithm so I have a vague idea of what logic to look for. I have already found functions for 32-bit multiply and 32-bit modulo, both of which are essential for RSA.

In part 4 I explored hacking the Apple Partition Map to load custom disk drivers before the authentication check takes effect. I was not successful with that experiment and decided not to pursue it further, but my discoveries here reveal a possible alternate avenue to explore: additional partitions. The authentication check is performed on the boot volume, and only the boot volume (emphasis on each word). The partition map is not included in the check, nor are any other partitions. It should be totally possible to take an existing signed Pippin disc with unpartitioned free space available (for example, the "Tuscon" disc) and graft an additional HFS partition containing whatever apps or documents one might want. The OS should mount the other partition as it would normally, without performing any checks beyond what would be done on a real Mac.

I leave that as an exercise for the reader. I'm going after the big fish: authoring an entire homebrew disc from scratch.

Exploring the Pippin ROM(s), part 5: Open Firmware

According to the NetBSD/macppc FAQ, Open Firmware “is part of the boot ROMs in most PowerPC-based Macintosh systems, and we use it to load the kernel from disk or network.”

Turns out, “most PowerPC-based Macintosh systems” happens to include the Pippin. If you have the rare keyboard/tablet (or an ADB keyboard via the AppleJack dongle) attached and hold down Command-Option-O-F at startup, the Pippin boots to an Open Firmware prompt. However, you won’t see anything on screen because it outputs to a serial console by default; specifically, all console I/O is handled through the GeoPort. My Mac Plus happens to sit next to my Pippin, so tonight I temporarily switched my ImageWriter II’s cable over, booted both machines, and fired up ZTerm.

The following is what I discovered.

Open Firmware, PipPCI.
To continue booting the MacOS type:
To continue booting from the default boot device type:
0 > dev / ls
FF829230: /PowerPC,603@0
FF829B28: /chosen@0
FF829C58: /memory@0
FF829DA0: /openprom@0
FF829E60: /AAPL,ROM@FFC00000
FF82A088: /options@0
FF82A528: /aliases@0
FF82A6F0: /packages@0
FF82A778:   /deblocker@0,0
FF82AF78:   /disk-label@0,0
FF82B4B8:   /obp-tftp@0,0
FF82D8F8:   /mac-files@0,0
FF82E0F0:   /mac-parts@0,0
FF82E850:   /aix-boot@0,0
FF82ECC8:   /fat-files@0,0
FF830298:   /iso-9660-files@0,0
FF830BE0:   /xcoff-loader@0,0
FF8315A0:   /terminal-emulator@0,0
FF831638: /aspen@F2000000
FF832900:   /gc@10
FF832D38:     /scc@13000
FF832E90:       /ch-a@13020
FF833540:       /ch-b@13000
FF833BF0:     /awacs@14000
FF833CD8:     /swim3@15000
FF834DE0:     /via-cuda@16000
FF835970:       /adb@0,0
FF835A60:         /keyboard@0,0
FF8361B0:         /mouse@1,0
FF836260:       /pram@0,0
FF836310:       /rtc@0,0
FF8367D8:       /power-mgt@0,0
FF836898:     /mesh@18000
FF838418:       /sd@0,0
FF839048:       /st@0,0
FF839CC8:     /nvram@1D000
FF839DA0: /taos@F0800000
FF839EC8: /aspenmemory@F8000000
0 > dev /openprom  ok
0 > .properties
name                    openprom
model                   Open Firmware, PipPCI.

0 > printenv auto-boot?

auto-boot?          true                true
0 > printenv use-nvramrc?

use-nvramrc?        false               false
0 > printenv real-base

real-base           -1                  -1
0 > printenv load-base

load-base           4000                4000
0 > printenv boot-device

boot-device         /AAPL,ROM           /AAPL,ROM
0 > printenv boot-file

0 > printenv input-device

input-device        ttya                ttya
0 > printenv output-device

output-device       ttya                ttya
0 > printenv nvramrc

0 > printenv boot-command

boot-command        boot                boot
0 > bye

This dump was generated on my @WORLD Pippin with ROM 1.2. Some observations:

  • OF doesn't report a version number, instead reporting "PipPCI" in its place. Searching the ROM for strings reveals "June 28, 1996" as the latest date I could find, so whatever Apple was using in its Power Macs at that time I imagine is what is running here.
  • The ROM is located at 0xFFC00000, which follows what I've seen from hardcoded addresses I've found.
  • "taos" is the video hardware starting at 0xF0800000. I'm not sure offhand if that address is the base of video memory, but I do know from the Pegasus Prime code that taos does allow for writing directly to VRAM.
  • There is a TFTP package(!)-- wonder how it works?
  • The Pippin has a SWIM III chip onboard. There is an official floppy drive expansion dock and an unofficial floppy drive expansion board, both of which appear to be "dumb" hardware that merely connect a drive directly to pins of the Pippin's X-PCI connector on the underside of the system. The drive itself is powered and controlled entirely by hardware already built in to the Pippin. However, as far as I know, the SWIM II and later floppy controllers (including the SWIM III) lack the low-level access necessary for HD20 support, so large drives emulated by hardware such as the Floppy Emu will not work.

Exploring the Pippin ROM(s), part 4: Enter the SCSI Manager

In part 1 of this series of posts, Daniel suggested taking a look at the role the SCSI Manager plays in the Mac’s– and by extension, the Pippin’s– startup process. I took a break from examining the auth check code to dissect how the SCSI Manager behaves at startup on the Pippin, with the ultimate goal to discover if an exploit was even possible. After all, from my initial search of the ROM it’s clear that Apple had patched part of the boot process to only allow starting up from the internal CD-ROM drive– who’s to say there aren’t further patches deep in the SCSI code?

The tl;dr is that, after careful examination, I have determined that the SCSI code in the Pippin’s boot process is for the most part unchanged compared to a Mac of the same era. However I’m not yet sure if a “patch partition”-based exploit is possible.

Most of what follows I garnered from the “Monster Disk Driver Technote,” a mirror of which can be found here:

Every SCSI-equipped Mac for the most part follows the same procedure for booting from a SCSI device:

  1. It looks for a valid Driver Descriptor Map in the first 512-byte block
  2. Loads the device’s driver(s) according to the information found in the DDM
  3. Mounts the disk’s HFS partition
  4. Loads the boot blocks from the startup volume and executes them

This has been the case since SCSI was introduced to the Macintosh line with the Mac Plus in 1986. Previously, Macs had primarily booted from floppy disk and occasionally from a non-SCSI hard drive such as the HD20 or HyperDrive (but usually with help from a boot floppy). In either event, the Mac had no concept of drive “partitions” or even “volumes” in the modern sense– the MFS file system consumed the entire disk, and that was it.

The Apple Partition Map, introduced at the same time SCSI arrived on the Mac, changes this behavior by placing the primary file system in its own partition. On a high level, the Mac sees the volume as its own big disk, but the APM also allows device drivers to live in their own partitions rather than in INIT files as was done with the HD20. This moves their load time to much earlier in the boot process independent of the System Software and– critically– eschews the need for a separate boot disk. The SCSI code in ROM recognizes the presence of an APM, loads the driver(s) directly from disk, then locates and boots a startup volume. In this way, Macs can boot directly from a hard drive, a CD-ROM drive, or any SCSI-capable storage medium so long as it has the proper drivers preconfigured.

What does all of this have to do with the Pippin? Well, the Pippin’s only internal storage is its 128KB of non-volatile Flash memory, and even if the Pippin ROM was written to look there for an OS, 128KB is barely enough to boot the original Macintosh– never mind the System 7.5-based OS that Pippin titles shipped with. Instead, the Pippin boots from its internal (SCSI) CD-ROM drive, presumably calling into the SCSI Manager to load any device drivers followed by mounting the HFS partition, verifying the contents of the “PippinAuthenticationFile,” then continuing to boot the OS if everything checks out.

Wait a minute…

Before searching for an operating system, the SCSI Manager is supposed to load device drivers directly from disk according to the Driver Descriptor Map. It does a basic checksum verification, then… executes them! They run before the auth check!

At least, in theory.

I took a look at the “Pippin Network CD” (product ID BDB-002) that originally shipped with the 1.0 Atmark units in Japan. I chose this particular disc because of two things: 1) It has an Apple Partition Map and 2) It contains a very small device driver partition with plenty of extra room in which I could hack a “proof-of-concept.” I rustled out the original driver from an ISO image of the disc with a hex editor, then added some code that does the equivalent of while (!Button());. I then recalculated my hacked driver’s checksum using a quick program I wrote, poked the driver (and its proper checksum) back into the ISO, burned it to disc, booted my Pippin with it, and…

It booted directly to the login screen.

Pippin Network CD Version 1.0
I don’t know what any of this text says, but to me it just means disappointment.

This tells me that one of two things might be happening:

  1. The Pippin’s version of the SCSI Manager in ROM does not in fact load device drivers from disc. The SCSI Manager does a number of checks before it determines that it needs to load drivers for a particular device, and it’s possible that the Pippin fails one of those checks, possibly intentionally. In this case, it may be that the driver lives in ROM and any drivers on Pippin discs exist for the benefit of mounting on actual Macs, where the hardware isn’t fixed and device drivers would be more necessary. Also, only a handful of Pippin discs have partition maps; the majority of them are formatted as a flat HFS volume. What does the SCSI Manager do in that case? Where do the requisite device drivers come from?
  2. I screwed up hacking my driver code into the disc image before burning it. 😛

Unfortunately, when I mount this disc on my Power Mac G3 in Mac OS 9.2.2, my driver code doesn’t run there either. There must be some condition(s) under which the SCSI Manager will skip loading drivers from a CD-ROM mastered with them.

Looks like I have some more digging to do…

Exploring the Pippin ROM(s), part 3

Last week, I found the ‘rvpr’ 0 resource in the Pippin 1.0 ROM and the role it appears to play in the Pippin’s startup process. I noted that there are no ‘rvpr’ resources in the 1.2 or 1.3 ROMs, but after digging a little deeper I discovered that is only half true: the contents of ‘rvpr’ 0 are in fact present in both the 1.2 and 1.3 ROMs. But since there’s no entry for it in either ROM’s resource map, a call to _GetResource won’t find it. If 1.2 executes ‘rvpr’ 0 to perform the auth check, then the loading code I found last week must therefore be different in that version.

‘rvpr’ 0 itself appears to be a bit obfuscated. There are many subroutines contained within– if the Link / Unlk / Rts pattern is used as a heuristic, I counted over 250 of them. However, I suspect that much of this code is unused and/or intentional red herrings.

I quickly skimmed the code for an overall first impression before stepping through it in an editor. Some of the aforementioned subroutines are duplicates for some reason:

108A   4E56 0000    Link      A6, #$0
108E   2F2E 0008    Move.L    $8(A6), -(A7)
1092   206E 0008    MoveA.L   $8(A6), A0
1096   2068 0004    MoveA.L   $4(A0), A0
109A   2050         MoveA.L   (A0), A0
109C   4E90         Jsr       (A0)
109E   4E5E         Unlk      A6
10A0   4E75         Rts

10F0   4E56 0000    Link      A6, #$0
10F4   2F2E 0008    Move.L    $8(A6), -(A7)
10F8   206E 0008    MoveA.L   $8(A6), A0
10FC   2068 0004    MoveA.L   $4(A0), A0
1100   2050         MoveA.L   (A0), A0
1102   4E90         Jsr       (A0)
1104   4E5E         Unlk      A6
1106   4E75         Rts

1172   4E56 0000    Link      A6, #$0
1176   2F2E 0008    Move.L    $8(A6), -(A7)
117A   206E 0008    MoveA.L   $8(A6), A0
117E   2068 0004    MoveA.L   $4(A0), A0
1182   2050         MoveA.L   (A0), A0
1184   4E90         Jsr       (A0)
1186   4E5E         Unlk      A6
1188   4E75         Rts
27E8   4E56 0000    Link      A6, #$0
27EC   206E 0008    MoveA.L   $8(A6), A0
27F0   226E 000C    MoveA.L   $C(A6), A1
27F4   2290         Move.L    (A0), (A1)
27F6   7000         MoveQ.L   #$0, D0
27F8   4E5E         Unlk      A6
27FA   4E75         Rts

2AB0   4E56 0000    Link      A6, #$0
2AB4   206E 0008    MoveA.L   $8(A6), A0
2AB8   226E 000C    MoveA.L   $C(A6), A1
2ABC   2290         Move.L    (A0), (A1)
2ABE   7000         MoveQ.L   #$0, D0
2AC0   4E5E         Unlk      A6
2AC2   4E75         Rts

… while others are mostly duplicates with one or two extra instructions:

1C64   4E56 0000         Link      A6, #$0
1C68   206E 0008         MoveA.L   $8(A6), A0
1C6C   20AE 000C         Move.L    $C(A6), (A0)
1C70   216E 0010 0004    Move.L    $10(A6), $4(A0)
1C76   216E 0014 0008    Move.L    $14(A6), $8(A0)
1C7C   216E 0018 000C    Move.L    $18(A6), $C(A0)
1C82   216E 001C 0010    Move.L    $1C(A6), $10(A0)
1C88   216E 0020 0014    Move.L    $20(A6), $14(A0)
1C8E   216E 0024 0018    Move.L    $24(A6), $18(A0)
1C94   4E5E              Unlk      A6
1C96   4E75              Rts

27FC   4E56 0000         Link      A6, #$0
2800   206E 0008         MoveA.L   $8(A6), A0
2804   20AE 000C         Move.L    $C(A6), (A0)
2808   216E 0010 0004    Move.L    $10(A6), $4(A0)
280E   216E 0014 0008    Move.L    $14(A6), $8(A0)
2814   216E 0018 000C    Move.L    $18(A6), $C(A0)
281A   216E 001C 0010    Move.L    $1C(A6), $10(A0)
2820   4E5E              Unlk      A6
2822   4E75              Rts

2824   4E56 0000         Link      A6, #$0
2828   206E 0008         MoveA.L   $8(A6), A0
282C   20AE 000C         Move.L    $C(A6), (A0)
2830   216E 0010 0004    Move.L    $10(A6), $4(A0)
2836   216E 0014 0008    Move.L    $14(A6), $8(A0)
283C   216E 0018 000C    Move.L    $18(A6), $C(A0)
2842   216E 001C 0010    Move.L    $1C(A6), $10(A0)
2848   216E 0020 0014    Move.L    $20(A6), $14(A0)
284E   4E5E              Unlk      A6
2850   4E75              Rts

Some subroutines elicit from me a “WTF?!” reaction, like this implementation of memcmp:

A3C    8854 5F6D    DC.L      $88545F6D       ; ' T_m'
A40    656D 636D    DC.L      $656D636D       ; 'emcm'
A44    7000 0000    DC.L      $70000000       ; 'p   '
A48    4E56 0000    Link      A6, #$0
A4C    41EC 8226    Lea.L     -$7DDA(A4), A0
A50    2948 8390    Move.L    A0, -$7C70(A4)
A54    41EC 825E    Lea.L     -$7DA2(A4), A0
A58    2948 8394    Move.L    A0, -$7C6C(A4)
A5C    41EC 81F2    Lea.L     -$7E0E(A4), A0
A60    2948 8398    Move.L    A0, -$7C68(A4)
A64    42AC 839C    Clr.L     -$7C64(A4)
A68    4E5E         Unlk      A6
A6A    4E75         Rts

Once I sat down and walked through the code from the very top, though, things started to become a little clearer, although I’m not finished analyzing this code yet by any stretch. Recall from last week that the Pippin loads ‘rvpr’ 0 from ROM and then copies it into a block of memory allocated on the system heap. ‘rvpr’ 0 starts by getting its own address in the heap and applying an offset to it:

104    41FA FEFA         Lea.L     @start, A0
108    D1FC 0001 06A2    AddA.L    #$106A2, A0
10E    2008              Move.L    A0, D0
110    A055              _StripAddress
112    C18C              Exg.L     D0, A4

Next, it gets its address again, but without the offset:

14     41FA FFEA    Lea.L     @start, A0
18     2008         Move.L    A0, D0
1A     A055         _StripAddress

Then, it calls a subroutine that reads and writes to some data located at the offset -$7C60 from the address calculated in the first step, effectively placing it at $8A42 from the start of its memory block. It subtracts the longword found here (initially zero) from the unmodified start address, and if the result is zero, returns without doing anything else. But if it’s not zero, as would be the case when first running this code, things get interesting. It checks to see if _HWPriv is implemented and if so, sets a Boolean to true at offset $8A46. Then it passes its address + $8A47 to yet another subroutine. Finally, it sets $8A42 to the unmodified start address (effectively short-circuiting future calls), checks the Boolean at $8A46 and if it’s true, flushes the instruction cache by calling _HWPriv with selector 1 in register D0.

Hmmm. Why would it need to explicitly flush the instruction cache? The answer to that question lies in the subroutine that gets passed @start + $8A47. I haven’t fully wrapped my head around it yet, but from reading the code there it looks like offset $8A47 of ‘rvpr’ 0 looks to be a compressed list of offset locations, used to patch ‘rvpr’ 0 in place. A-ha! Now it’s clear why ‘rvpr’ 0 is copied to the system heap, and abundantly clear why the instruction cache needs to be flushed after this subroutine returns: it’s self-modifying code.

(P.S. Josh Juran graciously pointed out that the Metrowerks runtime used by e.g. CodeWarrior performs this same in-place relocation of code resources at runtime. Side question: What’s the possibility the Pippin’s auth check was written with CodeWarrior?)

Exploring the Pippin ROM(s), part 2

I’ve spent the last couple of evenings taking a closer look at the Pippin 1.0 ROM– specifically the boot process– trying to determine precisely how it verifies that a provided boot disc is in fact signed properly before passing it off to get loaded.

The earliest parts of the Pippin ROM are not much different from the late Quadra “universal” ROMs, which kind of makes sense given how close the Pippin is to the first couple generations of Power Macs. It deviates in a few places by writing to some areas of high memory for reasons I haven’t yet deduced, but is otherwise pretty straightforward compared to a real Mac– in accordance with being derived from a “universal” ROM, it retains the checks for various 68K processors and their capabilities, even despite only having the 68LC040 emulator underneath.

Where things start to get interesting is after the ROM initializes the SCSI Manager. It then looks for an ‘iNiT’ 1 resource (note the capitalization) and executes it, followed by an ‘iNiT’ resource named “Install XFS.” I haven’t yet dug into these segments to see what is happening here, but somehow I don’t think the latter block is installing drivers for a popular filesystem… 😉

Elliot Nunn pointed out to me that the boot process is part of the Start Manager and hadn’t changed much in the years leading up to the Pippin’s release. He also kindly suggested that I specifically search for FindStartupDevice.

So I did that.

I found a few interesting things.

FindStartupDevice pretty much follows the same steps as a real Mac… until we find valid boot blocks. Then it runs this little snippet of code:

1592   303C FFDC        Move      #-36, D0
1596   322A 0008        Move      dqRefNum(A2), D1
159A   B240             Cmp       D0, D1
159C   66C0             BNE.B     @TryAgain

-36 is the refNum for the internal CD-ROM drive. What this code does is check to see if our current drive queue entry is using the .AppleCD driver with the internal drive. If it’s not, it loops back to search for other potential boot volumes. Looks a bit like a hotfix and/or conditionally compiled to me (Why didn’t they just refactor the code so that it only searches the CD-ROM drive? Hey, I wasn’t there…), but essentially this means definitively that a 1.0 Pippin will not fully boot from any device other than its internal optical drive.

After this code is where things start to heat up. Take a look:

159E   2F3C FFFF FFFF   Move.L    #-1, -(A7)
15A4   4EBA 0B0A        Jsr       @mysterySub1          ; hmmm...
15A8   588F             AddQ.L    #4, A7
15AA   4EBA 1C84        Jsr       @mysterySub2          ; HMMM...
15AE   4A40             Tst       D0
15B0   6738             BEQ.B     @GotIt                ; success! boot!
15B2   303C FFDC        Move      #-36, D0
15B6   B06A 0008        Cmp       dqRefNum(A2), D0
15BA   66A2             BNE.B     @TryAgain
15BC   4FEF FFCE        Lea.L     -ioQElSize(A7), A7
15C0   204F             MoveA.L   A7, A0
15C2   317C FFDC 0018   Move      #-36, ioRefNum(A0)    ; .AppleCD
15C8   4268 0016        Clr       ioVRefNum(A0)
15CC   42A8 0012        Clr.L     ioNamePtr(A0)
15D0   317C 0007 001A   Move      #7, csCode(A0)        ; eject the disc
15D6   A004             _Control
15D8   3028 0010        Move      ioResult(A0), D0
15DC   4FEF 0032        Lea.L     ioQElSize(A7), A7
15E0   3F3C 0002        Move      #2, -(A7)             ; ShutDwnStart
15E4   A895             _ShutDown                       ; restart the Pippin
15E6   6000 FF76        Bra       @TryAgain
15EA   4A78 08D0        Tst       (CrsrState)
15EE   6B02             BMI.B     @ShowHappyMac
15F0   A852             _HideCursor

Immediately before making the decision to advance to the “Happy Mac” state (such as it is on the Pippin), this block of code passes -1 on the stack to a mystery subroutine. Then, it calls a second mystery subroutine, the result of which, if zero, indicates the Pippin is free and clear to boot from that volume (provided it’s the CD-ROM drive– again with that check!). If the check fails, the disc is ejected and the Pippin restarts.

So, let’s start with mysterySub1. mysterySub1 calls down to $20B0, where this happens:

20B0   60FF 000C AB0E    Bra.L     @mysterySub3

Hmmm. OK. So where does that take us? We end up in a short subroutine that loads a ‘nint’ 43 resource, then through a series of calls to _CodeFragmentDispatch we jump into InitAnimation. A-ha! ‘nint’ 43 starts with the string “Joy!peffpwpc” indicating that it’s PPC code, and a list of symbols at its end suggests it draws the “tray loading” animation using a loop of _DrawPicture and associated Color QuickDraw calls. Neat.

But mysterySub2 is where things get really juicy. Check it out:

3230   48E7 3030         MoveM.L   D2-D3/A2-A3, -(A7)
3234   2078 0DDC         MoveA.L   (BootGlobPtr), A0
3238   41E8 FF7E         Lea.L     -$82(A0), A0
323C   20B8 020C         Move.L    (Time), (A0)
3240   594F              SubQ      #4, A7
3242   2F3C 7276 7072    Move.L    #'rvpr', -(A7)       ; 'rvpr' resource
3248   4267              Clr       -(A7)                ; ID 0
324A   A9A0              _GetResource
324C   221F              Move.L    (A7)+, D1
324E   6700 0050         BEQ       @fail
3252   2041              MoveA.L   D1, A0
3254   2648              MoveA.L   A0, A3               ; A3 = handle to loaded 'rvpr' resource
3256   594F              SubQ      #4, A7
3258   2F08              Move.L    A0, -(A7)
325A   A9A5              _SizeRsrc
325C   201F              Move.L    (A7)+, D0
325E   2600              Move.L    D0, D3               ; D3 = size of loaded 'rvpr' resource
3260   A71E              _NewPtrSysClear
3262   6600 003C         BNE       @fail
3266   2F08              Move.L    A0, -(A7)
3268   2003              Move.L    D3, D0
326A   2248              MoveA.L   A0, A1
326C   204B              MoveA.L   A3, A0
326E   2050              MoveA.L   (A0), A0
3270   A02E              _BlockMove                     ; copy 'rvpr' resource into new ptr
3272   2F0B              Move.L    A3, -(A7)
3274   A9A3              _ReleaseResource
3276   2657              MoveA.L   (A7), A3             ; A3 -> our 'rvpr' resource data
3278   554F              SubQ      #2, A7
327A   3F2A 0008         Move      dqRefNum(A2), -(A7)
327E   3F2A 0006         Move      dqDrive(A2), -(A7)
3282   41FA FF5C         Lea.L     someData, A0
3286   2F08              Move.L    A0, -(A7)
3288   41FA FF46         Lea.L     someOtherData, A0
328C   2F10              Move.L    (A0), -(A7)
328E   4E93              Jsr       (A3)
3290   301F              Move      (A7)+, D0
3292   205F              MoveA.L   (A7)+, A0
3294   3F00              Move      D0, -(A7)
3296   A01F              _DisposePtr
3298   301F              Move      (A7)+, D0
329A   4CDF 0C0C         MoveM.L   (A7)+, D2-D3/A2-A3
329E   4E75              Rts
32A0   303C FFFF         Move      #-1, D0
32A4   60F4              Bra.B     @exit

That’s a bit to take in, but here’s a summary. We load ‘rvpr’ 0, then copy it into a new block of memory within the system heap. Then we pass the current DCE refNum, drive, a pointer to four longs (the first of which having the value $4B, or 75), and then a pointer to a much larger data block immediately preceding this subroutine to our local copy of ‘rvpr’ 0. It returns a 16-bit result code on the stack, which we save before disposing of our local copy of ‘rvpr’ 0, then we return. From examining what FindStartupDevice does earlier, the result of ‘rvpr’ 0 must be zero in order for the Pippin to complete the startup process.

So what’s in ‘rvpr’ 0?

'rvpr' 0 in 0xED
Anything stand out to you?

and… InitRSAAlgorithmChooser

That smells like paydirt to me, at least in the 1.0 ROM. The best part is that it’s written in 68K, my reading comprehension skills of which are much better than that of PowerPC assembly. Curiously, there are no ‘rvpr’ resources in ROMs 1.2 or 1.3, even though 1.2 also does the auth check. I’m interested in discovering what replaces it in 1.2, but for now I will continue to investigate 1.0’s implementation. Stay tuned. 🙂

Exploring the Pippin ROM(s)

The Pippin never really had a fair chance at life. Produced by Bandai under license to Apple, it was too expensive for a gaming console yet overshadowed in the computer market by full desktop machines. The system suffered a bit of an identity crisis as well: Was it for gaming? The Internet? Neither? Both? Pippin was one of those late-90s, pre-Jobs Apple experiments that didn’t take the world by storm and as such is largely ignored by retro gaming and computing enthusiasts.

Which is, of course, why I have one. 😀

Welcome to Pippin. ..that's OK, I didn't see it either.
Neither did most of the gaming world back then.

Essentially a Power Mac 6100/66 crammed into a set-top box, in my opinion the Pippin packs a bit more power than a lot of folks realize. The Pippin had the most success in Japan, and most titles developed for it were localized for that region. Unfortunately many of those titles were authored in Macromedia Director and did little to show off the capabilities of the console. There were a number of third parties lined up to develop English software for the system in 1996 and early 1997, but this was also the era of Nintendo 64 and PlayStation, so Pippin was cancelled before many of these titles would ever ship– in some cases even before projects got off the ground. One of those titles was Presto Studios’ The Journeyman Project: Pegasus Prime, which I’ve had and continue to enjoy the tremendous honor of maintaining a modern rerelease for Windows, macOS, and Linux. Pegasus Prime almost didn’t ship at all– that Pippin was based on the Macintosh platform allowed Bandai to publish it for Power Macs instead, where certainly the returns were more promising. A number of Pippin discs could run on Macs– this was in fact a hallmark of the system. But this could also go both ways– if Pippin software is also Mac software, then shouldn’t the reverse be true? Why isn’t there a homebrew scene for the Pippin?

Pegasus Prime on the Pippin
The rerelease is way better. Trust me. 😉

The only built-in software shipped with the Pippin came in the form of a circuit board with four megabytes of ROM that fit into a slot on the logic board inside the system’s case. The Pippin itself has no built-in operating system– each title shipped on one or more bootable CD-ROM discs containing a modified version of System 7.5.2. In a time before ubiquitous broadband Internet access like we have today, this allowed Bandai/Apple to provide upgrades and new features in software without having to download patches or release new hardware. But only titles that were signed or “Pippinized” by Apple could boot the system. This was done less to combat piracy (as the CDs themselves were and continue to be easily duplicated), but more to control the library, limiting the selection of titles to those produced by officially licensed developers. This is not that different from how Sony, Microsoft, and Nintendo prepare software for their respective consoles today. While the check for Pippinized discs could be skipped on test or retail kits using a special dongle provided by Apple (extremely hard to find today), development units came with a special ROM that removes the check entirely, allowing programmers to more conveniently test and debug multiple revisions before finally reaching “Gold Master” status and sending their final CDs off to be signed and pressed.

There were a couple different ROM board PCB styles– very early developers typically would get pre-production hardware with a ROM board populated with erasable Flash chips. These developers would periodically be asked to send their boards back to Apple to be reflashed with the latest ROM revision. Once the ROM was finalized and the Pippin went into production, boards shipped populated with mask ROM chips that could not be rewritten. I know of two developer ROMs, plus three ROM revisions that were released with retail consoles: 1.0 (white atMark Pippins in Japan), 1.2 (black @WORLD Pippins in the US and late atMarks), and 1.3 (very late Pippins). I have images of these three retail ROMs plus an image of the “GM Flash” developer ROM; I have never seen a retail ROM 1.1. As ROM 1.3 only shipped with a small number of Pippin units late in the system’s lifecycle, it is very rare but represents the last revision of the Pippin ROM, culminating a number of bug fixes and other changes.

ROM 1.3 also… I am told… removes the authentication check at boot, much like the developer ROMs do. 😉

I want to find out how. 😀

Pippin ROM board

Power Macs produced in 1998 or later boot from Open Firmware and tend to have their ROMs loaded from a file on disk and loaded into RAM– the “New World” model. Earlier Power Macs like the Pippin also boot from Open Firmware but still have a ROM burned into a physical chip or chips attached to the logic board– the “Old World” model. Open Firmware is contained within the last 1MB of these ROMs, and overall its job is to start the 68LC040 emulator and use it to boot the rest of the ROM, which is written predominantly in 68K. Thus, the first 3MB of an Old World ROM is straight 68K code, mostly comprising the Mac Toolbox APIs and some necessary drivers for booting the system and loading the OS from a startup device. These drivers can be found in what are known as “resources”– self-contained chunks of data addressed by a four-byte “type” and a two-byte ID (or occasionally a human-readable name as a Pascal string). Resources aren’t just segments of code– they can contain any type of data such as pictures, icons, sounds, and fonts, just to name a few.

I’m quite familiar with resources– my earliest work on Pegasus Prime involved reverse-engineering some custom resource formats developed to hold metadata about the game’s QuickTime-based animations. I did some work a number of months ago trying to reverse-engineer the MacWorks XL ROM for the Apple Lisa, in an effort to port the HD20-enabled .Sony driver to it so my Floppy Emu could boot it (One of these days I should revisit that and do a writeup about it, but that project is definitely on the backburner). In the course of dissecting that ROM, I learned a lot about the Mac boot process and how the ROM’s address space in general is laid out. For example, the first four bytes of a 68K Mac ROM consist of a checksum of the ROM’s contents. Four bytes at offset 4 provide a reset vector, which the Mac uses to boot. And, at offset $1A, four bytes provide an offset to where the ROM’s resources are stored. An easy first step to understanding a Mac ROM– or in this case, that of a Pippin– would be to extract its resources and examine them in a tool such as ResEdit.

I think one of the reasons why I couldn’t readily find a utility that took a binary ROM image and spit out a .rsrc file is that the way resources are stored in the ROM has changed since the earliest Macs. The Mac Plus ROM, for example, literally stores a resource map as one would find in an actual resource fork or .rsrc file– quite simple. By the time of the Pippin, though, this had changed to a slightly more complicated scheme. I spent a few hours last night successfully reverse-engineering this and then halfway through writing an extraction tool before the evening got too late. This evening, I was greeted by a pleasant surprise:

A few seconds later, I had the resources extracted from all four of my ROM images. I spent a little time tonight skimming through all I’ve found. A few highlights:

– The initials “kc” and the name “Kurt” are used for padding– these refer to Kurt Clark, presumably an engineer at Apple at the time.
– Open Firmware contains strings referencing TFTP in all the ROM revisions I have. Maybe the Pippin can boot from TFTP somehow, bypassing the authentication check?
– Gary Davidian’s 68LC040 emulator and Eric Traut’s dynarec can both be found after Open Firmware in the last 1MB of the ROM. Maybe they can be replaced with the versions found in Connectix’s Speed Doubler, saving the need for loading that INIT into RAM?
– All four ROMs contain the resources for the blinking ? and X floppy icons, as well as the recognizable Happy Mac. These icons are shown during the boot process on a real Mac but unused on the Pippin.
– There are two PPC-based ‘ndrv’ resources: .Display_Video_Apple_Control (to drive the “taos” graphics hardware) and .DAVInput, which might be used with the rear RCA audio jacks. The Pippin doesn’t have any video input capabilities, but the Power Mac 6100AV did… maybe .DAVInput and some of its associated hardware is borrowed from that machine?
– There are five 68K-based ‘DRVR’ resources– .rdrvr (for the internal 128KB of Flash storage), .AppleCD, .Sound, .Sony, .AppleSoundInput, and .EDisk. The latter two are a bit interesting– .AppleSoundInput because it’s either used for the rear audio jacks or the internal connection to the CD-ROM drive’s audio, and .EDisk because it suggests the existence of a ROM disk somewhere.
– The CD-ROM driver is identical between the GM Flash and 1.0 ROMs. But then there are major changes going into 1.2– in addition to code changes, a new drive model appears to be added to a whitelist: the Matshita CR-8006, a.k.a. the Apple CD 600e. But then, in 1.3, the whitelist reverts back to that of 1.0, while incorporating additional code changes. I’ve successfully got a Toshiba DVD-ROM drive to boot my @WORLD, so I’m not entirely sure what this whitelist is used for yet.

More to come as I dig deeper…