detours 通过修改输入表注入DLL -- UpdateImports 函数的分析

来源:互联网 发布:李洪成四柱视频知乎 编辑:程序博客网 时间:2024/06/05 14:44
 

前段时间有一个需求,就是在进程启动的第一时间实现dll的注入,当时一想以为很简单嘛,比如拦截CreateProcessInternalW等不就行了吗?后来发现如果你在CreateProcessInternalW前处理进程还没有启动,而之后处理的话,进程的主线程已经开始工作了,再后来通过修改创建标志把创建的进程的主线程挂起,等进程创建后我来注入,此时才发现创建远线程搞不定, 此时进程只有NTDLL,其他模块还没有加载呢.再最后终于发现了detours 2.1为我们提供了一个函数DetourCreateProcessWithDllW,它可以实现在进程创建初期就将dll加载进去,下面我就来分析一下detours怎么做到在进程初期就将dll加载进程的.

先来看DetourCreateProcessWithDllW函数,可以分为四个步骤:

BOOL 
WINAPI 
DetourCreateProcessWithDllW(LPCWSTR lpApplicationName,
                            __in_z LPWSTR lpCommandLine,
                            LPSECURITY_ATTRIBUTES lpProcessAttributes,
                            LPSECURITY_ATTRIBUTES lpThreadAttributes,
                            BOOL bInheritHandles,
                            DWORD dwCreationFlags,
                            LPVOID lpEnvironment,
                            LPCWSTR lpCurrentDirectory,
                            LPSTARTUPINFOW lpStartupInfo,
                            LPPROCESS_INFORMATION lpProcessInformation,
                            LPCSTR lpDetouredDllFullName,
                            LPCSTR lpDllName,
                            PDETOUR_CREATE_PROCESS_ROUTINEW pfCreateProcessW)
{
    
// 第一步:修改创建标志位
    DWORD dwMyCreationFlags = (dwCreationFlags | CREATE_SUSPENDED);
    PROCESS_INFORMATION pi;

    
if (pfCreateProcessW == NULL) {
        pfCreateProcessW 
= CreateProcessW;
    }


    
// 第二步:调用原始CreateProcessW
    if (!pfCreateProcessW(lpApplicationName,
                          lpCommandLine,
                          lpProcessAttributes,
                          lpThreadAttributes,
                          bInheritHandles,
                          dwMyCreationFlags,
                          lpEnvironment,
                          lpCurrentDirectory,
                          lpStartupInfo,
                          
&pi)) {
        
return FALSE;
    }


    LPCSTR rlpDlls[
2];
    DWORD nDlls 
= 0;
    
if (lpDetouredDllFullName != NULL) {
        rlpDlls[nDlls
++= lpDetouredDllFullName;
    }

    
if (lpDllName != NULL) {
        rlpDlls[nDlls
++= lpDllName;
    }


    
// 第三步: 修改目标进程的输入表
    if (!UpdateImports(pi.hProcess, rlpDlls, nDlls)) {
        
return FALSE;
    }


    
if (lpProcessInformation) {
        CopyMemory(lpProcessInformation, 
&pi, sizeof(pi));
    }


    
// 第四步: 让目标进程的主线程继续运行
    if (!(dwCreationFlags & CREATE_SUSPENDED)) {
        ResumeThread(pi.hThread);
    }

    
return TRUE;
}

呵呵,看出来了,原来detours使用的是修改输入表来实现dll的注入的,在来看其中最关键的函数UpdateImports,

BOOL WINAPI UpdateImports(HANDLE hProcess, LPCSTR *plpDlls, DWORD nDlls)
{
    BOOL fSucceeded 
= FALSE;
    BYTE 
* pbNew = NULL;
    DETOUR_EXE_RESTORE der;
    DWORD i;

    ZeroMemory(
&der, sizeof(der));
    der.cb 
= sizeof(der);

    
// 从 0x10000 开始寻找MEM_IMGAE标志的内存,然后验证其是否是exe
    
// 如果是那么返回其首地址
    PBYTE pbModule = (PBYTE)FindExe(hProcess);

    IMAGE_DOS_HEADER idh;
    ZeroMemory(
&idh, sizeof(idh));

    
// 读取 IMAGE_DOS_HEADER
    if (!ReadProcessMemory(hProcess, pbModule, &idh, sizeof(idh), NULL)) {
        DETOUR_TRACE((
"ReadProcessMemory(idh) failed: %d ", GetLastError()));

      finish:
        
if (pbNew != NULL) {
            delete[] pbNew;
            pbNew 
= NULL;
        }

        
return fSucceeded;
    }

    CopyMemory(
&der.idh, &idh, sizeof(idh));
    der.pidh 
= (PIMAGE_DOS_HEADER)pbModule;

    
// 验证是否是 MZ
    if (idh.e_magic != IMAGE_DOS_SIGNATURE) {
        
goto finish;
    }


    IMAGE_NT_HEADERS inh;
    ZeroMemory(
&inh, sizeof(inh));

    
// 读取 IMAGE_NT_HEADERS
    if (!ReadProcessMemory(hProcess, pbModule + idh.e_lfanew, &inh, sizeof(inh), NULL)) {
        DETOUR_TRACE((
"ReadProcessMemory(inh) failed: %d ", GetLastError()));
        
goto finish;
    }

    CopyMemory(
&der.inh, &inh, sizeof(inh));
    der.pinh 
= (PIMAGE_NT_HEADERS)(pbModule + idh.e_lfanew);

    
// 验证是否是PE, 和验证MZ一样,没有多大必要,其实在FindExe里面都验证过了
    if (inh.Signature != IMAGE_NT_SIGNATURE) {
        
goto finish;
    }


    
// 如果没有导入表,直接返回
    if (inh.IMPORT_DIRECTORY.VirtualAddress == 0{
        DETOUR_TRACE((
"No IMAGE_DIRECTORY_ENTRY_IMPORT "));
        
goto finish;
    }


    
// Zero out the bound table so loader doesn't use it instead of our new table.
    
// 不懂为什么要这样
    inh.BOUND_DIRECTORY.VirtualAddress = 0;
    inh.BOUND_DIRECTORY.Size 
= 0;

    
// Find the size of the mapped file.
    
    DWORD dwFileSize 
= 0;

    
// FIELD_OFFSET 这个宏很精妙
    
// 得到 section 的偏移
    DWORD dwSec = idh.e_lfanew +
        FIELD_OFFSET( IMAGE_NT_HEADERS, OptionalHeader ) 
+
        inh.FileHeader.SizeOfOptionalHeader;

    
// 循环读取 节,主要作用就是计算文件的大小,然而文件大小本身又用不到,所以这个东西就是鸡肋
    for (i = 0; i < inh.FileHeader.NumberOfSections; i++{
        IMAGE_SECTION_HEADER ish;
        ZeroMemory(
&ish, sizeof(ish));

        
if (!ReadProcessMemory(hProcess, pbModule + dwSec + sizeof(ish) * i, &ish,
                               
sizeof(ish), NULL)) {
            DETOUR_TRACE((
"ReadProcessMemory(inh) failed: %d ", GetLastError()));
            
goto finish;
        }


        DETOUR_TRACE((
"ish[%d] : va=%p sr=%d ", i, ish.VirtualAddress, ish.SizeOfRawData));

        
// If the file didn't have an IAT_DIRECTORY, we create one...
        
// 不能理解其深意
        if (inh.IAT_DIRECTORY.VirtualAddress == 0 &&
            inh.IMPORT_DIRECTORY.VirtualAddress 
>= ish.VirtualAddress &&
            inh.IMPORT_DIRECTORY.VirtualAddress 
< ish.VirtualAddress + ish.SizeOfRawData) {

            inh.IAT_DIRECTORY.VirtualAddress 
= ish.VirtualAddress;
            inh.IAT_DIRECTORY.Size 
= ish.SizeOfRawData;
        }


        
// Find the end of the file...
        if (dwFileSize < ish.PointerToRawData + ish.SizeOfRawData) {
            dwFileSize 
= ish.PointerToRawData + ish.SizeOfRawData;
        }

    }

    DETOUR_TRACE((
"dwFileSize = %08x ", dwFileSize));

    
// Find the current checksum.
    WORD wBefore = ComputeChkSum(hProcess, pbModule, &inh);
    DETOUR_TRACE((
"ChkSum: %04x + %08x => %08x ", wBefore, dwFileSize, wBefore + dwFileSize));

    DETOUR_TRACE((
"     Imports: %8p..%8p ",
                  (DWORD_PTR)pbModule 
+ inh.IMPORT_DIRECTORY.VirtualAddress,
                  (DWORD_PTR)pbModule 
+ inh.IMPORT_DIRECTORY.VirtualAddress +
                  inh.IMPORT_DIRECTORY.Size));

    DWORD obRem 
= sizeof(IMAGE_IMPORT_DESCRIPTOR) * nDlls;
    
// 园整
    DWORD obTab = PadToDwordPtr(obRem + inh.IMPORT_DIRECTORY.Size);
    
// 用来存储 OriginalFirstThunk 和 FirstThunk指向的数组
    
// 每个数组包括一个有用的和一个结尾的,加起来就是4个DWORD_PTR
    
// 当然还要乘以 nDlls
    DWORD obDll = obTab + sizeof(DWORD_PTR) * 4 * nDlls;
    DWORD obStr 
= obDll;
    DWORD cbNew 
= obStr;
    DWORD n;
    
for (n = 0; n < nDlls; n++{
        cbNew 
+= PadToDword((DWORD)strlen(plpDlls[n]) + 1);
    }


    
// 现在本地分配一个缓冲区,填充好数据后再写到远程进程去
    pbNew = new BYTE [cbNew];
    
if (pbNew == NULL) {
        DETOUR_TRACE((
"new BYTE [cbNew] failed. "));
        
goto finish;
    }

    ZeroMemory(pbNew, cbNew);

    PBYTE pbBase 
= pbModule;
    PBYTE pbNext 
= pbBase
        
+ inh.OptionalHeader.BaseOfCode
        
+ inh.OptionalHeader.SizeOfCode
        
+ inh.OptionalHeader.SizeOfInitializedData
        
+ inh.OptionalHeader.SizeOfUninitializedData;
    
if (pbBase < pbNext) {
        pbBase 
= pbNext;
    }

    DETOUR_TRACE((
"pbBase = %p ", pbBase));

    
// 在远进程中分配一个和本地一样的大的 IMPORT 表,比便以后写进去
    PBYTE pbNewIid = FindAndAllocateNearBase(hProcess, pbBase, cbNew);
    
if (pbNewIid == NULL) {
        DETOUR_TRACE((
"FindAndAllocateNearBase failed. "));
        
goto finish;
    }


    
    DWORD dwProtect 
= 0;
    der.impDirProt 
= 0;
    
// 修改远程进程的输入表的属性,修改为PAGE_EXECUTE_READWRITE,其实没有必要,不要执行就可以了
    
// 反正以后要改回来的
    if (!VirtualProtectEx(hProcess,
                          pbModule 
+ inh.IMPORT_DIRECTORY.VirtualAddress,
                          inh.IMPORT_DIRECTORY.Size, PAGE_EXECUTE_READWRITE, 
&dwProtect)) {
        DETOUR_TRACE((
"VirtualProtextEx(import) write failed: %d ", GetLastError()));
        
goto finish;
    }

    DETOUR_TRACE((
"IMPORT_DIRECTORY perms=%x ", dwProtect));
    der.impDirProt 
= dwProtect;

    
// pbNewIid 是我们在远程线程分配的新的输入表的首地址,和pbModule想减后就得到它的RVA了
    DWORD obBase = (DWORD)(pbNewIid - pbModule);

    
// 将旧的输入表读入到本地,注意有一个偏移obRem
    if (!ReadProcessMemory(hProcess,
                           pbModule 
+ inh.IMPORT_DIRECTORY.VirtualAddress,
                           pbNew 
+ obRem,
                           inh.IMPORT_DIRECTORY.Size, NULL)) 
{
        DETOUR_TRACE((
"ReadProcessMemory(imports) failed: %d ", GetLastError()));
        
goto finish;
    }


    PIMAGE_IMPORT_DESCRIPTOR piid 
= (PIMAGE_IMPORT_DESCRIPTOR)pbNew;
    DWORD_PTR 
*pt;

    
// 这里是最关键的地方了,修改出一个新的输入表,当下面这个循环执行完成之后就在本地形成了
    
// 一个新的输入表了,下面这个图就是循环完成之后的内存布局图,
    
// 我们以添加两个dll为列
/*
________
________ IID 1   如:A.dll
________ IID 2   如:B.dll
________ IID 3   这后面的IID就是exe自身的, 看看上面一个ReadProcessMemory就了解了
________ IID 4   
________ IID N
________ NULL  IID结尾符号
________ IID 1 的 OriginalFirstThunk指向的数组 ,占用两个DWORD,第一个是序号输入值,第二个为NULL
________ IID 1 的FirstThunk指向的数组 ,占用两个DWORD,第一个是讯号输入值,第二个为NULL
________ IID 2 的OriginalFirstThunk指向的数组 ,占用两个DWORD,第一个是序号输入值,第二个为NULL
________ IID 2 的FirstThunk指向的数组 ,占用两个DWORD,第一个是讯号输入值,第二个为NULL
________ A.dll 的名称字符串 "全路径+A.dll",IID 1的Name字段指向这里
________ B.dll 的名称字符串 "全路径+A.dll",IID 2的Name字段指向这里
*/

    
for (n = 0; n < nDlls; n++{
        
// 填充路径字符串
        HRESULT hrRet = StringCchCopyA((char*)pbNew + obStr, cbNew - obStr, plpDlls[n]);
        
if (FAILED(hrRet))
        
{
            DETOUR_TRACE((
"StringCchCopyA failed: %d ", GetLastError()));
            
goto finish;
        }


        
// 得到OriginalFirstThunk指向的数组的偏移
        DWORD nOffset = obTab + (sizeof(DWORD_PTR) * (4 * n));
        
// 根据偏移+首地址,就得到RVA,然后填充到IID中
        piid[n].OriginalFirstThunk = obBase + nOffset;
        
// 得到本地实际的地址
        pt = ((DWORD_PTR*)(pbNew + nOffset));
        
// 使用序号来导入, 因此注入的DLL必须至少有一个导出函数
        pt[0= IMAGE_ORDINAL_FLAG + 1;
        pt[
1= 0// 数组末尾填充NULL

        
// 偏移两个DWORD,配置FirstThunk,和OriginalFirstThunk类似
        nOffset = obTab + (sizeof(DWORD_PTR) * ((4 * n) + 2));
        piid[n].FirstThunk 
= obBase + nOffset;
        pt 
= ((DWORD_PTR*)(pbNew + nOffset));
        pt[
0= IMAGE_ORDINAL_FLAG + 1;
        pt[
1= 0;
        piid[n].TimeDateStamp 
= 0;
        piid[n].ForwarderChain 
= 0;
        piid[n].Name 
= obBase + obStr;

        obStr 
+= PadToDword((DWORD)strlen(plpDlls[n]) + 1);
    }


    
// 只是打印出结果
    for (i = 0; i < nDlls + (inh.IMPORT_DIRECTORY.Size / sizeof(*piid)); i++{
        DETOUR_TRACE((
"%8d. Look=%08x Time=%08x Fore=%08x Name=%08x Addr=%08x ",
                      i,
                      piid[i].OriginalFirstThunk,
                      piid[i].TimeDateStamp,
                      piid[i].ForwarderChain,
                      piid[i].Name,
                      piid[i].FirstThunk));
        
if (piid[i].OriginalFirstThunk == 0 && piid[i].FirstThunk == 0{
            
break;
        }

    }


    
// 本地配置好了之后写入到远程进程中
    if (!WriteProcessMemory(hProcess, pbNewIid, pbNew, obStr, NULL)) {
        DETOUR_TRACE((
"WriteProcessMemory(iid) failed: %d ", GetLastError()));
        
goto finish;
    }


    DETOUR_TRACE((
"obBase = %p..%p ",
                  inh.IMPORT_DIRECTORY.VirtualAddress,
                  inh.IMPORT_DIRECTORY.VirtualAddress 
+ inh.IMPORT_DIRECTORY.Size));
    DETOUR_TRACE((
"obBase = %p..%p ", obBase, obBase + obStr));

    
// 在本地修改输入表的RVA,之后会写入到远程进程的
    inh.IMPORT_DIRECTORY.VirtualAddress = obBase;
    inh.IMPORT_DIRECTORY.Size 
= cbNew;

    
/////////////////////////////////////////////////// Update the CLR header.
    // 搞不懂为什么要清除 IL_ONLY 标志
    if (inh.CLR_DIRECTORY.VirtualAddress != 0 &&
        inh.CLR_DIRECTORY.Size 
!= 0{

        DETOUR_CLR_HEADER clr;
        PBYTE pbClr 
= pbModule + inh.CLR_DIRECTORY.VirtualAddress;

        
if (!ReadProcessMemory(hProcess, pbClr, &clr, sizeof(clr), NULL)) {
            DETOUR_TRACE((
"ReadProcessMemory(clr) failed: %d ", GetLastError()));
            
goto finish;
        }


        der.pclrFlags 
= (PULONG)(pbClr + offsetof(DETOUR_CLR_HEADER, Flags));
        der.clrFlags 
= clr.Flags;

        clr.Flags 
&= 0xfffffffe;    // Clear the IL_ONLY flag.

        
if (!VirtualProtectEx(hProcess, pbClr, sizeof(clr), PAGE_READWRITE, &dwProtect)) {
            DETOUR_TRACE((
"VirtualProtextEx(clr) write failed: %d ", GetLastError()));
            
goto finish;
        }


        
if (!WriteProcessMemory(hProcess, pbClr, &clr, sizeof(clr), NULL)) {
            DETOUR_TRACE((
"WriteProcessMemory(clr) failed: %d ", GetLastError()));
            
goto finish;
        }


        
if (!VirtualProtectEx(hProcess, pbClr, sizeof(clr), dwProtect, &dwProtect)) {
            DETOUR_TRACE((
"VirtualProtextEx(clr) restore failed: %d ", GetLastError()));
            
goto finish;
        }

    }


    
/////////////////////// Update the NT header for the import new directory.
    
/////////////////////////////// Update the DOS header to fix the checksum.

    // 修改属性
    if (!VirtualProtectEx(hProcess, pbModule, inh.OptionalHeader.SizeOfHeaders,
                          PAGE_EXECUTE_READWRITE, 
&dwProtect)) {
        DETOUR_TRACE((
"VirtualProtextEx(inh) write failed: %d ", GetLastError()));
        
goto finish;
    }


    idh.e_res[
0= 0;
    
// 写入 DOS 头
    if (!WriteProcessMemory(hProcess, pbModule, &idh, sizeof(idh), NULL)) {
        DETOUR_TRACE((
"WriteProcessMemory(idh) failed: %d ", GetLastError()));
        
goto finish;
    }


    
// 写入PE 文件头,IMAGE_NT_HEADER
    if (!WriteProcessMemory(hProcess, pbModule + idh.e_lfanew, &inh, sizeof(inh), NULL)) {
        DETOUR_TRACE((
"WriteProcessMemory(inh) failed: %d ", GetLastError()));
        
goto finish;
    }


    
// 计算效验和 . 搞不懂了,高人来指点
    WORD wDuring = ComputeChkSum(hProcess, pbModule, &inh);
    DETOUR_TRACE((
"ChkSum: %04x + %08x => %08x ", wDuring, dwFileSize, wDuring + dwFileSize));

    idh.e_res[
0= detour_sum_minus(idh.e_res[0], detour_sum_minus(wDuring, wBefore));

    
if (!WriteProcessMemory(hProcess, pbModule, &idh, sizeof(idh), NULL)) {
        DETOUR_TRACE((
"WriteProcessMemory(idh) failed: %d ", GetLastError()));
        
goto finish;
    }


    
// 修改会原来的属性
    if (!VirtualProtectEx(hProcess, pbModule, inh.OptionalHeader.SizeOfHeaders,
                          dwProtect, 
&dwProtect)) {
        DETOUR_TRACE((
"VirtualProtextEx(idh) restore failed: %d ", GetLastError()));
        
goto finish;
    }


    
// 最后再次计算效验和,和之前的相比较
    WORD wAfter = ComputeChkSum(hProcess, pbModule, &inh);
    DETOUR_TRACE((
"ChkSum: %04x + %08x => %08x ", wAfter, dwFileSize, wAfter + dwFileSize));
    DETOUR_TRACE((
"Before: %08x, After: %08x ", wBefore + dwFileSize, wAfter + dwFileSize));

    
if (wBefore != wAfter) {
        DETOUR_TRACE((
"Restore of checksum failed %04x != %04x. ", wBefore, wAfter));
        
goto finish;
    }


    
// 没有实际作用的
    if (!DetourCopyPayloadToProcess(hProcess, DETOUR_EXE_RESTORE_GUID, &der, sizeof(der))) {
        DETOUR_TRACE((
"DetourCopyPayloadToProcess failed: %d ", GetLastError()));
        
goto finish;
    }

    

    fSucceeded 
= TRUE;
    
goto finish;
}

一切都尽在注释中了,归纳一下,不外乎这几个步骤:
1. 读出PE头(IMAGE_NT_HEADERS)
2.根据OptionalHeader的DataDirectory找到输入表
3.构造一个新的输入表(这一步是关键)
4.把PE头,以及输入表都写到远程进程去
其他效验和和最后的DetourCopyPayloadToProcess都是后续工作了

原创粉丝点击