羽夏壳世界—— PE 解析的实现
写在前面
此系列是本人一个字一个字码出来的,包括代码实现和效果截图。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 羽夏壳世界——序 ,方便学习本教程。
PE 解析实现
既然要做一个加密保护壳,就首先得会解析它。在C++
中面向对象的编程的思想体现的比较明显,那么我就建一个类,名为CWingProtect
(一看就是老MFC
了)。然后头文件文件内容如下:
#pragma once
class CWingProtect
{
};
里面空空如也,啥也没有。为了保证别人用我的东西并且表示是我写的东西,我加了一个命名空间,它就成了这样:
#pragma once
namespace WingProtect
{
class CWingProtect
{
};
}
好,我们需要写一个构造函数,用来解析PE
文件。然后调用指定函数来进行保护,那么加上构造函数就这样了:
#pragma once
namespace WingProtect
{
class CWingProtect
{
public:
CWingProtect(const TCHAR* filename, UINT pagecount = 10);
};
}
有构造函数就必然析构函数,加上:
#pragma once
namespace WingProtect
{
class CWingProtect
{
public:
CWingProtect(const TCHAR* filename, UINT pagecount = 10);
~CWingProtect();
};
}
下面我们就开始实现这个构造函数了,思考我们要干什么?
我们首先得解析PE
文件,并把相应的处理信息存储起来,放到结构体是再好不过的选择。不过一切的一切前提你得把该文件打开,一种方式是调用文件读写相关的函数,不过这样效率比较低,且不方便。另一种方式直接内存映射的方式,这样读取数据就和在自己读取自己程序的变量一样容易。好,下面我们开始:
CWingProtect::CWingProtect(const TCHAR* filename,UINT pagecount)
{
//初始化分析需要用到的内存
auto alloc = AllocPageSizeMemory();
if (!alloc)
{
_lasterror = ParserError::CannotAllocMemory;
return;
}
memset(alloc, 0, PageSize);
peinfo.AnalysisInfo.ImportDllName = (DllImportName*)alloc;
alloc = AllocReadWriteMem(PageSize * 10);
if (!alloc)
{
_lasterror = ParserError::CannotAllocMemory;
return;
}
memset(alloc, 0, PageSize * 10);
peinfo.AnalysisInfo.ImportFunNameTable = (char*)alloc;
alloc = AllocPageSizeMemory();
if (!alloc)
{
_lasterror = ParserError::CannotAllocMemory;
return;
}
memset(alloc, 0, PageSize);
peinfo.AnalysisInfo.DllFirstThunks = (UINT*)alloc;
//进入分析正题
_lasterror = ParserError::LoadingFile;
if (wcscpy_s(_filename, filename))
{
_lasterror = ParserError::InvalidFileName;
return;
}
_lasterror = ParsePE();
//如果出错的话,清理相关资源
switch (_lasterror)
{
case ParserError::InvalidPE:
UnmapViewOfFile(hmap);
mapping = NULL;
case ParserError::MapViewOfFileError:
if (hmap) CloseHandle(hmap);
case ParserError::FileMappingError:
CloseHandle(hfile);
::memset(&peinfo, 0, sizeof(PEInfo));
break;
case ParserError::OpenFileError:
break;
case ParserError::TooManyImportDlls:
case ParserError::TooManyImportFunctions:
EnableIATEncrypt = FALSE;
break;
default:
break;
}
//解析完毕后创建一个节赋值
peinfo.WingSection = new IMAGE_SECTION_HEADER{};
peinfo.WingSection->VirtualAddress = peinfo.AnalysisInfo.MinAvailableVirtualAddress;
alloc = AllocReadWriteMem(PageSize * pagecount);
if (!alloc)
{
_lasterror = ParserError::CannotAllocMemory;
return;
}
memset(alloc, 0, PageSize * pagecount);
peinfo.WingSecitonBuffer = alloc;
auto filesize = peinfo.FileSize.QuadPart;
alloc = AllocReadWriteMem(filesize);
if (!alloc)
{
_lasterror = ParserError::CannotAllocMemory;
return;
}
packedPE = alloc;
memcpy_s(packedPE, filesize, mapping, filesize);
}
结果上来一看,一堆乱七八糟的代码,什么嘛,猛地一看也没有啥解析PE
相关的函数。在解析PE
前,我们要做一些初始化操作,比如里面调用分配内存相关的函数。并且你解析的PE
不一定是合法的,难免会有错误,最后需要一个返回值表示出错原因(当然最好是加一个异常处理try-catch
,当然我懒就没有加,如果以后做这方面的工作,一定要加上)。
其他细节请自行参考我的代码,因为这些代码都已成体系,拆解起来比较麻烦,我们就介绍我们本节比较重要的函数ParsePE
。
如下是其代码:
//
// GNU AFFERO GENERAL PUBLIC LICENSE
//Version 3, 19 November 2007
//
//Copyright(C) 2007 Free Software Foundation, Inc.
//Everyone is permitted to copyand distribute verbatim copies
//of this license document, but changing it is not allowed.
// Author : WingSummer (寂静的羽夏)
//
//Warning: You can not use it for any commerical use,except you get
// my AUTHORIZED FORM ME!This project is used for tutorial to teach
// the beginners what is the PE structure and how the packer of the PE files works.
//
// 注意:你不能将该项目用于任何商业用途,除非你获得了我的授权!该项目用来
// 教初学者什么是 PE 结构和 PE 文件加壳程序是如何工作的。
//
ParserError CWingProtect::ParsePE()
{
if (!PathFileExists(_filename))
{
return ParserError::FileNotFound;
}
if (PathIsDirectory(_filename))
{
return ParserError::InvalidFile;
}
hfile = CreateFile(_filename, FILE_READ_ACCESS, FILE_SHARE_WRITE | FILE_SHARE_READ,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hfile == INVALID_HANDLE_VALUE)
{
return ParserError::OpenFileError;
}
GetFileSizeEx(hfile, &peinfo.FileSize);
hmap = CreateFileMapping(hfile, NULL, PAGE_READONLY, NULL, NULL, NULL);
if (!hmap)
{
return ParserError::FileMappingError;
}
mapping = MapViewOfFile(hmap, FILE_MAP_READ, 0, 0, 0);
if (!mapping)
{
return ParserError::MapViewOfFileError;
}
auto dosHeader = (PIMAGE_DOS_HEADER)mapping;
if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE)
{
return ParserError::InvalidPE;
}
peinfo.ntHeaderOffset = dosHeader->e_lfanew;
auto ntHeader = (PIMAGE_NT_HEADERS)OFFSET(mapping, dosHeader->e_lfanew);
if (ntHeader->Signature != IMAGE_NT_SIGNATURE)
{
return ParserError::InvalidPE;
}
auto bits = *(WORD*)OFFSET(ntHeader, sizeof(DWORD) + IMAGE_SIZEOF_FILE_HEADER);
switch (bits)
{
case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
return Parse32(ntHeader);
case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
return Parse64(ntHeader);
default:
return ParserError::InvalidPE;
}
return ParserError::Success;
}
直到这行代码开始,之前都是进行校验和加载文件到内存的操作:
auto dosHeader = (PIMAGE_DOS_HEADER)mapping;
拿到一个PE
文件,首先就要检验它的合法性,比如上来这几行代码:
if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE)
{
return ParserError::InvalidPE;
}
peinfo.ntHeaderOffset = dosHeader->e_lfanew;
auto ntHeader = (PIMAGE_NT_HEADERS)OFFSET(mapping, dosHeader->e_lfanew);
if (ntHeader->Signature != IMAGE_NT_SIGNATURE)
{
return ParserError::InvalidPE;
}
初步校验PE
文件合法之后,我们就开始解析文件了,由于PE
文件有32位和64位的,我们需要通过一个标识进行获取,如下代码所示:
auto bits = *(WORD*)OFFSET(ntHeader, sizeof(DWORD) + IMAGE_SIZEOF_FILE_HEADER);
switch (bits)
{
case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
return Parse32(ntHeader);
case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
return Parse64(ntHeader);
default:
return ParserError::InvalidPE;
}
这个代码非指针的初学者可能做不出来,这个就是获取IMAGE_OPTIONAL_HEADER
的Magic
成员,由于IMAGE_FILE_HEADER
大小是固定的,再加上Signature
成员的大小,就是我们所谓判定位数的成员。如果是32位,就调用Parse32
;如果是64位的,就调用Parse64
,否则就是非法文件。
Parse32
和Parse64
代码几乎是一样的,所以我只以Parse64
为例开始介绍,如下是该函数的代码:
ParserError CWingProtect::Parse32(PIMAGE_NT_HEADERS ntHeader)
{
is64bit = FALSE;
auto nt = (PIMAGE_NT_HEADERS32)ntHeader;
peinfo.PNumberOfSections = (INT3264)&nt->FileHeader.NumberOfSections;
auto f = nt->FileHeader;
peinfo.NumberOfSections = f.NumberOfSections;
peinfo.PAddressOfEntryPoint = (INT3264)&nt->OptionalHeader.AddressOfEntryPoint;
peinfo.PSizeOfImage = (INT3264)&nt->OptionalHeader.SizeOfImage;
peinfo.PSizeOfHeaders = (INT3264)&nt->OptionalHeader.SizeOfHeaders;
auto o = nt->OptionalHeader;
peinfo.AddressOfEntryPoint = o.AddressOfEntryPoint;
peinfo.Subsystem = o.Subsystem;
if (o.Subsystem == 1)
return ParserError::DriverUnsupport;
peinfo.FileAlignment = o.FileAlignment;
peinfo.SectionAlignment = o.SectionAlignment;
peinfo.ImageBase = o.ImageBase;
peinfo.SizeOfImage = o.SizeOfImage;
peinfo.SizeOfHeaders = o.SizeOfHeaders;
peinfo.POptionalHeaderDllCharacteristics = &nt->OptionalHeader.DllCharacteristics;
return ParserDir(ntHeader);
}
这个函数只是在收集一些必要的PE
文件信息,方便我们加密使用,最后调用了比较关键的函数ParserDir
,下面是其代码:
//
// GNU AFFERO GENERAL PUBLIC LICENSE
//Version 3, 19 November 2007
//
//Copyright(C) 2007 Free Software Foundation, Inc.
//Everyone is permitted to copyand distribute verbatim copies
//of this license document, but changing it is not allowed.
// Author : WingSummer (寂静的羽夏)
//
//Warning: You can not use it for any commerical use,except you get
// my AUTHORIZED FORM ME!This project is used for tutorial to teach
// the beginners what is the PE structure and how the packer of the PE files works.
//
// 注意:你不能将该项目用于任何商业用途,除非你获得了我的授权!该项目用来
// 教初学者什么是 PE 结构和 PE 文件加壳程序是如何工作的。
//
ParserError CWingProtect::ParserDir(PIMAGE_NT_HEADERS ntHeader)
{
auto s = IMAGE_FIRST_SECTION(ntHeader);
peinfo.PSectionHeaders = s;
auto dd = (INT3264)s - (IMAGE_NUMBEROF_DIRECTORY_ENTRIES * sizeof(IMAGE_DATA_DIRECTORY));
peinfo.PDataDirectory = (PIMAGE_DATA_DIRECTORY)dd;
auto pdtls = peinfo.PDataDirectory[IMAGE_DIRECTORY_ENTRY_TLS];
if (pdtls.VirtualAddress && pdtls.Size)
HasTLS = TRUE;
auto pos = peinfo.AddressOfEntryPoint;
auto pis = encryptInfo.OldImportDataAddr = peinfo.PDataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
DWORD maxVirual = 0;
DWORD sizeraw = 0;
for (UINT i = 0; i < peinfo.NumberOfSections; i++)
{
auto psh = s[i];
if (psh.VirtualAddress <= pos && pos <= psh.VirtualAddress + psh.SizeOfRawData)
{
peinfo.PCodeSection = &s[i];
}
if (psh.VirtualAddress <= pis && pis <= psh.VirtualAddress + psh.SizeOfRawData)
{
peinfo.PImportSection = &s[i];
}
if (psh.VirtualAddress > maxVirual)
{
maxVirual = psh.VirtualAddress;
sizeraw = psh.SizeOfRawData;
}
}
auto d = div((INT3264)(maxVirual + sizeraw), peinfo.SectionAlignment);
peinfo.AnalysisInfo.MinAvailableVirtualAddress = (UINT)(GetBiggerQuot(d) * peinfo.SectionAlignment);
peinfo.AnalysisInfo.SectionsCanAddCount = (peinfo.SizeOfHeaders - (UINT)GETOFFSET(mapping, s)) / IMAGE_SIZEOF_SECTION_HEADER - peinfo.NumberOfSections - 1;
auto pdd = (PIMAGE_DATA_DIRECTORY)dd;
auto idd = pdd[IMAGE_DIRECTORY_ENTRY_IMPORT];
auto pidd = (PIMAGE_IMPORT_DESCRIPTOR)GetPointerByRVA(mapping, idd.VirtualAddress);
peinfo.PImportDescriptor = pidd;
IMAGE_IMPORT_DESCRIPTOR iid = pidd[0];
auto pdll = peinfo.AnalysisInfo.ImportDllName;
auto pfunhint = peinfo.AnalysisInfo.ImportFunNameTable;
auto pdiat = peinfo.AnalysisInfo.DllFirstThunks;
if (is64bit)
{
int i = 0, ii = 0, itotal = 0;
if (itotal >= MAXImportFunHintCount)
return ParserError::TooManyImportFunctions;
if (i >= MAXDllNameCount)
return ParserError::TooManyImportDlls;
while (iid.Characteristics)
{
pdll[i].Name = iid.Name;
pdiat[i] = iid.FirstThunk;
PIMAGE_THUNK_DATA64 pitd32 =
(PIMAGE_THUNK_DATA64)GetPointerByRVA(mapping, iid.FirstThunk);
IMAGE_THUNK_DATA64 itd32 = pitd32[0];
PIMAGE_IMPORT_BY_NAME iibn;
ii = 0;
while (itd32.u1.AddressOfData)
{
if (!IMAGE_SNAP_BY_ORDINAL64(itd32.u1.AddressOfData))
{
iibn = (PIMAGE_IMPORT_BY_NAME)GetPointerByRVA(mapping, itd32.u1.AddressOfData);
auto c = (char*)&iibn->Name;
auto le = strlen(c);
strcpy_s(pfunhint, PageSize, c); //偷懒写法
pfunhint += (le + 1);
}
ii++;
itd32 = pitd32[ii];
}
pdll[i].FunCount = ii;
itotal += ii;
i++;
iid = pidd[i];
}
peinfo.AnalysisInfo.ImportDllCount = i;
peinfo.AnalysisInfo.ImportFunCount = itotal;
peinfo.AnalysisInfo.PointerofImportFunNameTable =
(UINT)GETOFFSET(peinfo.AnalysisInfo.ImportFunNameTable, pfunhint);
}
else
{
int i = 0, ii = 0, itotal = 0;
while (iid.Characteristics)
{
pdll[i].Name = iid.Name;
pdiat[i] = iid.FirstThunk;
PIMAGE_THUNK_DATA32 pitd32 =
(PIMAGE_THUNK_DATA32)GetPointerByRVA(mapping, iid.FirstThunk);
IMAGE_THUNK_DATA32 itd32 = pitd32[0];
PIMAGE_IMPORT_BY_NAME iibn;
ii = 0;
while (itd32.u1.AddressOfData)
{
if (!IMAGE_SNAP_BY_ORDINAL64(itd32.u1.AddressOfData))
{
iibn = (PIMAGE_IMPORT_BY_NAME)GetPointerByRVA(mapping, itd32.u1.AddressOfData);
auto c = (char*)&iibn->Name;
auto le = strlen(c);
strcpy_s(pfunhint, PageSize, c); //偷懒写法
pfunhint += (le + 1);
}
ii++;
itd32 = pitd32[ii];
}
pdll[i].FunCount = ii;
itotal += ii;
i++;
iid = pidd[i];
}
peinfo.AnalysisInfo.ImportDllCount = i;
peinfo.AnalysisInfo.ImportFunCount = itotal;
peinfo.AnalysisInfo.PointerofImportFunNameTable =
(UINT)GETOFFSET(peinfo.AnalysisInfo.ImportFunNameTable, pfunhint);
}
return ParserError::Success;
}
上面的代码对执行的代码区进行确认和定位,记录做后续的加密,于此同时我们记录了必要的数据,接下来就是解析导入表,我们以64位为例进行介绍:
int i = 0, ii = 0, itotal = 0;
while (iid.Characteristics)
{
pdll[i].Name = iid.Name;
pdiat[i] = iid.FirstThunk;
PIMAGE_THUNK_DATA32 pitd32 =
(PIMAGE_THUNK_DATA32)GetPointerByRVA(mapping, iid.FirstThunk);
IMAGE_THUNK_DATA32 itd32 = pitd32[0];
PIMAGE_IMPORT_BY_NAME iibn;
ii = 0;
while (itd32.u1.AddressOfData)
{
if (!IMAGE_SNAP_BY_ORDINAL64(itd32.u1.AddressOfData))
{
iibn = (PIMAGE_IMPORT_BY_NAME)GetPointerByRVA(mapping, itd32.u1.AddressOfData);
auto c = (char*)&iibn->Name;
auto le = strlen(c);
strcpy_s(pfunhint, PageSize, c); //偷懒写法
pfunhint += (le + 1);
}
ii++;
itd32 = pitd32[ii];
}
pdll[i].FunCount = ii;
itotal += ii;
i++;
iid = pidd[i];
}
peinfo.AnalysisInfo.ImportDllCount = i;
peinfo.AnalysisInfo.ImportFunCount = itotal;
peinfo.AnalysisInfo.PointerofImportFunNameTable =
(UINT)GETOFFSET(peinfo.AnalysisInfo.ImportFunNameTable, pfunhint);
while (iid.Characteristics)
和while (itd32.u1.AddressOfData)
这个几个代码就是判断是否为空,为空就中断循环。pdll
是导入名称不定长字符串数组,pfunhint
是导入函数名称不定长字符串数组,pdiat
是每一个IMAGE_IMPORT_DESCRIPTOR
中的FirstThunk
拷贝来作为的数组,而这些都是我进行IAT
加密所需要的数据,这到后面的文章进行介绍,这里就不多说了。
经历过一系列的解析,如果成功,就会执行return ParserError::Success;
表示解析成功,构造函数继续执行后续加密所需资源的申请和处理,这些并不是本篇博文的关注点,就不赘述了。
下一篇
羽夏壳世界—— PE 解析的实现的更多相关文章
- 羽夏壳世界—— PE 结构(上)
羽夏壳世界之 PE 结构(上),介绍难度较低的基本 PE 相关结构体.
- 跟羽夏学 Ghidra ——窗口
写在前面 此系列是本人一个字一个字码出来的,包括示例和实验截图.本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正. 如有好的建议,欢迎反馈.码字不易,如果本篇文章 ...
- (三)羽夏看C语言——进制
写在前面 由于此系列是本人一个字一个字码出来的,包括示例和实验截图.本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正. 如有好的建议,欢迎反馈.码字不易,如果本篇 ...
- 羽夏逆向指引—— Hook
写在前面 此系列是本人一个字一个字码出来的,包括示例和实验截图.可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新. 如有好的建议,欢迎反馈.码字不易,如果本篇文章有帮助你的, ...
- 羽夏看Linux内核——段相关入门知识
写在前面 此系列是本人一个字一个字码出来的,包括示例和实验截图.如有好的建议,欢迎反馈.码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作.如想转载,请把我的转载信息附在文章后面,并 ...
- 羽夏看Linux内核——中断与分页相关入门知识
写在前面 此系列是本人一个字一个字码出来的,包括示例和实验截图.如有好的建议,欢迎反馈.码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作.如想转载,请把我的转载信息附在文章后面,并 ...
- 羽夏看Linux内核——引导启动(上)
写在前面 此系列是本人一个字一个字码出来的,包括示例和实验截图.如有好的建议,欢迎反馈.码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作.如想转载,请把我的转载信息附在文章后面,并 ...
- 羽夏看Linux内核——引导启动(下)
写在前面 此系列是本人一个字一个字码出来的,包括示例和实验截图.如有好的建议,欢迎反馈.码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作.如想转载,请把我的转载信息附在文章后面,并 ...
- 跟羽夏学 Ghidra ——工具
写在前面 此系列是本人一个字一个字码出来的,包括示例和实验截图.本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正. 如有好的建议,欢迎反馈.码字不易,如果本篇文章 ...
- PE解析器的编写(四)——数据目录表的解析
在PE结构中最重要的就是区块表和数据目录表,上节已经说明了如何解析区块表,下面就是数据目录表,在数据目录表中一般只关心导入表,导出表和资源这几个部分,但是资源实在是太复杂了,而且在一般的病毒木马中也不 ...
随机推荐
- elasticsearch 增删查改
#分词验证 POST _analyze { "analyzer":"ik_max_word", "text":"elasticse ...
- Fastjson反序列化分析
依赖 先研究1.2.24版本的,版本高了就有waf了,不过也能绕,高版本以后再说 <dependency> <groupId>com.alibaba</groupId&g ...
- LCD - 液晶显示原理(一)
1. 显示器介绍 显示器属于计算机的I/O设备,即输入输出设备.它是一种将特定电子信息输出到屏幕上再反射到人眼的显示工具.常见的有CRT显示器.液晶显示器. LED点阵显示器及OLED显示器. 液 ...
- Java面试题【2】
11)abstract class 和 interface 有什么区别? 含有 abstract 修饰符的 class 即为抽象类,abstract 类不能创建的实例对象.含有 abstract 方法 ...
- 5 CSS伪类选择器
5 伪类选择器 anchor伪类:专用于控制链接的显示效果 More Actions:link a:link 选择所有未被访问的链接. :visited a:visited 选择所有已被访问的链接. ...
- #单调栈,树状数组#CF1635F Closest Pair
题目 设 \(f(x,y)=|a_x-a_y|*(w_x+w_y)\),其中 \(a\) 单调递增 多组询问求 \(\min_{l\leq l'<r'\leq r}\{f(l',r')\}\) ...
- #并查集,线性筛#nssl 1470 X
分析 显然答案就是\(2^{连通块个数}-2\), 将每个数的质数所在的集合合并, 最后判断连通块个数即可(线性筛少了个等号改了半天QWQ) 代码 #include <cstdio> #i ...
- 设置 BCompare 打开文件时的默认字符编码
每次比对 .h .cpp 文件,BCompare总是默认以西欧字符编码打开文件,导致中文都变成乱码了,还需要手动的修改文件字符编码,这样才能正常的显示.非常的不方便 然后我们就需要设置默认的字符编码, ...
- climits 与 符号常量
climits 在老式中是 limits.h 一.引入 #include <climits> 或者 #include <limits.h> 二.符号常量 符号常量 表示 CH ...
- HarmonyOS 设备管理开发:USB 服务开发指导
基本概念 USB服务是应用访问底层的一种设备抽象概念.开发者根据提供的USB API,可以获取设备列表.控制设备访问权限.以及与连接的设备进行数据传输.控制命令传输等. 运作机制 USB服务系统包 ...