简介
本篇文章是个人对IDA
逆向工程的研究,相关内容:逆向工程的简介、IDA
基本使用和工作原理、反编译可执行文件部分源码、反编译的逆向代码逻辑分析、IDA
反编译存在的缺陷、防止反编译的措施。小小心得,与大家分享。
逆向工程的简介
计算机软件逆向工程(Reversepengineering)
也称为计算机软件还原工程,是指通过对他人软件的目标程序(可执行程序)进行“逆向分析、研究”工作,以推导出他人的软件产品所使用的思路、原理、结构、算法、处理过程、运行方法等设计要素,作为自己开发软件时的参考,或者直接用于自己的软件产品中。
在传统的软件开发模型中,程序员使用编译器、汇编器和链接器中的一个或者几个创建可执行的程序。为了回溯编程过程,我们使用各种工具来撤销汇编和编译过程。这个过程叫做反汇编和反编译,因此我们可以利用一些逆向工具将程序运行时的机器语言转化为汇编语言,或者更高级的语言。我们对程序进行逆向主要是为了分析恶意软件、分析闭源软件的漏洞和操作性、分析编译器生成的代码,验证编译器的性能和准确性、逆向生成程序的源代码。下面阐述我将以IDA
逆向工具操作相结合。
反汇编和反编译简介:反汇编是指把可执行的二进制机器码反汇编成为基本可读的汇编语言,一般包括静态反汇编和动态反汇编技术两部分。静态反汇编是吧二进制代码一次性全部翻译为汇编代码,它的耗时与二进制文件大小称正比。动态反汇编是人为可读的汇编代码。反编译技术是吧汇编程序进一步反编译为可读性较高的高级语言代码。
基本的逆向原理和算法
(1)确定进行反编译的代码区域。因为指令和数据混杂在一起,所以区分起来很难。以反编译可执行文件为例,可执行文件必须要符合可执行文件的通用格式,如window
所使用的可移植可执行格式(Protable Executable,PE
)或者Unix
系统下常用的可执行和链接格式(Executable and linking format ,ELF
),这些格式通常含有一种机制,用来确定文件中包含代码和代码入口点(指令地址)的部分位置。
(2)知道指令的起始地址后,我们就可以进一步的读取该地址所包含的值,并执行一次表查找,将二进制操作码的值和它的汇编语言的助记符对应起。
(3)获取指令并解码任何所需要的操作数,需要对它的汇编语言等价形式进行格式匹配,并将其在反汇编代码中输出。如X86
汇编语言所使用的的两种主要格式Intel
和AT&T
格式。
(4)输出一条指令,继续反汇编下一条指令,并重复上述过程,知道反汇编完文件中的所有指令。这个过程主要采用两种算法:线性扫描和递归下降两种反汇编算法。
IDA的基本使用
-
IDA
的启动。New
、Go
、Previous
三种方式加载文件。 -
从
PC
端拖拽文件到文件拖拽区域
-
弹出加载新页面的窗口。
-
IDA
数据库文件。当点击OK
后,开始加载文件,IDA
将选择指定的可执行文件加载到内存中,并对相关部分进行分析。随后IDA
会创建一个数据库,其组件分别保存在4
个文件中,这些文件的扩展名分别为.id0
,.id1
,.nam
和.til
。.id0
文件是一个二叉树形式的数据库,.id1
文件包含描述每个程序字节的标注,.name
文件包含与IDA
的Name
窗口中显示的给定程序位置有关的索引信息,.til
文件用于存储与每个数据库本地化定义有关的信息。
-
IDA
数据库的保存。
IDA
桌面介绍:
工具栏介绍
导航彩带:是被加载文件的地址空间的线性视图。默认情况下,它会呈现二进制文件的整个地址范围。不同的颜色表示不同类型的文件内容,如数据块、代码块等,黄色小光标指向与当前反汇编窗口中显示的地址范围对应的导航带地址。
颜色标签栏:显示导航彩带上不同彩带对应的二进制信息内容。
反汇编视图:主要呈现视图数据。有两种不同的形式:图形视图和列表视图。图形视图中IDA
显示的是某个函数在某个一时间的流程图。通过与导航彩带的结合使用我们就可以了解函数的运行情况。
图形概况视图:因为使用图形视图很难显示某个函数的完整视图。我们是可以使用图形概况视图显示整个函数的概况。
消息窗口:显示文件进程相关的信息,用户操作输出的信息,相当于设备的输出设备。
主要的数据显示窗口:
IDA View
:图形视图:将函数分解成许多基本块,生动的显示该函数由一个块到另一个块的控制流程。每个基本快都有唯一的入口点和退出点。视图列表:窗口中的汇编代码分行显示,虚拟地址默认显示:格式:【区域名称】:【虚拟地址】,如:
text:0040110C0.
箭头窗口:用于描述函数中非线性流程。实线:表示非条件跳转,虚线:表示条件跳转。粗线:表示一个跳转将控制权转交给程序的某个地址,这时候要使用粗线。出现这种类的情况说明程序中有循环。
注释:以分好开头,属于交叉引用情况,表示所注释的位置的指令。Name
窗口:列举二进制的所有全局名称,每个名称对应一个程序的虚拟地址。通过Name
窗口可以快速定位程序列表中已知的位置,双击虚拟地址跳到反编译视图。F:表示常规函数,
IDA
认为这些函数不属于库函数,属于自定义函数。L:库函数,
IDA
通过签名匹配算法识别库函数,如果某个库函数的签名不存在该函数就会标记为常规函数。I:导入的名称,通常为共享库导入的函数名称。他与库函数的区别:导入的函数没有代码,而库函数的主体将在反编译代码清单上显示。
G:命名代码,这些是已经命名的程序指令位置,IDA认为他们不属于任何函数。
D:数据,已命名数据的位置通常表示全局变量。
A:字符串数据,这是一个被引用的数据位置,其中包含的一串字符符合
IDA
的某种已知的字符串数据类型。
次要的窗口。
Hex View – A:
十六进制窗口显示程序内容和列表的标准十六进制代码,每行16
个字符,以及对应的ASCII
字符。这个窗口是和视图窗口是对应同步的。十六进制窗口中显示问好,这表示IDA
无法识别给定的虚拟地址的范围,如果程序中包含了bss
节就会出现这种情况。Bss
由编译器创建,用于保存的所有未初始化的静态变量。当程序执行时,加载器会为其分配所需要的空间,并将整个数据块的初始值设置为0
.Import
窗口:Export
窗口:函数窗口
(functionName)
:由于列出数据库中IDA
识别的所有函数。用户可以根据二进制中的虚拟地址对应的.text
部分找到函数,还可以显示函数在十六进制下的字节数,它返回调用方法(R)
并使用EBP
寄存器(B)
引用它的变量。Stuctures
(结构体窗口):显示IDA
在二进制数据中识别复杂数据结构的布局。Enums
(枚举窗口):如果IDA
检测到标准的枚举数据类型,它将被在这个窗口被列举出来。
调用相关
调用约定:创建栈帧时最重要的是通过调用函数,将函数函数参数存入栈中。调用函数必须存储被调用函数所需要的参数,否则会导致严重的错误。各个函数会选择并遵守特定的调用约定,以表明他们希望以何种方式接受参数。
Cdecl
调用的约定:调用方法按从右到左的顺序将函数参数放入栈中,在被调用的函数完成其操作时,调用方负责从栈中清楚参数。这样无论函数需要多少参数,我们都可以找到第一个参数。Cedecl
调用约定非常适用于那些参数数量可变的函数。_stdcall
调用约定:按照从右向左的顺序将函数参数放入栈中,程序执行结束后,由被调用的函数负责删除栈中的函数参数。对于被调用的函数而言,要完成这个任务,它必须清楚知道栈上有多少个参数,这只有在函数接受的参数两固定时才有可能。优点:在函数每次调用完后,不需要通过代码从栈中清除参数,因此生成体积小,速度快的程序。Fastcall
调用约定:是stdcall
函数约定的变体,它向cpu
寄存器(非程序栈)最多传递两个参数。如果指定使用fastcall
约定,则传递函数的前两个参数将分别位于ECX
和EDX
寄存器中。剩余的其他参数将按照类似与stdcall
的方式从右到左放入栈上。调用完成后,由fastcall
函数负责从栈中删除参数。
反编译的例子
反编译的oc底层语言
// TestViewController - (void)setDataFromDict:(id)
void __cdecl -[TestViewController setDataFromDict:](struct TestViewController *self, SEL a2, id a3)
{
struct TestViewController *v3; // r5@1
int v4; // r4@1
void *v5; // r0@1
int v6; // r0@2
int v7; // r0@2
int v8; // r0@2
int v9; // r0@2
int v10; // r0@2
int v11; // r6@2
unsigned int v12; // r11@3
void *v13; // r8@3
int v14; // r0@4
int v15; // ST28_4@4
int v16; // r0@4
int v17; // r10@4
int v18; // r0@4
int v19; // r6@4
int v20; // r0@4
int v21; // r4@4
int v22; // r0@4
int v23; // r4@4
void *v24; // r5@7
int v25; // r0@7
int v26; // r8@7
int v27; // r0@7
int v28; // r10@7
int v29; // r0@7
int v30; // r4@7
int v31; // [sp+4h] [bp-40h]@3
int v32; // [sp+8h] [bp-3Ch]@2
int v33; // [sp+Ch] [bp-38h]@2
void *v34; // [sp+24h] [bp-20h]@2
v3 = self; ;*v3指self本类对象
v4 = objc_retain((int)a3, (int)a2); ;保留一份dict ,函数约定的调用方式是fastball,将objc_retain的两各参数放在cpu的寄存器中,分别放在位于ECX和EDX寄存器中。
v5 = objc_msgSend(&OBJC_CLASS___NSNull, "class"); 运用runtime的消息机制输出函数的返回值空或者非空。
if ( !((unsigned int)objc_msgSend((void *)v4, "isKindOfClass:", v5) & 0xFF) ) ;判断v4(dict)是否为空,如果返回值为假,则执行do—while循环;否则return;
{
v6 = (int)objc_msgSend((void *)v4, "objectForKeyedSubscript:", CFSTR("d")); ;根据v4中的key“d”,返回一个字典v6。
v7 = objc_retainAutoreleasedReturnValue(v6);
v33 = v7;
v8 = (int)objc_msgSend((void *)v7, "objectForKeyedSubscript:", CFSTR("information"));字典v7调用key“information”,返回v8数组。
v9 = objc_retainAutoreleasedReturnValue(v8);
v32 = v9;
v10 = (int)objc_msgSend((void *)v9, "objectAtIndex:", 0);通过0索引位置找到数组v9,返回v10数组。
v11 = objc_retainAutoreleasedReturnValue(v10);
v34 = (void *)v11;
// 执行for循环,先判断v11.count是否有值,如否有值,执行do-while循环
if ( objc_msgSend((void *)v11, "count") )
{
v31 = v4;
v12 = 0;
v13 = v3;
do
{
v14 = (int)objc_msgSend((void *)v11, "objectAtIndex:", v12);
v15 = objc_retainAutoreleasedReturnValue(v14);
v16 = (int)objc_msgSend((void *)v15, "objectForKeyedSubscript:", CFSTR("title"));
v17 = objc_retainAutoreleasedReturnValue(v16);
v18 = (int)objc_msgSend((void *)v15, "objectForKeyedSubscript:", CFSTR("url"));
v19 = objc_retainAutoreleasedReturnValue(v18);
v20 = (int)objc_msgSend(v3, "arrayTitle");
v21 = objc_retainAutoreleasedReturnValue(v20);
objc_msgSend((void *)v21, "addObject:", v17);
objc_release(v21);
v22 = (int)objc_msgSend(v3, "arrayUrl");
v23 = objc_retainAutoreleasedReturnValue(v22);
objc_msgSend((void *)v23, "addObject:", v19);
objc_release(v23);
objc_release(v19);
objc_release(v17);
v11 = (int)v34;
objc_release(v15);
++v12;
}
while ( v12 < (unsigned int)objc_msgSend(v34, "count") );
}
else
{
v31 = v4;
v13 = v3;
}
//执行 [self createUI:self.arrayTitle urlList:self.arrayUrl number:self.Number];
v24 = v13;
v25 = (int)objc_msgSend(v13, "arrayTitle");
v26 = objc_retainAutoreleasedReturnValue(v25);
v27 = (int)objc_msgSend(v24, "arrayUrl");
v28 = objc_retainAutoreleasedReturnValue(v27);
v29 = (int)objc_msgSend(v24, "Number");
v30 = objc_retainAutoreleasedReturnValue(v29);
objc_msgSend(v24, "createUI:urlList:number:", v26, v28, v30);
objc_release(v30);
objc_release(v28);
objc_release(v26);
objc_release(v11);
objc_release(v32);
objc_release(v33);
v4 = v31;
}
j__objc_release(v4);
}
(10)分析得出的伪源代码
-(void)setDataFromDict:(NSDictionary *)v4{
if ( [v4 isKindOfClass:[NSNull class]]) {
return;
}
NSDictionary *v7 = dict[@"d"];
NSMutableArray *v9 = v7[@"information"];
NSMutableArray *v11= [v9 objectAtIndex:0];
for (int i=0; i< v11.count; i++) {
NSDictionary * v15 =[ v11 objectAtIndex:i];
NSString *v17= v15 [@"title"];
NSString *v18= v15[@"url"];
[self.arrayTitle addObject: v17];
[self.arrayUrl addObject: v18];
}
[self createUI:self.arrayTitle urlList:self.arrayUrl number:self.Number];
}
上述分析对应的源码:
函数对应的汇编语言及其执行逻辑。
IDA反编译存在的缺陷
IDA
工具对block
信息体被识别不了。由于block
的实质是用一些结构体来保存调用信息,而结构体信息在release
版的C++
编译结果中是无明文的,所以在反汇编中block
的传参看起来就是直接传到一块内存区域中,需要自己理解其排放顺序是参照那些结构体的。反编译的最终代码是
Object-C
的Runtime
运行时语言,而不我们正常编码的OC
语言。对运行时机制和语言不了解的程序员是很难理解的,研究反编译难度大,不过这是一个学习Runtime
运行时语言的好工具哦。IDA
反编译工具相对现在流行的反编译工具hopper disassembler
、class_dump
,过于古老,功能和目的性略显不足。
防止反编译措施
(1)代码混淆。
代码混淆是指将程序的代码,转换成一种功能上等价,但是难以阅读和和理解的形式行为。通常这种行为用于源代码,混淆原理是将代码中的各种元素和逻辑,如变量,函数,类的名称改为没有意义的名字,使得阅读的人无法根据名字猜测其用途,打乱代码的格式,尝试不同实现逻辑等。目前是市面上没有相应的IOS
混淆工具,不过我们在开发中,可以事先约定一套命名规则,如使用完全没有意义的字符代替、对名称进行MD5
加密等。
(2)加壳。
所谓的加壳,是一种通过一系列数学运算,将可执行程序文件的编码进行改变,已达到缩小文件体积或者加密程序编码的目的。当被加壳的程序运行时,外壳程序会被执行,然后由这个外壳程序负责将用户原有的程序在内存中解压缩,并把控制权交还给脱壳后的真正程序。目前常见到的壳有“UPX”
、“ASPack”
、“PePack”
、“PECompact”
、“UPack”
、“NsPack”
、“免疫007”
、“木马彩衣”
等等,我们可以通过Fileinfo
的工具来查看具体什么壳。加壳的本质就是隐藏程序的入口点(OEP)
,防止被破解。软件的壳分为:加密壳,压缩壳,伪装壳,多层壳等。
(3)采用更加高级的编程方式。
为了防止被反编译器反编译,我们可以使用比传统的命令式编程习惯更加高级而复杂的编程方式去编写程序,比如基于block
高阶函数的响应式编程(ReactiveCocoa)
,响应式编程可以提高我们代码的抽象级别,也更好的处理事件之间相互依存的业务逻辑。IDA
和其他反编译工具,是不能识别block
函数的。
(4)网络层我们要采用Https超文本安全传输协议。
其实HTTPS
从最终的数据解析的角度,与HTTP没有任何的区别,HTTPS
就是将HTTP协议数据包放到SSL/TSL层加密后,在TCP/IP层组成IP数据报去传输,以此保证传输数据的安全;而对于接收端,在SSL/TSL将接收的数据包解密之后,将数据传给HTTP协议层,就是普通的HTTP数据。HTTP和SSL/TSL都处于OSI模型的应用层。从HTTP切换到HTTPS是一个非常简单的过程
首先用这个是因为http的数据是裸露的(举个例子,之前微信朋友圈很火的红包看图功能,我们直接抓包,就可以看到图片url,就能查看原图,何必再去专门发个红包😄 -- 当然不差钱的童鞋除外)。 所以用https,一定程度上降低了传输速率(其实损耗并不大的),但是SSL/TSL层加密之后,加上证书的限制,可以解决50%的安全隐患。
(5)数据传输不仅要用Https,而且在传输过程中我们前段和后端进行自定加密。在Https的传输中在进行加密处理,token认证。
(6)App数据存储安全,主要指在磁盘做数据持久化的时候所做的加密。