0x03 C语言进阶¶
恭喜各位朋友已经对C语言有了一个初步的认识
那么接下来,你将会接触到C语言地狱级难度的部分~
C语言的数组、地址、指针、控制流与系统调用¶
上集回顾¶
回顾一下Episode 0x01,我们写了一个计算斐波那契数列特定项对应值的程序
#include<stdio.h>
int fibonacii(int target)
{
int i = 1;
int i_before = 0;
//第round轮计算完成之后,实际上i的值是第round+1项的值,因此这里需要循环*target-1*轮
for(int round=1;round<target;round++)
{
int temp = i;//这里因为需要暂时保存
i = i_before + i;
i_before = temp;
}
return i;
}
int main()
{
int target;
printf("Please enter the number of items you want to calculate: \n");
//像"\n"这类的转义字符用于表示不可视的字符,'\n'表示回车符
scanf("%d", &target);
int result = fibonacii(target);
printf("Result: %d\n", result);
return 0;
}
不知道你发现没有,我们实际上把数列的前target项都计算了,但并没能存储下来,导致哪怕是需要用到计算过的数据,都需要重新计算一遍...
那么有没有一种数据类型,能够存储一整组数据呢?
有的兄弟有的
数组¶
数组,就是 存储在连续空间的一组同类型的变量,便于存储便于取用
- 存取时直接使用 数组下标就可以找到 对应的某个元素的存储位置/值
int a[50];//这句话声明了一个可以存储50个整数类型元素的数组a,数组名和*数组首地址指针*都是a,指针的定义后续会讲~
a[0] = 5;//a数组的第1项(数组下标为0,项数-1)赋值为5
a[3] = 8;//第4项赋值为8
a[8] = 33550336;
a[49] = 114514; //可以看到数组具有随机存取的性质
随机存取:当我需要第20个元素时,无需one-by-one挨个数到第20个,而是可以直接索引到第20个元素。正式描述是:可以直接访问数据结构中的任何位置,而不需要按顺序访问
搭配循环结构和数组,我们可以很好的用计算机的高频处理性能,处理很多浪费人力的工作
比如我希望使用三个数列
A:1,2,3,4,...,n
B:1,3,5,7,9,...,2n-1
C:1,2,4,8,16,...,2^{n-1}
计算
D:a_1+b_1+c_1,a_2+b_2+c_2,a_3+b_3+c_3,...,a_n+b_n+c_n
的前n项并输出(n<100),就可以用以下的程序来完成
各位新手朋友可以一起操作,尽量手写勤加练习~
#include<stdio.h>
int expo(int base, int exp)//这里需要我们手写一个幂函数,
{ //注意这个幂函数没有处理exp为负数时的逻辑,不能处理负数输入
int result = 1;
//一共执行指数轮循环,每轮循环乘以一次底数,共指数个底数相乘,即 底数的指数次幂
for(int i=1;i<=exp;i++)
{
result = result * base;
}
return result;
}
int main()
{
int n;
scanf("%d", &n);
int D[100] = {0};
for(int i=0;i<n;i++)//从数组下标为0开始存储
{
D[i] = (i+1) + (2 * (i+1) - 1) + expo(2, (i+1)-1);
//没有现成幂计算运算符,需要自己实现
//由于i从0开始,会导致数组首项为D[0]= 0 + 2*0-1 + 2^(0-1),出现错位
//因此需要在数组下标为i时,使用各数列的第i+1项,这样才能避免错位发生
}
printf("%d %d\n", D[n-1], D[m-1]);//第n项、m项数组下标分别为n-1、m-1
return 0; //(因为数组下标从0开始)
}
不知道你会不会有这样的好奇:
我们上述写了一个数组D(下面给出前五项)
int D[5] = {3, 7, 12, 19, 30};
如果我们打印出来D这个变量会是什么情况呢?
printf("%d\n", D);
我们发现输出了一串根本读不懂的数字...这是为什么呢?
我们开始用不同格式的方式输出
printf("integer: %d\nhex integer: %x\naddress: %p\n", D, D, D);
发现输出结果如下
我们可以看到,每一次执行,整数格式和hex格式都在发生无规律的变化,但我们发现:
- address的后8位数字,永远和hex(16进制的整数格式)一致,而 计算机内存地址往往就是以16进制表示,64位系统的地址是16字节长度,32位系统地址为8字节长
这里说明,实际上我们打印出来的数组名D,其实就是一个地址
地址与指针¶
那么我们不妨打印出来看看,这个地址到底存储的内容是什么
printf("%d\n%d\n%d\n", *D, *(D+1), *(D+2));//星号*理解为 取某地址下存储的数据 即可
D,D+1,D+2 的地址下存储的数据,其实就是数组D存储的数据
不光是数组拥有地址,所有变量 乃至于 整个程序 都要加载到 计算机内存里 才能正常运行,因此整个程序都有对应的地址
我们用GDB工具打开可执行文件,可以看到程序运行过程中的实际地址。
大致计算一下,这样一个程序,需要用到30M左右的内存。
这里我们使用GDB的反汇编功能,生成了程序汇编代码,也可以找到main函数的存储地址
这里实际上说明,函数也拥有地址,在我们调用函数的过程中,实际上就是把这个地址交给了CPU,CPU控制程序跳转至这个函数的首地址,并把跳转之前的程序位置记录,当触发return指令时,CPU再带着返回值,控制程序跳回记录的位置,从而实现函数调用。
有细心的同学可能看到汇编代码里有一个些类似数组的语句:PTR [rbp-0x8]
,这个PTR
即为指针pointer
。
我们知道 取地址符& 可以使用变量名获取变量存储的地址
而指针* 就是为了便于 使用地址获取存储的变量值,是对内存地址的直接操纵
实际上,数组名本质上就是一个 指向数组首地址的指针
#include<stdio.h>
int add(int a,int b)
{
return a+b;
}
int main()
{
int a = 114514;
int *var_p;//声明一个整数指针p
var_p = &a;//指针p 指向 变量a的存储地址
printf("%d\n", *var_p);//等效于printf("%d", a);
int (*func_p)(int, int);//函数指针
func_p = &add;//func_p指向add函数的地址
//也可以简写为 func_p = add; 因为函数地址的特殊存储机制将函数名作为指向自己地址的指针
int result = (*func_p)(3, 5);
printf("%d\n", result);
return 0;
}
执行结果,发现可以使用变量指针直接使用变量值,也可以使用函数指针直接调用指定函数
注意:C语言也因为 指针与地址机制会对内存直接操作这个特性,导致极易出现漏洞进而被攻击!
对于我们这种攻击者而言,那自然是漏洞越多越好啦~ ○( ^皿^)っHiahiahia…
上述我们了解了函数指针,那么接下来我们正式进入PWN的核心领域——程序的栈机制
程序栈¶
有这样一种结构,像一个弹匣一样,越先压进去的子弹 开火的时候越靠后才能发射出来,而最后压入弹匣的子弹最早打出(LIFO后进先出 Last-In-First-Out)
而我们发现:这种结构极其适合用来当作函数调用的存储结构
例如:
#include<stdio.h>
void func3()
{
return;
}
void func2()
{
func3();
return;
}
void func1()
{
func2();
return;
}
int main()
{
func1();
return 0;
}
它的调用链如下图所示
如果使用栈的结构,就会形成这样的效果:
可以说是极其适配了~
每个程序都会开辟一块栈空间用来存放函数调用流、函数内声明的 局部变量
局部变量:定义在某个函数上的变量,存储在栈上,会随着这个函数结束 而 销毁并释放对应的存储空间
全局变量:定义在整个程序上的变量,存储在 堆 上,只有整个程序进程终止之后才会释放
除了栈,还有一个存储区域,叫做 堆,相比于栈空间,堆是巨大的,允许开发者使用malloc()
等函数进行动态内存分配,自行开辟堆空间用来存放数据
但是相对的,堆内存由于开发者自主操作,不会像栈一样自动释放,因此需要开发者自行使用
free()
函数释放空间。但开发人员往往会忽略这一点,造成了大量的漏洞和性能浪费
系统调用¶
有这样一种特殊的函数,它们连接着 这个程序本身 和 操作系统,作为这个独立程序和操作系统、甚至是其他程序交互的接口
例如system()
,参数为字符串,会直接使用命令行(shell)执行字符串内的指令
PWN手常用的getshell手段,就是让一个程序执行system("/bin/sh");
语句。这条语句可以直接开放一个shell供攻击者交互,攻击者完成了getshell的目标,就已经成功侵入了这台执行本程序的计算机/服务器。
题外话¶
至此,我们已经把所有(至少是大多数)学习PWN方向所需的C语言基础学习完毕了
各位新手朋友一定要多练习多练习多练习,C语言需要大量的实践才能比较有效地提升理解
接下来你会借助C语言,理解计算机最底层的编程语言——汇编语言
准备好接受计算机世界最极致最纯粹的力量吧!
搭配C语言和汇编语言,你可以很好地分析程序逻辑、调试程序、进而挖掘程序漏洞、利用程序漏洞完成攻击
至是,工程已毕(bushi 接下来你将接触到真正的PWN!
知识清单¶
- 数组
- 地址
- 指针
- 程序栈和堆内存
- 系统调用
- 常用漏洞函数