There is something wonderfully revealing about old Amiga software: “loading a game” could mean taking over the whole machine. The disk might not contain a neat directory of files. It might contain a boot block, a custom floppy reader, a depacker, a music module, and a lot of carefully arranged bytes that only make sense when the loader puts everything back together.
That is exactly what turned up in Intact, the 1990 vertical shooter from Sphinx Software and CrossTechnics. I started with Intact (1990)(Sphinx)[cr Defjam - CCS - ACC].adf and followed the code statically. A later FS-UAE run checked the broad cracktro-to-game sequence. That run did not validate every packed region or the exact decompressor semantics.
First, what is Intact?
Intact is an OCS Amiga shoot-'em-up: a ship moves through vertically scrolling levels, fights enemy formations, and collects credits that can be spent on upgrades. It is the kind of game where the end result is fast action, but the startup path is a small systems-programming project in its own right.
For background, see Lemon Amiga's Intact entry. Its externally hosted preview is unavailable in the current browser check, so the images retained below are the labeled FS-UAE captures, with the no-disk reference kept separate from the ADF run.
The disk gives us a useful first clue
The ADF is 901,120 bytes, which is exactly the size expected for an 80-track, double-sided, 11-sector Amiga floppy image. The boot block begins with DOS\0 and has a valid Amiga boot checksum. That sounds like a normal disk, but the area where an AmigaDOS root block would normally be found contains high-entropy loader data rather than a clean filesystem header.
In other words: the disk keeps enough of the Amiga boot contract to start, then uses its own rules for the rest. A directory tool is looking in the wrong place.
| Property | Value |
|---|---|
| Boot signature | DOS\0 |
| MD5 | 4293476ee4229d76803fbc749234136c |
| SHA-256 | 153a3bef09fa40b6f95a8c0f33aa21ececa6f4de9cf1f1726b672b1239d604f5 |
| Filesystem | Custom bootable track layout, not a clean mountable AmigaDOS volume. |
Step 1: the boot block loads two pieces
The first executable code is in the boot block itself. It builds an Amiga I/O request, uses read command 2, and performs two reads before jumping into the first one:
| Read | ADF offset | Bytes | RAM | Purpose |
|---|---|---|---|---|
| 1 | $000400 | $1200 | $20000 | Resident stage-1 loader |
| 2 | $0ad400 | $1b800 | $62cf0 | Cracktro payload |
; immediate values reconstructed from the boot block move.l #$1200,$24(a1) ; read length move.l #$20000,$28(a1) ; destination move.l #$400,$2c(a1) ; disk offset move.w #2,$1c(a1) ; device read jsr -$1c8(a6) move.l #$1b800,$24(a1) move.l #$62cf0,$28(a1) move.l #$ad400,$2c(a1) move.w #2,$1c(a1) jsr -$1c8(a6) jmp $20000
This is the key transition. The Amiga does not open a file called “loader”, and it does not launch a normal C/Intro program from a directory. The boot block copies raw bytes to known addresses and jumps to them.
Step 2: stage 1 becomes its own disk driver
Once running at $20000, the resident loader allocates nine destination buffers. It then talks directly to the floppy hardware through CIA and custom-chip registers. The track reader:
- selects the drive, side, direction, and head-step controls;
- starts disk DMA with
DSKLEN=$9980in read mode; - searches for the Amiga MFM sync word
$4489; - separates odd and even MFM words with
$55555555; - places sectors by their sector numbers; and
- checks the decoded XOR checksum.
That explains how a disk can look “wrong” to AmigaDOS and still boot perfectly. The loader is treating the floppy as a physical track stream, not as a collection of ordinary files.
Step 3: the cracktro is a preloaded mini-demo
The second boot read put $1b800 bytes at $62cf0. During stage-1 initialization, code at loader offset $1cc calls a small wrapper at $4b2; that wrapper calls the payload at $62cf0 and returns.
The payload is much more than a text string. Its entry code initializes pointers and custom-chip state, installs an interrupt vector at address $6c pointing into the payload at $62de4, calls several setup routines, and returns with RTS. The loader therefore gives the intro a chance to install its graphics, audio, and interrupt machinery while the rest of the startup continues.
| Evidence | What it tells us |
|---|---|
$62cf0 entry and RTS | The payload is called as an initialization stage, not simply copied and forgotten. |
Interrupt vector $6c → $62de4 | The intro installs code that can continue running asynchronously. |
graphics.library, custom-chip accesses | Graphics/display setup is part of the payload's environment. |
CRACKED IN 1990 and publisher/design credits | This is the cracktro/credit screen payload. |
introbeat plus six sample names | The intro carries its own tracker music and samples. |
The friendly way to think about it is: this is a tiny demo embedded in the loading process. It establishes a visual/audio presence, shows the group's credits, and can use interrupts while the loader does the less glamorous work of reading and expanding the game.
There is one important caveat. Static code proves the payload is loaded, called, and equipped to drive graphics/audio. It does not prove the exact order of the screens, whether the music starts before or after every track read, or precisely when the cracktro hands the display back. Those are emulator questions.
The soundtrack survived intact
At payload offset $dcac there is a complete standard 4-channel ProTracker module named introbeat. It has the M.K. signature, 22 song positions, 20 patterns, and six named 8-bit sample streams:
st-99:bdrum-finetimest-99:snare-bigfunst-99:hhop-alrightst-99:piano-oasisst-99:snare-pinkst-99:bass-oasis
The module was extracted byte-for-byte from the payload. That is a nice example of the difference between “not everything is decoded yet” and “nothing can be recovered”: the cracktro's music is already a normal, recognizable Amiga artifact.
Step 4: the game is stored as nine packed regions
The loader contains a descriptor table at file offset $f88. Each 12-byte entry says where a region will go, which side-track it starts on, how many tracks to read, and how many packed bytes to pass to the depacker.
| Region | Start side-track | Tracks | Packed | Output trailer |
|---|---|---|---|---|
| 0 | 1 | 29 | $27700 | $32f50 |
| 1 | 30 | 7 | $08c24 | $0cc24 |
| 2 | 37 | 4 | $04ff0 | $08790 |
| 3 | 41 | 6 | $06e40 | $0a6c8 |
| 4 | 47 | 10 | $0d014 | $11d80 |
| 5 | 57 | 9 | $0c458 | $1d8ca |
| 6 | 66 | 9 | $0ba0c | $1dc73 |
| 7 | 75 | 7 | $098ec | $1e979 |
| 8 | 82 | 10 | $0ceb4 | $1ec5c |
The “output trailer” column is the final output length stored at the end of each packed stream. It is not a decompressed file sitting on disk; it is a value the depacker reads while working backwards.
So what is the packing?
This is where the phrase custom backward-LZ-style depacker comes from. The label is descriptive, not a guess at a commercial packer name.
The routine is embedded in the loader at offset $6ca. It starts at the end of a packed region, consumes a least-significant-bit-first bitstream using a 68000-specific refill/anchor convention, and writes output toward the beginning of the destination. Its copy loop is unusually helpful:
move.b -1(a2,d2.l),-(a2)
That means “take a byte from a backward distance and move the destination backwards by one byte.” The routine has literal runs, reference classes, and several distance forms. From the instructions we can describe:
- a 3-bit literal count, including an extended-count branch;
- a 2-bit reference class;
- fixed and variable copy lengths;
- 8-, 14-, 12-, and 16-bit backward-distance paths; and
- a shared flag path selecting the 12- or 16-bit distance form.
Each stream ends with a 12-byte trailer. The final longword supplies the output length, the preceding longword is skipped by this routine, and the next longword seeds the initial bit reservoir.
That is enough to understand the shape of the codec and to avoid calling it PowerPacker or ByteKiller without evidence. It is not yet enough to reproduce all nine files: the current standalone translation still reaches impossible references on every exact stream. The likely missing piece is in the precise bitstream framing/refill behavior, not in the overall idea that the data is backward-output compressed.
The last move: handing control to the game
After the intro call and the track/decrunch sequence, stage 1 explicitly decrunches the $08c24-byte region to $25b48. It copies a 28-byte stub into low memory at $400 and jumps there. The stub copies 52,264 bytes from $25b48 to $1f0e0, then jumps to $50000.
; low-memory handoff stub lea $25b48,a1 lea $1f0e0,a2 move.w #$3309,d0 .copy: move.l (a1)+,(a2)+ dbra d0,.copy ; $330a longwords = 52,264 bytes jmp $50000
The retained loader bytes at file offset $51e are 30 3c 33 09: move.w #$3309,d0, not moveq. With dbra, the loop copies ($3309 + 1) × 4 = 52,264 bytes. This corrects the mnemonic without changing the stated copy length.
This is the final statically visible transfer into the loaded game/main-program address space. It is a very compact ending to a fairly elaborate startup: read tracks, expand regions, then jump into the reconstructed program.
What is confirmed, and what is still open?
| Confirmed from the image | Still to explore |
|---|---|
| Two boot-block reads and their RAM destinations. | The exact bit alignment/refill rule needed to decode all nine regions. |
| A raw MFM reader with sync, deinterleave, sector placement, and checksum logic. | Which expanded region contains code, sprites, fonts, level data, or other assets. |
| The cracktro entry, interrupt-vector installation, credits, and tracker module. | The exact order and timing of the intro screens and music relative to loading. |
| Nine packed regions, their track ranges, lengths, and final handoff addresses. | A byte-level comparison with an untouched original disk to identify the crack delta. |
| The final copy-and-jump sequence. | An instruction-level trace to confirm the exact live jump at $50000. |
There are also some tempting leads that deserve restraint. The loader contains strings such as Intact:C/Intro, but the disk is not a normal AmigaDOS filesystem, so that string should not be turned into a claim that a real C/Intro file exists. Likewise, a write to the 68000 bus-error vector is an observation in the sample, not proof of malicious behavior.
Watching the three stages run
The retained capture notes date the emulator follow-up to 7 September 2026: FS-UAE 3.2.35, an Amiga 500/512 KB profile, 512 KB chip memory, and no slow or fast memory. The cracktro and game frames came from the supplied ADF run. The ROM/bootstrap image is a separate no-disk reference.
Boot stage: the emulator is at the machine's ROM/bootstrap display before the disk's custom loader has drawn its own screen. This frame is a separate no-disk boot reference because the ADF's transition from boot reads to custom graphics is too quick to pause cleanly.
Crack intro: the preloaded stage-2 payload displays the INTACT title and credits Defjam, CCS, ACC, and Crosstechnics.
Post-intro game screen: the run has moved beyond the cracktro into the game's own display, consistent with the broad handoff reconstructed from the loader. A screenshot alone does not establish the exact program counter.
The runtime check confirms the broad chain: boot code, crack-intro payload, then game-side code. It does not by itself identify every packed region or prove the exact decompressor semantics; those remain byte-level questions for the disassembly.
Why this is a fun kind of reverse engineering
The crack did not merely remove a copy-protection check. It changed the computer's relationship with the disk. The Amiga boots a small program, that program becomes a disk driver, the disk driver reconstructs several memory regions, and a separate mini-demo announces the people who made the altered version. By the time the game starts, the “file” has been transformed several times.
What we can already recover is surprisingly tangible: the boot code, the raw-track loader, the descriptor table, the cracktro strings and setup logic, the complete introbeat module, and all nine packed regions. What remains is the satisfying hard part: making the depacker agree with the original 68000 on every bit, then identifying the assets hidden behind those streams.
Method note, corrected 8 October 2026: the analysis began as static, read-only inspection and was followed by the isolated emulator run described above. The retained private notes contain disassembly, hashes, packed-region extracts, soundtrack, decoder experiment, and capture settings. They are not linked here as a public download. The standalone decoder still fails validation on all nine exact streams.
Comments
Post a Comment