X64逆向异常分析 旧版本64位异常处理:
在x64的异常处理中,VS编译器不再采用在函数中注册SEH完成异常处理的方式,而是将异常信息存放在PE文件中的一个.pdata节中
在32位的情况下,想要找Funcinfo可以通过一个函数的开局 有一个
1 offset __ehhandler$_main
双击点进去可以看到CxxFrameHanlder的其中一个参数unk_41B2C4,这个就是FuncInfo
但是在64位,我们在函数开头貌似并没有看到
1 offset __ehhandler$_main
ThrowInfo 我们只能找到CxxThrowException这个函数,然后找到pThrowInfo,然后找到ThrowInfo这个结构体
我们点进去发现,最后一个参数是一个RVA参数,为什么要用RVA呢?
原因是因为这样可以支持随机基址,拿到正确的地址的话,就可以直接通过随机基地址加上这个RVA即可
但是每次都要自己手动计算RVA转换为正确的地址,难免有些繁琐,所以我们在结构体定义的时候,就可以自己加上ImageBase,变成正确的地址:
可以看到快捷键是Ctrl+R,然后我们跳到结构体的界面:
注意看勾选的选项
设置好偏移即可
这下就变成RVA了
Runtime_FuncInfo 现在Throw的类型可以被识别了,但是我们还是找不到Catch的,因为我们没找到FuncInfo这个结构体,如何处理?
之前我们32位程序,注册异常程序的时候,是将异常处理链压入栈中:
也就是说一个函数要是有异常处理机制,那么就会在函数开头,将异常处理链压入栈中,如果涉及到递归调用,那么将会不停不停的将异常处理链压栈,压栈,压栈,这其实对栈的消耗也是比较大的,32位下是一个一个注册。
但是在64位的条件下,开始有了不一样的机制,就是开局就已经注册好异常处理链了
Runtime_Function
Runtime_Function
是一个与异常处理相关的结构体。它通常包含函数的起始地址、结束地址以及异常处理的相关信息。这个结构体的定义可能因编译器和平台的不同而有所不同。在Windows的PE(Portable Executable)文件格式中,Runtime_Function
结构体常用于描述函数的范围和与异常处理相关的数据。
RUNTIME_FUNCTION结构必须在内存中为DWORD对齐,所有地址都是相对于映像,也就是说它们是相对于包含函数表条目的映像起始地址的32位偏移。这些条目会进行排序,并放入PE32+映像的.pdata节中。
所以我们直接用IDA Ctrl+s就可以打开节表进行查看,打开.pdata节
这样就可以找到了(debug版可能要往下滑,才可以找到)
当然还有一个更简便的方法去找到Runtime_FuncInfo,就是直接查找对main函数的引用
Runtime_Function具体结构:
1 2 3 4 5 6 7 8 typedef struct _IMAGE_RUNTIME_FUNCTION_ENTRY { DWORD BeginAddress; DWORD EndAddress; union { DWORD UnwindInfoAddress; DWORD UnwindData; } DUMMYUNIONNAME; } RUNTIME_FUNCTION, *PRUNTIME_FUNCTION, _IMAGE_RUNTIME_FUNCTION_ENTRY, *_PIMAGE_RUNTIME_FUNCTION_ENTRY;
值得注意的是,这结构体的三个成员都是RVA,所以要加上基地址才能正确的跳转
UNWIND_INFO 重点说一下UNWIND_INFO
1 2 3 4 union { DWORD UnwindInfoAddress; DWORD UnwindData; } DUMMYUNIONNAME;
它们指向的是UNWIND_INFO结构体:(UNWIND是异常展开,异常展开的作用就是能知道如何去释放资源,析构的时候,不过我们一般不关心析构,我们需要知道的是如何去处理抛出的异常)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct _UNWIND_INFO { UBYTE Version:3 ; UBYTE Flags:5 ; UBYTE SizeOfProlog; UBYTE CountOfCodes; UBYTE FrameRegister:4 X UBYTE FrameOffset:4 ; UNWIND_CODE UnwindCode[1 ]; union { OPTIONAL ULONG ExceptionHandler; OPTIONAL ULONG FunctionEntry; }; ULONG ExceptionData[1 ]; } UNWIND_INFO, *PUNWIND_INFO;
位域按照定义的顺序从最低位开始分配。因此:
Version
占用字节的低 3 位。
Flags
占用字节的高 5 位。
Version:异常展开信息的版本号,一般为0x001
Flags:共有四种标志: 1.当它为0x0的时候表示UNW_FLAG_NHANDLER,没有异常处理函数。
2.当它为0x1的时候表示UNW_FLAG_EHANDLER,有异常处理函数。
3.当它为0x2的时候表示UNW_FLAG_UHANDLER,有系统默认的处理函数。
4.当它为0x4的时候表示UNW_FLAG_CHAININFO,表示FunctionEntry指向的是前一个RUNTIME_FUNCTION的RAV。
SizeOfProlog:函数起始部分字节的长度
CountOfCodes:UNWIND_INFO结构包含的UNWIND_CODE结构数
FrameRegister: 当 FrameRegister
为 0 时,表示没有使用特定的栈帧基址寄存器。即函数没有显式地使用 RBP(或其他寄存器)作为帧指针。
FrameOffset:若上面字段不为0,表示函数偏移
UnwindCode :指定永久性寄存器与RSP的数组项目数
1 2 3 4 5 6 7 typedef union _UNWIND_CODE { struct { UBYTE CodeOffset; UBYTE UnwindOp : 4 ; UBYTE OpInfo : 4 ; }; } UNWIND_CODE, *PUNWIND_CODE;
CodeOffset
:指示栈操作对应的指令在函数代码中的偏移。
UnwindOp
:指示具体的展开操作。常见的展开操作如下:
OpInfo
:与 UnwindOp
结合,提供具体的操作信息。例如,对于 UWOP_ALLOC_SMALL
,OpInfo
指定分配的栈大小。
FrameOffset
:对于一些操作(如 UWOP_SAVE_NONVOL
),这个字段表示相对于栈帧基址的偏移量。
另外因为这个结构的地址需要和4的倍数对齐,所以可能会有补零填充
ExceptionHandler:异常句柄
FunctionEntry:展开信息链(函数)的映像相对地址指针(如果设置了UNW_FLAG_CHAININFO标识)
ExceptionData:异常处理程序的数据
UnwindCode后面如果出现CxxFrameHandler函数,其实具体是由Flag决定的,当它为0x1的时候表示UNW_FLAG_EHANDLER,有异常处理函数。( CxxFrameHandler
是 Microsoft Visual C++ 编译器生成的异常处理机制中的一个关键函数。它负责处理 C++ 异常以及进行栈展开(stack unwinding)操作 ),那么后面跟着的一个DWORD就是FuncInfo结构体,至此我们找到了64位程序的FuncInfo结构体
FuncInfo 我们观察一下这个32位程序CxxFrameHandler3,后缀是一个3,说明这是32位情况下的一个FuncInfo
然后看看64位的:
可以发现IDA识别出来的是叫做CxxFrameHandler4,这就代表是64位条件下的FuncInfo
不过这是由编译器版本决定的,64位也可以使用CxxFrameHanler3
具体来说我们可以在编译选项加上
1 2 /d2FH4 #这个代表启用CxxFrameHandler4 /d2FH4- #这个代表关闭CxxFrameHandler4,启用CxxFrameHandler3
以下都是CxxFrameHandler3:
64位条件下FuncInfo长这样
1 2 3 4 5 6 7 8 9 10 11 12 typedef struct _FuncInfo { unsigned int magicNumber; int maxState; const _UnwindMapEntry* pUnwindMap; unsigned int nTryBlocks; const _TryBlockMapEntry* pTryBlockMap; unsigned int nIPMapEntries; const _IPToStateMapEntry* pIPToStateMap; void * dispUwindHelp; void * pESTypeList; int EHFlags; } FuncInfo;
比32位多了后面四个成员
TryBlockMapEntry 1 2 3 4 5 6 7 8 struct TryBlockMapEntry { DWORD tryLow; DWORD tryHigh; DWORD catchHigh; DWORD nCatches; DWORD dispHandlerArray; }
HandlerType 1 2 3 4 5 6 7 8 struct HandlerType { DWORD adjectives; DWORD disType; DWORD disCatchObj; DWORD dispOfHandler; DWORD dispFrame; }
可以看到这个RTTI对应关系是:
1 H:int M:float N:double _J:_int64
DWORD dispOfHandler;代表的就是对应的catch块了
IptoStateMapEntry 1 2 3 4 5 struct IptoStateMapEnrty { DOWRD _Ip; DOWRD State; }
现在要解决的问题是,trylevel,不过在x64下,trylevel已经取消了,因为已经有全局表了,不再依赖栈注册了
64位开始用IpMap表来找
ESTypeList 1 2 3 4 5 struct ESTypeList { DWORD nCount; DWORD dispTypeArray; }
该结构与32位程序最大的区别就是多了一个IP状态映射表IptoStateMapEntry,在32位应用程序中,使用栈空间的一个变量标识try块的状态索引,在x64中便不再使用该变量,而是通过产生异常的RIP查询IP状态映射表来获取Try块的状态索引,其结构之间的关系图如下:
新版本64位异常处理 FuncInfo4 新版本64位处理比较麻烦,主要是它把FuncInfo这个表给压缩了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 struct FuncInfo4 { FuncInfoHeader header; uint32_t bbtFlags; int32_t dispUnwindMap; int32_t dispTryBlockMap; int32_t dispIPtoStateMap; uint32_t dispFrame; FuncInfo4 () { header.value = 0 ; bbtFlags = 0 ; dispUnwindMap = 0 ; dispTryBlockMap = 0 ; dispIPtoStateMap = 0 ; dispFrame = 0 ; } };
FuncInfoHeader结构如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 struct FuncInfoHeader { union { #pragma warning (push) #pragma warning (disable: 4201) struct { uint8_t isCatch : 1 ; uint8_t isSeparated : 1 ; uint8_t BBT : 1 ; uint8_t UnwindMap : 1 ; uint8_t TryBlockMap : 1 ; uint8_t EHs : 1 ; uint8_t NoExcept : 1 ; uint8_t reserved : 1 ; }; #pragma warning (pop) uint8_t value; }; FuncInfoHeader () { value = 0 ; } };
具体解析FuncInfo4的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 inline uint32_t compFuncInfo4 (FuncInfo4 funcInfo, buffer buffer, RVAoffsets * offsets) { uint32_t index = 0 ; offsets->count = 0 ; buffer[index] = funcInfo.header.value; index++; if (funcInfo.header.BBT) { index += getNETencoded (&buffer[index], funcInfo.bbtFlags); } if (funcInfo.header.UnwindMap) { *(reinterpret_cast <int32_t *>(&buffer[index])) = funcInfo.dispUnwindMap; addOffset (offsets, index); } if (funcInfo.header.TryBlockMap) { *(reinterpret_cast <int32_t *>(&buffer[index])) = funcInfo.dispTryBlockMap; addOffset (offsets, index); } *(reinterpret_cast <int32_t *>(&buffer[index])) = funcInfo.dispIPtoStateMap; addOffset (offsets, index); if (funcInfo.header.isCatch) { index += getNETencoded (&buffer[index], funcInfo.dispFrame); } assert_ehdata (index < maxBufferSize); return index; }
发现先进行解析funcInfo.header.BBT,然后是funcInfo.header.UnwindMap,再然后是funcInfo.header.TryBlockMap,最后是funcInfo.header.isCatch
FuncInfo4是一个会变长的结构体:具体这三项
1 2 3 4 uint32_t bbtFlags; int32_t dispUnwindMap; int32_t dispTryBlockMap; uint32_t dispFrame;
是否存在要看这个选项:
但是这个是一个位段,IDA如何解析位段呢?
然后再添加成员:
设置好之后就可以直接导入这个类型了:
例如图片上这个,我们只关心有存在UnwindMap和TryBlockMap,因此在FuncInfo4这个结构体里面,存在这些
TryBlockMapEntry4 这是TryBlockMapEntry4结构
1 2 3 4 5 6 struct TryBlockMapEntry4 { __ehstate_t tryLow; __ehstate_t tryHigh; __ehstate_t catchHigh; int32_t dispHandlerArray; };
以下分析涉及到压缩算法,会有一点复杂
首先找到TryBlockMap4是如何被解析的,先定位到这个类的构造函数
1 _buffer = imageRelToByteBuffer (imageBase, pFuncInfo->dispTryBlockMap);
这一句是获取指向TryBlockMap4表的地址
然后获取Try块的个数
1 _numTryBlocks = ReadUnsigned (&_buffer);
这个ReadUnsigned函数我们在下面进行分析
发现调用了一个DecompTryBlock();函数,我们跟进去看看
发现调用的是ReadUnsigned 和 ReadInt 函数
重点来了,我们来分析一下ReadUnsigned和ReadInt这俩函数
ReadUnsigned
1 2 3 4 5 6 7 8 9 10 11 12 inline uint32_t ReadUnsigned (uint8_t ** pbEncoding) { uint32_t lengthBits = **pbEncoding & 0x0F ; size_t negLength = s_negLengthTab[lengthBits]; uint32_t shift = s_shiftTab[lengthBits]; uint32_t result = *(reinterpret_cast <uint32_t *>(*pbEncoding - negLength - 4 )); result >>= shift; *pbEncoding -= negLength; return result; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 inline constexpr int8_t s_negLengthTab[16 ] ={ -1 , -2 , -1 , -3 , -1 , -2 , -1 , -4 , -1 , -2 , -1 , -3 , -1 , -2 , -1 , -5 , };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 inline constexpr uint8_t s_shiftTab[16 ] ={ 32 - 7 * 1 , 32 - 7 * 2 , 32 - 7 * 1 , 32 - 7 * 3 , 32 - 7 * 1 , 32 - 7 * 2 , 32 - 7 * 1 , 32 - 7 * 4 , 32 - 7 * 1 , 32 - 7 * 2 , 32 - 7 * 1 , 32 - 7 * 3 , 32 - 7 * 1 , 32 - 7 * 2 , 32 - 7 * 1 , 0 , };
我们来实战分析一下
定位到dispTryBlockMap以后,取出一个字节,这个字节如图就是4,然后取4这个字节的低4位,那么还是4,这就是lengthBits。
查表s_negLengthTab得到negLength是-1
查表得shift是 32 - 7 * 1
那么result存的值是指向的就是原来的buffer +1 -4 = buffer -3
计算得到result得到这个值,然后还要右移shift位,也就是25位,等价于取4的高7位,得到数字2,即_numTryBlocks为2。也就是说有在这个函数中,有两个try
ReadInt
1 2 3 4 5 6 inline int32_t ReadInt (uint8_t ** buffer) { int value = *(reinterpret_cast <int *>(*buffer)); *buffer += sizeof (int32_t ); return value; }
可以看到这个是直接取出4个字节,然后buffer地址+4即可
HandlerType4 HandlerType4结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 struct HandlerType4 { HandlerTypeHeader header; uint32_t adjectives; int32_t dispType; uint32_t dispCatchObj; int32_t dispOfHandler; uintptr_t continuationAddress[MAX_CONT_ADDRESSES]; void reset () { header.value = 0 ; adjectives = 0 ; dispType = 0 ; dispCatchObj = 0 ; dispOfHandler = 0 ; memset (continuationAddress, 0 , sizeof (continuationAddress)); } HandlerType4 () { reset (); } };
HandlerTypeHeader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 struct HandlerTypeHeader { enum contType { NONE = 0b00 , ONE = 0b01 , TWO = 0b10 , RESERVED = 0b11 }; union { #pragma warning (push) #pragma warning (disable: 4201) struct { uint8_t adjectives : 1 ; uint8_t dispType : 1 ; uint8_t dispCatchObj : 1 ; uint8_t contIsRVA : 1 ; uint8_t contAddr : 2 ; uint8_t unused : 2 ; }; #pragma warning (pop) uint8_t value; }; };
又是一个位段
如何定义这个位段呢?
我们发现有的位段占了两个比特,那这种情况需要如何定义呢??
因此如constAddr_NONE value填0,MASK填0x30,constAddr_ONE的value是0x10,mask同样是为0x30,同理,mask都是一致的,constAddr_TWO的value是0x20,constAddr_RESERVED是0x30
解析HandlerMap4的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 HandlerMap4 (const TryBlockMapEntry4 *tryMap, uintptr_t imageBase, int32_t functionStart) : _imageBase(imageBase), _functionStart(functionStart) { if (tryMap->dispHandlerArray != 0 ) { _buffer = imageRelToByteBuffer (_imageBase, tryMap->dispHandlerArray); _numHandlers = ReadUnsigned (&_buffer); _bufferStart = _buffer; DecompHandler (); } else { _numHandlers = 0 ; } } void DecompHandler () { _handler.reset (); _handler.header.value = _buffer[0 ]; ++_buffer; if (_handler.header.adjectives) { _handler.adjectives = ReadUnsigned (&_buffer); } if (_handler.header.dispType) { _handler.dispType = ReadInt (&_buffer); } if (_handler.header.dispCatchObj) { _handler.dispCatchObj = ReadUnsigned (&_buffer); } _handler.dispOfHandler = ReadInt (&_buffer); if (_handler.header.contIsRVA) { if (_handler.header.contAddr == HandlerTypeHeader::contType::ONE) { _handler.continuationAddress[0 ] = ReadInt (&_buffer); } else if (_handler.header.contAddr == HandlerTypeHeader::contType::TWO) { _handler.continuationAddress[0 ] = ReadInt (&_buffer); _handler.continuationAddress[1 ] = ReadInt (&_buffer); } else { } } else { if (_handler.header.contAddr == HandlerTypeHeader::contType::ONE) { _handler.continuationAddress[0 ] = _functionStart + ReadUnsigned (&_buffer); } else if (_handler.header.contAddr == HandlerTypeHeader::contType::TWO) { _handler.continuationAddress[0 ] = _functionStart + ReadUnsigned (&_buffer); _handler.continuationAddress[1 ] = _functionStart + ReadUnsigned (&_buffer); } else { } } }
解析结束后长这样
IPtoStateMapEntry4 1 2 3 4 struct IPtoStateMapEntry4 { int32_t Ip; __ehstate_t State; };
解析代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 IPtoStateMap4 (const SepIPtoStateMapEntry4 *mapEntry, uintptr_t imageBase) : _imageBase(imageBase) { _funcStart = mapEntry->addrStartRVA; uint8_t *buffer = imageRelToByteBuffer (imageBase, mapEntry->dispOfIPMap); _numEntries = ReadUnsigned (&buffer); _bufferStart = buffer; } IPtoStateMapEntry4 decompIP2State (uint8_t ** currOffset, uint32_t prevIp) { IPtoStateMapEntry4 IPEntry; IPEntry.Ip = _funcStart + prevIp + ReadUnsigned (currOffset); IPEntry.State = ReadUnsigned (currOffset) - 1 ; return IPEntry; }
照着这样解析即可
1 IPEntry.Ip = _funcStart + prevIp + ReadUnsigned (currOffset);
得到的IP是在原来的基础上叠加的,FuncStart指的是有try块的函数地址,然后prevIp指的是前面计算出来的ip基础上继续加的
至此,所有结构我们全部分析完毕!!!
不得不说难度超级大!(吐血