X64逆向异常分析

旧版本64位异常处理:

在x64的异常处理中,VS编译器不再采用在函数中注册SEH完成异常处理的方式,而是将异常信息存放在PE文件中的一个.pdata节中

在32位的情况下,想要找Funcinfo可以通过一个函数的开局 有一个

1
offset __ehhandler$_main

1720275163930

双击点进去可以看到CxxFrameHanlder的其中一个参数unk_41B2C4,这个就是FuncInfo

1720275318434

但是在64位,我们在函数开头貌似并没有看到

1
offset __ehhandler$_main

ThrowInfo

我们只能找到CxxThrowException这个函数,然后找到pThrowInfo,然后找到ThrowInfo这个结构体

1720275113632

我们点进去发现,最后一个参数是一个RVA参数,为什么要用RVA呢?

原因是因为这样可以支持随机基址,拿到正确的地址的话,就可以直接通过随机基地址加上这个RVA即可

1720275535346

但是每次都要自己手动计算RVA转换为正确的地址,难免有些繁琐,所以我们在结构体定义的时候,就可以自己加上ImageBase,变成正确的地址:

1720277569310

可以看到快捷键是Ctrl+R,然后我们跳到结构体的界面:

注意看勾选的选项

1720277616204

设置好偏移即可

这下就变成RVA了

1720277635349

Runtime_FuncInfo

现在Throw的类型可以被识别了,但是我们还是找不到Catch的,因为我们没找到FuncInfo这个结构体,如何处理?

之前我们32位程序,注册异常程序的时候,是将异常处理链压入栈中:

1720317105284

也就是说一个函数要是有异常处理机制,那么就会在函数开头,将异常处理链压入栈中,如果涉及到递归调用,那么将会不停不停的将异常处理链压栈,压栈,压栈,这其实对栈的消耗也是比较大的,32位下是一个一个注册。

但是在64位的条件下,开始有了不一样的机制,就是开局就已经注册好异常处理链了

Runtime_Function

Runtime_Function是一个与异常处理相关的结构体。它通常包含函数的起始地址、结束地址以及异常处理的相关信息。这个结构体的定义可能因编译器和平台的不同而有所不同。在Windows的PE(Portable Executable)文件格式中,Runtime_Function结构体常用于描述函数的范围和与异常处理相关的数据。

RUNTIME_FUNCTION结构必须在内存中为DWORD对齐,所有地址都是相对于映像,也就是说它们是相对于包含函数表条目的映像起始地址的32位偏移。这些条目会进行排序,并放入PE32+映像的.pdata节中。

所以我们直接用IDA Ctrl+s就可以打开节表进行查看,打开.pdata节

1720317649835

这样就可以找到了(debug版可能要往下滑,才可以找到)

1720319250296

当然还有一个更简便的方法去找到Runtime_FuncInfo,就是直接查找对main函数的引用

1720319314657

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是异常展开,异常展开的作用就是能知道如何去释放资源,析构的时候,不过我们一般不关心析构,我们需要知道的是如何去处理抛出的异常)

1720320723150

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:4X
UBYTE FrameOffset:4;
UNWIND_CODE UnwindCode[1];
union {
// If (Flags & UNW_FLAG_EHANDLER)
OPTIONAL ULONG ExceptionHandler;//异常/终止函数的映像相对地址指针
// Else if (Flags & UNW_FLAG_CHAININFO)
OPTIONAL ULONG FunctionEntry;//展开信息链的映像相对地址指针
};
// If (Flags & UNW_FLAG_EHANDLER)
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; //prolog中的偏移
UBYTE UnwindOp : 4;//展开操作代码
UBYTE OpInfo : 4;//操作信息
};
} UNWIND_CODE, *PUNWIND_CODE;

CodeOffset:指示栈操作对应的指令在函数代码中的偏移。

UnwindOp:指示具体的展开操作。常见的展开操作如下:

OpInfo:与 UnwindOp 结合,提供具体的操作信息。例如,对于 UWOP_ALLOC_SMALLOpInfo 指定分配的栈大小。

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结构体

1720322803083

FuncInfo

我们观察一下这个32位程序CxxFrameHandler3,后缀是一个3,说明这是32位情况下的一个FuncInfo

1720334682008

然后看看64位的:

1720334780389

可以发现IDA识别出来的是叫做CxxFrameHandler4,这就代表是64位条件下的FuncInfo

不过这是由编译器版本决定的,64位也可以使用CxxFrameHanler3

具体来说我们可以在编译选项加上

1
2
/d2FH4    #这个代表启用CxxFrameHandler4
/d2FH4- #这个代表关闭CxxFrameHandler4,启用CxxFrameHandler3

1720335022766

以下都是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;//指向栈展开函数表的偏移量,指向UnwindMapEntry结构
unsigned int nTryBlocks;//try块数量
const _TryBlockMapEntry* pTryBlockMap;//指向TryBlockMapEntry结构
unsigned int nIPMapEntries; //IP状态映射表的数量
const _IPToStateMapEntry* pIPToStateMap; //IP状态映射表的偏移量,指向IPtoStateMapEntry结构
void* dispUwindHelp; //异常展开帮助偏移量
void* pESTypeList; //异常类型列表的偏移量
int EHFlags; //一些功能的标志
} FuncInfo;

比32位多了后面四个成员

TryBlockMapEntry

1
2
3
4
5
6
7
8
struct TryBlockMapEntry
{
DWORD tryLow; //try块的最小索引
DWORD tryHigh; //try块的最大索引
DWORD catchHigh;//catch块最高索引,用来范围检查
DWORD nCatches; //catch块的个数
DWORD dispHandlerArray; //catch块描述数组的偏移量,指向HandlerType结构
}

HandlerType

1
2
3
4
5
6
7
8
struct HandlerType
{
DWORD adjectives; //用于catch块的匹配检查
DWORD disType; //catch块要捕捉的类型偏移量,指向C++ RTTI类型描述结构type_info的指针
DWORD disCatchObj; //用于定位异常对象的偏移量
DWORD dispOfHandler; //catch块代码的偏移量(这里可以找到catch)
DWORD dispFrame; //异常框架信息的偏移量
}

可以看到这个RTTI对应关系是:

1
H:int	M:float		N:double	_J:_int64

1720349729518

DWORD dispOfHandler;代表的就是对应的catch块了

IptoStateMapEntry

1
2
3
4
5
struct IptoStateMapEnrty
{
DOWRD _Ip; //try块起始的RIP偏移量
DOWRD State; //try块的状态索引,类似于32位下的trylevel
}

现在要解决的问题是,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块的状态索引,其结构之间的关系图如下:

1720338489188

新版本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; // flags that may be set by BBT processing

int32_t dispUnwindMap; // Image relative offset of the unwind map
int32_t dispTryBlockMap; // Image relative offset of the handler map
int32_t dispIPtoStateMap; // Image relative offset of the IP to state map
uint32_t dispFrame; // displacement of address of function frame wrt establisher frame, only used for catch funclets

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) // nonstandard extension used: nameless struct/union
struct
{
uint8_t isCatch : 1; // 1 if this represents a catch funclet, 0 otherwise
uint8_t isSeparated : 1; // 1 if this function has separated code segments, 0 otherwise
uint8_t BBT : 1; // Flags set by Basic Block Transformations
uint8_t UnwindMap : 1; // Existence of Unwind Map RVA
uint8_t TryBlockMap : 1; // Existence of Try Block Map RVA
uint8_t EHs : 1; // EHs flag set
uint8_t NoExcept : 1; // NoExcept flag set
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; // Image relative offset of the unwind map
int32_t dispTryBlockMap; // Image relative offset of the handler map
uint32_t dispFrame;

是否存在要看这个选项:

1
FuncInfoHeader      header;

但是这个是一个位段,IDA如何解析位段呢?

1720363588656

然后再添加成员:

1720403707650

设置好之后就可以直接导入这个类型了:

1720406395355

1720406381758

例如图片上这个,我们只关心有存在UnwindMap和TryBlockMap,因此在FuncInfo4这个结构体里面,存在这些

1720406592661

TryBlockMapEntry4

这是TryBlockMapEntry4结构

1
2
3
4
5
6
struct TryBlockMapEntry4 {
__ehstate_t tryLow; // Lowest state index of try
__ehstate_t tryHigh; // Highest state index of try
__ehstate_t catchHigh; // Highest state index of any associated catch
int32_t dispHandlerArray; // Image relative offset of list of handlers for this try
};

以下分析涉及到压缩算法,会有一点复杂

首先找到TryBlockMap4是如何被解析的,先定位到这个类的构造函数

1720427809072

1
_buffer = imageRelToByteBuffer(imageBase, pFuncInfo->dispTryBlockMap);  

这一句是获取指向TryBlockMap4表的地址

然后获取Try块的个数

1
_numTryBlocks = ReadUnsigned(&_buffer);

这个ReadUnsigned函数我们在下面进行分析

发现调用了一个DecompTryBlock();函数,我们跟进去看看

1720427890003

发现调用的是ReadUnsigned 和 ReadInt 函数

重点来了,我们来分析一下ReadUnsigned和ReadInt这俩函数

ReadUnsigned

1720428016972

1
2
3
4
5
6
7
8
9
10
11
12
inline uint32_t ReadUnsigned(uint8_t ** pbEncoding)
{
uint32_t lengthBits = **pbEncoding & 0x0F; //取出一个字节,与上一个0x0F
size_t negLength = s_negLengthTab[lengthBits]; //用LengthBits进行查表,得到一个数字赋值给negLength
uint32_t shift = s_shiftTab[lengthBits]; //再用LengthBits进行查表,得到一个数据赋值给shift
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, // 0
-2, // 1
-1, // 2
-3, // 3

-1, // 4
-2, // 5
-1, // 6
-4, // 7

-1, // 8
-2, // 9
-1, // 10
-3, // 11

-1, // 12
-2, // 13
-1, // 14
-5, // 15
};
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] =
{
//为什么要这么写?因为数据是7位

32 - 7 * 1, // 0
32 - 7 * 2, // 1
32 - 7 * 1, // 2
32 - 7 * 3, // 3

32 - 7 * 1, // 4
32 - 7 * 2, // 5
32 - 7 * 1, // 6
32 - 7 * 4, // 7

32 - 7 * 1, // 8
32 - 7 * 2, // 9
32 - 7 * 1, // 10
32 - 7 * 3, // 11

32 - 7 * 1, // 12
32 - 7 * 2, // 13
32 - 7 * 1, // 14
0, // 15
};

我们来实战分析一下

1720437996161

定位到dispTryBlockMap以后,取出一个字节,这个字节如图就是4,然后取4这个字节的低4位,那么还是4,这就是lengthBits。

查表s_negLengthTab得到negLength是-1

查表得shift是 32 - 7 * 1

那么result存的值是指向的就是原来的buffer +1 -4 = buffer -3

1720438446498

计算得到result得到这个值,然后还要右移shift位,也就是25位,等价于取4的高7位,得到数字2,即_numTryBlocks为2。也就是说有在这个函数中,有两个try

ReadInt

1720428033554

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; // Handler Type adjectives (bitfield)
int32_t dispType; // Image relative offset of the corresponding type descriptor这里是指向一个RTTI,描述类型信息的
uint32_t dispCatchObj; // Displacement of catch object from base
int32_t dispOfHandler; // Image relative offset of 'catch' code
uintptr_t continuationAddress[MAX_CONT_ADDRESSES]; // Continuation address(es) of catch funclet
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
{
// See contAddr for description of these values
enum contType
{
NONE = 0b00,
ONE = 0b01,
TWO = 0b10,
RESERVED = 0b11
};
union
{
#pragma warning(push)
#pragma warning(disable: 4201) // nonstandard extension used: nameless struct/union
struct
{
uint8_t adjectives : 1; // Existence of Handler Type adjectives (bitfield)
uint8_t dispType : 1; // Existence of Image relative offset of the corresponding type descriptor
uint8_t dispCatchObj : 1; // Existence of Displacement of catch object from base
uint8_t contIsRVA : 1; // Continuation addresses are RVAs rather than function relative, used for separated code
uint8_t contAddr : 2; // 1. 00: no continuation address in metadata, use what the catch funclet returns
// 2. 01: one function-relative continuation address
// 3. 10: two function-relative continuation addresses
// 4. 11: reserved
uint8_t unused : 2;
};
#pragma warning(pop)
uint8_t value;
};
};

又是一个位段

如何定义这个位段呢?

我们发现有的位段占了两个比特,那这种情况需要如何定义呢??

1720489301561

因此如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 {
// no encoded cont addresses or unknown
}
}
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 {
// no encoded cont addresses or unknown
}
}
}

解析结束后长这样

1720493246639

IPtoStateMapEntry4

1
2
3
4
struct IPtoStateMapEntry4 {
int32_t Ip; // Image relative offset of 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);
// States are encoded +1 so as to not encode a negative
IPEntry.State = ReadUnsigned(currOffset) - 1;

return IPEntry;
}

照着这样解析即可

1720494427956

1
IPEntry.Ip = _funcStart + prevIp + ReadUnsigned(currOffset);

得到的IP是在原来的基础上叠加的,FuncStart指的是有try块的函数地址,然后prevIp指的是前面计算出来的ip基础上继续加的

至此,所有结构我们全部分析完毕!!!

不得不说难度超级大!(吐血