静态链接可执行文件的ASLR保护机制

2019-04-04 约 12030 字 预计阅读 25 分钟

声明:本文 【静态链接可执行文件的ASLR保护机制】 由作者 Edvison 于 2019-01-05 09:30:00 首发 先知社区 曾经 浏览数 1949 次

感谢 Edvison 的辛苦付出!

本文是《ASLR PROTECTION FOR STATICALLY LINKED EXECUTABLES》的翻译文章

1 简介

本文提供了对静态链接可执行文件更加隐蔽的安全漏洞的见解,包括但不限于以下内容:

  • 静态链接可执行文件的glibc初始化代码
  • 对于静态链接的可执行文件,攻击面看起来是什么样的
  • 为什么像RELRO和ASLR这样的缓解对于静态链接的可执行文件和动态链接的可执行文件同样重要
  • 关于RELRO,ASLR和静态可执行文件的常见误解:静态链接会禁用重要的安全缓解,使程序容易受到攻击

目前,只读重定位(RELRO)是一种安全保护(在2.3节中讨论),对静态链接的可执行文件至关重要。即使是杰出的ELF二进制工程师也没有发现这种RELRO保护措施(于静态链接文件中)的存在,并且这种措施还被误认为对于静态链接的可执行文件并不重要。正如我们将通过一些RE示例演示的那样,静态和动态链接的可执行文件具有相同的攻击面。第二个问题是ASLR不能应用于静态可执行文件,但是像RELRO一样,现在是一个重要的安全保护措施。在这篇文章中,我们深入探讨了针对静态可执行文件使用RELRO和ASLR的细节,以及每个问题的解决方案。然后提供一个POC供读者推断或用于保护静态可执行安全风险,直到得出一个更完整的解决方案为止。

静态二进制文件通常用于常见的现成软件(COTS),原因有很多种,例如避免依赖动态库版本的兼容性问题。这忽略了静态链接可执行文件的安全性后果,部分原因是某些脚本(例如checksec.sh [3])错误地报告从而启用了部分RELRO的静态链接可执行文件。

本文的目的是为软件开发过程的安全性做出贡献,这个开发的生命周期一般应用于越来越多的支持ELF二进制格式的操作系统(OSs:operating systems)。静态可执行文件现在比以往任何时候都更像是一个主要目标,正如我们将在3.1节中看到的那样,RELRO和ASLR使得利用更难以实现。 如果某个软件存在内存损坏漏洞,则启用适当的二进制保护可能会完全阻止漏洞利用。例如,在攻击者只有一个指针宽度写入原语的情况下,没有mprotect@PLT将RELRO段标记为可写,这时RELRO会使漏洞无法利用。如上所述,我们将在3.1节中更详细地探讨这一点。

2 标准ELF安全保护的状态

多年来,glibc(GNU C库),GNU链接器和动态链接器都进行了大量改进,这使得各种安全保护成为可能,包括完整的ASLR,这需要修改编译器和链接器。Pipacs是一个备受尊敬且多产的PaX安全研究工程师,从ASLR到PAGEEXEC [4] ,他已经开发了许多用户空间和核心空间的保护技术。

Pipacs的一个发现是,除了随机化堆,堆栈和共享的地址空间之外,还可以随机化ELF可执行文件本身的地址空间,从而使ret2plt和ROP攻击更加困难。他还提出了两种解决方案:首先是RandExec [5],然后是更优雅的解决方案就是,引导PIE(位置无关的可执行文件)文件。

引导PIE文件背后的想法是,您可以创建一个ELF可执行文件,使其具有与共享库对象相同的属性:位置无关代码(PIC)和基本地址0x0,使内核可以在运行时重定位二进制文件。常规共享库和PIE可执行文件之间的唯一区别是初始化代码,并且可执行文件必须具有PT_INTERP段来描述动态链接器的路径。常规可执行文件具有ELF文件类型ET_EXEC,而PIE可执行文件具有ET_DYN文件类型,共享库也是如此。PIE可执行文件使用IP相对寻址模式来避免硬编码对绝对地址的引用。一个ELF ET_DYN且基址为0x0的程序可以在每次运行时随机重定位到不同的基址。

2.1支持静态PIE的相关工作

在写完本文的大部分内容之后,我发现了一些可以尝试将静态PIE可执行文件放入主函数线的方法。但是,据我所知,这仍然没有标准选项。我还发现有一个补丁可以添加静态PIE选项。有关的详细信息请参阅[6]和[7],这是我在查找资料时发现的两个论坛帖子。

2.2什么时候ASLR是完整的?

当可执行文件在高权限状态下运行时,例如sshd,在理想情况下,它将被编译并链接到PIE可执行文件中,该PIE可执行文件可以在将运行时将重定位放入随机地址空间,从而将攻击面强化为更加恶劣的游戏场。以root身份运行的敏感程序永远都不应构建为静态链接,并且在绝大多数的情况下应始终启用所有可用的二进制保护。

不使用PIE二进制文件的一个原因是IP相对寻址会影响各个级别的程序性能,如Red Hat团队的这篇有趣的论文所述[8]。偶尔会出现边缘情况,其中必须关闭保护措施,例如-fstack-protector,以便启用自定义保护。但一般情况下,应尽可能为敏感程序启用从金丝雀到ASLR和完整RELRO的所有内容。例如,sshd几乎总是在启用所有保护的情况下构建的,包括完整的ASLR,这意味着sshd是作为PIE可执行文件构建的。尝试运行readelf -e /usr/sbin/sshd | grep DYN,你会看到sshd(最有可能)以这种方式构建,尽管有一些例外,具体情况取决于架构。

2.3 RELRO入门

现在让我们来看看另一个鲜为人知的安全保护措施。RELRO是一种具有两种模式(部分和完全)的安全保护技术。在默认情况下,只执行部分RELRO,因为它使用缓慢链接(lazy linking),而完整RELRO需要精确链接(strict linking)[1]。精确链接与缓慢链接相比,在程序加载时间上更低效,因为动态链接器在精确链接中而不是按需绑定/重新定位。但是,通过将数据段中的特定区域标记为只读,即.init_array,.fini_array,.jcr,.got,.got.plt部分,完整的RELRO可以非常有力地强化攻击面。.got.plt部分和.fini_array是攻击者最常见的目标,因为它们分别包含共享库例程的函数指针和析构函数例程的函数指针。

3静态链接可执行文件和安全性的整体视图

开发人员经常使用静态链接的可执行文件,因为它们更易于管理,调试和发布; 一切都是自足的。用户遇到静态链接可执行文件问题的可能性远小于动态链接可执行文件,因为后者需要许多甚至有时数千个的动态库依赖项。作为这个领域的专业研究人员,我已经意识到静态链接的ELF可执行文件的更明显的优点和缺点,但我认为它们不会遇到与动态链接的可执行文件相同的ELF安全问题。事实上,令我惊讶的是,我发现静态链接的可执行文件容易受到许多与动态链接的可执行文件相同的攻击,包括3.1节中详述的那些:

3.1 RELRO保护的攻击点

  • 恶意软件的共享库注入
  • Dtors(.fin_array)中毒(注意:仅与某些静态链接的可执行文件相关)
  • Got.plt中毒(即GOTPLT劫持)

3.2为什么静态链接可执行文件中的这些漏洞长期以来都备受关注?

静态链接的可执行文件中的这些漏洞长期以来都备受关注,因为.got.plt部分并不总是用作一种聪明的优化方式。它根本不存在,因此没有出现攻击面。我不知道引入这个.got.plt优化的确切日期。

3.3完全RELRO保护与部分RELRO保护

完全RELRO保护所有部分(.got.plt,.got,.init_array,.fini_array,.dynamic和.jcr),而部分RELRO省略保护.got.plt,因为它需要在整个过程的生命周期内更新以支持按需动态链接。这是一个问题,因为它将.got.plt暴露为攻击面。

默认情况下,即使静态可执行文件仅在进程初始化时更新.got.plt,甚至没有启用RELRO,它仍然会暴露.got.plt攻击面。如果攻击者可以找到.got.plt部分,他们可以破坏关键函数指针。 那么,保护静态可执行文件的解决方案是什么?

注意:.init_array和.fini_array是.ctors和.dtors的新名称,并且具有相应的SHT_INIT_ARRAY和SHT_FINI_ARRAY类型。

3.4深入攻击面

发现.got.plt是静态链接二进制文件中暴露的攻击面让我以及我认识的几位熟练的ELF研究人员感到惊讶。让我们看看用RELRO保护可执行文件的所有方法。

3.4.1共享库注入保护

虽然共享库注入保护不是RELRO的最初目的,但它可以与DEP的各种运行相结合,例如PaX mprotect()限制,并防止运行时被恶意软件攻击。例如,共享库函数重定向因PaX禁止PTRACE_POKETEXT为只读段而被挫败。

3.4.2同样的旧开发技术是适用的

从开发的角度来看,当你意识到.got.plt部分仍然是静态链接的可执行文件中的相关攻击面时,事情会变得更有趣。我们将很快讨论.got.plt的目的,但是现在,重要的是要注意.got.plt包含指向libc例程的函数指针,并且曾经被动态链接的可执行文件利用。.init_array和.fini_array函数指针分别指向初始化和析构函数例程。

具体来说,.fini_array,也称为.dtors,尽管它的利用可能不像.got.plt部分那样普遍存在,但它也已经被用于许多类型漏洞中的代码执行。在以下部分中,我们分析静态链接可执行文件中缺少安全性保护,与此同时,我们还将探索一些逆向工程和二进制保护技术。该分析将向读者证明静态链接可执行文件的攻击面几乎与动态链接可执行文件的攻击面相同。此信息很重要,因为RELRO和ASLR等二进制保护不适用于与当前工具链的静态链接可执行文件。如果你很时间充裕,那就等一下。稍后我们将深入探讨具体细节,并通过将ASLR和RELRO应用于静态链接的可执行文件来探索一些创新的黑客攻击方式。

3.4.3静态链接的可执行文件中的RELRO歧义

下面的静态二进制文件是在使用gcc -static -Wl, -z, relro, -z, now命令启用完全RELRO的情况下构建的,之后即使是精明的反编译器也可能会误以为RELRO已启用。此时,部分RELRO和完整RELRO都与静态链接的可执行文件不兼容,因为动态链接器负责重新映射和保护数据段中的常见攻击点,例如.got.plt,并且如图所示。输出下面没有类型PT_INTERP的程序头来指定解释器,我们也不会静态链接的可执行文件中看到它,因为它们不使用动态链接。

默认链接描述文件就是指示链接器创建GNU_RELRO段,即使此段不起作用也会创建。我们设计了一个解决方案,使用PT_GNU_RELRO程序头,在静态链接的可执行文件上启用RELRO。PT_GNU_RELRO程序头与数据段的程序头共享相同的p_vaddr和p_offset,因为这是.init_array,.fini_array,.jcr,.dynamic,.got和.got.plt部分将存储在内存中的位置。内存中PT_GNU_RELRO的大小由程序头的p_memsz字段描述,该字段应与下一个PAGE_SIZE对齐,以获取动态链接器传递给mprotect()以标记页面的'len'值,从而将页面标记为只读。让我们仔细看看这些程序头和映射到它们的部分。

以下是静态链接的ELF可执行文件的程序头:

$ readelf -l test
Elf file type is EXEC (Executable file)
Entry point 0x4008b0

从偏移量64开始有6个程序头:

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000cbf67 0x00000000000cbf67  R E    200000
  LOAD           0x00000000000cceb8 0x00000000006cceb8 0x00000000006cceb8
                 0x0000000000001cb8 0x0000000000003570  RW     200000
  NOTE           0x0000000000000190 0x0000000000400190 0x0000000000400190
                 0x0000000000000044 0x0000000000000044  R      4
  TLS            0x00000000000cceb8 0x00000000006cceb8 0x00000000006cceb8
                 0x0000000000000020 0x0000000000000050  R      8
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x00000000000cceb8 0x00000000006cceb8 0x00000000006cceb8
                 0x0000000000000148 0x0000000000000148  R      1

节到段的映射:

Segment Sections...
   00     .note.ABI-tag .note.gnu.build-id .rela.plt .init .plt .text __libc_freeres_fn __libc_thread_freeres_fn .fini .rodata __libc_subfreeres __libc_atexit .stapsdt.base __libc_thread_subfreeres .eh_frame .gcc_except_table
   01     .tdata .init_array .fini_array .jcr .data.rel.ro .got .got.plt .data .bss __libc_freeres_ptrs
   02     .note.ABI-tag .note.gnu.build-id
   03     .tdata .tbss
   04   
   05     .tdata .init_array .fini_array .jcr .data.rel.ro .got

请注意,GNU_RELRO段指向数据段的开头,通常是您希望动态链接器将N个字节保护为只读的位置。通常,不超过数据段的前4096个字节是需要保护的。但是,尽管有GNU_RELRO段,静态可执行文件并没有考虑到安全性。一个明显的迹象是包含TLS数据的.tdata部分,作为数据段中的第一部分,如果它用mprotect()标记为PROT_READ,那么多线程程序将无法正确运行。如果glibc ld / gcc开发人员考虑过RELRO,他们可能会将.tdata部分放在.data部分和.bss部分之间。

3.4.4 重要的注释

正如@ulexec指出的那样,.init_array/.fini_array部分在glibc静态链接的可执行文件中不起作用。另一方面,.got.plt非常活跃,因为它被用作静态可执行文件中的libc例程的优化。.got.plt是最大的威胁,它用于glibc的静态链接模型,而我们传统上说的.ctors和.dtors却不会被使用,尽管它们存在而且通常为.init_array和.fini_array。但是,静态可执行文件并不总是像使用-static标志那样严格,当你从glibc的严格链接描述文件和标准init / deinit例程的上下文中删除静态ELF时,将会证明这一点。

因为-static并不像看起来那么严格,所以我们必须考虑通常受RELRO保护的所有ELF部分(不仅仅是.got.plt)在静态链接的可执行文件中的活动和行为几乎相同,除了经过证明的个别案例以外。我们现在已经知道了.got.plt用作libc例程的优化,这个将在稍后进行演示,这个函数指针的.got.plt表是一个很好的攻击面,并且早在2000年就已经用于动态链接的可执行文件中了,如多年前出版的Nergal的论文所示。[9]

3.4.5 checksec.sh无法提供准确的信息

checksec.sh [3]使用GNU_RELRO段作为表示是否在二进制文件上启用了RELRO的一种标志。在静态编译的二进制文件的情况下,checksec.sh将报告启用了部分RELRO,因为它无法找到DT_BIND_NOW动态段标志,静态链接的可执行文件中也没有动态段。为了使这更具体,让我们通过一个静态链接可执行文件的glibc初始化代码进行轻量级浏览。

在上面的输出中,数据段中有一个.got和.got.plt部分。通常,启用完整RELRO需要将它们合并为一个单独的“.got”部分。但是,我们设计了一个不需要合并的工具“RelroS”,只需要将它们都标记为只读。

4 更深入的静态链接和攻击面概述

注意:可以在这里查看ftrace工具的高级概述:https://github.com/elfmaster/ftrace 该工具可以执行函数级别的跟踪。

$ ftrace test_binary
LOCAL_call@0x404fd0:__libc_start_main()
LOCAL_call@0x404f60:get_common_indeces.constprop.1()
(RETURN VALUE) LOCAL_call@0x404f60: get_common_indeces.constprop.1() = 3
LOCAL_call@0x404cc0:generic_start_main()
LOCAL_call@0x447cb0:_dl_aux_init() (RETURN VALUE) LOCAL_call@0x447cb0:
_dl_aux_init() = 7ffec5360bf9
LOCAL_call@0x4490b0:_dl_discover_osversion(0x7ffec5360be8)
LOCAL_call@0x46f5e0:uname() LOCAL_call@0x46f5e0:__uname()
<truncated>

通常在动态链接器中发生的大部分繁重工作都是由函数generic_start_main()执行的,该函数除了其他任务外,还对数据段中的多个部分(包括.got)执行全面的重定位和修复,包括 plt部分。这样就能够设置一些观察点来观察早期的一个查询CPU信息的功能,如CPU缓存大小。此函数能让glibc init代码智能地确定应该使用哪个版本的给定函数(例如strcpy())进行优化。

请注意,当我们在GOT条目上为多个共享库例程设置监视点时,generic_start_main()在某种意义上用作动态链接器的类似机制,因为它的工作主要是执行重定位和修复。
-- 在一个带有静态二进制文件的探索性GDB会话中 --

(gdb) x/gx 0x6d0018 /* .got.plt entry for strcpy */
0x6d0018:    0x000000000043f600
(gdb) watch *0x6d0018
Hardware watchpoint 3: *0x6d0018
(gdb) x/gx       /* .got.plt entry for memmove */
0x6d0020:    0x0000000000436da0
(gdb) watch *0x6d0020
Hardware watchpoint 4: *0x6d0020
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/elfmaster/git/libelfmaster/examples/static_binary
Hardware watchpoint 4: *0x6d0020
Old value = 4195078
New value = 4418976
0x0000000000404dd3 in generic_start_main ()
(gdb) x/i 0x436da0
   0x436da0 <__memmove_avx_unaligned>:  mov    %rdi,%rax
(gdb) c

Continuing.

Hardware watchpoint 3: 0x6d0018
*Old value = 4195062
New value = 4453888
0x0000000000404dd3 in generic_start_main ()
(gdb) x/i 0x43f600
   0x43f600 <__strcpy_sse2_unaligned>:  mov    %rsi,%rcx
(gdb)

4.1 .got.plt使用GDB进行检查

在上述两种情况下,给定libc函数的GOT条目将其PLT存根地址替换为最有效的函数版本,给定CPU缓存大小由某些glibc初始化代码查找,例如,__ cache_sysconf()。由于这是一个有点高级的概述,我不会涉及到每个函数,但重要的是.got.plt是用libc函数更新的,并且可能会中毒,因为RELRO与静态链接的可执行文件不兼容。

这引出了几个可能的解决方案,包括我们的实验原型,RelroS(静态ELF的只读重定位),它使用一些ELF技巧来注入放置在非常特定位置的蹦床调用的代码。在调用enable_relro()例程之前,必须等到generic_start_main()完成对我们打算标记为只读的内存区域的所有写入。

4.2 RelroS(静态ELF的只读重定位)解决方案

由于时间限制,RelroS的初始(且唯一)版本被快速写入。因此,当前实现中存在若干问题,但接下来我将解释如何解决它们。目前的指令是使用注入技术将PT_NOTE程序头标记为PT_LOAD,因此我们可以有效地创建第二个文本段。此外,在generic_start_main()函数中,有一个非常特定的地方需要修补,它需要一个5字节的补丁(即,调用<imm>)。</imm>

不幸的是,在将这个指令转移到不同的段时,立即调用是不起作用的。相反,需要一个远远超过5个字节的lcall(远程调用)。解决方法是切换到反向文本感染,这将使enable_relro()代码保持在唯一的代码段中。目前,我们正在粗略地修补调用main()的代码

405b46:       48 8b 74 24 10          mov    0x10(%rsp),%rsi
  405b4b:       8b 7c 24 0c             mov    0xc(%rsp),%edi
  405b4f:       48 8b 44 24 18          mov    0x18(%rsp),%rax /* store main() addr */
  405b54:       ff d0                   callq  *%rax /* call main() */
  405b56:       89 c7                   mov    %eax,%edi
  405b58:       e8 b3 de 00 00          callq  413a10 <exit>

在当前示例中,我们使用push $enable_relro; ret来覆盖0x405b54处的6个字节指令集。我们的enable_relro()函数将PT_RELRO表示的数据段的一部分保存为只读,然后调用main(),然后是sys_exit。这是有缺陷的,因为main()之后的代码都没有被调用,包括deinitialization例程。

解决方案是使用反向文本扩展名或文本填充感染将enable_relro()代码保留在主程序文本段中。我们可以简单地将0x405b46处的5个字节用call <offset>覆盖到enable_relro(),并且该函数将确保我们返回将存储在%rax中的main()的地址。由于下一条指令是·callq *%rax`,它在RELRO启用后立即调用main(),因此没有指令被抛出对齐状态。

到目前为止,该解决方案是理想的。但是,.tdata位于数据段的开头也是一个问题,而我们只能在PAGE_SIZE的倍数的内存区域上使用mprotect()。因此,必须采用稍微复杂的步骤来使多线程应用程序与二进制检测的RELRO一起运作(或者,我们可以通过使用链接描述文件将线程数据和bss放入其自己的数据段来解决问题)。

在当前的原型中,我们使用push/ret序列修补从0x405b4f开始的指令字节,破坏以下指令。这是一个临时修复,需要在未来的原型中解决。

405b46:       48 8b 74 24 10          mov    0x10(%rsp),%rsi
  405b4b:       8b 7c 24 0c             mov    0xc(%rsp),%edi
  405b4f:       48 8b 44 24 18          mov    0x18(%rsp),%rax
  405b54:       68 f4 c6 0f 0c          pushq  $0xc0fc6f4
  405b59:       c3                      retq 
  /*
   * The following bad instructions are never crashed on because
   * the previous instruction returns into enable_relro() which calls
   * main() on behalf of this function, and then sys_exit's out.
   */
  405b5a:       de 00                   fiadd  (%rax)
  405b5c:       00 39                   add    %bh,(%rcx)
  405b5e:       c2 0f 86                retq   $0x860f
  405b61:       fb                      sti  
  405b62:       fe                      (bad)
  405b63:       ff                      (bad)
  405b64:       ff                      (bad)

可以看到这不是一个动态链接的可执行文件

$ readelf -d test
There is no dynamic section in this file.

可以观察到只有一个r + x文本段和一个r + w数据段,在数据段的第一部分缺乏只读存储器保护。

$ ./test &
[1] 27891
$ cat /proc/`pidof test`/maps
00400000-004cc000 r-xp 00000000 fd:01 4856460 /home/elfmaster/test
006cc000-006cf000 rw-p 000cc000 fd:01 4856460 /home/elfmaster/test
<truncated>

使用单个命令将RelroS应用于可执行文件

$ ./relros ./test
injection size: 464
main(): 0x400b23

我们可以观察到我们的补丁在强制执行只读重定位,而且我们在二进制文件中调用了名为'test'的补丁

$ ./test &
[1] 28052
$ cat /proc/`pidof test`/maps
00400000-004cc000 r-xp 00000000 fd:01 10486089 /home/elfmaster/test
006cc000-006cd000 r--p 000cc000 fd:01 10486089 /home/elfmaster/test
006cd000-006cf000 rw-p 000cd000 fd:01 10486089 /home/elfmaster/test
<truncated>
$

请注意,在我们在./test上应用RelroS后,数据段现在有一个4096字节区域,已标记为只读。这是动态链接器为动态链接的可执行文件完成的操作。

目前,我们正在努力改进我们的二进制检测项目[11],以便在静态链接的可执行文件上启用RELRO。在4.3和4.4节中,我将讨论另外两个解决这个问题的方法。

4.3链接器脚本和自定义函数

在静态可执行文件上启用RELRO的一种可能可行的方法是编写一个链接器脚本,将.tbss,.tdata和.data分隔成它们自己的段,然后放置原本应该是只读的段(即, .init_array,.fini_array,.jcr,.dynamic,.got和.got.plt)在另一个段中..可以将每个PT_LOAD段单独标记为PF_R | PF_W(读取+写入),这样它们就可以作为两个单独的数据段。

单独的段能让程序在检查argc/argv之前具有main()调用的自定义函数(非构造函数)。应该使用自定义而不是构造函数,这是因为存储在.init节中的构造函数例程在.got,.got.plt部分的写入指令之前被调用,等等。在glibc init代码完成执行其需要写入访问的修正之后,构造函数将尝试对第二个数据段执行mprotect()只读权限,从而无法运行。

4.4 GLIBC开发人员可以修复它

在静态可执行文件上启用RELRO的另一个解决方案是让glibc开发人员在调用main()之前添加一个由generic_start_main()调用的函数。目前,在静态链接的可执行文件中有一个_dl_protect_relro()函数,而它永远不会被调用。

5 ASLR问题

如上所述,二进制保护(如ASLR)无法应用于当前工具链的静态可执行文件。除非RANDEXEC [5]用于ET_EXEC ASLR,否则ASLR会采用ET_DYN作为可执行文件。静态链接的可执行文件只能链接为ET_EXEC类型的可执行文件。

$ gcc -static -fPIC -pie test2.c -o test2
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/5/crtbeginT.o:
relocation R_X86_64_32 against `__TMC_END__' can not be used when making a shared object; recompile with -fPIC
/usr/lib/gcc/x86_64-linux-gnu/5/crtbeginT.o: error adding symbols: Bad value
collect2: error: ld returned 1 exit status

这意味着你可以删除-pie标志并最终得到一个使用位置无关代码的可执行文件,但是没有我们所需要的以基址0开头的地址空间布局。那么该怎么办?

5.1 ASLR解决方案

我个人没有花太多的时间来用glibc链接器查看是否可以调整它来链接作为ET_DYN对象出现的静态可执行文件。值得一提的是,这样的可执行文件不应该有PT_INTERP段,因为它不是动态链接的。由于我自己的时间限制,我想把这作为读者的练习,也许有一些我不知道的解决方案。

来自ELF内核加载器的以下代码将进一步具体化可执行类型必须为ET_DYN的事实,以便将其重定位到随机生成的地址。
快速查看src/linux/fs/binfmt_elf.c在第916行显示此代码

line 916:      } else if (loc->elf_ex.e_type == ET_DYN) {
               /* Try and get dynamic programs out of the way of the
                * default mmap base, as well as whatever program they
                * might try to exec.  This is because the brk will
                * follow the loader, and is not movable.  */
             load_bias = ELF_ET_DYN_BASE - vaddr;
             if (current->flags & PF_RANDOMIZE)
                   load_bias += arch_mmap_rnd();
... 然后 ...
line 941:    if (!load_addr_set) {
             load_addr_set = 1;
             load_addr = (elf_ppnt->p_vaddr - elf_ppnt->p_offset);
             if (loc->elf_ex.e_type == ET_DYN) {
                   load_bias += error -
                       ELF_PAGESTART(load_bias + vaddr);
                   load_addr += load_bias;
                   reloc_func_desc = load_bias;
             }
      }

5.2 ASLR二进制检测/链接器混合解决方案

链接器可能还没有能够执行此任务,但我相信我们至少可以编译静态链接的可执行文件以便它使用位置无关代码(IP相对),来找到一个潜在的解决方案。以下是从二进制检测角度出发的以下算法:

  • gcc -static -fPIC test2.c -o test2 --static_to_dyn.c使用此算法

  • 将ehdr->e_type从ET_EXEC修改为ET_DYN

  • 修改每个PT_LOAD段的phdr(分别为text和data段)
    A. phdr [TEXT] .p_vaddr = 0x00000000;
    B. phdr [TEXT] .p_offset = 0x00000000;
    C. phdr [DATA] .p_vaddr = 0x200000 + phdr [DATA] .p_offset;

  • ehdr->e_entry = ehdr->e_entry - old_base;

  • 更新每个节头以反映程序头的新地址范围。

否则GDB和objdump将无法使用二进制文件:

$ gcc -static -fPIC test2.c -o test2
$ ./static_to_dyn ./test2
Setting e_entry to 8b0
$ ./test2
Segmentation fault (core dumped)

5.3 从ASLR的结果延伸我们对静态链接可执行文件的观点

哎呀,快速查看带有objdump的二进制文件证明了大多数代码不使用IP相对寻址而且也不是真正的PIC。像_start这样的glibc init例程的PIC版本位于/usr/lib/X86_64-linux-gnu/Scrt1.o中。我认为我们可能必须从一种新颖的方法开始,例如将'-static'gcc选项排除在等式之外并从头开始工作。以下是解决方案的几个起点。

也许test2.c应该同时具有_start()和main(),而_start()应该没有代码,也没有使用attribute ((weak)),这样Scrt1.o中的_start()例程就可以覆盖它。另一种可能的解决方案是使用IP相对寻址编译dietlibc,并使用它代替glibc以简化操作。有多种可能性,但主要的想法是开始思考开箱即用。为了POC,这里有一个程序,除了检查argc是否大于1之外什么都不做,然后每隔一次迭代在循环中递增一个变量。我们将演示ASLR如何在其上工作。使用_start()作为main(),编译器选项如下所示。

*** PoC of simple static binary made to ASLR ***

/* Make sure we have a data segment for testing purposes */
static int test_dummy = 5;

int _start() {
      int argc;
      long *args;
      long *rbp;
      int i;
      int j = 0;

      /* Extract argc from stack */
      asm __volatile__("mov 8(%%rbp), %%rcx " : "=c" (argc));

      /* Extract argv from stack */
      asm __volatile__("lea 16(%%rbp), %%rcx " : "=c" (args));

      if (argc > 2) {
             for (i = 0; i < 100000000000; i++)          
                   if (i % 2 == 0)
                          j++;
      }
      return 0;
}

$ gcc -nostdlib -fPIC test2.c -o test2
$ ./test2 arg1

$ pmap `pidof test2`
17370:   ./test2 arg1
0000000000400000      4K r-x-- test2
0000000000601000      4K rw--- test2
00007ffcefcca000    132K rw---   [ stack ]
00007ffcefd20000      8K r----   [ anon ]
00007ffcefd22000      8K r-x--   [ anon ]
ffffffffff600000      4K r-x--   [ anon ]
 total              160K
$

请注意,ASLR不存在,并且地址空间与Linux中的64类ELF二进制文件一样。让我们运行我们的static_to_dyn.c程序,然后再试一次。

$ ./static_to_dyn test2
$ ./test2 arg1
$ pmap `pidof test2`
17622:   ./test2 arg1
0000565271e41000      4K r-x-- test2
0000565272042000      4K rw--- test2
00007ffc28fda000    132K rw---   [ stack ]
00007ffc28ffc000      8K r----   [ anon ]
00007ffc28ffe000      8K r-x--   [ anon ]
ffffffffff600000      4K r-x--   [ anon ]
 total              160K

请注意,test2的文本和数据段映射到随机地址空间。这是我们现在正在谈论的!其余的功课应该相当简单。根据这项工作进行推断,找到更有创意的解决方案,直到GNU人员有时间解决问题,而不是使用技巧和工具来做更优雅的事情。

5.4改进我们的静态链接技术

由于我们通过使用'-nostdlib'编译器标志简单地将glibc从等式中删除来静态编译,我们必须考虑我们认为理所当然地认为必须手动编码和链接的东西,例如TLS和系统调用包装器。我之前提到的一个可能的解决方案是使用IP相对寻址模式编译dietlibc,并使用-nostdlib简单地将代码链接到它。这是我们更新的test2.c代码,它可以打印命令行参数:

*** updated test2.c ***

#include <stdio.h>

/* Make sure we have a data segment for testing purposes */

static int test_dummy = 5;

int _start() {
        int argc;
        long *args;
        long *rbp;
        int i;
        int j = 0;

        /* Extract argc from stack */
        asm __volatile__("mov 8(%%rbp), %%rcx " : "=c" (argc));

        /* Extract argv from stack */
        asm __volatile__("lea 16(%%rbp), %%rcx " : "=c" (args));

        for (i = 0; i < argc; i++) {
             sleep(10); /* long enough for us to verify ASLR */
                printf("%s\n", args[i]);
      }
        exit(0);
}

作为注释,读者可以弄清楚如何获得char **envp。我把它留作练习。现在我们实际上正在构建一个静态链接的二进制文件,它可以获取命令行参数并从diet libc调用静态链接函数:

# Note that first I downloaded the dietlibc source code and edited the
# Makefile to use -fPIC flags which will enforce the IP-relative addressing
# within dietlibc

$ gcc -nostdlib -c -fPIC test2.c -o test2.o
$ gcc -nostdlib test2.o /usr/lib/diet/lib-x86_64/libc.a -o test2
$ ./test2 arg1 arg2
./test2
ARG1
ARG2
$

现在我们可以在其上运行static_to_dyn工具来强制执行ASLR:

$ ./static_to_dyn test2
$ ./test2 foo bar
$ pmap `pidof test`
24411:   ./test2 foo bar
0000564cf542f000      8K r-x-- test2 # Notice ASLR!
0000564cf5631000      4K rw--- test2 # Notice ASLR!
00007ffe98c8e000    132K rw---   [ stack ]
00007ffe98d55000      8K r----   [ anon ]
00007ffe98d57000      8K r-x--   [ anon ]
ffffffffff600000      4K r-x--   [ anon ]
 total              164K

*** static_to_dyn.c ***

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <elf.h>
#include <sys/types.h>
#include <search.h>
#include <sys/time.h>
#include <fcntl.h>
#include <link.h>
#include <sys/stat.h>
#include <sys/mman.h>

#define HUGE_PAGE 0x200000

int main(int argc, char **argv)
{
      ElfW(Ehdr) *ehdr;
      ElfW(Phdr) *phdr;
      ElfW(Shdr) *shdr;
      uint8_t *mem;
      int fd;
      int i;
      struct stat st;
      uint64_t old_base; /* original text base */
      uint64_t new_data_base; /* new data base */
      char *StringTable;

      fd = open(argv[1], O_RDWR);
      if (fd < 0) {
             perror("open");
             goto fail;
      }

      fstat(fd, &st);

      mem = mmap(NULL, st.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
      if (mem == MAP_FAILED ) {
             perror("mmap");
             goto fail;
      }

      ehdr = (ElfW(Ehdr) *)mem;
      phdr = (ElfW(Phdr) *)&mem[ehdr->e_phoff];
      shdr = (ElfW(Shdr) *)&mem[ehdr->e_shoff];
      StringTable = (char *)&mem[shdr[ehdr->e_shstrndx].sh_offset];

      printf("Marking e_type to ET_DYN\n");
      ehdr->e_type = ET_DYN;

      printf("Updating PT_LOAD segments to become relocatable from base 0\n");

      for (i = 0; i < ehdr->e_phnum; i++) {
             if (phdr[i].p_type == PT_LOAD && phdr[i].p_offset == 0) {
                   old_base = phdr[i].p_vaddr;
                   phdr[i].p_vaddr = 0UL;
                   phdr[i].p_paddr = 0UL;
                   phdr[i + 1].p_vaddr = HUGE_PAGE + phdr[i + 1].p_offset;
                   phdr[i + 1].p_paddr = HUGE_PAGE + phdr[i + 1].p_offset;
             } else if (phdr[i].p_type == PT_NOTE) {
                   phdr[i].p_vaddr = phdr[i].p_offset;
                   phdr[i].p_paddr = phdr[i].p_offset;
             } else if (phdr[i].p_type == PT_TLS) {
                   phdr[i].p_vaddr = HUGE_PAGE + phdr[i].p_offset;
                   phdr[i].p_paddr = HUGE_PAGE + phdr[i].p_offset;
                   new_data_base = phdr[i].p_vaddr;
             }
      }
      /*
       * If we don't update the section headers to reflect the new address
       * space then GDB and objdump will be broken with this binary.
       */
      for (i = 0; i < ehdr->e_shnum; i++) {
             if (!(shdr[i].sh_flags & SHF_ALLOC))
                   continue;
             shdr[i].sh_addr = (shdr[i].sh_addr < old_base + HUGE_PAGE) ?
                 0UL + shdr[i].sh_offset : new_data_base + shdr[i].sh_offset;
             printf("Setting %s sh_addr to %#lx\n", &StringTable[shdr[i].sh_name],
                 shdr[i].sh_addr);
      }
      printf("Setting new entry point: %#lx\n", ehdr->e_entry - old_base);
      ehdr->e_entry = ehdr->e_entry - old_base;
      munmap(mem, st.st_size);
      exit(0);
      fail:
             exit(-1);
}

6 总结

本文的目的是澄清并帮助揭开周围模糊性的错误概念——静态链接可执行文件中攻击面的内容,以及默认情况下缺少哪些安全缓解措施。RELRO和ASLR不适用于静态链接的可执行文件。但是,在本文中,我们介绍了“RelroS”工具,该工具是在静态链接的可执行文件上启用完整RELRO的原型。我们还创建了一种将编译/链接技术与仪器技术相结合的混合方法,结合我们的RELRO启用,这种解决方案可以用于制作与ASLR一起使用的静态二进制文件。目前,我们的RELRO解决方案仅适用于传统构建的静态二进制文件(例如,-static标志),因为该工具会修补glibc初始化函数。

6.1 给读者的课外作业

目前,relros.c和static_to_dyn.c可以单独应用,但不能同时应用; 这是因为static_to_dyn.c不适用于标准的静态链接可执行文件,而relros.c仅适用于标准的静态链接可执行文件。理想情况下,我们需要一个可以在同一个静态链接的可执行文件上应用ASLR和RELRO的工具。完成此操作的一些常规步骤:
1.使用5.4节中的方法创建静态二进制文件。这将-nostdlib标志与使用位置无关代码编译的dietlibc版本相结合.

2.使用现有的static_to_dyn.c源代码将二进制文件转换为ET_DYN,以便可以将ASLR应用于它。

3.修改relros.c,使其适用于我们的静态PIE可执行文件。使用感染技术注入enable_relro()代码,将代码放入常规文本段,以便我们可以使用4.2节中讨论的立即调用指令; 这将使得标准的deinitialization例程在我们的代码之后运行,并且在main()之后在generic_start_main()中运行。

小贴士:
1..got.plt攻击存在于静态链接的可执行文件中

2.RELRO不适用于静态链接的可执行文件

3.ASLR不适用于静态链接的可执行文件

4.本文提供了一些原型解决方案

5.最干净的修复方法是通过gcc/ld工具链代码

本文介绍的自定义软件:https://github.com/elfmaster/static_binary_mitigations

参考文献:

[1] RELRO - https://pax.grsecurity.net/docs/vmmirror.txt
[2] ASLR - https://pax.grsecurity.net/docs/aslr.txt
[3] checksec.sh - https://github.com/slimm609/checksec.sh
[4] PAGEEXEC - https://pax.grsecurity.net/docs/pageexec.txt
[5] RANDEXEC - https://pax.grsecurity.net/docs/randexec.txt
[6] static-pie - https://gcc.gnu.org/ml/gcc/2015-06/msg00008.html
[7]静态补丁 - https://gcc.gnu.org/viewcvs/gcc?view=revision&revision=252034
[8] https://access.redhat.com/blogs/766093/posts/1975803
[9] http://phrack.org/issues/58/4.html
[10] libelfmaster - https://github.com/elfmaster/libelfmaster
[11] Relros / ASLRs - https://github.com/elfmaster/static_binary_mitigations

关键词:[‘技术文章’, ‘翻译文章’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now