所以我参考这篇论文:
二进制搅拌:遗留x86二进制代码的自随机指令地址
https://www.utdallas.edu/~hamlen/wartell12ccs.pdf
代码与数据交织:出于性能原因,现代编译器在PE和ELF二进制文件中的代码段内积极地交织静态数据。在编译的二进制文件中,通常没有区分数据字节与代码的方法。无意中随机化数据和代码会破坏二进制文件,给指令级随机化器带来了困难。可行的解决方案必须以某种方式保留数据,同时随机化所有可到达的代码。
但我有一些问题:
出于性能原因,现代编译器积极地在PE和ELF二进制文件中的代码段内交错静态数据
需要引文!根据我对GCC和clang等编译器的经验,以及从MSVC和ICC查看asm输出的一些经验,这对于x86来说简直是错误的。
普通编译器将静态只读数据放入section .rodata
(ELF平台)或section .rdata
(Windows)。 .rodata
部分(和.text
部分)作为文本段的一部分链接,但整个可执行文件或库的所有只读数据都组合在一起,并且所有代码分别组合在一起。 What's the difference of section and segment in ELF file format
在同一页面中混合代码和数据具有接近于零的优势,并且浪费了代码字节上的数据-TLB覆盖,并且浪费了数据字节上的指令-TLB覆盖。在64字节高速缓存行中也是如此,以浪费L1i / L1d中的空间。唯一的优势是统一缓存(L2和L3)的代码+数据位置,但通常不会这样做。 (例如,在代码获取将一行引入L2之后,从同一行获取数据可能会在L2中获取而不必从另一个缓存行中获取数据到RAM。)
但是,对于拆分的L1iTLB和L1dTLB,以及L2 TLB作为统一的牺牲缓存,x86 CPU并未针对此进行优化。在获取“冷”功能时,iTLB未命中在从现代Intel CPU上的同一缓存线读取字节时不会阻止dTLB未命中。
x86上的代码大小没有任何优势。 x86-64的PC相对寻址模式是[RIP + rel32]
,因此它可以处理当前位置的+ -2GiB内的任何内容。 32位x86甚至没有PC相对寻址模式。
也许作者正在考虑ARM,附近的静态数据允许PC相对负载(具有小偏移)将32位常数输入寄存器? (这在ARM上称为“文字池”,您可以在函数之间找到它们。)
我认为它们并不意味着立即数据,如mov eax, 12345
,其中32位12345
是指令编码的一部分。这不是用加载指令加载的静态数据;即时数据是一个单独的事情。
显然它只适用于只读数据;在指令指针附近写入将触发管道清除以处理自修改代码的可能性。而且你通常想要你的内存页面使用W ^ X(写或exec,而不是两者)。
以及CPU如何区分代码和数据?
增量。 CPU在RIP处获取字节,并将其解码为指令。在程序入口点开始后,执行继续执行分支,并通过未采用的分支等进行。
从架构上讲,它不关心当前正在执行的字节,或者是由指令加载/存储为数据的字节。最近执行的字节将在L1-I高速缓存中保留,以防它们再次需要,并且对于L1-D高速缓存中的数据也是如此。
在无条件分支或ret
之后立即拥有数据而不是其他代码并不重要。函数之间的填充可以是任何东西。可能存在罕见的极端情况,如果数据具有某种模式,数据可能会停止预解码或解码阶段(例如,因为现代CPU以16或32字节的宽块获取/解码),但CPU的任何后续阶段都是只查看来自正确路径的实际解码指令。 (或者是对一个分支的错误推测...)
因此,如果执行到达一个字节,那么该字节是指令的一部分。这对于CPU来说完全没问题,但是对于想要查看可执行文件并将每个字节分类为/或的程序没有帮助。
代码获取总是检查TLB中的权限,因此如果RIP指向非可执行页面,它将会出错。 (页表条目中的NX位)。
但就CPU而言,确实没有真正的区别。 x86是冯·诺依曼架构。如果需要,指令可以加载自己的代码字节。
例如movzx eax, byte ptr [rip - 1]
将EAX设置为0x000000FF,加载rel32 = -1 = 0xffffffff位移的最后一个字节。
考虑到代码部分是可执行的并且CPU可能错误地将恶意数据作为代码执行,这对于安全性是非常不好的吗? (也许攻击者将程序重定向到该指令?)
可执行页面中的只读数据可用作Spectre小工具,也可用作面向返回编程攻击的小工具。但通常在实际代码中已经有足够的这样的小工具,这不是什么大问题。
但是,是的,这是对此的一个小反对,这实际上是有效的,不像你的其他观点。