学习开源代码ReflectiveDLLInjection时做的一些思路总结。
代码大家可自行在github上面下载。
利用CreateFileA加载reflective_dll得到句柄
通过GetFileSize得到reflective_dll文件大小,分配一块堆内存利用ReadFile去将reflective_dll读入进程内存空间。
提权
利用OpenProcess函数去打开目标进程 必须带有以下标志
/*A handle to the process in which the thread is to be created. The handle must have the
PROCESS_CREATE_THREAD, PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE, and PROCESS_VM_READ access rights*/
LoadRemoteLibraryR( hProcess, lpBuffer, dwLength, NULL );
- 取ReflectiveLoader函数在DLL文件中的文件偏移通过传参进来的基地址 强制类型转换成PIMAGE_DOS_HEADER类型指向e_lfanew,加上基地址得到NT Header。 找到导出表 从导出表中通过AdressOfName表找到 ReflectiveLoader,计算出他的索引值,从而在AdressOfFuction表中找到该函数地址,再转换为在DLL中的文件偏移
- Rva2Offset通过传参进来的基地址 强制类型转换成PIMAGE_DOS_HEADER类型指向e_lfanew,加上基地址得到NT Header。
- RVA(相对虚拟地址)和FOA(文件偏移)的具体含义大家可以看看《Windows PE 权威指南》或者是小甲鱼的PE结构详解视频,我相信大家看完之后一定会理解的,我这里就不写这些概念了。之所以会产生两者的转换,是因为同一个文件在硬盘和内存中的对齐方式不一样,我们可以通过IMAGE_OPTIONAL_HEADER结构体的SectionAlignment(内存对齐方式)和FileAlignment(文件对齐方式)这两个字段,知道它们的对齐方式分别是以200h进行对齐和以1000h对齐的,那么当我们知道内存相对虚拟地址时,想转换成文件偏移时,还需要借助IMAGE_SECTION_HEADER这个结构体。该结构体记录了该Section在文件中的起始偏移(PointerToRawData)和内存映像中的起始RVA(VirtualAddress),这是我们可以找到它们唯一联系的地方。
我们转换的思路是:模拟内存对齐方式,看需要转换的虚拟地址是否在该区段之间。转化的步骤:1. 找到可执行文件中的Section的数目dwSectionCount,这个可以通过IMAGE_FILE_HEADER结构体中的NumberOfSections字段获取。2. 找到Section的对齐大小,可以通过IMAGE_OPTIONAL_HEADER的SectionAlignment字段获取这个值。3. 对dwSectionCount的循环,在这个循环中我们需要判断RVA位于哪个Section中,diff = 需要转换的虚拟地址-VirtualAddr计算出距离该节起始地址的偏移。4 PointerToRawData + diff就是转换后的结果。
在目标进程空间VirtualAllocEx分配一块PAGE_EXECUTE_READWRITE虚拟内存,
WriteProcessMemory 将DLL写入目标进程的虚拟内存
算出ReflectiveLoader函数在目标进程中的地址
hThread = CreateRemoteThread( hProcess, NULL, 1024*1024, lpReflectiveLoader, lpParameter, (DWORD)NULL, &dwThreadId );
HANDLE WINAPI CreateRemoteThread(
__in HANDLE hProcess,
__in LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in SIZE_T dwStackSize,
__in LPTHREAD_START_ROUTINE lpStartAddress,
__in LPVOID lpParameter,
__in DWORD dwCreationFlags,
__out LPDWORD lpThreadId
);
|
hProcess [in]
线程所属进程的进程句柄.
该句柄必须具有 PROCESS_CREATE_THREAD, PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE,和PROCESS_VM_READ 访问权限.
lpThreadAttributes [in]
一个指向 SECURITY_ATTRIBUTES 结构的指针, 该结指定了线程的安全属性.
dwStackSize [in]
线程初始大小,以字节为单位,如果该值设为0,那么使用系统默认大小.
lpStartAddress [in]
在远程进程的地址空间中,该线程的线程函数的起始地址.
lpParameter [in]
传给线程函数的参数.
dwCreationFlags [in]
线程的创建标志.
创建远程监控线程 要求是lpReflectiveLoader(函数必须在进程空间存在)
开始执行ReflectiveLoader
先获得我们写入目标进程的DLL的首地址
//__declspec(noinline) ULONG_PTR caller( VOID ) { return (ULONG_PTR)_ReturnAddress(); } 返回当前调用函数返回的地址,即函数下一条指令地址
//利用当前调用函数返回的地址去寻找DLL首地址
uiLibraryAddress = caller();//获得当前调用函数返回的地址
while( TRUE )
{
//找到DLL在进程空间中的地址 要注意这里的dll也是写进来的而不是加载进来的。
//利用当前调用函数返回的地址去寻找DLL首地址
if( ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE )
{
uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
// some x64 dll's can trigger a bogus signature (IMAGE_DOS_SIGNATURE == 'POP r10'),
// we sanity check the e_lfanew with an upper threshold value of 1024 to avoid problems.
if( uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024 )
{
uiHeaderValue += uiLibraryAddress;
// break if we have found a valid MZ/PE header
if( ((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE )
break;
}
}
uiLibraryAddress--;
}
通过PEB查找已加载模块,找到模块中3我们所需的函数地址 pLoadLibraryA && pGetProcAddress && pVirtualAlloc && pNtFlushInstructionCache
获得peb——>ldr-->InMemoryOrderModuleList.Flink
typedef struct _LDR_DATA_TABLE_ENTRY
{
//LIST_ENTRY InLoadOrderLinks; // As we search from PPEB_LDR_DATA->InMemoryOrderModuleList we dont use the first entry.
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STR FullDllName;
UNICODE_STR BaseDllName;
ULONG Flags;
SHORT LoadCount;
SHORT TlsIndex;
LIST_ENTRY HashTableEntry;
ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
利用获得的函数动态申请一块内存。PAGE_EXECUTE_READWRITE属性,可读可写可执行
- 先将PE头写入内存中
- 然后在将节写入(开始重定位)
uiValueB = ( uiBaseAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->VirtualAddress );
uiValueC = ( uiLibraryAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->PointerToRawData );
uiValueD = ((PIMAGE_SECTION_HEADER)uiValueA)->SizeOfRawData;while( uiValueD-- )*(BYTE *)uiValueB++ = *(BYTE *)uiValueC++;
映射导入表
OriginalFirstThunk
FirstThunk
现在可以看成是相同的(现在),它们都指向一个包含一系列IMAGE_THUNK_DATA结构的数组,数组中的每个IMAGE_THUNK_DATA结构定义了一个导入函数的信息,数组最后以一个内容为0的IMAGE_THUNK_DATA结构作为结束。
现在他们放的东西还是一样的,因为他们是被读进内存的
开始重写FirstThunk
判断OriginalFirstThunk->u1.Ordinal的最高位 从而确定重写方法
当 IMAGE_THUNK_DATA 值的最高位为 1时,表示函数以序号方式输入,这时候低 31位被看作一个函数序号。(读者可以用预定义值IMAGE_ORDINAL_FLAG32或80000000h来对最高位进行测试)
当 IMAGE_THUNK_DATA 值的最高位为 0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个 RVA,指向IMAGE_IMPORT_BY_NAME 结构。
IMAGE_IMPORT_BY_NAME STRUCT
Hint WORD ?
Name1 BYTE ?
IMAGE_IMPORT_BY_NAME ENDS
假如是序号方式 序号减去BASE基址得到索引,从导入表中获得函数偏移 在FirstThunk对应IMAGE_THUNK_DATA 上写入函数地址
假如是以字符串类型的函数名方式输入 利用已获得的函数指针 pGetProcAddress 获得 IMAGE_IMPORT_BY_NAME STRUCT 中的函数名Name1 所指的函数地址 在FirstThunk对应IMAGE_THUNK_DATA 上写入
- 重定位表(重写)
重定位的算法可以描述为:将直接寻址指令中的双字地址加上模块实际装入地址与模块建议装入地址之差。为了进行这个运算,需要有3个数据,首先是需要修正的机器码地址;其次是模块的建议装入地址;最后是模块的实际装入地址。
通过数据目录找到找到重定位表的首地址
每个重定位块以一个IMAGE_BASE_RELOCATION结构开头,后面跟着在本页面中使用的所有重定位项,每个重定位项占用16位的地址(也就是一个word),结构的定义是这样的:
IMAGE_BASE_RELOCATION STRUCT
VirtualAddress dd ? ;重定位内存页的起始RVA
SizeOfBlock dd ? ;重定位块的长度
IMAGE_BASE_RELOCATION ENDS
( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) ) / sizeof( IMAGE_RELOC );获得重定位项的数量
IMAGE_RELOC 结构大小为WORD
typedef struct
{
WORD offset:12;
WORD type:4;
} IMAGE_RELOC, *PIMAGE_RELOC;
判断type的类型来确定取模块实际装入地址与模块建议装入地址之差的位数
最后重写对应重定位项地址
最后利用已获得的函数指针pNtFlushInstructionCache 刷新缓存
注入成功
找到程序入口点,执行DLLMAIN