7 minute read

Badge 5

Task 5 - Core Dumped - (Reverse Engineering, Cryptography)

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:

Prompt:

  • Enter the token value extracted from the decrypted file.

Prerequisites

Reverse Engineering a binary is just looping through the sensations of knowing and not knowing what you’re doing until suddenly you know everything.

Reverse Engineering

This writeup was not written for entry level cybersecurity folks. You will need the following:

  • gdb - to debug the binary
  • Python3 - to run simple programming scripts
  • CyberChef - to run simple algorithms

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

How Does 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.

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

Finding identity structures

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

Reading identity structures

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

Reading sshkey and sshkey_cert Structures

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

Converting the 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.

Decrypting the key

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.

Using the Key

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-----
MIIG4wIBAAKCAYEA0M89msEDWvci6I37Sg4kb0W5gFfvgCqQobtERtAP/spIz9Fa
3SLGOSX5VSDf9uLksXm51C+p4MxcJgIKNXxyf0ktSn2nKd9BkiaNJ1b1Gwjb+v+3
4oQV01tg/w3XJXYdyDQPukrFfiNr3ikSjr2OBNCW/jDOVvao0e32OxfJ4tp6GbAV
aTZPhn2GUqVDXcW2BETrnC8r/M9iyH204Iv9KofAB/RApm/oN/NXuFGavO6KL74/
bMery+CYv8kpRCmBXBXdzHsJ/Q+ENs2oXVIM64GFwa1BLhe2h78yQW2YeJgiK+y9
UkBK2H+m1CzugPqL/0ePue3rnCxUvu1y/umTucFYvvh1ghiA26M9Vo0CQ/lNM7aW
ZV5kp8W/4NLYI0q87YDcnt9cEIK/+BI/gtRe4g+fgcJolgDXcxr5EmPcjq136FLy
fFIY2xIuJXqkFwXxd94GMQ3wP0V48M3VUNJ01ZIL8cAyQg5n8/cOcUsdHaQXpmMX
6k1engz+lxR1+6iZAgMBAAECggGALKj2kx/PGOicxcKASZGGx2nRSBsGtZHlB0Jn
UwHzOdqIAjLTzVI/tT9i+Ysje8mBglf3n+Nl4Re0acir6E6qqoC7OCBx3WnLu3eV
XyGhINwfOKry6Ha5uE/mCgcqye9eZVCFQKH3ZYgr7QM53d+6/VqjwNAtgboV13ie
3nqzClsSHGZl3e/oyGoWjZydMPQ1fupCvk/0h4jPpvQD+LbYOf142/qozTBIKZdZ
XqnNf7VBRv7Yz+pQ2zU5VmOu+M4rYr8CHRyTveQisgR74lTR1E1+wqmCGQY1Gu1U
CTrVzDxfG9iSwAl3SNblQMyaYOamLEEAWCLTx2WJ4Y6HEkERz+v8zBDXTr6aHC8G
ndCdN6gMFdgoich2r3AUz4O9P7Km1/3CV6IEjGHM80A+gipytWBUQns7H5mc3uVI
56NsV0fN5rS0cgdlx7DMRRj+ovqiOjsSZap3dum0TS88gPSHrLPgd2geQrD2oNn/
81R9xpQVpV2YFOe+QES9U41D3rs1AoHBAPOAjlKRcwt+ALBiRjB0dlZvyrlDw6mA
gY4qG3hLxUsu7DGlo8U6pLA4ov/Vn3uFb6wijV395fKA7Gp6BbX4GceyRh7+O0Eq
PSUAqj+kR7pK/wkewnpVn/+spM7wGGFLGRaMDODb9+fN+XLTNmWO7kzO88dc5EOI
5ZD5luXP/ddbKwDzHIBkEPuDfxYoqI1SDkii2d8+/jJTlPhWGjMFdGjkjOyBE7Np
GV8e6ILC+rPH2hSB5ANhzJORb0ctK2zPnwKBwQDbhtkpr7cUMsUkpsc/wWeWhkA1
K8zrBndOPoVAaeRvZOBH+gRMmkWXqdZXLwQdBFT65FhqFrSySL4qsWbg4APj49kc
r/9qhLjgv4/80xX1y9LtDwuRc5nwpSVgvNaY9lV+4OXs3vPCBa1Hgw2sOILw6qf8
z+QUuW3m1u077mcYaGVeHOEFJ9/3QLxbjCN2ZKgMtSTRaw9R8QKLt36fw3TjCJwg
nOig4wzWCJ+K6hRwzZIfLig49pSdqy4lEI8jPMcCgcEAri+bH8N+OY1ULtjN/uGA
uYpUyTyJXdpYUvsaFW6WXpbdTRKBWZf+sTSCnWISKMkmPkulNsRmVpgVBoHtTeOB
hZGoiYkxmAcAIFUedFIvITt+vuZrFhnkT4APkRy9Q/P1qWRb3gpch0yXkaU3d2TM
YNN2HXn8q7blCVURtamYmxJTa+V80PdEErdZFN/W2ukE4L7l/cXRDu0tVow5R6Ay
SsqbaJ9ZGXEoPhwdFVUnA64F+b+E/UpbjoBtysusEbVfAoHAer63hht93olUud2W
4wsdBIkkNZa8CV2gL9u9XfrXg4F/9j7RUJOh/d80vaLIRgE6Et7CoO75MgaCbhSr
VWlsQ2wO5X9y7Pgw91mlBNRyHvFMisgyy607kdaFQ8XSp8x0mXDensc6vG24KZgZ
eQZQEKzkKPOEsCHOVZgvJadzda5Jn4m+N8fH7tP/faCp43T8kb5nS8D02/hKWBzN
bD85iAg0Y/fYCr1pW/OOYXno2/nVjiGc09zr1Az2zDqqDiXZAoHARmaLerYeQnom
WgRvvjZSPvNs4/k/pudJl4IqymvuYtGwc736/dLqQnhNFzDL+IgV32ElqmREyWym
bOu+mVK3hzxoVCUw1jTj7o2XzaZuc7jV1lydfpKrwfshH2uju78GHw++jzvSX2QN
lsMhZ5qMFWvzJE2LAs/e6/zSySsu3nsLyXeOlQ9hOqcDx1b4KVV2mNrLpGX8cQl8
WWt0y+/w1hgmtdhfyRMXwJHhAZKhReowlbHr73fB7mpnZg16UrIJ
-----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.

Task 5 Success