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
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