探索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的函数)
// 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函数执行即可