Skip to content

Latest commit

 

History

History
135 lines (121 loc) · 7.77 KB

File metadata and controls

135 lines (121 loc) · 7.77 KB

Deobfuscation of .NET using PowerShelling & dnlib - Eternity Malware

[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]

Steps during deobfuscation:

  • 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

PowerShell Deobfuscation Script

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)

References

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