伪造面向对象编程——COOP

 

C和C++向来以“let the programmer do what he wants to do”的贴近底层而为广大开发者所喜爱。语言对开发者行为的较少限制,就使其成为不安全的语言。针对C和C++程序的控制流劫持攻击,如ROP、JOP等已经在长期的实践中证明了其破坏力,而众多保护措施也已经被提出。攻击者要么是将控制流转向其注入的恶意代码,要么是借由代码重用攻击来恶意地重用进程空间中已有的代码片段。无论是已有的攻击,还是相应的防御措施,往往都没有,或者是很少考虑C++的自身语言特性,包括其面向对象的特性。而现如今许许多多的应用程序都是通过C++开发,或者是包含部分C++代码,如Microsoft Internet Explorer,Google Chrome, Mozilla Firefox, Adobe Reader, Microsoft Office, LibreOffice, 和openJDK等等。因此,针对C++语言特性的攻击很可能造成巨大的破坏。接下来,我就为大家介绍一下针对C++特性的攻击——伪造面向对象编程(COOP)。

伪造面向编程(counterfeit object-oriented programming ,以下简称COOP)是由Felix  Schuster等人于2015年提出来的一种主要针对C++语言特性的攻击方式。C++提供了面向对象的特性,如类、方法以及虚函数等。而COOP就利用了C++程序中的虚函数都要进行取地址操作(因为要维持一张虚函数表),而这就意味着有一个存在一个不变的指针来对应一个虚函数,同时也会使得C++程序中相比同样体量的C程序中存在更多的进行取地址操作的函数。下面我具体给大家介绍一下。

 

前期知识:二进制层面的C++虚函数

对于每个至少包含一个虚函数的对象来说,在其内部偏移0处都会存在一个相应的指针,通常被称为vptr。如下图所示:

可以看到,类A不含虚函数,那么它的内存布局中就没有vptr指针和虚函数表。类B则恰好相反,在其内部偏移0处存放有vptr指针,指向虚函数表。

而调用一个虚函数通常会对应类似以下的汇编指令:

rcx寄存器存储着this指针,通过取内容将vptr指针存入rax寄存器,然后再通过相应的偏移量(此处是8,但有可能是其他数)读取虚函数表,取出对应虚函数地址进行调用。

 

攻击假设

COOP对攻击者的能力做了如下假设:

  1. 攻击者控制了一个包含虚函数的C++对象.
  2. 攻击者能够推断出一个他已经知道内存布局(至少是部分知道)的C++模块基址。

对于第一点来说,攻击者只要利用邻近该对象的某处溢出漏洞或者是use-after-free漏洞(正如后续某些攻击时所提到的那样)。

而对于第二点来说,一个公开的C++库就能符合要求。

 

攻击目标

COOP的设计初衷是为了达成以下目标:

(1)不出现已有代码重用攻击的特征,包括:

  1. 不会间接跳转(通过call 或者是jmp指令)到没有被取地址(被取地址的通常包括函数头等)的内存位置。
  2. 不会不经调用栈(call stack)执行return操作。
  3. 控制流中不出现过多的间接跳转。
  4. 不会劫持栈上指针。
  5. 不会注入新的或者是利用已有的代码指针(返回地址或者是函数指针)。

(2)使控制流和数据流尽量与一般的C++代码相似。

(3)能被广泛地用于攻击C++程序。

(4)在真实情景下实现图灵完备(Turing complet,具体定义比较复杂,可以自行了解,简单的说就是能够实现条件分支,循环、读写等操作)。

 

攻击实施步骤

为了更好地理解COOP攻击模式,在介绍COOP具体攻击实施步骤时,将会使用简单的代码来进行介绍,主要涉及以下几个类:Student、Course和Exam。

(1)劫持C++对象

每一次COOP攻击都要以劫持目标C++对象开始,称为initial  object。这一步是为了是控制流转向下一步伪造的对象当中。比如可以劫持以下类Course:

students是一个指向数组的指针。其中的Student类定义如下:

(2)伪造对象

在上一步操作之后已经可以使控制流发生改变,接下来要做的就是伪造包含有攻击者选定的vptr指针和一些数据域的对象。伪造的对象并不是目标程序自身有的,而是由攻击者注入到程序进程空间中的。伪造的对象和必要的一些数据将会作为连续的一个内存块(chunk)被注入到攻击者控制的某片内存区域。注入伪造对象后的内存布局如下图所示:

其中的object0和object1就是伪造的对象,而initial object(也就是上文提到的Course类)已经被劫持,students数组各成员已经指向相应的伪造对象,nStudents已经被设置为伪造对象的数量。接下来要做的就是通过各个vptr指针调用攻击者选定的虚函数了。

(3)调用虚函数

通过initial object和伪造对象,相应的vptr指针和需要的数据已经准备好,接下来就是调用攻击者选定的虚函数执行相应操作了。值得注意的是,由于COOP的目标之一是使控制流和数据流尽量与一般的C++代码相似,所以各个vptr指针应该指向实际存在的虚函数表(理想情况下应该是虚函数表开头)。与ROP中的gadgets类似,COOP中的目标虚函数被称为vfgadgets,从其功能上来看,具体可以分为以下几类:

  1. 主循环函数ML-G。

ML-G是包含有以指向伪造对象指针为循环变量的循环的虚函数,比如前述Course类中的˜Course函数:

该函数会在循环过程中访问每个students数组成员,而该数组已经指向伪造对象,也就是说,通过该循环能将控制流转向伪造对象。

现实当中也有相应的例子,比如VS 2013 agents.h中:

  1. 算数或逻辑运算函数ARITH-G。

如Exam类中以下虚函数:

  1. 内存读/写函数W-G/R-G。

如涉及字符串读写等函数。如以下函数:

  1. 调用函数指针函数INV-G。

如以下函数:

  1. 条件写函数W-COND-G。
  2. 带有initial object的数据作为参数的主循环函数ML-ARG-G。

  1. 写入第一个参数指针指向地址的函数W-SA-G。

  1. 只调整栈指针而无其他操作的函数MOVE-SP-G。
  2. 64位下读取参数寄存器rdx,r8,r9函数LOAD-R64-G。

有了这些类型的vfgadgets,COOP的整个流程就比较清晰了:从initial object的vptr指针开始,调用第一个虚函数进入到主循环函数ML-G(如果选定的主循环函数有参数的话就是ML-ARG-G),然后循环地通过各个伪造对象的vptr指针去调用对应的由攻击者选定的虚函数。整个流程如下图所示:

其中各个数字代表执行顺序。而为了实现不同的操作,就需要以上几类vfgadgets的协同,这就是接下来要介绍的。

(4)不同操作的实现

正如前述所讨论的那样,通过主循环函数ML-G ,任意数目的虚函数可以被执行。而通过不同vfgadgets的配合,可以实现以下操作:

  1. 任意写。

考虑如下Examl类:

其各个元素在内存中排布如下:

可以看到,SimpleString::set(object1)的buffer指针和Exam(object0)的score是重叠的。而buffer指针是字符串复制的目标地址,在此基础上,通过操纵score的值,实际上buffer的值也就能够控制,进而任意写的地址任意性就达成了,接下来就要解决源数据(此处的字符串s)。而注意到此处的字符串是传入的参数,那么接下来要解决的就是传参问题了。以上在内存中构造重叠的对象是COOP很常用的手法。

  1. 传递参数。

参数传递和具体的系统有关,主要有:

(A)windows x64

windows x64平台下函数的前四个参数由rcx,rdx,r8和r9四个寄存器来传递,更多的参数则通过栈来传递。特别的,对于C++程序来说,this指针通过rcx寄存器来传递。其他三个寄存器通常会作为临时寄存器使用。考虑下列64位下读取参数寄存器vfgadget:

其对应汇编指令如下:

相当于进行了如下操作:

因此,只要攻击者合理地选取对应偏移量为10h和18h的数据,就能对应的改变相应寄存器的值,进而通过这些寄存器为某个目标函数传参。

(B)Linux x64

Linux x64使用了6个寄存器来传递前六个参数,利用原理与windows x64是相同的,但因为有更多寄存器,所以更加简单。

(C)windows x86

windows x86下,this指针通过ecx寄存器传递,其他参数通过栈来传递。这个时候就要以下列带参数的主循环函数ML-ARG-G来传递参数。

其对应汇编指令为:

其中第9-22行进行了如下操作:

使用下列写入第一个对参数指针指向地址的vfgadget,就可以实现对以上arg0的控制,进而执行传参操作。

或者也可以借由内存写vfgadget来进行。

(D)Linux x86

该平台下的传参均通过栈来进行,所以使用带参数的主循环函数ML-ARG-G来传递参数。

以上均是只同时传递一个参数,要想同时传递多个参数,就有必要做栈平衡。下图展示了不同参数个数的栈情形:

栈平衡操作可以使用MOVE-SP-G类型的vfgadget来完成,最终达到的效果就是参数被逐个堆积到栈上,如下图所示:

  1. 调用API函数

要调用API函数,攻击者可以直接将伪造对象的vptr指针指向已加载模块的IAT表或者是EAT表,这二者均存有API函数信息。如果遇到权限问题,则可以用this指针作为参数调用VirtualProtect()函数设置即可。

又或者攻击者可以使用调用函数指针的vfgadget——INV-G来调用API函数。

  1. 实现条件分支和循环

如果选定的变量没有存储在栈上,那么下列条件写vfgadget——W-COND-G就能实现条件分支。

如果变量在栈上,MOVE-SP-G类型的vfgadget也可以被用作分支。

循环的实现也是类似的。

 

工具框架

为了完成COOP攻击,就要完成以下两个任务:

  1. 寻找目标vfgadgets

为了识别应用程序中有用的vfgadget,搜索时仅依赖于二进制代码以及可选的调试符号。使用IDA将目标C++模块反汇编,C ++模块中的每个虚函数都被视为潜在的vfgadget。使用调试符号来静态标识C ++模块中的所有虚函数表。如果没有该符号以标识虚函数表,就使用启发式的方法:把所有带有地址信息的函数指针数组作为一个潜在的虚函数表。检查所有已识别的虚函数,通常只将具有一个或三个基本块的虚函数作为潜在vfgadget。唯一的例外是ML-G和ML-ARG-G,由于带有循环,它们通常由更多基本块组成。

搜索时,每一个基本块用静态单赋值形式(static single assignment form,通常简写为SSA form或是SSA,是中间表示IR的特性,每个变数仅被赋值一次)进行总结以反映其I/O行为特征。依赖于METASM二进制代码分析工具包的backtracking功能,在基本块级别进行符号执行(symbolic execution)操作。接下来就是对潜在vfgadgets基本块的SSA表示应用过滤器。例如 “赋值的左侧不能引用任何参数寄存器;右侧不能引用this指针”能识别64位系统下的内存写vfgadget——W-G。

  1. 构造重叠的伪造对象

正如前述所说的,构造重叠的伪造对象对于COOP很重要。具体实现时,攻击者定义伪造的对象和标签。可以将标签分配给伪造对象内的任何字节。当为不同对象中的字节分配相同的标签时,应确保将这些字节映射到最终缓冲区中的相同位置,同时确保具有不同标签的字节被映射到不同的位置。这些约束通常可以满足,因为伪造对象中的实际数据通常很少。

比如,伪造的对象A仅仅在内部偏移+0处有vptr指针,偏移+16处有个整数,偏移+136处有标签X。伪造对象B只有vptr指针,内部偏移+8处有相同标签X。那么,B+8就可以映射到A+136处。

 

实际攻击

COOP进行了以下几个POC:

1.64位IE 10

攻击64位IE 10使用了21个伪造对象,其中8个是重叠的。另外使用了mshtml.dll中8个不同的vfgadgets。攻击有两条执行路径:a.弹出计算器和b.打开画图。以下是攻击使用的vfgadgets:

2.32位IE 10

攻击32位IE 10使用了22个伪造对象,其中6个是重叠的。另外使用了mshtml.dll、ieframe.dll、Jscript.dll等三个动态链接库中11个不同的vfgadgets。攻击调用了WinExec()弹出了计算器。以下是攻击使用的vfgadgets:

3.64位 Firefox 36.0a1

攻击使用了9个伪造对象,其中2个是重叠的。另外使用了libxul.so中5个不同的vfgadgets。攻击执行了system(“/bin/sh”)。以下是攻击使用的vfgadgets:

4.其他人进行的COOP攻击

链接为翻译,原文在链接都有相应链接。

使用最新的代码重用攻击绕过执行流保护(https://bbs.pediy.com/thread-217335.htm

 

防御COOP

由于COOP主要针对C++语言特性,所以较好考虑了C++语言特性的防御措施都能比较好的地防御   COOP攻击。COOP提出者在后续论文里也提出了表随机化(Table Randomization)来对抗COOP。

(完)