当对二进制文件进行逆向工程时,IDA PRO可能无法正确识别变量和函数的数据类型。这可能导致分析错误并阻碍对代码的理解。IDA PRO的类型修复功能通过允许用户手动指定或纠正数据类型来解决这个问题。
通过使用类型修复功能,用户可以提高反汇编代码的准确性和可读性。这在分析复杂的二进制文件或处理涉及各种数据结构和函数调用的代码时特别有用。
这段话是 notion 的 ai 生成的,有点意思啊,可惜教育版只有10次试用机会。
本文的实验材料已上传到 p21
函数返回值类型
对于没有返回值的函数,我们先查一下这个函数的所有交叉引用,确保这个函数每一处调用都不引用返回值。
确定该函数的返回值没有用处之后,我们就可以更改该函数的签名,将函数的返回值类型修改为 void,这样 IDA 生成的反汇编代码会更简洁。
以 sub_4011F0 这个函数为例(实验材料T7,是一个32位程序):
其交叉引用只有一处:
看伪代码,并没有使用返回值,所以我们直接更改其签名,使用 Y 快捷键:
点击 ok 之后,伪代码变成下面模样:
可以看到,基本上代码量缩小了一半,因为类型修正后, IDA 会进一步优化代码逻辑(因为返回值没有用到,所以就直接剪除了与返回值相关的指令),是可读性更高。
指针类型修复
通常情况下,IDA的伪代码里面都是一个int类型打天下,几乎所有函数的参数都是 int 类型。这是因为 IDA 将很多指针类型识别成了整数类型。
以 sub_4011F0 这个函数为例(实验材料T7,是一个32位程序):
int __cdecl sub_401270(int a1, const char *a2, int a3)
{
signed int v3; // kr00_4
int result; // eax
signed int v5; // [esp+10h] [ebp-8h]
v5 = 0;
v3 = strlen(a2);
while ( v5 < v3 )
{
*(_BYTE *)(a3 + *(_DWORD *)(a1 + 4 * v5)) = a2[v5];
++v5;
}
result = v5 + a3;
*(_BYTE *)(v5 + a3) = 0;
return result;
}
伪代码中,最重要的是这一行:
*(_BYTE *)(a3 + *(_DWORD *)(a1 + 4 * v5)) = a2[v5];
我们可以看到,在对 a1 进行访问的时候,是将它做了一些偏移,然后以 _DWORD * 的形式来进行访问。所以,a1 的正确类型应该是一个int *
类型,同理,a3 是一个 byte *
类型,更准确一点来说是一个 char *
。
我们更改其类型后,伪代码变为如下:
byte *__cdecl sub_401270(int *a1, const char *a2, char *a3)
{
signed int v3; // kr00_4
byte *result; // eax
signed int v5; // [esp+10h] [ebp-8h]
v5 = 0;
v3 = strlen(a2);
while ( v5 < v3 )
{
a3[a1[v5]] = a2[v5];
++v5;
}
result = &a3[v5];
a3[v5] = 0;
return result;
}
这下程序看起来就很像人写的了。同样的,这个函数的返回值也是没有用到的,所以我们可以继续优化代码,再综合程序的逻辑,给参数进行重命名,基本就可以完全还原源代码了:
void __cdecl sub_401270(int *table, const char *input, char *output)
{
signed int v3; // kr00_4
signed int v4; // [esp+10h] [ebp-8h]
v4 = 0;
v3 = strlen(input);
while ( v4 < v3 )
{
output[table[v4]] = input[v4];
++v4;
}
output[v4] = 0;
}
可以看出 table 是一个 index 乱序表。
数组修复
以 main 这个函数为例(实验材料T7,是一个32位程序),这是一个栈数组的例子:
int __cdecl main(int argc, const char **argv, const char **envp)
{
...
char Str[5]; // [esp+84h] [ebp-38h] BYREF
char Source[47]; // [esp+89h] [ebp-33h] BYREF
...
if ( sub_401000(Str, "flag{") != (_DWORD)Str || Source[32] != 125 )
sub_401340(v6);
...
return 0;
}
看到 if 语句里面,Source有个对 32 位置的引用,而Source却没有被初始化,看上去有点蛋疼。
这是因为IDA 错误识别了 Str 数组大小,导致 Str 数组的一部分变成了 Source 数组。
我们将 Str 的大小改为 5 + 47 = 52,看看伪代码变化(IDA会提示Source会被覆盖,这是我们期望的,点击Yes即可):
int __cdecl main(int argc, const char **argv, const char **envp)
{
。。。
char Str[52]; // [esp+84h] [ebp-38h] BYREF
。。。
if ( sub_401000(Str, "flag{") != (_DWORD)Str || Str[37] != 125 )
sub_401340(v6);
。
return 0;
}
现在,Str 与 Source 就合并了。
再看一个全局数组的例子,还是 main 函数:
sub_401270(a1, Destination, a3);
这里对 a1 有一个引用,它是一个全局数组,其作用我们上面已经分析过了,是一个索引乱序表。它的大小固定是32位(分析sub_4011F0可知),所以我们可以对其建立一个数组:
点击第一项地址按 d 键调整 a1 第一个元素的大小为数组元素类型对应的类型字节大小。右键选择 【Array】,在 【Array Size】填上数组对应的元素个数,最后点击 【ok】。
枚举值修复
IDA 的类型数据库内置了常见的枚举(宏)的值,可以直接引入并修复。这可以增加一下常量值的可读性,比如,我们使用 ptrace 函数,需要转递一个常量值,IDA 在反汇编的时候不会展示其常量值名字,只会展示数值,所以我们可以修复一下。
以sub_401F2F为例(实验材料:ptrace1):
这段伪代码里面的 sub_44CC50 就是 ptrace 函数,点进去可以看到 sys_ptrace 的调用。
选中 12 这个数字,按下快捷键 "M",在弹出的窗口中选择对应的常量值,在弹出的窗口 CTRL + F 搜索 ptrace 相关的常量值,可以找到 PTRACE_GETREGS,不知道常量值名字可以去查查开发文档。
最终函数变成了:
((void (__fastcall *)(_QWORD, _QWORD, _QWORD, char *))sub_44CC50)(PTRACE_GETREGS, a1, 0LL, v7);
结构体修复
确定结构体大小
-
内存分配可以直接确定结构体大小
-
memcpy / 局部变量偏移差 -> 间接确定 (结构体/类局部变量)
以 sub_2A83 为例(实验材料:monopoly):
v0 = operator new(0x70uLL);
sub_2602(v0, "Arbington", 0LL, 0LL, 0LL, 0LL);
qword_A1C0 = v0;
v1 = operator new(0x70uLL);
sub_2602(v1, "Bredwardine", 0LL, 0LL, 0LL, 2LL);
qword_A240 = v1;
v2 = operator new(0x70uLL);
sub_2602(v2, "Dangarnon", 0LL, 0LL, 0LL, 2LL);
qword_A2C0 = v2;
可以看到为代码里面有很多 new 操作,我们为其创建一个结构体,由于我们对这个结构体信息知道的非常少,所以我们先使用一些字段来填充这个结构体,让其大小为0x70即可:
00000000 struc_1 struc ; (sizeof=0x70, mappedto_9)
00000000 field_0 dq ?
00000008 field_8 dq ?
00000010 field_10 dq ?
00000018 field_18 dq ?
00000020 field_20 dq ?
00000028 field_28 dq ?
00000030 field_30 dq ?
00000038 field_38 dq ?
00000040 field_40 dq ?
00000048 field_48 dq ?
00000050 field_50 dq ?
00000058 field_58 dq ?
00000060 field_60 dq ?
00000068 field_68 dq ?
00000070 struc_1 ends
选择使用 dq 来填充,一是程序是64位,二是先看看其效果,如果字段对不上的话,IDA会出现一些奇怪的伪代码,我们后面修复即可。
创建好结构体之后,我们将相关变量、参数的类型修改为该结构体:
-
方法一:右键变量,Convert to struct * …
-
方法二:Y 键,手动输入类型定义
我们,对 v1 使用结构体之后,伪代码如下:
v1 = (struc_1 *)operator new(0x70uLL);
同样的,将其他的变量与参数都改下。
在伪代码中,new 操作后面都会跟一个 sub_2602 函数,其第一个参数是我们创建的结构体类型,更改其类型后,该函数伪代码如下:
实际上 sub_2602 是构造函数。
我们在使用new operator的时候,实际上是执行了三个步骤:
1)调用operator new分配内存 ;2)调用构造函数生成类对象;3)返回相应指针。
unsigned __int64 __fastcall sub_2602(struc_1 *a1, __int64 a2, int a3, int a4, int a5, int a6)
{
char v11; // [rsp+2Bh] [rbp-45h] BYREF
int i; // [rsp+2Ch] [rbp-44h]
char v13[40]; // [rsp+30h] [rbp-40h] BYREF
unsigned __int64 v14; // [rsp+58h] [rbp-18h]
v14 = __readfsqword(0x28u);
std::string::basic_string(a1);
std::string::basic_string(&a1->field_20);
if ( a6 == 1 )
{
for ( i = 0; i <= 4; ++i )
*((_DWORD *)&a1->field_48 + i + 1) = a3 / 2 * (i + 1);
std::string::operator=(&a1->field_20, &unk_A1A0);
a1->field_40 = 0LL;
LODWORD(a1->field_48) = -1;
LODWORD(a1->field_60) = a4;
HIDWORD(a1->field_60) = a5;
LODWORD(a1->field_68) = a4 / 2;
}
HIDWORD(a1->field_68) = a6;
std::allocator<char>::allocator(&v11);
std::string::basic_string(v13, a2, &v11);
std::string::operator=(a1, v13);
std::string::~string(v13);
std::allocator<char>::~allocator(&v11);
return __readfsqword(0x28u) ^ v14;
}
会发现,伪代码里面出现了 LODWORD HIDWORD 这种代码。
这是因为field_48\field_60\field_68 都是 DWORD 类型的字段,而我们定义结构体使用的是 dq,所以我们需要将结构体字段进行拆分:
-
在有问题的字段上按下 d 键,切成 dd
最终结构体如下:
00000000 struc_1 struc ; (sizeof=0x70, mappedto_9)
00000000 field_0 dq ?
00000008 field_8 dq ?
00000010 field_10 dq ?
00000018 field_18 dq ?
00000020 field_20 dq ?
00000028 field_28 dq ?
00000030 field_30 dq ?
00000038 field_38 dq ?
00000040 field_40 dq ?
00000048 field_48 dd ?
0000004C field_4C dd ?
00000050 field_50 dq ?
00000058 field_58 dq ?
00000060 field_60 dd ?
00000064 field_64 dd ?
00000068 field_68 dd ?
0000006C field_6C dd ?
00000070 struc_1 ends
调整完成之后,回到伪代码界面,按 F5 刷新伪代码,发现奇怪的指令正常了:
unsigned __int64 __fastcall sub_2602(struc_1 *a1, __int64 a2, int a3, int a4, int a5, int a6)
{
...
for ( i = 0; i <= 4; ++i )
*(&a1->field_4C + i) = a3 / 2 * (i + 1);
std::string::operator=(&a1->field_20, &unk_A1A0);
a1->field_40 = 0LL;
a1->field_48 = -1;
a1->field_60 = a4;
a1->field_64 = a5;
a1->field_68 = a4 / 2;
...
}
继续观察伪代码,发现循环里面,有对 field_4C 进行循环访问,而且比较像是对数组做访问。所以我们可以将 field_4C 改成一个数组,其大小为 5:
0000004C field_4C dd 5 dup(?)
再刷新伪代码:
for ( i = 0; i <= 4; ++i )
a1->field_4C[i] = a3 / 2 * (i + 1);
回到 sub_2A83 函数中:
v0 = operator new(0x70uLL);
sub_2602(v0, "Arbington", 0LL, 0LL, 0LL, 0LL);
qword_A1C0 = v0;
v1 = (struc_1 *)operator new(0x70uLL);
sub_2602(v1, "Bredwardine", 0LL, 0LL, 0LL, 2LL);
qword_A240 = (__int64)v1;
v2 = operator new(0x70uLL);
sub_2602(v2, "Dangarnon", 0LL, 0LL, 0LL, 2LL);
qword_A2C0 = v2;
每次 new 出来的对象,都赋值给了一个全局的地址 qword_A1C0/qword_A240/qword_A2C0等等,大概过一遍所有的地址,发现这是一个指针数组,元素间隔为8。
我们将 qword_A1C0 定义成指针数组:
struc_1 *qword_A1C0[64];
在 qword_A1C0 建立一个数组,刷新伪代码:
v0 = (struc_1 *)operator new(0x70uLL);
sub_2602(v0, (__int64)"Arbington", 0, 0, 0, 0);
qword_A1C0[0] = v0;
v1 = (struc_1 *)operator new(0x70uLL);
sub_2602(v1, (__int64)"Bredwardine", 0, 0, 0, 2);
qword_A1C0[16] = v1;
回到 main 函数:
sub_28BA(&unk_A3C0, 0LL);
sub_28BA(&unk_A440, 1LL);
有两个全局变量的引用,它们之间相差 0x80。使用交叉引用看一下其他位置对这两个变量的引用,使用了同一个函数,可以知道这两个变量是同一个类型。
猜测它是一个结构体,且结构体大小为0x80。当然改的时候记得备份一下,万一猜错也好恢复,虚拟机就直接来个快照就好了。
建立一个0x80大小的结构体,将 sub_28BA 的 a1 参数转换为该结构体指针类型,做一下字段修复,刷新伪代码:
unsigned __int64 __fastcall sub_28BA(struc_2 *a1, int a2)
{
char v3; // [rsp+1Fh] [rbp-41h] BYREF
char v4[40]; // [rsp+20h] [rbp-40h] BYREF
unsigned __int64 v5; // [rsp+48h] [rbp-18h]
v5 = __readfsqword(0x28u);
if ( a2 )
{
std::allocator<char>::allocator(&v3);
std::string::basic_string(v4, "AI", &v3);
std::string::operator=(a1, v4);
std::string::~string(v4);
std::allocator<char>::~allocator(&v3);
}
else
{
puts("what's your name?");
std::operator>><char>(&std::cin, a1);
}
memset(&a1->field_20, 0, 0x40uLL);
a1->field_60 = 0x927C000000000LL;
a1->field_68 = 0;
return __readfsqword(0x28u) ^ v5;
}
要修复这个结构体,我们还需要更多信息,所以,我们去看看其他引用该结构体的函数 sub_29CA,将结构体继续修复(将field_60进行拆分):
00000000 struc_2 struc ; (sizeof=0x80, mappedto_10)
00000000 field_0 dq ?
00000008 field_8 dq ?
00000010 field_10 dq ?
00000018 field_18 dq ?
00000020 field_20 dq ?
00000028 field_28 dq ?
00000030 field_30 dq ?
00000038 field_38 dq ?
00000040 field_40 dq ?
00000048 field_48 dq ?
00000050 field_50 dq ?
00000058 field_58 dq ?
00000060 field_60 dd ?
00000064 field_64 dd ?
00000068 field_68 dd ?
0000006C field_6C dd ?
00000070 field_70 dq ?
00000078 field_78 dq ?
00000080 struc_2 ends
接下来我们尝试恢复结构体字段的名字,主要是依靠程序里面的输出字符串。
首先将,全局变量改为我们创建的结构体类型,这样IDA会将对结构体字段的引用都显示到伪代码里面,否则只会展示一个全局的地址,按 Y 键设置全局变量的类型为 struc_2:
sub_28BA(&global_struct1, 0);
sub_28BA(&global_struct2, 1);
再看 sub_4B43 这个函数,我们可以看到一些直接对结构体字段的引用:
printf("your money: %d\n", (unsigned int)global_struct1.field_64);
像这样的代码,我们就可以知道,field_64 的字段名应该是 money。
printf("%s throw %d, now location: %d, %s\n", v2, v4, v1, v0);
v1 是 field_68,所以其名字是 location。
sub_27EA(qword_A1C0[global_struct1.location]);
该函数传递进去了一个数组元素,我们知道,qword_A1C0 是一个指针数组,所以其参数应该是 struc_1 类型:
int __fastcall sub_27EA(struc_1 *a1)
{
const char *v1; // rax
v1 = (const char *)std::string::c_str(&a1->field_20);
printf("owner: %s\n", v1);
printf("worth: %d\n", (unsigned int)a1->field_60);
if ( (unsigned __int8)sub_54B4(&a1->field_20, &unk_A1A0) )
return printf("toll_road: %d\n", (unsigned int)a1->field_64);
else
return printf("toll_road: %d\n", (unsigned int)a1->field_4C[a1->field_48]);
}
这里又可以修复一些变量名。
再进入到函数 sub_45DF,里面有一行代码:
*(_DWORD *)(qword_A1C0[global_struct1.location]->field_40 + 100) += v4;
我们使用快捷键 CTRL + ALT + X,查看全局对 field_40 这个字段的交叉引用:
可以看到这个字段是一个结构体指针类型,修复其类型之后,代码如下:
qword_A1C0[global_struct1.location]->field_40->money += v4;
再看该函数的这一段:
puts("property idx>>");
v3 = sub_43D1();
if ( v3 >= global_struct1.field_60 )
{
puts("invalid idx!");
return 1LL;
}
sub_452F(*((unsigned __int8 *)&global_struct1.field_20 + v3));
sub_43D1 应该是一个读取输入的函数,不用分析。
v3 是我们输入的 index。
field_60 是 index 的上边界。
field_20 有对 index 做操作,所以怀疑它是一个数组,搜索一下其交叉引用:
memset(&a1->field_20, 0, 0x40uLL);
有一个地方初始化了该数组,数组大小为 64。再次刷新代码:
puts("property idx>>");
index = sub_43D1();
if ( index >= global_struct1.max_index )
{
puts("invalid idx!");
return 1LL;
}
sub_452F((unsigned __int8)global_struct1.field_20[index]);
虚表分析
虚表理论知识之前讲过了,这次实操一下(实验材料:vtable)。
过程非常的套路化:
-
找到一个虚表的函数表
-
创建虚表结构体 vtable
-
创建对象结构体,并将第一个成员的类型设置成虚表指针
修复后能看到正常函数名了(Ctrl+alt+X 也能进行交叉引用)。
void __fastcall xiaoming::xiaoming(object *this)
{
peopele::peopele((peopele *)this);
this->vt = (vtable1 *)off_3CF8;
}
从这里我们可以判断,xiaoming 是继承了 people。
.data.rel.ro:0000000000003CE8
.data.rel.ro:0000000000003CE8 ; Segment type: Pure data
.data.rel.ro:0000000000003CE8 ; Segment permissions: Read/Write
.data.rel.ro:0000000000003CE8 _data_rel_ro segment qword public 'DATA' use64
.data.rel.ro:0000000000003CE8 assume cs:_data_rel_ro
.data.rel.ro:0000000000003CE8 ;org 3CE8h
.data.rel.ro:0000000000003CE8 public _ZTV8xiaoming ; weak
.data.rel.ro:0000000000003CE8 ; `vtable for'xiaoming
.data.rel.ro:0000000000003CE8 00 00 00 00 00 00 00 00 _ZTV8xiaoming dq 0 ; DATA XREF: LOAD:0000000000000710↑o
.data.rel.ro:0000000000003CE8 ; offset to this
.data.rel.ro:0000000000003CF0 58 3D 00 00 00 00 00 00 dq offset _ZTI8xiaoming ; `typeinfo for'xiaoming
.data.rel.ro:0000000000003CF8 C8 12 00 00 00 00 00 00 off_3CF8 dq offset _ZN8xiaoming5printEv ; DATA XREF: xiaoming::xiaoming(void)+1C↑o
.data.rel.ro:0000000000003CF8 ; xiaoming::print(void)
.data.rel.ro:0000000000003D00 06 13 00 00 00 00 00 00 dq offset _ZN8xiaoming3eatEv ; xiaoming::eat(void)
.data.rel.ro:0000000000003D08 58 13 00 00 00 00 00 00 dq offset _ZN8xiaoming5sleepEv ; xiaoming::sleep(void)
.data.rel.ro:0000000000003D10 AA 13 00 00 00 00 00 00 dq offset _ZN8xiaoming4workEv ; xiaoming::work(void)
.data.rel.ro:0000000000003D18 FC 13 00 00 00 00 00 00 dq offset _ZN8xiaoming3dayEv ; xiaoming::day(void)
找到 vtable 后,我们创建对应的结构体:
00000000 vtable1 struc ; (sizeof=0x28, mappedto_11)
00000000 print dq ?
00000008 eat dq ?
00000010 sleep dq ?
00000018 work dq ?
00000020 day dq ?
00000028 vtable1 ends
00000028
00000000 ; ---------------------------------------------------------------------------
00000000
00000000 object struc ; (sizeof=0x28, mappedto_12)
00000000 vt dq ? ; offset
00000008 field_8 dq ?
00000010 field_10 dq ?
00000018 field_18 dq ?
00000020 field_20 dq ?
00000028 object ends
00000028
然后将伪代码的对应变量设置成结构体类型即可。
总结
结构体修复
-
使用程序中的字符串来恢复字段名
-
使用全局交叉引用来确定字段类型