Windows逆向安全(一)之基础知识(十七)
指针四
指针数组
什么是指针数组
首先回顾一下先前关于数组的知识:
所谓数组就是用于存储相同数据类型的集合
再结合先前关于指针的知识:指针的本质也是一种数据类型
于是当数组中存储的成员的数据类型为指针时,该数组就可以称为指针数组(本质是数组)
代码
#include "stdafx.h"
void function(){
int** arr[5]={(int**)1,(int**)2,(int**)3,(int**)4,(int**)5};
}
int main(int argc, char* argv[])
{
function();
return 0;
}
反汇编代码
9: int** arr[5]={(int**)1,(int**)2,(int**)3,(int**)4,(int**)5};
00401038 mov dword ptr [ebp-14h],1
0040103F mov dword ptr [ebp-10h],2
00401046 mov dword ptr [ebp-0Ch],3
0040104D mov dword ptr [ebp-8],4
00401054 mov dword ptr [ebp-4],5
小总结
- 可以看到指针数组其实并没有什么特别之处,只不过存储的数组成员的数据类型为指针而已
- 指针数组的赋值也和先前对指针的赋值没有什么区别
结构体指针
什么是结构体指针
所谓结构体指针就是在结构体后加上若干个*使其称为一个指针类型
代码
#include "stdafx.h"
#include <typeinfo>
struct S1{
int a;
};
void function(){
S1* s1=(S1*)0x12345678;
printf("%x\n",s1);
}
int main(int argc, char* argv[])
{
function();
return 0;
}
运行结果
结果分析
可以看到,这里关于结构体指针的使用貌似和普通的指针没有什么区别,但此时会发现这里还没有操作结构体内部的成员
所以结构体指针的实际使用也并不是这样,下面看一个错误的例子
错误代码
void function(){
S1* s1=(S1*)0x12345678;
int a=s1->a;
}
只是在上面代码的基础上添加了一个读取结构体成员的语句,查看运行结果
运行结果
运行结果不出所料出错了,开始分析错误的原因
反汇编代码
14: S1* s1=(S1*)0x12345678;
00401038 mov dword ptr [ebp-4],12345678h
15: int a=s1->a;
0040103F mov eax,dword ptr [ebp-4]
00401042 mov ecx,dword ptr [eax]
00401044 mov dword ptr [ebp-8],ecx
反汇编分析
0.执行前s1和s1->a的状态
s1:
s1->a:
1.为结构体指针s1赋值
14: S1* s1=(S1*)0x12345678;
00401038 mov dword ptr [ebp-4],12345678h
此时再看看s1->a:
可以发现对s1的赋值操作,改变的不是s1->a的值,而是改变了s1->a的地址
其实从执行前s1和s1->a的状态就可以看出,s1存储的内容并不是直接存储结构体成员的内容,而是存储指向结构体成员的地址
所以这里对于先前对于s1的赋值操作改变的只是成员的地址,而没有改变成员的值
并且刚开始时,结构体成员并没有被分配对应的内存地址
2.访问s1->a
15: int a=s1->a;
0040103F mov eax,dword ptr [ebp-4]
00401042 mov ecx,dword ptr [eax]
00401044 mov dword ptr [ebp-8],ecx
此时出错的原因已经显而易见了,先前对s1的赋值操作修改了s1->a的地址,使其指向了一个不可访问的地址而导致出错
正确代码
前面已经知道了出错的原因是访问了不可访问的地址导致出错,并且刚开始结构体成员没有被分配对应的内存地址
于是只要手动为结构体成员分配内存地址即可,这里将使用到malloc函数来进行分配内存地址
malloc函数
void *malloc(size_t size)
参数:size,内存块的大小,以字节为单位
返回值:返回一个指针 ,指向已分配大小的内存。如果请求失败,则返回 NULL
相关头文件:malloc.h、alloc.h、stdlib.h
大致了解了malloc函数,现在来看代码:
#include "stdafx.h"
#include <malloc.h> //这里使用了malloc.h
struct S1{
int a;
int b;
int c;
};
void function(){
S1* s1=(S1*) malloc(sizeof(S1)); //申请一块空间大小正好为S1大小的内存
s1->a=610;
s1->b=666;
s1->c=52;
printf("%d\n",s1->a);
printf("%d\n",s1->b);
printf("%d\n",s1->c);
}
int main(int argc, char* argv[])
{
function();
return 0;
}
运行结果
可以看到结构体的成员能够正常地被改写和访问
反汇编代码
15: S1* s1=(S1*) malloc(sizeof(S1));
0040D778 push 0Ch
0040D77A call malloc (00401150)
0040D77F add esp,4
0040D782 mov dword ptr [ebp-4],eax
16: s1->a=610;
0040D785 mov eax,dword ptr [ebp-4]
0040D788 mov dword ptr [eax],262h
17: s1->b=666;
0040D78E mov ecx,dword ptr [ebp-4]
0040D791 mov dword ptr [ecx+4],29Ah
18: s1->c=52;
0040D798 mov edx,dword ptr [ebp-4]
0040D79B mov dword ptr [edx+8],34h
反汇编分析
1.先看这个malloc函数
15: S1* s1=(S1*) malloc(sizeof(S1));
0040D778 push 0Ch
0040D77A call malloc (00401150)
0040D77F add esp,4
0040D782 mov dword ptr [ebp-4],eax
- 压入了参数0C,对应十进制为12,也就是S1的大小
- 调用malloc函数
- 堆栈外平衡
- 将返回值eax赋值给S1
看看返回值eax的内容:
可以看到eax就对应了结构体中的成员
eax=结构体成员首地址,里面的结构体成员连续存储
2.赋值,将610对应十六进制262赋值给[eax],对应前面的003807B8
16: s1->a=610;
0040D785 mov eax,dword ptr [ebp-4]
0040D788 mov dword ptr [eax],262h
执行后:
3.赋值,将666对应十六进制29A赋值给[ecx+4],对应前面的003807BC
17: s1->b=666;
0040D78E mov ecx,dword ptr [ebp-4]
0040D791 mov dword ptr [ecx+4],29Ah
执行后:
4.赋值,将52对应十六进制34赋值给[edx+4],对应前面的003807C0
18: s1->c=52;
0040D798 mov edx,dword ptr [ebp-4]
0040D79B mov dword ptr [edx+8],34h
执行后:
小总结
-
结构体指针和普通的指针实际上并没有什么不同
-
在对结构体成员进行操作时,需要先对其进行初始化(为每个结构体成员分配内存地址)
-
结构体指针并不直接存储结构体成员,而是存储了指向结构体成员的地址,该地址里存放着所有结构体成员
数组指针
前面学了指针数组,现在又来个数组指针,中间用结构体指针作了过渡,避免混淆
什么是数组指针
所谓数组指针,就是指向数组的指针(本质是指针)
既然是指针自然满足先前指针的一切特征:指针的赋值、指针的数据宽度、指针的加减、指针类型相减、指针之间比较
数组指针的声明
int (*px)[2];
声明如上,数组指针变量为px,类型为:int(*)[2];该数组指针指向的数组为int[2]
数组指针和指向数组的指针区别
代码
#include "stdafx.h"
void function(){
int arr[6]={1,2,3,4,5,6};
//声明一个数组指针,该指针指向数组为:int[2]
int (*px)[2];
//给数组指针赋值,使该数组指针指向arr数组的首地址
px=(int (*)[2]) &arr[0];
//用一个临时变量parr2 存储数组指针
int (*parr2)[2]=px;
//*px为数组的首地址,也就是arr,这里就相当于int* arr2=arr;此时的arr2就是指向数组的指针
int* arr2=*px;
//初始化变量,准备循环
int i;
//循环遍历数组
for(i=0;i<6;i++){
printf("%x\t%d\n",arr2+i,arr2[i]);
}
printf("\n");
int a=(int) (parr2+1);
int b=(int) (arr2+1);
printf("%x\t%x\n",a,b);
}
int main(int argc, char* argv[])
{
function();
return 0;
}
运行结果
首先可以看到数组的正常遍历
然后分别输出了parr2+1和arr2+1的结果,注意这里的结果不同
反汇编代码
8: int arr[6]={1,2,3,4,5,6};
00401038 mov dword ptr [ebp-18h],1
0040103F mov dword ptr [ebp-14h],2
00401046 mov dword ptr [ebp-10h],3
0040104D mov dword ptr [ebp-0Ch],4
00401054 mov dword ptr [ebp-8],5
0040105B mov dword ptr [ebp-4],6
9:
10: int (*px)[2];
11:
12: px=(int (*)[2]) &arr[0];
00401062 lea eax,[ebp-18h]
00401065 mov dword ptr [ebp-1Ch],eax
13:
14: int (*parr2)[2]=px;
00401068 mov ecx,dword ptr [ebp-1Ch]
0040106B mov dword ptr [ebp-20h],ecx
15: int* arr2=*px;
0040106E mov edx,dword ptr [ebp-1Ch]
00401071 mov dword ptr [ebp-24h],edx
16:
17: int i;
18:
19: for(i=0;i<6;i++){
00401074 mov dword ptr [ebp-28h],0
0040107B jmp function+66h (00401086)
0040107D mov eax,dword ptr [ebp-28h]
00401080 add eax,1
00401083 mov dword ptr [ebp-28h],eax
00401086 cmp dword ptr [ebp-28h],6
0040108A jge function+8Fh (004010af)
20: printf("%x\t%d\n",arr2+i,arr2[i]);
0040108C mov ecx,dword ptr [ebp-28h]
0040108F mov edx,dword ptr [ebp-24h]
00401092 mov eax,dword ptr [edx+ecx*4]
00401095 push eax
00401096 mov ecx,dword ptr [ebp-28h]
00401099 mov edx,dword ptr [ebp-24h]
0040109C lea eax,[edx+ecx*4]
0040109F push eax
004010A0 push offset string "%x\t%d\n" (00422024)
004010A5 call printf (00401160)
004010AA add esp,0Ch
21: }
004010AD jmp function+5Dh (0040107d)
22: printf("\n");
004010AF push offset string "\n" (00422020)
004010B4 call printf (00401160)
004010B9 add esp,4
23:
24: int a=(int) (parr2+1);
004010BC mov ecx,dword ptr [ebp-20h]
004010BF add ecx,8
004010C2 mov dword ptr [ebp-2Ch],ecx
25: int b=(int) (arr2+1);
004010C5 mov edx,dword ptr [ebp-24h]
004010C8 add edx,4
004010CB mov dword ptr [ebp-30h],edx
26: printf("%x\t%x\n",a,b);
004010CE mov eax,dword ptr [ebp-30h]
004010D1 push eax
004010D2 mov ecx,dword ptr [ebp-2Ch]
004010D5 push ecx
004010D6 push offset string "%x\t%x\n" (00422fa4)
004010DB call printf (00401160)
004010E0 add esp,0Ch
反汇编分析
1.数组的初始化
8: int arr[6]={1,2,3,4,5,6};
00401038 mov dword ptr [ebp-18h],1
0040103F mov dword ptr [ebp-14h],2
00401046 mov dword ptr [ebp-10h],3
0040104D mov dword ptr [ebp-0Ch],4
00401054 mov dword ptr [ebp-8],5
0040105B mov dword ptr [ebp-4],6
数组初始化后对应地址和内容为:
2.数组指针的赋值
12: px=(int (*)[2]) &arr[0];
00401062 lea eax,[ebp-18h]
00401065 mov dword ptr [ebp-1Ch],eax
直接将arr数组的首地址也就是0012FF14传给了eax
然后再将eax赋值给数值指针px
数值指针赋值后:
可以看到数值指针里存储的内容为0012FF14即arr的地址
3.将数值指针px赋值给另一个数组指针parr2
14: int (*parr2)[2]=px;
00401068 mov ecx,dword ptr [ebp-1Ch]
0040106B mov dword ptr [ebp-20h],ecx
赋值后:
可以看到此时parr2里存储的内容=px里存储的内容=0012FF14=arr首地址
4.将数组的首地址赋值给arr2,即arr2=arr
14: int* arr2=*px;
0040D82E mov edx,dword ptr [ebp-1Ch]
0040D831 mov dword ptr [ebp-24h],edx
这里要注意到这里和前面一样都是赋值了[ebp-1Ch]
也就是明明赋值的是*px,但是赋值却和px一样
也可以得出结论px=px,那么为什么px和px是一样的**?
首先要明确无论是px还是*px 它们都是指针,一个为数组指针,而另一个则为普通指针
它们所指向的地址相同,都指向了arr的首地址0012FF14
区别px和*px的本质就在于其数据类型是两种不同的指针结构
指针在相加减时,加减的基本单位是指针去掉一个*后的数据宽度
- px的数据类型为:int ()[2],去掉一个后变为int [2],数据宽度为int的数据宽度×数组的成员数=4*2=8
- px的数据类型为:int,去掉一个*后变为int,数据宽度=4
赋值后:
5.循环遍历数组
就是普通的指针循环数组,在先前的文章中已经有详细介绍,这里不再赘述
6.第二种循环
24: int a=(int) (parr2+1);
004010BC mov ecx,dword ptr [ebp-20h]
004010BF add ecx,8
004010C2 mov dword ptr [ebp-2Ch],ecx
25: int b=(int) (arr2+1);
004010C5 mov edx,dword ptr [ebp-24h]
004010C8 add edx,4
004010CB mov dword ptr [ebp-30h],edx
通过前面可以得知*px=px,parr2=arr2,所以这里的[ebp-20h]=[ebp-24h]的:
这里的不同之处就在于一个add了8,另一个add了4,和先前所分析的指针加减的单位相符合,于是产生了不同的结果
小总结
- 在一个数组指针前加上*获得的就是指向数组的指针,如上例中的int arr2=px;
- 数值指针和指向数组的指针中存储的内容都是数组的首地址,如上例中的px=*px=arr=0012FF14
- 数组指针和指向数组的指针的主要区别在进行运算时的单位不同,前者为数据类型宽度×数组成员数,后者为数据类型宽度
数组指针的应用
可以利用数组指针进行加减时的单位不同来遍历数组的固定间隔的成员
下例为从数组的第二个成员开始,取出间隔为3的数组成员
代码
#include "stdafx.h"
void function(){
int arr[15]={1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
int (*px)[3];
//从数组的第二个成员开始
px=(int (*)[3]) &arr[1];
int i=0;
for(i=0;i<15/3;i++){
printf("%x\t%d\n",px+i,**(px+i));
//注意这里取了两次*,第一次获得的是指向数组成员的指针,第二个获得的才是数组成员
}
}
int main(int argc, char* argv[])
{
function();
return 0;
}
运行结果