自动化软件漏洞利用

admin 2022年3月30日16:23:59评论40 views字数 12295阅读40分59秒阅读模式

        Automatic Exploit Generation可以看作是一种算法,它集成了数据流分析和决策程序,目的是自动化构建漏洞利用。本文主要覆盖三种常见安全漏洞的自动化漏洞利用:基于堆栈的破坏存储指令指针的缓冲区溢出,破坏函数指针的缓冲区溢出,以及缓冲区溢出造成任意地址写。

1. 介绍

自动化软件漏洞利用


当前漏洞利用通常是手工构建的,需要手动分析应用程序的控制流及其对输入数据执行的操作,这是一个非常耗时的过程。自动化程序分析技术已经成功应用于诸多问题。本文大部分描述了由数据流分析组成的系统与决策程序相结合。
以下是常见一些漏洞检测的方法:
  • 假设有一个程序  和一个补丁版本 ,为  生成样本输入执行  中修补的漏洞,通过添加与漏洞相关的输入约束条件,然后生成违反这些约束的输入,将其沿着通往漏洞点的路径传递,结果是会触发漏洞。使用数据流分析得出路径条件,然后解决此类问题条件使用决策过程 STP 来产生一个新的程序输入。由于生成的程序输入旨在违反添加的约束,因此可能会导致由于某种形式的内存损坏而崩溃。
  • 使用可能导致崩溃的输入作为起点,再次使用数据流分析来构建路径条件,然后用于使用决策过程生成新输入。
  • 通过调试器附加到一个正在运行的进程,然后将测试用例交付给程序。这种方式会遵循一组静态规则来发现漏洞类型。这些规则试图确定输入数据的哪些部分覆盖了哪些敏感数据,因此可以用来控制程序的执行。一旦确定结果,这些值将用于漏洞类型的模板生成漏洞利用。
  • 通过API,例如 printf。一旦这些 API 功能的效果产生形式化后,指定漏洞利用所需的条件,然后通过调用函数确定漏洞类型。这种方法仅限于所有必然的内存损坏的漏洞,比如printf格式化字符串漏洞。
  • 自动为漏洞生成签名,并使用这些签名来描述是否有漏洞。
  • 现在常见的还有数据流分析、污点传播、约束求解和符号执行等方法。

约束

        我们需要从以上方法中找到一种综合性相对强的方法,因此我们施加一些实际限制:
  • 用户输入的数据破坏了存储的指令指针、函数指针或写目标地址的地址和源值;    
  • 可以启用ASLR,但没有其他漏洞利用保护;
  • 使用数据流分析和约束求解来检测控制流劫持漏洞;
  • 需要漏洞利用自动生成并执行;
  • 应该可靠地将漏洞归类为可利用漏洞和不可利用漏洞(比如DoS),这一点至关重要。


2. 问题定义

自动化软件漏洞利用


本节介绍基本的架构选择和所用到的知识内容,然后我们用公式符号定义算法过程中的一些类型。

操作系统和架构

CPU 架构的设计和指令集差异很大。现在就假设我们的目标架构是 32 位 Intel x86 CPU。32 位 x86 处理器定义了许多通用和专用寄存器。我们必须特别考虑其中四个:
  1. EIP寄存器:指令指针寄存器;
  2. EBP寄存器:栈数据相关;
  3. ESP寄存器:栈数据相关;
  4. EFLAGS寄存器:关乎程序执行流;
不同的操作系统漏洞利用技术不同,我们选择开源的Linux系统更便于将注意力集中在漏洞利用本身。需要用到的操作系统和架构知识:
  • 汇编语言、内存存储方式;
  • ELF文件格式;

构造函数和析构函数

GCC4.7 以前的版本,编译时会将构造函数和析构函数放在 .ctor 段和 .dtor 段中,分别由 __do_global_ctors_aux 和__do_global_dtors_aux 去执行:
$ objdump -s -j .ctors a.out

a.out: file format elf32-i386

Contents of section .ctors:
8049f00 ffffffff 00000000 ........
$ objdump -s -j .dtors a.out

a.out: file format elf32-i386

Contents of section .dtors:
8049f08 ffffffff 00000000 ........
$ nm ./a.out | grep '__[C|D]TOR'
08049f04 d __CTOR_END__
08049f00 d __CTOR_LIST__
08049f0c D __DTOR_END__
08049f08 d __DTOR_LIST__
从 GCC4.7 开始,.ctor 和 .dtor 段被移除,构造函数和析构函数分别存放到 .init_array 和 .fini_array 中了。用 4.7 的 GCC 编译后如下:
Contents of section .init_array:
8049f00 d0830408 fc830408 ........
Contents of section .fini_array:
8049f08 b0830408 10840408 ........
段尾的 fc830408 和 10840408 就是构造函数和析构函数的内存地址 0x080483fc 和 0x08048410。可以将我们生成的payload函数指针写入到析构函数中。
其他所用到的知识:
  • 包括但不限于ASLR、NX(DEP)、Canary、堆的基本保护机制及绕过方式

程序定义

一般来说,每个程序  都可以看作由一个“无限数组”组成路径  ,每条路径  是一个无限序列对 ,每一对包含一个地址 和该地址的汇编指令  ,每对可以出现一次或多次。
 路径  的指令由  表示。如果存在一对  使得  的话,那么  和  被视为两条不同的路径。

指令定义

当引用一对  当中的  时,我们将使用  本身的标识符,并在需要时将地址明确指定为  。指令可以有源操作数和目的操作数,源操作数会被读取,然后写入到目的操作数中。 中的源操作数由  表示,而目的操作数由  表示。 和  可以是内存地址或寄存器。

路径定义

具体的路径定义为  ,其中一条指令  的每个操作数是已知的  。我们必须静态的分析某些操作数的值。在分析中,我们假定给定输入  恰好对应程序的某个路径,那么程序运行可以定义为  为程序  的输入,导致路径  被执行。

内存定义

假设  是程序  的所有有效内存地址的集合,而  是所有字节值的集合{0x00, ... , 0xff},那么  是从 到  的总函数。每个正在运行的程序都定义了  ,并且任何写入内存的指令都可以修改所有内存地址  中的  。我们将  的域(内存地址)划分为两个子集,那些  的内存地址可以用于存储用户污染过的数据构成集合  。 内  的补集是内存地址  的集合,它们不能存放用户输入的数据,我们称其为  。 中有一个子集  ,包含了所有最终可能用于指令指针的值的内存地址,例如存储在栈上的指令指针。

CPU寄存器定义

总函数  是一组CPU寄存器。 的域是一个集合,其基数是CPU上可用寄存器的数量,成员是寄存器名称。例如EAX、ECX、ESP等, 的范围是dword值 {0x0, ..., 0xffffffff}。

漏洞利用定义

程序  输入利用 ,必须确保路径包含组件 
  •  是直接破坏  中的一个或多个字节的指令序列( 是一组不应该被用户输入污染的内存位置);
  •  是破坏内存位置  的指令序列,其地址是我们应该注入的payload地址;
  •  是漏洞注入进程的payload。
举个例子:对于直接利用,意味着它必须溢出缓冲区并且破坏  中的地址,从而导致执行注入的 payload。

AEG模型

综上所属,我们的AEG模型必须有如下功能:
  • 首先,它必须能够分析程序的内存状态,以便找到合适的位置注入我们的payload。它必须确定用户输入对内存造成影响的输入点,还必须能够发现输入后程序路径的结束点。从这些信息中,它将生成一个输入,填充所选定的输入点缓冲区;
  • 其次,必须检测  中被用户输入污染的位置,以便重定向到payload处,这将再次要求算法确定输入字节数,在程序的崩溃位置前进行修改;
  • 最后,必须能够根据前面的模型创建一个公式:输入  破坏  中的值导致重定向到恶意代码 
因此,AEG问题是一个重复性问题的组合:从运行时分析产生一个新问题,然后使用此信息生成输入。过程中组成的算法必须解决后一个问题,并使用程序流分析算法来解决程序执行过程中所需的信息。

3. 自动漏洞利用算法

自动化软件漏洞利用


        本节介绍AEG算法,包含动态数据流分析、路径条件枚举与决策过程。主要包含以下三个阶段:
  • 阶段一包含迭代检测和污点分析。通过跟踪程序来分析程序执行的路径,分析并记录与当前路径条件相关信息,直到发现可利用漏洞点为止;
  • 一旦发现漏洞点,就开始第二阶段。包含四个任务:
    1. 确定漏洞的类型。如果是指令存在漏洞,我们尝试间接利用;如果是函数指针类型,那么我们尝试直接利用函数指针;
    2. 然后构建我们第一个漏洞利用公式:输入  破坏  中的值导致重定向到恶意代码 。在污点分析过程中就应该过滤处理这些缓冲区,我们将这些位置作为下一个公式的跳板;
    3. 随后我们构造第二个公式用于约束存储的指令指针(间接利用)或函数指针(直接利用)。对于直接利用,我们限制EIP的值;而对于间接利用,我们限制  的操作数的值;
    4. 然后我们结合两个公式,计算所有的内存位置的路径条件。这个最终公式表达了对程序  利用所需的条件。
  • 阶段三使用决策程序来尝试生成满意的利用脚本。
流程图如下:
自动化软件漏洞利用

第一阶段 检测和运行时分析

这一阶段的目的是在程序开始执行时收集数据流和路径信息,这些信息用作之后的漏洞利用生成。我们使用一种二进制动态监测的方法,这种方法不需要考虑静态分析下的变量不确定性所带来的困扰。一般有如下三种方式:
  • 执行跟踪。这种方法将程序运行过程中的指令、寄存器和修改的内存储存在数据库中。有些调试器还提供了一些其他的功能,例如Ollydbg、gdb。这种方法很有用,因为它对程序运行性能影响很小,缺点就是需要大量的磁盘来保存数据;
  • 仿真。现在有很多工具可以模拟硬件功能,在虚拟环境中执行程序,例如QEMU,它允许控制仿真系统运行,并在运行期间分析数据流和路径条件信息;
  • 二进制检测。这是一种代码注入技术,即二进制插桩。注入的代码负责观察被检测的程序。
通过比较,我们决定使用动态二进制检测(DBI)框架,因为它允许在运行时分析不受限制,并且这种框架在一些项目中表现出色。Valgrind、DynamoRIO和Pin是三种不同的框架。但它们概念相同,它们都提供一个虚拟环境来运行程序  。DBI框架提供一种特殊机制,使得我们可以在CPU执行前监视和修改指令。一些框架还允许在功能级别上检测从而分析代码触发函数调用或事件。
以下是三种框架的对比:
  • Valgrind:与其他二者的主要区别是将程序的汇编代码转化为中间语言  表示,然后再提供给客户端分析,这种中间语言类似于RISC。这意味着分析客户端不必显式的添加x86汇编。因为需要转换中间语言,所以性能方面会有所折扣:
举个例子, x86汇编代码如下:
addl %eax, %ebx

IR中间语言表示如下:
------ IMark(0x24F275, 7) ------
t3 = GET:I32(0) # get %eax, a 32-bit integer
t2 = GET:I32(12) # get %ebx, a 32-bit integer
t1 = Add32(t3,t2) # addl
PUT(0) = t1 # put %eax
  • DynamoRIO:有跨平台和C++支持,但没有Pin支持的操作系统多;
  • Pin:有跨平台和C++支持,更少的性能开销。但没有  中间语言,可能写起来会更冗长一些。
最终我们选用Pin作为检测工具。分析的伪代码如下:
1: ins_operands= extractOperands(ins)
2: ins_dsts= extractDestinations(ins)
3: ins_srcs= extractSources(ins)

4: for idx ∈ len(insdsts) do
5: srcIndices = extractSources(ins, idx)
6: sources = vector()
7: for idx ∈ srcIndices do
8: sources.append(inssrcs[idx])
9: end for
10: ins_dsts[idx].sources= sources
11: end for

12: if setsEFlags(ins) then
13: eflags = eflagsWritten(ins)
14: PIN_InsertCall(AFTER, updateEflagsOperands(eflags, ins))
15: end if

16: if isConditionalBranch(ins) then
17: cond = getCondition(ins)
18: operands = getConditionOperands(eflagsRead(condition))

19: PIN_InsertCall(BRANCH TAKEN, addConditionalConstraints(cond, operands))
20: PIN_InsertCall(AFTER, addConditionalConstraints(!cond, operands))
21: else if writesMemory(ins) or writesRegister(ins) then
22: if writesMemory(ins) then
23: PIN_InsertCall(BEFORE, ins, checkWriteIntegrity(ins))
24: end if

25: PIN_InsertCall(BEFORE, ins, taintAnalysis(ins))
26: PIN_InsertCall(BEFORE, ins, convertToFormula(ins))
27: end if

28: instructionType = getInstructionType(ins)

29: if instructionType == ret then
30: PIN_InsertCall(BEFORE, ins, checkRETIntegrity(ins))
31: else if instructionType == call then
32: PIN_InsertCall(BEFORE, ins, checkCALLIntegroty(ins))
33: end if
  • Line 1-3:这部分的目的是解析寄存器和源操作数、目的操作数列表;
  • Line 4-11:虽然Pin提供了确定指令读和写的位置的功能,但为了准确的表示每条指令的语义还需要额外处理。从第 5 行开始使用 extractSources 函数获取影响目标的源索引向量处理。这个函数本质上是一个目标索引到源索引的映射,必须是我们希望处理的 x86 指令。第 7-9 行,遍历这些索引并从 inssrc中提取来源信息。第 10 行这个向量存储在当前目的地的源属性中;
  • Line 12-15:确定指令是否修改了 EFLAGS 寄存器,如果当前指令修改了 EFLAGS 寄存器,我们提取被修改的指令的索引。第 14 行使用 Pin 提供的函数插入对 updateEflagsOperands 函数的调用;
  • Line 16-20:处理条件分支指令,取决于 EFLAGS 中一个或多个索引值的指令记录。在这个算法中,我们只考虑条件指令的直接影响改变控制流程,即条件分支。x86 中的一些条件分支指令指令集有jljgjb等。第 17 行使用 getCondition 函数提取指令表达的条件。例如,如果指令是 jl,那么提取的条件即“小于”。第 18 行,检索最后设置该指令所依赖的 EFLAGS 索引的指令的操作数。例如,jl 指令检查符号标志是否等于溢出标志位。第 19 行,插入调用以将条件添加到我们的全局存储中,并在第 20 行插入调用以添加否定条件,然后将在运行时存储条件的正确版本,具体取决于走哪条路径;
  • Line 21-27:如果一条指令不是条件分支,那么我们检查它是否为写操作或者它是寄存器。如果指令是写操作,我们插入一个函数检查写入完整性。此函数将确定指令参数是否可被用户污染;
  • Line 28-33:对于直接修改 EIP 寄存器的指令,我们必须检查值是否可被污染。在执行指令 ret 和 call 之前Integrity 函数将进行检查,确定它们是否被用户输入污染。

污点分析

我们使用污点分析来确定被输入污染的内存地址和寄存器。污点分析是一个迭代过程,标记了一组初始内存地址和寄存器,如果被污染,则可以根据正在处理的指令语义,来在每个后续指令中添加和删除集合中的元素。这个概念也可以定义为标记:如果位置直接来自用户输入或另一个受污染的位置,则该位置被标记为污染。
我们可以用一种方法来表示目标是否被污染:使用两个不相交的集合,其中包含被污染和未被污染 和 的元素,给定一条指令  和一个内存位置或寄存器 ,当且仅当 $ xin i _dsts} ( y in i _{srcs} {yin T)xTi x in i _dsts}( y in i _{srcs} {yin T ){xin T}TTT$ 中删除。可以通过多种方式选择被污染的一组位置,例如,系统调用的目标缓冲区。
我们可以用以下算法来运作污点分析的基本功能:
1: for dst ∈ ins_dsts do
2: latticePos = TOP

3: for src ∈ dst.sources do
4: latticePos = meet(latticePos, L(src))
5: end for

6: L(dst) = latticePos
7: end for
  • Line 1-2:遍历每个目标操作数并计算其“格”位置,将格位置初始化为 TOP的一个临时变量来指示处理尚未发生的事件;
  • Line 3-5:对于每个目标地址,都会遍历影响其值的源列表。格目标地址的位置被计算为其所有源的格位置的交汇点。
  • Line 6-7:一旦找到当前目的地址的格位置,它就会被存储起来,并且处理下一个目的地址的指令。
现在我们将污点分析与决策结合,我们使用位向量来构建决策算法。位向量可以明显的表示电平状态,例如a * b 明显要比 a + b 的开销更大。
在其他的算法过程中求解是最大的瓶颈,而我们的算法只需要在诸多公式中找到一个满意的公式,就只需要专注于跟踪最简单指令执行,还有就是覆盖更多的路径来触发更多的错误。
我们将 x86 指令集分类为三类指令——赋值、线性算术和非线性算术,每个类别都会将以更大的复杂性引入位向量公式。然后修改上面的污点分析算法为下面这样:
1: insComplexity = getInsComplexity(ins)
2: for dst ∈ ins_dsts do
3: latticePos = TOP

4: for src ∈ dst.sources do
5: latticePos = meet(latticePos, L(src))
6: end for

7: if latticePos != untainted then
8: latticePos = meet(latticePos, insComplexity)
9: end if

10: L(dst) = latticePos
11: end for
  • Line 1:检索与当前指令类型相关的复杂性类。getInsComplexity 将希望处理的每条指令映射到新格表示分配、线性算术或非线性算术的位置;
  • Line 2:与前面的算法一样;
  • Line 3-6:与前面的算法一样;
  • Line 8-9:至此已经根据来源计算了目的地址的格位置。然后我们将该值与指令的格位置相交,以给出最终格目的地址的位置。我们需要检查源上的格位置是否受到污染,如果指令的源是未受污染的,那么指令的格位置都是未受污染的。

构建路径条件

构建路径条件进行在阶段一和阶段二之间。首先我们将解释什么是路径条件,然后描述如何将指令转换为操作数上的公式,最后解释如何将这些公式组合成一个路径条件。
以一个例子来观察将指令转换为公式的过程:
mov [12345], eax ; a = b
mov ebx, 10 ; c = 10
add ebx, [12345] ; d = c + a
mov edx, ebx ; e = d
它生成的路径公式如下:
现在我们添加约束,并使用决策分配过程来找到满意的输入。例如,要使edx在第四行包含值20,我们应当添加,可得公式:
使用上述逻辑的决策程序,我们可以求解公式得到结果 b = 10。由此我们可以确定,要使 edx 第 4 行包含值 20,eax 必须在第 1 行包含值 10。
构建路径条件的算法如下:
1: for idx ∈ len(ins_dsts) do
2: varId = generateUniqueId()
3: updateVarId(ins_dsts[idx]], varId)

4: rhs = makeFormulaFromRhs(ins, idx)
5: storeAssignment(varId, rhs)
6: end for
  • Line 1-3:分别处理指令的每个目标操作数,并从生成开始与该目的地关联的唯一名称;
  • Line 4-5:提取与目标 ins_dsts[idx] 关联的源,并在这些源上生成一个公式,描述指令对目的地址的影响。最后计算符号公式 varId = rhs 并将其存储以供之后处理。

第二阶段 构建漏洞利用公式

算法的第一阶段中,程序在运行和分析之间迭代,直到检测到漏洞。可以以如下四种方式进行检测:
  1. 对已知直接影响 EIP 的参数进行完整性检查失败:我们在运行时挂钩所有 ret 和 call 指令,并检查指针是否被污染;
  2. 对写入指令的目标地址和源值的完整性检查失败:与前一种情况一样,我们挂钩所有写入内存的指令来检测可能导致间接利用的漏洞;
  3. 操作系统的错误信号:如果发生内存损坏,则可能是程序试图读取、写入或执行此内存时,程序被操作系统终止,例如 SIGKILL、SIGABRT 或 SIGSEGV;
  4. 执行已知的“错误”地址:当发生潜在危险的内存损坏时,仅触发 libc 等库中的某些错误处理程序,例如堆分配检查时的错误输出函数。
在第二阶段里,当使用前面两种检测方式之一检测到错误时,可以断定指令处就是漏洞点。根据前面的定义,我们理想的漏洞利用公式是:。对于直接和间接漏洞利用,两者的共同点是控制敏感内存位置的值以及我们控制足够大的内存的连续缓冲区来注入我们的payload。伪代码如下:
1: if crashIns.type == WRITE then
2: exploitType = indirect
3: continueExecutionUntil(indirectControlIns)
4: else
5: exploitType = direct
6: end if

7: shellcodeBuffers = buildShellcodeBuffers(registers)
8: scBuf = None
9: for buf ∈ shellcodeBuffers do
10: if buf.size ≥ len(shellcode) then
11: scBuf = buf
12: break
13: end if
14: end for

15: if scBuf == None then
16: exit("No shellcode buffer large enough for shellcode")
17: end if

18: scConstraintFormula = buildFormula Shellcode(shellcode, scBuf)
19: ι = getTrampolineForRegister(scBuf.jmpRegister, trampolineAddrs)

20: if exploitType == direct then
21: eipControlFormula = buildFormula InsPointerControl(registers.ESP, ι)
22: else
23: dst = crashInsdsts[0]
24: src = dst.sources[0]
25: eipControlFormula = buildFormula WriteControl(dst, src, indirect mEIP, ι)
26: end if

27: eipAndScConstraints = createConjunct(eipControlFormula, scConstraintFormula)
28: exploitFormula = eipAndScConstraints

29: for varId ∈ eipAndScConstraints.variables do
30: pc = buildPathCondition(varId)
31: exploitFormula = createConjunct(exploitFormula, pc)
32: end for

33: return exploitFormula
  • Line 1-6:通过决定生成哪种类型的漏洞利用(直接或间接)开始算法。要是我们生成一个间接利用的漏洞,这个地址必须作为indirectControlIns 提供;
  • Line 7-14:处理在第一阶段收集的污染信息,生成可能存在利用的地址列表,遍历返回的 payload 所在的缓冲区,直到找到足够大的缓冲区来存储 payload;
  • Line 18:建立一个公式,选择缓冲区所需的约束等于提供的payload;
  • Line 19:根据我们决定使用的缓冲区和指向它的寄存器,选择地址列表中的跳板指令。这些指令可以有很多方法获得,比如Metasploit框架或者其他小工具;
  • Line 20-26:此时我们生成的公式将直接控制指令指针(直接利用)或控制写指令的源/目的操作数(间接利用);
  • Line 27-32:payload缓冲区公式与控制指令公式的结合,然后从程序输入为该公式中的每个变量构建路径条件;
  • Line 33:返回变量路径条件公式。

第三阶段 求解漏洞利用公式

我们可以使用决策程序来确定先前生成的公式是可满足的。如果是,我们将使用决策过程生成令人满意的变量分配给输入。如前所述,漏洞利用公式限制了其中出现的所有变量,除了对于那些直接被用户输入污染的变量,由决策过程产生的令人满意的分配,因此将指定用户可以控制的那些输入参数的值。这可以解析为 产生一个利用 。通过将  提供给程序 ,结果路径应该满足

4. 具体实现

自动化软件漏洞利用


在上一阶段介绍的算法大概有7000+行C++代码,接下来我们简单介绍一下如何使用Pin的功能进行实现。Pin的官方文档比较详细,多数API功能都可以参考官方文档,因此这里只是简单介绍一下如何实现。

二进制检测

使用Pin提供的功能可检测各种事件,包括线程创建、系统调用和指令执行。
  • 提取目的地址和源值,然后将这些传递给一个运行时分析例程,该例程查询对象来确定这些位置是否被污染。
  • 污点分析算法可挂钩与之相关的系统调用,例如readwriteopen。Pin允许我们在程序运行系统调用前插桩,这样就可以用于确定某些内存是否被我们的输入所污染。一旦一个位置被标记为受污染,指令级检测代码就可以传递污染信息,包含当前进程上下文。
  • 除了系统调用,我们还在线程创建和从操作系统接收到的信号上挂钩。在多线程应用程序中,我们有必要确定何时创建和销毁线程,以及在调用我们的分析例程时识别当前活动的线程。线程不共享寄存器,所以被其中一个线程污染的寄存器不应该被标记为被其他线程污染。当一个线程创建后,可以在污点分析引擎中实例化一个新对象,该对象表示其寄存器的污点状态;当线程被销毁时,该对象被删除。
  • 由于 Pin 不使用 ,因此要求我们对每条指令进行一些额外的处理以确定所需的模拟函数并提取指令操作数,具体操作方式可以查看官方文档。
  • Pin中的x86Simulator类包含我们希望分析的函数中的每条汇编指令。它包含一个或多个变体,具体取决于它的操作数类型。例如,一条 mov 指令有两个操作数,它可以从内存到到寄存器、从寄存器到寄存器或从寄存器到内存。
  • 可以通过检查某些指令的参数来检测潜在的漏洞。对于直接利用,我们要求 ret 指令中 ESP 寄存器指向的值被污染,或者调用指令使用的内存/寄存器被污染;对于间接利用,我们必须检查写目标地址是否被污染。
  • 记录操作数何时修改 EFLAGS 寄存器,然后在这些操作数上生成约束。当遇到条件跳转,检查 EFLAGS 寄存器是否在当前指令时被写入被修改寄存器列表中来检测指令是否写入 EFLAGS 寄存器。
  • 可以为每个条件跳转插入两个回调条件:一个在从真实条件产生的路径上,另一个为假条件的路径。

运行时分析

  • 检查读取的内存位置是否被污染,如果不是,则目标位置是未污染的。
  • 在运行时通过检测EFLAGS寄存器的位来监测程序执行流。
  • 通过分析函数当前 ESP 值以确定该地址处的内容是否为污染。如果发现内存位置或寄存器被污染,我们可以检索关联的对象,使用该位置并开始生成利用。
  • 检查可利用的写指令,要求目标地址和源地址被污染。因此,对于像 mov DWORD PTR [eax], ebx 这样的指令,我们必须检查 EAX 中的值是否为污染,而不是 EAX 指向的地址的内容。或者EBX 中的值被污染了。

生成利用

  • 为了构建漏洞利用公式,必须处理在运行时分析期间收集的信息。此阶段实现的工作主要是寻找合适的payload 缓冲区,和控制 EIP 所需的内容,并为所需的内容构建路径条件。为了建立这样一个公式,条件是发现需要更改哪些字节,然后构建它们的路径条件,附加指定的约束,对构成 payload 位置和劫持 EIP 而修改的位置执行此过程。
  • 对于直接利用,检索与该位置关联的对象ESP 寄存器或栈中指针。对于间接利用,我们提取对象与写入目标地址和源值相关联。将地址传递到我们在二进制检测阶段的分析例程。
  • 为了生成漏洞利用,我们需要运行所有可能的 payload。我们可以生成多个利用公式,然后对每个公式使用不同的payload。
  • 一旦构建了所有可能的 payload 缓冲区集合,我们就可以遍历寄存器并找到那些可以通过寄存器跳转指令访问的 payload 缓冲区。我们还处理列表,用于发现不受地址随机化影响的 payload 缓冲区。算法应该尽可能使用寄存器跳转指令访问到的缓冲区。
  • 在生成直接漏洞利用时,我们希望修改目标地址而不是该地址的值。可以间接的从寄存器、内存地址和常量的组合中得到最终的有效地址。

References

自动化软件漏洞利用


[MSc Computer Science Dissertation] Automatic Generation of Control Flow Hijacking Exploits for Software Vulnerabilities——Author: Sean Heelan

原文始发于微信公众号(山石网科安全技术研究院):自动化软件漏洞利用

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年3月30日16:23:59
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   自动化软件漏洞利用http://cn-sec.com/archives/855700.html

发表评论

匿名网友 填写信息