A tool for extracting OpenSSH ssh-agent shielded private keys from a
process core dump, offline, on x86_64 Linux.
It parses the core dump directly, recovers the shielded key blob and its 16 KB prekey, and reconstructs the plaintext private key.
The underlying technique is not mine. It was described by Piergiovanni Cipolloni of HN Security (Humanativa) in 2021:
OpenSSH ssh-agent Shielded Private Key Extraction (x86_64 Linux) by Piergiovanni Cipolloni, HN Security, 23 July 2021.
That article explains the vulnerability class, the sshkey struct layout, and
the use of sshkey_unshield_private() to recover the key. Read it first.
It is the source, and this repository is a reimplementation of the method it
describes.
The original proof of concept attaches gdb to the live ssh-agent process,
scans /proc/<pid>/maps, and locates the key by searching the heap for the key
comment string. shieldbreak reworks the extraction step into a standalone tool:
- Offline. It reads a saved ELF core dump, not a live process. Nothing has to be attached to a running agent.
- Structured parsing. A Go program walks the core dump's ELF segments and
reads the
sshkeystruct by field offset, rather than driving an interactive debugger. - Reproducible. The whole flow (build a debug
ssh-keygen, generate a test dump, extract, unshield) runs in Docker.
The final unshield step still uses gdb against a debug build of ssh-keygen to
call sshkey_unshield_private(), which is the OpenSSH function the original
article identified. The contribution here is engineering: turning a manual,
live-process procedure into a repeatable offline tool.
OpenSSH key shielding is designed to resist partial memory disclosure, for
example a speculative-execution side channel that leaks a fraction of RAM. It is
not designed to resist an attacker who can read the entire process memory. A
full core dump of ssh-agent is exactly that: anyone who can produce one already
has the privileges needed to compromise the process.
So this recovers a key from a full memory image within the threat model, not by defeating it. It is a demonstration and a tool, not a CVE.
shielded-key-parserreads the core dump, finds thesshkeystruct, and writes out two files: the shielded private key blob and the 16 KB prekey.- A gdb script (
shielded-key-decoder/decrypt.gdb) loads a debug build ofssh-keygen, reconstructs asshkeyfrom those two files, callssshkey_unshield_private(), and saves the plaintext key.
The struct field offsets in shielded-key-parser/parser/ssh_key_reader.go are
specific to a particular OpenSSH build and architecture (x86_64). Different
builds may lay the struct out differently, so adjust the offsets accordingly.
Everything is containerised so you do not need a matching OpenSSH build on the host.
# Build the image (compiles a debug ssh-keygen and the parser)
make build
# Generate a test ssh-agent core dump to work against
make run-test-dump-generator
# Extract and unshield a key from a dump
docker run --rm \
--cap-add=SYS_PTRACE \
--security-opt seccomp=unconfined \
-e KEY_COMMENT=test@key \
-e DUMP_PATH=/data/<dump-file>.mem \
-v "$(pwd)/dumps:/data:ro" \
-v "$(pwd)/dumps:/out" \
shieldbreakKEY_COMMENT is the comment string of the target key, used to locate it in the
dump. The recovered plaintext key is written to the mounted /out directory.
The --cap-add=SYS_PTRACE and --security-opt seccomp=unconfined flags are
required because the unshield step drives gdb, which makes calls into a live
ssh-keygen process. gdb needs ptrace to do that; without these flags it fails
on restricted sandboxes with Couldn't write extended state status: Bad address.
The parser has structural unit tests (Go's testing package) covering the ELF
segment math and the sshkey struct offset parsing, plus an end-to-end test
that generates a real ssh-agent core dump, extracts the key, and verifies the
recovered key matches the original by fingerprint.
# Fast structural unit tests
make test-unit
# Unit tests with a coverage profile and summary
make test-cover
# End-to-end test: generate a dump, extract, unshield, and verify recovery
make test
# Both unit and end-to-end
make test-allContinuous integration runs on GitHub Actions (.github/workflows/ci.yml): the
unit tests run with the race detector and coverage on every push and pull
request, coverage is uploaded to Codecov (the badge
above), and the end-to-end test runs as a separate job.
For research, education, and authorised testing only, for example
understanding the limits of ssh-agent key shielding, or recovering your own
keys from a crash dump. Do not use it against systems or data you are not
authorised to access.
I wrote this after forgetting the passphrase of one of my own SSH keys. The key
was still loaded in a running ssh-agent, which holds it in memory in shielded
form, so I dumped the agent's memory and used this tool to recover the plaintext
key. If your key is loaded in the agent, you never need the passphrase again to
get the key back.
MIT for the code in this repository.
The extraction technique and the sshkey_unshield_private() approach are the
work of Piergiovanni Cipolloni / HN Security (see Credit); this
repository claims no originality over the method, only over its implementation.