软件安全课程笔记
软件安全 张淼
概述
信息安全现状
信息安全现象
➔ 病毒、黑客攻击的泛滥
➢数量规模大、智能程度高(技术含量)、组织方式多样化(个人、组织和集团等)
➢危害程度严重
➔ 信息安全成为国家安全的重要内容
➢国家领导层的意识
➢信息化建设的经验、伊战的教训
➔ 关键技术受制于人
➢CPU、OS
➢专利、标准
信息安全威胁
信息安全威胁的表现:
➔ 网络协议的弱点
➔ 网络操作系统的漏洞
➔ 应用系统设计的漏洞
➔ 网络系统设计的缺陷
➔ 恶意攻击
➔ 来自合法用户的攻击
➔ 互联网的开放性
➔ 物理安全
➔ 管理安全
威胁的根源
➔ 信息系统的复杂性:
➢系统软硬件缺陷,网络协议的缺陷
➔ 信息系统的开放性:
➢系统开放:计算机及计算机通信系统是根据行业标准规定的接口建立起来的。
➢标准开放:网络运行的各层协议是开放的,并且标准的制定也是开放的。
➢业务开放:用户可以根据需要开发新的业务。
问题的原因和根源
软件问题的原因
⚫Connectivity(互联性)
- 终端设备的互联给了攻击者有更多的机会
- 攻击者利用系统弱点不再需要进行物理访问
- 平台设计缺陷
⚫Extensibility(扩展性)
- 目前很多软件技术都支持扩展技术
- 优点在于可以很方便的加载以及扩展新的功能与组件
- 问题在于扩展的功能与组件不可控,会带来未知的安全风险
⚫Complexity(复杂性)
- 软件系统代码增长速度惊人
系统越复杂,Bug越难避免,质量越难保证,可能发生的安全风险越多。
新的需求与传统安全技术的不足
传统网络安全技术只能对网络层进行防护
无法分辨与正常应用数据混杂在一起的攻击
➔XSS
➔SQL注入
➔未校验参数
➔etc.
传统的安全模式 | 新的安全模式 |
---|---|
保护”边界” | 构建安全的系统 |
网络安全 | 设计安全的软件 |
安全负责的人是 IT/MIS/CISSP等部门 | 软件开发人员和设计人员对安全负责 |
被动式 | 主动式 |
缓冲区溢出基础
缓冲区溢出概述-缓冲区溢出原理简介
缓冲区溢出是指当计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量溢出的数据覆盖在合法数据上。
在初始化、拷贝或移动数据时,C语言并不自动地支持内在的数组边界检查。
目前对于缓冲区溢出,主要分为静态保护和动态保护:
⚫**静态保护:**不执行代码,通过静态分析来发现代码中可能存在的漏洞.静态的保护技术包括编译时加入限制条件,返回地址保护,二进制改写技术,基于源码的代码审计等.
⚫**动态保护:**通过执行代码分析程序的特性,测试是否存在漏洞,或者是保护主机上运行的程序来防止来自外部的缓冲区溢出攻击
PE文件格式简介
PE(Portable Executable)是Win32平台下可执行文件遵守的数据格式。常见的可执行文件(如“*.exe”文件和“*.dll”文件)都是典型的PE文件。
PE 文 件 格 式 把 可 执 行 文 件 分 成 若 干 个 数 据 节(section),不通的资源被存放在不同的节中。一个典型的PE文件包含的节如下:
➔ .text 由编译器产生,存放着二进制的机器代码,也是我们反汇编和调试的对象。
➔ .data 初始化的数据块,如宏定义、全局变量、静态变量等。
➔ .idata 可执行文件所使用的动态链接库等外来函数与文件的信息。
➔ .rsrc 存放程序的资源,如图标、菜单等。
➔ 除此之外 , 还 可 能 出 现 的 节 包 括 “.reloc” 、“.edata”、 “.tls”、 “.rdata”等。
PE文件简单构成
其他 |
---|
.data节 |
.rdata节 |
.text节 |
文件头、节 |
表等 |
虚拟内存相关知识—虚拟内存简介
Windows的内存可以被分为两个层面:物理内存和虚拟内存 。 其 中 , 物理内存比较复杂 , 需要进入Windows内核级别ring0才能看到。通常,在用户模式下,我们用调试器看到的地址都是虚拟内存。
虽然每个进程都“相信”自己拥有4GB的空间,但实际上它们运行时真正能用到的空间根本没有那么多。
内存管理器只是分给进程一片“假地址”,或者说是“虚拟地址”,它们对进程来说只是一笔“无形的数字财富”;
当需要实际的内存操作时,内存管理器才会把“虚拟地址”和“物理地址”联系起来。
PE文件与虚拟内存之间的映射
静态反汇编工具看到的PE文件中某条指令的位置是相对于磁盘文件而言的,即所谓的文件偏移,我们可能还需要知道这条指令在内存中所处的位置,即虚拟内存的位置
反之,在调试时看到的某条指令的地址是虚拟内存地址,我们也经常需要回到PE文件中找到这条指令对应的机器码
概念
⚫ 文件偏移地址(File Offset)
➢数据在PE文件中的地址叫做文件偏移地址。这是文件在磁盘上存放时相对于文件开头的偏移。
⚫ 装载基址(Image Base)
➢PE装入内存时的基地址。默认情况下,EXE文件在内存中的基地址是0x00400000,DLL文件是0x10000000。这些位置可以通过修改编译选项更改。
⚫ 虚拟内存地址(Virtual Address,VA)
➢PE文件中的指令被装入内存后的地址。
⚫ 相对虚拟地址(Relative Virtual Address,RVA)
➢相对虚拟地址是内存地址相对于映射基址的偏移量。
系统栈的工作原理—内存的不同用途
成功地利用缓冲区溢出漏洞可以修改内存中的变量的值,甚至可以劫持进程,执行恶意代码,最终获得主机的控制权。
寄存器
⚫ 通用寄存器
➔ 32位的通用寄存器有EAX、EBX、ECX、EDX、ESP、EBP、ESI和EDL,它们的使用方法不总是相同的。一些指令赋予它们特殊的功能。
⚫ 段寄存器
➔ 段寄存器被用于指向进程地址空间不同的段。
➔ CS指向一个代码段的开始;
➔ SS是一个堆栈段;
➔ DS、ES、FS、GS和各种其他数据段,例如存储静态数据的段。
⚫ 程序流控制寄存器
⚫ 其他寄存器
程序员习惯中已经默认的给每个寄存器赋上了特殊的含义,比如:
➔ EAX一般用来做返回值
➔ ECX用于记数
➔ EIP:扩展指令指针。在调用一个函数时,这个指针被存储在堆栈中,用于后面的使用。在函数返回时,这个被存储的地址被用于决定下一个将被执行的指令的地址。
➔ ESP:扩展堆栈指针。这个寄存器指向堆栈的当前位置,并允许通过使用push和pop操作或者直接的指针操作来对堆栈中的内容进行添加和移除。
➔ EBP:扩展基指针。主要用与存放在进入call以后的ESP的值,便于退出的时候回复ESP的值,达到堆栈平衡的目的
内存的4个部分和相关用途:
名称 | 用途 |
---|---|
代码区 | 这个区域存储着被装入执行的二进制机器代码,处理器会到这个区域取指并执行。 |
数据区 | 用于存储全局变量等。 |
堆区 | 进程可以在堆区动态地请求一定大小的内存,并在用完之后归还给堆区。动态分配和回收是堆区的特点。 |
栈区 | 用于动态地存储函数之间的调用关系,以保证被调用函数在返回时恢复到父函数中继续执行。 |
栈与系统栈
从计算机科学的角度来看,栈指的是一种数据结构,是一种先进后出的数据表。栈的最常见操作有两种:压栈(PUSH)、弹栈(POP);用于标识栈的属性也有两个:栈顶(TOP)、栈底(BASE)。
内存的栈区实际上指的就是系统栈。系统栈由系统自动维护,它用于实现高级语言中函数的调用。对 于 类 似 C 语 言 这 样 的 高 级 语 言 , 系 统 栈 的PUSH/POP等堆栈平衡细节是透明的。
代码空间 | 系统栈空间 |
---|---|
程序被装入,由main函数代码空间依次取指执行 | 系统栈 栈顶为当前正在执行的main函数栈帧 |
执行到main代码区的call指令时,跳转到funca的代码区继续执行 | 为配合funca的执行,在系统栈中为其开辟新的栈帧并压入 |
执行到funca代码区的call指令时,跳转到funcb的代码区继续执行 | |
为配合funcb的执行,在系统栈中为其开辟新的栈帧并压入 | |
funcb代码执行完毕,弹出自己的栈帧并从中获得返回地址,跳回funca代码区继续执行 | 弹出funcb的栈帧。对应于当前正在执行的函数,当前栈顶栈帧重新恢复成funca函数栈帧 |
funca代码执行完毕,弹出自己的栈帧并从中获得返回地址,跳回main代码区继续执行 | 弹出funca的栈帧。对应于当前正在执行的函数,当前栈顶栈帧重新恢复成main函数栈帧 |
寄存器与函数栈帧
(1)ESP:栈指针寄存器(extended stackpointer),其内存放着一个指针,该指针永远指向系统栈最上面的一个栈帧的栈顶。
(2)EBP:基址指针寄存器(extended basepointer),其内存放着一个指针,该指针永远指向系统栈最上面的一个栈帧的底部。
(3)EIP :指令寄存器 (Extended InstructionPointer),其内存放着一个指针,该指针永远指向一条等待执行的指令地址。
在函数栈帧中,一般包含以下几类重要信息。
(1)局部变量:为函数局部变量开辟的内存空间。
(2)栈帧状态值:保存前栈帧的顶部和底部(实际上只保存前栈帧的底部,前栈帧的顶部可以通过堆栈平衡计算得到),用于在本帧被弹出后恢复出上一个栈帧。
(3)函数返回地址:保存当前函数调用前的“断点”信息,也就是函数调用前的指令位置,以便在函数返回时能够恢复到函数被调用前的代码区中继续执行指令
函数调用步骤
(1)参数入栈:将参数从右向左一次压入系统栈中。
(2)返回地址入栈:将当前代码区调用指令的下一跳指令地址压入栈中,
供函数返回时继续执行。
(3)代码区跳转:处理器从当前代码区跳转到被调用函数的入口处。
(4)栈帧调整
- 保存当前栈帧的状态值,以备后面恢复本栈帧时使用(EBP入栈);
- 将当前栈帧切换到新栈帧(将ESP值装入EBP,更新栈帧底部);
- 给新栈帧分配空间(把ESP减去所需空间的大小,抬高栈帧);
函数调用约定与相关指令
入栈
序列 | 备注 |
---|---|
push 参数 3 | 假设该函数有3个参数,将从右向左依次入栈 |
push 参数 2 | |
push 参数 1 | |
call 函数地址 | call指令将同时完成两项工作:a)向栈中压入当前指令在内存中的位置,即保存返回地址。 |
b)跳转到所调用函数的入口地址函数入口处 | |
push ebp | 保存旧栈帧的底部 |
mov ebp,esp | 设置新栈帧的底部(栈帧切换) |
sub esp,xxx | sub esp,xxx 设置新栈帧的顶部(抬高栈顶,为新栈帧开辟空间) |
类似的,函数返回的步骤如下。
(1)保存返回值:通常将函数的返回值保存在寄存器EAX中。
(2)弹出当前栈帧,恢复上一个栈帧,具体包括:
在堆栈平衡的基础上,给ESP加上栈帧的大小,降低栈顶,回收当前栈帧的空间。
将当前栈帧底部保存的前栈帧EBP值弹入EBP寄存器,恢复上一个栈帧。
将函数返回地址弹给EIP寄存器。
(3)跳转:按照函数返回地址跳回母函数中继续执行。
指令 | 备注 |
---|---|
add esp,xxx | 降低栈顶,回收当前的栈帧 |
pop ebp | 将上一个栈帧底部位置恢复到ebp |
retn | 这条指令有两个功能: |
a)弹出当前栈顶元素,即弹出栈帧中的返回地址。至此,栈帧恢复工作完成。 | |
b)让处理器跳转到弹出的返回地址,恢复调用前的代码区 |
第三讲 字符串安全
C-风格的字符串
c风格的字符串由一个连续的字符序列组成,并以一个空字符(null )作为结束。
➔ 一个指向字符串的指针实际上就是指向该字符串的起始字符。
➔ 字符串长度指空字符之前的字节数
➔ 字符串的值则是它所包含的按顺序排列的字符序列。
➔ 存储一个字符串所需要的字节数是字符串的字符数加1。 (x 是每个字符的大小)
C++ 字符串
C++的标准化促进了:
➔ 标准的类模板std::basic_string
➔ 及它的char 实例化 std::string
➔ 相对于C风格的字符串,basic_string 类更不容易出现安全漏洞 。
常见的字符串操作错误
最常见的错误有
➢ 无界字符串复制
➢ 空结尾错误
➢ 截断
➢ 差-错误
➢ 数组写入越界
➢ 不恰当的数据处理
无边界字符串复制
无边界字符串复制发生于从一个无边界数据源复制数据到一个定长的字符数组时
1 | int main(void) { |
复制和连接字符串时也容易出现错误,因为标准strcpy() 和strcat() 函数执行的都是无边界复制操作
1 | 1. int main(int argc, char *argv[]) { |
简单的解决方案
利用strlen() 测试输入字符串的长度然后动态分配内存。
1 | 1. int main(int argc, char *argv[]) { |
C++无界字符串复制
对于下列的C++程序,如果用户输入多于11个字符,也会导
致越界写。
1 | 1. |
简单的解决方案
1 | 1. |
空结尾错误
当使用C风格字符时,另一个常见的问题是字符串末尾没有正确的空字符。
1 | int main(int argc, char* argv[]) { |
strncpy 函数
1 | char *strncpy(char * restrict s1, |
从数组S2中复制不超过 n 个字符串 (空字符后的字符不会被复制) 到目标数组S1 *****中。
➔ *因此,如果第一个数组S2中的前n个字符中不存在空字符,那么其结果字符串将不会是以空字符结尾的。
字符串截断
⚫一些限制字节数的函数通常用来防止缓冲区溢出漏洞
➔ strncpy() 代替 strcpy()
➔ fgets()代替 gets()
➔ snprintf()代替 sprintf()
⚫当目标字符数组的长度不足以容纳一个字符串的内容时,就会发生字符串截断
⚫字符串截断会丢失数据,有时也会导致软件漏洞
数组写入越界
1 | 1. int main(int argc, char *argv[]) { |
程序栈
⚫栈( 栈 )通过存储下列内容来追踪程序的执行和状态。
➢ 调用函数的返回地址
➢ 函数参数
➢ 局部 (临时)变量
⚫在下列情况下栈需要被修改
➢ 在函数调用期间
➢ 函数初始化期间
➢ 从子例程返回时
栈粉碎
⚫堆栈支持嵌套调用
⚫帧指由函数调用引发的压入栈的数据。
⚫栈用于存储
➔ 调用函数的返回地址
➔ 子例程的实际参数
➔ 局部(自动)变量
⚫当前帧的地址被存储到帧或者基址寄存器中(英特尔架构中的EBP)
⚫帧指针在栈中是一个定点的引用。
⚫下列情况出现时,栈要要被修改
➔ 子例程调用
➔ 子例程初始化
➔ 从子例程返回
EIP:扩展指令指针 ESP: 扩展栈指针 EBP:扩展基指针
缓冲区溢出
当向为某特定数据结构分配的内存空间边界之外写入数据时, 就会发生缓冲区溢出。
➔当缓冲区溢出覆写分配给执行栈内存中的数据时,就会导致栈粉碎
➔成功的利用这个漏洞能够覆写栈返回地址,从而在目标机器中执行任意代码。
代码注入
⚫攻击者创建一个恶意参数
➔一个蓄意构造的字符串,其中包含一个指向某些恶意代码的指针,该代码也由攻击者提供。
⚫当函数返回时,控制就被转移到了那段恶意代码。
➔注入的代码就会以与该有漏洞的程序相同的权限运行
➔攻击者通常都以“以root或其他较高权限运行”的程序为目标
⚫恶意参数的目的是把控制权转移给恶意代码
➔ 可能包含在恶意参数 (如本实例)中
➔ 可能在一个有效的输入操作期间注入恶意代码
➔ 恶意代码可以执行以其他任何形式编程所能执行的功能,
不过它们通常只是简单地在受害机器上开一个远程。
弧注入 (return-into-libc)
弧注入将控制转移到已经存在于程序内存空间中的代码中
➔ 弧注入的利用方式是在程序的控制流“团”中插入一段新的“弧’(表示控制流转移),而不是进行代码注入。
➔ 可以安装一个已有函数的地址(如system() 或exec (),用于执行已存在于本地系统上的程序
➔ 更复杂的攻击可能会使用这种技术
缓解措施
⚫缓解措施包括:
➔预防缓冲区溢出
➔侦测缓冲区溢出并安全地恢复,使得漏洞利用的企图无法得逞。
⚫防范策略
➔静态分配空间
➔动态分配空间
静态方法 静态分配缓冲区
假设一个固定大小的缓冲区
➔在缓冲区满了以后,不可能添加再数据
➔因为静态的方法丢弃了超出的数据,所以实际的程序数据会丢失。
➔因此,生成的字符串必须被充分验证
输入验证
1 | 1. int myfunc(const char *arg) { |
strlcpy() 和 strlcat()
采用一个更不容易出错的方式来复制和链接
1 | size_t strlcpy(char *dst,const char *src, size_t size); |
strlcpy() 从src复制空结尾的字符串到dst (直到size 大小的字符)。
strlcat() 函数把非空结尾的字符串src 连接到dst末尾 (不超过size 的字符都能够连接到dst末尾)
两个函数都确保目标字符串对所有非零长度的缓冲区来说都是
非空结尾的。
ISO/IEC “Security” TR 24731
➔strcpy_s() 代替strcpy()
➔strcat_s()代替strcat()
➔strncpy_s()代替strncpy()
➔strncat_s()代替strncat()
strcpy_s() 函数
把字符从源字符串复制到目标字符数组,直到并包括终止null字
符。
1 | errno_t strcpy_s(char * restrict s1,rsize_t s1max,const char * restrict s2); |
动态方法 动态地分配缓冲区
⚫动态分配的缓冲区需要动态调整额外的内存。
⚫动态方法更好,而且不丢弃多余的数据。
⚫主要缺点是,如果输入被限制,则可能
➔耗尽机器内存
➔结果导致拒绝服务攻击
防范策略
为C提供丰富的字符串操作库,
➔ 拥有安全的语义
➔ 与遗留的库代码互操作
➔ 使用一种动态分配的方式,可以在需要时自动调整字符串的大小。
safestr_t 类型
safestr_t类型与char是兼容的,并且可以将safestr_t转型为char当作
C风格字符使用。
safestr_t类型保存了由该指针所引用的内存部分的说明信息(例如,
实际长度和分配的长度),保存子指针所指向的内存之前
⚫管理动态字符串
➔ 分配缓冲区
➔ 如果需要额外的内存,则重新调整内存大小
⚫管理字符串操作,以确保
➔ 字符串操作没有导致缓冲区溢出
➔ 数据没有丢失
➔ 字符串正常终止(字符串可能是也可能不是内部空结尾)
⚫缺点
➔ 无限制地消耗内存,可能导致拒绝服务攻击
➔ 性能开销
第四讲 软件安全漏洞基础
软件漏洞简介
在形形色色的软件逻辑缺陷中,有一部分如果被利用能够引起非常严重的后果。
例如:SQL注入,跨站脚本,缓冲区溢出
我们通常把这类能够引起软件做一些“超出设计范围的事情”的bug称为漏洞
含义 | 例子 | |
---|---|---|
BUG | 功能性逻辑缺陷,影响软件的正常功能 | 执行结果错误、图标显示错误等 |
漏洞 | 安全性逻辑缺陷,通常情况下不影响软件的正常功能,但被攻击者成功利用后,有可能引起软件去执行额外的恶意代码 | 软件缓冲区溢出漏洞、网站的跨站脚本漏洞(XSS)、SQL注入漏洞等 |
典型的软件漏洞
缓冲区溢出漏洞:它发生的原理是,由于用户处理用户数据时使用了不限边界的拷贝,导致程序内部一些关键数据被覆盖,引发了安全问题,严重的缓冲区溢出漏洞会使得程序被利用而安装上木马或病毒
整数溢出漏洞:对于有符号的16位整数来说,它的最大值就是0x7fff,也就是十进制的32767,当赋值给一个整数的值为其最大值时,如果此时再加上1,就会发生整数型的溢出。
格式化字符串漏洞:格式化字符串漏洞是由于程序的数据输出函数中对输出数据的格式解析不当而发生的。
SQL注入漏洞:通过控制传递给软件数据库操作语句 的关键变量来获得恶意控制软件数据库、获取有用信息或者制造恶意破坏的,甚至是控制用户计算机系统的漏洞,就成为”SQL注入漏洞”。
软件漏洞的危害
无法正常使用,引发恶性事件,关键数据丢失,秘密信息泄漏,被安装木马病毒
安全漏洞出现的原因
小作坊式的软件开发、赶进度带来的弊端、被轻视的软件安全测试、淡薄的安全思想、不完善的安全维护、
漏洞挖掘、分析、利用简介
⚫从技术角度讲,漏洞挖掘实际上是一种高级的测试(QA)。
⚫学术界一直热衷于使用静态分析的方法寻找源代码中的漏洞;
⚫而在工程界,不管是安全专家还是攻击者,普遍采用的漏洞挖掘方法是Fuzz,这实际是一种“灰”盒测试。
⚫当fuzz捕捉到软件一个严重的异常时,当你想透过厂商公布的简单描述了解漏洞细节的时候,我们就需要具备一定的漏洞分析能力。一般情况下,我们需要调试二进制级别的程序。
PE文件格式简介
PE(Portable Executable)是Win32平台下可执行文件遵守的数据格式。常见的可执行文件(如“*.exe”文件和“*.dll”文件)都是典型的PE文件。
PE 文 件 格 式 把 可 执 行 文 件 分 成 若 干 个 数 据 节(section),不通的资源被存放在不同的节中。一个典型的PE文件包含的节如下:
DOS头 |
---|
PE签名 |
PE头文件 |
PE可选头 |
节表 |
.idata节 |
.text节 |
.data节 |
虚拟内存相关知识
PE文件与虚拟内存之间的映射
⚫ 文件偏移地址(File Offset)
➢数据在PE文件中的地址叫做文件偏移地址。这是文件在磁盘上存放时相对于文件开头的偏移。
⚫ 装载基址(Image Base)
➢PE装入内存时的基地址。默认情况下,EXE文件在内存中的基地址是0x00400000,DLL文件是0x10000000。这些位置可以通过修改编译选项更改
⚫ 虚拟内存地址(Virtual Address,VA)
➢PE文件中的指令被装入内存后的地址。
⚫ 相对虚拟地址(Relative Virtual Address,RVA)
➢相对虚拟地址是内存地址相对于映射基址的偏移量。
➔ 虚拟内存地址、映射基址、相对虚拟内存地址三者有如下关系 VA=Image Base + RVA
第五讲 漏洞利用技术
shellcode概述
淹没返回地址
buffer[0-3]
buffer[4-7]
authenticated 左侧是变量的 排列顺序图
EBP
返回地址
观察一下我们发现我们只需要先放置16个字符串,然后接下来
的4个字节就能够淹没返回地址,达到控制返回地址的目的
栈帧移位与jmp esp
一般情况下,ESP寄存器中的地址总是指向系统栈中,且不会被溢出的数据破坏。函数返回时,ESP所指的位置恰好是我们所淹没的返回地址的下一个位置
获取“跳板”的地址
在利用跳板之前,我们必须在进程空间中找到一条jmp esp指令的地址作为“跳板”。除了PE文件的代码被读入内存空间,一些经常被用到的动态链接库也将会一同被映射到内存 。 其 中 , 注 入kernel32.dll,user32.dll之类的动态链接库几乎会被所有的进程加载,且加载基址始终相同。
缓冲区的组织
填充物:可以是任何值,但是一般用NOP指令对应的0x90来进行填充,这样只要能跳进填充区,处理器最终也能顺序执行到shellcode。
淹没返回地址的数据:可以是跳转指令的地址,shellcode的起始地址,或者近似的shellcode地址(跳转进NOP填充区)
shellcode:可执行的机器代码。
第六讲 漏洞挖掘与模糊测试
漏洞挖掘技术简介
工业界目前普遍采用的是进行Fuzz测试,与基于功能性的测试有所不同,Fuzz的主要目的是”崩溃crash”,”中断break”,”销毁destroy”。
Fuzz的测试用例往往是带有攻击性的畸形数据,用以触发各种类型的漏洞。
我们可以把Fuzz理解成为一种能自动进行”rough attack”尝试的工具。之所以说它是”rough attack”,是因为Fuzz往往可以触发一个缓冲区溢出的漏洞,但却不能实现有效的exploit。
测试人员需要实时地捕捉目标程序抛出的异常、发生的崩溃和寄存器等信息,综合判断这些错误是不是真正的可利用漏洞
动态测试技术与静态代码审计的对比
动态测试技术 | 静态代码审计 | |
---|---|---|
主要技术 | Fuzz等黑盒测试 | 数据流分析、类型验证系统、边界检验系统、状态机系统等 |
主要应用人群 | 攻击者 | 学者、QA工程师 |
优点 | 快捷,准确 | 检测出的漏洞数量多 |
缺点 | 不能发现系统里全部(或者大部分)漏洞 | 实现静态代码分析工具相当困难 |
Fuzz
Fuzz的测试用例往往是带有攻击性的畸形数据,用以触发各种类型的漏洞。
我们可以把Fuzz理解成为一种能自动进行”rough attack”尝试的工具。之所以说它是”rough attack”,是因为Fuzz往往可以触发一个缓冲区溢出的漏洞,但却不能实现有效的exploit。
Fuzz的测试优点是很少出现误报,能够迅速地找到真正的漏洞;缺点是Fuzz永远不能保证系统里已经没漏洞。
文件类型漏洞挖掘—文件格式Fuzz的基本方法
不管IE还是OFFICE,它们都有一个共同点,那就是用文件作为程序的主要输入。从本质上来说,这些软件都是按照事先约定好的数据结构对文件中不同的数据域进行解析,以决定用什么颜色、在什么位置显示这些数据。
假如我们习惯了这样的思维,也就是假设他们所使用的文件是严格遵守软件规定的数据格式的。因为用Word生成的doc文件一般不会存在什么非法数据。
文件格式Fuzz就是利用这种“畸形文件”测试软件的方法。我们可以在网上找到很多FileFuzz的工具。
一个File Fuzz工具通常包括以下几个步骤:
(1)以一个正常的文件模板作为基础,按照一定规则产生一批畸形文件。
(2)将畸形文件逐一送入软件进行解析,并监视软件是否会抛出异常
(3)记录软件产生的错误信息,如寄存器状态、栈状态等。
(4)用日志或其他UI形式向测试人员展示异常信息,以进一步鉴定这些错误是否能被利用
Blind Fuzz即通常所说的“盲测”,就是在随机位置插入随机的数据以生成畸形文件。然而现代软件往往使用非常复杂的私有数据结构。
针对Blind Fuzz的不足,Smart Fuzz被越来越多地提出和应用。通常Smart Fuzz包括三方面的特征:
面向逻辑(Logic Oriented Fuzzing)
面向数据类型(Data Type Oriented Fuzzing)
基于样本(Sample Based Fuzzing)
面向数据类型测试:测试中可以生成的数据通常包括以下几种类型。
1)算术型:包括以HEX、ASCII、Unicode、Raw格式存在的各种数值。
2)指针型:包括Null指针、合法/非法的内存指针等。
3)字符串型:包括超长字符串、缺少终止符(0x00)的字符串等。
4)特殊字符:包括#,@,’,<,>,/,,../等等。
第七讲 指针安全
缓冲区溢出覆写指针条件:
➔ 缓冲区与目标指针必须分配在同一个段内
➔ 缓冲区必须位于比目标指针更低的内存地址处
➔ 该缓冲区必须是界限不充分的,因此容易被缓冲区溢出利用
虚函数攻击
多态是面向对象的一个重要特性,在C++中,这个特性主要靠对虚函数的动态调用来实现。
在仅仅关注漏洞利用的前提下,我们可以简单地把虚函数和虚表理解为一下几个要点。
1)C++类的成员函数在声明时,若使用关键字virtual进行修饰,则被称为虚函数
2)一个类中可能有很多个虚函数。
3)虚函数的入口地址被统一保存在虚表(Vtable)中。
4)对象在使用虚函数时,先通过虚表指针找到虚表,然后从虚表中取出最终的函数入口地址进行调用。
5)虚表指针保存在对象的内存空间中,紧接着虚表指针的是其他成员变量。
6)虚函数只有通过对象指针的引用才能显示出其动态调用的特性
SEH攻击
1)S.E.H结构体存放在系统栈中。
2)当线程初始化时,会自动向栈中安装一个S.E.H,作为线程默认的异常处理。
3)如果程序源代码中使用了__try{}__except{}或者Assert宏等异常处理机制,编译器将最终通过向当前函数栈帧中安装一个S.E.H来实现异常处理。
4)栈中一般会同时存在多个S.E.H
5)栈中的多个S.E.H通过链表指针在栈内由栈顶向栈底串成单向链表,位于链表最顶端的S.E.H通过T.E.B(线程环境块)0字节偏移处的指针标识。
6)当异常发生时,操作系统会中断程序,并首先从T.E.B的0字节偏移处取出距离栈顶最近的S.E.H,使用异常处理函数句柄所指向的代码来处理异常。
7)当离“事故现场”最近的异常处理函数运行失败时,将顺着S.E.H链表依次尝试其他的异常处理函数。
8)如果程序安装的所有异常处理函数都不能处理,系统将采用默认的异常处理函数。通常,这个函数会弹出一个对话框,然后强制关闭程序
首先,S.E.H存放在栈内,我们学过的栈溢出漏洞可能会派上用场。
其次,我们可以精心制造出溢出数据来把S.E.H的异常处理的函数地址替换为shellcode的起始地址。
再次,溢出后错误的栈帧或堆块数据往往会触发异常,或者我们能查到哪些能触发异常的片段。
最后,当Windows开始处理溢出后的异常时,会把hellcode当作异常函数来处理异常。
第八讲 格式化输出
格式化输出函数参数由一个格式字符串和可变数目的参数构成
➔格式化字符串提供了一组可以由格式化输出函数解释执行的指令
➔用户可以通过控制格式字符串的内容来控制格式化输出函数的执行
变参函数在C语言中实现的局限性导致格式化输出函数的使用中容易产生漏洞!