IDA PRO 0B - 类型修复

2023-12-20 13:30:36

?当对二进制文件进行逆向工程时,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

然后将伪代码的对应变量设置成结构体类型即可。

总结

结构体修复

  • 使用程序中的字符串来恢复字段名

  • 使用全局交叉引用来确定字段类型

文章来源:https://blog.csdn.net/a5right/article/details/135103158
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。