Analysing a low-prevalence loader written in Go
No language is safe from malware authors and Golang is no exception. In this blog post, I'll cover a low-prevalence loader that has not been publicly reported on to my knowledge. It displays a message-box about an updated version of Java, but downloads and executes a payload behind the scenes.
The loader arrives as an MPRESS-packed 64-bit PE binary and can be executed normally. According to this Reddit post, this loader was distributed to victims via malicious advertisements. Unpacking this binary by following this swell guide presents us with a Golang binary.
Using GoReSym on the unpacked binary recovers a lot of the original metadata about the binary, including the address of the original main function. While Go malware can sometimes be cumbersome to reverse-engineer, I was able to recover much of the symbol information using the PCLNTAB alongside SentinelOne's AlphaGolang scripts.
The following files were examined during analysis:
Filename | Submission Date |
Setup_Chrome-394010981.exe | 2023-09-05 |
Java_Edg-1207792131.exe | 2021-09-27 |
The following file is likely another sample of this loader, but I was not able to download it:
Filename | Submission Date |
JavaPlayerChrome-1157890851.exe | 2020-08-21 |
String Decryption
Although the loader is not particularly obfuscated, it does make use of the gobfuscator library to encrypt its static strings.
Writing an automatic string decryptor for this is relatively straight-forward. We can make use of the fact that this pattern of encryption calls the runtime:slicebytetostring()
function to return the decrypted contents. Let's use a YARA-rule to identify this runtime function in the binary and trace any references that are made to it.
import yara
yara_rule = """
rule runtime_slicebytetostring
{
strings:
$chunk_1 = {
48 83 F? 01
7? ??
[0-150]
48 85 C?
74 ??
48 83 F? 20
7? ??
[10-150]
E9 ?? ?? ?? ??
CC CC CC CC
}
condition:
any of them
}
"""
# Compile the YARA rule
compiled_rule = yara.compile(source=yara_rule)
# Scan the bytearray with the compiled rule
matches = compiled_rule.match(data=data)
We can then work backwards from the call instruction to extract the original one-time pads and decrypt the strings ourselves:
I have uploaded the string decryption script to GitHub. The 2021 sample of the loader starts off by contacting several hard-coded .local
domains, though with no apparent purpose. The newer sample from 2023 no longer exhibits this behavior.
Next, it shows the Java pop-up message to the user:
At the same time, the main loader functionality is executed.
Evasion
First, the loader extracts its own filename and attempts to execute a regular expression against it, similar to the below pseudocode.
import sys
import re
binary_filename = sys.argv[0] # C:\Users\Donald\Downloads\go-loader(sample).exe
binary_filename_stem = binary_filename.split("\\")[-1].split(".exe")[0].split("-")[1]
binary_filename_stem = re.sub(r"\([^()]*\)", "", binary_filename_stem) # loader
if (len(binary_filename_stem) > 0):
execute()
else:
"System failed"
Assuming that the loader's filename is of the right format, it then performs rudimentary anti-VM checks and attempts to contact any of its pre-configured C2 domains.
The anti-VM checks include enumerating over the registered display adapters and reading the DriverDesc
and MatchingDeviceId
values to compare their contents against the following hard-coded driver and vendor names:
String | Vendor |
VEN_8086 | Intel |
VEN_10DE | NVIDIA |
VEN_1002 | AMD |
VEN_102B | Matrox |
VEN_1A03 | Aspeed |
DISPLAY | |
IDDCX | |
DGLVRKDOD | |
IDDBUS2 |
These are legitimate device vendor IDs for display adapters. If the values on the host machine do not match any of the listed vendors, the payload will not execute any further. For reference, a typical VMWare analysis machine will contain the string VEN_15AD
whereas QEMU may contain VEN_1234
, neither of which are contained in the list.
In addition, the loader attempts to open the HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PrefetchParameters
registry key and read the BootId
value. If this value does not exist or it is less than 5, the loader will refuse to execute. According to this answer on StackOverflow, the BootId
value specifies the number of times that a Windows machine has rebooted. It would follow that a detonation machine might periodically restore a fresh snapshot and thus fail this check.
Communication
If the host machine is suitable for execution, the loader begins to contact its pre-configured addresses in search of an active C2 server. It does so by attempting to retrieve the file hosted at https[:]//<C2_URL>/robots.txt
and refusing to execute if no such file is hosted at the server or if the file is empty.
api.maxiscn[.]com
api.mx-share[.]com
api.mx-disk[.]com
api.octabx[.]club
api.oclabxc[.]xyz
api.kakz[.]info
api.4share[.]icu
api.picoh[.]xyz
api.mylinks[.]pw
api.octavity[.]xyz
Next, the loader will attempt to send a GET
request to the address https[:]//<C2_URL>/app/1
in order to read a string from the C2. If this string is not equal to "1"
, the loader will refuse to execute.
If a suitable C2 server is found, the loader will execute its main functionality and download a payload. To achieve this, the loader will attempt to download a file at https[:]//<C2_URL>/p.txt
from a separate list of pre-configured C2 servers.
secured.mx-share[.]com
secured.mx-disk[.]com
secured.octabx[.]club
secured.oclabxc[.]xyz
secured.kakz[.]info
secured.4share[.]icu
codec.picoh[.]xyz
codec.mylinks[.]pw
codec.maxiscn[.]com
codec.octavity[.]xyz
185.4.67[.]104
130.0.232[.]145
130.0.235[.]240
188.116.25.241
5.61.39.128
The content of this file is validated through an arithmetic expression. An example number that passes this validation for the 2021 sample is 24
.
If this file exists and its contents can be validated by the loader, the C2 domain is selected for further communication. Next, the loader will attempt to download a payload from the C2 server using a pre-configured filename at the following URL constructed during runtime:
https[:]//<C2_URL>/update/SearchIndexer.exe
After successfully downloading the payload, the loader will attempt to verify its MD5 checksum by comparing it against the content of a text file hosted at the following URL:
https[:]//<C2_URL>/update/SearchIndexer.exe.txt
Finally, the loader will write this file to disk and execute it with a few command-line parameters:
- The extracted loader name from before
- A hard-coded float value
0.50
- A hard-coded argument
--ayooe
Following either successful or unsuccessful execution of the payload, the loader deletes itself using cmd.exe /C del "<loader_path>"
:
Payload
Based on several unobfuscated field names in the binary, it's likely that the payload was intended to be a bitcoin miner of some sort.
config->MinerExeName = "SearchIndexer.exe"
config->CoinApiId = "1"
config->CoinSuffix = ""
In the sample at the center of this analysis, the "upx"
configuration option was enabled rather than the "trtl"
configuration option. The impact of this option changes the URL of the downloaded payload, which may indicate that the payload is expected to be either upx-
or trtl-
compressed.
Searching for files that match this description on various malware analysis platforms, this file stands out as a potential match. While the C2 infrastructure could not be contacted during analysis, it appears that this loader would have been used to serve an XMRig instance.
Indicators of Compromise
The following host IOCs were observed during analysis:
Path | Mode |
C:\Windows\Temp\SearchIndexer.exe | Write |
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PrefetchParameters | Read |
HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\0000 | Read |
HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\0001 | Read |
The following network IOCs were observed during analysis:
URL |
secured.mx-share[.]com |
secured.mx-disk[.]com |
secured.octabx[.]club |
secured.oclabxc[.]xyz |
secured.kakz[.]info |
secured.4share[.]icu |
codec.picoh[.]xyz |
codec.mylinks[.]pw |
codec.maxiscn[.]com |
codec.octavity[.]xyz |
185.4.67[.]104 |
130.0.232[.]145 |
130.0.235[.]240 |
188.116.25[.]241 |
5.61.39[.]128 |
api.maxiscn[.]com |
api.mx-share[.]com |
api.mx-disk[.]com |
api.octabx[.]club |
api.oclabxc[.]xyz |
api.kakz[.]info |
api.4share[.]icu |
api.picoh[.]xyz |
api.mylinks[.]pw |
api.octavity[.]xyz |
bull.mx-share[.]com |
YARA rules
rule go_loader {
meta:
description = "Matches a Golang loader that displays a Java pop-up"
strings:
$config_coin_1 = "CoinApiId"
$config_coin_2 = "CoinSuffix"
$config_domain_1 = "APIDomains"
$config_domain_2 = "SecuredDomains"
$config_name_1 = "InstallerName"
$config_name_2 = "MinerExeName"
$config_path_1 = "TempPath"
$config_path_2 = "System32Path"
$config_path_3 = "InstallPath"
$config_version_1 = "AppVersion"
$config_domain_3 = "PostbackDomain"
$mz = "MZ"
condition:
$mz at 0x0 and 8 of ($config*)
}
rule go_loader_insn {
meta:
description = "Matches a Golang loader that displays a Java pop-up"
strings:
$insn_p_txt_validation = {
48 B9 AB AA AA AA AA AA AA AA
48 0F AF C8
48 B? A8 AA AA AA AA AA AA 2A
48 01 ??
48 C1 C? 3D
48 B? AA AA AA AA AA AA AA 0A
48 39 C?
}
$insn_compare_rax_five = {
48 83 F8 05
7?
}
$insn_http_timeout = {
48 B? 00 50 5C 18 A3 01 00 00
48 89 ?? 28
}
$mz = "MZ"
condition:
$mz at 0x0 and all of ($insn*)
}