多维数组是指包含两个或两个以上维度的数组。常见的多维数组有二维数组、三维数组等。对于程序的逆向工程,多维数组的分析通常需要理解数组的内存布局、存储方式以及程序对数组的访问方式。
多维数组的存储方式
多维数组在内存中可以采用按行优先(Row-major order)或按列优先(Column-major order)来存储:
按行优先(Row-major order):数组的元素是按行连续存储的。大多数编程语言如 C/C++ 使用按行优先的方式。
按列优先(Column-major order):数组的元素是按列连续存储的。这种存储方式常见于 Fortran 语言。
在逆向工程的分析中当涉及多维数组的程序时,需要留意以下几个方面:
①内存布局:了解数组的内存布局有助于正确识别数组的起始地址、偏移量和元素访问方式。通常在逆向过程中,通过观察汇编代码对内存的访问模式,可以推断出数组的类型、大小和维度。②数组的下标计算:多维数组的下标计算公式通常遵循 arr[i][j]
对应的地址为:
按行优先:arr + i * 列数 + j
按列优先:arr + j * 行数 + i
假设有一个 C 语言二维数组定义如下:
int arr[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
在汇编代码中,如果我们看到类似以下的代码片段:
mov eax, [ebp-20h] ; 加载基地址(arr)到 eax
mov ecx, 2 ; 第三行
mov edx, 3 ; 第四列
mov ebx, 4 ; 每行 4 个元素
imul ecx, ebx ; ecx = ecx * 4
add ecx, edx ; ecx += 列数
shl ecx, 2 ; 元素大小是 4 字节,所以偏移量乘以 4
mov eax, [eax + ecx] ; 访问 arr[2][3] 的元素
从这个汇编代码中可以推断出这是一个二维数组访问的模式。通过观察计算偏移量的操作(如 imul
、add
、shl
),可以确认数组的大小和内存布局。
接着我们就通过一个简单的二维数组的 C 代码示例,包含对二维数组的定义和访问操作。根据这个例子进行分析。
#include <stdio.h>
#include <stdlib.h>
int main() {
int arr[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
int row = 2;
int col = 3;
int value = arr[row][col]; // 访问 arr[2][3],即第 3 行第 4 列的元素
printf("Value at arr[%d][%d] = %dn", row, col, value);
system("pause");
return 0;
}
使用VS生成exe
文件后载入IDA
中进行静态分析,观察特征。
载入完成后我们快速定位到main()
函数主体位置,查看相关的反汇编代码;首先就是二维数组的初始化:
mov [ebp+var_34], 1
mov [ebp+var_30], 2
mov [ebp+var_2C], 3
mov [ebp+var_28], 4
mov [ebp+var_24], 5
mov [ebp+var_20], 6
mov [ebp+var_1C], 7
mov [ebp+var_18], 8
mov [ebp+var_14], 9
mov [ebp+var_10], 0Ah
mov [ebp+var_C], 0Bh
mov [ebp+var_8], 0Ch
代码中使用了一系列 mov
指令,将 12 个值存储在栈上,这表明这些值在内存中是连续存储的,从汇编指令来看,这些值都被存放在 ebp
基址下的不同偏移位置上,并且每个值的偏移量都相差 4 字节。这与 C 语言中数组的连续存储模型相符(尤其是 int
类型的元素在 x86 平台通常占 4 字节),所以基本上但看这串代码基本上可以断定这是一个整型数组。那么在IDA中我们就可以双击偏移量,来到main函数的栈上,选中第一个偏移量,输入*
:
输入数组的大小:12,对该数组进行标记
标记完后可以很直观的看出这就是一个数组结构。
二维数组的寻址
接着我们从反汇编代码的角度去查看现二维数组是如何进行寻址的;在这个例子中我们将要取出二维数组中的第 3 行第 4 列的元素即arr[2][3]
进行打印,相关代码如下:
int row = 2;
int col = 3;
int value = arr[row][col]; // 访问 arr[2][3],即第 3 行第 4 列的元素
printf("Value at arr[%d][%d] = %dn", row, col, value);
此时,这个操作的反汇编代码如下:
mov [ebp+var_40], 2
mov [ebp+var_4C], 3
mov eax, [ebp+var_40]
shl eax, 4
lea ecx, [ebp+eax+var_34]
mov edx, [ebp+var_4C]
mov eax, [ecx+edx*4]
mov [ebp+var_58], eax
mov eax, [ebp+var_58]
push eax
mov ecx, [ebp+var_4C]
push ecx
mov edx, [ebp+var_40]
push edx
push offset Format ; "Value at arr[%d][%d] = %dn"
call j__printf
接着我们就逐步分析这个代码,首先将行索引 2
存入 [ebp+var_40]
,再将列索引 3
存入 [ebp+var_4C]
(第一行和第二行代码指令);接着计算行的偏移量:
mov eax, [ebp+var_40] ; 将行索引(2)加载到 eax 中
shl eax, 4 ; eax 左移 4 位,相当于乘以 16
lea ecx, [ebp+eax+var_34] ; 计算基地址 + 行偏移,存入 ecx
这部分代码中,shl eax, 4
将 eax
左移 4 位,等效于将 eax
乘以 16。这表示每一行有 4 列,每列占用 4 个字节(元素是 int
类型,大小为 4 字节),所以每一行的大小是 4 * 4 = 16
个字节,此时eax
中的值为32。lea ecx, [ebp+eax+var_34]
的目的是计算 arr[row]
的基地址,其中 var_34
是数组的起始地址偏移量,这个时候放到ecx
中的值为[ebp+32(20h)+var_34]
;此时对应的就是数组中第9个元素的值,也就是第三行的第一个元素。
行的偏移量计算完成后,接着就是计算获取数组中的值,相关代码如下:
mov edx, [ebp+var_4C] ; 将列索引加载到 edx 中
mov eax, [ecx+edx*4] ; 计算 arr[row][col] 的地址,并将其值存入 eax
mov [ebp+var_58], eax ; 将值存储到 [ebp+var_58]
edx
中保存了列索引的值,然后 ecx + edx*4
计算得到二维数组 arr[2][3]
的具体内存地址,ecx
中此时存储的值为第三行的首地址也就是行的偏移量,[行的偏移量+edx*4
]即可获得arr[2][3]
最后的地址。乘以 4 的原因是每个元素占 4 个字节。接着,访问该内存地址并将数组元素的值存入 eax
。
最后就是准备 printf
调用的参数:
mov eax, [ebp+var_58] ; 加载数组元素的值
push eax ; 将值压入栈(对应于 printf 中的 %d)
mov ecx, [ebp+var_4C] ; 加载列索引的值
push ecx ; 将列索引压入栈(对应于 printf 中的第二个 %d)
mov edx, [ebp+var_40] ; 加载行索引的值
push edx ; 将行索引压入栈(对应于 printf 中的第一个 %d)
push offset Format ; 压入格式化字符串的地址,格式化字符串:"Value at arr[%d][%d] = %dn"
call j__printf ; 调用 printf 函数
这段代码准备了调用 printf
所需的参数。格式化字符串 Format
"Value at arr[%d][%d] = %dn"
。通过依次压入行索引、列索引、数组元素的值和格式化字符串的地址,这就实现了对数组元素值的打印。
在这篇文章中,我们详细探讨了如何通过汇编代码来识别和分析二维数组的特征。从数据的内存布局、索引计算,到访问和读取数组元素,在今后的逆向工程中,希望这些思路能够帮助你更高效地应对类似问题,为分析和理解程序奠定坚实的理论依据。
原文始发于微信公众号(风铃Sec):C/C++逆向:二维数组分析
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论