探索QueueUserApc(1)

本文详细介绍了用户模式下的异步程序调用(APC)原理及其在Windows系统中的实现方式,包括如何利用QueueUserAPC API进行远程线程注入,并提供了一个简单的MFC示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

声明:这里只看重要的调用信息和为什么这样做,其他的就不看了

APC(Asynchronous procedure call)异步程序调用,
在NT中,有两种类型的APCs:用户模式和内核模式。

用户APCs运行在用户模式下目标线程当前上下文中,并且需要从目标线程得到许可来运行。特别是,用户模式的APCs需要目标线程处在alertable等待状态才能被成功的调度执行。通过调用下面任意一个函数,都可以让线程进入这种状态。这些函数是:KeWaitForSingleObject, KeWaitForMultipleObjects, KeWaitForMutexObject, KeDelayExecutionThread。
对于用户模式下,可以调用函数SleepEx, SignalObjectAndWait, WaitForSingleObjectEx, WaitForMultipleObjectsEx,MsgWaitForMultipleObjectsEx 都可以使目标线程处于alertable等待状态,从而让用户模式APCs执行,原因是这些函数最终都是调用了内核中的KeWaitForSingleObject,KeWaitForMultipleObjects,KeWaitForMutexObject, KeDelayExecutionThread等函数。

另外通过调用一个未公开的alert-test服务KeTestAlertThread,用户线程可以使用户模式APCs执行。
当一个用户模式APC被投递到一个线程,调用上面的等待函数,如果返回等待状态STATUS_USER_APC,在返回用户模式时,内核转去控制APC例程,当APC例程完成后,再继续线程的执行.

以上解释是我拿别人的

但是不太对,首先不用WaitForSingleObjectEx等上面的函数也可注入成功,这方面可以,我测试可行,为什么不可行我们就要探索一下,如下,其次内核中当KernelRoutine 执行完后,NormalRoutine为空的时候才会调用KeTestAlertThread去 检查该线程是否可以交付另一个用户模式APC

 

探索:

用户模式APCwindows2000调用过程如下

QueueUserAPC(
PAPCFUNC pfnAPC,
HANDLE hThread,
ULONG_PTR dwData
)
/*++

Routine Description:

This function is used to queue a user-mode APC to the specified thread. The APC
will fire when the specified thread does an alertable wait.

Arguments:

pfnAPC - Supplies the address of the APC routine to execute when the
APC fires.

hHandle - Supplies a handle to a thread object. The caller
must have THREAD_SET_CONTEXT access to the thread.

dwData - Supplies a DWORD passed to the APC

Return Value:

TRUE - The operations was successful

FALSE - The operation failed. GetLastError() is not defined.

--*/

{
NTSTATUS Status;

Status = NtQueueApcThread(
hThread,
(PPS_APC_ROUTINE)BaseDispatchAPC,
(PVOID)pfnAPC,
(PVOID)dwData,
NULL
);

if ( !NT_SUCCESS(Status) ) {
return 0;
}
return 1;
}

 

NtQueueApcThread(
IN HANDLE ThreadHandle,
IN PPS_APC_ROUTINE ApcRoutine,
IN PVOID ApcArgument1,
IN PVOID ApcArgument2,
IN PVOID ApcArgument3
)

/*++

Routine Description:

This function is used to queue a user-mode APC to the specified thread. The APC
will fire when the specified thread does an alertable wait

Arguments:

ThreadHandle - Supplies a handle to a thread object. The caller
must have THREAD_SET_CONTEXT access to the thread.

ApcRoutine - Supplies the address of the APC routine to execute when the
APC fires.

ApcArgument1 - Supplies the first PVOID passed to the APC

ApcArgument2 - Supplies the second PVOID passed to the APC

ApcArgument3 - Supplies the third PVOID passed to the APC

Return Value:

Returns an NT Status code indicating success or failure of the API

--*/

{
PETHREAD Thread;
NTSTATUS st;
KPROCESSOR_MODE Mode;
KIRQL Irql;
PKAPC Apc;

PAGED_CODE();

Mode = KeGetPreviousMode();

st = ObReferenceObjectByHandle(
ThreadHandle,
THREAD_SET_CONTEXT,
PsThreadType,
Mode,
(PVOID *)&Thread,
NULL
);

if ( NT_SUCCESS(st) ) {
st = STATUS_SUCCESS;
if ( IS_SYSTEM_THREAD(Thread) ) {
st = STATUS_INVALID_HANDLE;
}
else {
Apc = ExAllocatePoolWithQuotaTag(
(NonPagedPool | POOL_QUOTA_FAIL_INSTEAD_OF_RAISE),
sizeof(*Apc),
'pasP'
);

if ( !Apc ) {
st = STATUS_NO_MEMORY;
}
else {
KeInitializeApc(
Apc,
&Thread->Tcb,
OriginalApcEnvironment,
PspQueueApcSpecialApc,
NULL,
(PKNORMAL_ROUTINE)ApcRoutine,
UserMode,
ApcArgument1
);

if ( !KeInsertQueueApc(Apc,ApcArgument2,ApcArgument3,0) ) {
ExFreePool(Apc);
st = STATUS_UNSUCCESSFUL;
}
}
}
ObDereferenceObject(Thread);
}

return st;

标黑色地方看到了对系统线程的“照顾”。自己实现这套流程岂不是很吊。。。。就没有这么多限制。。。。上面一些内容不懂的拿windbg调试一下,然后看看书就知道了

用户模式APC被插入链表后
交付完内核APC链表中的APC对象以后,如果APC_ LEVEL软件中断发生在用户模式下(即原来的模式是UserMode),并且该线程确实有用户模式APC对象在等待交付,则进人用户模式APC对象的交付处理。其过程类似,它在访问APC链表时,也要提升IRQL
至DISPATCH LEVEL并且锁住APC链表。类似于普通内核模式APC的交付过程,KiDeliverApc函数通过线程对象中ApcState成员的UserApcPending标志保证一一个用户模式APC不会打断另一个用户模式APC。用户模式APC的交付是这样完成的:先调用

APC对象的KernelRoutine 例程,将NormalRoutine 作为参数传递给它,如果它返回以后,NormalRoutine 为NULL,则调用KeTestAlertThread函数, 检查该线程是否可以交付另一个用户模式APC; 否则,调用KiInitializeUserApc函数,为该用户模式APC初始化一个执行环境。

由于用户模式APC的NormalRoutine是一个在用户模式下运行的函数(位于用户地址空间),而KiDeliverApc 是在内核模式下运行的,所以,KiDeliverApc 只是调用KilnitializeUserApc来设置好用户APC例程将来被调用的环境。由于从内核模式到用户模式是通过一个陷阱帧返回的,所以,KiInitializeUserApc 将陷阱帧中的用户模式返回地址
(即Eip寄存器)设置为KeUserApcDispatcher函数的地址,并且将NormalRoutine等信息传递到用户栈中恰当的位置上,以便将来KeUserApcDispatcher 可以访问。这里,KeUserApcDispatcher是ntdll.dll 中的函数地址(函数名称为KiUserApcDispatcher) 。

上面的话可能一时不好理解,但是我们可以看源码(KiDeliverApc 他是执行APC的函数)

 // Kernel APC queue is empty. If the previous mode is user, user APC
    // pending is set, and the user APC queue is not empty, then remove
    // the first entry from the user APC queue, set its inserted state to
    // FALSE, clear user APC pending, release the dispatcher database lock,
    // and call the specified kernel routine. If the normal routine address
    // is not NULL on return from the kernel routine, then initialize the
    // user mode APC context and return. Otherwise, check to determine if
    // another user mode APC can be processed.
    //

    if ((IsListEmpty(&Thread->ApcState.ApcListHead[UserMode]) == FALSE) &&
       (PreviousMode == UserMode) && (Thread->ApcState.UserApcPending == TRUE)) {
        Thread->ApcState.UserApcPending = FALSE;
        NextEntry = Thread->ApcState.ApcListHead[UserMode].Flink;
        Apc = CONTAINING_RECORD(NextEntry, KAPC, ApcListEntry);
        KernelRoutine = Apc->KernelRoutine;
        NormalRoutine = Apc->NormalRoutine;
        NormalContext = Apc->NormalContext;
        SystemArgument1 = Apc->SystemArgument1;
        SystemArgument2 = Apc->SystemArgument2;
        RemoveEntryList(NextEntry);
        Apc->Inserted = FALSE;
        KiUnlockApcQueue(Thread, OldIrql);
        (KernelRoutine)(Apc, &NormalRoutine, &NormalContext,
                        &SystemArgument1, &SystemArgument2);

        if (NormalRoutine == (PKNORMAL_ROUTINE)NULL) {
            KeTestAlertThread(UserMode);

        } else {
            KiInitializeUserApc(ExceptionFrame, TrapFrame, NormalRoutine,
                                NormalContext, SystemArgument1, SystemArgument2);
        }

 

所以在NormoalRoutine不为NULL下,KeUserApcDispatcher很重要,这是NTDLL他的实现(IDA f5大法)

 

后面的实现还要深入才行,先吃饭下午搞

这是APCr3简单注入(MFC)

 

编程实现思路:

我们用CreateProcess以挂起的方式打开目标进程。
WriteProcessMemory向目标进程中申请空间,写入DLL名称。
使用QueueUserAPC()这个API向队列中插入Loadlibrary()的函数指针,加载我们的DLL

API介绍
DWORD QueueUserAPC( PAPCFUNC pfnAPC, // APC function
HANDLE hThread, // handle to thread 
ULONG_PTR dwData // APC function parameter);
参数1:APC回调函数地址;
参数2:线程句柄
参数3:回调函数的参数


void CAPCInjectDlg::OnInject() 
{
// TODO: Add your control notification handler code here
DWORD dwRet = 0;
PROCESS_INFORMATION pi;
STARTUPINFO si;
ZeroMemory(&pi,sizeof(pi));
ZeroMemory(&si,sizeof(si));
si.cb = sizeof(STARTUPINFO);

//以挂起的方式创建进程
dwRet = CreateProcess(m_strExePath.GetBuffer(0),
NULL,
NULL,
NULL,
FALSE,
NULL,
NULL,
NULL,
&si,
&pi);

if (!dwRet)
{
MessageBox("CreateProcess失败!!");
return;
}

PVOID lpDllName = VirtualAllocEx(pi.hProcess, 
NULL, 
m_strDllPath.GetLength(), 
MEM_COMMIT, 
PAGE_READWRITE); 


if (lpDllName)
{
//将DLL路径写入目标进程空间
if(WriteProcessMemory(pi.hProcess, lpDllName, m_strDllPath.GetBuffer(0),m_strDllPath.GetLength(), NULL))
{
LPVOID nLoadLibrary=(LPVOID)GetProcAddress(GetModuleHandle("kernel32.dll"),"LoadLibraryA");
//向远程APC队列插入LoadLibraryA
if(QueueUserAPC((PAPCFUNC)nLoadLibrary,pi.hThread,(DWORD)lpDllName))
{

MessageBox("QueueUserAPC成!!");

}
}
else
{
MessageBox("WriteProcessMemory失败!!");
return;
}
}

MessageBox("APC注入成功");
}

 //注入代码

#include "stdafx.h"
#define EXPORT __declspec(dllexport)

extern "C" EXPORT void MsgBox()
{
MessageBox(NULL, L"成功注入了", NULL, MB_OK);
}

main函数执行即可

 

转载于:https://www.cnblogs.com/L-Sunny/p/9182787.html

### QueueUserAPC 函数参数详解 `QueueUserAPC` 是 Windows 提供的用于将异步过程调用(APC)排队到指定线程的函数。其函数原型如下: ```c DWORD QueueUserAPC( PAPCFUNC pfnAPC, HANDLE hThread, ULONG_PTR dwData ); ``` #### 参数说明 1. **`pfnAPC`** 该参数是一个指向 APC 回调函数的指针,类型为 `PAPCFUNC`。回调函数的定义如下: ```c VOID CALLBACK APCFunc(ULONG_PTR dwParam); ``` 在 APC 被调度时,目标线程会执行该回调函数。需要注意的是,该函数必须遵循特定的调用约定,且不能抛出异常或执行可能导致线程状态不一致的操作。此外,回调函数的实现必须是线程安全的,以避免并发访问导致的数据竞争或资源冲突[^1]。 2. **`hThread`** 该参数是目标线程的句柄,表示 APC 将被排队到哪个线程上执行。目标线程必须具有适当的访问权限,并且必须处于可等待状态(例如调用 `SleepEx`、`WaitForSingleObjectEx` 等函数时),才能处理 APC 请求。如果目标线程不在等待状态,APC 可能不会立即执行,甚至可能永远不会执行[^1]。 3. **`dwData`** 该参数是一个 `ULONG_PTR` 类型的值,表示传递给 APC 回调函数的参数。它通常用于向回调函数传递上下文信息或数据指针。由于 APC 在目标线程的上下文中执行,因此需要确保该数据在目标线程的地址空间中是有效的,并且访问该数据时不会引发异常或竞争条件[^1]。 #### 返回值 `QueueUserAPC` 返回一个 `DWORD` 类型的值,表示函数调用的结果。如果返回值为非零值,则表示 APC 成功排队;如果返回值为零,则表示失败。可以通过调用 `GetLastError` 获取具体的错误信息。 #### 使用示例 以下是一个使用 `QueueUserAPC` 的典型示例: ```c #include <windows.h> #include <stdio.h> VOID CALLBACK APCFunc(ULONG_PTR dwParam) { printf("APCFunc called with dwParam = %ld\n", dwParam); } int main() { HANDLE hThread = GetCurrentThread(); // 获取当前线程句柄 DWORD result = QueueUserAPC(APCFunc, hThread, 100); // 排队 APC if (result == 0) { printf("QueueUserAPC failed with error %lu\n", GetLastError()); } SleepEx(1000, TRUE); // 使线程进入可等待状态以执行 APC return 0; } ``` 在上述代码中,`SleepEx` 的调用确保当前线程进入可等待状态,从而允许 APC 被调度执行。 #### 注意事项 - APC 的执行依赖于目标线程的状态。如果目标线程未处于可等待状态,APC 可能不会被及时处理。 - APC 回调函数不能调用某些可能导致线程阻塞的 Windows API 函数,否则可能影响线程的正常行为。 - 如果 APC 操作涉及多个线程,需要确保数据同步和线程安全,以避免潜在的并发问题。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值