从反射注入学习pe文件加载的记录

从反射注入学习pe文件加载

前言?后记与总结

这是我写完代码后写的总结。重新梳理一下反射注入到底想干什么、以及怎么干。以及从中学到了什么。

收获?目标?反射注入是什么

首先是学习反射注入的收获,当然也可以作为学习的目标,同时也是反射注入实际做的东西。

我的理解是反射注入实际上就是手工加载模块。通过LoadLibrary加载dll会在peb中留下记录,通过手工实现加载过程,我们的dll能像正常dll那样工作,且不再peb中留下痕迹。

也就是说,通过学习反射注入,可以了解到windows系统加载一个pe文件的流程。其中涉及到了部分的peb以及大量的pe结构。

我们主要需要peb中的ldr结构,这个结构中保存了该进程已经加载了的dll。

既然我们的主要工作是手工加载pe文件,自然要对pe文件格式有一定了解。但笔记中不会多提pe文件结构,实际上只要大概了解pe文件格是是个什么,然后在写代码时多去看pe结构的定义,就可以对pe文件结构有一个更深的理解。

参考

在学习过程中参考了许多资料,最主要的就是msf的反射注入payload的源码。其他如有不懂通过百度也可以找到详细的解释。相关文章比较多,dddd,就不一一列举了。

关于这篇东西

这篇东西由我阅读源码,查资料时做的笔记发展而来。国内虽然少但也有一些优秀的反射注入的文章,看雪中也有类似文章,但阅读门槛稍微有点高。因为是由笔记发展而来,这更像是一个零基础初学者的学习笔记(实际上在开始学反射注入之前,只知道pe文件格是是什么东西,几乎完全不了解。对windows的机制也完全不了解),希望能帮助到初学者,这项技术对我学习windows有很大帮助,虽然我只是一个初学者,但这项实践使我之后对书本、资料上的内容有了更深的了解。可能会有错误,希望发现错误的dalao可以帮帮我这个初学者纠正。

前置要求

va、fa、rva这几个概念搞清楚好像就行。实际上就是对pe文件如何从文件映射到内存有一个大体的认识。我们的工作就是具体完成这个过程。可以参考下图。

pe文件的格式网上有比较多的图片,这里就不贴了。

流程

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

(前置条件序号) 序号 流程内容

============

注入器:

============

(0)   1 打开dll文件(CreateFile),获取dll长度(GetFileSize)

(1)   2 分配内存(HealAlloc),读取文件(ReadFile)

(0)   3 打开目标进程(OpenProcess)

(2,34 调用反射注入函数(LoadLibraryR.c>LoadRemoteLibraryR)

(2)   5 获取反射加载函数的文件偏移(LoadLibraryR.c>GetReflectiveLoaderOffset)

(2,36 在目标进程中分配空间(VirtualAllocEx),写入dll(WriteProcessMemory)

(6)   7 修改目标进程中的空间为可执行(VirtualProtectEx)

(5,78 创建远程线程,执行反射加载函数(CreateRemoteThread)

=======================

反射加载函数(运行在被注入进程的新建线程中):

=======================

1 获取基地址

2 获取需要的kernel32.dll及ntdll.dll的函数的va

3 分配空间作为映像空间,并复制pe头到新的位置

4 复制所有段到映像的对应位置

5 处理导入表,填写iat

6 重定位

7 跳转到ep(_DllMainCRTStartup)

8 返回entry point地址

注入器

主要流程

首先打开dll文件,获取长度,并在堆中分配空间读取文件。

1

2

3

hFile = CreateFileW(dllPathname, GENERIC_READ, 00, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);

dwDllLen = GetFileSize(hFile, 0);

lpDll = HeapAlloc(GetProcessHeap(), 0, dwDllLen);

然后找到dll中ReflectiveLoader的入口点。

最后以RW申请空间,写入dll后改成RX,然后以ReflectiveLoader作为线程函数创建远程线程。

计算fa

LoadLibraryR中有个函数Rva2Offset用于获取rva对应的fa。

原理是遍历区块获取区块的section_rva和section_fa,然后比较rva和section_rva找到rva所在的section,最后计算出fa。再用fa+baseAddr得到内存中的位置。

1

2

3

4

5

6

7

8

9

10

DWORD Rva2Fa(DWORD rva, PIMAGE_SECTION_HEADER sections, int sectionNum)

{

    for (int = 0; i < sectionNum; i++) {

        int sectionVa = sections[i].VirtualAddress;

        if ((rva >= sectionVa) && ((sectionVa + sections[i].SizeOfRawData) > rva))

            return rva - (sectionVa - sections[i].PointerToRawData);

    }

    return 0;

}

获取ReflectivelLoader位置(输出表)

通过nt头,计算出sections的fa,以及通过nt头的optionalheader获取输出表的rva。

然后遍历section找到输出表的fa,接着遍历输出表的函数名字rva表,计算出rva对应fa得到导出函数名字,与需要的导出函数做对比,确定要找的函数在函数名表中的下标。用此下标在序号表中找到序号,最后再用序号去地址表找到地址。

反射加载函数

1. 获取基址

首先获取代码的位置,然后再往前找dos头。

_ReturnAddress()返回当前调用函数的返回地址。所以在loader中调用一个函数,该函数再调用_ReturnAddress(),返回调用函数的返回地址,即loader中调用函数的下一条语句的地址。
其中 __declspec(noinline) 用于防止编译器优化该函数成内联函数,否则返回的就是loader的返回地址。

使用_ReturnAddress需要intrin.h,并使用#pragma intrinsic防止内联优化。

1

2

3

4

5

6

#include<intrin.h>

#pragma intrinsic(_ReturnAddress)

__declspec(noinline) PVOID NextAddr()

{

    return (PVOID)_ReturnAddress();

}

根据pe格式可知,dos头(IMAGE_DOS_HEADER)中有有一个e_magic标志,值是0x5A4D(MZ)。
所以向前遍历内存,直到找到MZ标志,再检查pe头的PE标志,这样就找到dos头了。
需要注意的是,检查PE标志时要检查pe头偏移是否正确,防止错误的内存访问。

1

2

3

4

5

6

7

8

9

10

11

while (TRUE) {

    if (dosHeadAddr->e_magic == 0x5A4D) {

        LONG e_lfanew = dosHeadAddr->e_lfanew;

        if (e_lfanew >= sizeof(IMAGE_DOS_HEADER) && e_lfanew < 1024) {

            ntHeadAddr = (PIMAGE_NT_HEADERS)((PVOID)dosHeadAddr + (PVOID)e_lfanew);

            if (ntHeadAddr->Signature == 0x4550)

                break;

        }

    }

    dosHeadAddr--;

}

这里也可以取巧,远程线程是可以传递一个参数的,对于我们这个简单的dll,imagebase实际上就是分配空间的首地址,可以作为参数传入。

2. 获取需要的内核导出函数的va

目标

接下来的步骤中需要用到一些ntdll.dll,kernel32.dll中的导出函数,所以需要先找到这些函数的va。这些系统模块都是已经加载了的,可以在peb中找到其加载的位置。

这里利用hash避免直接比较字符串。

我们需要LoadLibraryA、GetProcAddress加载导入表中的dll的对应的函数。

需要VirtualAlloc分配内存给我们把pe文件加载到其中。

需要NtFlushInstructionCache刷新指令缓存。

LDR_DATA_TABLE_ENTRY

InMemoryOrderModuleList对应的链表是一个环形双向链表,且有一个头节点(或者说哨兵节点)。InMemoryOrderModuleList的Flink指向链表的第一个节点,Blink指向链表最后一个节点。头节点的Flink是第一个节点,可以以此为跳出条件遍历该链表。

这里借用一张网图。

思路

首先从peb中找到ldr,然后遍历InMemoryOrderModuleList,通过hash(BaseName)找到kernel32.dll和ntdll.dll对应的LDR_DATA_TABLE_ENTRY结构。

找到dll对应的LDR_DATA_TABLE_ENTRY后,获取其imagebase,然后解析pe头,计算出导出表位置。同样利用hash比较字符串找到所需的导出函数,并计算出va。

实现代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

// 找pLdrDataTableEnrty

DWORD pPeb = __readfsdword(0x30);

DWORD pLdr = *(DWORD*)(pPeb + 0xc);

DWORD pInMemoryOrderModuleList = pLdr + 0x14// 第一个节点的二级指针

DWORD pLdrDataTableEnrty = *(DWORD*)(pInMemoryOrderModuleList + 0);

// 遍历LdrDataTableEnrty

do{

    WCHAR* name = (WCHAR*)*(DWORD*)(pLdrDataTableEnrty + 0x24 + 0x4);

    hash = YourHashFun(name); // 使用你自己的函数计算hash

    if(hash == DLLHASH) { // DLLHASH由你自己的函数计算得出

        DWORD baseAddr = *(DWORD*)(pLdrDataTableEnrty + 0x10);

        // 解析pe头过程省略

        for(int = 0; i < funcNum; i++) { // funcNum是导出函数的个数

            char* name = (char*)(baseAddr + ((DWORD*)nameRvas)[i]);

            DWORD hash = YourHashFun(name);

            if (hash == FUNCHASH) {

                pFunc = (FUNC)(baseAddr + ((DWORD*)funcRvas)[((WORD*)ordRvas)[i]]);

            }

        }

    }

}while(*(DWORD*)(pLdrDataTableEnrty) != *(DWORD*)(pInMemoryOrderModuleList))

3. 给映像分配空间,并加载pe头

新分配大小等于sizeOfImga的内存作为映像加载的空间,然后把pe头复制到新内存里,这里我只更新了新nt头的imagebase地址。太简单就不贴代码了。

4. 加载段

遍历section_header获取fa和rva,计算出section在旧内存中的va和新内存中的va。然后复制section到新内存中的对应位置。

1

2

oldVA = oldImageBase + sections[i].PointerToRawData;

newVA = newImageBase + sections[i].VirtualAddress;

5. 处理导入表

目标

找到导入表,然后遍历导入表,依次加载对应的dll,及需要的dll的导出函数,并填写对应iat。

导入表结构

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

// winnt.h

typedef struct _IMAGE_THUNK_DATA32 {

    union {

        DWORD ForwarderString;      // PBYTE

        DWORD Function;             // PDWORD

        DWORD Ordinal;

        DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME (补充一下,这是个rva)

    } u1;

} IMAGE_THUNK_DATA32;

typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

typedef struct _IMAGE_IMPORT_BY_NAME {

    WORD    Hint;

    CHAR   Name[1];

} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

和导出表不同,导入表是一个结构体数组。它不提供结构体数量,最后一个结构体仅作为结束标志,不包含导入信息,其成员Characteristics为0,这可以作为遍历的退出条件。

对于每个导入表,在文件中时OriginalFirstThunk和FirstThunk都是RVA,指向同一个IMAGE_THUNK_DATA结构体数组。
当加载到内存时,FirstThunk改为函数的VA,即iat。

文件中时,OriginalFirstThunk和FirstThunk指向的结构体数组中,每一个IMAGE_THUNK_DATA的成员u1都被解释为Ordinal,若该函数应该通过序号导入,则Ordinal的最高位会被置为1。

思路

见实现代码注释。

实现代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pNewDosHeader + pNewNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

for (; pImportDescriptor->Characteristics; pImportDes++) {

    // 加载dll

    HMODULE libraryAddress = pLoadLibraryA((LPCSTR)((DWORD)pNewDosHeader + pImportDes->Name));

    if (!libraryAddress)

            continue;

    // parsing pe structure

    PIMAGE_THUNK_DATA32 pOriginalThunk = (PIMAGE_THUNK_DATA32)((DWORD)pNewDosHeader + pImportDes->OriginalFirstThunk);

    PIMAGE_THUNK_DATA32 pThunk = (PIMAGE_THUNK_DATA32)((DWORD)pNewDosHeader + pImportDes->FirstThunk);

    PIMAGE_NT_HEADERS32 pLibNtHeader = (PIMAGE_NT_HEADERS32)((DWORD)libraryAddress + ((PIMAGE_DOS_HEADER)libraryAddress)->e_lfanew);

    PIMAGE_EXPORT_DIRECTORY pExportDir = (PIMAGE_EXPORT_DIRECTORY)((DWORD)libraryAddress + pLibNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

    PDWORD funcRvas = (PDWORD)((DWORD)libraryAddress + pExportDir->AddressOfFunctions);

    while (*(DWORD*)pThunk) {

        if (pOriginalThunk && pOriginalThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG) {

            // import by ord

            WORD ord = pOriginalThunk->u1.Ordinal - pExportDir->Base;

            *(DWORD*)pThunk = ((DWORD)libraryAddress + funcRvas[ord]);

        }

        else {

            // import by name (this is a rva)

            *(DWORD*)pThunk = (DWORD)pGetProcAddress(libraryAddress, ((PIMAGE_IMPORT_BY_NAME)((DWORD)pNewDosHeader + pThunk->u1.AddressOfData))->Name);

        }

        pThunk++;

        if (pOriginalThunk)

            pOriginalThunk++;

    }

}

6. 重定位

目标

完成重定位过程。

重定位表结构

重定位表是一个结构体数组,DataDirectory中的重定位表项保存着第一个重定位表的rva,遍历每一个重定位表,并遍历重定位表中的表项,根据其重定位类型,执行重定位操作。

1

2

3

4

5

6

7

8

9

typedef struct {

    WORD offset : 12;

    WORD type 4;

} RELOC;

typedef struct {

    DWORD VA;

    DWORD size;

    // RELOC reloc[];

} IMAGE_BASE_RELOCATION;

其中每一个重定位表保存着一个rva,重定位实际上就是遍历IMAGE_BASE_RELOCATION的成员reloc,然后执行*(rva+baseAddr+reloc[i].offset) += baseAddr - ImageBase

思路

两层循环,遍历重定位表,再遍历每个表的 RELOC reloc[]。然后根据重定位类型进行重定位。

实现代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

// 解析pe,并计算offset

PIMAGE_DATA_DIRECTORY pDDBaseReloc = &pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];

PIMAGE_BASE_RELOCATION pBaseRelocation;

ULONG_PTR offset = (ULONG_PTR)pNewDosHeader - (ULONG_PTR)pNtHeaders->OptionalHeader.ImageBase;

if (pDDBaseReloc->Size) {

    DWORD size = pDDBaseReloc->Size;

    pBaseRelocation = (PIMAGE_BASE_RELOCATION)((DWORD)pNewDosHeader + pDDBaseReloc->VirtualAddress);

    // 遍历重定位表结构体

    while (size && pBaseRelocation->SizeOfBlock) {

        DWORD va = (DWORD)pNewDosHeader + pBaseRelocation->VirtualAddress;

        DWORD num = (pBaseRelocation->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(IMAGE_RELOC); // 计算reloc[]大小

        PIMAGE_RELOC reloc = (PIMAGE_RELOC)((DWORD)pBaseRelocation + sizeof(IMAGE_BASE_RELOCATION));

        // 遍历reloc[],根据重定位类型重定位

        while (num--) {

            DWORD type = reloc->type;

            if (type == IMAGE_REL_BASED_HIGH) {

                *(WORD*)(va + reloc->offset) += HIWORD(offset);

            }

            else if (type == IMAGE_REL_BASED_LOW) {

                *(WORD*)(va + reloc->offset) += LOWORD(offset);

            }

            else if (type == IMAGE_REL_BASED_HIGHLOW) {

                *(DWORD*)(va + reloc->offset) += (DWORD)offset;

            }

            reloc++;

        }

        size -= pBaseRelocation->SizeOfBlock;

        pBaseRelocation = (PIMAGE_BASE_RELOCATION)((DWORD)pBaseRelocation + pBaseRelocation->SizeOfBlock);

    }

}

7. 跳转到ep

跳转到dll的ep。实际上就是执行dll原本的_DllMainCRTStartup函数。该函数会完成一些初始化工作并转到dllMain,让我们的dllMain像正常dllmain那样运行,但又不在peb中留下dll加载的痕迹。

1

2

3

4

5

6

7

typedef BOOL(WINAPI* DLLMAIN)(HINSTANCE, DWORD, LPVOID);

PVOID entryPoint = (PVOID)((DWORD)pNewDosHeader + pNewNtHeaders->OptionalHeader.AddressOfEntryPoint);

pNtFlushInstructionCache((HANDLE)-1, NULL, 0);

((DLLMAIN)entryPoint)((HMODULE)pNewDosHeader, DLL_PROCESS_ATTACH, lpParameter);

8. 返回

最后返回entrypoint。

参考

https://github.com/rapid7/ReflectiveDLLInjection

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值