C语言代码逆向基础-数组和指针的寻址

C语言代码逆向基础-数组和指针的寻址

数组和指针都是针对地址操作。数组是相同数据类型的数据集合,以线性连续存储在内存中,而指针是一个保存地址值的4字节变量。在使用中,数组名是一个地址常量值,保存数组首元素地址不可修改,以此为基地址访问其他元素;而指针是个变量,只要修改指针中所保存的地址数据就可以随意访问。

数组在函数内

在函数内定义数组时,该数组为局部变量

数组与局部变量对比如下:

可以观察到数组数据在内存中连续且类型都一致

这里var_14为常量-14,执行“ebp+var_14”后访问的地址最小,以此地址为数组的首地址,数组地址是从低地址向高地址延伸。

字符串初始化为字符数组

就是复制字符串的过程,使用寄存器每次复制4字节的数据,字符串规定最后一个数据使用0来作为字符串结束符。

其反汇编代码如下:

这里使用了eax,ecx,edx三个寄存器,将常量字符串分为3段共12个字节,分为三个寄存器来进行保存实现中间传递。

当字符串不为4的倍数时如何进行传递,请看下面反汇编代码

这里的字符串为11字节,不为4的倍数。其中前八个字节数据复制过程没有变化,最后三个字节被分割为两部分,先使用dx复制两字节的数据,在使用al复制一个字节数据。

数组作为参数

当数组作为参数进行传递时,数组所占内存大小通常大于4字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>

void show(char a[])
{
strcpy(a,"Hello World");
printf(a);
}

void main()
{
char b[20]={0};
show(b);
}

main()函数关键反汇编代码如下

先对数组进行初始化为0,将数组首地址作为参数调用show函数。

在show函数中,取常量“Hello World”首地址和函数参数首地址压入栈中作为strcpy函数的参数,然后add esp 8指令用来平衡栈。

可以观察到当数组作为函数形参时,函数参数保存的是数组的首地址,是一个指针变量。使用sizeof(数组名)可以获取数组的大小,而对指针或者形参中保存的数组名使用sizeof只能得到当前平台的指针长度,32位环境下指针长度位4字节。应当使用**strlen()**函数获取字符串长度。

数组作为返回值

数组作为返回值和数组作为函数参数大同小异,都是将数组首地址以指针方式进行传递。而两者也有不同,当数组作为函数参数时,其定义的作用域必然在函数调用之前就已经存在;但数组作为局部变量数据时,当退出函数时需平衡栈,产生了稳定性问题。

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>

char* show()
{
char buff[] = {"Hello World"};
return buff;
}

void main()
{
printf("%s \r\n",show());
}

show函数关键反汇编代码如下,使用eax、ecx、edx三个寄存器传递对字符串数组初始化字符串,最后使用eax保存数组首地址作为函数返回值。

main函数关键反汇编代码如下,由于eax保存数组首地址将其压入栈中,最后栈平衡。

由于数组buff为局部变量,其地址是栈空间中的某段内存空间,其中的数据会在作用域切换时被新数据替换,因此返回地址随时会产生错误。要想避免该错误,可以使用全局数组、静态数组或者上层函数中定义的局部数组。

使用全局数组

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>

int a[5] = {1,2,3,4,5};
void main()
{
int* pint = a;
do{
printf("%d \r\n",*pint);
pint++;
}while(pint<a+5);
}

反汇编关键代码如下

循环体中使用”cmp [ebp+var_4], offset unk_42B63C”与常量值相比,这里常量地址为0042B63C,而数组首地址为42B628,所以常量地址正是全局数据的结尾地址。

下标寻址和指针寻址

访问数组有两种方式:下标寻址和通过指针寻址。指针寻址的方式不但没有下标寻址方式便利,效率也比下标寻址低,我们来看下两者区别。

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>

void main()
{
char * a = NULL;
char buff [] = "Hello";
a=buff;
printf("%c\r\n",*a);
printf("%c\r\n",buff[0]);
}

反汇编关键代码如下

首先对指针变量var_4进行初始化,将”Hello World”子符串传递给buff数组,使用edx获取数组首地址,取出指针变量中保存的地址数据,使用movsx指令对地址实现间接访问其他的数据;而下标寻址则是直接将数组首地址的数据传给edx压入栈中。指针寻址需要经过2次寻址才能得到目标数据,而下标寻址只需1次寻址就可以得到,由此可以得出结论,下标寻址比指针寻址操作效率高。

但是使用下标寻址的话,需要注意越界访问的错误。

1
2
3
4
5
6
7
8
#include<stdio.h>

void main()
{
int a[4] = {1,2,3,4};
int number = 5;
printf("%d\r\n",a[-1]);
}

由于vc++6.0没有对数组下标进行访问检查,程序运行结果为5

运行结果为什么为5呢?我们来看下其反汇编关键代码

可以从反汇编代码中看到此时a[-1]的地址为ebp-14h,这正是number变量所在的地址,根据局部变量定义,人为将变量定义在数组之下,从而造成负数下标的越界访问。

存放指针类型数据的数组

存放指针类型数据的数组就是数组中各数据元素都是由相同类型的指针组成的。对指针数组的数据访问需要对其进行间接访问。

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>

void main()
{
char * a[3] = {"Hello ","World ","! \r\n"};
int i=0;
for(;i<3;i++)
{
printf(a[i]);
}
}

反汇编关键代码为

指针数组中存储这每个字符串的首地址,二维字符数组存储着每个字符的字符数据。

函数指针

函数指针是用来保存函数指针首地址的指针变量。定义如下:返回值类型 (* 函数指针变量名称) (参数信息)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>

void show()

{
printf("show\r\n");
}

void main()
{
void (*a)()=&show;
a();
show();
}

反汇编关键代码如下

其过程是将show函数地址入口取出保存在指针变量a中,然后再调用a保存的地址,与函数直接调用需要间接调用。

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2024 John Doe
  • 访问人数: | 浏览次数:

让我给大家分享喜悦吧!

微信