TA的每日心情 | 郁闷 2022-3-17 02:16 |
---|
签到天数: 1 天 [LV.1]初来乍到
|
逆向分析的两大基本功:
· 核心代码的分析(分析反汇编代码).
· 核心代码的定位.
2. 核心代码的定位
逆向一般是带有强烈的目的性的, 比如逆出一个程序的某种功能的实现函数,或者是逆出一个程序的流程等等.
要完成这样的功能就需要定位到具体的函数上, 然后才能分析其代码,否则在庞大的二进制流中, 信息的获取必定是极其艰难的.
在庞大的二进制流中找到一个地址未知的函数. 需要推测这个函数的一些特征,没有特征就很难定位到这个函数. 因为在庞大的二进制流中有着上万乃至数十万行以上的机器码,在这里面使用常规的方法找到几百行特定的机器码是非常耗时耗力的.
所以, 必须要推测出函数的一些特征,才能快速定位到核心代码处.
一般一个函数的特征会有:
· 这个函数将会完成什么样的功能,在完成这些功能的时候调用了什么 API.
· 使用了容易搜索出的字符串.
· 有一些特定的机器码等等.
· 读写了特定的地址.
· 只要是稍微异于其他代码的特征都可以.
对于第 1点特征, 可以使用堆栈回溯找到核心代码.
对于第 2点特征, 利用到的是字符串搜索找到核心代码.
对于第 3点特征, 利用到的是二进制字串搜索找到核心代码.
对于第 4点特征, 利用到的是硬件读写断点或软件读写断点.
2.1 关于堆栈回溯的原理:
有两个函数 UnKnownFunction(), 和 WellKnowFunction(), 其中, 已知条件有:
· UnKnownFunction 代码的地址是未知的.
· WellKnowFunction代码的地址是已知的.
· 在 UnKnownFunction函数的代码中, 有调用 WellKnowFunction函数的语句.
现在, 需要找出 UnKnownFunction 代码的地址.
· 从第3条已知条件中可以得到一个信息, 当 WellKnowFunction函数被调用时, 有可能就是在 UnlKnowFunction函数中调用的, 如果 WellKnowFunction函数被调用,那么在栈中就一定保存着返回地址(call WellKnowFunction时,call语句先把下一条语句的首地址压栈,然后才跳转到WellKnowFunction函数中), 这个返回地址将会返回到 WellKnowFunction函数被调用的地址.
· 根据第二条已知条件可知, WellKnowFunction函数的地址是已知的, 因此可以在这个函数中下断点. 当这个函数被中断时, 就可以根据栈中的返回地址找到这个函数执行完后将会返回到的地方(也就是调用这个函数的语句之后).
· 因此利用这一点可以找到程序的核心代码. 在一个程序中, 程序代码的函数地址是未知的. API的地址是已知的,所以只要能够推测在核心代码的中会调用什么 API,就可以在 API处下断,通过堆栈回溯找到核心代码.
2.2 关于字符串搜索的原理
如果核心代码中使用了字符串. 那么只要搜索到这个字符串.在 OD中就可以直接找到引用这个字符串的代码.
2.3 关于二进制字串搜索的原理
和字符串搜索的原理差不多 , 只不过字符串容易被搜索到, 但是二进制字串需要自行推测出才能够搜索到.
比如在一个函数中, 函数的内部会调用一个获取对话框子控件句柄的 API.则函数如下:
?
1机器码 | 汇编代码 | 注释
2.-----------+----------------+------------------------------------
3.68ED010000 | push 0x1ED | ;子控件的 ID(随便给出的)
4.6811110000 | push 0x1111 | ;对话框的窗口句柄(随便给出的)
5. | call GeDlgItem | ;调用函数
在推测到核心代码会有这样的代码时, 就可以直接查找二进制字串 68ED01000 来查,当然在实际操作中如果前期分析到的信息过少,就难以猜测出具体的代码.所以这招想要用得好,前期详细的分析必不可少.
2.4 关于核心代码中读写了特定的地址,使用读写断点的原理
假设在前期的分析中, 已知核心代码处会对一个全局变量进行读写操作. 全局变量的地址在前期的分析中找到了,但是读写全局变量的代码很难找到.那么读写断点就很适用于这样的场景. 如果核心代码访问这个全局变量. 只要在这个全局变量的地址上设置一个读写断点. 当有代码读写这个全局变量的地址时就会断下来.
1. 核心代码的分析(分析反汇编代码)
个人感觉分析正常的(没有花指令)反汇编代码最大的挑战就是难以分清反汇编出的代码是用户的代码还是库的代码,还是编译器插入的代码.克服这一关需要丰富的经验. 因此. 逆向的入门大多数是难倒在这一关上. 这一关挺过去了,逆向也就入门了.
除了上述这一挑战. 当然还有来自其他方面许许多多的挑战.
1.1. 识别一个函数
程序都是以函数为一个单位. 函数内的多条代码使函数完成了特定的功能, 在分析当中也是以一个函数作为一个分析单位. 在分析时, 应当尽可能多的分析出这个函数所完成的具体功能.
反汇编中的函数一般都有以下格式:
1.1.1 第一种函数格式
1.push ebp
2.mov ebp,esp
3. ....
4.mov esp,ebp
5.pop ebp
6.ret
1.1.2 第二种函数格式
这种函数没有特定的格式, 但可以肯定的是在函数的末尾肯定有 ret语句.
1.2. 识别函数的返回值, 形参个数,形参, 局部变量
· 函数一般是使用 eax 寄存器来保存返回值.但有一些编译器可能用其他的方式来传递返回值, 因此,在分析程序的前期,需要确定该程序是用什么编译器进行编译的(通过PEId可以查看),才能确定其返回值是用什么方式进行传递的.
· 函数的形参个数可以通过函数平衡多少字节的栈空间来推理出. 因为存在着许许多多的传参方式,所以这种方法并不是绝对正确的. 因此要先观察出一个函数的传参方式是何种方式.
和函数的传参方式息息相关的函数调用方式:
不同的调用方式平衡堆栈的方式是不同的. C 方式调用时,是调用者进行堆栈平衡.传参时,一般是自右往左将实参入栈.在 32位下实参一般是 4个字节一个,但也有例外的情况(比如传递一个结构体变量).
在平衡堆栈时,一般是在函数调用语句后由调用者平衡堆栈.
例如:
call fun ; 调用一个函数
add esp , 4 ; 平衡堆栈.
stdCall 是被调用自己平衡
stdCall方式调用时, 形参入栈的方式和 C方式相同, 不同就在于平衡堆栈的形式. std方式调用是由被调用的函数自己本身来平衡堆栈也就是在函数返回的时候平衡堆栈.
ret 0x4 // stdCall平衡堆栈的实例
如果函数传入形参每个都是 4个字节的, 那么就可以算出有多少个形参公式就是 :
形参个数 = 平衡堆栈用到的字节数 / 4
函数形参在汇编语言中随着调用方式不同其表现形式也跟着不同.
C方式和 std方式调用时,形参的表现形式为: [ebp+8] ,即 ebp+一个数字去索引.
附注:
[ebp] : 保存的一般是上层函数的栈底
[ebp+4] : 保存的一般是返回地址
[ebp+8] : +8 之后保存的是形参.
但这种表现形式是建立在第一种函数格式的前提之上的. 在第二种函数格式下,函数的形参用的就不是这种索引方式了.
2.4 函数的局部变量
在第一种函数格式下, 局部变量的表现形式为: [ebp - XXX]
在第二种函数格式下, 局部变量可能是[esp + XXX].
3. 识别出分支选择语句
· 在C语言中, 使 if语句产生分支的可以是由丰富的运算符来组成的表达式 , 在汇编中, 没有这么多的运算符, 表达式也不能太复杂, 但在汇编中有标志位, 标志位与条件跳转的组合实现了 C语言中的逻辑运算符(>,<,<=,>=,&&,||).
· 因此, 汇编中凡是出现条件跳转语句的,条件跳转语句之上必定有会使标志位产生变化的指令(一般是 cmp, test等),这两种语句的组合就形成 C语言当中的分支选择语句.
比如:
1. 01285000 837D FC 00 cmp dword ptr ss:[ebp-0x4],0x0
2. 01285004 74 FA +-{ je 0128500B ;当 zf 标志位==1 这条语句才执行
3. 01285006 B8 01000000 | mov eax,0x1
4. 0128500B C3 +-> ret
这样的反汇编代码用 C 语言表示:
1. if([ebp-4] == 0 )
2. return ;
3.eax = 1;
4.return ;
4. 识别出循环控制语句
循环语句是在流程分支选择语句的前提上形成的.如果在分支选择语句中发现了有向上的短跳转, 且在向上的短跳转的范围中发现有累加的寄存器或内存(一般是寄存器),那么这种汇编代码一般都是循环语句.
当然,汇编语句中,带有 rep前缀的指令也是一种循环指令.
例如:
1. 01285000 B8 00000000 mov eax,0x0
2. 01285005 83F8 10 +---> cmp eax,0x10
3. 01285008 7D 03 | +-{ jge 0128500D ; 向下跳转
4. 0128500A 40 | | inc eax ; 累加寄存器
5. 0128500B EB F3 +-+-{ jmp 01285000 ; 向上跳转
6. 0128500D C3 +-> ret
5. 数组的识别
数组的空间是连续的,并且没个元素的大小都是相等的, 因此一般在访问数组元素的时候, 一般都是通过基址加偏移的形式去访问的. 因此, 在反汇编中, 如果碰到一些基址变址或相对基址变址访问内存时, 基址一般都是数组的首地址.
例如:
01285000 B8 00000000 mov eax,0x0
01. 01285005 8D35 00004000 lea esi,dword ptr ds:[0x400000] ;取基
址
02. 0128500B 8D3D 10004000 lea edi,dword ptr ds:[ebp+4] ;取基址
03.
01285011 83F8 10 +---> cmp eax,0x10
04.
01285014 74 EA | +-{ je 0128501F
05.
01285016 8B0C30 | | mov ecx,dword ptr ds:[eax+esi] ;基址加
06.
偏移
07.
01285019 890C38 | | mov dword ptr ds:[eax+edi],ecx ;基址加
08.
偏移
09.
0128501C 40 | | inc eax
10.
0128501D EB F2 +-+-{ jmp 01285011
0128501F C3 +-> ret
6. 结构体的识别
结构体和数组是类似的, 结构体中的成员都是在以一个基址为偏移的连续空间中. 但
结构体中各个成员的字节数可能是不相等的,因此和数组是有区别的.一般在访问结构体中
的成员时, 是以结构体的首地址作为基址,然后加上成员在结构体中的偏移进行访问.
例如有这样一个结构体:
1.struct NODE
2. {
3. char ch;
4. char array[8];
5. int num;
6.}NODE;
假设在函数中定义了一个结构体变量: [ebp+4], 那么访问各个成员的形式是这样的(假设结构体以 4字节对齐):
1. lea ecx , [ebp+0x4] ; 取结构体变量的首地址
2. mov eax , [ecx] ; 访问 ch的值
3. lea eax , [ecx+0x4] ; 得到 array成员的首地址
4. mov eax , [ecx+0xc] ; 访问 num成员的值
|
评分
-
查看全部评分
|