这是从0开始写ShellCode加载器的第3篇文章,文章列表,样本demo已上传到GitHub

前言

作为一名计算机专业的毕业生,C/C++是大学必学内容。但是学了之后除了写写数据结构和算法,其它地方基本没用过。用C++写shellcode加载器的过程中,遇到了很多问题,今天系统的来解决一下,也是为以后的工作打下基础。

开始之前先测试一下,如果以下代码每一行你都能理解,那么这篇文章你没必要看。

1
2
3
4
5
6
7
8
9
10
11
12
char buf;
char *buf;
char buf[];
char *buf[];

printf("%s",buf);
printf("%s",&buf);
printf("%s",*buf);
printf("%s",buf[0]);

char load(char str[]){}
char *load(char str[]){}

觉得晕了的请继续往下看。

指针与变量

要理解c语言的指针,就得先理解c语言变量的定义和存储。我们都很熟悉一个概念,即:*返回指针型变量所指变量的内容 &返回指定变量存储空间的首地址。这个概念讲的是*&两个运算符的作用,我们先来看看&,取地址符。

1
2
3
4
5
char buf = 'a';
printf("buf = %c , size of buf = %dbyte \n",buf,sizeof(buf));
//buf = a , size of buf = 1byte
printf("&buf = %p , size of &buf = %dbyte \n",&buf,sizeof(&buf));
//&buf = 0061FF17 , size of &buf = 4byte

char类型变量buf,存储的内容是字符a,占空间大小为1字节。buf变量存储的地址为0061FF17,此地址占空间大小为32比特,4字节。

再来看看*,取内容运算符。首先第一个要注意的地方是指针类型变量的初始化,只能有两种情况:

1
2
3
指针变量初始化:将已存在的变量地址赋值 或 NULL
char *buf = 这里一定是个地址 或 NULL
char *buf = 'a' //这种写法是错误的

我们继续往下写

1
2
3
4
5
char buf = 'a';
char *buf1 = &buf; //将buf变量的地址赋值
//buf1 = &buf; //如果上一步赋值为NULL,也可这样写
printf("buf1 = %p ,*buf1 = %c, size of buf1 = %d \n",buf1,*buf1,sizeof(buf1));
//buf1 = 0061FF17 ,*buf1 = a, size of buf1 = 4

此时我们发现,变量buf和指针型变量buf1指向的内存地址是一样的,这也就是说,*buf1和buf是等价的(就好比土豆和马铃薯的关系),更改一个另一个也会改变。

指针与数组

了解了一般变量和指针的联系,我们再来看看数组和指针的联系。

1
2
3
int list[5] = {1,2,3,4,5};
int *l = &list[0]; //将数组首元素的地址赋值
int *j = list; //将数组名赋值

在数据结构中我们学习过,数组又叫线性表,在内存中是一段连续的地址空间。数组名就是数组存储空间的首地址,同时也是首元素的地址。所以以上代码中,指针l和j所指向的地址是完全一样的。

image-20220326210740519

那么,既然数组名list、指针l、指针j,在内存中地址是相同的,他们的意义自然也是等价的。

1
2
printf("j[1] = %d ,l[1] = %d ,list[1] = %d  \n",j[1],l[1],list[1]); 
//j[1] = 2 ,l[1] = 2 ,list[1] = 2

指针对于数组访问还支持加减法

1
2
3
4
5
6
7
8
9
10
11
12
printf("*j = %d \n",*j);//*j = 1 
j = j+1;
printf("*j = %d \n",*j); //*j = 2
j--;
printf("*j = %d \n",*j); //*j = 1
printf("*(j + 1) = %d \n",*(j+1)); //*(j + 1) = 2

l = &list[1];
j = &list[4];
printf("j = %p, l = %p j-l = %d\n",j,l,j-l);
//j = 0061FF0C, l = 0061FF00 j-l = 3
//注意:0061FF0C - 0061FF00 = 0000000C,也就是十进制的12,然而减法结果是3,此处的3其实代表3 × sizeof(int) = 12,即l与j之间相隔3个int型数据。

其实数组名也支持上述运算,只不过为了保持代码可读性,常常使用下标访问数组元素。


第一次学习指针时我就有一个疑问,既然指针能直接操作内存中的数据,那么能不能用本程序的指针去修改其它程序的数据呢?我写了一个小程序测验一下,看看这段程序会不会引起系统其它程序的崩溃。

1
2
3
4
5
6
7
8
int list[5] = {1,2,3,4,5};
int *j = list;
while (true)
{
j++;
printf("j = %p, *j= %d\n",j,*j);
*j = 0;
}

image-20220326222956461

可以看到程序自己崩溃了,并没有引起系统的崩溃。

指针与函数

指针型参数

C语言中函数采用值传递机制,在函数中对形参进行任何修改,都不会影响到实际参数,所以函数中无法访问和更新外部定义的变量。

指针型参数将外部变量的地址传递给函数,函数可以通过指针访问和更新外部变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void swap(char *a,char *b) //a b为形参
{
char temp;
temp = *a;
*a = *b;
*b = temp;
}
int main () {
char a = 'a';
char b = 'b';
printf("a = %c, b = %c \n",a,b);
//a = a, b = b
swap(&a,&b); //a b为实参
printf("a = %c, b = %c \n",a,b);
//a = b, b = a
return 0;
}

对于数组型参数,也是一样的效果,因为我们已经知道了,数组名其实就是指向数组存储空间首地址的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void change(int buf[])
//void change(int *buf) 这两种定义是一样的
{
for(int i=0;i<5;i++)
{
buf[i] = 0;
}
}

int main(){
int buf[5] ={1,2,3,4,5};
for(int i=0;i<5;i++)
{
printf("%d ",buf[i]);
}
//1 2 3 4 5
cout<<endl;
change(buf);
for(int i=0;i<5;i++)
{
printf("%d ",buf[i]);
}
//0 0 0 0 0
}

注意:对于void change(int buf[])的形式参数定义,不管有没有数组大小的描述,都不是一个数组名,而是一个指针变量,仅保存了数组首元素的地址,而没有保存整个数组。

1
2
3
4
5
6
7
8
9
10
11
void change(int buf[])
{
printf("change::size of buf = %d\n",sizeof(buf));
//change::size of buf = 4
}

int main(){
int buf[5] ={1,2,3,4,5};
printf("main::size of buf = %d\n",sizeof(buf));
//main::size of buf = 20
}

分离免杀文章中,我有一个疑惑:data的size为什么是4字节, char *data = new char[length];创建对象,大小为什么不是length的长度?

现在这个问题解决了,data存储new char[length]返回的新创建的数组地址,而不是整个数组。

指针型返回值

函数的返回值类型也可以定义为指针,也就是说函数可以把计算结果放在某个存储单元内,把其存储地址作为返回值送回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int *newlist(int *list,int size)
{
int *newL = new int(size);
//int newL[5]; //以上两种初始化方式都可

//int newL[size]; //错误用法
//int *newL; //错误 注意,指针变量在初始化时一定要赋值地址或NULL,否则指向的是一个无效地址,关于动态内存分配我会在写一篇文章详细探讨
for(int i=0;i<5;i++)
{
newL[i]=list[i];
}
return newL;
}

int main(){
int buf[5] ={1,2,3,4,5};
int *newL = newlist(buf,5);
printf("newL: ");
for(int i=0;i<5;i++)
{
printf("%d ",newL[i]);
}
//newL: 1 2 3 4 5
printf("address of buf = %p, address of newL = %p \n",buf,newL);
//address of buf = 0061FEE8, address of newL = 00EC7EF0
}

学习了这些,函数传参再也不会晕了。


总结

指针型变量,指向内存中的地址,其值是一个地址

数组名也是指针型变量,指向数组存储空间首地址,可用操作指针的方式操作数组名

指针型参数,接收一个地址作为参数

指针型返回值,返回一个地址

指针型变量初始化

1
char *buf = 这里一定是个地址 或 NULL

经过本次学习,对于指针有了更清晰的认识,使用起来也更加得心应手。