tpbreak eats Fortinet: outer XOR, FORT-RC4, and the ARM shift nobody published
tpbreak was already chewing through TP-Link, D-Link, QNAP, Synology, Wecon, the usual suspects. Fortinet sat on the list for a while as "someone else's problem" because the public writeups are good and the tools exist. then i actually tried to feed a current .out through the published path end-to-end and watched half of it fail depending on product, arch, and whether you trusted the wrong VA pair for the RSA blob.
so the work wasn't inventing FortiOS crypto from scratch. it was making the full chain reliable, finding where aarch64 disagrees with x86 on a two-bit shift, and shipping it as another auto vendor on tpbreak instead of a pile of one-off scripts.
what a FortiOS .out actually is
a modern FortiGate/FortiWeb VM image is not one cipher. it's a stack:
# simplified pipeline
.out file
│ often gzip wrapper with a Fortinet-looking fname
▼
outer body: independent 512-byte blocks, rolling XOR
│ 32-char alphanumeric key, IV 0xFF
│ KPA on 32 nulls @ +48, magic FF 00 AA 55 @ +12
▼
disk image / MBR + ext2 partition
│ pure blockmap extract (no loop mount needed)
▼
flatkc rootfs.gz
│ │
│ ├─ last 256 bytes = RSA-2048 sig/trailer
│ └─ body = FORT-RC4 ciphertext of a gzip initrd
▼
vmlinux-ish ELF (x86 bzImage) or aarch64 Image
holds obfuscated RSA pubkey used to open the trailer
layer 0 is Bishop Fox / FortiCrack:
known-plaintext on the null pad, validate the header contains
build, then decrypt every 512-byte block with the same key.
that's the easy part. our lab set all shared the outer key
aA8BWlDd0EFfCQUh8IAJMKZLmMCNYOzP. nice when it happens, not assumed.
after outer decrypt you do not get a contiguous flatkc sitting at a fixed offset. the boot members live in an ext2 filesystem. binwalk can get you there; so can a few hundred lines of pure-Python blockmap walking, which is what tpbreak uses so the service doesn't depend on a working binwalk install (the pip package is still a mess on older Pythons).
rootfs: the part people get wrong by year
once you have rootfs.gz, the scheme depends on FortiOS generation:
- older / mid 7.x — ChaCha20 with a seed pulled out of the kernel
(Bishop Fox "further adventures", Optistream
getrootfskey, GreyNoise on 7.0 aarch64). non-RFC seek:seek(le32(iv[:4])*64), nonce =iv[4:]. - 7.4.3+ / RandoriSec on 7.4.7 — RSA pubkey obfuscated with ChaCha from a 32-byte seed; trailer holds AES-256 key + custom CTR counter; body is AES-CTR with a nibble-fold increment.
- 8.0 and, for our builds, also 7.6.7 —
forticrack_v8:
RSA pubkey XOR-obfuscated in the kernel ELF (270-byte DER ^ 32-byte key),
trailer is PKCS#1 v1.5 with
SHA256(body)at0xA0and a 32-byte RC4 key at0xE0, body is FORT-RC4.
FORT-RC4 is not stock RC4. KSA is normal. PRGA mixes the S-box with
((i << 5) ^ (j >> 3)) style indices and an 0xAA
constant. FortiGate builds reset j after KSA; FortiWeb often
does not. try both. that detail is in forticrack_v8 and it matters.
the RSA blob VAs for build 0167 are published. they still move. ALI images
were not at the KVM pair. scanning the high PT_LOAD for
30 82 … after XOR with a nearby 32-byte key, then validating
against the rootfs trailer hash, is what actually opened FGVMAL-8.00 without
hand-picking VAs every time.
seven images, six fell over, one did not
lab set (all hosted under https://afflicted.sh/fw/fortinet/):
FFW_VM64_KVM-v7.6.7 → FORT-RC4 / FGT variant → 156M cpio
FFW_VM64_KVM-v8.0.0 → FORT-RC4 / FGT → 158M cpio
FGT_VM64_ALI-v7.6.7 → FORT-RC4 / FGT → 126M cpio
FGT_VM64_ALI-v8.0.0 → FORT-RC4 / FGT → 128M cpio
FGT_VM64_KVM-v7.6.7 → FORT-RC4 / FFW → 159M cpio
FGT_VM64_KVM-v8.0.0 → FORT-RC4 / FFW → 161M cpio
FGT_ARM64_KVM-v7.6.7 → ??? → stuck
x86 was boring once the VA scan and native helpers were in. pure Python FORT-RC4 on a 90MB body is fine for a proof and awful for a web service, so there's a small C helper for the outer XOR and another for FORT-RC4. full pipeline on a lab image is ~6–10 seconds on the box.
ARM was the one that lied.
ARM64: correct RSA, wrong stream
FGT_ARM64_KVM-v7.6.7 / FGVMKA-7.06-build3704:
- outer KPA: same key as the x86 lab set
- ext2 carve: fine — 17MB flatkc (MZ +
ARMd), 82MB rootfs.gz - no honest vmlinux ELF from gunzip/xz of the Image the way x86 gives you one from bzImage
- XOR-scan of flatkc still found the RSA blob at
flatkc+0xEEC140, key 32 bytes earlier at+0xEEC120 - RSA open of the trailer: valid PKCS#1,
SHA256(body)matched at0xA0, RC4 material at0xE0
then FORT-RC4 with the published PRGA produced garbage. AES-CTR (RandoriSec
layouts, every endian and counter field i could invent), ChaCha seed scans,
keystream drop-N, XTS, reverse key bytes — nothing gave 1f 8b.
the trailer hash matching is not a soft check. if
pow(sig, e, n) produces PKCS#1 with the real body hash, the
public key and the key material in that trailer are the ones Fortinet put
there. so the algorithm was wrong, not the key.
the aarch64 PRGA is the same cipher with the shifts flipped
x86 / forticrack_v8:
idx1 = ((i << 5) ^ (j >> 3)) & 0xff
idx2 = ((j << 5) ^ (i >> 3)) & 0xff
mix = (S[idx2] + S[idx1]) ^ 0xaa
brute over the two shift constants on the ARM keystream, keeping everything
else identical (KSA, 0xAA, FGT j reset):
FORT VAR HIT sh_a=3 sh_b=5 xor=0xaa mix_add reset_j=True
magic 1f 8b 08 00
cpio 070701 … 138616716 bytes
so aarch64 is:
idx1 = ((i << 3) ^ (j >> 5)) & 0xff
idx2 = ((j << 3) ^ (i >> 5)) & 0xff
same FORT-RC4, same trailer layout, swapped shift pair. that is the entire
difference that kept the seventh image closed. once the C helper
(fort-rc4-arm) existed, the production path just probes x86
shifts then ARM shifts on the first 64 bytes before spending time on the
full body.
what's live on tpbreak
vendor id fortinet. content-sniff on the .out head, same as the
other modules. analyze on a catalog key returns outer key, scheme, product
variant, and a downloadable decrypt that is the initrd cpio — not the outer
body alone.
# live analyze (ARM lab image)
status: decrypted
format: Fortinet rootfs.gz (FORT-RC4-ARM → gzip) → cpio
decryption: outer aA8BWlDd0EFfCQUh8IAJMKZLmMCNYOzP
· FORT-RC4-ARM · FGT · FGVMKA-7.06-FW-build3704-26060
payload: squashfs @ 0x34d26da, xz, …
catalog rows under firmware/fortinet/* point at
https://afflicted.sh/fw/fortinet/. all seven lab images open.
coverage text on the site used to claim "outer XOR only" because that was
true when the first stub landed; that note is wrong now and has been fixed.
what this is not: a claim that every FortiOS build from 6.x through whatever ships next month will hit the same path. older pure-ChaCha kernels and pure RandoriSec AES-CTR builds are still second-class citizens in the auto pipeline. the lab set that matters for current VM images — 7.6.7 and 8.0.0, FGT and FFW, KVM and ALI, x86_64 and ARM64 — is fully open.
credits, not reinvention
outer scheme: Bishop Fox FortiCrack. FORT-RC4 and the 8.0 trailer layout: hacefresko/forticrack_v8. AES-CTR era B: RandoriSec. ChaCha seed tooling: Optistream fortigate-crypto, GreyNoise on aarch64 7.0. i glued it, fixed the VA mess on ALI, found the ARM shift swap, and put it behind the same button as the rest of the fleet.
service: tpbreak.afflicted.sh
lab images: afflicted.sh/fw/fortinet/