最近连肝几日,把PE结构学了点毛皮,看了下看雪《解密解密有》有一小段是编写PE结构的分析工具,就有点手痒痒了,也希望加深对PE格式的理解和掌握。
因为本人堪堪大二,才疏学浅,写起来也是磕磕碰碰的,不过收获还是蛮多的。
winn.h中定义了PE结构的数据类型,windows.h头文件中已经引入了winn.h,
PE结构分析工具
最终效果 未完成 最近做了几个实验 碰到了许多疑惑 等捋清楚了再接着写 7.03
涉及知识
PE文件结构
Win32API
C++ QT编程
程序结构
FilePE类用来判断 加载 卸载 PE文件
GetPE类用来获取PE文件详细内容
MainWindow接收信号 绑定槽 调用各类静态方法 显示数据 实现如日志记录 区块表详情展示等功能
文件的加载
创建FilePE类来实现文件的加载,将数据保存在如下静态类方法中
public:
static QString fileName; //文件路径
static HANDLE hFile; //文件句柄
static HANDLE hMapping; //映像文件句柄
static LPVOID ImageBase; //基地址
//一定要初始化若不然会报undefined reference to 错误
QString MainWindow:: fileName="";
HANDLE MainWindow:: hFile=NULL;
HANDLE MainWindow:: hMapping=NULL;
LPVOID MainWindow::ImageBase=NULL;
获取文件路径
这里借鉴其他PE查询工具,有两种获取文件路径方式
1)用户通过文件对话框选择文件
2)用户直接拖拽文件
用户选择获取路径
这里用了QFileDialog类的getOpenFileName来获取文件的路径
fileName=QFileDialog::getOpenFileName(this,"选择文件","C:/");
用户拖拽获取路径
这里用到了QT中的QDrag类,其继承关系如下图
QDragEnterEvent:拖动进入事件。当拖动操作进入部件时,该事件被发送到部件,忽略该事件,将会导至后续的拖放事件不能被发送,此时在该部件上光标通常会在外观上显示为禁用的图形。
QDragMoveEvnet:拖动移动事件。当拖动操作正在进行时,以及当具有焦点时按下键盘的修饰键(比如Ctrl)时,发送该事件,要使部件能接收到该事件,则该部件必须接受QDragEnterEvent事件。
QDropEvent:放下事件。在完成拖放操作时发送该事件,即当用户在部件上放下一个对象时,发送此事件。要使部件能接收到该事件,则该部件必须接受QDragEnterEvent:事件,且不能忽略QDragMoveEvnt事件。
QDragLeaveEvent:当拖放操作离开部件时发送该事件,注意:要使部件能接收到该事件,必须要使拖动先进入该部件(即产生QDragEnterEvent事件),然后再离开该部件,才会产生QDragLeaveEvent事件。因很少使用该事件,因此本文不做重点介绍。
必须接受是指必须重新实现该事件的处理函数并接受该事件,不能忽略是指在处件事理函数中不明确调用ignore()函数忽略该事件。
//用户拖拽获取文件路径
protected:
void dragEnterEvent(QDragEnterEvent* e); //鼠标进入 用于筛选拖拽事件
void dropEvent(QDropEvent* e); //鼠标放下 用于处理拖拽事件
this->setAcceptDrops(true); //必须使部件接受放置事件
void MainWindow::dragEnterEvent(QDragEnterEvent* e)
{
//对拖放事件进行筛选 QMimeData类提为数据提供一个容器,用来记录关于MIME类型数据的信息
if (true)
{
e->acceptProposedAction(); //必须使部件接受放置事件
}
}
void MainWindow::dropEvent(QDropEvent* e)
{
//获取文件路径 (QString)
QList<QUrl> urls = e->mimeData()->urls();
if (urls.isEmpty())
{
return;
}
fileName = urls.first().toLocalFile();
}
补充一下,这里如果是选着快捷方式将会无法解析,所以需要将快捷方式转换成所指向的文件路径,找了三天WIN32API的轮子硬是没找到,好在天无绝人之路,无意间发现QT有个QFileInFo的一个成员方法可以实现此功能,给我激动的。。。。。。 参考资料附在文章末
//判断是否是快捷方式
if(fileName.split(".")[1]=="lnk")
{
QFileInfo fileLnk(fileName);
fileName=fileLnk.symLinkTarget();//返回快捷方式的绝对路径
}
LoadPE ,UnLoadePE,IsPE
在FilePEL类下创建LoadFile方法用来加载文件 ,UnLoadePE方法卸载文件,IsPE判断是否是PE文件 通过如下 API获取文件句柄 映射文件句柄 基地址句柄 其原型如下
HANDLE CreateFile(
LPCTSTR,lpFileName, //指向文件名的指针
DWORD dwDesiredAccess, //访问模式(读/写)
DWORD dwShareMode, //共享模式
LPSECURITY_ATTRIBUTES lpSecurityAttributes,//指向安全属性的指针
DWORD dwCreationDisposition, //如何让创建
DWORD dwFlagAndAttributes, //文件属性
HANDLE hTemplateFile //用于复制文件句柄
);
HANDLE CreateFileMapping(
HANDLE hFile, //物理文件句柄
LPSECURITY_ATTRIBUTES lpAttributes, //安全设置
DWORD flProtect, //保护设置
DWORD dwMaximumSizeHigh, //高位文件大小
DWORD dwMaximumSizeLow, //低位文件大小
LPCTSTR lpName //共享内存名称
);
LPVOID MapViewOfFile(
HANDLE hFileMappingObject, //文件映射句柄
DWORD dwDesiredAccess, //访问模式
DWORD dwFileOffsetHigh, //高32位地址
DWORD dwFileOffsetLow, //低32位地址
SIZE_T dwNumberOfBytesToMap //指定文件映射对象基址
);
LoadePE方法
加载PE文件
bool FilePE::LoadPE()
{
//判断传递进来的文件路径是否为空
if(fileName.isEmpty()==TRUE)
{
return false;
}
//判断是否是快捷方式
if(fileName.split(".")[1]=="lnk")
{
QFileInfo fileLnk(fileName);
fileName=fileLnk.symLinkTarget();//返回快捷方式的绝对路径
}
LPCWSTR fileNameResult = reinterpret_cast<LPCWSTR>(fileName.data()); //将其转化为LPCWSTR类型
//获取文件句柄
hFile=CreateFile(fileNameResult,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if(hFile==INVALID_HANDLE_VALUE)
{
return false;
}
//创建文件映射对象 获取映射文件句柄 PAGE_READONLY 只读
hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if (NULL == hMapping)
{
CloseHandle(hFile); //关闭文件句柄
return false;
}
//在创建文件映射对象后使用可以调用MapViewOfFile函数映射到本进程的地址空间内。
//将文件数据映射到进程的地址空间 获取镜像基址
ImageBase = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);//FILE_MAP_READ 映射只读。文件映射对象必须通过PAGE_READ 或 PAGE_READWRITE访问创建
if (NULL == ImageBase)
{
CloseHandle(hFile);
CloseHandle(hMapping);
return false;
}
return true;
}
UnLoadePE方法
每次加载新文件时 需卸载前一个文件
bool FilePE::UnLoadPE()
{
if(ImageBase!=NULL)
{
UnmapViewOfFile(ImageBase);
}
if(hMapping!=NULL)
{
CloseHandle(hMapping);
}
if(hFile!=NULL)
{
CloseHandle(hFile);
}
hFile=NULL;
hMapping=NULL;
ImageBase=NULL;
LoadPEKey=false;
return true;
}
bool FilePE::IsPE()
{
if(ImageBase==NULL)
{
return false;
}
pDH=(PIMAGE_DOS_HEADER)ImageBase;
//判断signatur字段是否为IMAGE_DOS_SIGNATURE 即0x5A4D
if(pDH->e_magic!=IMAGE_DOS_SIGNATURE)
{
return false;
}
pNtH=(PIMAGE_NT_HEADERS)((DWORD)pDH+pDH->e_lfanew);
if(pNtH->Signature != IMAGE_NT_SIGNATURE)//判断是否是0x00004550
{
//重置文件名
fileName="";
return false;
}
//置为开始
LoadPEKey=true;
return true;
}
IsPEFile方法
IsPEFile类判断是否为PE文件
bool FilePE::IsPE()
{
if(ImageBase==NULL)
{
return false;
}
pDH=(PIMAGE_DOS_HEADER)ImageBase;
//判断signatur字段是否为IMAGE_DOS_SIGNATURE 即0x5A4D
if(pDH->e_magic!=IMAGE_DOS_SIGNATURE)
{
return false;
}
pNtH=(PIMAGE_NT_HEADERS)((DWORD)pDH+pDH->e_lfanew);
if(pNtH->Signature != IMAGE_NT_SIGNATURE)//判断是否是0x00004550
{
//重置文件名
fileName="";
return false;
}
//置为开始
LoadPEKey=true;
return true;
}
在MAinWindows中调用上述三个静态类方法即可实现文件的加载
if(!FilePE::UnLoadPE())
return;
if(!FilePE::LoadPE())
{
ui->fileNameEdit->setText("文件加载失败!");
return;
}
if(!FilePE::IsPE())
{
ui->fileNameEdit->setText("不是PE格式文件!");
return;
}
获取PE结构详情
创建GetPE类来获取,声明了如下静态成员变量
public:
static QString NumberOfSectionStr;
static QString MajorLinkerVersionStr;//主连接器版本
static QString MinorLinkerVersionStr;//次连接器版本;
static QString SizeOfImageStr;
static QString EntryPointStr;
static QString ImageBaseStr;
static QString SubSystemStr;
static QVector<QString> NameStr;
static QVector<QString> VirtualSizeStr;
static QVector<QString> VirtualAddressStr;
static QVector<QString> SizeOfRawDataStr;
static QVector<QString> PointerToRawDataStr;
static QVector<QString> CharacteristicsStr;
static QVector<QString> DataDirectoryArray;
获取PEHeader和OptionalHeader
这里在GetPE类下创建了GetPEHeader方法来获取
bool GetPE::GetPEHeader()
{
pFH=&pNtH->FileHeader;
pOH=&pNtH->OptionalHeader;
WORD NumberOfSection=pFH->NumberOfSections; //区块数
DWORD AddressOfEntryPoint=pOH->AddressOfEntryPoint; //程序入口RVA
DWORD ImageBase=pOH->ImageBase; //镜像基址
DWORD SizeOfImage=pOH->SizeOfImage; //载入后的总尺寸
DWORD EntryPoint=AddressOfEntryPoint+ImageBase;//程序入口
WORD SubSystem=pOH->Subsystem;
switch(SubSystem)
{
case 0:SubSystemStr="未知";break;
case 1:SubSystemStr="无子系统";break;
case 2:SubSystemStr="Windows GUI";break;
case 3:SubSystemStr="Windows Console";break;
case 5:SubSystemStr="Windows OS";break;
case 7:SubSystemStr="Windows POSIX";break;
case 9:SubSystemStr="Windows CE";break;
default: SubSystemStr="";
}
NumberOfSectionStr=QString::number(NumberOfSection,16);
EntryPointStr=QString::number(EntryPoint,16);
SizeOfImageStr=QString::number(SizeOfImage,16);//转化为十六进制表现形式的字符串
ImageBaseStr=QString::number(ImageBase,16);
return true;
}
}
获取区块表
紧跟IMAGE_NT_HEADER的就是区块表(Section Table),区块表是有IMAGE_SECTION_组成的一个结构体数组,VC++中以定义了宏IMAGE_FIRST_SECTION可以直接获取第一个区块表的指针,又因为在IMAGE_FILE_HEADER中的NumberOfSection字段获取到了区块表的个数,所以一个循环遍历即可获取区块表的全部信息
bool GetPE::GetSectionHeader()
{
DWORD NumberOfSections=pFH->NumberOfSections;
pSH=IMAGE_FIRST_SECTION(pNtH);//IMAGE_FIEST_SECTION 宏 可以获取第一个区块表的位置
CHAR Name[9];
for (DWORD i=0 ;i<NumberOfSections;i++) {
memset(Name,0,sizeof (Name));
memcpy(Name,pSH->Name,8);
NameStr.append(QString(Name));
//格式化为8位 十六进制表现形式的字符串
VirtualSizeStr.append(QString("%1").arg(pSH->Misc.VirtualSize,8,16,QLatin1Char('0')));
VirtualAddressStr.append(QString("%1").arg(pSH->VirtualAddress,8,16,QLatin1Char('0')));
SizeOfRawDataStr.append(QString("%1").arg(pSH->SizeOfRawData,8,16,QLatin1Char('0')));
PointerToRawDataStr.append(QString("%1").arg(pSH->PointerToRawData,8,16,QLatin1Char('0')));
CharacteristicsStr.append(QString("%1").arg(pSH->Characteristics,8,16,QLatin1Char('0')));
qDebug()<<pSH->VirtualAddress;
++pSH;//这里直接++便可以获取下一个区块表的指针
}
return true;
}
辅助功能
这里又加了亿点点细节和小功能,小细节就不多说了,主要说一下小功能
日志记录
第一项是日志记录功能,实现了记录每次解析的PE文件时间和路径,并且实现了双击快捷加载文件
这里将每次加载文件的时间和路径写入到log文件中,查看日志时再读取
void MainWindow::MakeLog()
{
//获取日期
QDate date=QDate::currentDate();
QDateTime dateTime=QDateTime::currentDateTime();
QDir tempDir;//获取当前文件路径
QString currentDir=tempDir.currentPath();
if(!tempDir.exists("logs"))//判断logs文件夹是否存在
{
tempDir.mkpath("logs");
}
QFile fileLog("logs/"+date.toString("yyyy-MM-dd")+".log");//创建文件对象
fileLog.open(QIODevice::Append|QIODevice::Text|QIODevice::ReadWrite);//以读写的方式打开,不存在则创建
fileLog.write(dateTime.toString("yyyy-MM-dd hh:mm:ss").toUtf8()+"\n"+fileName.toUtf8()+"\n");
fileLog.close();
}
void MainWindow::ReadShowLogs()
{
//您可以使用QDirIterator一次遍历一个目录
//显示所有日志文件
QStringList logArray;//保存日志文件信息
QDirIterator iter("logs",QStringList("*.log"),QDir::Files|QDir::NoSymLinks,QDirIterator::Subdirectories);
while(iter.hasNext())
{
iter.next();
logArray.append(iter.filePath().split("/")[1]);//对获取的文件名称字符串做处理
qSort(logArray.begin(),logArray.end());
}
QDialog logDia(this);//模态对话框
QListWidget logList(&logDia);//列表
QTreeWidget* logTree=new QTreeWidget(&logDia);
logTree->setHeaderLabels(QStringList()<<"时间"<<"路径");
//判断日志文件是否存在
if(logArray.length()==0)
{
logList.addItem(new QListWidgetItem(QIcon(":/icon/Image/表单定制01.png"),"抱歉,您暂无解析日志。"));
}
else
{
for(int i=0;i<logArray.length();i++)
{
logList.addItem(new QListWidgetItem(QIcon(":/icon/Image/表单定制01.png"),logArray[logArray.length()-i-1]));
}
//统计数量
logList.insertItem(0,new QListWidgetItem("日志数量:"+QString::number(logList.count())));
}
//点击选取日志文件
connect(&logList,&QListWidget::itemDoubleClicked,[=](QListWidgetItem* item){
QFile fileLog("logs/"+item->text());
if(fileLog.open(QIODevice::Text|QIODevice::ReadOnly))
{
QStringList contentList;//保存读取日志内容
while(1)
{
int i=0;
QByteArray logList=fileLog.readLine(i++);//每次读一行
if(logList.isEmpty())break;
contentList.append(QString::fromUtf8(logList).remove("\n")) ;//转化为utf8编码)
}
logTree->clear();//每次显示前清除
for (int i=0;i<contentList.length();) {
logTree->addTopLevelItem( new QTreeWidgetItem(QStringList()<<contentList[contentList.length()-i-2]<<contentList[contentList.length()-i-1]));
i+=2;
}
}
fileLog.close();
});
//点击日志路径 便捷加载
connect(logTree,&QTreeWidget::itemDoubleClicked,[=](QTreeWidgetItem *item){
try {
//每次调用前先卸载 若有异常立即停止
UnLoadPE unLoadPe;
fileName = item->text(1);
LoadPE loadPe(this);
IsPEFile isPEFile(this);
this->MakeLog();
ShowPE showPe(this);
} catch (int e) {
}
});
logList.setIconSize(QSize(30,30));
logList.move(10,10);
logList.setFixedSize(200,332);
logTree->setFixedSize(599,332);
logTree->move(220,10);
logList.setStyleSheet("QListWidget{border:2px solid #00aa88;}");
logTree->setStyleSheet("QTreeWidget{border:2px solid #00aa88;}");
logDia.setFixedSize(829,352);
logDia.setWindowTitle("日志");
logDia.move(300,300);
logDia.exec();
}
设置
第二项就是设置,才实现了个别功能,是将配置信息以JSON文件的形式保存起来,每次启动时在进行读取。
QT提供了如下类来操作JSON数据
#include <QJsonDocument>//json文本操作
#include <QJsonObject>//json对象
#include <QJsonParseError>//json错误信息
#include <QJsonArray>//json数组
这里展示下主要的代码,要注意坑的是虽然用QIODevice::ReadWrite方式来读写文件也能进行覆盖重写,但当户重复快速的读写时,会导致上次操作的数据还未写完,就再次写入,就会导致格式出错。所以这里用QIODevice::Truncate方法 以重写的方式打开
QJsonObject setJsonObj;//设置Json
QJsonObject setHelpJson;//使用QjsonObject对象插入键值对
QJsonObject setStyleJson;
QJsonArray setJsonArray;//json数组
QFile setFile("set/set.json");
setFile.open(QIODevice::Text|QIODevice::ReadWrite|QIODevice::Truncate);//QIODevice::Truncate 以重写的方式打开
setHelpJson["Log"]=LogKey;//这几个静态变量都是提前定义好的bool型或字符型数据
setHelpJson["Up"]=UpKey;
setHelpJson["Windowz"]=WindowzKey;
setStyleJson["FontColor"]=fontColor;
setStyleJson["BgColor"]=bgColor;
setStyleJson["GlobalStatus"]=globalStatus;
setJsonArray.append(setHelpJson);
setJsonArray.append(setStyleJson);
setJsonObj["PEGAME"]=setJsonArray;
QJsonDocument setJsonDoc(setJsonObj);//使用QjsonDocument设置该对象为json文档
setFile.seek(0);/*从头开始写*/
setFile.write(setJsonDoc.toJson()); //将Json以文本形式写入文件
setFile.flush();//刷新缓冲区
return true;
实现了在set.json中保存如下json格式数据
{
"PEGAME": [
{
"Log": true,
"Up": true,
"Windowz": false
},
{
"BgColor": "",
"FontColor": "",
"GlobalStatus": false
}
]
}
1)将窗口始终置顶
如下代码可实现窗口始终置顶
this->setWindowFlags(this->windowFlags() | Qt::WindowStaysOnTopHint);
但这又会有个坑,可能会导致窗口直接给干没了,所以还需要加上这一行代码
this->showNormal();//应对置顶后窗口反而消失
刷新一下窗口
2)个性化设置
这里使用的是QT提供的颜色对话框
#include <QColorDialog>//颜色选择器
还有调色板
QPalette color;
if(fontColor!="" ||bgColor!="")
{
QPalette color;//立即显示 使用户可以得到及时反馈
if(bgColor!="")
{
QColor bgrgba(bgColor.split(",")[0].toUInt(),bgColor.split(",")[1].toUInt(),bgColor.split(",")[2].toUInt(),bgColor.split(",")[3].toUInt());
color.setColor(QPalette::Window,bgrgba);
}
if(fontColor!="")
{
//进行处理
QColor fontrgba(fontColor.split(",")[0].toUInt(),fontColor.split(",")[1].toUInt(),fontColor.split(",")[2].toUInt(),fontColor.split(",")[3].toUInt());
color.setColor(QPalette::WindowText,fontrgba);
}
this->setPalette(color);
}
else
{
QPalette color;
this->setPalette(color);
}
参考资料
锻钢《加密解密 第四版》
Win32技术手册
QT官方帮助文档
PE文件解析器的编写
CreateFileMapping 、MapViewOfFile、UnmapViewOfFile函数用法及示例
拖放基本原理(QDrag类)
拖拽文件并获取其路径
Qt的QFileInfo
QT Json文件的读和修改