「进程注入」通过远程线程注入代码插图

概述

上一篇文章文章学习了通过CreateRemoteThread创建远程线程后在目标进程中通过LoadLibrary加载我们自己的DLL来将DLL注入目标进程。在学习的过程中了解到还有一种通过CreateRemoteThread创建远程线程来注入的操作:代码注入。

不同于DLL注入,代码注入的体积量更小也更加隐蔽,注入的代码隐藏在进程的内存中,不容易被发现,而DLL注入后很容易就会被发现注入的痕迹(比如前面的文章中完成注入操作后使用ProcessExplorer查看进程加载DLL)

下面通过一个例子来实际操作完成一次代码注入。

CodeInjection.cpp:

首先给出总的源代码:

#include"windows.h"
#include"tchar.h"
#include"stdio.h"
#include"psapi.h"
#include"stdlib.h"

#define ProcessName1 L"notepad.exe"
#define ProcessName2 L"Notepad.exe"
#define THREAD_SIZE 0x4000
#define LENGTH 30

HANDLE hProcess = NULL;

typedef struct _PARAMENT {
    FARPROC pLoadLibrary;
    FARPROC pGetProcessAddress;

    char DllName1[LENGTH];
    char FuncName[LENGTH];

    char Content[LENGTH];
    char title[LENGTH];
}RemoteParament, *pRemoteParament;

BOOL EnableDebugPriv()
{
    HANDLE hToken;
    LUID Luid; 
    TOKEN_PRIVILEGES tkp;

    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) 
    {
        printf("提权失败\n");
        return FALSE;
    }

    if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &Luid))
    {
        CloseHandle(hToken);
        printf("提权失败\n");
        return FALSE;
    }
    tkp.PrivilegeCount = 1;
    tkp.Privileges[0].Luid = Luid;
    tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; 
    if (!AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof tkp, NULL, NULL))
    {
        printf("提权失败\n");
        CloseHandle(hToken);
    }
    else
    {
        printf("提权成功!\n");
        return TRUE;
    }

}


DWORD WINAPI ThreadProc(LPVOID ThreadPara) //线程注入函数
{
    pRemoteParament para = (pRemoteParament)ThreadPara;

    HMODULE(WINAPI * fpLoadLibrary)(LPCSTR);
    FARPROC(WINAPI * fpGetProcAddress)(HMODULE, LPCSTR);
    int(WINAPI * fpMessageBox) (HWND, LPCSTR, LPCSTR, UINT);

    fpLoadLibrary = (HMODULE(WINAPI *)(LPCSTR))para->pLoadLibrary;
    fpGetProcAddress = (FARPROC(WINAPI*)(HMODULE, LPCSTR))para->pGetProcessAddress;

    HMODULE hMod = fpLoadLibrary(para->DllName1);
    fpMessageBox = (int(WINAPI*)(HWND, LPCSTR, LPCSTR, UINT))fpGetProcAddress(hMod, para->FuncName);

    fpMessageBox(NULL, para->Content, para->title, MB_OK);

    return 0;
}

BOOL Inject(DWORD dwPID)
{
    if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID))) 
    {
        printf("open process failed!\n");
        return FALSE;
    }

    RemoteParament para = { 0 };

    HMODULE hMod = LoadLibrary(L"kernel32.dll"); 

    para.pLoadLibrary = (FARPROC)GetProcAddress(hMod, "LoadLibraryA"); 
    para.pGetProcessAddress = (FARPROC)GetProcAddress(hMod, "GetProcAddress");

    strcpy_s(para.DllName1, "user32.dll");
    strcpy_s(para.FuncName, "MessageBoxA");
    strcpy_s(para.Content, "code inject!");
    strcpy_s(para.title, "inject");

    LPVOID vPara = VirtualAllocEx(hProcess, NULL, sizeof(para), MEM_COMMIT, PAGE_READWRITE);
    if (vPara == NULL)
    {
        printf("para's virtual memory alloc failed!\n");
        return -1;
    }

    if (!WriteProcessMemory(hProcess, vPara, (LPVOID)&para, sizeof(para), NULL))
    {
        //DWORD dwerror = GetLastError();
        printf("para's virtual memory write failed!\n");
        return -1;
    }

    DWORD dwSize = (DWORD)Inject - (DWORD)ThreadProc;
    /*LPVOID pRemoteThreadProc = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);*/
    LPVOID pRemoteThreadProc = VirtualAllocEx(hProcess, NULL, THREAD_SIZE, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (pRemoteThreadProc == NULL)
    {
        printf("threadproc's virtual memory alloc failed!\n");
        return -1;
    }

    /*if (!WriteProcessMemory(hProcess, pRemoteThreadProc, (LPVOID)&ThreadProc, dwSize, NULL))*/
    if (!WriteProcessMemory(hProcess, pRemoteThreadProc, (LPVOID)&ThreadProc, THREAD_SIZE, NULL))
    {
        printf("threadproc's virtual memory write failed!\n");
        return -1;
    }

    HANDLE hThread = NULL;
    hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteThreadProc, vPara, 0, NULL);

    if (hThread) {
        printf("non dll inject success.\n");
    }
    else {
        printf("inject failed!\n");
        return FALSE;
    }

    WaitForSingleObject(hThread, INFINITE); 

    CloseHandle(hThread);
    CloseHandle(hProcess);

    return TRUE;
}

int _tmain(int argc,_TCHAR *argv[])
{
    DWORD dwError = 0;
    DWORD dwPID = (DWORD)_tstol(argv[1]);

    EnableDebugPriv();

    Inject(dwPID);

    return 0;
}

程序主要流程大致如下:

  1. 将注入代码的所需要的参数集成为一个结构体
  2. 设置提权函数进行权限提升
  3. 进行注入操作的主要代码(包括注入代码所需参数常量等的写入、线程操作函数的写入及远程线程的创建)
  4. 主函数调用各个函数完成注入操作

struct _PARAMENT:

这个部分是后面注入的代码需要用到的参数。这个部分也是DLL注入与代码注入一个比较不同的地方,DLL注入中,进行注入操作的主程序进行的分配操作是为LoadLibrary这个API的参数:DLL文件路径分配内存空间,而DLL中进行的各个函数操作所需要的参数是不需要单独为其分配内存,因为DLL的代码与数据在内存的角度上是与目标进程共享的;而代码注入后为了使被注入的代码能够正常运行,就需要将其对应所需的参数数据等写入目标进程的内存空间。

这次注入操作是为了完成在目标进程中使其出现一个弹窗,需要用到user32.dll这个库中MessageBoxA这个API函数,所以可以大概理出这次代码注入操作所需要的参数:

  1. LoadLibrary及GetProcessAddress这两个函数的地址(这两个函数是注入操作中基本都会使用到的函数)
  2. user32.dll这个库的名称字符串以及MessageBoxA这个函数的名称字符串(注入的代码中要将user32.dll这个库加载入目标进程)
  3. MessageBoxA这个函数所需要参数(也就是弹出窗口的内容以及窗口的标题)

这一部分的代码及注释为:

typedef struct _PARAMENT {
    //下面的FARPROC为指向在后面需要调用的一些函数的内存地址
    FARPROC pLoadLibrary; //用于存储pLoadLibrary的函数地址
    FARPROC pGetProcessAddress; //用存储GetProcessAddress的函数地址

    char DllName1[LENGTH]; //后面可能要通过使用GetModuleHandle和DLL名字获取模块地址,也就是存储user32.dll这个模块名称
    char FuncName[LENGTH]; //存储需要查找的MessageBox的函数名称

    char Content[LENGTH]; //注入代码后调用MessageBox输出的内容
    char title[LENGTH]; //输出的标题
}RemoteParament, *pRemoteParament;

再提一下这个数据结构:FARPROC

这是一个4字节长的指针类类型,它在minwindef.h中被解释为一个FAR WINAPI类型的函数指针,如下:

typedef INT_PTR (FAR WINAPI *FARPROC)();

当获取到LoadLibrary及GetProcessAddress这两个函数的实际地址时,由于都是WINAPI函数,所以要将其解释为这样一个FARPROC类型的指针。

EnableDebugPriv:

这个部分为提权函数,其中具体的数据类型解释以及操作流程可以看一下我的上一篇文章,这里只给出大概的函数流程:

  1. 获取当前与当前进程相关联的用户的访问令牌
  2. 查找所需要修改权限的LUID
  3. 对应访问令牌中的特权属性进行修改
  4. 调整特权

这一部分的代码即注释如下:

BOOL EnableDebugPriv() //提权函数
{
    HANDLE hToken; //指向后面打开访问令牌的句柄
    LUID Luid; //接受后面查找的局部唯一标识符
    TOKEN_PRIVILEGES tkp; //后面修改访问令牌时用到的结构体

    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) //打开当前进程的访问令牌
    {
        printf("提权失败。");
        return FALSE;
    }

    if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &Luid)) //查找需要修改的权限对应的LUID值
    {
        CloseHandle(hToken);
        printf("提权失败。");
        return FALSE;
    }
    //下面为访问令牌的中特权属性的修改操作
    tkp.PrivilegeCount = 1; //设置要修改的权限数量,这里只需要修改一项权限,即为1
    tkp.Privileges[0].Luid = Luid; //设置Privileges数组中LUID的值为前面查找到的对应权限的LUID值
    tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; //设置该权限对应的执行状态更改为可行状态
    if (!AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof tkp, NULL, NULL)) //修改访问令牌的对应权限
    {
        printf("提权失败。");
        CloseHandle(hToken);
    }
    else
    {
        printf("提权成功!");
        return TRUE;
    }

}

ThreadProc

这个函数就是后面远程线程创建后需要注入目标进程执行的代码了,它的函数流程大致如下:

  1. 将线程函数的参数强制转化为前面定义的RemoteParament类型
  2. 对需要使用到的API函数进行函数指针的重新声明
  3. 对前面声明的函数指针进行赋值,也就是将其指向传入参数中每个函数所自带的真实地址
  4. 调用MessageBoxA进行弹窗操作

首先要注意的一点是对所需要使用到的API函数指针进行声明,这一步操作的目的是为了在目标进程中能够正常的调用这些API函数,上一篇文章中其实有所提及:在大多数进程中,kernel32.dll这个库以及其中函数的位置是基本保持不变的,我们可以直接在本程序中查找到所需要使用的API函数的真实地址(即本次注入需要使用到的LoadLibrary以及GetProcAddress),而我们目的是要调用MessageBoxA这个user32.dll中的API,所以在注入代码中就需要进行如下操作:

  1. LoadLibrary加载user32.dll
  2. GetProcAddress查找MessageBoxA函数的地址(注意此时装载user32.dll的操作就是在目标进程中进行了,所以其函数地址可以是在目标进程中使用

而我们查找到的LoadLibrary、GetProcAddress及MessageBoxA的地址被作为参数传入线程函数后只是一串单纯的数值而已,所以我们需要将其对应函数功能进行声明,例如GetProcAddress函数的声明:

FARPROC(WINAPI * fpGetProcAddress)(HMODULE, LPCSTR);

这个声明的各个部分意义如下:

  • FARPROC:函数的返回值
  • WINAPI *:函数的类型,也就是一个WINAPI类型的函数
  • (HMODULE, LPCSTR):函数的参数列表

这一部分的代码即注释如下:

DWORD WINAPI ThreadProc(LPVOID ThreadPara) //线程注入函数
{
    pRemoteParament para = (pRemoteParament)ThreadPara;

    //由于user32.dll在每个进程中的内存地址是不一样的,所以需要将后面要是用到的user32.dll中的API函数的地址替换为被注入进程中的地址

    //下面为函数指针声明
    HMODULE(WINAPI * fpLoadLibrary)(LPCSTR);
    FARPROC(WINAPI * fpGetProcAddress)(HMODULE, LPCSTR); //FARPROC为一个4字节指针,一般用于指向一个函数的内存地址
    int(WINAPI * fpMessageBox) (HWND, LPCSTR, LPCSTR, UINT);

    //给前面声明的函数指针赋值,使其指向对应函数的真实地址,要注意赋值前要对para中数据的类型进行强转
    fpLoadLibrary = (HMODULE(WINAPI *)(LPCSTR))para->pLoadLibrary;
    fpGetProcAddress = (FARPROC(WINAPI*)(HMODULE, LPCSTR))para->pGetProcessAddress;

    HMODULE hMod = fpLoadLibrary(para->DllName1); //在目标进程中加载user32.dll

    fpMessageBox = (int(WINAPI*)(HWND, LPCSTR, LPCSTR, UINT))fpGetProcAddress(hMod, para->FuncName); //查找到MessageBoxA函数的地址

    fpMessageBox(NULL, para->Content, para->title, MB_OK); //执行MessageBoxA函数

    return 0;
}

Inject

这个函数即执行注入操作的函数,它的函数流程大概如下:

  1. 打开目标进程的进程句柄
  2. 创建参数结构体,并对其各个成员进行赋值
  3. 分别为参数结构体以及线程操作函数分配虚拟空间并在内存中写入数据
  4. 创建远程线程将代码注入目标进程

函数的逻辑比较简单,与前面的DLL注入流程差不多,比较重要的地方就是关于内存申请这个问题。

相较于DLL注入,代码注入有两次内存申请以及对应的数据写入,分别为:

  • 参数结构体内存空间的分配与写入
  • ThreadProc线程函数内存空间的分配与写入

为什么会有两次分配与写入的原因前面部分中有提到过,简单来说就是要将注入代码所需要的数据与可执行的代码一同放入目标进程的内存空间中。

这里主要要细说的是关于ThreadProc函数所需的内存空间大小问题。我在为ThreadProc分配内存时,第一次是直接给出了一个固定大小,也就是程序中宏定义的THREAD_SIZE,其大小为0x4000,这个大小分配对于很多有保护机制的程序来说是很异常的,虽然本次我们目标进程为notepad,基本没有保护措施;但是这个问题很值得思考,在为ThreadProc函数分配内存空间时应该怎么操作?

后来根据网上师傅的文章找到了一个解决办法:

DWORD dwSize = (DWORD)Inject - (DWORD)ThreadProc;
LPVOID pRemoteThreadProc = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE)

这里的大小计算是通过将两个函数的起始地址相减得到的(在这个程序中Inject,ThreadProc这两个函数相邻,将其地址强转为DWORD后相减即可的得到ThreadProc的实际大小)

这个办法在使用时要注意编译器编译模式的选择:一定要使用Release方式编译程序,因为在Debug下编译的程序中,函数结构没有进行优化且程序中存在调试信息,这样直接相减的话就会出错

这一部分的代码及注释如下:

BOOL Inject(DWORD dwPID)
{   
    if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID))) //打开被注入线程的进程句柄
    {
        printf("open process failed!\n");
        return FALSE;
    }

    RemoteParament para = { 0 };

    HMODULE hMod = LoadLibrary(L"kernel32.dll"); //kernel32.dll在每个进程中的位置是一样的,所以可以直接使用里面的函数地址
    //下面为结构体中的函数地址参数赋值
    para.pLoadLibrary = (FARPROC)GetProcAddress(hMod, "LoadLibraryA"); //查找LoadLibraryA函数的地址,传入para注意类型转换
    para.pGetProcessAddress = (FARPROC)GetProcAddress(hMod, "GetProcAddress");

    strcpy_s(para.DllName1, "user32.dll");
    strcpy_s(para.FuncName, "MessageBoxA");
    strcpy_s(para.Content, "code inject!");
    strcpy_s(para.title, "inject");

    //为注入线程的参数等分配进程空间

    //下面为para分配进程的虚拟空间,注入代码的函数参数依托于我们写入的数据
    LPVOID vPara = VirtualAllocEx(hProcess, NULL, sizeof(para), MEM_COMMIT, PAGE_READWRITE);
    if (vPara == NULL)
    {
        printf("para's virtual memory alloc failed!\n");
        return -1;
    }
    //将para数据写入目标进程
    if (!WriteProcessMemory(hProcess, vPara, (LPVOID)&para, sizeof(para), NULL))
    {
        //DWORD dwerror = GetLastError();
        printf("para's virtual memory write failed!\n");
        return -1;
    }

    //下面将线程操作函数写入目标进程
    DWORD dwSize = (DWORD)Inject - (DWORD)ThreadProc;
    //下面这种定义dwSize的方式是通过函数的起始地址相减得到区段大小,也是为什么要使用release版本的原因,因为debug版本的程序在内存中会有调试信息存在
    //可能会对相减出来的结果造成影响
    /*LPVOID pRemoteThreadProc = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);*/
    LPVOID pRemoteThreadProc = VirtualAllocEx(hProcess, NULL, THREAD_SIZE, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    //注意这里分配虚拟内存的时候由于是给后面要执行的线程函数分配内存,所以在属性的选择上要有EXECUTE(可执行)的属性
    if (pRemoteThreadProc == NULL)
    {
        printf("threadproc's virtual memory alloc failed!\n");
        return -1;
    }

    /*if (!WriteProcessMemory(hProcess, pRemoteThreadProc, (LPVOID)&ThreadProc, dwSize, NULL))*/
    if (!WriteProcessMemory(hProcess, pRemoteThreadProc, (LPVOID)&ThreadProc, THREAD_SIZE, NULL))
    {
        printf("threadproc's virtual memory write failed!\n");
        return -1;
    }

    HANDLE hThread = NULL;
    hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteThreadProc, vPara, 0, NULL); //创建远程线程,在目标进程中注入代码

    if (hThread) {
        printf("non dll inject success.\n");
    }
    else {
        printf("inject failed!\n");
        return FALSE;
    }

    WaitForSingleObject(hThread, INFINITE); //等待远程线程执行

    CloseHandle(hThread);
    CloseHandle(hProcess);

    return TRUE;
}

调试运行

首先来运行一下这个程序,这里是在Win10环境下运行的,注意如果要在64位环境下运行的话,程序在编译时就要选择64位的编译生成,不然可能会造成目标进程崩溃。

首先打开记事本(notepad),然后找到其PID:例如 6314

然后在命令行运行注入程序;

运行之后我们在x64dbg中运行notepad后,并设置在新线程开启时中断程序(因为这里我采用的编译生成的是64位程序,使用OD可能会出现调试失败的情况):

然后运行注入程序,就会在线程处中断,此处即为我们注入的代码,步进执行程序就会跳转到对应的API函数处执行