_._     _,-'""`-._
(,-.`._,'(       |\`-/|
    `-.-' \ )-`( , o o)
          `-    \`_`"'-

tpbreak eats Fortinet: outer XOR, FORT-RC4, and the ARM shift nobody published

2026-07-30 · tpbreakfortinetfortiosfirmwarereverse-engineeringfort-rc4aarch64

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:

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:

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/