Description
With the malware extracted, Holmes inspects its logic. The strain spreads silently across the entire network. Its goal? Not destruction-but something more persistent…friends.
TL;DR
- DLL initializing COM:
ole32.dll(IDA -> Imports tab, functionsCoInitialize,CoCreateInstance,OleRun) - Object GUID (CLSID):
DABCD999-1234-4567-89AB-1234567890FF(from.rdatadefinition ofrclsid) - .NET bridge feature:
COM Interop(fromQueryInterfaceonpUnknown[0]->lpVtbl) - Opcode for first managed call:
FF 50 68(Hex View of call using offset+104LL) - Key generation constants:
7, 42h(loop:7 * v6 + 66) - Opcode calling decryption logic:
FF 50 58(Hex View, call offset+88LL) - Win32 API resolving killswitch:
getaddrinfo(call made before killswitch check) - Network API for share enumeration:
NetShareEnum(inside functionsub_140001310) - Opcode running encrypted payload:
FF 50 60(Hex View, call offset+96LL) - Generated XOR key (Base64):
QklQV15lbHN6gYiPlp2kq7K5wMfO1dzj6vH4/wYNFBs=(from keygen function) - Killswitch domain:
k1v7-echosim.net(decrypted from"KXgmYHMADxsV8uHiuPPB3w==") - Flag:
HTB{Eternal_Companions_Reunited_Again}(Block DNS -> last page)
Solution
Attached to the challenge is an executable malware binary, AetherDesk-v74-77.exe, and a PDB for the executable.
During execution, the malware initializes the COM library on its main thread. Based on the imported functions, which DLL is responsible for providing this functionality? (filename.ext)
When opening the executable in IDA, we can find calls to CoInitialize, CoCreateInstance and OleRun at the beginning of the main function.
All three of these belong to the Component Object Model (COM) API.

To find out which DLL these functions are imported from, we can open the Imports tab and locate the function names.

Answer: ole32.dll
Which GUID is used by the binary to instantiate the object containing the data and code for execution?
To find what GUID is used, we can take a look at the CoCreateInstance call.
mov [rsp+278h+var_208], rdi
lea rax, [rsp+278h+pUnknown]
mov [rsp+278h+ppv], rax ; int
lea r9, riid ; riid
xor edx, edx ; pUnkOuter
mov r8d, 17h ; dwClsContext
lea rcx, rclsid ; rclsid
call cs:CoCreateInstance
One of those values used in the CoCreateInstance call is the GUID we are looking for. According to Microsoft Learn, the first parameter (rclsid) specifies the CLSID of the object being instantiated.
Looking up the value for rclsid in .rdata reveals the raw bytes for the struct.
.rdata:0000000140005A48 ; const IID rclsid
.rdata:0000000140005A48 rclsid IID <0DABCD999h, 1234h, 4567h, <89h, 0ABh, 12h, 34h, 56h, 78h, 90h, \
.rdata:0000000140005A48 ; DATA XREF: sub_140001310+446↑o
.rdata:0000000140005A48 ; main+5B↑o
.rdata:0000000140005A48 0FFh>>
Following the type definition from IID -> GUID -> _GUID shows the struct definition used for the GUID.
00000000 struct _GUID // sizeof=0x10
00000000 { // XREF: GUID/r
00000000 unsigned int Data1;
00000004 unsigned __int16 Data2;
00000006 unsigned __int16 Data3;
00000008 unsigned __int8 Data4[8];
00000010 };
Using the data found in rclsid and the _GUID struct definition, we can reconstruct the GUID.
Answer: DABCD999-1234-4567-89AB-1234567890FF
Which .NET framework feature is the attacker using to bridge calls between a managed .NET class and an unmanaged native binary? (string)
If we use the decompiler in IDA, we can get c-style pseudocode of the assembly.
In the beginning of the decompiled code, we can see the COM calls.
v3 = 0;
CoInitialize(0);
v74[0] = 0;
Instance = CoCreateInstance(&rclsid, 0, 0x17u, &riid, (LPVOID *)pUnknown);
if ( Instance < 0 )
goto LABEL_6;
Instance = OleRun(pUnknown[0]);
if ( Instance >= 0 )
Instance = ((__int64 (__fastcall *)(LPUNKNOWN, void *, _QWORD *))pUnknown[0]->lpVtbl->QueryInterface)(
pUnknown[0],
&unk_140005AB8,
v74);
((void (__fastcall *)(LPUNKNOWN))pUnknown[0]->lpVtbl->Release)(pUnknown[0]);
The call to pUnknown[0]->lpVtbl->QueryInterface indicates that calls between native and managed code are handled through COM Interop.
Answer: COM Interop
Which Opcode in the disassembly is responsible for calling the first function from the managed code? (** ** **)
The first managed call using a pointer with an offset is: (*(void (__fastcall **)(__int64, __int64 *))(*(_QWORD *)v5 + 104LL))(v5, &v73);
To find out what opcode is used, we can synchronize the pseudocode with the hex view by right clicking on a row and select “Synchronize with”. Now we have a green highlight marking the row that’s synchronized with the hex viewer.

Marking the row and opening the hex view shows the hex values for the opcode.

Answer: FF 50 68
Identify the multiplication and addition constants used by the binary’s key generation algorithm for decryption. (*, **h)
Looking through the decompilation, we can find the key generation in this loop.
if ( dword_140008098 < 6 )
{
do
{
*((_BYTE *)&pHints.ai_flags + v6) = 7 * v6 + 66;
++v6;
}
while ( v6 < 0x20 );
}
Answer: 7, 42h
Which Opcode in the disassembly is responsible for calling the decryption logic from the managed code? (** ** **)
The next call, after key setup, is:
v54 = (*(__int64 (__fastcall **)(__int64, _QWORD, _QWORD, PCSTR *))(*(_QWORD *)v50 + 88LL))(
v50,
*(_QWORD *)v53,
*v51,
&pNodeName);
We can use the same technique as before to find the opcode.

Answer: FF 50 58
Which Win32 API is being utilized by the binary to resolve the killswitch domain name? (string)
We can find a call to getaddrinfo in the code right before the kill switch string.
v58 = getaddrinfo(v57, 0, &pHints, (PADDRINFOA *)pUnknown);
if ( pUnknown[0] )
freeaddrinfo((PADDRINFOA)pUnknown[0]);
WS2_32_116();
if ( v58 )
{
LABEL_57:
sub_1400027B0(std::cout, "[No Kill Switch Detected] Continuing Execution...\n");
sub_1400029D0(std::wcout, L"[*] Started SMB propagation ...\n");
Answer: getaddrinfo
Which network-related API does the binary use to gather details about each shared resource on a server? (string)
In the loop after the kill switch check, we can find the code responsible for scanning the network. In this code, we can find the following snippet.
v64 = sub_1400027B0(std::cout, "[*] Scanning.. ");
sub_1400027B0(v64, "...\n");
sub_140001310((LPCCH)&pHints);
Inside the function sub_140001310 we can find a call to NetShareEnum.
if ( !NetShareEnum(v15, 1u, &bufptr, 0xFFFFFFFF, &entriesread, &totalentries, 0) && bufptr )
Answer: NetShareEnum
Which Opcode is responsible for running the encrypted payload? (** ** **)
In the function sub_140001310, we can find the following call.
(*(void (__fastcall **)(__int64, __int64, _QWORD, _QWORD))(*(_QWORD *)v37 + 96LL))(
v37,
v50,
*(_QWORD *)v41,
*(_QWORD *)v40);
Using the previous technique, we can get the opcode.

Answer: FF 50 60
Find → Block → Flag: Identify the killswitch domain, spawn the Docker to block it, and claim the flag. (HTB{_})
Now we have to recreate the XOR key. We found the key generation algorithm previously.
do
{
*((_BYTE *)&pHints.ai_flags + v6) = 7 * v6 + 66;
++v6;
}
while ( v6 < 0x20 );
Converting this to Python, we get something like:
seq = bytes((7 * i + 0x42) & 0xFF for i in range(32))
Base64 encoding the key, we get: QklQV15lbHN6gYiPlp2kq7K5wMfO1dzj6vH4/wYNFBs=
Now we have to find the data containing the domain name. Right around the call to the decryption function, we find *(_QWORD *)v53 = sub_1400031B0("KXgmYHMADxsV8uHiuPPB3w==");. Using this value, and the previously generated key, we can decrypt the domain name by XORing the values (e.g. in Python or CyberChef), which gives: k1v7-echosim.net.
Browsing to the web page in the Docker container, we get a DNS blocklist manager.

Entering our domain name and clicking block forwards us to a new screen.

Stepping through the steps reveals the last screen containing the final flag.

Answer: HTB{Eternal_Companions_Reunited_Again}