“Weird Machines” in ELF: A Spotlight on the Underappreciated Metadata

https://www.usenix.org/system/files/conference/woot13/woot13-shapiro.pdf

不经意计算软件利用开始于黑客在程序中植入一段shellcode,后面很快发现可以通过不包含任意代码的数据构造不经意执行。本文展示了ABI元数据也可以导致任意计算。本文提出了Cobbler,可以把图灵完整语言编译为ELF元数据,这些数据可以被运行时加载器(RTLD)执行。

介绍

应用安全研究的主要挑战已经从恶意代码模型转向恶意计算研究,即无需恶意代码,利用精心构造的数据执行恶意计算。

数据驱动的攻击研究开始于2000年左右。目的是只使用数据修改程序的控制流,这时目标程序就像虚拟机,数据就像字节码,这些字节码就会驱动虚拟机改变控制流或进行内存传输。例如ROP,SROP,DWARF攻击。

本文提出了一种元数据驱动的计算。本文通过Cobbler展示元数据也可以像ROP或代码那样可以利用,Cobbler可以修改元数据,并在RTLD中执行图灵完全计算,Cobbler产生可以被ld.so解释的元数据。Cobbler不会受ALSR影响,这就使得有机会隐藏无代码木马。

元数据

元数据促进了软件的组合性,适应性和多样性。现代大多数一般目的软件设计上都可以在很多平台执行。

元数据允许从相同代码构建的数据在内存中分布不同。强制攻击者处理目标的多样性。Forest指出元数据可以让系统针对攻击和漏洞更鲁棒,但是本文也展示了充足的元数据也是一个攻击面。

利用

Eli Fox-Epstein展示了HTML+CSS可以完成图灵计算

Todd L. Veldhuizen展示了C++模板可以完成图灵计算:VELDHUIZEN, T. L. C++ Templates are Turing Complete. http://ubietylab.net/ubigraph/content/Papers/pdf/CppTuring.pdf. Indiana University Computer Science.

防御

目前大多数防御方法无法防御元数据攻击,例如病毒分析工业界广泛应用的签名技术等,对于软件完整性检查技术,REcon 2012, Igor Gl ̈ucksmann展示了如何将一段代码注入到签名后的可执行文件中。

运行时链接和加载

软件加载如图1所示

  1. 利用exec创建新进程,内核会读取一些元数据,将可执行文件和ld.so(RTLD)映射到进程空间

  2. 转换到用户空间,对于ld.so来说调用RTLD_START(),加载各种库,并且在进入程序入口前对程序内存按照元数据打补丁。

RTLD加载可执行文件需要的运行时库时,对于每一个ELF对象会维护一个link_map结构,包含如下信息

  • 对应的ELF文件名

  • ELF对象加载的基址

  • ELF对象动态表入口的虚址

  • 指向其他link_map的指针,这样所有的link_map就可以构成双向链表

由于双向链表结构,只要可以定位到一个link_map,就可以定位所有link_map,符号解析器会便利所有link_map找到所需信息

ELF元数据

元数据是编译器、链接器和加载器沟通的渠道。使用元数据的目的是指出代码的各种属性,例如在哪种机器上运行,加载器应该patch哪里来放置运行时库

ELF文件都包含一个头,用Elf64_Ehdr表示。包含ELF文件类型,架构,以及链接器和加载器所需信息。

ELF节和元数据表

ELF文件包含多个元数据表,每一个表包含在一个节中。ELF头包含如何定位这些表的信息。每个表包含在一个数据结构中,例如Elf64_Sym

  • 动态表:Elf64_Dyn,包含tag和data域,tag指出结构体剩下的内容是什么。动态表用于指出结构体包含什么,如何解析。

  • 符号元数据:指出了运行时需要定位的函数和数据Elf64_Sym

    包含了指向符号名、符号类型、和符号本身的指针。STT_IFUNC指出了要使用的函数版本

  • 重定位元数据:告诉链接器和加载器哪个虚址应该patch,如何patch。包含两个重定位表,一般是懒加载

    • .rela.plt

    • .rela.dyn:其中的符号都被编码成下标,写入Elf64 Rela’s r info中的.dynsym

    三种重定位方法

实现

ELF符号和重定位入口允许代码重用和代码兼容,但是也可以用来构造其他类型的计算。作者实现了一个基于此的brainfuck。

元语

实现了mov,jnz,add指令。重定位组成了字节码,符号元数据作为寄存器。包含内联数据。

支持四种寻址

  • 立即数

  • 直接

  • 寄存器

  • 寄存器间接

move

mov <dest>, <value>

利用重定位入口

立即数寻址:mov *0xbeef0000, $0x04-->{type=RELATIVE, offset=0xbeef0000, symbol=0, addend=0x04}

间接寻址:mov *0xbeef0000, [%foo]-->{type=COPY, offset=0xbeef0000, symbol=foo, addend=0}

memcpy: mov *0xbeef0000, qword ptr *0xb00000000-->{name=foo,value=0xb0000000, type=FUNC, shndx=1,offset=0xbeef0000, size=8}

add

add <dest>, <addend1>, <addend 2>

其中dest是直接寻址,addend1是寄存器,addend2是立即数

add *0xbeef0000, %foo, $0x02 --> {type=SYM, offset=0xbeef0000, symbol=foo, addend=2}

符号可以如下表示

{name=foo, value=1, type=FUNC, shndx=1, size=8}

jnz

jnz <dest>, <value>

dest是立即数,value是直接寻址

要实现jnz,需要逐步实现jmp指令

如下是RTLD处理reloaction的代码

while (lm != NULL) {
    r = lm->dyn[DT_RELA];
    end = r + lm->dyn[DT_RELASZ];
    for (r ; r < end; r++) {
        relocate(lm, r, &dyn[DT_SYM]);
    }
    lm = lm->prev;
}

即遍历link_map,并且寻找位置和大小

实现jmp指令需要如下几步

  1. 设置lm->prev,以便下次处理相同的重定位表,并且后面需要恢复原来的lm->prev

  2. 设置lm->dyn[DT RELA]指向目标地址

  3. 更新lm->dyn[DT RELASZ]的大小反映新的重定位表大小

  4. 设置目标位置的下一个为end,防止RTLD处理下一个重定位入口

第一步需要如下指令获取全局偏移表 mov *<addr of prev>, $<addr of link_map>

第二步和第三步需要动态表的虚地址mov *(<address of DT_RELA>), <address of next relocation entry to process>}

第四步通过_dl_axuv构造end

mov *<addr of next Rela’s offset>, %sym-end
mov *<(value overwritten)>, $0

条件跳转通过IFUNC类型进行,RTLD如下处理IFUNC类型

  • 如果shndx域非0,则是间接函数,并指向函数的返回值

  • 如果为0,则value直接使用

则只需要寻找间接的返回0的函数(在ld.so重很常见的gadget),设为sym-zero,条件跳转如下实现

mov *<addr of sym-zero shndx>, $<test val>
add *<addr of end>, %sym-zero, $0

定位库函数

给定link_map,使用如下四步

  1. 解引用link_map,得到link_map的基址

  2. link_map+0x18,得到next域的地址

  3. 重复1-2步知道到达想要的域

  4. 解引用得到link_map基址

  5. 拷贝link_map开头的值到一个寄存器便于后续使用

假设sym-lm为寄存器

  1. mov *<address of sym-lm’s value>, [%sym-lm]

  2. add *<address of sym-lm’s value>, %sym-lm, $0x18

  3. mov *<address of sym-lm’s value>, [%sym-lm]

  4. mov *<address of sym-lm’s value>, [%sym-lm]