从一个黑客的角度看简单的‘hello world!’程序

时间:2023-01-12 20:51:39
#include <stdio.h>
int main()
{
printf("hello word!");
}

5行代码,我们学习编程最先接触到的5行代码,电脑里最先敲入进去的5行代码。大一刚开学的学弟刚接触C语言,跑来问我怎样怎样才能成为电影中的黑客一样。我告诉他,你去把“hello, world!”程序弄明白。一天后,他又跑过来找我,我让他给我讲讲。他给我讲了下面的一句话:我把代码输入电脑,编译之后运行,电脑上输出:hello world!
完了?
完了!

那么,如何站在一个黑客的角度看“hello world!”程序:
(一)编程语言在电脑为什么能够运行(基础扎实的老A们请直接跳过这一过程)
计算机系统由软硬件组成,其中硬件是底部设备。硬件主要包括输入输出于存储设备。软件就是所谓的操作系统。硬件由软件驱动,所以只要能跟操作系统打好关系,它就能帮你走后门。操作系统的依靠是机器语言,机器语言来自于高级编程语言,因此人与操作系统交流则主要靠编程语言。
我们在电脑上敲入hello world!程序的顺序如下:打开一个”.c”后缀的文件——>输入5行代码——>保存——>编译——>运行。具体的过称涉及到很多软硬件的协调工作,如果你足够聪明或者足够的熟练的话,你可以完后很多别人无法想象的效果。
我们按照下面的图来解释说明下:
从一个黑客的角度看简单的‘hello world!’程序

上图的总线是一个基本的界限,总线以上可以看成是操作系统,总线以下是硬件系统。其中,总线以上有一个设备为main memory,也就是我们所说电脑主存,它实际上是一个存在的设备,我们可以插主存条来扩大电脑的主存,但是它存在掉电自动擦除的性能,我们在理解操作系统的时候最好把它抽象成软件部分。总线下有个设备:Disk。就是我们电脑磁盘。有下面的顺序:
打开“.c”的文件后,文件存在于磁盘中,在里面输入代码,保存前这些代码存在于主存中,点击保存后才会存入磁盘。其中代码编译的过称如下:
从一个黑客的角度看简单的‘hello world!’程序
编译的文件都存在于磁盘中,但是编译的过称是调入到主存中完成的。
编译的过称如下:
预处理阶段
这个阶段会对你写的源代码进行预处理,首先是找到#开头的行,比如说#include 这时候就需要加载需要的文件。最后形成了一个完整的源文件内容:以.i结尾的文件。
编译阶段
编辑器在这个阶段会发挥作用把C语言转换为汇编语言。
汇编阶段
这个阶段汇编器把.s结尾的汇编文件转化为以.o结尾的二进制文件
链接阶段
经过上面的几个步骤后程序还不能真正的运行起来,需要将程序用到的系统类库等于这个二进制文件进行组合,最后生成一个可执行的目标文件,这个目标文件就可以执行了~
当然,说起来简单不过的过称充满了可以攻击的漏洞。
(二)代码运行的时候操作系统内部发生了什么
当我们在windows系统或者在linux上运行代码的时候,操作系统将磁盘中的代码与数据通过总线复制到主存中去,当然,代码是可执行代码。假设我们运行了一个程序,那么这个运行的程序叫做进程。程序运行就需要内存来存放运行的代码与用到的数据。而分配内存的过称由操作系统完成。假设我们的主存是4G,其中操作系统占用了一部分。而剩下的部分被操作系统任意的分配给各个进程。这些进程的得到的固定空间就是进程空间。也就是进程虚拟地址空间。进程空间是一个最基本的数据结构,也是技术岗面试的常问问题。它的具体构造如下:
从一个黑客的角度看简单的‘hello world!’程序
我把这个图画成了高地址在下,一般我们看到的图是高地址在上,等降到内存溢出攻击的时候就能看出来为什么这样画。
图形的最下方是环境变量,一般都是操作系统的自我设置,我们一般设计不到。我们一般关心的是栈,因为我们在程序中定义的局部变量一般会存到栈。并且栈有一个很特殊的用处,我们在main()函数中调用了一个函数,栈就会给这个函数分配一定的空间,如果函数调用结束,这片空间会立即被释放,记住,是立即被释放。释放的空间称为无效空间。可以接着被下一个函数调用。中这里可以解释一个我们刚开始接触编程的时候会出现的一个很迷惑的问题。我们定义了一个变量:int x。我用printf把x的值输出,在并没有给定值的情况下,为什么会输出一个随机的值。很明显,x这个变量申请到的空间是刚刚别的函数释放的空间,这个空间虽然被释放,但是里面的值确并没有别擦除,操作系统也不会去做这些无效的工作。所以打印出来的就是一个我们人为地随机值,这就是所谓的残值。
(安全隐患一)这里面存在一个很明显的安全问题,要知道,我们在电脑上输入的账号与密码都是被做为变量存在了栈申请的空间中,输入账号密码的函数调用结束后,这些空间被释放出来,但是并没有被擦掉。如果有一个函数,向主存申请一片足够大的进程空间,然后并没有设置变量,而是将申请的内存空间检查了一遍任何输出,这样一来,账号与密码被读出来的可能是很大的。
再来看一下堆的不同之处,堆与栈不同之处是它申请到的空间是不会被重复利用的,除非你用函数free()把它申请到的空间释放。这又是一个明显的安全隐含。
(安全隐含二)假如说你在堆中申请的空间没有释放,而你还一直在申请内存,那么内存就会按照箭头的方向叠加。但是进程空间总是有限的,如果申请的空间超过了限度,你写的申请空间的函数还在想操作系统索要空间,操作系统再分配的内存就跑出了进程空间,并且是随机分给你的。一旦你动了不属于你的内存空间的话,最好的结果是出现典型错误:segmentation fault!严重的结果是随机(指针操作)分配的内存跑到了系统的内存段中,接下来的就是系统奔溃,并且是不可逆的崩溃。当然这种情况并不常见,因为系统现在都有内存保护机制。单如果你是root登录的话,并不是不能呢出现这种情况。
再看另外的部分,text文本是程序的代码段,与初始化和未初始化的数据是程序的全局变量。这些变量存在于低地址中,且空间是固定的。
(三)缓冲区溢出攻击
我们看到的进程空间的地址是从上往下来增长的,而缓存区溢出攻击就是利用了操作系统的这一特点。利用这一特点可以往别人的代码中插入一大段数据或者可执行代码。
看下列的典型攻击代码:

#include<string.h>

void foo(char *bar)
{
char c[10];
memcpy(c, bar, strlen(bar));
}

int main (int argc, char **argc)
{
foo(argc[1]);
}

上面的代码直接依然对很多的服务器存在攻击型。
其中memcpy函数是将bar的数组复制到c数组中。
(代码中的安全隐患)我们来看看栈的构造
从一个黑客的角度看简单的‘hello world!’程序
这是栈的内存分配效果,main函数在下端,调用的foo函数在上端。但是这里有一个安全隐患。当调用foo函数后,计算机的指针将会返回重新指导main函数。但是在调用foo函数的时候,没有考虑到内存溢出的问题。在执行memcpy函数的时候,如果bar的数组足够大,那么c[10]根本就装不下,但是执行的是全部复制。于是安装往大的地址反向溢出。但是bar是在foo函数中定义的,所以foo函数不会动bar函数的内存。那就只能对不起main()函数了。如果计算的足够精确的话,代码就刚好能够覆盖掉main()函数的原内容。覆盖后,指针返回main()函数接着执行。但是在执行就是你刚写入的代码,这就是典型的缓冲区溢出攻击。
**(四)**hello word!函数的执行过程就是如上,当然这里并没有涉及到线程的运用,如果写入多线程控制CPU的话,安全的隐患将会大大增加。可以在多核CPU中利用单核执行攻击代码,操作系统也不会发现。当然,能做到这一步,需要把脚下的路走好啊!