软件系统安全
课程介绍
软件安全的重要性
软件的定义
–1983年IEEE为软件下的定义是:计算机程序、方法、规则和相关的文档资料以及在计算机上运行时所需的数据
–通俗解释:软件 = 程序 + 数据 + 文档资料
•程序是完成特定功能和满足性能要求的指令序列
•数据是程序运行的基础和操作的对象
文档与程序开发、维护和使用有关的图文资料
软件的分类
按软件的功能分类
docker import filename.tar - imagename:versionpowershell
§为计算机使用提供最基本的功能
§Windows、Linux、Android、IOS
•应用软件
§为了某种特定的用途而被开发的软件
§Ms Office、QQ、迅雷
•支撑软件
§支持软件的开发、维护与运行的软件
§编译器,数据库管理系统,软件开发工具 (Visual Studio)
按软件运行平台划分
–服务器端软件
–PC端软件
–手机应用软件
嵌入式软件
软件安全的概念
*什么是安全
安全是指不受威胁,没有危险,不受危害,不受损失的一种可接受状态。
*软件安全:
–提供表示、分析和追踪具有危害性功能的软件缓解措施与控制的系统性方法
–采取工程的方法使得软件受到恶意攻击的情形下依然能够继续正确运行
*软件安全问题的根源是软件存在弱点
•软件存在弱点
•现有方法并不能解决软件安全问题
–反病毒程序和防火墙之类的保护程序
–密码学之类的信息加密技术
*软件的安全属性(CIA属性):
保密性、完整性、可用性、可认证性、授权、可审计性、抗抵赖性、可控性、可存活性
软件缺陷(Defect),常常又被称作Bug
§指计算机软件或程序中存在的某种破坏正常运行能力的问题、错误、或者隐蔽的功能缺陷。
*软件漏洞
•软件漏洞(Vulnerability)是指软件在设计、实现、配置策略及使用过程中出现的缺陷,其可能导致攻击者能够在未授权的情况下访问或破坏系统。
恶意软件
•恶意软件是对非用户期望运行的、怀有恶意目的或完成恶意功能的软件的统称
恶意软件的恶意行为:
篡改或破坏已有的软件功能
窃取重要数据
监视用户行为
控制目标系统
•典型恶意软件种类
计算机病毒、蠕虫、特洛伊木马、后门、僵尸、间谍软件等
软件侵权
•软件版权是指软件作者对其创作的作品享有的人身权和财产权
软件逆向工程基础
*逆向工程定义
一种分析目标系统的过程, 其目的是识别出系统的各个组件以及它们之间的关系, 并在较高的抽象层次上以另一形式创建对系统的表示[1], 从而理解目标系统的结构和行为。
*软件逆向工程的基本方法
逆向工程的主要活动: 反汇编 + 分析 + 可能的修改(破解)

•静态方法
–分析但不运行代码
–较动态方法更为安全
–反汇编器,如IDA Pro,objdump等
•动态方法
–检查进程执行过程中寄存器、内存值的实时变化
–允许操作进程,通常应在虚拟机中运行
–调试器,如Windows下的WinDBG, Immunity, OllyDBG等,及Linux下的GDB
软件逆向工程的典型应用场景
•恶意代码分析
•闭源软件漏洞分析
•闭源软件互操作性分析
验证编译器的性能和准确性;调试器中生成汇编代码清单
*GDB调试常用命令

*字节序
字节序:多字节数据在内存中存储或在网络上传输时各字节的存储/传输顺序
–小端序(little endian):低位字节存储在内存中低位地址,效率较高(Intel CPU使用)
–大端序(big endian):低位字节存储在内存中高位地址,符合思维逻辑。RISC架构处理器(如MIPS, PowerPC)采用
通用寄存器(GPR)
•EBP: 栈内数据指针, 栈帧的基地址, 用于为函数调用创建栈帧
•ESP: 栈指针, 栈区域的栈顶地址
汇编指令格式
•AT&T: source在destination前, 在较早期的GNU工具中普遍使用(如gcc,gdb等)
•Intel : destination在source前,“[… ]”含义类似于解引用,MASM,NASM等工具中使用

函数调用与内存破坏漏洞
进程地址空间

栈stack: 由系统自动分配。 void func() {int a;} 系统自动在栈中为 a 开辟空间
堆heap: 需要程序员自己申请,并指明大小 (例如 malloc)。
*栈
在IA-32架构下, 栈是连续的内存区域, 存在于一个栈段内,
–任意时刻, ESP寄存器所包含的栈指针均指向栈顶位置
–通常由高地址向低地址扩展(PUSH时ESP自减,POP时ESP自增)
*栈帧
将调用函数和被调用函数联系起来的机制, 栈被分割为栈帧,栈帧组成栈。栈帧的内容包含
–函数的局部变量
–向被调用函数传递的参数
–函数调用的联系信息(栈帧相关的指针: 栈帧基址针,返回指令指针)
•栈帧基指针(在EBP中)
–被调用函数栈帧的固定参考点

•esp寄存器始终指向栈的顶部。
•ebp寄存器指向了函数活动记录的一个固定位置
*x86函数调用的步骤
•参数入栈:将参数从右向左依次压入系统栈中。
•返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行。
•代码区跳转:处理器从当前代码区跳转到被调用函数的入口处。
•栈帧调整:具体包括。
–保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP 入栈)
–将当前栈帧切换到新栈帧(将 ESP 值装入 EBP,更新栈帧底部)
–给新栈帧分配空间(把 ESP 减去所需空间的大小,抬高栈顶);
*函数返回的步骤
•保存返回值:通常将函数的返回值保存在寄存器 EAX 中
•弹出当前栈帧,恢复上一个栈帧
–在堆栈平衡的基础上,给 ESP 加上栈帧的大小,降低栈顶,回收当前栈帧的空间
–将当前栈帧底部保存的前栈帧 EBP 值弹入 EBP 寄存器,恢复出上一个栈帧
–将函数返回地址弹给 EIP 寄存器。
跳转:按照函数返回地址跳回母函数中继续执行

*x64函数调用约定
•x64的多数调用惯例更多地通过寄存器传递参数
•Windows x64中, 只有一种调用惯例用到栈,且其中前4个参数通过RCX, RDX, R8, R9 传递
•Linux x64中, System V AMD64 ABI调用惯例: 前6个参数通过RDI, RSI, RDX, RCX, R8, R9传递
•相比IA-32, 减小了在栈上存储和查找值的时间, 一些函数根本不需要访问栈
| Cdecl | Syscall | StdCall | |
|---|---|---|---|
| 参数入栈顺序 | 右->左 | 右->左 | 右->左 |
| 恢复栈平衡的位置 | 母函数 | 子函数 | 子函数 |
*缓冲区溢出
–指数据写出到为特定数据结构开辟的内存空间的边界之外
–通常可能发生在缓冲区边界被忽略或没有被检查时
–缓冲区溢出可被利用于修改
•栈上的返回指令指针,函数指针局部变量. . .
堆数据结构
*缓冲区溢出原因:
- 输入验证不足:当程序没有对输入进行充分的验证和边界检查时,攻击者可以通过输入超过缓冲区容量的数据来触发溢出。例如,如果程序接受用户输入并将其存储在固定大小的缓冲区中,但没有检查输入的长度,那么当用户输入超过缓冲区容量时,就会发生溢出。
- 缓冲区操作错误:在程序中使用不安全的缓冲区操作函数(如C/C++中的
strcpy、strcat等)时,如果目标缓冲区的容量不足以存储要复制或连接的数据,就会导致溢出。这些函数没有对目标缓冲区的长度进行检查,容易导致溢出。 - 格式化字符串漏洞:当程序使用格式化字符串函数(如
printf、sprintf等)时,如果格式化字符串中包含了过多的参数,而实际提供的参数数量不足,就可能导致溢出。攻击者可以通过精心构造的格式化字符串来修改程序的内存内容,甚至执行恶意代码。
代码注入攻击与格式化字符串
*代码注入
概念
•将ret设置为被注入代码的起始地址, 被注入代码可以做任何事情,如下载和安装蠕虫
•攻击者创建一个恶意的参数——一个精巧构造出的字符串,该字符串包含一个指向攻击者恶意代码的指针
•当被调用函数返回时, 程序控制流交给恶意代码 (shellcode)
–当函数返回时,被注入的代码以与漏洞程序相同的权限运行
–以root或其他较高权限运行的程序是攻击目标
shellcode 概述
•统一用 shellcode 这个专用术语来通称缓冲区溢出攻击中植入进程的代码
基本思想

步骤
•分析并调试漏洞程序,获得淹没返回地址的偏移。
•获得 buf 的起始地址,并将其写入 abc 的相应偏移处,用来冲刷返回地址。
• 向abc 中写入可执行的机器代码
| esp | ? | buf |
|---|---|---|
| ? | old_ebp (?) | |
| ? | ret addr | |
*使用“跳板”的溢出利用

基本原理
•用内存中任意一个 jmp esp 指令的地址覆盖函数返回地址,而不是原来用手工查出的shellcode (buf) 起始地址直接覆盖。
•函数返回后被重定向去执行内存中的这条jmp esp指令,而不是直接开始执行shellcode。
•由于 esp 在函数返回时仍指向栈区(函数返回地址之后),jmp esp 指令被执行后,处理器会到栈区函数返回地址之后的地方取指令执行。
•重新布置 shellcode。在淹没函数返回地址后,继续淹没一片栈空间。将缓冲区前边一段地方用任意数据填充,把 shellcode 恰好摆放在函数返回地址之后。这样,jmp esp 指令执行过后会恰好跳进 shellcode。
shellcode 摆放位置
•直接放在缓冲区 buf[80]里,
–shellcode 位于函数返回地址之前。
•使用跳转指令 jmp esp 来定位 shellcode
–shellcode 恰好在函数返回地址之后
| 好处 | 坏处 | 类型 | |
|---|---|---|---|
| shellcode 位于函数返回地址之前 | 对程序破坏小,比较稳定;不会大范围破坏前栈帧 | 自身可能被压栈数据破坏 | 使用静态地址定位shellcode |
| shellcode 位于函数返回地址之后 | 不用担心自身被压栈数据破坏 | 破坏前栈帧数据 | 使用“跳板”的溢出利用 |
*格式化字符串漏洞
格式化字符串函数介绍
格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数
| 参数 | 含义 | Passed as |
|---|---|---|
| %d | 十进制 | Value |
| %u | 无符号十进制 | Value |
| %x | 十六进制 | Value |
| %s | 字符串 | Reference |
| %n | 将已经输出过的字节数输入到指定的地址中 | Reference |
格式化字符串漏洞的原理
•对于printf函数,其要打印的内容及格式是由该函数的第一个参数确定的。如果第一个参数指定的格式与其后续参数匹配,则不会发生错误。
•然而如果指定的格式与其后续参数不匹配,则将会输出错误的结果,在某些情况下还会泄露内存变量的值。
•如果攻击者可以控制输入的字符串(含打印格式),则有可能利用该漏洞执行shellcode,从而入侵目标系统
格式化字符串漏洞的利用
使程序崩溃
•原理:
–**%s**将会把栈中的内容作为地址进行解析,读取对应地址的内容。如果栈中内容不是有效地址时,程序将会崩溃
读取栈上的数据
•从栈顶开始的第7个(4字节)单元开始保存变量int_input,A、B、C值。
•想读取某个内存单元的值,可以将int_input设置为内存地址,然后设置第7个格式化参数为%s
•读取栈顶0xffffcd4c (十进制数为4294954316)中的数据

修改栈内容
•格式字符**%n**
–把栈中的内容当作地址进行解析,并且把已经输出的字符的数量输入到对应的地址中
–printf(“hello%n”,&i)
–printf(“%d\n”,i) —— 输出i的值为5
•原理:将目标地址放入栈之后,利用%m.n的格式,通过设定宽度和精度,控制%n的计数值,计数值就等于目标单元的值
代码复用攻击
*代码复用攻击的基本概念
原因
•Stack不具有可执行属性?
–Data Execution Prevention (DEP)
–无法执行注入的恶意代码(shellcode)
•可能的解决办法
–缓冲区溢出
–覆写返回地址,使其指向某个函数的地址
–通常为系统调用的地址
概念
•攻击者利用程序本身已有的代码片段构造能够被自己利用的代码链
绕过系统提供的一些安全机制
–如Windows提供的数据执行保护(Data Execution Prevention, DEP)
–Linux提供的NX(Non-eXecute)
*Ret2libc
•在程序的控制流 (返回地址)被劫持后, 使其跳转到已在被攻击进程的地址空间中存在的代码中 (共享库中的函数中)
•共享库中有很多可以被攻击者利用的函数
e.g. system, execve, execl, etc

基本方法
•在共享库(libc.so)中找到感兴趣的函数的地址
•在栈上部署该函数的参数
•利用缓冲区溢出覆写返回地址使其指向该函数的地址

攻击步骤
1.获取函数system()和exit()的地址
2.获取字符串bin/sh的地址(难点)
3.为系统调用system() 创建形参
4.覆写返回地址
获取字符串 “/bin/sh”的地址(非重点,了解即可)
•方法1:将字符串“/bin/sh部署在栈中

•方法2:将“/bin/sh写入环境变量中

•方法3:在共享库libc.so中定位“/bin/sh”的地址
*Return-oriented Programming (ROP)
•面向返回编程
•链接代码片段(Gadgets)来执行恶意行为
•存在于不同的平台
–X86, ARM, SPARC
•被证明图灵完备
Gadget
•以分支指令结尾的代码片段
•ret, call , jmp
•ROP 攻击:以ret指令结尾
•指令数较少 (2-6条指令组成)
•Intented 指令序列和 Unintented 指令序列
Intented 指令序列
•正常反汇编得到的代码序列
•直接以ret指令作为结尾的代码序列
•主要存在于函数的末尾处
Unintented 指令序列
•不通过正常反汇编得到的以ret指令结尾的指令序列
–进行反汇编时,向后偏移一个或几个字节对指令进行解析
•由于CISC支持变长指令

ROP攻击
•找到合适的gadgets
•将gadgets的地址存入栈中
防御
*数据执行保护DEP
概念
•很多缓冲区溢出攻击涉及到将机器码复制到目标缓冲区,然后将执行转移到这些缓冲区
•阻止在栈/堆/全局数据区中执行代码,并假定可执行代码只能在进程地址空间的除这些位置外的其他位置出现
– 需要从CPU的内存管理单元(MMU)提供支持,将虚拟内存的对应页标记为非执行(向MMU中加入no_x0002_execute位)
示例
Linux 关闭DEP
•-z execstack
–gcc -z execstack stack.c -o stack-exec
攻击DEP:代码重用攻击
•思路: 重用程序本身中的代码
–不需要注入代码
•Ret2Libc: 用危险的库函数的地址替换返回地址l
–攻击者构造合适的参数(在栈上,在返回指令指针的上方)
•在x64架构上,还需要更多的工作:设置参数传递寄存器的值
–函数返回,库函数得到执行
•例如:execve(“/bin/sh”)
甚至可以链接两个库函数调用
•在很多攻击中,代码重用攻击用来作为禁用DEP的第一步
–目标是允许对栈内存进行执行
–有一个系统调用可以更改栈的读/写/执行属性
•int mprotect(void *addr, size_t len, int prot);
– 设置对于起始于addr的内存区域的保护
–调用此系统调用,允许在栈上的“执行”属性,然后开始执行被注入的代码
*地址空间随机化ASLR
随机化方法分类
•地址随机化
•指令随机化
•数据随机化
•接口随机化
•基地址随机化
–地址空间随机化 (ASLR)
•相对地址随机化
*地址空间随机化 (ASLR)
•Address Space Layout Randomization
•将进程的堆、栈、共享库等内存空间基地址进行随机化来增大入侵者预测目的地址的难度,从而降低进程被成功入侵的风险
–Linux、Windows 等主流操作系统都已经采用该项技术
•开启 ASLR,在每次程序运行时的时候,装载的可执行文件和共享库都会被映射到虚拟地址空间的不同基地址处
攻破ASLR的方法
•如果随机地址空间很小,可以进行一个穷举搜索
–例如,Linux提供16位的随机
•可以在约200秒以内被穷举搜索攻破
•ASLR经常被memory disclosure攻破
–例如,如果攻击者可以读取指向栈的一个指针值
•他就可以使用该指针值发现栈在哪里
*相对地址随机化
| 类型 | 操作 |
|---|---|
| 指令级随机化 | 等价指令替换;指令重排序;无关指令填充 |
| 基本块级随机化 | 基本块重排序;虚假基本块插入;基本块分解 |
| 函数级随机化 | 函数参数随机化;栈布局随机化 |
| 程序级随机化 | 函数重排序;库函数入口地址随机化; |
指令集随机化
•为每一个运行的进程创建特有的指令集,使得攻击者注入的代码与现在的指令集不兼容而无法正常运行
–常在应用程序加载前或加载时对指令进行加密操作,在执行时对指令进行解密操作
可以有效地抵抗代码注入攻击
缺点:时间开销大
数据随机化
将内存中的指针或非指针数据作为随机化对象,通过随机化变换(加密操作),使得数据在内存中以密文存储,使用时解密数据
接口随机化
•将与底层进行交互的接口进行随机化
–常将系统调用接口的相关信息作为随机化对象
–系统调用号、系统调用参数
控制流完整性检测(CFI)
•判断程序的执行流程是否按照事先得到的控制流图(Control Flow Graph, CFG)进行
CFI基本思想
–限制程序运行中的控制转移,使之始终处于原有的控制流图所限定的范围内。
–规定软件执行必须遵循提前确定的控制流图(CFG)的路径。
•控制流劫持攻击
–引进额外的控制流转移

具体做法
–通过分析程序的控制流图,获取间接转移指令(包括间接跳转、间接调用、和函数返回指令)目标的白名单
–在程序运行过程中,核对间接转移指令的目标是否在白名单中。
•通过二进制代码重写实现:插桩
•利用二进制重写技术向软件函数入口及调用返回处分别插入标识符ID和ID_check
•通过对比ID和ID_check的值是否一致判断软件的函数执行过程是否符合预期,从而判断软件是否被篡改。
CFI分类
| 类型 | 技术 | 简介 |
|---|---|---|
| 基于编译器 | Require source code CFG is more precise 需要源代码CFG更精确 | 1.静态分析源代码构建CFG 2.编译时,插入CFI 检查 3.运行时进行CFI检测 |
| 基于二进制重写 | No source code is needed CFG is more over-approximated | •通过静态二进制文件分析构造CFG(不如通过分析源代码获得的CFG准确) |
| 基于硬件 | Transparent runtime monitoring 透明的运行时监控 | 利用CPU提供的分支记录机制记录程序的执行流 |
绕过CFI检测
–由于构造的CFG不是100%准确
•利用不违反CFG的gadgets进行攻击
–整个函数作为一个gadget
预防(防御性编程)
1.使用更安全的编程语言
2.安全的库libsafe