This walks through how BullfrogPlug-In.exe (a 1998 InstallShield PFTW installer pulled from the Wayback Machine) was repaired after FTP ASCII-mode corruption. The same approach generalizes to any binary file damaged by the same kind of upload mistake.
- Corruption rule: every standalone
0x0D(CR not followed by LF) had a0x0Ainserted after it. - Naive "strip every
0Aafter0D" doesn't work — legitimate0D 0Apairs exist in the file (DOS stub, compressed-data coincidences) and must be preserved. - Stage 1: align against a "good enough" reference file to identify ~99.9% of insertions.
- Stage 2: use the appended CAB's per-block checksums to find the handful of legitimate
0D 0Apairs that stage 1 wrongly stripped.
If you just want the working file, grab it from the Releases page — no need to clone the repo.
.
├── BullfrogPlug-In.exe ← the fully repaired output (root)
├── original/
│ └── BullfrogPlug-In.exe ← the corrupted input
├── wip/
│ └── BullfrogPlug-In.exe ← the manual-repair reference
├── repair.py ← reference-driven repair script
├── repair_no_ref.py ← reference-free repair script
├── README.md ← this file
└── LICENSE
| Path | Size | What it is |
|---|---|---|
BullfrogPlug-In.exe (root) |
1,464,222 | The repaired file. Passes 7z t with no errors; all 16 internal files extract cleanly including the 915,526-byte Bullfrog.dat payload. SHA-256 7642116c…356575c. Also available as a download on the Releases page. |
original/BullfrogPlug-In.exe |
1,469,760 | The corrupted input. The Wayback Machine copy with 5,538 stray 0x0A bytes inserted after standalone 0x0D bytes by an FTP ASCII-mode upload. This is the only known copy on the public internet that's not damaged in some other way. |
wip/BullfrogPlug-In.exe |
1,464,214 | The "frankensteined" reference. A manual repair attempt that grafts bytes from a different-but-related Bullfrog/EA installer from the same era. The first ~650 KB matches the true original; the rest doesn't (compressed payload differs). Useful as an alignment reference for stage 1 of repair.py, but 7z t reports a data error on Bullfrog.dat — i.e. this file alone won't extract correctly. The CAB checksums in this file are still the unmodified Microsoft canonical csums, so nothing was faked. |
repair.py |
— | Reference-driven, two-stage repair (alignment + CAB CFDATA csum oracle). Reads the corrupted input and the WIP reference, produces the repaired output. |
repair_no_ref.py |
— | Reference-free, four-stage repair (aggressive strip + DOS stub template + PE-string heuristic + CAB csum). Reads only the corrupted input and produces a byte-identical output to repair.py. |
Look at the DOS stub bytes. The standard MSVC stub ends with mode.\r\r\n$, which is 0D 0D 0A 24. Compare:
Orig (corrupted): mode. 0D 0A 0D 0A 24
WIP (reference): mode. 0D 0D 0A 24
The first 0D (which had a non-0A byte after it in the original) became 0D 0A. That's the FTP ASCII-mode "standalone CR → CRLF" normalization. The file grew by one byte at this position.
Count CR/LF patterns across the whole file:
Orig: 0d_then_0a: 5592 0a_alone: 5316 0d_alone: 0
WIP: 0d_then_0a: 47 0a_alone: 5315 0d_alone: 5545
Math: 5592 - 47 = 5545 extra CRLFs in the corrupted file, matching the WIP's 5545 standalone CRs and the size delta. The corruption rule is confirmed.
Greedy walk through orig and ref simultaneously:
- If
orig[i:i+2] == b"\x0d\x0a"andref[j] == 0x0d:- If
ref[j+1] == 0x0a→ legitimate pair, advance both by 2 - Else → FTP-inserted
0a, mark for stripping
- If
- On byte mismatch (content actually differs between files), resync via a 16-byte rolling-window hash lookup against
ref
This works because:
- For the first ~86% of the file (stock InstallShield bootstrap files, common across installers from the same era),
origandrefare byte-identical after fixing FTP insertions - For the differing parts (the actual Bullfrog game payload), the window-hash resync still finds enough common anchor points to keep total insertion count correct
Result: 5,545 insertions identified. Strip them → 1,464,215 bytes.
7z t on the stage-1 output gives:
ERROR: Data Error : /disk1/data/Bullfrog.dat
Unexpected end of data
The repaired file is 7 bytes short of the CAB's declared size (cb_cabinet = 0x14C3D8 ending at file offset 0x16579E, but our file ends at 0x165797). Seven of our 5,545 "FTP insertions" were actually legitimate 0D 0A pairs in the compressed payload — the reference file had different bytes at those positions, so alignment misclassified them.
The appended CAB consists of CFDATA blocks, each with:
- 4-byte
csum(XOR-based checksum ofcbData + cbUncomp + compressed_data) - 2-byte
cbData(compressed payload length) - 2-byte
cbUncomp(decompressed length) cbDatabytes of payload starting with"CK"then raw deflate
For each block:
- Verify the stored
csummatches the computed checksum of the current data. - If yes → move on.
- If no → for each stage-1 insertion that falls inside this block's payload range, trial-restore that
0aand re-check both:- The
csummatches - The MSZip deflate decompresses to exactly
cbUncompbytes with no leftover input (using the previous block's output as the deflate dictionary — MSZip chains)
- The
- Accept the candidate that satisfies both.
Important detail: decompression length alone isn't sufficient. Several candidate positions can produce 32,768 bytes of plausible output (deflate's "last block" marker can land anywhere in a corrupted bit-stream). Only the correct restoration matches the stored csum.
Restorations found:
| CFDATA block | orig offset of restored 0a |
|---|---|
| 22 | 0x9F2A1 |
| 29 | 0xD2F07 |
| 30 | 0xDA221 |
| 31 | 0xE4AAC |
| 36 | 0x1088FE |
| 42 | 0x132EFD |
| 48 | 0x1637B1 |
All seven sit inside the Bullfrog.dat portion of the payload — the part unique to this installer that the WIP reference didn't share.
$ 7z t BullfrogPlug-In.exe
Everything is Ok
Files: 16
Size: 1611336
Compressed: 1464222
All 16 files extract cleanly, including the 915,526-byte Bullfrog.dat.
The two-stage approach works for any FTP-ASCII-corrupted file as long as you have:
- A reference file that's mostly similar to the target. Doesn't need to be perfect — even a different-but-related file of the same compiler/installer family gives enough structural overlap for stage 1.
- A built-in integrity check on the payload. For an InstallShield PFTW that's the CAB CFDATA checksum. Other formats with similar block-level checks: ZIP (CRC32 per file), 7z (CRC + SHA), gzip (Adler32 + final CRC32), MP4 atoms with
mfra, etc. Without an integrity check, stage 2 has no oracle to validate candidate restorations.
If your file lacks both a reference and an internal checksum, you're stuck with the ambiguity: every 0D 0A in the corrupted file could be either FTP-inserted or legitimate, and there's no principled way to choose.
python3 repair.py original/BullfrogPlug-In.exe wip/BullfrogPlug-In.exe BullfrogPlug-In.exe
Output SHA-256: 7642116c8ecd304bfd280817a33f5a54ccc17ae7ef667444380c39d93356575c
Two scripts are included in this folder. Both produce a byte-identical output for the Bullfrog file (SHA-256 7642116c…356575c). They differ in what input they need and how they handle the ambiguity of legitimate 0D 0A pairs.
Usage:
python3 repair.py <corrupted> <reference_wip> <output>
Inputs needed: the corrupted file AND a "good enough" reference (the frankensteined WIP, or any related installer with similar PE structure / stock InstallShield content).
How it works:
| Stage | Purpose | Oracle |
|---|---|---|
| 1 | Identify FTP insertions via byte-by-byte alignment against the reference; 16-byte window-hash resync when content diverges (e.g. frankensteined regions) | The reference's byte at each aligned position |
| 2 | For each appended CAB CFDATA block whose stored csum doesn't match, trial-restore stage-1 insertions inside that block until both csum and MSZip decompression validate | CAB CFDATA csum + deflate decompress-to-declared-length |
Strengths: stage 1 gets ~99.9% of insertions right in one pass when a similar-enough reference exists, so stage 2 has little to fix.
Weaknesses: requires a reference. If the reference is from an unrelated file the window-hash resync degrades and stage 1 becomes less helpful, though stage 2 will still rescue the CAB region.
Usage:
python3 repair_no_ref.py <corrupted> <output>
Inputs needed: just the corrupted file. No reference required.
How it works:
| Stage | Purpose | Oracle |
|---|---|---|
| 1 | Aggressively strip every 0A that follows a 0D (treat all as FTP insertions) |
None — deliberate over-strip |
| 2 | Restore the legitimate 0D 0A in the DOS stub |
Standard MSVC \r\r\n$ template at known offset |
| 3 | Restore CRLFs in PE string sections (.rdata, .rsrc) |
Two-pronged heuristic: (a) byte-before printable AND byte-after printable-or-null, OR (b) ≥3 consecutive printable ASCII bytes AND ≥50% printable density in ±16-byte window. Distinguishes ANSI strings from UTF-16 (no consecutive printables) and binary headers with short text tags like vih8 (low density) |
| 4 | Restore CRLFs in CAB CFDATA blocks | CAB CFDATA csum + deflate decompress-to-declared-length (same as repair.py's stage 2) |
Strengths: zero external dependency. Works on any FTP-corrupted PE + appended CAB file in isolation.
Weaknesses: the PE-string heuristic in stage 3 is tuned for MSVC-compiled binaries with conventional .rdata / .rsrc content (error message tables, RTF resources, version info). Files with unusual section content (heavy UTF-16 string tables, embedded binary blobs in .rdata, custom resource formats) could see false positives or misses. Stage 4 still works regardless and guarantees the CAB extracts correctly.
Both scripts converge on the same answer through different paths:
repair.py |
repair_no_ref.py |
|
|---|---|---|
| Initial strips | 5,545 (alignment-guided) | 5,592 (every 0A after 0D) |
| DOS stub restored | implicit (alignment got it right) | 1 byte (template) |
| PE-section restorations | implicit (alignment got them right) | 41 bytes (heuristic) |
| CAB-region restorations | 7 bytes (csum oracle) | 12 bytes (csum oracle) |
| Net change | -5,538 bytes | -5,538 bytes |
| Output size | 1,464,222 | 1,464,222 |
| Output SHA-256 | 7642116c…356575c |
7642116c…356575c |
The no-reference script does 7-zip-style "everything is broken until proven legitimate" and rebuilds via templates + heuristics + checksums. The reference-driven script trusts the reference for 99% of the file and only escalates to checksum validation for the CAB region where the reference was unreliable.
- Have a reference of any kind? Use
repair.py. It's faster (one stage 1 pass) and more robust to unusual section content. - No reference at all? Use
repair_no_ref.py. It needs the file to be a PE+CAB with MSVC-conventional sections, but works standalone. - Different file type entirely (no PE or no CAB)? Neither script is plug-and-play, but the staged approach (overstrip → template-fix structural regions → checksum-validate payload) generalizes. Swap the CAB walker for ZIP/7z/gzip block validation.