0x01深入内核
我们直接从ReactOS中了解其中的具体实现,比如NtQueueApcThreadEx,其中主要使用了一个KAPC对象:
typedef struct _KAPC {
UCHAR Type;
UCHAR SpareByte0;
UCHAR Size;
UCHAR SpareByte1;
ULONG SpareLong0;
struct _KTHREAD *Thread;
LIST_ENTRY ApcListEntry;
#ifdef _NTSYSTEM_
PKKERNEL_ROUTINE KernelRoutine;
PKRUNDOWN_ROUTINE RundownRoutine;
PKNORMAL_ROUTINE NormalRoutine;
#else
PVOID Reserved[3];
#endif
PVOID NormalContext;
PVOID SystemArgument1;
PVOID SystemArgument2;
CCHAR ApcStateIndex;
KPROCESSOR_MODE ApcMode;
BOOLEAN Inserted;
} KAPC, *PKAPC, *RESTRICTED_POINTER PRKAPC;
从内核模式对用户 APC 进行排队非常简单。每个 APC 对象都由一个 KAPC 对象表示。KAPC 对象主要有 3 个重要功能:
- NormalRoutine:这是在交付 APC 时应在用户模式下执行的函数。(ApcRoutine)
- KernelRoutine : 这是一个在 APC 被交付之前在内核模式下的 APC_LEVEL 中执行的函数。
- RundownRoutine:这是一个函数,如果线程在 APC 被传递到用户模式之前终止,它应该释放 APC 对象。
对于用户态 APC,我们需要在 KeInitializeApc 的 ApcMode 参数中指定“UserMode”,其中 SystemArgument1 被传递给 KeInitializeApc,而 SystemArgument2 和 SystemArgument3 被传递给 KeInsertQueueApc。
KeInsertQueueApc 将 APC 插入到目标线程的队列中,如果线程处于alertable等待状态,它还可以“取消等待状态”线程并确保当它返回到用户模式时 APC 将执行。这两个函数(KiInitializeApc 和 KeInsertQueueApc)都由 NTOSKRNL 导出,例如,NSA开发的DoublePulsar内核模式payload使用KeInsertQueueApc在用户模式下执行payload:
那我们就来看看KeInsertQueueApc 实现:
BOOLEAN
NTAPI
KeInsertQueueApc(IN PKAPC Apc,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2,
IN KPRIORITY PriorityBoost)
{
PKTHREAD Thread = Apc->Thread;
KLOCK_QUEUE_HANDLE ApcLock;
BOOLEAN State = TRUE;
ASSERT_APC(Apc);
ASSERT_IRQL_LESS_OR_EQUAL(DISPATCH_LEVEL);
/* Get the APC lock */
KiAcquireApcLockRaiseToSynch(Thread, &ApcLock);
/* Make sure we can Queue APCs and that this one isn't already inserted */
if (!(Thread->ApcQueueable) || (Apc->Inserted))
{
/* Fail */
State = FALSE;
}
else
{
/* Set the System Arguments and set it as inserted */
Apc->SystemArgument1 = SystemArgument1;
Apc->SystemArgument2 = SystemArgument2;
Apc->Inserted = TRUE;
/* Call the Internal Function */
KiInsertQueueApc(Apc, PriorityBoost);
}
/* Release the APC lock and return success */
KiReleaseApcLockFromSynchLevel(&ApcLock);
KiExitDispatcher(ApcLock.OldIrql);
return State;
}
先获取锁,将 APC 插入 APC 队列。APC队列实际上是一个链表,保存在KTHREAD对象的ApcState成员中。
0x02 KiSignalThreadForApc
KeInsertQueueApc 将 APC 插入目标队列后,KiSignalThreadForApc 运行。该函数的目的是根据 APC 的类型检查是否应该向目标线程发出信号以及如何发出信号。主要检查以下三个点:
- 线程正在等待。
- 线程是否挂起——当一个线程被挂起时,它处于waiting状态——内核不让挂起的线程等待执行 APC。
- 线程是alertable——当线程使用 Alertable = TRUE 调用 KeWaitForSingleObject 时,“Alertable”成员变为 TRUE。
0x03 KiDeliverApc
KiDeliverApc 处理所有类型的 APC:
- 获取 APC 队列锁。
- 检查 APC 队列是否为空。如果不是,则从队列中弹出第一个用户 APC
- 调用内核例程。KernelRoutine 是在 APC 传递到用户模式之前运行在 APC_LEVEL 的代码。最常见的是,此代码释放 APC (ExFreePool)。APC 的值保存在局部变量中,KernelRoutine 可以根据需要更改这些值。
- 初始化 TRAP_FRAME 以返回 APC 代码而不是现有代码。
这里我使用起进程注入dll的方式,实际演示:
RemoteLibAddress = WriteLibraryNameToRemote(ProcessInformation.hProcess, L"calc.dll");
Status = NtQueueApcThread(
ProcessInformation.hThread,
(PPS_APC_ROUTINE)LdrLoadDll,
NULL, // PathToFile
0, // Flags
RemoteLibAddress // ModuleFileName
);
0x04 总结
用户 APC 用于在 Windows 中实现异步回调,用户 APC 使用 KeInsertQueueApc 在内核模式下排队,用户 APC 被保存为每个线程队列中的 KAPC 对象,用户 APC 最常在线程返回用户模式且 UserApcPending = TRUE 之前执行,当使用 Alertable = TRUE 调用 KeWaitForSingleObject 或调用 NtTestAlert 时,UserApcPending 为 TRUE。所以利用此方式在实战攻防中绕过杀软还是可行的。
发表评论
您还未登录,请先登录。
登录