本文将会探索Linux程序混淆的基本技术和动机。通过对二进制文件进行混淆或者加密来保护二进制文件不被篡改的技术被称作软件保护。说到软件保护,指的是二进制保护或者二进制加固技术。二进制加固并不是Linux所独有的,事实上,Windows操作系统有许多关于二进制加固的产品,可供讨论的例子非常多。

许多人没有意识到的是,Linux在这方面也有一定的市场,尽管这方面的技术主要应用于政府使用的反篡改软件产品中。过去十年间,在黑客社区上也发布了许多ELF二进制保护软件,其中一些软件为目前正在使用的许多保护技术奠定了基础。

作为近期的ELF二进制保护技术书籍的作者,我可以任性地利用整本书来讲解软件保护的艺术,在本文中也可以轻松地对相关技术进行介绍。不过,我会对使用到的一些基本原理和有趣的技术进行解释,并对自己设计的二进制保护器——Maya's Veil进行深入的讲解。二进制保护相关的工程和技巧都比较复杂,这使得本文的主题比较难以表述,但我会尽力描述清楚。


1.1 ELF二进制加壳器


加壳器(packer)是一种恶意软件作者或者黑客常用的软件,用来对可执行文件进行压缩或加密,来对代码和数据进行混淆。一种常见的加壳工具UPX(​​http://upx.sourceforge.net​​)在大多数Linux发行版中都作为包的形式存在。这种加壳器的初衷是将可执行文件压缩成更小的体积。

由于代码被压缩,因此程序在内存中执行之前需要通过某种方式进行解压。这个过程就比较有趣,在下一节中会进行讨论。无论如何,恶意软件的作者肯定会意识到,被恶意软件感染了的文件在压缩后由于代码进行了混淆,便能够“躲过”杀毒软件的检测。于是,恶意软件的作者或者杀毒软件研究人员就会开发自动脱壳器,现在大多数杀毒产品中都使用了自动脱壳器。

如今,“加壳二进制文件”不仅指压缩了的二进制文件,也涵盖了加密过的二进制文件,以及使用了任意形式的模糊层进行屏蔽的二进制文件。自从21世纪初以来,一些比较著名的ELF二进制保护器引领了Linux二进制保护的未来。下面会对这些保护器进行介绍,并利用这些保护器对不同的ELF二进制文件保护技术进行建模。在这之前,先来看一下存根(stub)机制是如何加载并执行被压缩或者被加密过的二进制文件的。


1.2 存根机制和用户层执行


首先,我们需要知道,软件保护器实际上是由以下两个程序组成的。

  • 保护阶段的代码:应用到目标二进制文件上的保护程序。
  • 运行时引擎或存根:与目标二进制文件合并在一起,负责运行时反混淆和反调试的程序。

应用到二进制目标文件上的保护类型不同,保护器的程序就会不同。无论哪种类型的保护,都需要能够被运行时的代码所理解。运行时代码(或存根)必须要知道如何对一起合并的二进制文件进行解密或者反混淆。大多数的软件保护机制会有一个相对简单的运行时引擎与被保护的二进制合并在一起,其唯一目的就是对二进制文件进行解密,然后在内存中将控制权交由解密后的二进制文件。

这种类型的运行时引擎其实不是一种真正的引擎,我们一般称之为存根。存根通常在没有libc链接的情况下进行编译(如使用​​gcc-nostdlib​​​),也可以使用静态编译。虽然这种类型的存根比真正的运行时引擎简单,但其实它本身也是比较复杂的,因为它需要在内存中执行(​​exec()​​)程序——这里就涉及了用户层执行。我们在此要感谢grugq做出的贡献。

​SYS_execve​​​系统调用会加载并运行一个可执行文件,该系统调用通常用在​​glibc​​​封装器中,如​​execve​​​、​​execv​​​、​​execle​​​和​​execl​​​。在使用软件保护器的情况下,可执行文件是被加密的,在执行之前需要先进行解密。一个没有经验的黑客才会编码一个存根来解密可执行文件,并将其以明文的方式写到磁盘上,然后再去使用​​SYS_exec​​来执行这个可执行文件。不过原始的UPX加壳器就是这么做的。

有一种比较巧妙的方式,即在内存中对可执行文件进行解密,然后在内存而不是文件中加载并执行。由于这个过程可以使用用户层的代码来完成,因此我们将这项技术称为用户层执行。许多的软件保护器都实现了可以在用户层执行的存根。要实现一个用户层执行存根,所面临的挑战就是需要将段加载到指定的地址范围中,通常情况下这跟存根可执行程序本身所指定的地址范围是一样的。

只有ET_EXEC类型的可执行文件存在这个问题(因为它们不是位置独立的),要解决这个问题,可以使用定制的链接器脚本,通知存根将可执行段加载到一个指定的地址,而不是默认的地址。在第1章的链接器脚本一节(1.3.3节)中讲过这样的链接器脚本示例。


在x8632位机上,默认基址是0x8048000,而在x8664位机上,默认基址为0x400000。存根的加载地址不要与默认的地址范围冲突。例如,我最近写的一个链接脚本,将text段加载到了0xa000000。


图1-1直观地显示了加密过的可执行文件是如何嵌入到存根可执行程序的data段中,然后被封装起来的,因此存根也称为封装器。

图1-1 二进制保护器存根模型

  • 一个典型的存根会执行下面的任务:
  • 解密负载文件(原始的可执行文件);
  • 将可执行文件的可装载段映射到内存中;
  • 将动态链接器映射到内存中;
  • 创建栈(使用mmap);
  • 准备栈相关的信息(argv、envp、辅助向量);
  • 将控制权交由程序的入口点。


如果被保护的程序是动态链接的,则需要将控制权交由动态链接器的入口点,随后会交给可执行程序。


这种性质的存根实质上是一个用户层执行的实现,这种存根会加载并执行嵌入到存根本身中的程序,而不是去加载执行一个独立的可执行文件。


对用户层执行最初的研究和算法可以在Grugq的论文The Design and Implementation of Userland Exec中找到,详见​​https://grugq.github.io/docs/ul_exec.txt​​。


保护器示例

下面来看一下一个可执行程序在使用了我设计的简单的保护器之前和之后是什么样的。使用​​readelf​​命令来观察程序头,可以看到这个二进制文件中有我们希望在Linux动态链接可执行文件所具备的所有段:

$ readelf -l test
Elf file type is EXEC (Executable file)
Entry point 0x400520
There are 9 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R E 8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000008e4 0x00000000000008e4 R E 200000
LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
0x0000000000000248 0x0000000000000250 RW 200000
DYNAMIC 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
0x00000000000001d0 0x00000000000001d0 RW 8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 4
GNU_EH_FRAME 0x0000000000000744 0x0000000000400744 0x0000000000400744
0x000000000000004c 0x000000000000004c R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
GNU_RELRO 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
0x00000000000001f0 0x00000000000001f0 R 1

现在,对二进制文件运行保护器程序,然后观察之后的程序头内容:

$ ./elfpack test
$ readelf -l test
Elf file type is EXEC (Executable file)
Entry point 0xa01136
There are 5 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000a00000 0x0000000000a00000
0x0000000000002470 0x0000000000002470 R E 1000
LOAD 0x0000000000003000 0x0000000000c03000 0x0000000000c03000
0x000000000003a23f 0x000000000003b4df RW 1000

可以看到文件在被保护前后有诸多不同之处。入口点变成了​​0xa01136​​,只有两个可以加载的段,即text和data段。这两个段的地址与之前的加载地址也不一样。

这当然是因为存根的加载地址不能与嵌入其中的加密可执行文件的加载地址冲突,加密的可执行文件需要加载并映射到内存中。原始可执行文件的text段的加载地址为​​0x400000​​​。存根负责对嵌入其中的可执行文件进行解密,然后将解密的文件映射到​​PT_LOAD​​程序头所指定的加载地址中。

如果可执行文件的加载地址和存根的加载地址冲突了,被保护程序就无法正常工作。通常情况下,我们会先对ld所使用的现有链接器脚本进行修改定制,然后存根程序使用自定义的链接器脚本进行翻译。在这个示例使用的保护器中,我修改了链接器脚本的一行代码。

  • λ 下面是原始的代码行:
PROVIDE (__executable_start = SEGMENT_START("text-segment",

0x400000));

. = SEGMENT_START("text-segment",

0x400000

)
+
SIZEOF_HEADERS;
  • λ 下面是修改后的代码行:
PROVIDE (__executable_start = SEGMENT_START("text-segment",
0xa00000))
; . = SEGMENT_START("text-segment",
0xa00000
)
+
SIZEOF_HEADERS;

还可以看到,被保护的可执行文件的程序头中缺少了​​PT_INTERP​​​段和​​PT_DYNAMIC​​段。如果没有查看原始可执行文件的程序头,在不了解二进制保护的读者看来,这会是一个静态链接的可执行文件,因为看上去似乎没有使用动态链接。


要记住,原始的可执行文件经过加密后是嵌入在存根可执行文件中的,因此看到的程序头是存根的程序头,而不是正在保护的可执行文件的程序头。在许多情况下,存根本身不会使用许多编译和链接选项,本身不需要动态链接。一个比较好的用户层执行实现的主要特点就是能够将动态链接器装载到内存中。


正如我曾经提到的,存根是一个用户层执行,在对嵌入其中的可执行文件解密并映射到内存之后,存根还会将动态链接器映射到内存中。动态链接器在将控制权交给解密好的程序之前会处理符号解析和运行时重定位。


1.3 保护器存根的其他用途


除了作为用户层执行组件要对可执行文件进行解密并加载到内存之外,存根也有其他的用途。存根通常会开启反调试和反模拟模式,保护二进制程序,提高对程序调试或者模拟的门槛,从而增加逆向工程的难度。

第4章讨论过基于​​ptrace​​的反调试技术。这些技术能够阻止包括GDB在内的大多数调试器来追踪二进制程序。在本章后面的内容中,会对Linux二进制保护所使用的反调试技术进行总结。


1.4 现存的ELF二进制保护器


多年来,存在一些比较有名的公开发行或者地下发行的二进制保护器。我会对其中一些Linux保护器进行讨论,并对它们的功能特性进行概述。

1.4.1 DacryFile——Grugq于2001年发布

DacryFile是我较早了解的Linux二进制保护器(​​https://github.com/packz/binary-​​​ ​​encryption/tree/master/binary-encryption/dacryfile​​)。DacryFile比较简单,却比较巧妙,与ELF病毒寄生感染机制类似。许多保护器都是使用存根将加密好的二进制文件进行封装,而DacryFile的存根则是一个简单的注入到被保护的二进制文件中的解密程序。

DacryFile采用RC4加密算法,从二进制文件的​​.text​​节一直加密到text段的结尾。解密存根是一个用汇编语言和C语言写的简单程序,并不具备用户级执行的功能,只能够对加密过的代码进行解密。存根会被插入到data段末尾,跟病毒插入寄生代码的机制类似。执行程序的入口点会被指向存根,在执行二进制程序时,存根会对程序的text段进行解密,然后将控制权交给原始的程序入口点。


在支持NX bit的系统上,data段不能够存放代码,除非将data段的可执行权限标记显式地修改为:​​'p_flags |= PF_X'​​。


1.4.2 Burneye——Scut于2002年发布

许多人都说Burneye是第一个不错的Linux二进制加密示例。但是以今天的标准来看,这种说法有点牵强,不过Burneye的确带来了一些创新性的功能特性。其中一项特性即为3层加密,第3层是一个密保层。

密码会被转换为哈希和(hash-sum)类型,用于对最外层进行解密。也就是说,除非给定一个正确的密码,否则二进制文件就无法进行解密。另外一层,被称为指纹层,可以用来替代密保层。该功能可以通过某种算法产生一个密钥,这个算法带有被保护的二进制文件所在的当前系统的指纹特征。通过这种方式保护的二进制文件只能在当时进行保护的系统上进行解密,在其他的任何系统上都无法进行解密。

Burneye还有自我销毁机制,在运行一次后就会把二进制文件删除。将Burneye与其他保护器区别开来的一个特点是,Burneye是第一个使用用户层执行技术来封装二进制文件的保护器。从技术的角度来讲,John Resier的UPX加壳器第一次使用了用户层执行,不过,UPX通常被视作一个二进制压缩器,而不是保护器。 据说John将用户层执行的相关知识传给了Scut,可以参考Scut和Grugq发表的关于ELF二进制保护的文章,链接为​​http://phrack.org/issues/58/5.html​​。这篇文章对Burneye的内部工作原理进行了详细的文档描述,强烈推荐读者阅读。


一个名为​​objobf​​的工具,即目标文件混淆器(object obfuscator),也是Scut设计的。​​objobf​​将ELF32 ET_REL(目标文件)进行混淆后的代码就非常难以进行反编译,不过在功能上跟保护器是等价的。通过使用不透明分支技术和错位汇编技术,能够非常有效地阻止静态分析。


1.4.3 Shiva——Neil Mehta和Shawn Clowes于2003年发布

Shiva可能是目前最优秀的公开发行的Linux二进制保护器之一。Shiva的源码并没有发布,不过Shiva的作者多次在Blackhat这样的会议上对这个保护器进行过展示,也透漏了Shiva用到的许多相关技术。

Shiva主要用于对32位ELF可执行文件进行保护,并提供一个完整的运行时引擎(不只是解密存根),该运行时引擎可以在对文件进行保护的过程中帮助解密和反调试。Shiva提供了3层加密,最内层不会对整个可执行文件进行解密,它会一次解密1024字节大小的块,然后重新进行加密。

对于一个足够大的程序,在任意给定的时间内程序的破解度也不会超过1/3。Shiva另一个强大的功能就是其内置的反调试功能——Shiva保护器使用clone()技术在运行时引擎中生成一个线程,然后这个线程会去追踪父线程,同时父线程反过来去追踪生成的子线程。这种情况下就可以使用基于ptrace的动态分析了,因为一个进程(或线程)只能有一个追踪者。两个进程都在互相进行追踪,因此调试器就无法追踪到这个进程上了。


著名的逆向工程师Chris Eagle使用IDA的x86模拟器插件成功地破解了使用Shiva保护器的二进制文件,并在Blackhat上面对这一行为进行了展示。据说对Shiva的反编译过程是在3周之内完成的。

λ 作者的展示:


https://www.blackhat.com/presentations/
bh-usa-03/bh-us-03-mehta/bh-us-03-mehta
.pdf


λ Chris Eagle(破解Shiva的人)的展示:


http://www.blackhat.com/presentations/bh-
federal-03/bh-federal-03-eagle/bh-fed-03-
eagle.pdf

1.4.4 Maya's Veil——Ryan O'Neill于2014年发布

Maya's Veil是我在2014年针对64位ELF二进制文件设计的保护器。到目前为止,保护器仍处在原型阶段,还未公开发布,不过有好几个分支版本分布在不同的Maya项目中。其中一个版本为​​https://github.com/elfmaster/fast-cflow​​,在这一版本中只包含了漏洞防御利用技术,如保持控制流完整性。作为Maya保护器的发起人和设计者,我可以对保护器的内部工作细节进行阐述,主要是为了激发对这方面感兴趣的读者的好奇心和创造性。除了是本书的作者,我也是一个非常随和的人,因此,如果有关于Maya's Veil的问题,随时可以联系我。

首先,这个保护器只是作为用户层的解决方案进行设计的,也就意味着不会借助于内核模块的功能,同时还能对一个二进制文件进行保护,使其不被篡改,另外还有漏洞防御的特性。到目前为止,Maya的许多功能都被视作编译器插件,直接作用于已经编译好的可执行文件。

Maya极为复杂,要对Maya的内部工作逻辑进行详尽的文档描述才能够充分解释二进制保护这一概念,不过我会对Maya最重要的几个特性来进行总结。Maya可用来创建第一层、第二层或者第三层的二进制保护文件。在第一层,Maya采用了一个智能运行时引擎,该引擎会作为一个名为​​runtime.o​​的目标文件进行编译。

通过逆向text填充扩展技术(参考第4章)和重定位代码注入重链接技术,将上述的目标文件​​runtime.o​​​文件注入到内存中。从本质上讲,运行时引擎的目标文件是被链接到被保护的可执行文件上的。​​runtime.o​​目标文件非常重要,文件中存放了反调试、漏洞防御、使用了加密堆的定制malloc的相关代码,还有被保护二进制文件的元数据等信息。该目标文件大约有90%是用C语言编写的,10%是用x86汇编语言来写的。

1.Maya的保护分层

Maya有多个保护和加密分层。每额外加一层,就会增加攻击者对被保护文件脱壳的工作量,就能将安全级别提高一个等级。最外层是最有用的,可以防止静态分析,而最内层(第一层)只是对当前调用栈中的函数进行解密,在调用结束后重新进行加密。下面是对Maya每层的详细介绍。

(1)第一层

经过Maya第一层保护的二进制文件包含了原二进制文件中所有经过单独加密的函数。每个函数都会在调用的时候进行解密,在返回的时候重新进行加密。这是因为​​runtime.o​​拥有智能自动的自调试功能,能够密切监控进程的执行,并检测到文件何时被攻击或分析。

运行时引擎本身就使用了代码混淆技术(如Scut上的目标文件混淆工具)进行了混淆。用于对函数进行解密-重新加密的密钥和元数据存储在一个定制的​​malloc()​​​实现中,在定制的​​malloc()​​实现中使用了运行时引擎所创建的加密堆。这就增加了定位密钥的难度。第一层保护是第一步也是最复杂的保护级别,经过第一层保护的二进制文件具有智能、自动的自追踪功能,用于动态解密、反调试和漏洞防御。经过第一层保护的二进制文件布局简化图如图2-2所示。

图2-2 经过第一层保护的二进制文件布局简化图

(2)第二层

经过第二层保护的二进制文件与经过第一层保护的二进制文件类似,在第一层保护的基础上,对二进制文件所有的函数和节都进行了加密,以防止静态分析。这些节在运行时进行解密,如果能够输出进程的相关信息,那么这些节在解密的时候就会暴露一些特定的信息。不过要输出进程的相关信息,需要通过使用内存驱动来完成。因为​​prctl()​​​函数可以对进程进行保护,用户层通过​​/proc/$pid/mem​​来输出进程信息的这一操作就无法完成,同时也阻止了进程输出任何的core文件。

(3)第三层

经过第三层保护的二进制文件与经过第二层保护的二进制文件类似,在第二层的基础上,通过将第二层保护后的二进制文件嵌入到第三层的存根的data段中,从而增加了一层完整的保护。第三层存根的工作原理与传统的用户层执行类似。

2.Maya的nanomites特性

Maya's Veil有许多的功能特性,增加了反编译的难度。其中一项特性被称为nanomites。这项特性能够将原始二进制文件的特定指令完全删除,代之以垃圾指令或者断点。

当Maya的运行时引擎发现这样的垃圾指令或者断点的时候,就会去检查它的nanomites记录来确定本该执行的原始指令是什么。这些nanomites记录存放在运行时引擎的加密堆段中,因此对于一个逆向工程师而言,要获取这些信息并非易事。一旦Maya知道了最初的指令要完成的工作,就会使用​​ptrace​​系统调用来对原始指令进行模拟。

3.Maya的漏洞防御特性

与其他的保护器相比,Maya最突出的特点就是其漏洞防御特性。大多数保护器的目标是增加反编译的难度,而Maya能够对二进制文件进行加固,从而防止二进制文件内在的一些漏洞(如缓冲区溢出)被利用。具体来说,Maya能够使用嵌入在运行时引擎中的特定控制流完整性技术来对二进制文件进行加固,来防止ROP(Return-Oriented Programming,返回导向编程)。

被保护的二进制文件的每个函数的入口点和返回指令位置处都会使用​​int3​​​断点进行修改。​​int3​​断点会传输一个SIGTRAP信号触发运行时引擎,随后运行时引擎会执行下面的某项任务:

  • 对函数进行解密(匹配入口​​int3​​断点时);
  • 对函数进行加密(匹配返回​​int3​​断点时);
  • 检查返回地址是否被重写;
  • 检查​​int3​​断点是否是一个nanomite;如果是,对该处指令进行模拟。

第三个重要特性就是反ROP特性。运行时引擎会对一个存放了程序不同点的有效返回值的哈希映射进行检查。如果返回的地址是无效的,Maya就会跳出,漏洞攻击就会失败。

下面是软件代码的一个漏洞片段,专门用来测试并展示Maya的反ROP特性。

(1)vuln.c源码

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
/*
* This shellcode does execve("/bin/sh", …)
/
char shellcode[] =
"\xeb\x1d\x5b\x31\xc0\x67\x89\x43\x07\x67\x89\x5b\x08\x67\x89\x43\"
"x0c\x31\xc0\xb0\x0b\x67\x8d\x4b\x08\x67\x8d\x53\x0c\xcd\x80\xe8"
"\xde\xff"\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x4e\x41\x41\x41\x41"
"\x42\x42";
/*
* This function is vulnerable to a buffer overflow. Our goal is
to
* overwrite the return address with 0x41414141 which is the
addresses
* that we mmap() and store our shellcode in.
*/
int vuln(char *s)
{
char buf[32];
int i;
for (i = 0; i < strlen(s); i++) {
buf[i] = *s;
s++;
}
}
int main(int argc, char **argv)
{
if (argc < 2)
{
printf("Please supply a string\n");
exit(0);
}
int i;
char *mem = mmap((void *)(0x41414141 & ~4095),
4096,
PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED,
-1,
0);
memcpy((char *)(mem + 0x141), (void *)&shellcode, 46);
vuln(argv[1]);
exit(0);
}

(2)利用漏洞vuln.c的示例

来看一下如何利用漏洞​​vuln.c​​:

$ gcc -fno-stack-protector vuln.c -o vuln
$ sudo chmod u+s vuln
$ ./vuln AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
# whoami
root
#

下面使用Maya的​​-c​​选项来对vuln文件进行保护,即控制流完整性。然后对被保护的二进制文件进行漏洞攻击:

$ ./maya -l2 -cse vuln
[MODE] Layer 2: Anti-debugging/anti-code-injection, runtime function
level protection, and outter layer of encryption on code/data
[MODE] CFLOW ROP protection, and anti-exploitation
[+] Extracting information for RO Relocations
[+] Generating control flow data
[+] Function level decryption layer knowledge information:
[+] Applying function level code encryption:simple stream cipher S
[+] Applying host executable/data sections: SALSA20 streamcipher (2nd
layer protection)
[+] Maya's Mind-- injection address: 0x3c9000
[+] Encrypting knowledge: 111892 bytes
[+] Extracting information for RO Relocations
[+] Successfully protected binary, output file is named vuln.maya
$ ./vuln.maya AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[MAYA CONTROL FLOW] Detected an illegal return to 0x41414141, possible
exploitation attempt!
Segmentation fault
$

上面的示例展示了Maya在返回指令执行成功之前成功检测到了无效的返回地址​​0x41414141​​。Maya的运行时引擎会通过将程序安全退出来防止漏洞利用攻击。

Maya加强的另一个漏洞防御特性是relro(read-only relocation,只读重定位)。大多数现代的Linux操作系统都启用了这项功能。如若没有,Maya则可以通过使用包含了​​.jcr​​​、​​.dynamic​​​、​​.got​​​、​​.ctors(.init_array)​​​和​​.dtors(.fini_array)​​​节的​​mprotect()​​函数创建一个只读的页来对relro进行增强。Maya的其他漏洞防御特性,如函数指针完整性,尚在规划中,还未写入代码库。


1.5 下载Maya保护的二进制文件


对逆向工程感兴趣的读者可以从网站​​http://www.bitlackeys.org/maya_crackmes.tgz​​下载几个经过Maya' s Veil保护过的二进制文件的程序示例。上述链接包含了3个文件:​​crackme.elf_hardest​​、​​crackme.elf_medium​​和​​test.maya​​。