探索QueueUserApc(1)

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

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函数执行即可

 

posted @ 2018-06-14 14:16  _Flame  阅读(944)  评论(0编辑  收藏  举报