The FBI knew who that was, and got a warrant to seize their laptop. It looks like they had an encrypted file, which may be of use to your investigation.
We believe that the attacker may have been clever and used the same RSA key that they use for SSH to encrypt the file. We asked the FBI to take a core dump of
ssh-agent
that was running on the attacker’s computer.Extract the attacker’s private key from the core dump, and use it to decrypt the file.
Hint: if you have the private key in PEM format, you should be able to decrypt the file with the command
openssl pkeyutl -decrypt -inkey privatekey.pem -in data.enc
Downloads:
- Core dump of ssh-agent from the attacker’s computer (core)
- ssh-agent binary from the attacker’s computer. The computer was running Ubuntu 20.04. (ssh-agent)
- Encrypted data file from the attacker’s computer (data.enc)
Prompt:
- Enter the token value extracted from the decrypted file.
Reverse Engineering a binary is just looping through the sensations of knowing and not knowing what you’re doing until suddenly you know everything.
This writeup was not written for entry level cybersecurity folks. You will need the following:
gdb
- to debug the binaryPython3
- to run simple programming scriptsCyberChef
- to run simple algorithmsYou will NOT need Ghidra
. Some people used Ghidra
with success, but I found it distracted from the process of getting the private key. For this reason, I will not mention it in my writeup.
ssh-agent
Work?Thank you to “thái”, the vnhacker and the OpenSSH Project. Your content helped me gain some initial insight before starting this task.
idtable
Search the core dump for any strings containing socket names starting with /tmp/ssh-
and ending with agent.#
. Note that the address will appear in Little Endian.
# INPUT
(gdb) xxd -e -g 8 task/input-core | grep -A2 -B2 "/tmp/ssh"
# OUTPUT
00008e50: 0000000000000000 000055e5acfd83c0 .............U..
00008e60: 0000000000000000 0000000000000000 ................
00008e70: 0000000000000000 6873732f706d742f ......../tmp/ssh
00008e80: 4b4c476d72376e2d 67612f6c66486f34 -n7rmGLK4oHfl/ag
00008e90: 000038312e746e65 0000000000000000 ent.18..........
The string /tmp/ssh-n7rmGLK4oHfl/agent.18
at address 0x00008e70
is the best fit.
We also see the idtable
address, which appears right before the /tmp/ssh-xxxx
socket name. Looks like the address we need is 0x55e5acfd83c0
.
identity
structuresOn an Ubuntu 20.04 machine, start gdb
to analyze the core dump:
# INPUT
gdb task/ssh-agent task/input-core
# OUTPUT
(gdb)
Here is what we are looking for within an idtable
structure:
struct idtable {
int nentries;
TAILQ_HEAD(idqueue, identity) idlist;
};
We need to read the nentries
variables within idtable
. When empty, nentries
is a 0
integer followed by null pointer and idles.tqh_last
pointer. If nentries
equals 1, it should have a non-null pointer to the identity
structure
# INPUT
(gdb) x/3xg 0x55e5acfd83c0
# OUTPUT
0x55e5acfd83c0: 0x0000000000000001 0x000055e5acfddb90
0x55e5acfd83d0: 0x000055e5acfddb90
Because nentries
is 0x1
, the identity
structure’s address is 0x55e5acfddb90
.
identity
structuresHere is the structure of an identity
in ssh-agent
:
typedef struct identity {
TAILQ_ENTRY(identity) next;
struct sshkey *key;
char *comment;
char *provider;
time_t death;
u_int confirm;
char *sk_provider;
struct dest_constraint *dest_constraints;
size_t ndest_constraints;
} Identity;
The identity
structure contains 10 values, but we’re only going to look at the first 48 bytes of data (6 values).
# INPUT
(gdb) x/6xg 0x55e5acfddb90
# OUTPUT
0x55e5acfddb90: 0x0000000000000000 0x000055e5acfd83c8
0x55e5acfddba0: 0x000055e5acfdbee0 0x000055e5acfd9c00
0x55e5acfddbb0: 0x0000000000000000 0x0000000000000000
The third value in the identity
structure is the pointer for the key
structure. In this case, the key
can be found at 0x55e5acfdbee0
. To identify which key you’re looking at, read the fourth value in the identity
structure, which is the comment
. It will tell you the name that the user selected for the key
.
# INPUT: Asking for the comment value
(gdb) x/s 0x000055e5acfd9c00
# OUTPUT: This is the comment.
0x55e5acfd9c00: "uHloYVSGRxPfv8313XEKXA"
sshkey
and sshkey_cert
StructuresThe key
is stored in a sshkey
structure. Here is what that looks like:
sshkey
/* XXX opaquify? */
struct sshkey {
int type;
int flags;
/* KEY_RSA */
RSA *rsa;
/* KEY_DSA */
DSA *dsa;
/* KEY_ECDSA and KEY_ECDSA_SK */
int ecdsa_nid; /* NID of curve */
EC_KEY *ecdsa;
/* KEY_ED25519 and KEY_ED25519_SK */
u_char *ed25519_sk;
u_char *ed25519_pk;
/* KEY_XMSS */
char *xmss_name;
char *xmss_filename; /* for state file updates */
void *xmss_state; /* depends on xmss_name, opaque */
u_char *xmss_sk;
u_char *xmss_pk;
/* KEY_ECDSA_SK and KEY_ED25519_SK */
char *sk_application;
uint8_t sk_flags;
struct sshbuf *sk_key_handle;
struct sshbuf *sk_reserved;
/* Certificates */
struct sshkey_cert *cert;
/* Private key shielding */
u_char *shielded_private;
size_t shielded_len;
u_char *shield_prekey;
size_t shield_prekey_len;
};
Here is the size of each variable:
Bytes | Structure | Total Bytes |
---|---|---|
4 | type | 4 |
4 | flags | 8 |
8 | *rsa | 16 |
8 | *dsa | 24 |
8 | ecdsa_nid | 32 |
8 | *ecdsa | 40 |
8 | *ed25519_sk | 48 |
8 | *ed25519_pk | 56 |
8 | xmss_name | 64 |
8 | *xmss_filename | 72 |
8 | *xmss_state | 80 |
8 | *xmss_sk | 88 |
8 | *xmss_pk | 96 |
8 | *sk_application | 104 |
8 | sk_flags | 112 |
8 | *sk_key_handle | 120 |
8 | *sk_reserved | 128 |
8 | *cert | 136 |
8 | *shielded_private | 144 |
8 | shielded_len | 152 |
8 | *shield_prekey | 160 |
8 | shield_prekey_len | 168 |
The key
structure contains 4 addresses (2 ints + 2 pointers). The 2nd address should lead to an RSA
structure. In normal cases, the 3rd pointer would say id_rsa
, but that is not always the case.
# INPUT
(gdb) x/21xg 0x55e5acfdbee0
# OUTPUT
0x55e5acfdbee0: 0x0000000000000000 0x000055e5acfdf0e0
0x55e5acfdbef0: 0x0000000000000000 0x00000000ffffffff
0x55e5acfdbf00: 0x0000000000000000 0x0000000000000000
0x55e5acfdbf10: 0x0000000000000000 0x0000000000000000
0x55e5acfdbf20: 0x0000000000000000 0x0000000000000000
0x55e5acfdbf30: 0x0000000000000000 0x0000000000000000
0x55e5acfdbf40: 0x0000000000000000 0x0000000000000000
0x55e5acfdbf50: 0x0000000000000000 0x0000000000000000
0x55e5acfdbf60: 0x0000000000000000 0x000055e5acfdeab0
0x55e5acfdbf70: 0x0000000000000570 0x000055e5acfdfc00
0x55e5acfdbf80: 0x0000000000004000
That means that the values for each variable are as follows:
rsa = 0x000055e5acfdf0e0
shielded_private = 0x000055e5acfdeab0
shielded_len = 0x0000000000000570
shield_prekey = 0x000055e5acfdfc00
shield_prekey_len = 0x0000000000004000
This tells us that the shielded_private
key is a length of 1392 bytes or char
, and the shield_prekey_len
is a total of 16384 bytes or char
.
shielded_private
to a private key:Let’s read the value of the 1392 byte shielded_private
:
# INPUT
(gdb) x/174xg 0x55e5acfdeab0
# OUTPUT
0x55e5acfdeab0: 0x1bd278a99e38e3f0 0x81012461862f0aeb
# continues for a lot of rows...
Now, let’s read the 16384 byte shield_prekey
value:
# INPUT
(gdb) x/2048xg 0x000055e5acfdfc00
# OUTPUT (truncated for readability)
0x55e5acfdfc00: 0x03f01c66fdc10e6d 0x18376821588efb1c
# continues for a lot of rows...
IMPORTANT
I’ll need to save these values for use later. To do this, I created a recipe in CyberChef that allows me to convert Little Endian gdb
output to binary and save locally to my computer.
Using the approach provided by Piergiovanni Cipolloni, it is theoritically possible to call the function sshkey_unshield_private
in ssh-keygen
to decrypt the key.
In order to copy Piergiovanni’s technique, let’s download the latest tarball version of openssh Portable and compile a debugged version.
tar xvfz openssh-9.0p1.tar.gz
cd openssh-9.0p1
./configure --with-audit=debug
make ssh-keygen
mv ssh-keygen ~/task/
cd ~
Next, let’s execute the script provided by Piergiovanni, with a few tweaks to the folder structure:
gdb task/ssh-keygen
b main
b sshkey_free
r
set $miak = (struct sshkey *)sshkey_new(0)
set $shielded_private = (unsigned char *)malloc(1392)
set $shield_prekey = (unsigned char *)malloc(16384)
set $fd = fopen("/home/rjamison/task/shielded_private", "r")
call fread($shielded_private, 1, 1392, $fd)
call fclose($fd)
set $fd = fopen("/home/rjamison/task/shield_prekey", "r")
call fread($shield_prekey, 1, 16384, $fd)
call fclose($fd)
set $miak->shielded_private=$shielded_private
set $miak->shield_prekey=$shield_prekey
set $miak->shielded_len=1392
set $miak->shield_prekey_len=16384
call sshkey_unshield_private($miak)
bt
f 1
x *kp
call sshkey_save_private(*kp, "/home/rjamison/task/private.pem", "", "comment", 0, "\x00", 0)
k
q
Because ssh-keygen
has built-in checks for key validity, you will only get the key if you did everything correctly. In my case, it took a few tries because I messed up the endianness and failed to convert the shielded_private
and shield_prekey
back to their binary format before running the script.
HINT: if you can read the characters of either shielded_private
or shield_prekey
, you are looking at it in hex format.
Let’s see what the key looks like!
# INPUT
cat privatekey.pem
# OUTPUT
-----BEGIN OPENSSH PRIVATE KEY-----
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
Sdqy4lEI8jPMcAAAAHY29tbWVudAECAwQ=
-----END OPENSSH PRIVATE KEY-----
That looks right, but wait, we can’t decrypt the file with an openssh
certificate! Since Piergiovanni’s technique depended on ssh-keygen
, we now have to convert the certificate to a format we can use. Let’s swap it to RSA using this bash command:
# INPUT
ssh-keygen -p -N "" -m pem -f Desktop/Codebreaker/5/privatekey.pem
# OUTPUT
Key has comment 'comment'
Your identification has been saved with the new passphrase.
Let’s see our new certificate!
# INPUT
cat privatekey.pem
# OUTPUT
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----
Excellent! Let’s use it to decrypt the file and see the contents:
# INPUT
openssl pkeyutl -decrypt -inkey privatekey.pem -in data.enc
# OUTPUT
# Netscape HTTP Cookie File
wrfbgtsocesalacv.ransommethis.net FALSE / TRUE 2145916800 tok eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NTM2MzI0OTYsImV4cCI6MTY1NjIyNDQ5Niwic2VjIjoiVjZiTE5hT1o3c1F6SkFIOE9wdVVhVkZ0eFprUHNFV2kiLCJ1aWQiOjIyNzEyfQ.Jy6N_kO13PTnW--HIFks60AqoO_b0uSeoZUQ9_shrw0
Looks like the token value
we were looking for was eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NTM2MzI0OTYsImV4cCI6MTY1NjIyNDQ5Niwic2VjIjoiVjZiTE5hT1o3c1F6SkFIOE9wdVVhVkZ0eFprUHNFV2kiLCJ1aWQiOjIyNzEyfQ.Jy6N_kO13PTnW--HIFks60AqoO_b0uSeoZUQ9_shrw0
. Upon submitting it to the nsa-codebreaker website, we verify that it is in fact the correct token value.