之前处理外网崩溃最头痛的时莫过于拿到了崩溃的Dump但是VS对他无从下手,主要原因是加壳之后exe生成的Dump无法与未加壳的exe匹配,进而对不上符号文件,不能在源码级别调试。
直接汇编级调试Dump根据调用栈地址来判断具体调用的是哪个函数也是个方法,但来来回回跳转几层之后真是有点力不从心哇,下面给大家分享一段解决方案。
之前百度这个问题的时候看到这么一段内容:
VS调试Dump时是通过ModuleName、SizeOfImage、CheckSum、TimeDateStamp这四个属性进行匹配的,四项全部匹配才认为Exe与Dump是配套的
那么简单来说,哪怕Dump与Exe不配套,我们把这四个属性改成一一对应的,那岂不是可以强制匹配调试了?
对于PE文件而言,这四个属性很好弄。ModuleName就是文件名——XXX.exe或者XXX.dll。参者PE文件结构,TimeDateStamp在IMAGE_FILE_HEADER中、SizeOfImageI和CheckSum在MAGE_OPTIONAL_HEADER32中,对应着取出来就好。
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp; // 目标字段1
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage; // 目标字段2
DWORD SizeOfHeaders;
DWORD CheckSum; // 目标字段3
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
复杂的是Dump文件的结构,好不容易在一本软件调试的书里翻到一些内容。
Dump文件头是MINIDUMP_HEADER结构,_MINIDUMP_HEADER::NumberOfStreams表示Dump中Streams的个数,别的我们不关心我们只要找到StreamType为ModuleListStream的Stream就好。
typedef struct _MINIDUMP_HEADER {
ULONG32 Signature;
ULONG32 Version;
ULONG32 NumberOfStreams;
RVA StreamDirectoryRva;
ULONG32 CheckSum;
union {
ULONG32 Reserved;
ULONG32 TimeDateStamp;
};
ULONG64 Flags;
} MINIDUMP_HEADER, *PMINIDUMP_HEADER;
_MINIDUMP_HEADER::StreamDirectoryRva指向MINIDUMP_DIRECTORY*类型的数组,其长度就是_MINIDUMP_HEADER::NumberOfStreams。
遍历_MINIDUMP_HEADER::StreamDirectoryRva,找到_MINIDUMP_DIRECTORY::StreamType为ModuleListStream的那一项。
//
// The MINIDUMP_HEADER field StreamDirectoryRva points to
// an array of MINIDUMP_DIRECTORY structures.
//
typedef struct _MINIDUMP_DIRECTORY {
ULONG32 StreamType;
MINIDUMP_LOCATION_DESCRIPTOR Location;
} MINIDUMP_DIRECTORY, *PMINIDUMP_DIRECTORY;
_MINIDUMP_DIRECTORY::Location记录着当前类型的Stream的位置及长度,读取数据段,强转为MINIDUMP_MODULE_LIST*类型。
_MINIDUMP_MODULE_LIST::NumberOfModules记录着Dump加载的所有模块的数量,模块的信息作为数组储存在_MINIDUMP_MODULE_LIST::Modules中,我们要修改的是exe的信息,一般来说不用去遍历_MINIDUMP_MODULE_LIST::Modules了,第一个元素就是。
//
// The minidump module list is a container for modules.
//
typedef struct _MINIDUMP_MODULE_LIST {
ULONG32 NumberOfModules;
MINIDUMP_MODULE Modules [ 0 ]; // 已加载模块
} MINIDUMP_MODULE_LIST, *PMINIDUMP_MODULE_LIST;
//
// The MINIDUMP_MODULE contains information about a
// a specific module. It includes the CheckSum and
// the TimeDateStamp for the module so the module
// can be reloaded during the analysis phase.
//
typedef struct _MINIDUMP_MODULE {
ULONG64 BaseOfImage;
ULONG32 SizeOfImage; // 目标字段2
ULONG32 CheckSum; // 目标字段3
ULONG32 TimeDateStamp; // 目标字段1
RVA ModuleNameRva;
VS_FIXEDFILEINFO VersionInfo;
MINIDUMP_LOCATION_DESCRIPTOR CvRecord;
MINIDUMP_LOCATION_DESCRIPTOR MiscRecord;
ULONG64 Reserved0; // Reserved for future use.
ULONG64 Reserved1; // Reserved for future use.
} MINIDUMP_MODULE, *PMINIDUMP_MODULE;
取出_MINIDUMP_MODULE_LIST::Modules[0],对应着上面取出来的Exe的信息把_MINIDUMP_MODULE::SizeOfImage、_MINIDUMP_MODULE::CheckSum、_MINIDUMP_MODULE::TimeDateStamp修改掉然后保存。
然后愉快的双击Dump按下调试,开始愉快的源码级调试吧~
题外话:改Dump的时候各类指针乱飞,最方便的做法是加载整个Dump进内存,就不用管部分加载计算偏移的事了,但是对于动辄800M+的游戏Dump文件,还要做批量修改,还是老老实实部分加载吧。
P.S. 如果崩溃点在你加壳代码混淆的部分内,放弃调试Dump吧,你啥也看不到 ╮(╯▽╰)╭。