逆向-C++判定PE文件

思路分析

经过前面的介绍,我们对PE文件结构已经有了大致了解,与其他文件的不同是DOS部首和PE文件头的内容,我们可以检测DOS_HEADER中的e_magic魔术字字段是否为“MZ”来判断是否为PE文件,但这还不够,如果有其他文件的起始字符也是“MZ”不就造成误判了么,所以我们还需验证PE文件头中的SINGATURE是否为“PE00",两者都满足,我们才认为该文件是PE文件。

大致思路就是解析PE文件,根据其中的关键字来判断是否是PE文件,如果需要,我们还可以通过PE文件头中IMAGE_FILE_HEADER内的characteristics来识别该PE文件是exe还是dll,这部分思路较简单。根据PE文件的基本概念,想要获得解析PE文件,首先要获得该PE文件在内存中的基地址,这是不是代表我们只能解析正在运行的PE文件呢?我们又如何获得PE文件的基地址呢?这才是本文要着重学习的难点。

函数使用

获取基地址

要完成获取基地址的目的,我们需要MapViewOfFile这个API,该API用于将一个文件映射对象映射到当前应用程序的地址空间并返回映射的地址空间,也就是将映射对象装进内存,并返回基地址。其函数定义和参数如下:

LPVOID WINAPI MapViewOfFile(
  __in HANDLE hFileMappingObject,
  __in DWORD dwDesiredAccess,
  __in DWORD dwFileOffsetHigh,
  __in DWORD dwFileOffsetLow,
  __in SIZE_T dwNumberOfBytesToMap
  );/*
参数1:hFileMappingObject 为CreateFileMapping()或OpenFileMapping()返回的文件映像对象句柄。
参数2:dwDesiredAccess 映射对象的文件数据的访问方式,而且同样要与CreateFileMapping()函数所设置的保护属性相匹配。 可取以下值:
FILE_MAP_ALL_ACCESS 等价于CreateFileMapping的 FILE_MAP_WRITE|FILE_MAP_READ. 文件映射对象被创建时必须指定PAGE_READWRITE 选项.
FILE_MAP_COPY 可以读取和写入文件.写入操作会导致系统为该页面创建一份副本.在调用CreateFileMapping时必须传入PAGE_WRITECOPY保护属性.
FILE_MAP_EXECUTE 可以将文件中的数据作为代码来执行.在调用CreateFileMapping时可以传入PAGE_EXECUTE_READWRITE或PAGE_EXECUTE_READ保护属性.
FILE_MAP_READ 可以读取文件.在调用CreateFileMapping时可以传入PAGE_READONLY或PAGE_READWRITE保护属性.
FILE_MAP_WRITE 可以读取和写入文件.在调用CreateFileMapping时必须传入PAGE_READWRITE保护属性.
参数3:dwFileOffsetHigh 表示文件映射起始偏移的高32位.
参数4:dwFileOffsetLow 表示文件映射起始偏移的低32位.(64KB对齐不是必须的)
参数5:dwNumberOfBytes 指定映射文件的字节数.*/

获取文件映像对象

根据上面API需要的参数,下面的任务就是获得文件的映像对象,该步需要CreateFileMapping API来实现,该API的作用是根据物理句柄创建一个文件映射内核对象,并返回一个文件映像句柄,其定义和参数如下:

HANDLE WINAPI CreateFileMapping(
_In_HANDLE hFile,
_In_opt_LPSECURITY_ATTRIBUTES lpAttributes,
_In_DWORD flProtect,
_In_DWORD dwMaximumSizeHigh,
_In_DWORD dwMaximumSizeLow,
_In_opt_LPCTSTR lpName);
/*
hFile:Long,指定欲在其中创建映射的一个文件句柄。0xFFFFFFFF(-1,即INVALID_HANDLE_VALUE)表示在页面文件中创建一个可共享的文件映射。
lpFileMappigAttributes:SECURITY_ATTRIBUTES,它指明返回的句柄是否可以被子进程所继承,指定一个安全对象,在创建文件映射时使用。如果为NULL(用ByVal As Long传递零),表示使用默认安全对象。
flProtect:Long,下述常数之一:
PAGE_READONLY 以只读方式打开映射
PAGE_READWRITE 以可读、可写方式打开映射
PAGE_WRITECOPY 为写操作留下备份
可组合使用下述一个或多个常数:
SEC_COMMIT 为文件映射一个小节中的所有页分配内存
SEC_IMAGE 文件是个可执行文件
SEC_RESERVE 为没有分配实际内存的一个小节保留虚拟内存空间
dwMaximumSizeHigh:Long,文件映射的最大长度的高32位。
dwMaximumSizeLow:Long,文件映射的最大长度的低32位。如这个参数和dwMaximumSizeHigh都是零,就用磁盘文件的实际长度。
lpName:String,指定文件映射对象的名字。如存在这个名字的一个映射,函数就会打开它。用vbNullString可以创建一个无名的文件映射。
调用CreateFileMapping的时候可能会出现的GetLastError的相应错误:
ERROR_FILE_INVALID (错误_文件_无效)如果企图创建一个零长度的文件映射
ERROR_INVALID_HANDLE(错误_无效_处理) 内存空间的命名和现有的内存映射,互斥量,信号量,临界区有同名
ERROR_ALREADY_EXISTS (错误或已经存在)表示内存空间命名已经存在
在调用CreateFileMapping()时,可以用GetLastError()来检查其返回的错误信息。如果返回值为ERROR_ALREADY_EXISTS,则表示内存映射对象指定名字已经存在。有关其他返回值的意义见MSDN的详细说明。
*/

获取物理句柄

最后也是最简单的一步,我们需要一个物理文件的句柄,可通过CreateFile完成,该API 是一个多功能的函数,可打开或创建文件或者I/O设备,并返回可访问的句柄:控制台,通信资源,目录(只读打开),磁盘驱动器,文件,邮槽,管道。函数定义和参数如下:

HANDLE WINAPI CreateFile(
_In_ LPCTSTR lpFileName,
_In_ DWORD dwDesiredAccess,
_In_ DWORD dwShareMode,
_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
_In_ DWORD dwCreationDisposition,
_In_ DWORD dwFlagsAndAttributes,
_In_opt_ HANDLE hTemplateFile
);/*
lpFileName String要打开的文件的名或设备名。这个字符串的最大长度在ANSI版本中为MAX_PATH,在unicode版本中为32767。
dwDesiredAccess指定类型的访问对象。如果为 GENERIC_READ 表示允许对设备进行读访问;如果为 GENERIC_WRITE 表示允许对设备进行写访问(可组合使用);如果为零,表示只允许获取与一个设备有关的信息 。
另外,还可以指定下面的控制标志:
标准控制权限(16-23位掩码):
DELETE 删除对象的权限。
READ_CONTROL 从对象的安全描述符中读取信息的权限,但不包括SACL(系统访问控制列表)中的信息。
WRITE_DAC 修改对象安全描述符中的DACL(随机访问控制列表)的权限
WRITE_OWNER 修改对象安全描述符中的属主的权限
SYNCHRONIZE 同步化使用对象的权限,即可以创建一个线程等待信号量释放(但有些对象不支持这个权限)。
STANDARD_RIGHTS_REQUIRED 等价于前面四种权限的总合(通常这四种是必须具有的权限)。
STANDARD_RIGHTS_READ 一般等价于READ_CONTROL
STANDARD_RIGHTS_WRITE 一般等价于READ_CONTROL
STANDARD_RIGHTS_EXECUTE 一般等价于READ_CONTROL
STANDARD_RIGHTS_ALL 等价于前面五种权限的总合。
特殊控制权限(0-15位掩码):
SPECIFIC_RIGHTS_ALL
ACCESS_SYSTEM_SECURITY
MAXIMUM_ALLOWED
GENERIC_READ
GENERIC_WRITE
GENERIC_EXECUTE
GENERIC_ALL
注:实质上是通过ACCESS_MASK结构体的一个双字值来设置标准权限、特殊权限和一般权限的。
dwShareModeLong, 如果是零表示不共享; 如果是FILE_SHARE_DELETE表示随后打开操作对象会成功,但只有删除访问请求的权限;如果是FILE_SHARE_READ随后打开操作对象会成功只有请求读访问的权限;如果是FILE_SHARE_WRITE 随后打开操作对象会成功,但只有请求写访问的权限。
lpSecurityAttributesSECURITY_ATTRIBUTES, 指向一个SECURITY_ATTRIBUTES结构的指针,定义了文件的安全特性(如果操作系统支持的话)
dwCreationDispositionLong,下述常数之一:
CREATE_NEW 创建文件;如文件存在则会出错
CREATE_ALWAYS 创建文件,会改写前一个文件
OPEN_EXISTING 文件必须已经存在。由设备提出要求
OPEN_ALWAYS 如文件不存在则创建它
TRUNCATE_EXISTING 将现有文件缩短为零长度
dwFlagsAndAttributesLong, 一个或多个下述常数
FILE_ATTRIBUTE_ARCHIVE 标记归档属性
FILE_ATTRIBUTE_COMPRESSED 将文件标记为已压缩,或者标记为文件在目录中的默认压缩方式
FILE_ATTRIBUTE_NORMAL 默认属性
FILE_ATTRIBUTE_HIDDEN 隐藏文件或目录
FILE_ATTRIBUTE_READONLY 文件为只读
FILE_ATTRIBUTE_SYSTEM 文件为系统文件
FILE_FLAG_WRITE_THROUGH 操作系统不得推迟对文件的写操作
FILE_FLAG_OVERLAPPED 允许对文件进行重叠操作
FILE_FLAG_NO_BUFFERING 禁止对文件进行缓冲处理。文件只能写入磁盘卷的扇区块
FILE_FLAG_RANDOM_ACCESS 针对随机访问对文件缓冲进行优化
FILE_FLAG_SEQUENTIAL_SCAN 针对连续访问对文件缓冲进行优化
FILE_FLAG_DELETE_ON_CLOSE 关闭了上一次打开的句柄后,将文件删除。特别适合临时文件
也可在Windows NT下组合使用下述常数标记:
SECURITY_ANONYMOUS, SECURITY_IDENTIFICATION, SECURITY_IMPERSONATION, SECURITY_DELEGATION, SECURITY_CONTEXT_TRACKING, SECURITY_EFFECTIVE_ONLY
hTemplateFile,hTemplateFile为一个文件或设备句柄,表示按这个参数给出的句柄为模板创建文件(就是将该句柄文件拷贝到lpFileName指定的路径,然后再打开)。它将指定该文件的属性扩展到新创建的文件上面,这个参数可用于将某个新文件的属性设置成与现有文件一样,并且这样会忽略dwAttrsAndFlags。通常这个参数设置为NULL,为空表示不使用模板,一般为空。
*/

示例代码

带有文件选择器的基地址获取函数代码如下:

#include <stdio.h>
#include <windows.h>
#include <Commdlg.h>
#include <iostream>
LPVOID GetImageBase() {
    TCHAR szFilePath[MAX_PATH];//要分析的文件名及路径
    OPENFILENAME ofn;//定义结构,调用打开对话框选择要分析的文件及其保存路径

    HANDLE hFile;// 文件句柄
    HANDLE hMapping;// 映射文件句柄
    LPVOID ImageBase;// 映射基址
    //必要的初始换
    memset(szFilePath, 0, MAX_PATH);//szfilepath后面max_path个字节 用0替换
    //cout << "Changed path:" << szFilePath << endl;
    memset(&ofn, 0, sizeof(ofn));
    ofn.lStructSize = sizeof(ofn);
    ofn.hwndOwner = NULL;
    ofn.hInstance = GetModuleHandle(NULL);
    ofn.nMaxFile = MAX_PATH;
    ofn.lpstrInitialDir = L".";
    ofn.lpstrFile = szFilePath;
    ofn.lpstrTitle = L"选择 PE文件打开";
    ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;
    ofn.lpstrFilter = L"*.exe\0*.exe\0*.dll\0*.dll\0\0";//过滤器选择exe和DLL
    if (!GetOpenFileName(&ofn))//调用打开对话框,选择要分析的文件
    {
        MessageBox(NULL, L"打开文件错误", NULL, MB_OK);
        return 0;
    }
    //选择要分析的文件后,经过3步打开并映射选择的文件到虚拟内存中
    //1.创建文件内核对象,其句柄保存于hFile,将文件在物理存储器的位置通告给操作系统
    hFile = CreateFile(szFilePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
    if (!hFile)
    {
        MessageBox(NULL, L"打开文件错误", NULL, MB_OK);
        return 0;
    }
    //2.创建文件映射内核对象(分配虚拟内存),句柄保存于hFileMapping
    hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
    if (!hMapping)
    {
        CloseHandle(hFile);
        return FALSE;
    }
    //3.将文件数据映射到进程的地址空间,返回的映射基址保存在ImageBase中
    ImageBase = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
    if (!ImageBase)
    {
        CloseHandle(hMapping);
        CloseHandle(hFile);
        return FALSE;
    }
    return ImageBase;
}

根据基地址判定是否为PE文件的辨别函数代码如下:

BOOL IsPEFile(PVOID ImageBase) {
    PIMAGE_DOS_HEADER  pDH = NULL;//指向IMAGE_DOS结构的指针
    PIMAGE_NT_HEADERS  pNtH = NULL;//指向IMAGE_NT结构的指针
    PIMAGE_FILE_HEADER pFH = NULL;//指向IMAGE_FILE结构的指针
    PIMAGE_OPTIONAL_HEADER pOH = NULL;//指向IMAGE_OPTIONALE结构的指针
        //IMAGE_DOS Header结构指针
    pDH = (PIMAGE_DOS_HEADER)ImageBase;
    //IMAGE_NT Header结构指针
    pNtH = (PIMAGE_NT_HEADERS)((DWORD)pDH + pDH->e_lfanew);
    //IMAGE_File Header结构指针
    pFH = &pNtH->FileHeader;
    //IMAGE_Optional Header结构指针
    pOH = &pNtH->OptionalHeader;
    if (pDH->e_magic != IMAGE_DOS_SIGNATURE)//如果dos头不为MZ
        return false;
    else if (pNtH->Signature != IMAGE_NT_SIGNATURE) //DOS头不为PE
        return false;
    else if (pFH->Characteristics == 34) //判定为exe
    {
        cout << "it is exe"<<endl;
        return true;
    }
    else if (pFH->Characteristics == 8450) //判定为dll
    {
        cout << "it is dll"<<endl;
        return true;
    }

总结

如今写起来思路清晰,需求导向研究,但是自己也是搞了两天才有了个大致印象,最后还是借鉴别人的代码实现之后才大概明白。
此次研究学到的就是基础要牢靠,因为刚开始,之前PE文件的结构还不是很熟练,每一步需要获取到什么信息都不记得,需要基地址来分析PE文件都不清楚。而且由于API了解的匮乏导致无从下手,把整个需求到网上去查,这怎么查得到呢,还是慢慢积累吧,任重道远。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
PE文件格式逆向解析汇编语言是一个非常复杂的任务,需要对PE文件格式有深入的了解,并且需要具备较强的反汇编技能。以下是一个简单的示例,演示如何将PE文件头部信息逆向解析汇编语言: ``` ; 定义IMAGE_DOS_HEADER结构体 IMAGE_DOS_HEADER struct e_magic dw ? e_cblp dw ? e_cp dw ? e_crlc dw ? e_cparhdr dw ? e_minalloc dw ? e_maxalloc dw ? e_ss dw ? e_sp dw ? e_csum dw ? e_ip dw ? e_cs dw ? e_lfarlc dw ? e_ovno dw ? e_res dw 4 dup(?) e_oemid dw ? e_oeminfo dw ? e_res2 dw 10 dup(?) e_lfanew dd ? IMAGE_DOS_HEADER ends ; 定义IMAGE_NT_HEADERS结构体 IMAGE_NT_HEADERS struct Signature dd ? FileHeader IMAGE_FILE_HEADER <> OptionalHeader IMAGE_OPTIONAL_HEADER32 <> IMAGE_NT_HEADERS ends ; 定义IMAGE_FILE_HEADER结构体 IMAGE_FILE_HEADER struct Machine dw ? NumberOfSections dw ? TimeDateStamp dd ? PointerToSymbolTable dd ? NumberOfSymbols dd ? SizeOfOptionalHeader dw ? Characteristics dw ? IMAGE_FILE_HEADER ends ; 定义IMAGE_OPTIONAL_HEADER32结构体 IMAGE_OPTIONAL_HEADER32 struct Magic dw ? MajorLinkerVersion db ? MinorLinkerVersion db ? SizeOfCode dd ? SizeOfInitializedData dd ? SizeOfUninitializedData dd ? AddressOfEntryPoint dd ? BaseOfCode dd ? BaseOfData dd ? ImageBase dd ? SectionAlignment dd ? FileAlignment dd ? MajorOperatingSystemVersion dw ? MinorOperatingSystemVersion dw ? MajorImageVersion dw ? MinorImageVersion dw ? MajorSubsystemVersion dw ? MinorSubsystemVersion dw ? Win32VersionValue dd ? SizeOfImage dd ? SizeOfHeaders dd ? CheckSum dd ? Subsystem dw ? DllCharacteristics dw ? SizeOfStackReserve dd ? SizeOfStackCommit dd ? SizeOfHeapReserve dd ? SizeOfHeapCommit dd ? LoaderFlags dd ? NumberOfRvaAndSizes dd ? DataDirectory dd 16 dup(?) IMAGE_OPTIONAL_HEADER32 ends ; 定义节表结构体 IMAGE_SECTION_HEADER struct Name db 8 dup(?) VirtualSize dd ? VirtualAddress dd ? SizeOfRawData dd ? PointerToRawData dd ? PointerToRelocations dd ? PointerToLinenumbers dd ? NumberOfRelocations dw ? NumberOfLinenumbers dw ? Characteristics dd ? IMAGE_SECTION_HEADER ends ; 定义变量 dos_header IMAGE_DOS_HEADER <> nt_headers IMAGE_NT_HEADERS <> section_headers IMAGE_SECTION_HEADER 16 dup(?) ; 读取PE文件 filename db 'test.exe', 0 handle dw ? buffer db 512 dup(?) bytes_read dw ? section_table_offset dd ? size_of_section_table dd ? ; 打开文件 mov ah, 3dh mov al, 0 ; 只读模式 mov dx, offset filename int 21h mov handle, ax ; 读取DOS头部信息 mov ah, 3fh mov bx, handle mov cx, sizeof IMAGE_DOS_HEADER mov dx, offset dos_header int 21h ; 获取PE头部偏移地址 mov ax, word ptr [dos_header+0x3c] mov bx, handle mov cx, sizeof IMAGE_NT_HEADERS mov dx, offset nt_headers add dx, ax int 21h ; 解析PE头部信息 mov ax, word ptr [nt_headers.Signature] cmp ax, 'PE' jne exit_program ; 解析文件头部信息 mov ax, word ptr [nt_headers.FileHeader.NumberOfSections] mov section_table_offset, dx mov size_of_section_table, ax * sizeof IMAGE_SECTION_HEADER add dx, sizeof IMAGE_FILE_HEADER mov cx, sizeof IMAGE_FILE_HEADER mov si, offset nt_headers.FileHeader mov di, dx rep movsb ; 解析可选头部信息 mov ax, word ptr [nt_headers.OptionalHeader.Magic] cmp ax, IMAGE_NT_OPTIONAL_HDR32_MAGIC jne exit_program mov cx, sizeof IMAGE_OPTIONAL_HEADER32 mov si, offset nt_headers.OptionalHeader mov di, dx rep movsb ; 解析节表信息 mov ah, 3fh mov bx, handle mov cx, size_of_section_table mov dx, offset section_headers add dx, section_table_offset int 21h exit_program: ; 关闭文件 mov ah, 3eh mov bx, handle int 21h ``` 以上代码仅为示例,实际上解析PE文件格式的过程要比这个更加复杂,需要对不同的结构体进行不同的解析方式,并且需要处理一些特殊情况。同时,反汇编的过程中还需要考虑一些优化问题,例如去除无用代码、还原函数调用等,以确保反汇编的代码正确、可读性强。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值