吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 21947|回复: 47
收起左侧

[系统底层] PE加载器的简单实现

[复制链接]
VicZ 发表于 2019-1-25 20:53
本帖最后由 VicZ 于 2019-1-25 20:57 编辑

模拟PE加载器加载PE文件,对导入表以及重定位表的操作过程。

PE加载器的步骤:

1.将PE文件用ReadFile读取数据

char szFileName[] = "1.exe";

//打开文件,设置属性可读可写
HANDLE hFile = CreateFileA(szFileName,
        GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_ARCHIVE,
        NULL);

if (INVALID_HANDLE_VALUE == hFile)
{
        printf("文件打开失败\n");
        return 1;
}

//获取文件大小
DWORD dwFileSize = GetFileSize(hFile, NULL);

//申请空间将exe读取到内存中
char *pData = new char[dwFileSize];
if (NULL == pData)
{
        printf("空间申请失败\n");
        return 2;
}

DWORD dwRet = 0;
ReadFile(hFile, pData, dwFileSize, &dwRet, NULL);
CloseHandle(hFile);

2.根据PE结构获取镜像大小,在自己的程序中申请可读可写可执行的内存。

char* chBaseAddress = NULL;

//获取镜像大小
DWORD dwSizeOfImage = GetSizeOfImage(pFileBuff);

//在进程中开辟一块内存空间
chBaseAddress = (char*)VirtualAlloc(NULL,
        dwSizeOfImage,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE);
if (NULL == chBaseAddress)
{
        printf("申请进程空间失败\n");
        return NULL;
}

3.将申请的空间全部填为0

RtlZeroMemory(chBaseAddress, dwSizeOfImage);

4.将用ReadFile读取的数据映射到内存中
PE文件映射到内存.png

由上图可知,PE文件在加载到内存中对齐粒度不同,因此在写代码时要注意这个问题

PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuff;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pFileBuff + pDos->e_lfanew);
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);
//所有头 + 结表头的大小
DWORD dwSizeOfHeaders = pNt->OptionalHeader.SizeOfHeaders;
//获取区段数量
int nNumerOfSections = pNt->FileHeader.NumberOfSections;

// 将前一部分都拷贝过去
RtlCopyMemory(chBaseAddress, pFileBuff, dwSizeOfHeaders);

char* chSrcMem = NULL;
char* chDestMem = NULL;
DWORD dwSizeOfRawData = 0;
for (int i = 0; i < nNumerOfSections; i++)
{
        if ((0 == pSection->VirtualAddress) ||
                (0 == pSection->SizeOfRawData))
        {
                pSection++;
                continue;
        }

        chSrcMem = (char*)((DWORD)pFileBuff + pSection->PointerToRawData);
        chDestMem = (char*)((DWORD)chBaseAddress + pSection->VirtualAddress);
        dwSizeOfRawData = pSection->SizeOfRawData;
        RtlCopyMemory(chDestMem, chSrcMem, dwSizeOfRawData);

        pSection++;
}

return TRUE;

5.修复重定位

重定位后的地址 = 需要重定位的地址  -  默认加载基址  +  当前加载基址

PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)chBaseAddress;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(chBaseAddress + pDos->e_lfanew);
PIMAGE_BASE_RELOCATION pLoc = (PIMAGE_BASE_RELOCATION)(chBaseAddress + pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);

//判断是否有重定位表
if ((char*)pLoc == (char*)pDos)
{
        return TRUE;
}

while ((pLoc->VirtualAddress + pLoc->SizeOfBlock) != 0) //开始扫描重定位表
{
        WORD *pLocData = (WORD *)((PBYTE)pLoc + sizeof(IMAGE_BASE_RELOCATION));
        //计算需要修正的重定位项(地址)的数目
        int nNumberOfReloc = (pLoc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);

        for (int i = 0; i < nNumberOfReloc; i++)
        {
                // 每个WORD由两部分组成。高4位指出了重定位的类型,WINNT.H中的一系列IMAGE_REL_BASED_xxx定义了重定位类型的取值。
                // 低12位是相对于VirtualAddress域的偏移,指出了必须进行重定位的位置。

                if ((DWORD)(pLocData[i] & 0x0000F000) == 0x00003000) //这是一个需要修正的地址
                {
                        DWORD* pAddress = (DWORD *)((PBYTE)pDos + pLoc->VirtualAddress +                                                                                                                                (pLocData[i] & 0x0FFF));        
                        DWORD dwDelta = (DWORD)pDos - pNt->OptionalHeader.ImageBase;
                        *pAddress += dwDelta;
                }
        }

        //转移到下一个节进行处理
        pLoc = (PIMAGE_BASE_RELOCATION)((PBYTE)pLoc + pLoc->SizeOfBlock);
}

return TRUE;

6.根据PE结构的导入表,加载所需的dll,并获取导入函数的地址并写入导入表中

PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)chBaseAddress;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(chBaseAddress + pDos->e_lfanew);
PIMAGE_IMPORT_DESCRIPTOR pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pDos +
        pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

// 循环遍历DLL导入表中的DLL及获取导入表中的函数地址
char *lpDllName = NULL;
HMODULE hDll = NULL;
PIMAGE_THUNK_DATA lpImportNameArray = NULL;
PIMAGE_IMPORT_BY_NAME lpImportByName = NULL;
PIMAGE_THUNK_DATA lpImportFuncAddrArray = NULL;
FARPROC lpFuncAddress = NULL;
DWORD i = 0;

while (TRUE)
{
        if (0 == pImportTable->OriginalFirstThunk)
        {
                break;
        }

        // 获取导入表中DLL的名称并加载DLL
        lpDllName = (char *)((DWORD)pDos + pImportTable->Name);
        hDll = GetModuleHandleA(lpDllName);
        if (NULL == hDll)
        {
                hDll = LoadLibraryA(lpDllName);
                if (NULL == hDll)
                {
                        pImportTable++;
                        continue;
                }
        }

        i = 0;
        // 获取OriginalFirstThunk以及对应的导入函数名称表首地址
        lpImportNameArray = (PIMAGE_THUNK_DATA)((DWORD)pDos + pImportTable->OriginalFirstThunk);
        // 获取FirstThunk以及对应的导入函数地址表首地址
        lpImportFuncAddrArray = (PIMAGE_THUNK_DATA)((DWORD)pDos + pImportTable->FirstThunk);
        while (TRUE)
        {
                if (0 == lpImportNameArray[i].u1.AddressOfData)
                {
                        break;
                }

                // 获取IMAGE_IMPORT_BY_NAME结构
                lpImportByName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pDos + lpImportNameArray[i].u1.AddressOfData);

                // 判断导出函数是序号导出还是函数名称导出
                if (0x80000000 & lpImportNameArray[i].u1.Ordinal)
                {
                        // 序号导出
                        lpFuncAddress = GetProcAddress(hDll, (LPCSTR)                                                   (lpImportNameArray[i].u1.Ordinal & 0x0000FFFF));
                }
                else
                {
                        // 名称导出
                        lpFuncAddress = GetProcAddress(hDll, (LPCSTR)lpImportByName->Name);
                }
                lpImportFuncAddrArray[i].u1.Function = (DWORD)lpFuncAddress;
                i++;
        }

        pImportTable++;
}

7.修改PE文件的加载基址

PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)chBaseAddress;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(chBaseAddress + pDos->e_lfanew);
pNt->OptionalHeader.ImageBase = (ULONG32)chBaseAddress;

8.跳转到PE的入口点处执行

PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)chBaseAddress;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(chBaseAddress + pDos->e_lfanew);
char* ExeEntry = (char*)(chBaseAddress + pNt->OptionalHeader.AddressOfEntryPoint);
// 跳转到入口点处执行
__asm
{
        mov eax, ExeEntry
        jmp eax
}

通过上述代码,成功加载并运行程序:
2.png

免费评分

参与人数 7吾爱币 +17 热心值 +6 收起 理由
Hmily + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
yyhf + 1 好久没有看到这么细致的文章了,感谢~
Pear + 1 + 1 热心回复!
nemo_li + 1 + 1 我很赞同!
610100 + 3 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
周峻弘 + 1 + 1 热心回复!
wmsuper + 3 + 1 我很赞同!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

popkun222 发表于 2020-8-26 21:50
本帖最后由 popkun222 于 2020-8-26 21:51 编辑

可能我描述的不清楚, 我很想知道一个exe启动时windows做的所有工作, 比如下面的Call stack就是exe创建进程启动的全部过程。。。特别想知道这些个东西具体的作用。。。只能猜个大概就是创建PEB ,主线程,(分配内存??)。。。
ntdll!LdrpDoDebuggerBreak+0x30
ntdll!LdrpInitializeProcess+0x1e57
ntdll!_LdrpInitialize+0x50589
ntdll!LdrpInitialize+0x3b
ntdll!LdrInitializeThunk+0xe
tfrist 发表于 2020-8-25 04:13
popkun222 发表于 2020-8-3 15:01
好帖子。第八张图是main函数?如果是那就是 在一个exe里面读取加载pe到内存,跳转到pe的位置执行。。。  这 ...

作者完全实现了一个windows加载exe的过程! 对这个过程的了解是非常有意义的!
有这个可以加载任何PE文件
七宗罪丶 发表于 2019-1-25 22:00
linuxprobe 发表于 2019-1-25 22:14
学这个需要有基础才能看得懂,否则是在浪费时间。
hbkccccc 发表于 2019-1-25 23:39
先收藏了,不错,支持,感谢分享
tfrist 发表于 2019-1-26 02:07
分析的不错
dywm 发表于 2019-1-26 07:14
不错,支持
gvi123a 发表于 2019-1-26 07:20
虽然看不懂,还是回复 一下
鱼无论次 发表于 2019-1-26 09:15
讲的非常好
windows11 发表于 2019-1-26 09:35
在pe这里卡主了,感谢分享。
cwz 发表于 2019-1-26 10:14
向楼主学习
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-4-20 08:40

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表