Post

NSA Codebreaker 2022 Task 5

Description:

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

Solution:

The challenge provides three files.

  1. ssh-agent - SSH Agent binary used for storing SSH keys and certificates.
  2. core - Core dump of SSH Agent containing the private key of the attacker in memory.
  3. data.enc - Encrypted message that needs to be decrypted with the extracted private key.

The core dump can be both statically and dynamically analyzed using GDB or Ghidra. Because I had never reversed a core dump before, I started by researching how I would recover the private key from a core dump of SSH-Agent and came across this article:

https://security.humanativaspa.it/openssh-ssh-agent-shielded-private-key-extraction-x86_64-linux/

The article walks through how the private key is stored in memory and how to find it.

Key Storage in SSH Agent

The beginning of the article displays a screenshot of the OpenSSH source code. To get a better idea of how ssh-agent works, I pulled the source code and started looking through it.

The screenshots in the article show that the ssh key and prekey are stored in an sshkey struct. The sshkey struct is then used in the identity struct.

SSH Agent identity struct relations

The identity struct is then used in the idtable struct which stores a linked list of identities. The idtable is instantiated as an uninitialized global variable which means it can be found in the .bss section.

1
2
3
4
5
6
7
struct idtable {
  int nentries;
  TAILQ_HEAD(idqueue, identity) idlist;
};

/* private key table*/
struct idtable *idtab;

The problem with finding where idtable is located in the .bss section is that PIE is enabled on the ssh-agent binary, meaning that the address of idtable in the core file will not be the same as the idtable address in the static binary. To find idtable in the core dump, I would need to figure out the PIE offset.

Finding the PIE Offset

Because the process was run on Ubuntu 20.04 as described in the challenge description, I had to spin up a docker container to do the dynamic analysis with GDB.

Using GDB it is possible to see the call stack up to where the program was crashed. Stack frame #2 shows __libc_start_main, which is followed immediately by an unnamed location in from #1. It can be reasonably assumed that this is the main function.

Core dump stack trace

At the time I solved this, I didn’t notice that the __libc_start_main function call has the pointer to main as the first parameter. If I had seen that I could have just grabbed the static address from the entry in Ghidra and gotten PIE that way without having to locate poll and __errno_location.

Disassembling the instruction pointer at this address shows some instructions being executed which are presumably inside main. The addresses here are the run time addresses that need to be compared to the static addresses to determine the PIE offset. Using this information and the presumption that the instructions are in main it is trivial to find the static address in Ghidra.

Note: the reason why I call x/32i $rip-5 instead of x/32i $rip is because RIP is pointing to the address after the active call function call which happens to be 5 bytes in size.

Disassembly of main in core dump

From the disassembly it can be seen that is program is calling poll before having a call to __errno_location. These two functions can be used to find the corrosponding static addresses in the ssh-agent binary. To find this address statically the ssh-agent binary must be loaded in Ghidra. Because symbols are stripped, it is impossible to just jump to main, so it will need to be found by checking the program’s entrypoint and seeing the function pointer that is loaded into RDI before the function call.

Ghidra: entrypoint call to main

After the main function is located, the call to poll and __errno_location can be found by searching either the listing window or the decompiler window.

Assembly instructions corrosponding to core dump in main

Now all that is left is to find the PIE offset using the static and dynamic addresses.

1
2
3
4
5
6
7
Poll function call:
Static = 0x88cc
Dynamic = 0x55af6a28f8cc

Offset = Dynamic - Static
       = 0x55af6a28f8cc - 0x88cc
       = 0x55af6a287000

Locating the Identity Struct

The PIE offset can now be used to find the dynamic address of any static item in the binary, including idtable. To find idtable in the core file, the function call to idtab_init must be found in main.

Ghidra: idtab_init function call in main

The reason this function is known to be idtab_init is because it preceeds a function called ssh_signal that is called 4 times, as shown in the code snippet from the source code

1
2
3
4
5
6
7
8
9
10
11
12
new_socket(AUTH_SOCKET, sock);
if (ac > 0)
  parent_alive_interval = 10;
idtab_init();
ssh_signal(SIGPIPE, SIG_IGN);
ssh_signal(SIGINT, (d_flag | D_flag) ? cleanup_handler : SIG_IGN);
ssh_signal(SIGHUP, cleanup_handler);
ssh_signal(SIGTERM, cleanup_handler);

if (pledge("stdio rpath cpath unix id proc exec", NULL) == -1)
	fatal("%s: pledge: %s", __progname, strerror(errno));
platform_pledge_agent();

The pointer returned from idtab_init is loaded into DAT_000567c0. The value 0x000567c0 is the static address in .bss where idtable is initialized to.

The dynamic address for idtable can now be calculated using the PIE offset found earlier.

1
2
3
4
Static = 0x000567c0
Offset = 0x55af6a287000
Dynamic = Static + Offset
        = 0x55af6a2dd7c0

Ghidra: idtable pointer in the core dump

The pointer above is pointing to 0x55af6b9d63c0, which is the head of idtable. This is confirmed by the structure of the memory matching the idtable struct as it is defined in the source code.

1
2
3
4
struct idtable {
  int nentries;
  TAILQ_HEAD(idqueue, identity) idlist;
};

idtable struct in code the dump

Extracting the Keys

The second value in the idtable is the head of the identity linked list and points to the identity, which is holding the desired sshkey struct.

identity struct in the code dump

Inside the identity struct, the next pointer, sshkey pointer, and comment string are listed sequentially in memory. Following the second pointer leads to the desired sshkey struct.

Following the data structure down the heap eventually shows the last four values in the struct: shielded_private, shielded_len, shield_prekey, shield_prekey_len, which matches the definition of the struct in the source code.

Pointers to the encrypted private key and the prekey in the core dump

Now that the locations and lengths of the necessary data are known, they can be extracted in gdb using dump memory.

GDB memory dump of private key and pre key

Decrypting the Private Key

The final step is to decrypt the RSA key either using python or gdb with ssh-keygen. I used the same process listed in the original article, using gdb with ssh-keygen to decrypt the RSA key.

1
2
3
4
5
$ tar xvfz openssh-8.6p1.tar.gz
$ cd openssh-8.6p1
$ ./configure --with-audit=debug
$ make ssh-keygen
$ gdb ./ssh-keygen

The following gdb script can be used to decrypt the private key and place it in a file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 = (FILE*)fopen("./shielded.dat", "r")
call (void)fread($shielded_private, 1, 1392, $fd)
call (void)fclose($fd)
set $fd = (FILE*)fopen("./prekey.dat", "r")
call (void)fread($shield_prekey, 1, 16384, $fd)
call (void)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, "./privkey.ssh", "", "comment", 0, "\x00", 0)
k
q

The result of this process is the private key in OpenSSH format.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
-----BEGIN OPENSSH PRIVATE KEY-----
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
Z0d2Ak+vNfRBkhAAAAB2NvbW1lbnQBAgM=
-----END OPENSSH PRIVATE KEY-----

Decrypting the Message

To decrypt the message and solve the challenge, the private key must be converted to PEM format. Converting the key format is as simple as running ssh-keygen -p -f privkey.pem -m pem.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
-----BEGIN RSA PRIVATE KEY-----
MIIG5AIBAAKCAYEA3NtUCpvh1iU/tcsMg0qOyvUa1PBU0hQLMRmOzJp6WfvcHAZ3
x8HUnCnwz2bjKBi3QIhPwhUyF4oXb4mykYavv7IkQOCJQ03Nlpp8i3swCiA/WQT6
lBORmcrNodG1QqUJBFq82dRxdk8t31liXnwby70Zxgkope7pgnSj44FVQIC2rymF
inErTcrGd03eKrrgh4Qm4XxqNKMFXh+OpZD0Dbmd5b3cUC38SDwbYU8nMkOeXjo4
gj25SMEWTAT0Lec+Qz83Pho15DoYsJdtB9DKy3jmp8kvfMntuHgvVRXhsBPmbBjD
xj5ZKykOHeRyfDUiRCY2mWoExUa1lqw69nOqCpUpDmUQtLGx9RNoh18yHLyvNfsm
I/+m1JX0W7IObs/I+7g8j1mkmxElml8pSKy+gihZBHS1HSmqUtBBU7TODRWYpZ9a
IttUu00r4MIOneV2Mh2Ucr/swlkEJY8buRxG+iTEIhI1HNXuwSbxWlx4i5SpsbUz
Cxv6dILmX9LylSdrAgMBAAECggGAd1mvWOxUZr1KaJuJ74ljERrTnS8jJ0PdqHL/
UGJKrEYG9L4qDLEajCm+ENaw+wIgRadkMqXxo/bkI0puTWZTo2xJWyX8B3sMFs71
bwrrMw2qLhkasNrCXDHUXhZNte4pqUi/tZewmRbA22oaVqULAFb4jqR0avdpCS6v
QkqqH2lvT8lIeUAe/rMN/Xr/DGhg3dr0h/YMDtXqGKtFEwP6X1Bnm7e2Tz4Kj56r
zTJRJECWXKVp1Dg24LI3sm0a55OGeOxb+UOVIExHNZIi9jpJ8XDBPb2M7oxKPAQ5
ADWdwCDwrKVYqYB805JVW8wu5J1wmVrpGOAeLbZQp5WtUQjgXFy2y88eSPID/oxk
Gkvht3Q8pdN2wH6J8B/vqoZHr9bTTU8d5uPEhzWI7a/4RENPzUKk4aTetpELFZSi
Ow9vS4gxldYtXjf7cE6DyiMLhfy9DcwZVGt/uuZtYJsF0vUA+O03QuMaNaai1zCF
+vrb7oE25G9kKvyPeRL08fx9Pt4BAoHBAPeTG4wjVUCioDRPeUaCwko1ok/RkxPD
rMxElVYoBl919mkwFXjwLrfRZWDEt96wZO0+NKnybTDCMRpaO530EWrI5gFS61xo
wfcAC7MfD2Pd4cEKqCHUSsYgWzPiaT4F1BKhynwbFsAdmaZQ+0RzTHUIN7fFwAGx
CM6gh+V4CuCjvtkK/t4JTtDuEpH5WZtRDekp+IscnnvPE2BK37JiEwNGnZVdB4+3
1J2xPpAcTqq5KhQ+wBeBdS2ikT8LTFazCwKBwQDkX3O5mcmnVxzfyqMkREJc3r6K
+rbdiXUUk3FA63FVmXqj2w2b8ROUXiEISbEgO+XTurYYzf4Qso9BsiDWOjGYtNG0
141ROlRRdZdq1QLV2z7XL8eX9fe8gUfkDYEP9DSbHpN0q7Tsv0UmW72R/YHkSDBN
ti7o0QlTYetlNE15aocQH4XbBCWpEhn/OPdUhMWoPfsVk1DGD4TQijdqK1eK+cpf
aYDf3KNvelhSxigTazejMzEglnR3YCT6819EGSECgcEAiRQz2YkqyAoDiFNExAzc
hPhjcayJshTTFZsX0MeCl9KZ6C4OhZL/Wxoe9tCVOkES8OVThZHMcYXkaEHz5oZg
Km8oIy2FUfpTA29MCxa0j8goGpnK9Eg2SrNZrEW9nfDeNp7MnaDmHOOG0rbeGU15
1QcCysc8g/NA/B+Yfy7TXwRrRIO5ELm4oShgseCNg9kCScrKakQjYEwM33E1oPB6
tIKh+DS1XhccK2AbUvHJgO/bY7BG7fzpI6Zyo6Se1RZ3AoHAX4HL1AMM4n78BFuq
frBNUKmW5miTsXKbFE/VPWE5tKLLN1uVBXJ8zb/P8Ldg7Cogo7uiDB2Z80G5x6/H
K9CKjWKRkR/UafQK70ZOXM9YsDdQwI2q21JymNM4TZeYMiPfHEBdSp3EvH4BXVlg
nn12pRHLobRfSd6iF80LtPd6rxxt/8AvKrlBRsPbO3GHfkFIqGPDbfJ+BVbYJJ6p
Li2SHvz4NY7Z5sVPVH/GEFfuyrA8RHRUR1ykuIfs70Z4wPIBAoHBAKIL3XE2alWr
yL7bdBQIQyaYjMv/00jnamMngqzl9odqRMmdd7Njz8lVOsCftBCXsbDxCLmollut
uKC64cCXFeHPtdfOTBkcBoSf6DcqrPCjKTr1Yzfe49ZDFd1jbQQ8XasTun/K+PxA
oRvwugpbg0Uev90WUA1rUMVRAP5BvYfWqZtl8/X2TWW6TwuKxi28XmKR1l7CIL6g
h1StuRAQ/Do+Anx3nIxS2stUlilDKly1T8WFFSOG7skqR0X3fodzDA==
-----END RSA PRIVATE KEY-----

The final step is to decrypt the file using openssl pkeyutl -decrypt -inkey privatekey.pem -in data.enc.

1
2
# Netscape HTTP Cookie File
jbjl<redacted>pcxooy.ransommethis.net	FALSE	/	TRUE	2145916800	tok	eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NTQ4Mzg3MDMsImV4cCI6MTY1NzQzMDcwMywic2VjIjoiV1JlQXNabkM4YXRFRUxkQVBwY0lzMHlIQ0x6cVBpMWEiLCJ1aWQiOjI2NTY3fQ.Qn5LtF2XCtKOPLkGzPfrLG7WTMFwvnNihI_yzIJDAYk 
This post is licensed under CC BY 4.0 by the author.