数组在逆向工程中是一个重要的分析对象,数组是存储数据的重要数据结构,尤其在程序中用于存储一系列相同类型的元素。通过逆向分析数组,可以理解程序是如何处理和组织这些数据的,从而揭示程序的内部逻辑。在软件安全领域,逆向分析数组可以帮助安全研究人员发现程序中的潜在安全漏洞,例如缓冲区溢出、数组越界访问等。这些漏洞可能会被恶意利用,因此通过逆向分析可以增强软件的安全性。
数组分析的关键步骤和方法
识别数组结构:
反汇编代码中识别数组结构通常涉及以下步骤:
①识别连续的内存访问模式:数组元素通常在内存中连续存储。如果在反汇编代码中观察到连续的内存地址被访问,且访问模式符合特定数据类型的固定间隔(例如,每个整数类型数据间隔4字节),这可能是一个数组。
②分析寄存器和内存地址:在x86
架构中,数组访问常常涉及到基地址寄存器(如ebp
)和偏移量。通过分析这些寄存器的操作,可以确定数组的基地址和元素大小。
③手动分析和标记:在某些情况下,反汇编工具可能无法自动识别数组。这时可以手动分析代码,并在工具中标记数组。例如,在IDA Pro
中,可以通过右键点击内存位置并选择Array
来手动定义数组。
下面是一个简单的C语言程序,它使用了一个数组和一些基本的操作。接着我们编译这个程序生成一个exe
文件然后进行逆向分析来观察数组。
#include<stdio.h>
#include<stdlib.h>
int main() {int nArr[] = { 1,2,3,4,5 };printf("%d,%d,%d,%d,%d", nArr[0], nArr[1], nArr[2], nArr[3], nArr[4], nArr[5]);
int a = 1;int b = 2;int c = 3;int d = 4;int e = 5;printf("%d,%d,%d,%d,%d", a, b, c, d, e);
system("pause");return 0;
}
放入IDA
中进行静态分析,查看数组数据结构与连续声明的同类型变量在反汇编代码中是否存在区别。
在IDA
中的Function Windows
中对main
函数进行定位,得到main
函数的反汇编代码:
接着我们就针对这个代码进行简单分析,首先红色方框中的代码就是在上一篇文章中重点提到的函数的初始化以及检查操作(请看笔者的上一篇文章《函数逆向分析-总体流程(整型&指针)》);因为不是我们这部分的重点所以就不做过多赘述(下同)。
在该代码后就是我们代码的主体了,首先来看一下第一部分代码。
mov [ebp+var_18], 1
mov [ebp+var_14], 2
mov [ebp+var_10], 3
mov [ebp+var_C], 4
mov [ebp+var_8], 5
这串反汇编代码将数值1-5
移动(存储)到以ebp
为基址,偏移量为var_18
、var_14
、var_10
、var_C
、var_8
的内存地址中,这实际上就是数组的初始化操作。这个时候我们按下空格切到带有内存地址的视图。
接着点击这些数据的偏移量,查看数据段中对应的这些数据。
可以看到在内存当中这些数据的地址都是连续的,并且我们可以发现这些数据都是dd
类型的也就是4字节,且观察上面对这些地址空间的数据进行赋值时赋值的都是数字,所以基本上可以判断这个就是一个整型数组(int/short
)结构。那么这个时候我们可以在IDA中进行手动标志。
选中一个变量,输入*
对数组进行标识:
输入数组大小,因为我们这边有5个元素,所以可以在方框中输入5。
这边弹出确认框询问是否直接转化为数组,这边选择yes
;
这边这些连续的数据就被我们手动标记为数组了,这个时候我们再来看一下数组初始化的反汇编代码。
mov [ebp+var_18], 1
mov [ebp+var_18+4], 2
mov [ebp+var_18+8], 3
mov [ebp+var_18+0Ch], 4
mov [ebp+var_18+10h], 5
将数值1-5
存储到[ebp+var_18+(0-10h)]
所指向的内存地址。这样子可以更加直观的看出这些元素的地址空间时连续的。那么接下去来看一下连续声明并初始化的多个同类型的变量,与数组之间的区别,接下去就是代码中同类型的变量连续初始化部分。
mov [ebp+var_24], 1
mov [ebp+var_30], 2
mov [ebp+var_3C], 3
mov [ebp+var_48], 4
mov [ebp+var_54], 5
同样的我们可以双击偏移量值,查看data
段中的内容:
可以看到代码中的变量在内存空间中的地址并不连续,由此我们就可以区分数组和连续变量的区别。
数组的寻址方式
数组的寻址(addressing)是指在内存中定位数组中某个元素的具体地址的过程。在程序执行时,数组中的每个元素都被存储在一块连续的内存区域中,而“寻址”就是根据数组的基址(base address)和下标(index)来计算并访问特定数组元素的内存位置。分析数组的寻址方式是理解程序内存布局和数据访问的关键步骤,数组的寻址方式通常涉及基址(base address)和偏移量(offset)的计算。在上述的案例代码中我们使用printf
去打印数组中所有元素的值,那么这边就涉及数组的寻址。
printf("%d,%d,%d,%d,%d", nArr[0], nArr[1], nArr[2], nArr[3], nArr[4], nArr[5]);
相关代码如下:
从printf
函数的调用形式来看我们可以很清楚的发现其调用约定为cdecl
若不知道怎么判断则可以去看一下笔者前面的文章(《C/C++逆向:函数逆向分析-调用约定分析》);在这到了这个条件时我们可以直接定位call
,然后从下往上查看其压入栈中的参数(参数都是从右往左的顺序压入栈中的,所以最左边的参数应该是在最下面),从代码中我们可以看到第一个压入栈中的是一个格式化字符串;
push offset Format ; "%d,%d,%d,%d,%d\r\n"
从这个格式化字符串中的%d
个数我们可以看到此处要打印的元素个数为5个,所以接着向上看5个被压入栈中的参数,这五个就是上述被压入代码中的5个数组元素,我们这边就来看一下这边数组是如何进行寻址的;同样我们也是从后往前看,第一个元素是如何被找到并且压入栈中的。
mov eax, 4
imul ecx, eax, 0
mov edx, [ebp+ecx+var_18]
push edx
代码做了这些事情:将常数值 4
赋值给寄存器 eax
,接着对寄存器 ecx
进行乘法运算。imul
指令的格式是:imul 目标寄存器, 源寄存器, 常数
。它将 eax
的值与常数 0
相乘,并将结果存储在 ecx
中。ecx = 4 * 0 = 0
,所以 ecx
的最终值为 0
。然后通过基址 ebp
和偏移量 ecx + var_18
从内存中读取值,并将其存入 edx
寄存器。ecx
的值为 0,因此实际的偏移量为 ebp + var_18
最后将 edx
中的值压入栈中,至此第一个元素成功被压入栈中,接着来看一下第二个参数的寻址,相关代码如下:
mov ecx, 4
shl ecx, 0
mov edx, [ebp+ecx+var_18]
push edx
这串代码首先将立即数 4
移动到 ecx
寄存器中,接着通过代码shl ecx, 0
将 ecx
寄存器的值左移 0
位由于移位量是 0
,ecx
的值不会发生变化。接着将内存地址 [ebp+ecx+var_18]
中的值移动到 edx
寄存器中,由于 ecx
的值是 4
,有效地址是 [ebp + 4 + var_18]
,这就是数组中第二个元素的地址。
至此数组中的第二个元素成功被压入栈中。接着第3-5个参数的寻址以及压栈代码如下:
第5个参数:
mov eax, 4
shl eax, 2 ;edx中的值向左偏移2位,0000 0100(4) --左移2位--> 0001 0000(16)=10h
mov ecx, [ebp+eax+var_18] ;[ebp+10h+var_18]
push ecx
-------------------------------
第4个参数:
mov edx, 4
imul eax, edx, 3 ;eax = 4 * 3 = 12 (C)
mov ecx, [ebp+eax+var_18] ;[ebp+12+var_18]
push ecx
-------------------------------
第3个参数:
mov edx, 4
shl edx, 1 ;edx中的值向左偏移1位,0100(4) --左移--> 1000(8)
mov eax, [ebp+edx+var_18] ;[ebp+8+var_18]
push eax
可以看到第3到第5个参数也都是先将数组类型大小存入寄存器中,接着再通过shl
或者imul
指令设置偏移量,然后通过基址(base address)和偏移量(offset)获得对应的元素地址,最后压入栈中。
希望本文的讨论能为大家提供一个清晰的思路,帮助更高效地识别和分析数组结构。