![](https://i-blog.csdnimg.cn/img_convert/01630fa1e0e988e34f4c0c24e234a7dc.png)
上一篇文章:《从0到1CTFer成长之路》逆向工程个人笔记--逆向工程基础
IDA 使用入门
加载文件
打开 IDA,点击 GO,即可把程序拖拽到 IDA 中
![](https://i-blog.csdnimg.cn/img_convert/646533c6ebdbf50056b6bd12dda104ab.png)
IDA 分为 32bit 和 64bit 两种架构,选择哪种结构,可以在把程序拖拽到 IDA 后,根据 Load a new file 界面决定是否更换另一种架构
![](https://i-blog.csdnimg.cn/img_convert/5fd9a0119834590b7899dda457d955af.png)
界面介绍
- 1:导航栏(程序不同类型的数据:普通函数,未定义函数的代码,数据,未定义)
- 2:反汇编主窗口
- 3:函数窗口(函数名称+地址)
- 4:输出窗口(下方输入框可以输入命令)
- 5:状态指示器(AU:idle 代表 IDA 完成了对程序的自动化分析)
![](https://i-blog.csdnimg.cn/img_convert/623ffb656cb8a6e91e7752399db9fced.png)
数据类型
在反汇编窗口,右键菜单 或按空格键可以在控制流图和文本界面之间切换
IDA 会将代码段多数数据正确标注为代码类型,并反汇编,特殊段部分位置标注为 8 字节整型 qword,用户可自行纠正
被标注为代码的位置:地址黑色显示
被标注为数据的位置:地址灰色显示
未定义数据类型的位置:显示黄色
![](https://i-blog.csdnimg.cn/img_convert/072c1b89169b9368a6ce195166f5fe3a.png)
定义数据类型的快捷键
- U 键:取消一个地方已有的数据类型定义
- D 键:让某一个位置变成数据,一直按,会改变字节大小(1/2/4/8)
- C 键:让某一个位置变成指令,定义为指令后,IDA 会自动以此为起始位置进行递归下降反汇编
- A 键:以该位置为起点定义一个以"\0"结尾的字符串类型
- *键:将此处定义为一个数组,可设置数组属性
- O 键:将此处定义为一个地址偏移
函数操作
反汇编不是完全连续的,而是由分散的各函数拼凑而成的。
每个函数都有局部变量、调用约定等信息,控制流图也只能以函数为单位生成和显示,所以需要正确定义函数
- 删除函数:在函数窗口中选中函数后,按 Delete
- 定义函数:在反汇编窗口中选中对应行后,按 P
- 修改函数参数:在函数窗口中选中并按 Ctrl+E,或者在反汇编窗口内部按 Alt+P
定义函数后就可以进行调用约定分析、栈变量分析、函数调用参数分析
导航操作
IDA 有导航历史功能,可以后退或前进到某次浏览的位置
- 后退到上一位置:Esc
- 前进到下一位置:Ctrl+Enter
- 跳转到某一特定为止:G,输入地址或已经定义的名称
- 跳转到某一区段:Ctrl+S
类型操作
处理 C/C++语言的各种数据类型(函数声明、变量声明、结构体声明),并且允许用户自由指定
选中变量或函数后按 Y,在对话框中输入正确的 C 语言类型,IDA 就可以解析并自动应用这个类型
![](https://i-blog.csdnimg.cn/img_convert/58298f43f1991913d0370909c457269f.png)
IDAPython
Alt+F7 或 File→Script file 菜单,可以执行 py 脚本文件
Shift+F2 或 File →Script command 菜单,打开脚本面板
其他功能
- Strings 窗口:Shift+F12 打开,可识别程序中的字符串
- 十六进制窗口:F2 对数据库数据进行修改,再按 F2 应用修改
HexRays 反编译入门
HexRays 作为 IDA 的插件运行,可以利用 IDA 确定的函数局部变量和数据类型,优化后生成类似 C 语言的伪代码,然后用户就可以浏览生成的伪代码,添加注释,重命名标识符,修改变量类型,切换数据的显示格式
生成伪代码
在反汇编窗口中定位到目标函数,按 F5 即可生成伪代码
![](https://i-blog.csdnimg.cn/img_convert/181fe46599cb8ed4494f07214b5daa03.png)
每个局部变量后面的注释实际代表这个变量所在的位置
在标识符上按 N,可以更换名称
![](https://i-blog.csdnimg.cn/img_convert/3571305f98cdf435e7a27527f31f0f5b.png)
切换数据显示格式
在常量点击右键,可以切换格式:
- Hexadecimal 十六进制,
- Octal 八进制
- Char:转化为诸如'A'的格式
- Enum:转化为枚举中的一个值
- Invert sign:按照补码解析为负数
- Bitwise negate:按位取反,~0xF0
修改变量类型
当源代码经过复杂的编译器优化流程后(如 GCC 编译器 O3 优化),再进行伪代码生成,会发现代码的阅读性差
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 i; // rsi
unsigned int v4; // eax
__m128i si128; // [rsp+0h] [rbp-98h]
char v7[16]; // [rsp+10h] [rbp-88h] BYREF
char v8[96]; // [rsp+20h] [rbp-78h] BYREF
int v9; // [rsp+80h] [rbp-18h]
unsigned __int64 v10; // [rsp+88h] [rbp-10h]
v10 = __readfsqword(0x28u);
strcpy(v7, "nf_ddzbff!}");
memset(v8, 0, sizeof(v8));
si128 = _mm_load_si128((const __m128i *)&xmmword_9F0);
v9 = 0;
__printf_chk(1LL, "Input your answer: ", envp);
__isoc99_scanf("%s", v8);
if ( strlen(v8) != 27 )
{
puts("Wrong input length!");
return 1;
}
for ( i = 0LL; i != 28; ++i )
{
LOBYTE(v4) = v8[i];
if ( (unsigned __int8)(v4 - 97) <= 0x19u )
{
v4 = (102 * ((char)v4 - 97) + 3) % 0x1Au + 97;
LABEL_4:
if ( si128.m128i_i8[i] != (_BYTE)v4 )
goto LABEL_9;
continue;
}
if ( (unsigned __int8)(v4 - 65) > 0x19u )
goto LABEL_4;
if ( si128.m128i_i8[i] != (102 * ((char)v4 - 65) + 3) % 0x1Au + 65 )
{
LABEL_9:
puts("Wrong answer!");
return 1;
}
}
puts("Congratulations!");
return 0;
}
这是因为 HexRays 把字符串数组识别成了 3 个变量,字符串赋值处理操作变成了 128 位浮点数赋值+64 位 qword 赋值+32 位 dword 赋值(__m128i 类型的 v6,__int64 的 v7,__int 的 v8)
- byte-1 字节整型,8 位,char、__int8
- word-2 字节整型,16 位,short、__int16
- dword-4 字节整型,32 位,int、__int32
- qword-8 字节整型,64 位,__int6,long long
也就是说,__m128i si128,v7,v8 实际上是整个字符串数组,因此我们需要修正变量的类型
在 v6 上按 Y,输入 char[28],28=16+8+4
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 i; // rsi
unsigned int v4; // eax
char v6[28]; // [rsp+0h] [rbp-98h] BYREF
char v7[96]; // [rsp+20h] [rbp-78h] BYREF
int v8; // [rsp+80h] [rbp-18h]
unsigned __int64 v9; // [rsp+88h] [rbp-10h]
v9 = __readfsqword(0x28u);
strcpy(&v6[16], "nf_ddzbff!}");
memset(v7, 0, sizeof(v7));
*(__m128i *)v6 = _mm_load_si128((const __m128i *)&xmmword_9F0);
v8 = 0;
__printf_chk(1LL, "Input your answer: ", envp);
__isoc99_scanf("%s", v7);
if ( strlen(v7) != 27 )
{
puts("Wrong input length!");
return 1;
}
for ( i = 0LL; i != 28; ++i )
{
LOBYTE(v4) = v7[i];
if ( (unsigned __int8)(v4 - 97) <= 0x19u )
{
v4 = (102 * ((char)v4 - 97) + 3) % 0x1Au + 97;
LABEL_4:
if ( v6[i] != (_BYTE)v4 )
goto LABEL_9;
continue;
}
if ( (unsigned __int8)(v4 - 65) > 0x19u )
goto LABEL_4;
if ( v6[i] != (102 * ((char)v4 - 65) + 3) % 0x1Au + 65 )
{
LABEL_9:
puts("Wrong answer!");
return 1;
}
}
puts("Congratulations!");
return 0;
}
第二段代码通过统一的数据存储方式 、清晰的变量访问方式 和简洁的逻辑结构,显著提高了代码的可读性。它更符合人类的思维习惯,减少了理解代码时的认知负担
HexRays 还可以修改参数类型、函数原型、全局变量类型、结构体、枚举
- Shift+F1:Local Types 窗口,可以操作 C 类型,再按 Insert:添加类型
- Ctrl+F9:加载 C 语言头文件
IDA 与 HexRays 进阶
寻找 main 函数
很多可执行文件时先经过 CRT 的初始化,再转到 main()函数
寻找 main 函数
- main()函数经常在可执行文件的靠前位置(因为很多链接器是先处理对象文件后处理静态库)
- VC 的入口点(IDA 中的 start()函数)会直接调用 main()函数,再 start()函数中被调用的函数有 3 个参数,并且返回值被传入 exit()函数的,可以重点查看
- GCC 将 main()函数的地址传入 __libc_start_main 来调用 main()函数,查看调用的参数即可找到 main()函数的地址
手动应用 FLIRT 签名
函数列表和导航条底色为青色的区域,与 FLIRT 函数前面识别库有关
Shift+F5 可以打开 Signature 列表,显示已经应用的函数签名库。查看导航条可以发现,VC(Visual C++) 运行时很多代码没有识别,这是因为 IDA 没有自动为这个程序应用其他 VC 运行时的签名。
处理 HexRays 失败情况
对于没有符号、优化等级较高的程序,HexRays 会经常出现各种失败情况,大多数出错原因是与这个函数相关的某些参数设置错误 ,如这个函数中调用其他函数的调用约定 出现错误,导致参数解析失败 或调用前后栈不平衡
比如:
- 一个使用 __stdcall 的函数被误认为使用 __cdecl 调用约定,两个调用约定清理参数空间的方法不一样,导致跟踪栈指针时出现问题
- 一个 __thiscall 被错误识别成 __fastcall。函数多出一个不存在的参数
- 某个 __fastcall 函数被错误识别成 __cdecl 函数,而参数个数都是 1,这时反编译器没办法找到栈上的参数,因为它实际上使用的是近期传参
下面给出两种常见的错误形式,注意:一开始的代码形式就是正常的,下面会故意改错,然后意在通过错误方式重新纠正代码
call analysis failed
文件:3-UPX_packed_dumo_SCY.exe(用 32 位的 IDA 打开)
刚进入就警告
![](https://i-blog.csdnimg.cn/img_convert/b36ee7c925a54803cac799bf9438b238.png)
IDA 检测到文件的 导入表(Imports Segment)可能被破坏或修改。这通常意味着文件可能被加壳(Packed)或经过某种形式的修改,目的是增加逆向分析的难度。这个与我们的主要目的无光,可以忽略此警告
进去之后,通过 start 函数寻找 main 函数,进入scrt_common_main_seh 函数
![](https://i-blog.csdnimg.cn/img_convert/aa754c940cf8a8e66ed6179eca9920bf.png)
双击右边的函数进入
![](https://i-blog.csdnimg.cn/img_convert/48c09d795b532e88ab74d30f7d84ab48.png)
双击第 42 行的函数
![](https://i-blog.csdnimg.cn/img_convert/feb22c6d5c08c9510964a94466b493b6.png)
即可进入 main 函数内部
![](https://i-blog.csdnimg.cn/img_convert/619447dd6f7c3c66cd4b7b517f3f5a41.png)
如果将 sub_271010 的类型从原来的"__thiscall sub_271010",修改为"__cdeccl sub_271010"。也就是修改调用约定(单击第三行,注意不要进入函数,按 Y 键就可以修改)
![](https://i-blog.csdnimg.cn/img_convert/27310ae21e1e8bbbcef9c4c83c315c26.png)
反编译器就会自动重新反编译进行刷新,然后弹出警告。此时就是我们人为地制作了一个错误,下面我们就来学习根据这个警告,反向地修正这个错误。
![](https://i-blog.csdnimg.cn/img_convert/3a852a038e137213a8354cc4057bc00c.png)
反编译失败并提示 call analysis failed
通常意味着反编译器在分析函数调用时遇到了问题,无法正确解析或重建调用关系。
根据警告按 G 跳转到 0x271006
![](https://i-blog.csdnimg.cn/img_convert/5787e70946dd6c353250827d26b57941.png)
通过刚刚的操作,我们可以明确知道,报错是因为我们修改了函数的调用约定,然后反编译器在寻找函数调用的参数的时候出现了错误,这时我们只要把在 0x271006 地址处的函数原型声明修复就可以了
在上图红框处按 Y,修改为int __thiscall sub_271010(int *this);
![](https://i-blog.csdnimg.cn/img_convert/0282479981f460fca22465196f61f2e6.png)
然后就可以发现,又可以进入 main 函数了,说明反编译又可以正常进行了。如果不修正的话,会发现我们想进入 main 函数,一定会出现上面的报错
![](https://i-blog.csdnimg.cn/img_convert/e5977b8268b6333a3fdc9aebe0c62f66.png)
sp-analysis failed
这种错误的原因是,当优化等级较高时,编译器将省略帧指针 rbp 的使用,转而使用 rsp 引用所有的局部变量,为了找到局部变量,IDA 通过跟踪每条指令对 rsp 的修改来查找并解析局部变量。但是 IDA 在跟踪 rsp 时出现了问题,导致反编译失败
根源是某个函数调用的调用阅读出错,或该函数的参数个数出错,导致 IDA 算错了栈指针的变化量
针对这种情况,"Options→General",在新对话框里,勾选"Stack pointer"(堆栈指针)
![](https://i-blog.csdnimg.cn/img_convert/077ab1f4c59d5a15f2b1ea42de2fc49c.png)
然后反汇编窗口每行的地址旁边就会多出一列,即 IDA 分析的函数执行到每个地址时栈的偏移量
![](https://i-blog.csdnimg.cn/img_convert/c18f3a61427aec2e5b3590a820b865fd.png)
对于没有使用动态长度数组的正常程序,在初始化完毕,调用前后栈的偏移量不变
遇到此类报错时,就一点点看完栈指针,将其与正常栈指针的变化规律相比较,就可以快速找到有问题的地方