本文最早上传于先知社区IDA旧版本插件移植后卡死的研究及修复-先知社区 (aliyun.com)

修复后ida下载地址

通过网盘分享的文件:IDA_Pro_v8.3_Portable.7z
链接: https://pan.baidu.com/s/1AWm31qhGrJ1IK5wKPQwJUQ?pwd=ddos 提取码: ddos

如还存在问题可以联系Q :3824970852 笔者抽空时可以研究修复一下

近日在研究异构路由器漏洞时,笔者使用的IDA出现了反编译异构路由器卡死的问题,和身边是师傅们交流后发现,这种问题不在少数,这种工具类的问题严重影响了正常的工作,故进行研究。

​ 出现问题的直接原因在于此前由于IDA 8.3及以后的hexrays并未泄露,无法使用过去IDA 7.7的插件进行反编译,一通操作下通过ida_dll_shim等项目对7.7的hexrays进行了移植。移植完成后虽然可以使用,但部分函数仍出现IDA反编译时出现卡死等情况,导致vulfi等一众扫描类的插件不能以预期状态运行。

​ 不过功夫不负有心人,笔者在搜寻的过程中找到了一篇帖子 在新版IDA中使用旧版本反编译插件,帖子提供了一种修复的方法,但帖子中提供的修复方法并非不详细,并且修复的方法并不合理对正常逆向存在一定的影响,故笔者决定自己对这次异常进行研究,并给予修复方案。

问题分析

​ 在研究开始时,我的第一反应是需要了解IDA是在什么函数的什么地方出现了卡死,毕竟只有跟踪到了出现异常的点,才能进一步研究。这里我选择了xdbg进行跟踪分析函数调用,来解答我的下面两个疑问:

  • 出现问题的函数是什么?
  • 产生问题的原因是什么?

​ 根据异常出现在arm32位反编译的情况下,不难猜测出这个问题大概率出现在hexarm.dll这个链接库中,所以接下来的分析主要针对涉hexarm.dll调用的相关函数。为了找到卡死的点,我先写了一个IDAPython脚本,来依次反编译程序的所有函数,直至卡死为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import idautils
import idaapi

def decompile_all_functions():
functions = list(idautils.Functions())
total = len(functions)
for i, func_ea in enumerate(functions):
func_name = idaapi.get_func_name(func_ea)
print(f"\r进度: {i+1}/{total} | 当前函数: {func_name}", end="")
try:
cfunc = idaapi.decompile(func_ea)
if not cfunc:
idc.generate_pseudo(func_ea)
except Exception as e:
print(f"\n反编译失败 @ {func_name} (0x{func_ea:x}): {str(e)}")

if __name__ == "__main__":
decompile_all_functions()

​ 这里我使用的是一个16KB的小的ARM32文件,在正常情况下只需要不到几秒中的时间就会运行完毕,但发现即使经过长时间后程序仍然卡在在sub_8DB0这里,那么不必多是,此时就是想要的异常情况了。

image-20250529203002649-1748747986602-1.png

​ 接下来我使用xdbg来附加现在处于卡死状态的IDA,为了找出此时死循环运行的函数,我通过alt+k去查看目前函数的调用关系,来确定函数,下面是我捕获的函数调用关系(主线程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
线程 ID       地址               返回到              返回自              大小  方    注释                                                                                                                                      
13192 - 主线程
0000001794DF9408 00000000589D775B 00000000592B2861 30 用户模块 ida-orig.is_numop+191
0000001794DF9438 00000000589D5321 00000000589D775B 60 用户模块 hexarm_0.00000000589D775B
0000001794DF9498 00000000589E08EE 00000000589D5321 30 用户模块 hexarm_0.00000000589D5321
0000001794DF94C8 0000000058967AFE 00000000589E08EE 30 用户模块 hexarm_0.00000000589E08EE
0000001794DF94F8 0000000058967D78 0000000058967AFE 30 用户模块 hexarm_0.0000000058967AFE
0000001794DF9528 0000000058967C20 0000000058967D78 30 用户模块 hexarm_0.0000000058967D78
0000001794DF9558 0000000058967D78 0000000058967C20 30 用户模块 hexarm_0.0000000058967C20
0000001794DF9588 000000005896D1F1 0000000058967D78 30 用户模块 hexarm_0.0000000058967D78
0000001794DF95B8 0000000058967AFE 000000005896D1F1 30 用户模块 hexarm_0.000000005896D1F1
0000001794DF95E8 0000000058967D78 0000000058967AFE 30 用户模块 hexarm_0.0000000058967AFE
0000001794DF9618 0000000058967C5F 0000000058967D78 30 用户模块 hexarm_0.0000000058967D78
0000001794DF9648 0000000058967EA4 0000000058967C5F 70 用户模块 hexarm_0.0000000058967C5F
0000001794DF96B8 00000000589D0829 0000000058967EA4 C0 用户模块 hexarm_0.0000000058967EA4
0000001794DF9778 00000000589689B1 00000000589D0829 B0 用户模块 hexarm_0.00000000589D0829
0000001794DF9828 0000000058924226 00000000589689B1 220 用户模块 hexarm_0.00000000589689B1
0000001794DF9A48 00000000588555CC 0000000058924226 1C0 用户模块 hexarm_0.0000000058924226
0000001794DF9C08 0000000058854904 00000000588555CC 50 用户模块 hexarm_0.00000000588555CC
0000001794DF9C58 0000000058921295 0000000058854904 400 用户模块 hexarm_0.0000000058854904
0000001794DFA058 00007FFDACEB1646 0000000058921295 E0 用户模块 hexarm_0.0000000058921295
0000001794DFA138 00007FFD95A4DF8D 00007FFDACEB1646 30 用户模块 _ida_hexrays.00007FFDACEB1646

​ 通过栈帧的调用关系可以发现程序在不停止的运行ida-orig.is_numop+191函数,此函数是ida.dll中的API,7.7的插件这里出现了问题,且两个插件本在各自的版本都没有出现问题,移植后却出现卡死的问题,这里合理怀疑是IDA对这个API进行了部分改动,所以我特地去查看了其相关的信息。

image-20250529211942841-1748747986603-2.png

​ 根据hexray官网给出的信息,这个函数确实在7.x到8.x进行了内容的变动,并且这个函数的功能是判断转入的数是否为该进制表示方法数字。

​ 对此,问题出现的原因就已经明确了,IDA再进行更新时有着API的变动,而可能时函数内部逻辑的改变使得结果的不同,导致陷入死循环,那么接下来就是这时候我选择先对IDA7.7和IDA8.4的ida-orig.is_numop进行对比分析。

is_numop函数分析

7.7版本代码

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
bool __fastcall is_numop(int a1, char a2)
{
int v2; // edx
int v3; // eax
int v4; // ecx
int v6; // ecx

v2 = a2 & 0xF;
if ( v2 )
{
if ( v2 == 15 )
{
v3 = a1 & 0xF00000;
if ( (a1 & 0xF00000) == 0x200000 || v3 == 0x700000 || v3 == 0x600000 || v3 == 0x100000 )
return 1;
}
v4 = a1 & 0xF000000;
if ( v4 == 0x2000000 || v4 == 0x7000000 || v4 == 0x6000000 )
return 1;
return v4 == 0x1000000;
}
else
{
v6 = a1 & 0xF00000;
if ( v6 == 0x200000 || v6 == 0x700000 || v6 == 0x600000 )
return 1;
return v6 == 0x100000;
}
}

8.4版本代码

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
bool __fastcall is_numop(__int64 a1, char a2)
{
unsigned int v2; // r8d
unsigned int v3; // eax
__int64 v4; // r10
__int64 v5; // r8
__int64 v6; // r10
__int64 v7; // r8
__int64 v8; // r10
__int64 v9; // r8
__int64 v10; // r10
__int64 v11; // r8

v2 = a2 & 0xF;
if ( v2 == 15 )
{
v3 = 0;
while ( 1 )
{
if ( v3 > 7 )
{
v4 = 0i64;
v5 = 0i64;
}
else
{
v4 = 2i64 << (4 * ((unsigned __int8)v3 + (v3 > 1) + 5));
v5 = 15i64 << (4 * ((unsigned __int8)v3 + (v3 > 1) + 5));
}
if ( (a1 & v5) == v4 )
break;
if ( v3 > 7 )
{
v6 = 0i64;
v7 = 0i64;
}
else
{
v6 = 7i64 << (4 * ((unsigned __int8)v3 + (v3 > 1) + 5));
v7 = 15i64 << (4 * ((unsigned __int8)v3 + (v3 > 1) + 5));
}
if ( (a1 & v7) == v6 )
break;
if ( v3 > 7 )
{
v8 = 0i64;
v9 = 0i64;
}
else
{
v8 = 6i64 << (4 * ((unsigned __int8)v3 + (v3 > 1) + 5));
v9 = 15i64 << (4 * ((unsigned __int8)v3 + (v3 > 1) + 5));
}
if ( (a1 & v9) == v8 )
break;
if ( v3 > 7 )
{
v10 = 0i64;
v11 = 0i64;
}
else
{
v10 = 1i64 << (4 * ((unsigned __int8)v3 + (v3 > 1) + 5));
v11 = 15i64 << (4 * ((unsigned __int8)v3 + (v3 > 1) + 5));
}
if ( (a1 & v11) == v10 )
break;
if ( (int)++v3 >= 8 )
return 0;
}
return 1;
}
else
{
return v2 < 8
&& ((a1 & (15i64 << (4 * ((unsigned __int8)v2 + (v2 > 1) + 5)))) == 2i64 << (4*unsigned __int8)v2 + (v2 > 1) + 5))
|| (a1 & (15i64 << (4 * ((unsigned __int8)v2 + (v2 > 1) + 5)))) == 7i64 << (4*((unsigned __int8)v2 + (v2 > 1) + 5))
|| (a1 & (15i64 << (4 * ((unsigned __int8)v2 + (v2 > 1) + 5)))) == 6i64 << (4*unsigned __int8)v2 + (v2 > 1) + 5))
|| (a1 & (15i64 << (4 * ((unsigned __int8)v2 + (v2 > 1) + 5)))) == 1i64 << (4*((unsigned __int8)v2 + (v2 > 1) + 5)));
}
}

分析

这里我们从复杂的8.4版本入手,抛开其他结构只观察函数逻辑本身的情况下,函数is_numop的作用是检查给定的64位整数a1中特定的四位字段是否匹配预定义的数值模式,具体取决于第二个参数a2的低四位值。

关键逻辑分析:

  • **当v2 == 15**:遍历8组不同的四位字段,检查是否有任意一组匹配以下模式之一:0x20x70x60x1
  • **当v2 < 8**:直接检查a1中由v2指定的四位字段是否匹配上述模式。
  1. 字段位置计算

    • 每个四位字段的位置由公式 4 * (v3 + (v3 > 1) + 5)4 * (v2 + (v2 > 1) + 5) 确定。
    • 例如:
      • v3 = 0,字段位于第20-23位(4 * (0 + 0 + 5) = 20)。
      • v3 = 2,字段位于第32-35位(4 * (2 + 1 + 5) = 32)。
  2. 模式匹配

    • 每个四位字段需要等于以下值之一(掩码为0xF左移后的结果):

      • 0x2(二进制0010
      • 0x7(二进制0111
      • 0x6(二进制0110
      • 0x1(二进制0001

​ 而我们分析完复杂的8.4版本后,再看一眼7.7版本时,顿时感到7.7的函数眉清目秀的,这里就不详细分析它的逻辑了,只给出它俩的差异。

特性 8.4 7.7
参数类型 __int64 a1 (64位) int a1 (32位)
位域覆盖范围 8个位域 (位20-23,24-27,32-35,36-39,…) 仅2个位域 (位20-23, 24-27)

​ 简单的理解来看新版的is_numop传入的参数a1是去判读64个bit的操作,而7.7版本及以前都是32个bit判断的简单逻辑,并且逻辑不完全相同。既然分析出了具体原因,接下来的就是对dll文件进行修复了。

修复问题

修复思路

修复这个问题,也分为多个思路,而笔者并不采用先前文章中的修复方法(先前文章是通过使用is_numop0替换is_numop),因为新版is_numop0函数的本质逻辑是调用了is_numop(a1,0),根据我们上方的分析来看,is_numop0对应的判断逻辑应该是这样的

1
2
3
4
5
6
7
bool __fastcall is_numop0(__int64 a1)
{
return (a1 & 0xF00000) == 0x200000
|| (a1 & 0xF00000) == 0x700000
|| (a1 & 0xF00000) == 0x600000
|| (a1 & 0xF00000) == 0x100000;
}

​ 这样的情况下is_numop0只会对20-23位进行操作,但正常情况下对24-27的判断就会失效,也就是当n传入的值不等于00xf时,进行的判断和原先的函数是不同的。为了不影响反编译的结果和程序的健壮性,我需要一个更靠谱的方法去修复。

​ 最容易想到的方法是将老版本is_numop写入hexarm.dll并修改名称,并将原先的is_numop调用修改为新函数,不过这种方法在操作上稍显麻烦。第二种容易想到方法是根据函数的逻辑,对函数外的代码进行部分修改,使他进入函数是时满足条件切正确,但过少的空间使得第二种方法并不容易,甚至无法完成,第三种方法是则是通过dll劫持,将is_numop指向到新的函数。思考再三,为了追求稳定性,最终选择了第三种修复方法。

在修复之前,先缕一便当前IDA在执行反编译时链接库之间的依赖关系,下图是笔者修复前链接库的关系。

image-20250530144708554-1748747986603-3.png

​ 经过上图我们可以看到,我先前使用的项目ida_dll_shim本质上就是进行了一个dll的劫持来修复旧版本插件不识别的问题,但由于is_numop只是改变函数内容,如果将把8.4dllis_numop直接修改为7.7的is_numop后,原先自带的8.4hexrays-x86_64则可能出现问题,为了不影响新插件的功能,故这个项目并没有对这种内容改变函数进行明确修改,最终导致这个问题。

​ 再回头来考虑我们修复的实现方法,为解决这个问题我们可以通过在ida.dll中创建一个新的函数is_num0p来代替is_numop,并在老版本插件将新函数的替换原先的is_num0p

image-20250530205623078-1748747986603-4.png

修复操作

首先我们要重新编译ida_dll_shim中的ida(这里建议大家先看一遍原先的项目文件,并跟着项目操作完,因为这里是在操作完一次项目操作后进行的,这里我默认大家已经跟着项目修复完第一次了,已经有ida-origida64-orig文件),上文提到过,ida_dll_shim是通过dll劫持实现的,本质上是创建一个新ida.dll转发了ida-orig中的函数,并添加了新函数,这里我们在项目的ida_dll_exports.h中创建一个新的函数并放入老版ida的代码。

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
// 下列代码添加到ida_dll_exports.h 末尾
idaman int ida_export is_num0p(int a1, char a2)
{
int v2; // edx
int v3; // ecx
int v5; // eax
int v6; // ecx
int v7; // ecx

v2 = a2 & 0xF;
if ( v2 )
{
if ( v2 == 15 )
{
v5 = a1 & 0xF00000;
if ( (a1 & 0xF00000) == 6291456 )
return 1i64;
switch ( v5 )
{
case 2097152:
return 1i64;
case 1048576:
return 1i64;
case 7340032:
return 1i64;
}
v6 = a1 & 0xF000000;
if ( v6 == 100663296 || v6 == 0x2000000 || v6 == 0x1000000 )
return 1i64;
return v6 == 117440512;
}
else
{
v3 = a1 & 0xF000000;
if ( v3 == 0x2000000 || v3 == 117440512 || v3 == 100663296 )
return 1i64;
return v3 == 0x1000000;
}
}
else
{
v7 = a1 & 0xF00000;
if ( v7 == 0x200000 || v7 == 7340032 || v7 == 6291456 )
return 1i64;
return v7 == 0x100000;
}
}

​ 最终使用VS重新编译出ida和ida64,替换第一次生成的ida和ida64,生成完成后可以使用ida查看export来检查是否生成正确,正确的情况下是可以看到is_numopis_num0p同时存在并且可以点击进入的。至此,我们对ida.dll的修复已经完成。接下来就是改变hex异构反编译dll的导入函数表了。

这里我使用的是CFF ,将异构hex反编译dll->importDirectory->导入函数is_numop改为is_num0p并保存,如图是修改后的情况。

image-20250530214656586-1748747986603-5.png

最终结果测试

修复完成后再一次启动ida,并用xdbg进行跟踪,发现程序已经进入到我们所创建的函数中。

image-20250531111803502-1748747986603-6.png

再次运行测试案例,程序可以正常运行完毕,并未出现卡死的情况,至此我们的修复已经完成。