bishopfox.com Open in urlscan Pro
2606:4700:20::ac43:532a  Public Scan

URL: https://bishopfox.com/blog/breaking-fortinet-firmware-encryption
Submission: On March 06 via manual from TR — Scanned from DE

Form analysis 2 forms found in the DOM

<form id="mktoForm_1049" __bizdiag="196351718" __biza="W___" novalidate="novalidate" style="font-family: inherit; font-size: 13px; color: rgb(51, 51, 51); width: 1601px;" class="mktoForm mktoHasWidth mktoLayoutLeft" digitalpi-utms-added="true">
  <style type="text/css"></style>
  <div class="mktoFormRow">
    <div class="mktoFieldDescriptor mktoFormCol" style="margin-bottom: 5px;">
      <div class="mktoOffset" style="width: 5px;"></div>
      <div class="mktoFieldWrap mktoRequiredField"><label for="Email" id="LblEmail" class="mktoLabel mktoHasWidth" style="width: 100px;">
          <div class="mktoAsterix">*</div>Email Address:
        </label>
        <div class="mktoGutter mktoHasWidth" style="width: 5px;"></div><input id="Email" name="Email" maxlength="255" aria-labelledby="LblEmail InstructEmail" type="email" class="mktoField mktoEmailField mktoHasWidth mktoRequired"
          aria-required="true" style="width: 320px;"><span id="InstructEmail" tabindex="-1" class="mktoInstruction"></span>
        <div class="mktoClear"></div>
      </div>
      <div class="mktoClear"></div>
    </div>
    <div class="mktoClear"></div>
  </div>
  <div class="mktoButtonRow"><span class="mktoButtonWrap mktoNative" style="margin-left: 110px;"><button type="submit" class="mktoButton">Submit</button></span></div><input type="hidden" name="formid" class="mktoField mktoFieldDescriptor"
    value="1049"><input type="hidden" name="munchkinId" class="mktoField mktoFieldDescriptor" value="136-UTJ-516"><input type="hidden" name="Utm_Orig_Medium__c" class="mktoField mktoFieldDescriptor" value="none"><input type="hidden"
    name="Utm_Orig_Source__c" class="mktoField mktoFieldDescriptor" value="none">
</form>

<form __bizdiag="-1328652579" __biza="W___" novalidate="novalidate" style="font-family: inherit; font-size: 13px; color: rgb(51, 51, 51); visibility: hidden; position: absolute; top: -500px; left: -1000px; width: 1600px;"
  class="mktoForm mktoHasWidth mktoLayoutLeft"></form>

Text Content

Bishop Fox named “Leader” in 2024 GigaOm Radar for Attack Surface Management.
Read the Report ›

Cosmos
Services
Resources
Customers
Partners
About Us
Get Started

Introducing Cosmos


NAMED LEADER OF THE GIGAOM RADAR FOR THE THIRD YEAR IN A ROW!

Request A Demo
Cosmos Overview


Meet Cosmos: The continuous offensive security solution designed to provide
proactive defense.

Cosmos Attack Surface Management


Get Cosmos Attack Surface Management (CASM) for unmatched visibility into your
changing external attack surface with continuous discovery and mapping.

Cosmos Application Penetration Testing


Cosmos Application Penetration Testing (CAPT) strengthens the security of
business-critical applications with in-depth assessments.

Cosmos External Penetration Testing


Cosmos External Penetration Testing (CEPT) builds on Cosmos Attack Surface
Management to provide the highest level of attack surface protection with
post-exploitation activities.

The Best Defense is a Great Offense


SEE WHY WE'RE THE LEADERS IN OFFENSIVE SECURITY

Explore Services
Application Security


Ensure your applications are secure and improve your DevSecOps practices.

 * Application Pen Testing
 * Hybrid App Assessment
 * Mobile App Assessment
 * View More

Red Team & Readiness


Get a holistic view of your ability to defend against a real-world attack.

 * Social Engineering
 * Incident Response Tabletop Exercise
 * Ransomware Readiness

IoT & Product Security


Validate interconnected devices and products are secure against attackers.

Cloud Security


Assess cloud security posture with expert testing and analysis of your
environment.

Network Security


Get insight into how skilled adversaries could establish network access and put
sensitive systems and data at risk.

 * External Pen Testing
 * Internal Pen Testing
 * Continuous Attack Surface Testing

Compliance, Regulations, & Frameworks


Satisfy governance, risk, and compliance programs with our testing services.

Assessments for Our Partners


We're proud to work with Google, Facebook, and Amazon to increase security in
their partner ecosystems.

 * Cloud App Security Assessments (CASA)
 * Unqork Security Assessments
 * Meta Workplace Assessments
 * Amazon Alexa Assessments
 * ioXt Alliance Testing & Certification
 * View More

A Ponemon Institute Report


THE STATE OF OFFENSIVE SECURITY

Get the blueprint. Insights into how mature security organizations invest in
offensive strategies.

Get the Report
Resource Center


Discover new offensive security resources, ranging from reports and eBooks to
slide decks from speaking gigs.

 * Webcasts
 * Reports
 * eBooks & Guides
 * Art & Science of Cyber Leadership Series
 * Cybersecurity Style Guide
 * View All

Bulletins & Advisories


Explore the latest security bulletins and advisories released by our team.

 * Exploit for Fortinet CVE-2022-42475
   Latest
 * View All

Blog


Dive into our blog for insights and perspectives from our offensive security
experts.

 * Industry
 * Technology

Bishop Fox Labs


Learn more about our research — and our commitment to openly sharing
information.

Research & Tools


We are the innovators behind some of the most popular open source security
tools. Check them out here!

 * Tool Talk Series
 * What The Vuln Series



Why Partner with Us?


JOIN FORCES WITH THE LEADERS IN OFFENSIVE SECURITY

Independent Assessment by TAG Cyber

Get the Report
Partner Program Overview


Learn about our partner programs and see how we can work together to provide
best-in-class security offerings.

Find a Partner


Check out our awesome ecosystem of trusted partners to find the right solution
for your needs.

Become a Partner


Explore partnership opportunities and apply to join forces with Bishop Fox.

Assessments for Our Partners


We're proud to work with Google, Facebook, and Amazon to increase the security
of their partner ecosystems.

 * Cloud Application Security Assessments
 * Mobile Application Security Assessment
 * Nest Assessments
 * Meta Workplace Assessments
 * Amazon Alexa Assessments

We're Hiring!


WANT TO WORK WITH THE BEST MINDS IN OFFENSIVE SECURITY?

Be part of an elite team and work on projects that have a real impact.

Explore Openings
Company Overview


Get to know us. Learn about our roots and see why we're on a mission to improve
security for all.

Events


Join us at an upcoming event or peruse our speaking engagements, past and
present.

Newsroom


Read the latest articles, announcements, and press releases from Bishop Fox.

Contact Us


Want to get in touch? We're ready to connect.

Career Opportunities


We're hiring! Explore our open positions and discover why the Fox Den is a great
place to build your career.

Intern & Educational Programs


Starting your offensive security journey? Check out our internships and
educational programs.

Bishop Fox Mexico


¡Celebramos! Bishop Fox is now in Mexico. Learn more about our expansion.


Cosmos
 * Overview
 * Cosmos Overview
 * Cosmos Attack Surface Management
 * Cosmos Application Penetration Testing
 * Cosmos External Penetration Testing

Services
 * Overview
 * Application Security
 * Red Team & Readiness
 * IoT & Product Security
 * Cloud Security
 * Network Security
 * Compliance, Regulations, & Frameworks
 * Assessments for Our Partners

Resources
 * Overview
 * Resource Center
 * Bulletins & Advisories
 * Blog
 * Bishop Fox Labs
 * Research & Tools

Customers
Partners
 * Overview
 * Partner Program Overview
 * Find a Partner
 * Become a Partner
 * Assessments for Our Partners

About Us
 * Overview
 * Company Overview
 * Events
 * Newsroom
 * Contact Us
 * Career Opportunities
 * Intern & Educational Programs
 * Bishop Fox Mexico

Get Started
Blog // Tech // Aug 02, 2023


BREAKING FORTINET FIRMWARE ENCRYPTION

By: Jon Williams, Senior Security Engineer


Share




INTRODUCTION


This blog is based on previous research conducted by Carl Livitt, Bishop Fox
alumnus.

The previous article in our Fortinet series, CVE-2023-27997 is exploitable, and
69% of FortiGate firewalls are vulnerable, described how to use intelligent
Shodan queries to identify FortiGate SSL VPN endpoints exposed on the internet.
By comparing the dates in their Last-Modified response headers to patch release
dates, we were able to estimate how many devices were vulnerable to a recently
discovered heap overflow exploit allowing remote code execution.

In this blog, we will dive deeper into an issue we had to overcome to perform
comprehensive research on FortiGate firmware. Our intent here is to expose more
of the (often rigorous) process involved in performing security research, and to
share with the wider security community what we learned along the way.




THE CHALLENGE

Performing comparative analysis on a particular software product requires
obtaining several different versions of that software. In the case of FortiGate,
this was relatively straightforward – all we had to do was register a free
account on Fortinet’s support site and request a free trial license (note that
Fortinet has since restricted trial access – more on this later). We could then
download firmware images for a wide variety of versions, hardware appliances,
and even product lines (not just FortiGate). The total number of available
images numbered in the tens of thousands (!), so we wrote a Python script using
Playwright to help us download as much of the available library as possible.

The trick, however, was this: although FortiGate virtual machines were
distributed in the clear, most of the firmware images intended for bare-metal
deployment were encrypted. Below is a table summarizing the distribution of
cleartext vs. encrypted images for various Fortinet products. FortiGate and
FortiProxy are called out because they were affected by CVE-2023-27997 and
comprise nearly half of the available images. Other products distributed with
encryption included FortiAnalyzer, FortiAuthenticator, FortiMail, FortiManager,
and FortiVoice.



FIGURE 1 - Distribution of encrypted vs. cleartext firmware images across
Fortinet products



To thoroughly compare different versions of FortiGate firmware, we needed to
find a way to decrypt the encrypted firmware images.




REVERSING THE ENCRYPTION SCHEME

Solving this problem required using reverse engineering to meet the following
goals:

 1. Understand the firmware image file formats (cleartext and encrypted)
 2. Locate the logic (in a cleartext image) responsible for decrypting firmware
    (if possible)
 3. Reproduce the decryption function with our own code
 4. Write the corresponding encryption function

If we could accomplish this, then we would understand the encryption method well
enough to identify any weaknesses in its cryptographic implementation. 





UNWRAPPING THE FILE FORMATS

Achieving the first goal was straightforward. A simple run of the file command
revealed that both image formats were compressed with gzip, e.g.:

❯ file FGT_*

FGT_100D-v6-build9451-FORTINET.out: gzip compressed data, was "FG100D-6.04-FW-build1966-230310-patch09", last modified: Fri Mar 10 00:29:41 2023, from Unix, original size modulo 2^32 926036017

FGT_30E-v6-build0076-FORTINET.out: gzip compressed data, was "FGT30E-6.00-FW-build0076-180329-patch00", last modified: Thu Mar 29 03:19:25 2018, from Unix, original size modulo 2^32 926036017


Extracting the files required adding a couple of parameters to the gunzip
command since it did not recognize the .out file extension:

❯ gunzip -cf FGT_100D-v6-build9451-FORTINET.out >FGT_100D-v6-build9451-FORTINET
gunzip: FGT_100D-v6-build9451-FORTINET.out: trailing garbage ignored

❯ gunzip -cf FGT_30E-v6-build0076-FORTINET.out >FGT_30E-v6-build0076-FORTINET
gunzip: FGT_30E-v6-build0076-FORTINET.out: trailing garbage ignored

Once the images were extracted, it was easy to see which were encrypted vs.
cleartext by inspecting them with file and xxd:

❯ file FGT_*

FGT_100D-v6-build9451-FORTINET:    data

FGT_30E-v6-build0076-FORTINET:     DOS/MBR boot sector; partition 1 : ID=0x83, active, start-CHS (0x7,230,32), end-CHS (0xa,50,40), startsector 126976, 36864 sectors; partition 2 : ID=0x83, start-CHS (0xa,50,41), end-CHS (0xc,125,49), startsector 163840, 36864 sectors; partition 3 : ID=0x83, start-CHS (0xc,125,50), end-CHS (0x10,81,1), startsector 200704, 61440 sectors

❯ xxd -l 80 FGT_100D-v6-build9451-FORTINET.out
00000000: 90d0 b0f1 bcda 8be8 85bb f79a f6bc 4c40  ..............L@
00000010: 7f6e 474e 3d2f 0001 2a10 3036 675c 4796  .nGN=/..*.06g\G.
00000020: 8ca7 ab8e f2af d78e ded2 a9f4 acd5 a3f7  ................
00000030: 8ed6 aef7 d48a f0ab deb4 c095 bae8 a6e9  ................
00000040: 86c6 a6e7 aacc 8eed 80be f29f f4be f89f  ................

❯ xxd -l 80 FGT_30E-v6-build0076-FORTINET
00000000: 0000 0000 0000 1100 0000 0000 ff00 aa55  ...............U
00000010: 4647 5433 3045 2d36 2e30 302d 4657 2d62  FGT30E-6.00-FW-b
00000020: 7569 6c64 3030 3736 2d31 3830 3332 392d  uild0076-180329-
00000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000  ................

In this example, the FGT_30E image was identified as a bootable volume with a
build name clearly visible in the 80-byte file header. The FGT_100D image, by
contrast, was only identified as “data” with garbled text in place of a build
name. From this vantage point, you may already see some indicators of weakness
in the encryption scheme.




ACCESSING THE FILE SYSTEM

Having determined the minimal processing needed to differentiate encrypted
images from cleartext ones, our next goal was to locate decryption code within
the cleartext images. We first needed to gain access to the file system, and the
easiest way to do that was to mount the disk image included with one of the
freely distributed virtual machines.

Starting with a VMware image from the Fortinet support site (this example uses
FortiGate version 6.4.13), we performed the following actions:

 1. Unzipped the download and looked for fortios.vmdk within the OVF file
    structure
 2. Instead of importing the FortiGate OVF, attached fortios.vmdk to an existing
    Linux virtual machine (we used Kali Linux), then booted that VM
 3. Logged in and opened a root shell
 4. Created a mount point, e.g., mkdir /mnt/fortigate
 5. Listed attached volumes with fdisk -l and looked for the FortiGate volume
    (usually the second one listed)
 6. Mounted the bootable partition of the FortiGate volume, e.g., mount
    /dev/nvme0n2p1 /mnt/fortigate
 7. Browsed to /mnt/fortios to obtain any necessary files

This is what the process looked like in our case:

❯ fdisk -l
...omitted for brevity...
Device         Boot  Start     End Sectors  Size Id Type
/dev/nvme0n2p1 *      2048  526335  524288  256M 83 Linux
/dev/nvme0n2p2      526336 4194303 3667968  1.7G 83 Linux

❯ mkdir /mnt/fortigate

❯ mount /dev/nvme0n2p1 /mnt/fortigate

❯ ls /mnt/fortigate
boot.msg  datafs.tar.gz  datafs.tar.gz.bak  extlinux.conf  extract.flag  filechecksum  flatkc  flatkc.chk  image.src  ldlinux.c32  ldlinux.sys  lost+found  rootfs.gz  rootfs.gz.chk


We observed that the boot partition contained several archives that would be
decompressed to create the file system during initial setup. With some analysis,
we found that each of the .gz  files was a cpio archive compressed with
gzip. Extracting one of these archives looked like this:

❯ gunzip rootfs.gz

❯ file rootfs
rootfs: ASCII cpio archive (SVR4 with no CRC)

❯ cpio -i <rootfs
...omitted for brevity...
194419 blocks

❯ ls
bin.tar.xz  boot  data  data2  dev  etc  fortidev  init  lib  migadmin.tar.xz  node-scripts.tar.xz  proc  rootfs  sbin  sys  tmp  usr  usr.tar.xz  var


After spending some time reviewing the file structure and noting the large
number of symlinks used in place of various binaries, we determined that the
vast majority of program functionality was handled by a single monolithic binary
located at /sbin/init:

> ls -og sbin
total 180
-rwxr-xr-x 1  22936 Jun 28 20:21 ftar
-rwxr-xr-x 1  15016 Jun 28 20:21 init
-rwxr-xr-x 1 141832 Jun 28 20:21 xz

> file sbin/init
sbin/init: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /fortidev/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=bfdb4a4bf008f20b0bf7c83474a2eeb7cbeacc70, stripped

Also noteworthy were the ftar and xz binaries in the sbin directory, as we found
these were modified from the standard versions and were necessary to extract the
.tar.xz archives that contained other parts of the file system.




HUNTING FOR DECRYPTION CODE

Our next task was to examine the init binary to see if it contained functions to
download and decrypt images. We loaded the binary into Ghidra and dug through
the disassembly to find symbols relating to installation and/or upgrade
processes. The fact that symbols were stripped from the binary made this
analysis extremely time-consuming, but eventually we discovered this gem:



FIGURE 2 - Ghidra decompile view showing the firmware encryption/decryption
algorithm



This function was called by others that were related to firmware upgrades and it
contained multiple XOR operations, both strong indicators that the symbol’s
purpose involved encryption and/or decryption.

Further analysis of the decompiled code confirmed this assumption, so we
proceeded to reproduce the code in Python to have a clean working copy to play
with.




REWRITING THE DECRYPTION FUNCTION

Ghidra generally does a very good job of decompiling code, and this case was no
exception. We analyzed the code structure to understand how it worked, and
identified several key points:

 * The function accepts three inputs:
   * A long integer acting as a pointer to some input data
   * An unsigned integer indicating the size of the input data
   * An integer that alters the function’s flow if equal to 0
 * The size of the input data (in bytes) must be at least 512, and it must be
   divisible by 512.
 * The function contains nested while loops that:
   * Iterate through the input data in 512-byte blocks.
   * Iterate through each block one byte at a time.
   * Modify the value of each byte of input data in sequential order using one
     of two mathematical XOR operations.
   * The two mathematical operations appear to be opposites.
 * The function also contains an internal pointer to some data that:
   * Must exist (the pointer cannot be null).
   * Is used as input to the mathematical operations.
   * Is accessed byte-by-byte within the nested while loops.
   * Never has more than 32-bytes accessed, so it must be 32 bytes long.
 * The third input value determines which of two mathematical operations is used
   to modify the input data.
   * The two inner while loops appear to be nested, but upon closer inspection
     are a misrepresentation of an if/else statement.

In summary:

 * The function takes arbitrary data as input, which must be evenly divisible
   into 512-byte blocks.
 * It uses the input data along with a 32-byte key to perform mathematical
   operations that alter each byte of the input data in sequence (repeating for
   each block).
 * Another input variable controls whether the function encrypts or decrypts the
   input data.
 * The cipher is symmetric; it uses the same key for encrypting and decrypting.

With this understanding of the decompiled code, we wrote a functionally
equivalent decryption function in Python:

def decrypt(ciphertext, key):
    ptr = 0
    num_bytes = len(ciphertext)
    cleartext = bytearray()

    while True:
        key_offset = 0
        block_offset = 0
        previous_ciphertext_byte = 0xFF  # IV is always 0xFF

        # Decrypt one 512-byte block at a time
        while block_offset != 0x200:
            offs = ptr + block_offset
            if offs >= num_bytes:
                return bytes(cleartext)

            # For each byte in the block, bitwise XOR the current byte with the
            # previous byte (both ciphertext) and the corresponding key byte
            ciphertext_byte = ciphertext[offs]
            xor = (
                previous_ciphertext_byte ^ ciphertext_byte ^ key[key_offset]
            ) - key_offset  # subtract the key offset to undo obfuscation
            xor = (xor + 256) & 0xFF  # mod 256 to loop negatives
            cleartext.append(xor)

            # Proceed to next byte
            block_offset += 1
            key_offset = (
                key_offset + 1  # increment key offset
            ) & 0x1F  # mod 32 to loop around the key
            previous_ciphertext_byte = ciphertext_byte

            if block_offset == 0x200:
                # Reached end of block
                break

            if ptr + block_offset > num_bytes:
                # Reached end of file
                return bytes(cleartext)

        # Proceed to next block
        ptr = ptr + 0x200
        if ptr >= num_bytes:
            # Reached end of file
            return bytes(cleartext)

The corresponding encryption function only required changes to three lines:
(lines ending in + key_offset, ^key[key_offset], = xor) 


def encrypt(cleartext, key):
    ptr = 0
    num_bytes = len(cleartext)
    ciphertext = bytearray()

    while True:
        block_offset = 0
        previous_ciphertext_byte = 0xFF
        key_offset = 0

        while block_offset != 0x200:
            offs = ptr + block_offset
            if offs >= num_bytes:
                return bytes(ciphertext)

            cleartext_byte = cleartext[offs] + key_offset
            xor = previous_ciphertext_byte ^ cleartext_byte ^ key[key_offset]
            xor = (xor + 256) & 0xFF
            ciphertext.append(xor)

            previous_ciphertext_byte = xor
            key_offset = (key_offset + 1) & 0x1F
            block_offset += 1

            if block_offset == 0x200:
                break

            if ptr + block_offset > num_bytes:
                return bytes(ciphertext)

        ptr = ptr + 0x200
        if ptr >= num_bytes:
            return bytes(ciphertext)

We then wrote some scaffolding to test the functions using an arbitrary 32-byte
key, and confirmed that they successfully encrypted and decrypted a test string
(validating our understanding of the mathematical operations):

❯ ./encrypt.py
Key:        4b5659463738474e473435594f4e47453450344f384e344546364e4f45524739
Input text: I'm a little teapot, short and stout. Here is my handle; here is my spout.
Ciphertext: fd83b5d0829faa94afe6a58cef2014219545f7878b4d07c408b3c7f43be8913a05230d3c39242d0f3268775a6a0935f8fcd5925c1cd39c8bf54273b1751ada711a22006525685a685350
Cleartext:  I'm a little teapot, short and stout. Here is my handle; here is my spout.

At that point, we believed we had successfully reproduced the cryptographic
algorithm used to decrypt FortiGate firmware images, but of course we could not
confirm that without a valid key. Our reverse engineering effort had not
revealed hard-coded decryption keys within the init binary nor anywhere else on
the system.

To obtain decryption keys, then, we needed to analyze the cryptographic scheme
for weaknesses that might allow us to derive them.




PERFORMING CRYPTANALYSIS

Looking over the structure of the encrypt function above, we saw two nested
while loops:

 * The outer loop iterates through 512-byte blocks of data and processes them
   separately without passing any input from one to the next. This appears to be
   a basic block cipher like AES (Advanced Encryption Standard) using ECB
   (Electronic Code Book) mode.
 * The inner loop iterates through the bytes within a block and processes each
   byte using a mathematical XOR operation that includes a key byte as well as
   the output from the operation on the previous byte. This looks like a basic
   stream cipher.

We then had to review best practices to recall which features of block and
stream ciphers mitigate cryptographic attacks.




REVIEWING BLOCK CIPHER SECURITY

ECB is the simplest block cipher mode in AES encryption. In this mode, the
cleartext is divided into blocks of equal size, and each block is encrypted
separately using a key as input:

FIGURE 3 - Electronic Codebook (ECB) mode encryption (image credit: Wikipedia)



This method of encryption can be performed very quickly because the blocks can
be processed in parallel, but it does not hide code patterns well because
identical cleartext blocks produce identical ciphertext blocks after encryption.

CBC (Cipher Block Chaining) mode improves on this method by XOR’ing the initial
block of cleartext with a non-repeating, pseudo-random initialization vector
(IV) before it is encrypted with the key. Each resulting block of ciphertext is
XOR’d with the next block of cleartext before encrypting it. The improvements
made in CBC ensure that any patterns in the cleartext are obscured after
encryption, as highlighted by this comparison:



FIGURE 4 - Comparison of ECB and CBC encryption modes (image credit: Wikipedia)



Since we saw that the Fortinet encryption function was using a block cipher
similar to AES in ECB mode, we assumed that it had the same weaknesses: it would
produce recognizable patterns in ciphertext, and it would decrypt identical
ciphertext to identical cleartext. This only constituted the outer layer of the
encryption function, however, so we moved on to the stream cipher.




REVIEWING STREAM CIPHER SECURITY

> A stream cipher is a symmetric key cipher where plaintext digits are combined
> with a pseudorandom cipher digit stream (keystream). In a stream cipher, each
> plaintext digit is encrypted one at a time with the corresponding digit of the
> keystream, to give a digit of the ciphertext stream. - Wikipedia

In practice, a static key is typically used to seed an algorithm that produces
the keystream, and XOR is the operation used to combine the keystream with the
cleartext to produce ciphertext.



FIGURE 5 - Basic stream cipher encryption (image credit: The SSL Store)



This describes the inner loop of the Fortinet encryption function reasonably
well, with one crucial difference: the “keystream” in this cipher consists
solely of the 32-byte key repeated 16 times. Similar to what we saw with the
block cipher, repetition of the key can produce recognizable patterns in the
ciphertext. Furthermore, because of the mathematical properties of the XOR
operation, the repetition allows us to conduct a known-plaintext attack against
any 32 bytes of the ciphertext and recover the key (more on this below).




IDENTIFYING CIPHER WEAKNESSES

To summarize what we learned about the Fortinet encryption cipher after
reviewing standard encryption techniques:

 * The block cipher used in the outer loop lacks chaining to prevent
   recognizable patterns between 512-byte blocks of ciphertext.
 * The stream cipher used in the inner loop lacks a pseudo-random keystream to
   mitigate known-plaintext attacks against each 32-byte sequence within the
   ciphertext.

We therefore concluded that it was possible to perform a known-plaintext attack
against any 32 bytes in the ciphertext to recover the encryption key, which we
could then use to decrypt the entire file.

Since we had access to encrypted and cleartext firmware images, all we had to do
was compare them and make an educated guess about where we could predict 32
bytes of cleartext that would be the same in every decrypted image.




CONDUCTING PATTERN ANALYSIS

We returned to our earlier comparison of the file headers of two firmware
images:

❯ xxd -l 80 FGT_30E-v6-build0076-FORTINET
00000000: 0000 0000 0000 1100 0000 0000 ff00 aa55  ...............U
00000010: 4647 5433 3045 2d36 2e30 302d 4657 2d62  FGT30E-6.00-FW-b
00000020: 7569 6c64 3030 3736 2d31 3830 3332 392d  uild0076-180329-
00000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000  ................

❯ xxd -l 80 FGT_100D-v6-build9451-FORTINET.out
00000000: 90d0 b0f1 bcda 8be8 85bb f79a f6bc 4c40  ..............L@
00000010: 7f6e 474e 3d2f 0001 2a10 3036 675c 4796  .nGN=/..*.06g\G.
00000020: 8ca7 ab8e f2af d78e ded2 a9f4 acd5 a3f7  ................
00000030: 8ed6 aef7 d48a f0ab deb4 c095 bae8 a6e9  ................
00000040: 86c6 a6e7 aacc 8eed 80be f29f f4be f89f  ................



As we anticipated, patterns were evident even within the first 80 bytes. The
cleartext image began with a series of six null bytes, followed by a single byte
value, five more null bytes, then four “magic bytes” that seemed to serve the
purpose of a file signature. The next 32 bytes comprised part of the image name
(with at least 30 printable characters) and were followed by 32 null bytes.

We compared the headers of several cleartext images and found this pattern to be
remarkably consistent. We were able to confirm that the magic bytes were always
present and immediately followed by the letters “FG” (if the product was
FortiGate). The rest of the image name was not always formatted consistently,
but always contained the word “build.” Null bytes from offsets 48 to 79 were
consistent as well.

One important thing we noted, however, was that this “file header” did not
always appear at the beginning of the file. Several firmware images had one or
more blocks with other content (often just null bytes) preceding it, but this
header always appeared at a 512-byte block boundary somewhere in the file.


Thus, our strategy became apparent:

 1. Read 32 bytes from the first 512-byte block of ciphertext, starting at
    offset 48.
 2. Encrypt each of these bytes with its corresponding known plaintext (in each
    case, a null byte) to produce a key.
 3. Use the key to decrypt the first 80 bytes of the block and validate that the
    content matches the standard file header:
    1. 4 “magic bytes” at offset 12
    2. 30 printable characters at offset 16
    3. The word “build” somewhere in that 30-character string
 4. Repeat the above for each 512-byte block in the file until a valid key is
    found.
 5. Use the valid key to decrypt the entire firmware image.
    


RECOVERING KEYS

We wrote some Python code to perform the key recovery attack against bytes 48–79
from any 512-byte block. This started with a function to perform the byte-level
XOR operation, which you will recognize as the same code used in our
encrypt function above, but with the cleartext and key bytes swapped:

def derive_key_byte(
    key_offset, ciphertext_byte, previous_ciphertext_byte, known_plaintext
):
    key_byte = (
        previous_ciphertext_byte ^ (known_plaintext + key_offset) ^ ciphertext_byte
    )
    key_byte = (key_byte + 256) & 0xFF  # mod 256 to loop negatives
    return key_byte

This works because the following XOR operations are all true (we’re using a
caret to represent XOR here, since that is what Python code uses):

A ^ B = C
A ^ C = B
B ^ C = A

If we substitute the variable letters with the components of our encryption
cipher, it gets a little more interesting:

key ^ cleartext = ciphertext (encrypt)
key ^ ciphertext = cleartext (decrypt)
cleartext ^ ciphertext = key (recover)

Thus, if we know the cleartext and ciphertext, we can derive the key simply by
feeding them into the encryption algorithm. Note that this can become more
complicated when the encryption cipher does more than a simple XOR operation:
decryption must perform the mathematical inverse of the encryption operations,
while recovery can simply reuse the encryption operations.

The next function we wrote implemented recovery of the full key:

def derive_block_key(ciphertext):
    key = bytearray()
    known_plaintext = 0x00

    # Derive the key for this block
    for i in range(32):
        key_offset = (i + 16) % 32  # mod 32 to wrap around key
        plaintext_offset = i + 48
        ciphertext_byte = ciphertext[plaintext_offset]
        previous_ciphertext_byte = ciphertext[plaintext_offset - 1]
        key.append(
            derive_key_byte(
                key_offset, ciphertext_byte, previous_ciphertext_byte, known_plaintext
            )
        )
    key = key[16:] + key[:16]  # swap the first/second halves of the key
    return key


You will note that we reversed the first and second halves of the key after
recovery – this is because of the position of the known plaintext we chose.
Offset 48 from the start of the ciphertext block corresponds to offset 16 from
the start of the key, so we actually recover the last half of the key before the
first half.

The encrypt function adds the offset from the start of the key to the value of
the key byte before XOR’ing it with the other inputs (it is unclear to us why
this was done), so we have to be sure we encrypt the correct key offset along
with each ciphertext byte in order to derive the correct key byte.

After deriving the full key for the block, we used a modified version the
decrypt function above to decrypt the first 80 bytes of the block, and wrote a
validation function to check if it contained the expected content:

def validate_decryption(cleartext):
    if (
        # Length must be at least 80 chars
        len(cleartext) >= 80
        # Validate the file signature "magic bytes"
        and cleartext[12:16] == b"\xff\x00\xaa\x55"
    ):
        # Make sure the image name is readable
        try:
            image_name = cleartext[16:46].decode("utf-8", errors="strict")
        except:
            return False
        # Make sure the word "build" is in the image name
        if "build" in image_name.lower():
            # Valid Fortinet image
            return True
    # Unknown format
    return False

We ran the code against the first 512-byte block from one of the encrypted
firmware images and confirmed that it successfully recovered a valid key:

❯ ./decrypt.py
[+] Decrypting ./FGT_100D-v6-build9451-FORTINET.out
[+] Loaded image data
[+] Found key: oAbBIcDde7FfgGHhiIjJ7KlLmsnN3OPP
[+] Validated: FG100D-6.04-FW-build1966-23031

As we discovered during our analysis, the “file header” did not always appear at
the start of every file, so it was necessary to split the encrypted image into
512-byte blocks and attempt to derive a key from each block. Fortunately, the
weak block cipher did not prevent us from processing the blocks in parallel.

The function below uses the Python multiprocessing library to do just that (and
terminate as soon as it finds a valid key):

def derive_key(ciphertext):
    # Determine the number of blocks to read
    num_blocks = (len(ciphertext) + BLOCK_SIZE - 1) // BLOCK_SIZE
    block_header_size = 80

    # Create a pool of worker processes
    with multiprocessing.Pool(processes=multiprocessing.cpu_count()) as pool:
        # Start the workers
        results = [
            pool.apply_async(
                derive_block_key,
                (  # Each worker attacks the 80-byte header of a 512-byte block
                    ciphertext[
                        block_num * BLOCK_SIZE : block_num * BLOCK_SIZE
                        + block_header_size
                    ],
                ),
            )
            for block_num in range(num_blocks)
        ]
        # Look for a successful result
        for result in results:
            key = result.get()
            if key:
                # Kill the workers as soon as we find a valid key
                pool.terminate()
                pool.join()
                return key
    return None




DECRYPTING FIRMWARE

Having successfully cracked Fortinet’s encryption scheme, we just had to put all
the pieces together into an easy-to-use decryption tool. We are pleased to
release the fruits of our labor to the public: download FortiCrack today!

❯ ./forticrack.py FGT_100D-v6-build9451-FORTINET.out
 ___  __   __  ___    __   __        __       
|__  /  \ |__)  |  | /  ` |__)  /\  /  ` |__/ 
|    \__/ |  \  |  | \__, |  \ /~~\ \__, |  \ 

[+] Decrypting FGT_100D-v6-build9451-FORTINET.out
[+] Loaded image data
[+] Found key: oAbBIcDde7FfgGHhiIjJ7KlLmsnN3OPP
[+] Validated: FG100D-6.04-FW-build1966-23031
[+] Decrypted: FGT_100D-v6-build9451-FORTINET.decrypted

Once we had a working decryption tool, all that was left was to derive keys for
all the firmware images and decrypt them. For each image, after deriving its
key, we used multiprocessing to decrypt each 512-byte block in parallel, then
reassemble the decrypted file. As a result, most firmware images completed
decryption in less than a minute.

Decryption of the FortiGate images proceeded smoothly, but we ran into hiccups
with some of the other product lines that required us to tweak the decryption
algorithm. Although most products used a similar 80-byte “file header,” small
differences in the known plaintext for some, such as the offset of the first
non-null byte or the order of the magic bytes (big endian vs. little endian for
different architectures), required extra steps in the key recovery process.

In the end, our efforts were successful – here is the final tally of the
Fortinet firmware images we decrypted: 




FIGURE 6 - Total numbers of decrypted Fortinet firmware images

Some other interesting trends emerged at this stage. From the approximately 29k
images we decrypted across 28 product lines, there were only 25 unique
encryption keys, indicating rampant key reuse. Furthermore, we discovered that
valid keys used only alphanumeric characters, so we added an additional key
validation step (prior to decryption and content validation) to speed up the key
recovery process.

In the end, we found that many of the keys were, in fact, hard coded within
Fortinet firmware images, but we were unable to recognize them as keys before we
put in the effort to determine their characteristics. Nevertheless, our work
provided us with a reliable way to recover the keys whether or not they were
hard coded.




EXTRACTING FILE CONTENTS

For those following along at home, we will also show how to extract the file
contents from a decrypted firmware image. Typically, the easiest way is to mount
the image file on a Linux system – most of the images have the base filesystem
starting at offset 512, right after the MBR data. For example:

❯ mkdir firmwarefs

❯ sudo mount -o ro,loop,offset=512 FGT_100D-v6-build9451-FORTINET.decrypted firmwarefs

❯ ls -al firmwarefs                                                                            
total 48708
drwxr-xr-x  8 root root     4096 Aug 19  2020 .
drwxrwxrwx  1 root root     4096 Jul 11 09:20 ..
drwxr-xr-x  2 root root     4096 Aug 19  2020 bin
drwxr-xr-x  2 root root     4096 Aug 19  2020 cmdb
drwxr-xr-x  2 root root     4096 Aug 19  2020 config
-rw-r--r--  1 root root     6110 Aug 19  2020 devicetree.dtb
drwxr-xr-x 11 root root     4096 Aug 19  2020 etc
-rw-r--r--  1 root root       86 Aug 19  2020 filechecksum
-rwxr-xr-x  1 root root  3319135 Aug 19  2020 flatkc
-rw-r--r--  1 root root      256 Aug 19  2020 flatkc.chk
drwxr-xr-x  2 root root     4096 Aug 19  2020 lib
drwx------  2 root root    16384 Aug 19  2020 lost+found
-rw-r--r--  1 root root 46431042 Aug 19  2020 rootfs.gz
-rw-r--r--  1 root root      256 Aug 19  2020 rootfs.gz.chk

Some images do not follow this convention, in which case you may need to
manually locate the beginning of the filesystem. For example, the firmware image
in the figure below has a filesystem that starts at offset 0x400000:

❯ xxd FMG_300F-v7.4.0-build2223-FORTINET.decrypted | grep -A10 ": 0000 0100 0000 04"

00400400: 0000 0100 0000 0400 0000 0000 c7fe 0200  ................
00400410: f1ff 0000 0000 0000 0200 0000 0200 0000  ................
00400420: 0080 0000 0080 0000 0020 0000 89aa 6164  ......... ....ad
00400430: 8baa 6164 0100 ffff 53ef 0100 0100 0000  ..ad....S.......
00400440: 89aa 6164 0000 0000 0000 0000 0100 0000  ..ad............
00400450: 0000 0000 0b00 0000 8000 0000 3c00 0000  ............<...
00400460: 0200 0000 0300 0000 d0a3 e29e 3e86 4a80  ............>.J.
00400470: acfa 042c 4472 6d8c 464f 5254 495f 424f  ...,Drm.FORTI_BO
00400480: 4f54 5f44 4556 0000 2f63 6f64 652f 466f  OT_DEV../code/Fo
00400490: 7274 694d 616e 6167 6572 2f73 6572 7665  rtiManager/serve
004004a0: 722f 6d6e 7400 0000 0000 0000 0000 0000  r/mnt...........
...omitted for brevity...

❯ sudo mount -o loop,ro,offset=0x400000 FMG_300F-v7.4.0-build2223-FORTINET.decrypted firmwarefs

❯ ls -al firmwarefs
                            
total 221036
drwxr-xr-x 2 root root     4096 May 14 20:44 .
drwxr-xr-x 4 user user     4096 Jul 19 14:50 ..
-rw-r--r-- 1 root root  4066471 May 14 20:44 flatkc
-rw-r--r-- 1 root root 98350388 May 14 20:44 rootfs-ext.tar.xz
-rw-r--r-- 1 root root 70010824 May 14 20:44 rootfs.gz
-rw-r--r-- 1 root root 53657084 May 14 20:42 syntax.tar.xz
-rw-r--r-- 1 root root      122 May 14 20:44 widgets.tar.gz


The rootfs.gz file is a gzipped CPIO archive as discussed above and can be
extracted like so:

❯ mkdir unpack

❯ gzip -d rootfs.gz

❯ cpio --no-absolute-filenames -D unpack -i <rootfs

❯ ls -al unpack          
total 33832
drwxr-xr-x 14 user user     4096 Jul 11 13:17 .
drwxr-xr-x  7 user user     4096 Jul 11 13:17 ..
-rw-r--r--  1 user user 25817736 Jul 11 13:17 bin.tar.xz
drwxr-xr-x  2 user user     4096 Jul 11 13:17 boot
drwxr-xr-x  3 user user     4096 Jul 11 13:17 data
drwxr-xr-x  2 user user     4096 Jul 11 13:17 data2
drwxr-xr-x  7 user user     4096 Jul 11 13:17 dev
lrwxrwxrwx  1 user user        8 Jul 11 13:17 etc -> data/etc
-rw-r--r--  1 user user      256 Jul 11 13:17 .fgtsum
lrwxrwxrwx  1 user user        1 Jul 11 13:17 fortidev -> /
lrwxrwxrwx  1 user user       10 Jul 11 13:17 init -> /sbin/init
drwxr-xr-x  2 user user     4096 Jul 11 13:17 lib
-rw-r--r--  1 user user  8737348 Jul 11 13:17 migadmin.tar.xz
drwxr-xr-x  4 user user     4096 Jul 11 13:17 node-scripts
drwxr-xr-x  2 user user     4096 Jul 11 13:17 proc
drwxr-xr-x  2 user user     4096 Jul 11 13:17 sbin
drwxr-xr-x  2 user user     4096 Jul 11 13:17 sys
drwxr-xr-x  2 user user     4096 Jul 11 13:17 tmp
drwxr-xr-x  3 user user     4096 Jul 11 13:17 usr
-rw-r--r--  1 user user    20360 Jul 11 13:17 usr.tar.xz
drwxr-xr-x  8 user user     4096 Jul 11 13:17 var

As mentioned above, the .tar.xz files inside the archive are in a format
customized by Fortinet, and so can only be unpacked using the ftar and xz
binaries included in the sbin directory.




RESPONSIBLE DISCLOSURE

After completing our decryption and analysis of Fortinet products, we reported
the issue to the vendor. The response from Fortinet PSIRT was twofold. On the
one hand, they informed us that they did not consider the weak encryption to be
a vulnerability:

> "Encryption here is not used for confidentiality because of the availability
> of VM images. Creating a malicious images (sic) and running it on the device
> is not possible because of image signing and verification." - Fortinet PSIRT

On the other hand, they promptly locked down access to firmware downloads,
limiting each account to products with active licenses. As a trial user, you can
now only download virtual machine images.

Disclosure timeline:

Initial report

5/26/2023

Initial vendor response requesting clarification

6/3/2023

Clarification provided

6/12/2023

Final vendor response

6/26/2023




CONCLUSION

Regardless of Fortinet’s stance on the matter, breaking encryption on the
firmware images allowed our team to derive several benefits from the research
effort:

 * Detection: Fortinet products are not always easy to identify on the public
   internet. Analyzing a large number of firmware images allowed us to develop
   new techniques to find these devices when they are deployed with publicly
   accessible interfaces.
 * Fingerprinting: Most Fortinet products do not advertise their running
   software versions or the hardware they are deployed on. Our analysis produced
   a new technique for precisely identifying hardware and software versions of
   FortiGate and FortiProxy appliances without authenticated access.
 * Exploit development: With access to the entire library of Fortinet firmware
   images across multiple product lines, our team is now uniquely positioned to
   conduct ongoing research. When new vulnerabilities are discovered, such as
   CVE-2023-27997, we can develop scanners and exploits very quickly, and in
   time we may uncover 0-day vulnerabilities as well.

This is great for us, because at Bishop Fox we love doing this kind of research
– but the real value goes to Cosmos customers, who benefit by having greater
knowledge of their attack surface, more precise information about the risk of
exposure, and an improved understanding of the impact of exploitation. All of
this information helps our customers advocate for the upgrades and policy
changes they need to stay safe and secure – which, at the end of the day, is why
we do what we do.



Subscribe to Bishop Fox's Security Blog

Be first to learn about latest tools, advisories, and findings.

*
Email Address:




Submit

Thank You! You have been subscribed.

--------------------------------------------------------------------------------

About the author, Jon Williams

Senior Security Engineer

As a researcher for the Bishop Fox Capability Development team, Jon spends his
time hunting for vulnerabilities and writing exploits for software on our
customers' attack surface. He previously served as an organizer for BSides
Connecticut for four years and most recently completed the Corelan Advanced
Windows Exploit Development course. Jon has presented talks and written articles
about his security research on various subjects, including enterprise wireless
network attacks, bypassing network access controls, and malware reverse
engineering.

More by Jon




RECOMMENDED POSTS


YOU MIGHT BE INTERESTED IN THESE RELATED POSTS.

Mar 01, 2024

CVE-2024-21762 Vulnerability Scanner for FortiGate Firewalls



Jan 15, 2024

It’s 2024 and Over 178,000 SonicWall Firewalls are Publicly Exploitable



Dec 18, 2023

GWT: Unpatched, Unauthenticated Java Deserialization



Dec 12, 2023

Introducing Swagger Jacker: Auditing OpenAPI Definition Files

 * Cosmos Platform
 * Cosmos Attack Surface Management
 * Cosmos Application Penetration Testing
 * Cosmos External Penetration Testing

 * Services
 * Application Security
 * Cloud Security
 * IoT & Product Security
 * Network Security
 * Red Team & Readiness
 * Compliance, Regulations, & Frameworks
 * Google, Facebook, & Amazon Partner Assessments

 * Resources
 * Resource Center
 * Blog
 * Advisories
 * Tools

 * Our Customers
 * Our Customer Stories

 * Partners
 * Partner Programs
 * Partner Directory
 * Become a Partner

 * Company
 * About Us
 * Careers
   We're Hiring
 * Events
 * Newsroom
 * Bishop Fox Mexico
 * Bishop Fox Labs
 * Contact Us



Copyright © 2024 Bishop Fox

Privacy Statement Responsible Disclosure Policy

This site uses cookies to provide you with a great user experience. By
continuing to use our website, you consent to the use of cookies. To find out
more about the cookies we use, please see our Privacy Policy.

Accept




Live Chat