2017-2018-1 20155333 《信息安全系统设计基础》第五周学习总结

2017-2018-1 20155333 《信息安全系统设计基础》第五周学习总结

教材学习内容总结

  • 机器级代码

计算机系统使用了多种不同形式的抽象,利用更简单的抽象模型来隐藏实现的细节。对于机器级编程来说,其中两种抽象尤为重要:

  • 指令集体系结构(Instruction set architecture ISA)。
    它定义了处理器状态、指令的格式,以及每条指令对状态的影响。
    IA32将程序的行为描述成好像每条指令时按顺序执行的,一条指令结束后,下一条再开始。(实际上处理器并发地执行许多指令,但是可以采取措施保证整体行为与ISA指定的顺序执行完全一致)

  • 机器级程序使用的存储器地址是虚拟地址。提供的存储器模型看上去是一个非常大的字节数组。存储器系统的实际实现是将多个硬件存储器和操作系统软件组合起来。

程序存储器(program memory)包含:程序的可执行机器代码、操作系统需要的一些信息、栈、堆。程序存储器用虚拟地址来寻址(此虚拟地址不是机器级虚拟地址)。操作系统负责管理虚拟地址空间(程序级虚拟地址),将虚拟地址翻译成实际处理器存储器中的物理地址(机器级虚拟地址)。

  • 函数调用过程
  • 进程的虚拟地址空间
  • 函数调用时的用户栈

IA32程序用程序栈来支持过程调用。机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。
栈向低地址方向增长,而栈指针%esp指向栈顶元素。

  • 寄存器使用惯例

程序寄存器组是唯一能被所有函数共享的资源。
虽然在给定时刻只能有一个函数是活动的,但是我们必须保证当一个函数调用另一个函数时,被调用者不会覆盖某个调用者稍后会用到的值。为此,IA32采用了一组统一的寄存器使用规则,所有的函数都必须遵守,包括程序库中的函数。
根据惯例:寄存器%eax、%edx、%ecx被划分为调用者保存寄存器。当过程P调用Q时,Q可以覆盖这些寄存器,不会破坏任何P所需要的数据。
另一方面,寄存器%ebx、%esi、%edi被划分为被调用者寄存器。这意味着Q必须在覆盖这些寄存器的值之前,先把它们保存到栈中,并在返回前恢复它们。此外还必须保持寄存器%ebp和%esp。

  • 转移控制

call Label 过程调用

call *Operand 过程调用

leave 为返回准备栈

ret 从过程调用中返回

call指令的效果是将返回地址入栈,并跳转到被调用过程的起始处。(返回地址是在程序正文中紧跟在call后面的那条指令的地址,这样当被调用过程返回时,执行流会从此处继续。)

ret指令从栈中弹出地址,并跳转到这个位置。(使用这条指令前,要使栈做好准备,栈顶指针要指向前面call指令存储返回地址的位置)

leave指令 使栈做好返回的准备。它等价于:

movl %ebp, %esp ; 把寄存器%ebp中的值复制到寄存器%esp中(回收本函数的栈空间)

popl %ebp
leave指令的使用在返回前,既重置了栈指针,也重置了基址指针。

【示例】

int 
swap_add(int* xp, int yp)
{
    int x = *xp ;
    int y = *yp ;
   
    *xp = y ;
    *yp = x ;

    return x+y ;
}

int 
caller()
{
    int arg1 = 534 ;
    int arg2 = 1057 ;
    int sum = swap_add(&arg1, &arg2) ;
    int diff = arg1 - arg2 ;

    return sum*diff ;
}

函数的返回值是放在寄存器%eax中的。

Gcc分配了从不使用的空间,
为caller参数的代码在栈上分配了24个字节,但是只使用了其中的16个。因为,GCC坚持一个X86编程指导方针,也就是一个函数使用的所有栈空间必须是16字节的整数倍。
包括保存%ebp值的4字节和返回值的4字节,caller一共使用了32字节。采用这个规则是为了保证访问数据的严格对齐(alignment)。

在返回前,函数必须将栈恢复到原始条件,可以恢复所有的被调用者保存寄存器(手动) 和%ebp,并且重置%esp使其指向返回地址。(由leave指令完成)
然后,ret指令把返回地址弹出到pc寄存器,CPU继续从原调用处开始执行。

  • 缓冲区溢出

通常,在栈中分配某个字节数组来保存一个字符串,但是字符串的长度超出了为数组分配的空间。C对于数组引用不进行任何边界检查,而且局部变量和状态信息,都存在栈中。这样,对越界的数组元素的写操作会破坏存储在栈中的状态信息。当程序使用这个被破坏的状态,试图重新加载寄存器或执行ret指令时,就会出现很严重的错误。

【示例】

void echo()
{
    char buf[8] ;
    gets(buf) ;
    puts(buf) ;
}

由于栈是向地地址增长的,数组缓冲区是向高地址增长的。故,长一些的字符串会导致gets覆盖栈上存储的某些信息。
随着字符串变长,下面的信息会被破坏:

输入的字符数量 被破坏的状态

0---7 无

输入的字符数量 被破坏的状态
0---7
8---11 保存的%ebx的值
12---15 保存的%ebp的值
16---19 返回地址
20+ caller中保存的状态

如果破坏了存储%ebp的值,那么基址寄存器就不能正确地恢复,因此调用者就不能正确地引用它的局部变量或参数。
如果破坏了存储的返回地址,那么ret指令会使程序跳转到完全意想不到的地方。

缓冲区溢出的一个更加致命的使用就是让程序执行它本来不愿意执行的函数。这是一种最常见的通过计算机网络攻击系统安全的方法。通常,输入给程序一个字符串,这个字符串包含一些可执行代码的字节编码,称为攻击代码,另外还有一些字节会用一个指向攻击代码的指针覆盖返回地址。那么,执行ret指令的效果就是跳转到攻击代码。

通常,使用gets或其他任何能导致存储溢出的函数,都不是好的编程习惯。不幸的是,很多常用库函数,包括strcpy、strcat、sprintf,都有一个属性——不需要告诉它们目标缓冲区的大小,就产生一个字节序列。

  • 对抗缓冲区溢出攻击

1、栈随机化
为了在系统中插入攻击代码,攻击者不但要插入代码,还要插入指向这段代码的指针,这个指针也是攻击字符串的一部分。产生这个指针需要知道这个字符串放置的栈地址。在过去,程序的栈地址非常容易预测,在不同的机器之间,栈的位置是相当固定的。
栈随机化的思想使得栈的位置在程序每次运行时都有变化。因此,即使许多机器都运行相同的代码。它们的栈地址都是不同的。
实现的方式是:程序开始时,在栈上分配一段0--n字节之间的随机大小空间。程序不使用这段空间,但是它会导致程序每次执行时后续的栈位置发生了变化。

在Linux系统中,栈随机化已经变成了标准行为。(在linux上每次运行相同的程序,其同一局部变量的地址都不相同)

2、栈破坏检测
在C语言中,没有可靠的方法来防止对数组的越界写,但是,我们能够在发生了越界写的时候,在没有造成任何有害结果之前,尝试检测到它。
最近的GCC版本在产生的代码中加入了一种栈保护者机制,用来检测缓冲区越界,其思想是在栈中任何局部缓冲区与栈状态之间存储一个特殊的金丝雀值。这个金丝雀值是在程序每次运行时随机产生的,因此,攻击者没有简单的办法知道它是什么。
在恢复寄存器状态和从函数返回之前,程序检查这个金丝雀值是否被该函数的某个操作或者函数调用的某个操作改变了。如果是,那么程序异常终止。

3、限制可执行代码区域
限制那些能够存放可执行代码的存储器区域。在典型的程序中,只有保存编译器产生的代码的那部分存储器才需要是可执行的,其他部分可以被限制为只允许读和写。
现在的64位处理器的内存保护引入了”NX”(不执行)位。有了这个特性,栈可以被标记为可读和可写,但是不可执行,检查页是否可执行由硬件来完成,效率上没有损失。

教材学习中的问题和解决过程

  • 问题1:栈帧结构(stack frame)
  • 问题1解决方案:寄存器%ebp作为帧指针,寄存器%esp作为栈指针。

栈指针可以移动,大多数信息的访问都是相对于帧指针的。

参数,返回地址在调用者P的栈帧中。返回地址是当程序Q(被调用者)返回时应该继续执行的地方。

过程Q也用栈来保存其他不能存放在寄存器的局部变量,是因为:
寄存器不够存放所有的局部变量。

有些局部变量是数组或结构,必须通过数组或结构引用来访问。

要对一个局部变量使用地址操作符&,因此我们必须能够为它们产生一个地址。

  • 问题2:字长是CPU的主要技术指标之一,指的是CPU一次能并行处理的二进制位数,字长总是8的整数倍,通常PC机的字长为16位(早期),32位,64位。32位处理器每次能处理 32bit(4Byte),同理,64位处理器每次能处理 64bit(8Byte) 。

32bit : 1111,1111, 1111,1111, 1111,1111, 1111,1111 = 232 = 4294967295 Byte = 4 GB

 这里的单位为什么是Byte而不是bit?

  • 问题2解决方案:因为字长限制的是最大虚拟地址空间,而不是内存容量。而一个虚拟地址空间能够引用一个Byte,也就是说有多少虚拟地址空间就有多少Byte的内存容量,其间不需要作*8/8的换算。

  总结:虚拟地址空间的最大大小,受CPU位数的限制,而虚拟地址空间的最大值等价于内存能够被利用的最大容量。

  • ...

代码调试中的问题和解决过程

  • 问题1:XXXXXX
  • 问题1解决方案:XXXXXX
  • 问题2:XXXXXX
  • 问题2解决方案:XXXXXX
  • ...

代码托管

(statistics.sh脚本的运行结果截图)

上周考试错题总结

  • 错题1及原因,理解情况
  • 错题2及原因,理解情况
  • ...

结对及互评

点评模板:

  • 博客中值得学习的或问题:
    • xxx
    • xxx
    • ...
  • 代码中值得学习的或问题:
    • xxx
    • xxx
    • ...
  • 其他

本周结对学习情况

- [20155312](博客链接)
- 结对照片
- 结对学习内容
    - 实验一
    - 教材第三章
    - ...

其他(感悟、思考等,可选)

xxx
xxx

学习进度条

代码行数(新增/累积) 博客量(新增/累积) 学习时间(新增/累积) 重要成长
目标 5000行 30篇 400小时
第一周 10/10 1/1 10/10
第二周 80/90 1/2 15/25
第三周 100/190 1/3 15/40
第四周 150/340 1/4 18/58
第五周 2/6 20/78

尝试一下记录「计划学习时间」和「实际学习时间」,到期末看看能不能改进自己的计划能力。这个工作学习中很重要,也很有用。
耗时估计的公式
:Y=X+X/N ,Y=X-X/N,训练次数多了,X、Y就接近了。

参考:软件工程软件的估计为什么这么难软件工程 估计方法

  • 计划学习时间:25小时

  • 实际学习时间:20小时

  • 改进情况:

(有空多看看现代软件工程 课件
软件工程师能力自我评价表
)

参考资料

posted @ 2017-10-22 23:14  弥光  阅读(173)  评论(1编辑  收藏  举报