软件安全期末总结

zhyjc6于2020-01-04发布 约23292字·约51分钟 本文总阅读量

概述

软件安全特点

软件安全缺陷

软件安全威胁

软件安全风险

其它相关概念

软件安全范围

软件保护

软件风险

GaryMcgraw软件漏洞分类:

软件基础

计算机引导

冯·诺依曼体系

BIOS启动程序

  1. 打开计算机电源开关,处理器进入复位(Reset)状态

    • 将所有内存清零,并执行内存同位测试

    • 将段寄存器CS的内容设为FFFFH

    • 其它寄存器都清零,IP=0000H

      因此第一个要执行的指令是位于CS:IP中的指令,物理地址为0FFFF0H,所以将存储器的高地址分配给ROM BIOS,作为BIOS的入口地址

  2. 随后BIOS启动一个程序,进行主机自检

    • 确保系统的每个部分都得到了电源支持,内存储器、主板上的其它芯片、键盘、鼠标、磁盘控制器及一些I/O端口正常可用
  3. 自检程序将控制权还给BIOS

    • BIOS 读取 BIOS 设置,得到引导驱动器的顺序,依次检查
    • BIOS 将所检查磁盘的第一个扇区(512B)载入内存,放在0x0000:0x7c00处,如果这个扇区的最后两个字节是”55AAH”,那么这就是一个引导扇区,磁盘也就是一块可引导盘,调用该驱动器上磁盘的引导扇区进行引导
  4. 系统加载程序

    • 一旦BIOS将控制权移交给操作系统后,就可以向操作系统申请运行程序了
    • 可执行的程序有两种:*.com程序和*.exe程序

Windows系统启动过程

Win32内存体系

win32内存体系可以分为4大层次,速度由低到高分别是:外存、内存、高速缓存、寄存器

寄存器

Intel x86的寄存器可以分为以下几类:

在通用寄存器里面有很多寄存器虽然他们的功能和使用没有任何的区别,但是在长期的编程和使用中,在程序员习惯中已经默认的给每个寄存器赋上了特殊的含义,比如:

虚拟内存

Windows内存被分为两个层面:物理内存和虚拟内存。其中物理内存比较复杂,需要进入Windows内核级别ring0才能看到。

通常在用户模式下,我们用调试器看到的地址都是虚拟内存

虚拟内存与物理内存的映射

内存管理与银行的类比

PE文件结构

定义

PE(Portable Executable)是Win32平台下可执行文件遵守的数据格式。常见的可执行文件(如*.exe文件和*.dll文件)都是典型的PE文件。

作用

一个可执行文件不光包含了二进制的机器代码,还包含了许多其它信息如字符串、图标、位图等。

PE文件格式规定了所有的这些信息在可执行文件中如何组织。在程序被执行时,操作系统会按照PE文件格式的约定去相应的地方准确地定位各种类型的资源,并分别装入内存的不同区域

PE文件格式

PE文件格式把可执行文件分成了若干个数据节(section),不同的资源被放在不同的节中。一个典型的PE文件包含的节如下:

简单构成:

PE文件与虚拟内存之间的映射

静态反汇编工具看到的PE文件中某条指令的位置是相对于磁盘文件而言的,即所谓的文件偏移,我们可能还需要知道这条指令在内存中所处的位置,即虚拟内存的位置

反之,在调试时看到的某条指令的地址是虚拟内存地址,我们也经常需要回到PE文件中找到这条指令对应的机器码

我们首先要清除几个概念:

文件偏移地址在与它们计算时还需要考虑存放方式的不同:

进程空间分区

进程空间的功能分区

把计算机类比成一个工厂

栈区

从计算机科学的角度来看,栈指的是一种数据结构,是一种先进后出的数据表。

栈的最常见操作有两种:压栈(PUSH)、弹栈(POP);

用于标识栈的属性也有两个:栈顶(TOP)、栈底(BASE)。

系统栈

内存的栈区实际上指的就是系统栈。系统栈由系统自动维护,它用于实现高级语言中函数的调用。对于类似C语言这样的高级语言,系统栈的PUSH/POP等堆栈平衡细节是透明的。

一般说来,只有在使用汇编语言开发程序的时候,才需要和它直接打交道。

系统栈工作

栈帧

每一个函数独占自己的栈帧空间。当前正在运行的函数的栈帧总是在栈顶。Win32系统提供两个特殊的寄存器用于标识位于系统栈顶端的栈帧:

  1. ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面的一个栈帧的栈顶
  2. EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面的一个栈帧的底部

ESP与EBP的使用:

在函数栈帧中,一般包含以下几类重要信息:

  1. 局部变量:为函数局部变量开辟的内存空间。
  2. 栈帧状态值:保存前栈帧的底部,前栈帧的顶部可以通过堆栈平衡计算得到,用于在本帧被弹出后恢复出上一个栈帧。
  3. 函数返回地址:保存当前函数调用前的“断点”信息,也就是函数调用前的指令位置,以便在函数返回时能够恢复到函数被调用前的代码区中继续执行指令。

一个至关重要的寄存器

EIP:指令寄存器(Extended Instruction Pointer),其内存放着一个指针,该指针永远指向一条将要执行(当前运行指令的下一条指令)的指令地址。

可以说如果控制了EIP寄存器的内容,就控制了进程—我们让EIP指向哪里,CPU就会去执行哪里的指令。

函数调用

函数调用大致包括以下几个步骤:

其中,第四步栈帧调整具体又分为如下几个步骤:

stdcall调用约定

对于stdcall调用约定,函数调用时用到的指令序列大致如下:

函数调用时系统栈的变化过程

  1. 压参数入栈

  1. call指令和push ebp

  1. 保存旧栈帧,开辟新栈帧

函数返回

与函数调用类似:

  1. 保存返回值:通常将函数的返回值保存在寄存器EAX中。
  2. 弹出当前栈帧,恢复上一个栈帧,具体包括:
    • 在堆栈平衡的基础上,给ESP加上栈帧的大小,降低栈顶,回收当前栈帧的空间
    • 将当前栈帧底部保存的前栈帧EBP值弹入EBP寄存器,恢复上一个栈帧
    • 将函数返回地址弹给EIP寄存器
  3. 跳转:按照函数返回地址跳回母函数中继续执行。

相关指令

字符串安全

字符串基础

C-风格的字符串

在软件工程中,字符串是一个基本的概念,但它并不是C或C++的内建类型。

C++字符串

常见的字符串操作错误

在C和C++中,使用C风格的字符串编程很容易产生错误。最常见的错误有:

  1. 无界字符串复制

    从一个无界数据源复制数据到一个定长的字符数组时

    问题

    解决:利用strlen() 测试输入字符串的长度然后动态分配内存

  2. 空结尾错误

    字符串末尾没有空字符NULL

  3. 截断

    当目标字符数组的长度不足以容纳一个字符串的内容时,就会发生字符串截断。

    字符串截断会丢失数据,有时也会导致软件漏洞

    解决:一些限制字节数的函数通常用来防止缓冲区溢出漏洞:

    • strncpy() 代替strcpy()
    • fgets()代替 gets()
    • snprintf()代替 sprintf()
  4. 差一错误

    差一错误(英语:Off-by-one error,缩写OBOE)是在计数时由于边界条件判断失误导致结果多了一或少了一的错误

  5. 数组写入越界

  6. 不恰当的数据处理

字符串漏洞

缓冲区溢出

什么是缓冲区溢出

当向为某特定数据结构分配的内存空间边界之外写入数据时, 就会发生缓冲区溢出。

可通过修改下列参数来利用缓冲区溢出:

栈粉碎

这是一种很严重的漏洞,因为它会对程序的可靠性和安全性造成严重的后果

当缓冲区溢出覆写分配给执行栈内存中的数据时,就会导致栈粉碎

成功的利用这个漏洞能够覆写栈返回地址,从而在目标机器中执行任意代码

弧注入(return-into-libc)

代码注入

缓解措施

缓解措施包括:

  1. 预防缓冲区溢出
  2. 侦测缓冲区溢出并安全地恢复,使得漏洞利用的企图无法得逞

防范策略:

静态方法

动态方法

防范策略:SafeStr

示例:

管理字符串

黑名单

白名单

指针安全

定义

指针安全是通过修改指针值来利用程序漏洞的方法的统称

可以通过覆盖函数指针将程序的控制权转移到攻击者提供的外壳代码

对象指针也可以被修改,从而执行任意代码

缓冲区溢出覆写指针的条件

  1. 缓冲区与目标指针必须分配在同一个段内
  2. 缓冲区必须位于比目标指针更低的内存地址处
  3. 该缓冲区必须是界限不充分的,因此容易被缓冲区溢出利用

UNIX可执行文件包含data段和BSS段;

data段包含了所有已初始化的全局变量和常数;

BSS( Block Started by Symbols )段包含了所有未初始化的全局变量;

已初始化的全局变量和未初始化变量分开是为了让汇编器不将未初始化的变量内容写入目标文件

示例:

修改指令指针

全局偏移表

Global Offset Table(GOT)

  1. 程序首次使用一个函数前,GOT入口项包含运行时连接器RTL(runtime linker)的地址
  2. 如果该函数被程序调用,则程序的控制权被转移到RTL,然后函数的实际地址被确定且被插入到GOT中
  3. 接下来就可以通过GOT中的入口项直接调用函数

如何利用GOT

.ctors区 & .dtors区

覆写.dtors区的优缺点

对于攻击者而言,覆写.dtors区的好处在于:

然而:

虚函数

定义

虚函数是在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数

虚函数是:

实现

虚函数表VTBL:

虚指针VPTR:

攻击

攻击者可以:

攻击者可以通过任意内存写或者利用缓冲区溢出直接写入对象实现这一操作

atexit() & on_exit()

atexit()

on_exit()

用法

示例

longjump()

异常处理

异常是指函数操作中发生的意外情况

Windows提供了3种形式的异常处理程序:

  1. 向量化异常处理 Vectored Exception Handling (VEH)
    • Windows XP增加了对这种异常处理程序的支持
    • VEH首先调用以重写SEH
  2. 构化异常处理 Structured Exception Handling (SEH)
    • 被实现为每函数或每线程的异常处理程序
  3. 系统默认异常处理 System Default Exception Handling

SEH

栈帧初始化

攻击者可以:

动态内存管理

动态内存的程序员视角

动态内存函数

动态内存管理器

内存分配的不同算法
  1. 连续匹配:从当前指针位置开始查询匹配的第一个空闲区域
  2. 最先匹配:从内存开始位置寻找第一个空闲区域
  3. 最佳匹配:有m个字节的区域被选中,其中m是(或其中一个)可用的最小的等于或大于n个字节的连续存储的块
  4. 最优匹配:对空闲块取样,选取第一个比样本更合适的块
  5. 最差匹配:挑最大的空闲块
  6. 伙伴系统:伙伴系统只分配 $2^i$大小的块。若需要m大小的块,则分配$2^{[log_bm]+1}$或者更大的块;当块返回时,尝试和它相邻的同样大小的块合并
  7. 隔离:保持单独的大小一致的块的列表

常见错误

初始化错误

未检查返回值

引用已释放内存

对同一块内存释放多次

不正确配对的内存管理函数

未能区分标量和数组

分配函数使用不当

Doug Lea内存分配器

该分配器在gcc和大多数的Linux版本中都是默认;表述均针对dlmalloc 2.7.2版。但是其中包含的安全缺陷原理是所有版本都具有的

内存块结构

空闲块

已分配块和空闲块都使用一个PREV_INUSE位区分:

Unlink技术

在free()时,内存块如果满足条件会被合并:

Unlink宏

#define unlink(P, BK, FD) {
	FD = P->fd;
	BK = P->bk;
	FD->bk = BK;
	BK->fd = FD;
} 

解链技术

即unlink技术,由Solar Designer提出,被成功用来攻击多个版本的Netscape浏览器、traceroute和slocate这些使用了dlmalloc的程序

利用缓冲区溢出来操作内存块的边界标志:

漏洞程序示例

#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[]) {
    char *first, *second, *third;
    first = malloc(666);//内存分配块1
    second = malloc(12);//内存分配块2
    third = malloc(12);//内存分配块3
    strcpy(first, argv[1]); //无界strcpy操作引发缓冲区溢出
    free(first); 
    free(second); 
    free(third); 
    return(0); 
}

分析:

  1. 程序第9行释放第一块内存块。如果此时第二块内存处于未分配状态,free()操作会试图将其与第一块合并

  2. 我们的目的是要调用unlink宏,所以必须要合并,但是第二块却是已分配的内存块,咋办呢

  3. 继续分析,为了判断第二块内存是否处于空闲状态,free()会检查第3块的PREV_INUSE标志位

  4. 如果我们改写该标志位呢?改写后就可以欺骗free()了,如何改写?

  5. 在程序开始时,内存是连续分配的,所以内存地址逻辑上是连续的,而第一块内存又存在缓冲区溢出,所以可以覆写第二块内存块

  6. 把第2块内存的size覆写为-4,这样当free()需要确定第3块内存地址时,就会将第2块内存起始地址加上其大小,导致得到的值是其位置减4,同时标志位也被置空

  7. Doug Lea的malloc此时会错误地认为下一连续内存块是自第2块内存前面4字节开始(把第2块当作第3块)

  8. 输入字符串进行缓冲区溢出

  9. 执行unlink宏

    此时覆写已经完成,不用管第4步的结果了

  10. unlink()宏将攻击者提供的4个字节的数据(CODE_ADDRESS)写到同样是由攻击者指定的4个字节的地址(FUNCTION_POINTER)处

一旦攻击者可以在任意地址处写入4字节数据,利用该漏洞程序本身的权限执行任意代码就变得简单多了

Frontlink技术

与unlink是解链相比,frontlink则相反,是构造链表,但是却更难以利用也更危险!

当一块内存被释放时,它必须被正确地链接进双链表中。而在dlmalloc的某些版本中,此项操作是由frontlink()代码段来完成的

代码段

BK = bin;
FD = BK->fd;
if (FD != BK) {
    while (FD != BK && S < chunksize(FD)) {
        FD = FD->fd;
    }
    BK = FD->bk;
}
P->bk = BK;
P->fd = FD;
FD->bk = BK->fd = P

漏洞程序示例

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
int main(int argc, char *argv[])
{	if (argc !=3){
		printf("Usage: prog_name arg1 \n");
		exit(-1);	
	}	
	char *first, *second, *third;	
	char *fourth, *fifth, *sixth;	
	first = malloc(strlen(argv[2]) + 1);	
	second = malloc(1500);	
	third = malloc(12);	
	fourth = malloc(666);	
	fifth = malloc(1508);	
	sixth = malloc(12);	
	strcpy(first, argv[2]);	//无界copy
	free(fifth);	
	strcpy(fourth, argv[1]);//无界copy	
	free(second);	
	return(0);
}
  1. 首先攻击者提供恶意实参

  2. 代码第17行将argv[2]复制到first内存块

  3. 代码第18行当fifth内存块被释放时,它被放入一个匡中(大于1024)

  4. 代码第19行出现缓冲区溢出,fourth被精心设计的数据(argv[1]) 填满,并且fifth的前向指针指向了一个假的内存块

  5. 这个假的内存块的后向指针为一个函数指针的地址减8(一个合适的函数指针是存储于程序.dtors区中的第一个析构函数的地址,这个地址可以通过检查可执行映像获得)

  6. 第20行当second块被释放时,程序将调用frontlink()代码段将其插入到与fifth块相同的匡中(因为大于1024)

  7. 执行frontlink代码

    此时frontlink已经完成了地址覆写的任务(覆写析构函数为shellcode)

  8. 程序调用return(0)触发析构函数

双重释放漏洞

这种类型的漏洞是由于对同一块内存释放两次所造成的(在这两次释放之间没有对内存进行重新分配)

要成功地利用双重释放漏洞,有两个条件必须满足:

  1. 被释放的内存块必须在内存中独立存在
  2. 该内存所被放入的筐(bin)必须为空

漏洞原理

RtlHeap

Win32内存管理API

虚拟内存API

堆内存API

局部内存API和全局内存API

CRT内存函数

内存映射文件API

需要理解的RTL数据结构:

进程环境块

空闲链表

空闲链表举例

后备缓存链表

边界标志

基于堆的缓冲区溢出攻击

更容易的方法

缓解策略

整数安全

整数表示方法

  1. 原码表示法
  2. 反码表示法
  3. 补码表示法
  4. 对整数表示法而言,需要考虑的问题主要就是负数的表示

原码表示法

反码表示法

补码表示法

带符号和无符号

4位的有符号补码表示法

4位无符号补码表示法

整型类型

整数类型可以分为两大类:标准整型和扩展整型

整数取值范围

整型转换

整数转换级别

从无符号整型转换

带符号整型转换

普通算术转换

  1. 如果两个操作数具有同样的类型,则不需要进一步的转换。
  2. 如果两个操作数拥有同样的整型(带符号或无符号),具有较低整数 转换级别的类型的操作数会被转换到拥有较高级别的操作数的类型。
  3. 如果具有无符号整型操作数的级别大子或等于另一个操作数类型的级别,则带符号整型操作数将被转换为无符号整型操作数的类型。
  4. 如果带符号整型操作数类型能够表示无符号整型操作数类型的所有可能值,则无符号整型操作数将被转换为带符号整型操作数的类型。
  5. 否则,两个操作数都被转换为与带符号整型操作数类型相对应的无符号整型。

整数错误

整数溢出

符号错误

无符号整型转换到带符号整型,它们是

如果无符号整数的最高位

带符号整型转换到无符号整型,它们是

如果带符号整数的值是

截断错误

截断错误发生于

原值的低位被保留下来而高位则被丢弃。

整数错误侦测

通过硬件

先验条件

后验条件

漏洞

1.JPEG例子

2.内存分配例子

3.符号错误例子

4.截断漏洞

5.非异常的整数逻辑错误

缓解策略

范围检查

强类型

抽象数据类型

SafeInt类

格式化输出

格式化输出函数

格式化输出函数是由一个格式字符串和可变数目的参数构成

格式化输出函数

格式字符串

函数调用约定

漏洞利用

使程序崩溃

原理

构造

当用以下格式字符串调用格式化输出函数时,就会触发无效指针存取或未映射的地址读:

printf("%s%s%s%s%s%s%s%s%s%s%s%s");

转换指示符%s显示执行栈上相应参数所指定的地址的内存。

由于在这个例子中没有提供字符串参数,因此printf()可以依次读取栈中该格式字符串开始后的内存位置,直到格式字符串耗尽或者遇到一个无效指针或未映射地址为止。

查看栈内容

printf("%08x%08x%08x%08x")

格式字符%08x%08x%08x%08x指示函数printf()从栈中取回4个参数并将它们以8位十六进制数的形式显示出来。

同理可以把%x换成%s来查看内存内容,同时可以使用%x来占位,向前推进%s使其达到想要查看的地址

覆写内存

printf("\xdc\xf5\x42\x01%08x.%08x.%08x%n”);

利用%08x推进%n使其对应我们构造的地址0x0142f5dc,然后printf执行后会将输出的字符个数写入到该地址达到覆写内存的目的

缓解策略

  1. 限制字节写入来控制缓冲区溢出
  2. 使用具有增强的安全性的函数
  3. C++可以使用iostream库,这个库提供了通过流来实现输入输出的功能

并发

并发机制

并发是一种系统属性,是指系统中几个计算同时执行,并可能彼此交互。

多线程与并发

多线程安全

并行与并发

数据并行与任务并行

并行包括数据并行(data parallelism)和任务并行(task parallelism)

并行的性能目标

并行的性能目标是并行度(parallelism)

例一:

例二:

常见错误

竞争条件

竞争条件难以察觉、重现和消除,并可能导致错误,如数据损坏或崩溃

成因:竞争条件是运行时环境导致的,这个运行时环境包括必须对共享资源的访问进行控制的操作系统,特别是通过进程调度进行控制的

损坏的值

如8位存储平台上写入16位数值

易变的对象

缓解策略

同步原语

在竞争窗口之前获取同步对象,然后在窗口结束后释放它,使竞争窗口中关于使用相同的同步机制的其他代码是原子的。

竞争窗口最终成为一个代码临界区。所有临界区对执行临界区的线程以外的所有适当的同步线程都是原子的。

锁机制

防止临界区并发执行的策略中,大多数涉及锁机制

锁机制导致一个或多个线程等待,直到另一个线程退出临界区。

互斥量

最简单的一种锁机制是称为互斥量的一个对象。

原子操作

原子操作是不可分割的。一个原子操作不能被任何其他的操作中断,当正在执行原子操作时,它访问的内存,也不可以被任何其他机制改变。因此,必须在一个原子操作运行完成后,其他任何事物才能出问该操作所使用的内存,

原子操作不能被划分成更小的部分。简单的机器指令,例如,装载一个寄存器,可能是不可中断的。被一个原子加载访问的内存位置不可以由其他任何线程访问,直到此原子操作完成。

原子对象是保证它执行的所有操作都是原子的任何对象。通过对某个对象上的所有操作施加原子性,一个原子对象不会被同时读取或写人破坏。原子对象不存在数据竞争,虽然它们仍然可能会受到竞争条件的影响。

不可变数据结构

提供线程安全的一种常用的方法是简单地防止线程修改共享数据,在本质上,即是使数据只读。保护不可改变的共享数据不需要锁。

在C 和C++中一种常见的战术是声明一个共享对象为const(常量)。

另一种方法是复制一个线程可能要修改的任何对象。在这种情况下,所有共享对象都是只读的,任何需要修改一个对象的线程都创建一个共享对象的私有副本,其后只能用它的副本工作。因为副本是私有的,所以共享的对象仍然是不变的。

线程安全

线程安全函数的使用可以帮助消除竞争条件。根据定义,一个线程安全函数通过锁或其他互斥机制来防止共享资源被并发访问。因此,一个线程安全的函数可以同时被多个线程调用,而不用担心。

如果一个函数不使用静态数据或共享资源,它明显是线程安全的。然而,使用全局数据引发了线程安全的红旗,且任何对全局数据的使用必须同步,以避免竞争条件。为了使一个函数成为线程安全的,它必须同步访问共享资源。特定数据的访问或整个库可以锁定。然而,在库上使用全局锁会导致争用(contention)

可重入

可重入(reentrant) 函数也可以减轻并发编程错误。函数是可重入的,是指相同函数的多个实例可以同时运行在相同的地址空间中,而不会创建潜在的不一致的状态。

IBM定义的可重入函数,是指它在连续调用时不持有静态数据,也不会返回一个指向静态数据的指针。因此,可重入函数使用的所有数据都由调用者提供,并且可重人函数不能调用不可重人函数。可重人函数可以中断,并重新进入(reentered) 而不会丢失数据的完整性,因此,可重人函数是线程安全的[IBM 2012b] 。

缓解陷阱

并发实现的常见错误

  1. 没有用锁保护共享数据(即数据竞争)
  2. 当锁确实存在时,不使用锁访问共享数据
  3. 过早释放锁
  4. 对操作的一部分获取正确的锁,释放它,后来再次取得它,然后又释放它,而正确的做法是一直持有该锁
  5. 在想要用局部变量时,意外地通过使用全局变量共享数据
  6. 在不同的时间对共享数据使用两个不同的锁
  7. 由下列情况引起死锁
    1. 不恰当的锁定序列(加锁和解锁序列必须保持一致)
    2. 锁定机制使用不当或错误选择
    3. 不释放锁或试图再次获取已经持有的锁
  8. 缺乏公平——所有线程没有得到平等的机会来获得处理。
  9. 饥饿——当一个线程霸占共享资源、阻止其他线程使用时发生。
  10. 活锁——线程继续执行,但未能获得处理。
  11. 假设线程将
    1. 以一个特定的顺序运行
    2. 不能同时运行
    3. 同时运行
    4. 在一个线程结束前获得处理
  12. 假设一个变量不需要锁定,因为开发人员认为只有一个线程写人它且所有其他线程都读取它。这还假定该变量上的操作是原子的。
  13. 使用非线程安全库。如果一个库能保证由多个线程同时访问时不会产生数据竞争,那么认为它是线程安全的。
  14. 依托测试,以找到数据竞争和死锁。
  15. 内存分配和释放问题。当内存在一个线程中分配而在另一个线程中释放时,这些问题可能出现,不正确的同步可能会导致内存仍然被访问时被释放。

死锁

  1. 同步原语的不正确使用可能会导致死锁
    1. 当两个或多个控制流以彼此都不可以继续执行的方式阻止对方时,就会发生死锁。
    2. 特别是,对于一个并发执行流的循环,如果其中在循环中的每个流都已经获得了导致在循环中随后的流悬停的同步对象,则会发生死锁。
  2. 死锁(和其他的数据竞争)可能对以下条件敏感:
    1. 处理器速度
    2. 进程或线程调度算法的变动
    3. 在执行的时候,强加的不同内存限制
    4. 任何异步事件中断程序执行的能力
    5. 其他并发执行进程的状态

著名漏洞

  1. 在多核动态随机访问存储器系统中的DoS攻击
  2. 系统调用包装器中的并发漏洞

文件输入输出

文件I/O基础

文件系统

程序与文件系统交互方式的不规则性是文件IO漏洞的根源

UNIX

Linux

Mac OS X

文件

文件是由块(通常在磁盘上)的集合组成

目录

目录是由目录条目的列表组成的特殊文件。

目录条目的内容包括目录中的文件名和相关的i-节点的数量

i-节点

MS-DOS 文件名

文件IO接口

C中的文件IO接口在中定义的所有函数

IO操作的安全性依赖于具体的编译器实现、操作系统和文件系统

字节输入函数

  1. fgetc()
  2. fgets()
  3. getc()
  4. getchar()
  5. fscanf()
  6. scanf()
  7. vfscanf()
  8. vscanf()

字节输出函数

  1. fputc()
  2. fputs()
  3. putc()
  4. putchar()
  5. fprintf()
  6. vfprintf()
  7. vprintf()

宽字节输入函数

  1. fgetwc()
  2. fgetws()
  3. getwc()
  4. getwchar()
  5. fwscanf()
  6. wscanf()
  7. vfwscanf()
  8. vwscanf()

宽字节输出函数

  1. fputwc()
  2. fputws()
  3. putwc()
  4. putwchar()
  5. fwprintf()
  6. wprintf()
  7. vfwprintf()
  8. vwprintf()

C文本流

标准C程序在启动时,预定义了3个文本流,操作前不必打开它们:

  1. stdin:标准输入(用于读常规输入)
  2. stdout:标准输出(用于写常规输出)
  3. stderr:标准错误(用于写入诊断输出)

文本流stdin、stdout和stderr是FILE指针类型的表达式。在最初打开时,标准错误流不是完全缓冲的。如果流不是一个交互设备,那么标准输入和标准输出流是完全缓冲的

C++文件IO

C++提供下列的:

打开、关闭文件

文件打开

fopen()函数打开一个文件,其名称是由文件名指向的字符串,并把它与流相关联。

FILE *fopen(
    const char* restrict filename,
    const char* restrict mode,
    ); 

参数mode 指向一个字符串,如果该字符串是有效的,那么该文件以指定的模式打开;否则,其行为是未定义的。

C99支持以下模式:

C11增加的独占模式:

文件关闭

文件访问控制

UNIX文件权限

权限与特权

用户与认证

文件的特权与权限

文件鉴定

目录遍历

特殊文件名

目录遍历漏洞

如果服务器接收如”../“形式的输入而没有适当的验证,那么就会允许攻击者遍历文件系统来访问任意文件,例如:/home/../etc/shadow会被解析成/etc/shadow

等价错误

当一个攻击者提供不同但等效名字的资源来绕过安全检查时,就会发生路径等价漏洞

例如

符号链接

符号链接(symbolic link)是一个方便的解决文件共享的方案

符号链接实际上创建了一个具有特殊的i-节点(i-node)的新文件。符号链接是特殊的文件,其中包含了实际文件的路径名

如果路径名称解析过程中遇到符号链接,则用符号链接的内容替换链接的名称。例如:一个路径名/usr/tmp,其中tmp是一个指向../var/tmp的符号链接,那么它就被解析成/usr/../var/tmp,由于..这进一步又被解析为 /var/tmp

竞争条件

检查时间和使用时间

文件I/O期间可能出现检查时间和使用时间(Time Of Check, Time Of Use , TOCTOU)竞争条件。首先测试(检查)某个竞争对象属性,然后再访问(使用)此竞争对象,TOCTOU竞争条件形成一个竞争窗口。

TOCTOU漏洞可能是首先调用stat(),然后调用open(),或者它可能是一个被打开、写入、关闭,并被一个单独的线程重新打开的文件,或者它也可能是先调用一个access(),然后再调用fopen()

创建而不替换

如果在open()调用执行时file_name已经存在,那么打开该文件,并截断它。如果file_name是一个符号链接,那么该链接引用的目标文件被截断。攻击者所有需要做的事就是在此调用之前在file_name创建一个符号链接。假设这个有漏洞的过程有相应的权限,那么目标文件将被覆写。

独占访问

由独立的进程产生的竞争条件不能用同步原语来解决,因为这些过程不可能访问共享的全局数据(如一个互斥变量)。

通过将文件当作锁来使用,仍可以同步这类并发控制流。例8.15包含两个函数,它们实现了一个Linux文件锁机制。对lock()的调用用于获得锁,而对unlock()的调用则可以释放锁

int lock(char *fn) { //例8.15
    int fd;
    int sleep_time = 100;
    while (((fd = open(fn, O_WRONLY | O_EXCL |
       O_CREAT, 0)) == -1) && errno ==EEXIST) {
        usleep(sleep_time);
        sleep_time *= 2;
        if (sleep_time > MAX_SLEEP)
            sleep_time = MAX_SLEEP;
    }
    return fd;
}
void unlock(char *fn) {
    if (unlink(fn) == -1) {
        err(1, file unlock);
    }
}

共享目录

缓解策略

关闭竞争端口

互斥缓解方案

UNIX和Windows支持很多能够在一个多线程应用程序中实现临界区的同步原语,包括互斥变量、信号量、管道、命名管道、条件变量、CRITICAL_SECTION(临界区)对象以及锁变量等。

线程间的同步可能引入死锁的潜在威胁。当进程从饥饿状态转变为恢复执行时,存在相关联的活锁( live lock) 问题。

避免死锁的标准措施是要求资源的获取按照特定的顺序进行。从概念上说,所有要求互斥的资源都可以被编号为r1、r2、…、rn。只要保证进程在捕获资源rk之前已经捕获了所有的资源 rj (其中j < k) ,就可以避免死锁。

线程安全的函数

在多线程应用程序中,仅仅确保应用程序自己的指令内不包含竞争条件是不够的,被调用的函数也有可能造成竞争条件。当宣告一个函数为线程安全的时候,就意味着作者相信这个函数可以被并发线程调用,同时该函数不会导致任何竞争条件问题。不应该假定所有函数都是线程安全的,即使是操作系统提供的API。当要使用的函数必须为线程安全时,最好去查阅它的文档以确认这一点。

如果必须调用一个非线程安全的函数,那么最好将它处理为一个临界区,以避免与任何其他代码调用冲突。

使用原子操作

同步原语依赖于原子(不可分割的)操作。当调用了Enter 'CriticalRegion()pthreadmutex-lock()之后,本质上直到函数运行完成为止,该函数都不会被中断。如果一个EnterCriticalRegion()调用允许与另一个EnterCriticalRegion()调用(也许是由另一个线程调用的)重叠,或者与一个LeaveCriticalRegion() 调用重叠,那么这些函数内部可能会存在竞争条件。正是这种原子属性使得这些函数在同步操作中非常有用。

重新打开文件

重新打开一个文件流一般应避免,但对于长期运行的应用程序,这可能是必要的,以避免消耗可用文件描述符。由于文件名在每次打开时重新与文件关联,因此无法保证重新打开的文件就是原始文件。

检查符号链接

消除竞争对象

软件开发人员也应该消除对系统资源不必要的使用,以尽量减小漏洞的暴露。比方说,Windows的ShellExecute()函数尽管是为打开一个文件提供了便利的方式,但是这个命令依赖于注册表来选择一个将要应用于文件的应用程序。显而易见,调用CreateProcess()并显式指定应用程序的做法比依赖注册表更可取。

使用文件描述符而非文件名

控制对竞争对象的访问

最小特权原则

可以通过减少进程的特权来消除竞争条件,而其他时候减少特权仅仅可以限制漏洞的暴露。无论如何,最小特权原则都是一种缓解竞争条件以及其他漏洞的明智策略

暴露

避免通过用户接口或其他的API 暴露你的文件系统的目录结构或文件名

一个更好的方法可能是让用户指定一个键作为标识符。然后,此键可以通过一个哈希表或其他数据结构映射到文件系统中一个特定的文件,而不把文件系统直接暴露给攻击者。

竞争检测工具

静态分析

静态分析工具并不通过实际执行软件来进行竞争条件软件分析。这种工具对软件源代码(或者,在某些情况下,二进制执行行文件)进行解析,这种解析有时依赖于用户提供的搜索信息和准则。静态分析工具能报告那些显而易见的竞争条件,有时还能根据可察觉的风险为每个报告项目划分等级。

动态分析

动态分析工具通过将侦测过程与实际的程序执行相结合,克服了静态分析工具存在的一些问题。这种方式的优势在于可以使工具获得真实的运行时环境。只分析实际的执行流具有一个额外的好处,即可以减少必须由程序员进行分析的误报情况。

动态侦测的主要缺点包括:(1) 动态工具无法侦测未执行到的路径; (2) 动态检测通常会带来巨大的运行时开销。

软件安全实践

基本没讲,可以忽略

安全生命周期

安全需求

安全设计

安全实现

安全验证

参考资料