【逆向工程】QT编写PE结构分析工具 附源码

最近连肝几日,把PE结构学了点毛皮,看了下看雪《解密解密有》有一小段是编写PE结构的分析工具,就有点手痒痒了,也希望加深对PE格式的理解和掌握。
因为本人堪堪大二,才疏学浅,写起来也是磕磕碰碰的,不过收获还是蛮多的。
winn.h中定义了PE结构的数据类型,windows.h头文件中已经引入了winn.h,

最终效果 未完成 最近做了几个实验 碰到了许多疑惑 等捋清楚了再接着写 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文件的读和修改

源码链接

白菜信安网

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘乙兵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值