Inside the APT28 DLL Backdoor Blitz
This blog post is a follow-up to Flirting With IDA and APT28, where we covered generating IDA Pro signatures to identify and eliminate benign library code. In doing so, we can focus our attention on the bespoke code responsible for defining the implant’s behavior.
This time around we perform a deep dive into the same APT28 sample by analyzing its capabilities and providing insight into its features.
Overview
Hash: B40909AC0B70B7BD82465DFC7761A6B4E0DF55B894DD42290E3F72CB4280FA44 File Type: Windows x64 DLL PE Compilation Timestamp: 4th July 2018 14:38:54 |
Figure 1: APT28 sample details
Analysis reveals the implant is a multi-threaded DLL backdoor that gives the threat actor (TA) full access to, and control of, the target host. When commanded by C2, the implant can upload or download files, create processes, interact with the host via a command shell and connect to C2 according to a defined sleep/activity schedule.
As covered in Part 1, the implant is written in C++ and statically linked against OpenSSL and the Poco C++ framework. Since the file is packaged as a DLL, the intention would be to inject it into a long-running process that is granted Internet access (such as a NetSvc service group) or one having local firewall permissions. We do not believe this DLL is intended to operate as a module for a larger tool.
Entry Point
Malicious code entry occurs via the DllMain export. The implant’s first task is to load the legitimate Microsoft npmproxy.dll found in “c:\windows\system32”. With this loaded, the addresses of the five implant exports, “DllCanUnloadNow”, “DllGetClassObject”, “DllRegisterServer”, “DllUnregisterServer” and “GetProxyDllInfo” are set to the addresses of the same, benign exports found in the Microsoft DLL.
The reason for this is unconfirmed, but most likely serves a defensive measure to evade end point protection that scans export addresses looking for code changes (when compared to a predetermined signature database), or against a database of known-malicious code. Perhaps by setting its own exports to those of the benign copy, the implant is attempting to remain undetected:
Figure 2: Substitution of PE export addresses
With export addresses replaced, a thread is launched to execute the main code path. Within this, a second thread is launched responsible for send and receive operations with C2. A global variable forms the basis of inter-thread synchronization for processing of C2 messages.
Mutex
The primary thread’s first task is the creation of a Globally Unique ID (GUID) mutex “1b8232f6-6806-4733-901d-62bf3ef33e6c”. The GUID string can be found as plain text within the binary, offering a potential (albeit trivially replaceable) IoC. As is customary, if mutex creation fails the sample terminates with no further action taken.
Machine Fingerprint
Following mutex creation, the implant generates a unique CRC-32 host fingerprint using the primary network interface’s ethernet (MAC) address, the host name, and Windows version string. The final CRC-32 result is then XOR’d with fixed constant 0x64113. The generated host fingerprint is used later to build the C2 beacon URL, and presumably identifies the implant’s malicious use on each infected machine.
Command-and-Control (C2)
The main communication protocol with the C2 server is RSA encrypted, Base64 encoded JSON exchanged over HTTPS (TLS, port 443), or as fallback, on plain HTTP using port 80. During our analysis the implant did not attempt to use any other application protocols.
The user-agent string for HTTP requests is populated from either the return value of the “ObtainUserAgentString” API call, or if that fails, a hard-coded alternate (see IoCs below).
Support for egress proxy servers is included through a call to “WinHttpGetIEProxyConfigForCurrentUser”. The result is parsed and supplied to the Poco framework’s “HTTPClientSession::setProxyConfig” prior to C2 activity.
A 1024-bit RSA key pair is embedded within the implant and used to encrypt and decrypt communication between host and C2. The private key decrypts inbound traffic while the public key encrypts outbound. No stream cipher(s) or session keys are negotiated.
The initial HTTPS beacon to “malaytravelgroup[.]com” is a GET request consisting of a randomly selected URL path concatenation, together with a query string containing the parameter split count, the XOR constant 0x64113 (also used when generating the host CRC-32), and the computed CRC-32 fingerprint. The query string is padded with random data to prevent ad-hoc analysis and finally Base64 encoded:
Field | Name | Bytes | Value | Comment |
1 | - | 1 | 1-254 | Parameter split count as ASCII value. |
2 | “aid” | 4 | 0x64113 | XOR constant used when generating host CRC-32. Possibly a unique implant ID. |
3 | “bid” | 4 | variable | CRC-32 host fingerprint. |
Table 1: C2 beacon query parameters
The URL path component is built as one to three strings (“/” separated), generated from a set of encrypted string tables. The decrypted versions are provided in the Appendix.
The Base64 encoded data is split into a random number of parameters and padded as a means of obfuscation. The query parameter’s names are randomly generated using 1-4 upper/lower case characters:
Name | Value | Explanation |
<rand_str> | <2_rand_chars> + b64(<n>) | Specifies the number of parameters across which the beacon data is split |
<rand_str_1> | <2_rand_chars> + b64(<rand_byte> + <data_part_1>) | Value contains first part of encoded data, preceded by 2 random characters |
<rand_str_2> | <2_rand_chars> + b64(<rand_byte> + <data_part_2>) | Value contains second part of encoded data, preceded by 2 random characters |
... | ... | ... |
<rand_str_n> | <2_rand_chars> + b64(<rand_byte> + <data_part_n>) | Value contains last part of encoded data, preceded by 2 random characters |
Table 2: Query parameter deconstruction
Examples of beacon request URLs:
GET /pricing/training/news/?GPFi=mLMg&CYlp=Tj9RNBBg%3D%3D&JOM=uJfgAz9Zpw GET /forum/feedback/switch/?LnY=YNA&D=TH%2FRM%3D&rMuo=BFXUEG&cs=bkqgA%3D&HZql=bXgTP1mnA%3D HTTP/1.1 GET /activity/?FLVE=JNA&Y=vKYxNB&F=Hg6wY%3D&Slq=QBuAAz&ux=lSGfWacA%3D%3D HTTP/1.1 |
Figure 3: Beacon URL examples
Using the first example in Figure 3, the first query parameter indicating the beacon data split count would be Base64 decoded as:
GPFi=mLMg >> b64_decode(Mg) >>0x32 ("2")
All successive query parameters have the first two characters removed prior to Base64 decoding. The first byte of the decoded result is then discarded to arrive at the final value:
CYlp=Tj9RNBBg== >> b64_decode(9RNBBg==) >> F5 13 41 06
JOM=uJfgAz9Zpw >> b64_decode(fgAz9Zpw) >> 7E 00 33 F5 9A 70
|
Once the beacon is received and decoded, the next phase of interaction is to issue commands that drive the implant’s functions, such as the provision of an activity schedule/sleep interval limiting implant activity.
Domain Generation Algorithm (DGA)
The primary C2 domain of “malaytravelgroup[.]com” is used for the initial beacon. Like all sensitive strings, it is XOR encoded and revealed only when needed.
The implant also contains a Linear Congruential Generator (LCG) based algorithm for generating what appear to be backup C2 domain names. Curiously the implementation is 100% deterministic owing to the use of a fixed seed value (0xC31) as shown in Figure 4. Every invocation of the DGA will produce the same five domains:
schooltillhungryprocess[.]com
reasonwithusefulpolicy[.]com
streetunderrelevantpeople[.]com
experiencewithweakkid[.]com
systembeforeniceparent[.]com
Figure 4: Encoded C2 servers
Once the domains are generated, they are XOR encoded. The LCG implementation can be seen here, together with its constant seed value:
Figure 5: DGA LCG using fixed seed value
Domain names are generated as the concatenation of 4 string-table lookups from 3 encrypted string tables. Table 3 is referenced twice during generation. The Top-Level Domain (TLD) suffix is hardcoded to “.com”.
Table | Number of strings |
1 | 99 |
2 | 31 |
3 | 149 |
Table 3: String table sizes for domain generation
Assuming the continued adoption of a four-part domain name, the LCG would be capable of generating millions of unique permutations. The reason for limiting its output to the same five, irrespective of date, time, host identity, or other input variable is unclear. A copy of the decrypted string tables is provided in the Appendix.
By manually decreasing the delay between check-in attempts, we accelerated the amount of C2 activity generated by the implant. Curiously, during a 24-hour observation window, no attempts were made to communicate with the generated domains. Different reachability scenarios were tested, ranging from malformed responses to unreachable IP addresses. At no point did the implant generate any DNS or HTTP traffic relating to the generated domains.
It’s conceivable the backup domains are accessed via HTTP 302 redirects issued by the primary site, but this would seemingly defeat the purpose of having primary and backup domains to cater for failure, migration, or shutdown. It’s also possible the generated C2 domains are active only for certain implant commands such as file upload or download.
Lacking any evidence for their direct use, we can speculate the generated domains may be operating as a honeypot. Access to them from any Internet addresses would immediately be considered “suspicious” by virtue of their hidden existence only within the implant code. The source of such requests could be directly attributable to threat researchers or analysts investigating the implant’s inner workings.
Functions
The implant lacks any modularity and is therefore limited to eight functions. Each function is invoked from C2 by supplying a value from which a known CRC-32 is calculated. The computed CRC-32 value serves as a key or look-up to execute the corresponding function.
Parameters for each backdoor supported function are supplied with each command and detailed in Table 4:
CRC-32 | Function | Parameters | Description |
15512FB9 | Spawn process | file_name, file_path, args | Spawn a process using the supplied arguments. |
76BCDC67 | Create file | file_name, file_path, body | Creates a file with the supplied body, i.e. file is pushed (downloaded) from C2. Files have the DOS hidden attribute set by default. |
B21CEBD4 | Delete file/folder | file_name, file_path, recursive | Deletes file or folder. Optional recursive parameter for folder and contents deletion. |
406C9E35 | Launch shell | start, stop, status, bash | Launches COMSPEC (cmd.exe) shell. Stdin, stdout, stderr relayed over main C2 channel. |
72E99D37 | Read file | file_name, file_path | Read a file, i.e. upload to C2. |
04365407 | Set C2 check-in interval | timeout | Sets the interval between C2 check-in. Default is 3 hours. |
99598005 | Set C2 check-in/activity schedule | day_of_month, day_of_week, hour, | Sets day of month/week and hour that implant should attempt to reach C2. Default setting is 0 for all values, meaning no exclusions; check-in would therefore be every 3 hours – the default sleep interval. |
1E46EC18 | Get status information | None | Returns Build_Id of implant, Computer_Id, Full_Info. Details of last command and status (success/fail). |
Table 4: Implant functions
The shell capability is perhaps the most interesting, giving the operators a full-duplex, interactive COMSPEC (cmd.exe) session, with stdin (input), stdout, and stderr (output returned by commands) redirected over the HTTPS C2 channel. Commands to the shell are issued by the TA with the resulting output being returned almost immediately.
Despite the shell pipe creation taking place in (as identified by IDA signatures) the Poco framework’s “SMTPChannel” constructor, the implant makes no use of SMTP as a transport. The “bash” parameter, synonymous with Linux environments, hints at a multi-platform capability. However, in this instance it is used to send commands to the shell by writing the accompanying value to stdin. Windows 10 does offer a Linux Subsystem that would make a bash shell available, but we have no evidence to suggest this is what the operators are installing or using:
Figure 6: Implant function dispatch
String Encryption
Thirteen unique XOR keys are used throughout the implant for runtime decoding of sensitive strings. Each implant function (as listed in Table 4) is provided with its own XOR key, as are general C2, URL/HTTP, and RSA related operations:
Key: | Used during: |
F226E34F0B64528CF0 | Embedded RSA key decryption |
371F41ABE4C8880BC1 | URL/HTTP related operations |
40A521B0866411BAC4 | General string decryption key |
EFEA221C400E2D1227 | C2 messaging |
82EF8E95992055B6B5 | C2 messaging |
4F840D589769 | Create process function |
6EFEAF1AAA6932 | Create file/download function |
59938EF1C8EFBA79B | Delete file/folder function |
722C1B76 | Shell handler function |
1F4582D80B | Read file/upload function |
6A39E0FC68D6 | Set C2 check-in interval function |
A24B726DFD | Set C2 activity schedule function |
597EB47AAD | Get host/install info function |
Table 5: XOR encoding/decoding keys
Conclusion
Analysis shows the implant carries a feature set designed to provide the fundamental capabilities of a backdoor: file download, upload, remote command execution, and interactive shell access.
Comparing the functions of this implant to published descriptions of APT-28’s “x-tunnel”, we find no tunneling, proxying, or VPN-like capabilities. It also lacks credential harvesting, network service scanning, or Windows registry manipulation. Given the lack of modularity only an updated version could provide these features. The alternative (and perhaps more likely) scenario would be for such capabilities to exist in subsequent tools downloaded and executed by this implant.
Indicators of Compromise (IoCs)
SHA256:
b40909ac0b70b7bd82465dfc7761a6b4e0df55b894dd42290e3f72cb4280fa44
C2 beacon domain:
malaytravelgroup[.]com
DGA domains:
schooltillhungryprocess[.]com
reasonwithusefulpolicy[.]com
streetunderrelevantpeople[.]com
experiencewithweakkid[.]com
systembeforeniceparent[.]com
Mutex:
1b8232f6-6806-4733-901d-62bf3ef33e6c
Hard-coded User-Agent string:
“Mozilla/5.0 (Windows NT 6.3; WOW64; rv:28.0) Gecko/20100101 Firefox/28.0”
Yara Signature
rule apt28_backdoor_cls
{
strings:
$st1 = "AES_256_poco" ascii
$st2 = "TEncryption" ascii
$st3 = "shell" ascii
condition:
all of them
}
rule apt28_backdoor_crc32
{
strings:
$xor1 = { 48 8B 07 39 48 0C 75 3A 44 8B 70 08 4C 8B 38 4D 85 C0 74 2E 45 85 E4 74 29 }
condition:
$xor1
}
Appendix
Decrypted String Tables:
Table 1 | Table 2 | Table 3 |
different | at | street
|
Table 6: C2 DGA Strings
Table 1 |
news |
Table 7: URL Path String Table