TL;DR
We gave Claude Code, running Sonnet 4.5, an encrypted SonicWall firmware image and a copy of the decrypted filesystem. The instruction was one line: decrypt the image. With no prior knowledge of the format and only occasional input from us, the model reverse engineered SonicWall’s proprietary SWI encryption end-to-end. It traced the decryption logic through a decompiled binary, discovered that the key lived inside a HashiCorp Vault instance shipped on the appliance, reconstructed Vault’s master key from embedded Shamir secret shares, recovered the RSA private key, and used it to decrypt the image. In doing so, it independently reproduced work that one of our senior vulnerability researchers had previously done by hand. What follows is that story, first in Claude’s own words and then with our analysis of what it means.
Network appliance vendors routinely encrypt their firmware images. Their goal is typically to keep researchers (and competitors) from unpacking the firmware, studying the code, and finding vulnerabilities. SonicWall ships some SonicOS images in a proprietary container format it calls SWI, and that format has resisted casual inspection for years. A senior researcher on our team eventually cracked it the hard way: manual reverse engineering, cryptographic reconstruction, and a fair amount of persistence. We documented that original work in our SonicWall firmware deep dive, and it is the kind of senior-level effort that defines our team.
We wanted to know how much of that effort a current frontier LLM (large language model) could reproduce on its own. So, we set up a deliberately black-box experiment. Driving Binary Ninja through its Model Context Protocol (MCP) integration inside Claude Code, we gave Claude Sonnet 4.5 two inputs:
We did not tell Claude how the format worked, where the key was, how the two artifacts related, or how our researcher had approached it. We told it to decrypt the image, then mostly got out of the way.
Working almost entirely on its own, Claude traced the encryption to a HashiCorp Vault instance hidden inside the appliance, reconstructed Vault's master key from Shamir secret shares embedded in a config file and used the recovered RSA key to decrypt the firmware. It hit one real wall along the way: reverse-engineering Vault's internal encryption by hand failed repeatedly, until we suggested a different approach. “Stop fighting Vault's encryption and just run Vault itself.” Once redirected, Claude carried out the rest independently, including spinning up the original Vault binary in Docker and regenerating an expired authentication token to export the key.
The pattern that emerged is the real story here. Claude brought the technical depth of a senior researcher: finite-field math, key-management internals, cryptographic padding schemes, without being taught any of it. What it lacked wasn't knowledge but judgment: knowing when it was stuck and when to change approach. That gap is what we filled, and filling it took one well-placed question, not days of hands-on effort.
Claude’s account (see technical details section below) is faithful to how the work unfolded, including the dead ends. The framing matters, however, so before we draw conclusions, a few qualifications are in order:
With those qualifications stated plainly, here is what we see as the important takeaways.
We gave Claude two artifacts and a goal. There was no documentation of the SWI format, no hints about Vault, no pointer to the Shamir shares, and no explanation of how the two artifacts related. Claude found the OpenSSL EVP structure from raw bytes, turned to the filesystem and a decompiler when the key was not in the image, read the decryption logic out of a stripped binary, identified an entire embedded key-management system as the place the key lived, and broke into it. The single hardest cryptographic step was reconstructing a master key from Shamir shares over a finite field. It implemented correctly with no help, and finite-field arithmetic is exactly the kind of detail where one wrong constant silently ruins the output.
Most of that chain it did independently. The result is a senior-level reverse engineering outcome reproduced by a model with one human question along the way.
When manual reconstruction of Vault’s nested encryption stalled, Claude did not step back on its own. It kept grinding at the failing decryption, and the way out came from working the problem together: the appliance already shipped a working implementation of the exact decryption, so rather than reimplement it, why not run it? Once pointed that way, Claude executed expertly, spinning up the original Vault binary, unsealing it, and regenerating an expired root token to export the key, the move an experienced researcher makes. The skill to carry it out was entirely Claude’s; the decision to abandon the dead end was something we reached together.
This takeaway is worth stating carefully because on the surface it sounds like a contradiction. The experience was not “run a tool and read the output,” it was closer to mentoring. Claude worked independently for moderate stretches, came back with findings, and occasionally got stuck. Our job was to let it run, check its reasoning at the right moments, and ask a pointed question when it was circling. Anyone who has supervised a junior researcher will recognize that rhythm. You are not doing the work, and you are not idle either. You are steering.
But here’s the part that does not fit the usual mentoring picture: you coach a junior because they lack knowledge, and the coaching is how they acquire it. Claude did not lack knowledge. It brought the technical depth of a senior researcher: it knew finite-field arithmetic, OpenSSL’s key derivation, Vault’s internals, and the parameter space of RSA padding without being taught any of it. What it lacked was the autonomy and self-calibration we associate with seniority. It could pursue a wrong approach with full confidence, and it did not reliably know when it was stuck or when to step back. So, the thing we were “coaching” was never skill. It was direction and verification.
That inversion, a colleague with the knowledge of a senior and the supervisory needs of a junior, is genuinely new, and it is the single most important thing we took from this project. It does not map onto any role a research team already has. It is not a senior researcher, who needs no oversight, and it is not a junior, who needs to be taught. It is a third thing, and getting value from it depends on recognizing that and supervising accordingly: trusting the skill but not yet trusting the judgment.
The practical implication follows directly. A senior researcher’s scarcest resource is hours. If a model can carry the long, methodical middle of a project while a human supplies direction and verification at the inflection points, the human’s expertise stretches a great deal further. The teams that internalize the inversion and build their workflow around supervising skill rather than teaching it will get the most out of these tools. The ones that expect either a finished autonomous researcher or a tool that needs no steering will both be disappointed, for opposite reasons.
Nothing about this experiment was exotic. We used a current frontier model, a standard agent framework, and an off-the-shelf decompiler with an MCP bridge. That combination is available to any researcher today. It is the same class of setup as Codex, OpenCode, and other agentic coding environments that have become routine over the past year. The capability on display here is not a lab demo, it is what these tools do when you point them at a hard, real problem.
The direction of travel is the part worth sitting with. This is the floor, not the ceiling. The models keep improving, the frameworks around them keep maturing, and the tooling integrations (decompilers, debuggers, fuzzers, sandboxes reachable over MCP) keep widening what a model can touch. A task that needed a senior researcher and several days last year took a model and a few sessions of light supervision this year. We do not think that curve is close to flattening.
What follows is Claude’s own write-up of the work, reproduced as written, with cryptographic details redacted. A story of reverse engineering, cryptographic puzzles, and persistence.
It started with a single 119.8 MB file: sonicwall_nsv_vmware_6.5.4.4-44v-21-2472-b8c642cd.0.swi
The goal seemed straightforward: decrypt the firmware. But as with most reverse engineering challenges, what appeared simple at first would reveal layers of complexity, each requiring different tools and techniques to overcome.
The First Clues
Opening the file in a hex editor revealed a clear structure:
SWI_HEADER PLATFORM=vmware VERSION=3" Salted__" at offset 0x366The presence of “Salted__” was the first breakthrough. This is OpenSSL’s signature for password-based encryption using the EVP format. The next 8 bytes would be the salt, followed by the encrypted payload.
Discovery #1: The firmware uses OpenSSL’s enc format with AES-256-CBC encryption. But where was the password?
The Line 2 Mystery
The base64 blob on line 2 was suspicious. Decoding it produced exactly 256 bytes - the size of an RSA-2048 ciphertext. This wasn’t a coincidence.
Discovery #2: Line 2 contains an RSA-encrypted password that decrypts the firmware payload. Now I needed to find the RSA private key.
Dead End: Embedded Keys
My first instinct was to search for RSA keys embedded in the firmware itself. Using grep and strings, I found references to PGP and encryption, but no private keys. The PGP block turned out to be just a signature for authenticity verification, not encryption.
No obvious private key in the firmware file itself.
The Binary Ninja Revelation
Using Binary Ninja’s MCP (Model Context Protocol) integration, I decompiled the web-front binary - SonicWall’s management interface. This revealed something unexpected:
func processImageFile() {
// Read line 2 from SWI file
encryptedKey := readLine2()
// Decrypt using Vault API
password := vaultutil.DecryptPgpKey("swidec", encryptedKey)
// Use password to decrypt firmware
decryptFirmware(password)
}
Discovery #3: The key isn’t stored in the firmware - it’s stored in HashiCorp Vault, under the name “swidec” (SWI decrypt).
This was a game-changer. Instead of finding a static key, I needed to extract it from a full key management system.
Diving into Vault
Exploring the extracted firmware filesystem, I found:
/opt/sonicwall/soniccore/vault/data/
This directory contained Vault’s data files, including:
core/_master: Encrypted master key core/_keyring: Encrypted keyring logical/.../key/_swidec: Encrypted RSA transit key But everything was encrypted. How could I decrypt Vault’s own storage?
The Shamir Discovery
Reading through systemd service files, I found vault-unseal.service:
ExecStart=/usr/bin/vault operator unseal Bysy[REDACTED] ExecStart=/usr/bin/vault operator unseal pUl6[REDACTED] ExecStart=/usr/bin/vault operator unseal ar2/[REDACTED]
Discovery #4:Three Shamir’s Secret Sharing keys, embedded in plaintext!
Vault uses a (3, 5) threshold scheme - 3 shares are enough to reconstruct the master key. The firmware developers had embedded these shares because the read-only firmware was considered a trusted environment.
Implementing Shamir’s Secret Sharing
Vault’s Shamir implementation operates over GF(256), the Galois field with 256 elements. This required implementing:
The tricky part was getting the field arithmetic exactly right. A single error in the irreducible polynomial or the inverse calculation would produce garbage output.
def gf256_mul(a, b):
p = 0
for _ in range(8):
if b & 1:
p ^= a
hi_bit_set = a & 0x80
a <<= 1
if hi_bit_set:
a ^= 0x1b # Critical: correct polynomial
b >>= 1
return p & 0xff
After implementing the field arithmetic, I successfully reconstructed the master key:
87e9[REDACTED]
Vault’s Nested Encryption
Vault’s storage has multiple layers:
Attempting to decrypt the keyring with the reconstructed master key failed with “MAC check failed” errors. The AES-GCM authentication kept rejecting my decryption attempts, despite having the correct master key.
The Problem: Vault 0.9.3’s exact key derivation and encryption parameters were undocumented. The keyring encryption included additional context strings and nonces that weren’t obvious from the code.
Impasse: I couldn’t get past Vault’s keyring encryption layer manually.
The Elegant Solution: Let Vault Do The Work
Instead of reverse-engineering Vault’s entire encryption stack, I realized: why not just run Vault itself?
The firmware contained:
/opt/sonicwall/soniccore/vault/data/ vault-unseal.serviceI could spin up Vault, unseal it, and ask it politely to export the key.
Building the Extraction Container
I created a Docker container that would:
_master, _keyring, _seal-configlogical/.../key/_swidecvault-unseal.service file with Shamir keysThe Token Problem
All stored Vault tokens had expired. The firmware had been powered off for weeks, and Vault tokens have TTLs measured in hours or days.
I needed to generate a new root token. Vault’s root token generation process is designed for disaster recovery and requires:
The tricky part was parsing Vault 0.9.3’s output format. The CLI included ANSI color codes that broke parsing, and the token was labeled “Root Token” but was actually still encoded.
# Generate OTP
OTP=$(vault operator generate-root -generate-otp)
# Initialize and extract nonce (stripping ANSI codes)
INIT_OUTPUT=$(vault operator generate-root -init -otp="$OTP" 2>&1 | sed 's/\x1b\[[0-9;]*m//g')
NONCE=$(echo "$INIT_OUTPUT" | grep "Nonce" | awk '{print $2}')
# Provide the 3 unseal keys
vault operator generate-root -nonce="$NONCE" "${UNSEAL_KEY_1}" >/dev/null
vault operator generate-root -nonce="$NONCE" "${UNSEAL_KEY_2}" >/dev/null
FINAL_OUTPUT=$(vault operator generate-root -nonce="$NONCE" "${UNSEAL_KEY_3}" 2>&1 | sed 's/\x1b\[[0-9;]*m//g')
# Extract and decode the token
ENCODED_TOKEN=$(echo "$FINAL_OUTPUT" | grep "Root Token" | awk '{print $3}')
ROOT_TOKEN=$(vault operator generate-root -decode="$ENCODED_TOKEN" -otp="$OTP")
Success! With the root token, I could now authenticate.
The Export
With full access to Vault, extracting the key was straightforward:
# Make the transit key exportable (keys are not exportable by default) vault write transit/keys/swidec/config exportable=true # Export the RSA private key vault read transit/export/encryption-key/swidec The output included the complete PEM-encoded RSA-2048 private key: -----BEGIN RSA PRIVATE KEY----- MIIE[REDACTED] -----END RSA PRIVATE KEY-----
Discovery #5: Rather than fighting Vault’s encryption, using the Vault binary itself was the elegant solution. A simple Docker container could reproduce the entire extraction process.
Now I had the RSA-2048 private key. Time to decrypt line 2.
Wrong Padding Scheme
With the RSA key in hand, I attempted to decrypt line 2:
cipher = PKCS1_v1_5.new(rsa_key) password = cipher.decrypt(encrypted, sentinel)
Result: Sentinel returned. Decryption failed.
I tried OAEP padding next:
cipher = PKCS1_OAEP.new(rsa_key) password = cipher.decrypt(encrypted)
Result: Incorrect decryption error.
This was frustrating. I had the correct RSA key - I could see its fingerprint matched the Vault metadata - but decryption kept failing.
The User’s Insight
At this point, the user asked a critical question: “Did you try different hash algorithms with OAEP?”
This was the breakthrough I needed. OAEP padding isn’t just one scheme - it’s parameterized by a hash function. The default in PyCryptodome is SHA-1, but Vault might use something different.
Finding the Right Hash
Vault’s documentation revealed that the transit engine uses SHA-256 as the default hash for RSA-2048 keys in OAEP mode. This wasn’t an arbitrary choice - SHA-1 was being phased out for security reasons, and Vault had adopted SHA-256 as its new default.
I modified the code:
from Crypto.Hash import SHA256 cipher = PKCS1_OAEP.new(rsa_key, hashAlgo=SHA256) password = cipher.decrypt(encrypted)
Result: Success! 🎉
[+] OAEP SHA-256 SUCCESS! [+] Decrypted: 128 bytes [*] Password (hex): 9ef9[REDACTED]
The password was 128 bytes of high-entropy random data - exactly what you’d expect for a cryptographically secure session key.
With the password in hand, the rest was straightforward:
0c3346e876aca15e
key, iv = evp_bytes_to_key(password, salt)
<em># key: d04e[REDACTED]</em>
<em># iv: c5</em>dd[REDACTED]
cipher = AES.new(key, AES.MODE_CBC, iv)
firmware = cipher.decrypt(encrypted_payload)
[*] First 16 bytes as ASCII: b'soniccore_sonico'
[+] TAR magic found! Decryption successful!The decrypted firmware was a TAR archive containing the complete SonicWall virtual appliance.
This challenge had multiple points where I could have given up:
Each obstacle required stepping back, gathering more information, and trying a different approach. The solution came not from a single brilliant insight, but from methodically eliminating wrong answers until only the right one remained.
The breakthrough came from a user question. Sometimes when you’re deep in the technical details, an outside perspective asking “did you try X?” can illuminate the blind spot you’ve been missing. The user’s knowledge that OAEP has different hash options was the catalyst for the final success.
We handed a frontier model an encrypted image and a decrypted filesystem from another version of the appliance, then asked it to do something a senior researcher previously had to do by hand. With minimal guidance, it succeeded. It read raw bytes, decompiled a binary, mapped and then broke an embedded key-management system, reconstructed a master key from finite-field math, recovered a private key by running the vendor’s own software against it, and decrypted the image.
The lasting lesson for us was not the crack itself but the shape of the collaboration. Claude arrived with the knowledge of a senior researcher and the autonomy of a junior, a combination that does not match any role a security team already staffs. Get that wrong in either direction, by waiting for a finished autonomous researcher or by treating it as a tool that needs no steering, and you will misjudge what these models are good for today. Get it right, by trusting the skill while supervising the judgment, and a senior researcher’s expertise stretches across far more work than it used to. That is where serious security research with these tools is heading, and it is arriving faster than most teams have planned for.
Our Cosmos customers benefit directly from the research and tooling our team develops, including the ways we are integrating frontier models into our offensive testing. If you are interested in learning more about managed services delivered through our Cosmos platform, visit bishopfox.com/services/continuous-threat-exposure-management.
For more research from our team, visit the Bishop Fox Blog.