[音乐] 本周的主要内容是介绍高级语言程序的机器级表示
前几讲分别介绍了过程调用、 选择结构和循环结构的机器级表示
以及数组、 指针、 结构体和联合体这些复杂数据
类型的分配,数据的对齐、 访问及其处理逻辑对应的机器级表示
本讲将通过介绍对数组的越界访问 和缓冲区溢出攻击来结束本周的课程
大家应该还记得 前面我们讲过一个例子,这个例子当中
有一个函数,这个函数里面有两个数组都是局部变量
一个是d,一个是a。
然后呢,最后 返回的是其中一个数组元素它的值
也就是d[0],但是呢执行的结果,当入口参数
等于0和1的时候,返回的是正确的 大于1的时候,返回的就是错误的了
这个问题是由于数组访问越界引起的 大家应该还记得这样一个例子
在这个里面,如果i等于0的时候,a[0] 在这个地方赋值的时候,它就是
改变的是这个四个单元的内容 如果是i等于1的时候,改变的是这四个单元的内容
当i等于2的时候,用2去访问的时候,改变的是这四个单元的内容 这时候就把原来的3.14冲掉了一部分
然后当i等于3的时候呢,又冲掉的是高位部分,所以这个
值就更离谱,离3.14更远,当i等于4的时候 它就把返回的
一些状态,原来保存的一些状态给冲掉了,这时候就会发生 保护错。
虽然这个d3.14还能正确地显示 但是呢,后面执行的时候,就会出现保护错了。
从这个例子当中我们知道 它的这些错误的引起是因为 当i大于1的时候,等于2、
3、 4的时候,实际上已经超出了这个数组 a这个数组它的边界,是由于对数组的引用
没有进行边界约束,没有去看数组访问的时候是不是
超越了这个数组最大的边界,没有去进行
这种判断而直接执行了赋值语句,这样而造成的一些结果
这种称为缓冲区溢出,这种 有意或者无意地超越数组存储范围的
这样的一些访问,实际上是 无法发现的,这种情况下
就产生了缓冲区溢出,也就是刚才我们看到的 a[0]、
a[1]这两个数组元素的访问没有问题,到a[2]、 a[3]、 a[4]的时候都已经
超出了这个数组的区域,就会把其他的信息给破坏掉。
这种破坏 我们称为缓冲区溢出,也称为写溢出
比如说我们有10个数组元素,都是char型的,也就说这个缓冲区里面最多可以放1- 0个字节
如果我们构造一个字符串,这个字符串呢 包含的字节多于9个的时候,实际上
就会超出了10个字符所占的缓冲区 那么就会发生写溢出,会把其他的信息给破坏掉
这种缓冲区溢出是一种非常普遍,或者是非常危险
的一种漏洞,在很多地方是广泛存在的。
这种缓冲区溢出 会被利用来进行一些攻击,这种攻击会导致程序运行失败
甚至系统关机或者重启等等一些 非常严重的后果,下面我们来举个例子
看一看如何利用缓冲区溢出这样的漏洞进行攻击 这个缓冲区攻击是因为
我们进行数组访问的时候,超出了数组的边界而引起的
下面的这个例子就是利用这种缓冲区溢出
然后转到自己设定的一个程序,这个程序比如说是一个hacker
这样的一个程序去执行,下面我们举这样一个例子 这个例子当中,这个main函数
调用了一个outputs这样一个函数 然后给定的是参数,就是给定的是一个命令行后面的这个第一个参数
这个参数实际上是一个字符串,把这个字符串传递给被调用函数
然后这个被调用函数当中,有一个局部变量这个数组
实际上是分配在栈里面的,然后通过传递进来的这个参数的字符串的首地址
这个首地址开始一串字符把它复制到
局部变量,就是这个buffer所占的这个区域里面去 采用的是字符串拷贝的这样一种方式
拷贝过来以后,再把拷贝过来的字符串把它打印出来 就是这样的一个程序。
我们假定这个程序它的可执行文件名为test 就是它和它,这个main和outputs
这两个函数构成的可执行文件名为test
然后呢我们知道在函数调用的过程当中 它会生成一个一个栈帧,这儿是main的栈帧
在这个里面,会把这个参数先压栈,压的参数
就是命令行当中的第一个参数的首地址,然后执行call指令
就会把返回地址,就是从outputs返回到main的这个返回地址
压栈,然后就进入outputs去执行,在这个里面那当然是
先给局部变量分配空间。
这里有16个 char类型的一个数组,这个buffer是一个数组,每个元素是一个char类型,八个
八位一个字节,所以一共16个字节,因此在outputs的栈帧里面
申请了16个字节的空间 将来分别会放buffer[0]到buffer[15]这16个数组元素
然后紧接着呢就要去调strcpy 调strcpy之前要把参数入栈,这个参数
先把传递过来的首地址会送到这儿
所以我们可以看到这个首地址呢就会送到这儿,这个实际上是EBP加8 EBP加4、
加8的内容送到这儿,然后 再把左边这个参数,就是buffer,也就是这个地址
这个buffer[0]所在的这个地址 也就是EBP减16,就是减4、
减8、 减12、 减16这些地址 送过来,完了以后就去call,就是用一条call指令
去调用strcpy函数的第一条指令,转到那儿去执行
执行call指令的时候,当然是会把返回地址压栈了,整个过程是这样子。
我们可以 看到outputs的栈帧里面,应该有16个字节的buffer数组
然后呢,它的栈底,栈帧的底部,总是存放EBP的旧值
在这个栈上面就是从outputs返回main的这个返回 地址。
所以我们可以看出,如果在这个strcpy这边
在这个里面,如果这个str这个参数开始的一个字符串
这个字符串因为strcpy这个函数是看这个原串最后
空,到一个空字符,表示一个字符串结束
那么到空为止的所有的字符,一一会拷贝到buffer里面
如果我们拷贝的这个字符的个数多于
25个的时候,就是拷贝了这16个字符以后,紧接着再 这个4个字节,再4个字节,那就是
最后加一个空字符,拷贝了25个字符,也就是说这边传递过来的
outputs从main这个地方传递过来的
这个参数,也就是一个字符串的个数大于25的时候
就会把这个返回地址给冲掉,一旦返回地址冲掉,它就不可能再返回到main执行
也就是说当命令行当中的字符串,也就是这个,超过25个的时候,我们这个函数
就会使得这个缓冲区buffer 溢出,溢出到EBP的旧值
并且溢出到返回地址,使得返回地址这个地方的值被破坏
就不能够返回到main执行了 这个是缓冲区溢出的基本的原理
那么我们来看一看outputs它的汇编代码,我们对刚才的可执行文件反汇编以后
就会得到outputs的汇编代码,在这个里面我们可以看到
我们前面讲过所有的函数或者过程开始的2条语句都是构成一个栈帧的底部,
也就是形成这个,然后esp
减去一个值就使得这个栈帧 长出来一段,减去的值就是所长出来的这个空间。
esp减16进制的18也就是24,
也就是esp本来是指向这个地方,减去24以后就指向了这个地方。
那么这里面一共有24个字节, 这24个字节就是16个字节的局部变量
然后加上2个字节的参数,一共是26个字节, 也就是16进制的18,所以执行完这条指令以后
esp就指向这个位置。
然后呢,我们对esp这个地方进行赋值,赋的是什么呢?赋的是ebp
减16这个是一个负数,负16 就ebp减16的那个地方的地址,
ebp是在这个位置,减16就是buffer[0]的位置,
因此实际上是把buffer[0]的地址送到esp所指 的这个地方,我们刚才讲过esp已经指向了这个位置了。
实际上也就是把buffer的参数送到这个地方,也就是buffer[0]的地址送到这- 个地方,
然后是把ebp 加8的内容,ebp在这个位置
ebp在这个位置加8的内容当然就是 传递过来的这个参数,把这个参数再送到esp加4的位置,
esp在这儿,esp加4的位置就在这儿, 这2条指令的功能就是把命令行参数首地址,也就是这个参数
送到这个位置,我们这个实际上就是strcpy 这个函数的2个实参,2个实参送到了这个位置,
也就是源操作数的地址和末操作数的地址拷贝功能
把源字符串拷贝到末字符串,这是两个字符串的 首地址,完了以后就去执行strcpy了,就是call
这个地方的转到这个地方去执行,这个地方就是strcpy的首地址。
所以我们可以看到通过output当中的一些指令实际上构造了一个
outputs的一个栈帧,在这个栈帧当中 有保存了旧的值和局部变量的一个空
间以及调用其它函数或者过程的一些参数,
因此在是strcpy当中复制25个字符就从这个地方开始复制,
25个的话,就会把16,就是buffer[0]到buffer[15]这16个字符
紧接着这4个再加4个那么这样就是24个,再加上空 空字符表示结束,空字符表示结束。
这样的话就会发生问题就会把返回地址给冲掉,
如果我们把返回地址这个地方正好放的是hacker这样一个 预设的这个程序的首地址的话,
它就会返回到预设的这个程序执行而不会返回到main去执行,
这样的话就可以实行攻击,用预设的一段程序来进行攻击。
我们在举具体的例子之前我们先来看看怎么加载攻击代码,
实际上攻击代码是通过调用这个函数来实施的, 而这个函数实际上是一个系统调用,是在这些操作系统当中
的系统调用,它可以用来加载并执行一个程序。
这个实际上就是它通过这个函数把刚才我们 讲的那个test有漏洞的程序加载到系统运行。
这个函数它的用法是这样的,它这个里面有一个参数呢是
被加载或者执行的一个文件的名称,实际上也就是
可执行文件的名称,比如说我要加载./hello这个程序执行
那么这个filename实际上就是./hello这个可执行文件名。
这个当然是一个字符串,这个字符串的首地址传递过来作为第一个参数
当然这样一个可执行文件实际上是一个命令,这个命令可以带参数,
这个参数是用这个指针数组构造的,如果有参数 的话,这边可以带上指针数组,如果没有的话,那可以是空。
如果带环境变量的话,这儿可以带环境变量, 如果没有的话,这个地方也是可以用空来进行调用。
在执行 这个函数过程当中如果发生了错误了,比如说我们要找这个可执行文件
在这个可执行文件名指定的这个可执行文件,在盘上找不到,
那么就返回负1,然后控制权呢就交给调用这个函数的那个程序,实际上就 结束了。
这个函数执行过程当中它实际上是会到盘上去找指定的这个可执行文件
有没有,如果能找到那么它把它加载进来,加载成功的话,那么就
不返回调用程序,这时候它将控制权呢就传递到
可执行目标中的main函数,比如说hello这个 可执行程序,hello这个可执行程序当中一定有一个main函数
这个函数执行完了以后,实际上就转到hello那个里面的main函数去执行了。
而每一个main函数它的原型 是这样的,这个实际上是任何一个可执行文件
或者是可执行程序它的一个入口,因为它是一个命令行,命令行当中
所有的个数也就是命令行是由以空格字符分割的 若干个字符串,这个字符串的个数就是
这个参数,这个命令行或者叫参数列表 它是用一个指针数组来构造的,
然后还有环境变量,所以这个环境变量最后可以从这个地方就是传递过来。
我们可以看到举个例子比如说我们这个hello程序,它 是不带参数的,所以它的命令行只有一个可执行文件名,
因此它的参数列表实际上就只有一个可执行文件名。
这个命令行最后总是用一个空字符串来表示的,
所以整个命令行或者叫参数列表当中的这个长度
字符串的个数有2个,所以这个参数等于2,一个呢就是这个字符串 然后再加上一个空字符串所以是2。
如果我们构造一个刚才的test这个例子,构造这么一个命令行就表示
执行这个test这个程序刚才那个可执行文件, 这可执行文件的第一个参数是这样的一个字符串,
这个字符串实际上就是刚才我们看到的str针指的那个字符串。
这个命令行有这里一个字符串中间这儿是空格,
然后后面一个字符串,所以有2个字符串再加上空结尾,所以它是等于3。
这个是指针数组,指针数组的第一个
数组元素是它,这个数组元素实际上是一 个指针,我们讲过因为是指针数组,每个元素是一个指针。
这个指针里面指向的是这个字符串的
第一个字符的地址,第二个元素当然也是一个指针,
指向的是后面这个字符串的第一个字符的 位置,它是一个地址,指向的是0这个
字符的位置,这个呢指向的是点这个字符的位置。
刚才我们讲的这个是一个有漏洞的程序,在
这个里面如果把它编译汇编链接以后生成一个可执行文件叫test,
那么因此我们可以在命令行提示符下面输入这样一串命令,
这串命令的意思就是执行这个可执行文件空格后面的这个 是一个字符串,这个字符串的首地址
是由这个数组元素来指定的,它是一个指针。
实际上我们把这个命令行
按照空格划分,这儿有一个空格划分成
2个字符串,这2个字符串实际上就构成一个指针数组,
这个指针数组就是它,就是这个指针数组
实际上就是构造成这样的一个指针数组 第一个数组元素是一个指针指向的是
这个空格前面的这样一个字符串,第二个数组元素
也是个指针指向的是空格后面的第二个字符串 最后再加一个空,这样的一个数组元素
所以这个地方的数组元素一共有3个, 因此我们要构造这个命令行的时候我们实际上是
第0个这个字符串,就是这儿的这个字符串
第1个实际上是这个字符串,这个字符串事先可以进行初始化
就是构造一个char类型的字符串,这个字符串 的内容就是0123456789然后一直到这儿
然后后面是几个不可显示的字符,就是这边的这个不可显示的
字符,最后一个是空,是一个以空结尾的一个字符串
就赋给了它,最后这一个数组元素空再赋给它 这个指针数组一共有3个元素0、
1、 2 3三个元素,然后我们用这个刚才我们讲的
加载并执行,这样的一个函数实际上是一个系统调用的封装函数。
在这个里面我们可以表示要启动
这个程序执行,也就是test这个程序执行它的命令行参数
是由这个指出来的,实际上我们已经对它进行了初始化
而环境变量设定的这些参数是空,我们不管
这样的话我们就可以通过执行这个函数 我们就可以启动可执行文件test执行
并且把test的参数设定为这样一个字符串0/1/2/3/4设定为这样一个字符串,
字符串的首地址赋给这个变量 然后来调用output这样的一个
被调用函数,这个0/1/2/3/4这个字符串的首地址实际上通过
参数传递就赋给了str,在这个里面实际上就把这个0/1/2/3/4
ABCD这样一个字符串,25个字符的字符串要把它拷贝到buffer
当中去,而buffer呢在栈里面占了16个单元,那么
这个25个字节拷贝到这16个单元所占的空间里面去
执行完这个程序以后实际上已经发生了缓冲 区溢出了,我们去打印的时候打印出来的
就是code构造的这个值,也就是通过code构造好了以后
传递给它它呢再通过这个变量 传递给它,它再实际上就是
传递给它,它在传递给它 它在这个里面再传递给buffer
buffer再把它打印出来,实际上打印出来的就是这样的一串字符。
后面的这几个不可显示的字符会把 outputs返回到main的那个返回地址
给冲掉,这样的话这个漏洞就可以被利用来进行攻击
可以设定一个攻击程序,如果这个黑客程序
它的首地址被置到这个返回地址当中去的话,那么执行完outputs以后就会执行这个
这个程序,然后显示出这样一个字符串出来。
这个执行完了以后实际上是,如果说刚才我们讲 的没有任何错误的话,就会把main调出来执行
main调出来执行的过程就是,main调outputs,outputs返回
返回到hacker去执行,而不会返回到main执行,是因为我们事先把一个
地址,这个地址就是这儿804 8411实际上是这样一个地址也就是说如果
这个地址就是这个函数的首地址的话,那么我们预先把这个返回
地址被置到了这个地方所以会 按照黑客自己的去执行它预设的一个程序。
我们可以看到这个
里面就是我们刚才讲的设定的这个8048400 那这时候我们可以构造这个8048400
作为它的一部分,然后在这个过程当中我们可以看到这边
0123456789ABCDEF这16个字符
是冲的是就是,执行strcpy的时候
实际上是从这儿开始复制的,复制到buffer0,buffer1一直到buffer15
因此这16个字符冲到这儿 然后呢这4个字符4个大写的X
就紧接着把这4个位置冲掉,4个X就显示在这儿, 然后紧接着这个地方放的这个
地址就是0804 8411
那么这样的话,执行完这个outputs以后它就从这个地方去返回地址执行
这个返回地址执行实际上就是执行的是这个
程序的首地址,实际上就是转到这个程序的首地址去执行,
执行的结果就是把它给显示出来,因此我们可以看出
执行这个攻击程序实际上也就是执行这个main函数
这个攻击程序,先执行它执行它以后就执行test
执行test以后实际上是 从test里面的main调到了outputs执行
outputs执行也就是进行 复制,复制完了以后就把这个炸弹给埋在这个地方,
当它返回的时候就从这个地方返回 因此output执行完了以后就执行到了hacker,
因此我们可以看到在outputs里面通过printf会把这个
str传过来的在buffer里面以及溢出 到这儿的这个字符串全部显示出来,显示出来以后就返回,
返回以后执行hacker,因此就会执行这个 把这个打印出来,然后紧接着就发生了
存储保护错了,这个存储保护错是因为执行hacker
过程的时候在最后执行return指令也就是hacker程序执行到最后
的时候一定要执行return指令,而执行它的return指令 的时候它的这个返回地址是一个不确定值,其实
进hacker的时候并没有设置返回地址,所以这个返回地址 是一个不确定的值,因此就可以呢跳转到一些
非法的区域,比如说数据区系统区或者其它的一些非法的区域执行, 这样就会造成段错误Segmentation fault。
从这个例子可以看到黑客是怎样利用缓冲区溢出这种漏洞来进行攻击的。
[音乐] [音乐]