[YouTube Video]
In this video, I will guide you through .NET deobfuscations covering a few exciting tricks and tips.
We will be using PowerShell and dnlib library.
We will create a universal string deobfuscator for Eternity Malware that uses some kind of custom obfuscation that is not so trivial at first sight.
[Original Eternity Stealer Sample] – Password:infected
[VirusTotal] MD5: 81255BA900760F07DE634DEB83472328
[Deobfuscated Eternity Stealer Sample] – Password:infected
[PowerShell Deobfuscation Script]
- Load dnlib via reflection so we can use it
- Load malware as moduleDef with dnlib
- Load malware via reflection so we can invoke functions
- Find string decryption method
- Create some global variable to collect all methods that will be removed after processing
- Find all call references to our string decryption method
- Process these locations to find arguments and decrypt strings by invoking string decryption routines on reflectively loaded malware (processing instructions)
- Patch locations with decrypted string
- Remove instructions related to string decryption
- Performing function inlining of dummy methods containing only ldstr and ret
- Find all those method´s locations from where they are called
- Replace those locations with the string value - function inlining
- Remove all methods that are not needed anymore
function FindStringDecryptionMethod($methods)
{
foreach($method in $methods)
{
if(-not $method.HasBody){continue}
if($method.Parameters.Count -eq 2 -and $method.Parameters[0].Type.FullName -eq "System.String" -and $method.Parameters[1].Type.FullName -eq "System.Int64" -and $method.ReturnType.FullName -eq "System.String")
{
return $method
}
}
return $null
}
function GetStateString($instr)
{
if($instr.OpCode.Name -like "call")
{
$global:methodsToRemove += $instr.Operand
return ($moduleRefl.ResolveMethod($instr.Operand.MDToken.ToInt32())).Invoke($null, $null)
}
if($instr.OpCode.Name -like "ldstr")
{
return $instr.Operand
}
return $null
}
[System.Reflection.Assembly]::LoadFile("C:\Users\Inferno\Desktop\test\dnlib.dll") | Out-Null
$dot2Patch = "C:\Users\Inferno\Desktop\test\eternity_stealer.exe"
$patchedDot = $dot2Patch + "_mod.exe"
$moduleRefl = [System.Reflection.Assembly]::LoadFile($dot2Patch).modules
$moduleDefMD = [dnlib.DotNet.ModuleDefMD]::Load($dot2Patch)
$methods = $moduleDefMD.GetTypes().ForEach{$_.Methods}
$decryptionMethod = FindStringDecryptionMethod -methods $methods
$global:methodsToRemove = @($decryptionMethod)
# string decryption
if(-not $decryptionMethod){Write-Host "Something went wrong, string decryption method was not found!!!" -ForegroundColor Red; Exit}
foreach($method in $methods)
{
if(-not $method.HasBody){continue}
foreach($instr in $method.MethodBody.Instructions.ToArray())
{
if($instr.OpCode.Name -like "call" -and $instr.Operand -eq $decryptionMethod)
{
$indexDecryptionMethodInstr = $method.MethodBody.Instructions.IndexOf($instr)
$intInstr = $method.MethodBody.Instructions[$indexDecryptionMethodInstr-1]
$decryptionMethod2 = $method.MethodBody.Instructions[$indexDecryptionMethodInstr-2]
$stateStr1 = GetStateString -instr $method.MethodBody.Instructions[$indexDecryptionMethodInstr-4]
$stateStr2 = GetStateString -instr $method.MethodBody.Instructions[$indexDecryptionMethodInstr-3]
if(-not $stateStr1 -or -not $stateStr2){Write-Host "Something went wrong, cannot find all arguments!!!" -ForegroundColor Red; continue}
$stateStr3 = ($moduleRefl.ResolveMethod($decryptionMethod2.Operand.MDToken.ToInt32())).Invoke($null, @($stateStr1, $stateStr2))
$decryptedString = ($moduleRefl.ResolveMethod($instr.Operand.MDToken.ToInt32())).Invoke($null, @($stateStr3, $intInstr.Operand))
$global:methodsToRemove += $decryptionMethod2.Operand
# We cant patch the instruction this way as it is a target of branch in 1 method (this way will not refresh the branch target - result in exception on writing)
# $patchInst = [dnlib.DotNet.Emit.Instruction]::Create([dnlib.DotNet.Emit.OpCodes]::Ldstr, $decryptedString)
# $method.MethodBody.Instructions.Insert($indexDecryptionMethodInstr-4, $patchInst)
# $method.MethodBody.Instructions[$indexDecryptionMethodInstr-4] = $patchInst
# workaround to avoid patching of branch target (this way will refresh the branch target)
$method.MethodBody.Instructions[$indexDecryptionMethodInstr-4].Opcode = [dnlib.DotNet.Emit.OpCodes]::Ldstr
$method.MethodBody.Instructions[$indexDecryptionMethodInstr-4].Operand = $decryptedString
$method.MethodBody.Instructions.RemoveRange($indexDecryptionMethodInstr-3, 4)
}
}
$method.MethodBody.UpdateInstructionOffsets() | Out-Null
}
# function inlining of dummy methods containing only ldstr and ret
foreach($method in $methods)
{
if(-not $method.HasBody){continue}
foreach($instr in $method.MethodBody.Instructions.ToArray())
{
if(-not ($instr.OpCode.Name -like "call" -and $instr.Operand.IsMethod)){continue}
if($instr.Operand.MethodBody.Instructions.Count -eq 2 -and $instr.Operand.MethodBody.Instructions[0].OpCode.Name -like "ldstr" -and $instr.Operand.MethodBody.Instructions[1].OpCode.Name -like "ret")
{
$strToInline = $instr.Operand.MethodBody.Instructions[0].Operand
$global:methodsToRemove += $instr.Operand
$instrIndex = $method.MethodBody.Instructions.IndexOf($instr)
$method.MethodBody.Instructions[$instrIndex].Opcode = [dnlib.DotNet.Emit.OpCodes]::Ldstr
$method.MethodBody.Instructions[$instrIndex].Operand = $strToInline
}
}
$method.MethodBody.UpdateInstructionOffsets() | Out-Null
}
foreach($method in ($global:methodsToRemove | Sort-Object -Property MDToken -Unique))
{
$method.DeclaringType.Remove($method)
}
$moduleWriterOptions = [dnlib.DotNet.Writer.ModuleWriterOptions]::new($moduleDefMD)
$moduleWriterOptions.MetadataOptions.Flags = $moduleWriterOptions.MetadataOptions.Flags -bor [dnlib.DotNet.Writer.MetadataFlags]::KeepOldMaxStack
# to ignore exception during writing - could be good to write even if some exception thrown
# $moduleWriterOptions.Logger = [dnlib.DotNet.DummyLogger]::NoThrowInstance
$moduleDefMD.Write($patchedDot, $moduleWriterOptions)
dnlib library - https://github.com/0xd4d/dnlib
dnSpyEx - https://github.com/dnSpyEx/dnSpy
SizeOf Fixer - https://github.com/RivaTesu/SizeOf-Fixer
List of .NET deobfuscators - https://github.com/NotPrab/.NET-Deobfuscator