At Bishop Fox, we spend a lot of time researching network appliances used by our customers. These are often high-privilege devices that protect the boundary between the public internet and sensitive internal networks. Our research includes both vulnerability research and advanced fingerprinting techniques, which give us a detailed view of our customers' attack surface. Gaining access to unencrypted and unobfuscated firmware is a prerequisite for nearly all of this research.
This is the first in a three-part series detailing our work in decrypting and analyzing SonicWall firewalls. We begin with a walkthrough of SWI firmware decryption, since this file format has not been covered by previous research. A second article will leverage our improved fingerprinting capabilities to survey the current state of SonicWall firewall security, based on internet-facing exposures. The third part in the series will provide a deep technical analysis of SIG firmware, going beyond prior research to detail how we decrypted the most prevalent SonicOS file format and made the file system installer accessible to future research.
SonicOS File Formats
To start things off, let’s take a look at the file formats SonicWall uses to distribute SonicOS, the operating system underlying its firewall appliance. The following table shows a summary of the different file formats provided for each firewall model, based on the images available for download on SonicWall’s support site.
Firewall Model |
Customer Tier |
Firmware File Format(s) |
SOHO |
Small business/home office |
SIG |
TZ |
Small to medium business |
SIG |
NSa |
Medium to large business |
SIG |
Supermassive |
Large, distributed enterprise (legacy) |
SIG |
NSsp |
Large, distributed enterprise |
SIG, SWI |
NSv |
Cloud and hybrid virtual environment |
SIG, SWI, OVA, VHD, QCOW2 |
Prior research primarily focused on NSv OVA (VMware) firmware images, as these are the most accessible in terms of cost and technical complexity (along with VHD/Hyper-V and QCOW2/KVM images). Current research, including our own, has been taking a closer look at the SIG format because of its universal applicability, but until recently this format presented significant hurdles to reverse engineering.
The SWI format is unique in that it is only used for the NSsp 15700 appliance and NSv virtual machine updates. Additionally, only SonicOS versions 6.5.4.4 through 7.0.1 were released in this format. This pattern of limited use suggests that SonicWall may have developed the format as a first attempt at providing additional security for virtual machine images. The first NSv release was version 6.5.4.4, and virtual machines by their nature must include the capability to decrypt any encrypted components, so it is understandable that SonicWall would want to obscure that process to help protect other firmware from reverse engineering. With the release of SonicOSX at version 7.0.0, SonicWall improved the encryption process used for SIG firmware images and, starting with version 7.1.0, NSv images adopted this format as well. Our assumption is that the SWI format was retired at that point.
For the rest of this article, we will be looking at SWI firmware and detailing the process we followed to reverse-engineer its encryption scheme.
SWI File Header
We began our analysis of the SWI format by looking at a hex dump. This revealed a large text header followed by the string “Salted__
” and a body of random-looking data. The “Salted__
” string strongly implied that the body was encrypted using OpenSSL’s custom format. That format has an 8-byte salt immediately following the “Salted__
” string, and that salt is combined with a password, and passed into a key derivation function to derive a key and initialization vector (IV).
We examined the text header prior to the binary data and found that it consists of three sections. The first line includes the string “SWI_HEADER”, a platform (“PLATFORM=vmware”) and a version (VERSION=3). The next line is 256 bytes of base64-encoded random-looking data. The final section is a PGP signature block. The purpose of the first and last sections was clear to us, but we could only try to guess the purpose of the random base64.
We initially hoped that this base64 data could be the password, but our attempts to use this with various combinations of OpenSSL parameters never resulted in a successful decryption. Despite that, given that this seemed to be random data with no other clear purpose in the header, we moved forward to reverse engineering with the assumption that this random header data was probably involved in the process of obtaining the encryption password.
Reverse Engineering for the Password
At the start, we had chicken-and-egg problem. To reverse engineer the firmware decryption process, we needed unencrypted firmware containing decryption code. The solution to this problem was provided by Praetorian’s sonicwall-nsv-decrypter and blog post, where they walk through the process of decrypting and rooting a SonicWall NSv virtual machine. Using Praetorian’s tool, we were able to obtain decrypted firmware and access a shell on a SonicWall NSv 6.5.4.4-44v-21-1519 appliance.
Using this access, we searched for any code that may have been responsible for processing SWI files. A recursive grep for ‘.swi
’ turned up a relatively small number of results, and when we looked at the web-front binary, the function components/imagemgmt.readSWIHeader()
caught our eye because, as the name suggests, it was responsible for parsing the SWI header.
Tracing function calls back from readSWIHeader()
, we found components/imagemgmt.processImageFile(). This function uses OpenSSL to decrypt a file using a password that is obtained from a call to utils/vaultutil.DecryptPgpKey()
.
DecryptPgpKey
is a helper function which interfaces with a HashiCorp Vault API running at http://127.0.0.1:8200. We found that this function uses a hardcoded API token to authenticate, and it sends the request to /v1/transit/decrypt/swikey
. According to the Vault documentation, this endpoint would decrypt base64-encoded data using a key named “swikey”. Reading further in the documentation, we noticed that we could export
the private key if it was configured to be exportable, and that it should be possible to modify
the key configuration to enable the exportable flag.
With our root shell, we were able to recover the private key with the following commands
$ curl –H “X-Vault-Token: $VAULT_TOKEN” http://127.0.0.1/v1/transit/keys/swidec/config -d ‘{"exportable”:”true”}’ $ curl –H “X-Vault-Token: $VAULT_TOKEN” http://127.0.0.1/v1/transit/export/encryption-key/swidec
The key configuration indicated it was a “rsa-2048” key. According to the documentation, this means data would be decrypted using the OAEP padding algorithm with SHA-256 as the hash function and mask generation function. Using this information, we decided to try decrypting the base64 string from our SWI file header using the swidec
key. This resulted in 128 bytes of random-looking data, but the decryption did not result in any padding errors, which strongly implied that we were decrypting the data correctly. Finally, we used this decrypted data as the password for decrypting the file body. This produced a tar file containing a cleartext version of the root filesystem stored as a compressed tar file, and a GPG signature.
We converted this process into the following Python script:
import base64 from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_OAEP, AES from Crypto.Hash import SHA256 from hashlib import md5 import sys input_swi=sys.argv[1] output_tgz=sys.argv[2] def EVP_BytesToKey(password, salt, key_len, iv_len): # copied from https://stackoverflow.com/questions/13907841/implement-openssl-aes-encryption-in-python dtot = md5(password + salt).digest() d = [ dtot ] while len(dtot)<(iv_len+key_len): d.append( md5(d[-1] + password + salt).digest() ) dtot += d[-1] return dtot[:key_len], dtot[key_len:key_len+iv_len] fw_key=RSA.importKey(open("swi.key").read()) unwrap=PKCS1_OAEP.new(fw_key, hashAlgo=SHA256) with open(input_swi,'rb') as fd, open(output_tgz,"wb") as fd2: hdr=fd.readline().strip() pw=fd.readline().strip() wrapped_key=base64.b64decode(pw) try: unwrapped_key=unwrap.decrypt(wrapped_key) except Exception as e: print(e) print("Key unwrapping failed") exit() sig=b'' while 1: line=fd.readline() sig+=line if line.startswith(b"-----END"): break assert fd.read(8)==b'Salted__' salt=fd.read(8) k, iv, = EVP_BytesToKey(unwrapped_key, salt, 32, 16) aes=AES.new(k, mode=AES.MODE_CBC, iv=iv) fd2.write(aes.decrypt(fd.read()))
Conclusion
Using our script, we were able to decrypt many variants of NSv and NSsp firmware images for analysis. This access, combined with access to other image formats we had decrypted through similar processes, allowed us to build detailed fingerprinting capabilities for SonicWall devices. Stay tuned for part two of this series, which will discuss our follow-on research that leveraged knowledge gained from the decrypted firmware to analyze hundreds of thousands of SonicWall appliances exposed to the internet.
Subscribe to Bishop Fox's Security Blog
Be first to learn about latest tools, advisories, and findings.
Thank You! You have been subscribed.
Recommended Posts
You might be interested in these related posts.
Nov 27, 2024
The Growing Concern of API Security
Nov 01, 2024
A Brief Look at FortiJump (FortiManager CVE-2024-47575)
Sep 24, 2024
Broken Hill: A Productionized Greedy Coordinate Gradient Attack Tool for Use Against Large Language Models
Sep 11, 2024
Exploring Large Language Models: Local LLM CTF & Lab