Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__/
.venv*/
200 changes: 151 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,63 +1,165 @@
# FortiGate `rootfs.gz` decryption tool

Fortinet regularly updates crypto obfuscation for their firmware.
`decrypt_rootfs.py` decrypts FortiGate VM `rootfs.gz` ramdisk payloads using
the matching `flatkc` kernel ELF from the same firmware image.

`decrypt_rootfs.py` is an all-in-one tool allowing decryption of `rootfs.gz` (ramdisk file).
Fortinet changes this boot-time crypto code across releases. The tool supports
both the older ChaCha20/AES path and the newer XOR/RSA/custom-stream path, and
selects the supported path automatically.

Tested on:
- FortiGate 7.4.7
## Supported versions

Tested targets:

| Version | Status | Notes |
| --- | --- | --- |
| FortiGate 7.4.7 | Supported | Original ChaCha20-obfuscated RSA key and AES body decrypt path. |
| FortiGate 7.6.6 | Supported | New verifier path with XOR-recovered RSA key and custom stream body decrypt. |
| FortiGate 8.0.0 | Supported | New verifier path with alternate stream PRGA initialization. |

Community-reported targets:

| Version | Status | Notes |
| --- | --- | --- |
| FortiGate 7.4.6 | Reported working | Legacy decrypt path with the stack-size fallback locator. |
| FortiGate 7.6.3 ALI build3510 | Not merged | Upstream PR reports a different legacy layout; not validated in this fork. |

Other FortiGate releases may work if they use one of these verifier layouts.
Run with `--debug` if a nearby release fails, then compare the verifier code in
`.init.text`.

## Inputs

The script needs two files from the same firmware image:

- `flatkc`: the extracted kernel ELF, usually named `flatkc.elf` after unpacking.
- `rootfs.gz`: the encrypted root filesystem payload. The final 256 bytes are
the RSA signature/trailer used to recover and verify the body decryption key.

The output is a decrypted gzip-compressed cpio archive. It is still compressed;
extract it after decryption.

## Requirements

- Python 3
- GNU `objdump` for the legacy verifier path
- Python packages from `requirements.txt`

The Python dependencies include `miasm`, `pycryptodome`, `pyasn1`,
`pyasn1_modules`, `pyfiglet`, and `tqdm`.

## Setup

```bash
$ python3 -m venv .venv
$ . .venv/bin/activate
(.venv) $ pip install -r requirements.txt
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
```

## Usage

```bash
user@randorisec:~/fortigate-crypto$ python decrypt_rootfs.py flatkc.elf.x64.7.4.7 ./vmdk/fs/rootfs.gz rootfs.gz.dec
____ _ _ _ ____ ___ ____ ___ ____ _____ ____
| _ \ / \ | \ | | _ \ / _ \| _ \|_ _/ ___|| ____/ ___|
| |_) | / _ \ | \| | | | | | | | |_) || |\___ \| _|| |
| _ < / ___ \| |\ | |_| | |_| | _ < | | ___) | |__| |___
|_| \_\/_/ \_\_| \_|____/ \___/|_| \_\___|____/|_____\____|


https://randorisec.fr


[INFO] Retrieving crypto material...
[INFO] Decrypting ./vmdk/fs/rootfs.gz...
73822496it [00:19, 3812345.28it/s]
[INFO] DONE.
user@randorisec:~/fortigate-crypto$ file rootfs.gz.dec
rootfs.gz.dec: gzip compressed data, last modified: Mon Jan 20 18:44:06 2025, from Unix, original size modulo 2^32 119727260
user@randorisec:~/fortigate-crypto$ gzip -dc -S .dec < rootfs.gz.dec > rootfs.cpio
user@randorisec:~/fortigate-crypto/tmp$ mkdir tmp; cd tmp; sudo cpio -idv < ../rootfs.cpio
user@randorisec:~/fortigate-crypto/tmp$ ll
total 47212
drwxr-xr-x 13 user user 4096 Feb 20 13:32 ./
drwxr-xr-x 5 user user 4096 Feb 20 13:32 ../
-r--r--r-- 1 root root 33109932 Feb 20 13:32 bin.tar.xz
drwxr-xr-x 2 root root 4096 Feb 20 13:32 boot/
drwxr-xr-x 3 root root 4096 Feb 20 13:32 data/
drwxr-xr-x 2 root root 4096 Feb 20 13:32 data2/
drwxr-xr-x 8 root root 20480 Feb 20 13:32 dev/
lrwxrwxrwx 1 root root 8 Feb 20 13:32 etc -> data/etc/
lrwxrwxrwx 1 root root 1 Feb 20 13:32 fortidev -> //
lrwxrwxrwx 1 root root 10 Feb 20 13:32 init -> /sbin/init*
drwxr-xr-x 5 root root 4096 Feb 20 13:32 lib/
lrwxrwxrwx 1 root root 4 Feb 20 13:32 lib64 -> /lib/
-r--r--r-- 1 root root 14456836 Feb 20 13:32 migadmin.tar.xz
-r--r--r-- 1 root root 549180 Feb 20 13:32 node-scripts.tar.xz
drwxr-xr-x 2 root root 4096 Feb 20 13:32 proc/
drwxr-xr-x 2 root root 4096 Feb 20 13:32 sbin/
drwxr-xr-x 2 root root 4096 Feb 20 13:32 sys/
drwxr-xr-x 2 root root 4096 Feb 20 13:32 tmp/
drwxr-xr-x 3 root root 4096 Feb 20 13:32 usr/
-r--r--r-- 1 root root 148572 Feb 20 13:32 usr.tar.xz
drwxr-xr-x 9 root root 4096 Feb 20 13:32 var/
python decrypt_rootfs.py <flatkc.elf> <encrypted-rootfs.gz> <decrypted-rootfs.cpio.gz>
```

Example:

```bash
python decrypt_rootfs.py \
flatkc.elf \
rootfs.gz \
rootfs.cpio.gz
```

For additional locator/decryption details:

```bash
python decrypt_rootfs.py --debug flatkc.elf rootfs.gz rootfs.cpio.gz
```

## Verify the output

The decrypted output should start with gzip magic and pass `gzip -t`:

```bash
gzip -t rootfs.cpio.gz
file rootfs.cpio.gz
```

Expected `file` output is similar to:

```text
rootfs.cpio.gz: gzip compressed data, from Unix
```

## Extract the decrypted rootfs

```bash
mkdir rootfs
gzip -dc rootfs.cpio.gz | (cd rootfs && sudo cpio -idmv)
```

`sudo` is useful because FortiGate ramdisks contain root-owned files, device
nodes, and absolute symlinks. If you only need a quick listing, you can inspect
the archive without extraction:

```bash
gzip -dc rootfs.cpio.gz | cpio -it | less
```

## How it works

The script first splits the encrypted `rootfs.gz` into:

- encrypted rootfs body
- 256-byte RSA trailer/signature

It then tries the newer verifier path:

1. Parse `flatkc` as an ELF64 file.
2. Use `__ksymtab` to locate `rsa_parse_pub_key`, even when kallsyms are
tampered.
3. Locate the init-time verifier callsite in `.init.text`.
4. Recover the DER-encoded RSA public key by XORing a 270-byte blob with a
repeating 32-byte pad.
5. Verify the RSA trailer and `sha256(encrypted-rootfs)`.
6. Recover the 32-byte stream key and decrypt the body with the FortiOS stream
transform.

If that path is unavailable, the script falls back to the legacy path:

1. Locate `fgt_verify_initrd` from the `rsa_parse_pub_key` callsite.
2. If the callsite is unavailable in the symbolized kernel, try a stack-size
heuristic reported for nearby 7.4.x builds.
3. Recover the ChaCha20-obfuscated RSA public key.
4. Verify the RSA trailer and encrypted rootfs hash.
5. Decrypt the body using the AES-derived stream used by older releases.

## Troubleshooting

`New verifier path unavailable, falling back to legacy path`

This is normal for older supported releases. Use `--debug` to see the exception
that caused the newer path to be skipped.

`expected a little-endian ELF64 flatkc`

The first argument is not the expected x86-64 kernel ELF. Re-check your firmware
extraction and make sure you are passing `flatkc.elf`, not the compressed kernel
container.

`rootfs hash check failed`

The `flatkc` and `rootfs.gz` probably came from different firmware images, or
the encrypted `rootfs.gz` was truncated or modified.

`neither stream mode produced gzip magic`

The RSA trailer verified, but the body stream layout did not match the tested
7.6.6/8.0.0 variants. Diff the verifier's body-decrypt helper against a known
supported build.

## Notes

This tool is intended for firmware analysis and defensive research. It does not
modify the input files.
Loading