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
@GotIt
15EA 4A78 08D0 Tst (CrsrState)
15EE 6B02 BMI.B @ShowHappyMac
15F0 A852 _HideCursor
@ShowHappyMac
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
@exit
329A 4CDF 0C0C MoveM.L (A7)+, D2-D3/A2-A3
329E 4E75 Rts
@fail
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?
GetVolAuthFileInfo
CleanseInputChunk
VerifyDigestInfo
VerifySignature
CreateDigest
CompareDigest
RangedRand
CleanseVCB
GetVAFileInfoGivenMDB
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. 🙂
Sure looks promising! Can you find those strings *anywhere* in later ROM versions?
There don’t seem to be any obvious holes in this integrity check. But could it be possible for LoadSCSIDrivers to run some code from disk before the check?
Failing that, have you got any thoughts about how to “take over” an existing signed Pippin disk without altering its digest? For example, is it possible to sneak a disk image into an existing file, along with some code to “switch boot” that image?
I eagerly await the next episode!
The Monster Disk Driver Technote (https://www.fenestrated.net/mirrors/Apple%20Technotes%20(As%20of%202002)/tn/tn1189.html) indicates that yes you can run code before the boot blocks are located. There is even a special partition just for that. It is called the Patch Partition (its type is “Apple_Patches”). The actual code in the ROM does not call the Patch Partition: the driver on disk is what does it.
Those strings are present in ROMs 1.2 and 1.3. It looks like the data is there even though there are no entries for it in either ROM’s resource map. If this code is called in 1.2 then perhaps its location in ROM is hardcoded elsewhere.
I spent some time recently going over how the Pippin boots from SCSI. I have a post in progress that should be going up over the weekend. 🙂
If you replace the first part of the ‘rvpr’ with the following code, would it cause the check to succeed? I think it would work, but I am not sure I used the right stack offsets.
movea.l (sp), $000C(sp)
adda.l $000C, sp
clr.w $0004(sp)
rts
I suppose my question is better worded as “If the _BlockMove trap were to ‘accidentally’ copy some totally different string of instructions instead of the ‘rvpr’ resource, what would you want that string of instructions to be?”. I have already written code that will probably do this, but I am not sure what the payload should be.