ld -z分隔码


本文将重点介绍GNU ld在2018年12月发布2.30版本中增加的一个小型安全功能。 在俄语中, opennet上提到了此改进,并带有以下注释:


“ -z分离代码”模式,以增加大小和内存消耗为代价,提高了可执行文件的安全性

让我们弄清楚。 为了说明我们在讨论哪种安全问题以及解决方案是什么,让我们从二进制漏洞利用的一般功能开始。


利用控制流问题


攻击者可以在各种漏洞的帮助下将数据传输到程序并以这种方式进行操作:按索引写超出数组范围,不安全地复制字符串,在释放后使用对象。 此类错误是C和C ++程序代码的典型错误,并可能导致程序的某些输入数据导致内存损坏。


内存损坏漏洞

CWE-20:输入验证不正确
CWE-118:可索引资源的错误访问(“范围错误”)
CWE-119:内存缓冲区范围内的操作限制不当
CWE-120:缓冲区复制,不检查输入大小(“经典缓冲区溢出”)
CWE-121:基于堆栈的缓冲区溢出
CWE-122:基于堆的缓冲区溢出
CWE-123:在何处写入条件
CWE-124:缓冲区下溢(“缓冲区下溢”)
CWE-125:越界读取
CWE-126:缓冲区过度读取
CWE-127:缓冲区读取不足
CWE-128:环绕式错误
CWE-129:数组索引的验证不正确
CWE-130:长度参数不一致的不正确处理
CWE-131:缓冲区大小的错误计算
CWE-134:使用外部控制的格式字符串
CWE-135:多字节字符串长度的错误计算
CWE-170:不正确的Null终止
CWE-190:整数溢出或环绕
CWE-415:双重免费
CWE-416:免费使用
CWE-476:空指针解除引用
CWE-787:越界写入
CWE-824:访问未初始化的指针
...


类似于内存损坏的漏洞的经典利用元素是覆盖内存中的指针。 然后,程序将使用该指针将控制权转移到另一个代码:从另一个模块调用类方法或函数,从一个函数返回。 并且由于指针已被覆盖,因此控件将被攻击者拦截-即,将执行由他准备的代码。 如果您对这些技术的变化和细节感兴趣,我们建议阅读文档


此类漏洞利用程序的这种常见时刻是众所周知的,而对于攻击者而言,这里早已设置了障碍:


  1. 在通过控制之前检查指针的完整性:堆栈cookie,控制流保护,指针身份验证
  2. 段地址与代码和数据的随机化:地址空间布局随机化
  3. 防止代码执行外部代码段:可执行空间保护

接下来,我们将重点保护后一种类型。


可执行空间保护


程序存储器是异构的,分为具有不同权限的段:读取,写入和执行。 处理器能够用页表中的访问标志标记内存页,从而确保了这一点。 保护的思想是严格区分代码和数据:在处理过程中,从攻击者收到的数据应放在不可执行的段(堆栈,堆)中,而程序本身的代码应放在单独的不可 更改的段中。 因此,这将剥夺攻击者在内存中放置和执行无关代码的能力。


为了避免在数据段中禁止执行代码,使用了代码重用技术。 即,攻击者将控制权转移到位于可执行页面上的代码片段(以下称为小工具)。 这类技术的难度在不断增加,依次为:


  • 将控制权转移到对攻击者足够的功能:具有受控参数的system()函数以运行任意shell命令(ret2libc)
  • 将控制权转移到一个功能或一系列小工具,这些小工具将禁用保护或使部分内存成为可执行文件(例如,调用mprotect() ),然后执行任意代码
  • 使用一长串小工具执行所有所需的动作

因此,攻击者面临着在一个或另一个卷中重用现有代码的任务。 如果这比返回单个功能更复杂,那么将需要一系列小工具 。 要按可执行段搜索小工具,有一些工具: ropperropgadget


孔READ_IMPLIES_EXEC


但是,有时带有数据的存储器区域是可执行的,并且上述代码和数据的分离原理明显受到违反。 在这种情况下,攻击者无需再寻找小工具或功能来重用代码。 有趣的发现是可执行堆栈和所有数据段都位于同一“工业防火墙”上。


Listing /proc/$pid/maps


 00008000-00009000 r-xp 00000000 08:01 10 /var/flash/dmt/nx_test/a.out 00010000-00011000 rwxp 00000000 08:01 10 /var/flash/dmt/nx_test/a.out 00011000-00032000 rwxp 00000000 00:00 0 [heap] 40000000-4001f000 r-xp 00000000 1f:02 429 /lib/ld-linux.so.2 4001f000-40022000 rwxp 00000000 00:00 0 40027000-40028000 r-xp 0001f000 1f:02 429 /lib/ld-linux.so.2 40028000-40029000 rwxp 00020000 1f:02 429 /lib/ld-linux.so.2 4002c000-40172000 r-xp 00000000 1f:02 430 /lib/libc.so.6 40172000-40179000 ---p 00146000 1f:02 430 /lib/libc.so.6 40179000-4017b000 r-xp 00145000 1f:02 430 /lib/libc.so.6 4017b000-4017c000 rwxp 00147000 1f:02 430 /lib/libc.so.6 4017c000-40b80000 rwxp 00000000 00:00 0 be8c2000-be8d7000 rwxp 00000000 00:00 0 [stack] 

在这里,您可以看到测试实用程序过程中的存储卡。 映射由存储区-表行组成。 首先,请注意右边的列-它说明了区域的内容(代码段,函数库的数据或程序本身)或其类型(堆,堆栈)。 按顺序在左侧是每个存储区占用的地址范围,此外,还包括访问权限标志:r(读),w(写),x(执行)。 当尝试在这些地址上读取,写入和执行内存时,这些标志确定系统的行为。 如果违反了指定的访问模式,则会发生异常。


请注意,进程内的几乎所有内存都是可执行的:堆栈,堆和所有数据段。 这是一个问题。 显然,内存的rwx页的存在将使攻击者的生活变得更轻松,因为在将数据(数据包,文件)传输到此类程序进行处理时,攻击者将能够在自己的代码所能到达的任何地方以这种过程自由地执行代码。


为什么在支持硬件禁止在数据页上执行代码的现代设备上出现这种情况,企业和工业网络的安全性是否取决于该设备,以及已解决的问题及其解决方案已经很长时间了?


该图由内核在进程初始化(分配堆栈,堆,加载主ELF等)期间以及在执行核进程调用期间的行为确定。 影响此的关键属性是个性标记READ_IMPLIES_EXEC 。 该标志的作用是任何可读存储器也可以执行。 可以将标志设置为您的进程有以下几种原因:


  1. 可以通过ELF标头中的软件标志显式请求旧版,以实现一种非常有趣的机制:堆栈上的跳板( 1,2,3 )!
  2. 子进程可以从父级继承它。
  3. 它可以由内核针对所有进程独立安装! 首先,如果架构不支持不可执行的内存。 其次,以防万一,以支持其他一些古代拐杖 。 该代码位于2.6.32(ARM)内核中,该内核的使用寿命很长。 这只是我们的情况。

在ELF图像中查找小物件的空间


函数库和程序可执行文件为ELF格式。 gcc编译器将语言构造转换为机器代码,并将其放在一个部分中,而该代码将操作的数据在其他部分中。 有很多部分,它们由ld链接器分组为段。 因此,ELF包含一个具有两种表示形式的程序映像:分区表和段表。


 $ readelf -l /bin/ls Elf file type is EXEC (Executable file) Entry point 0x804bee9 There are 9 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x08048034 0x08048034 0x00120 0x00120 RE 0x4 INTERP 0x000154 0x08048154 0x08048154 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x08048000 0x08048000 0x1e40c 0x1e40c RE 0x1000 LOAD 0x01ef00 0x08067f00 0x08067f00 0x00444 0x01078 RW 0x1000 DYNAMIC 0x01ef0c 0x08067f0c 0x08067f0c 0x000f0 0x000f0 RW 0x4 NOTE 0x000168 0x08048168 0x08048168 0x00044 0x00044 R 0x4 GNU_EH_FRAME 0x018b74 0x08060b74 0x08060b74 0x00814 0x00814 R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 GNU_RELRO 0x01ef00 0x08067f00 0x08067f00 0x00100 0x00100 R 0x1 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame 03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag .note.gnu.build-id 06 .eh_frame_hdr 07 08 .init_array .fini_array .jcr .dynamic .got 

在这里,您将看到部分到ELF图像中段的映射。


实用程序使用该表来分析程序和库,但装入程序不使用该表将ELF投影到进程内存中。 节表比段表更详细地描述了ELF结构。 一个段中可以包含多个节。


内存中的ELF映像由ELF加载程序根据表的内容创建。 分区表不再用于将ELF加载到内存中。


但是该规则也有例外。

例如,实际上,有一个针对ARM体系结构的ELF ld.so加载器的Debian开发人员补丁,它正在寻找特殊的“ .ARM.attributes”部分(如SHT_ARM_ATTRIBUTES),并且在这样的系统中未加载带有锯齿形节表的二进制文件...


ELF段具有确定该段在内存中具有哪些权限的标志。 传统上,大多数用于GNU / Linux的软件都是按照以下方式安排的:在段表中声明了两个PT_LOAD (可加载到内存中)段-如上面的清单所示:


  1. 带有RE标志的细分


    1.1。 ELF 可执行代码: .init.text.fini


    1.2。 ELF中的不可变数据: .symtab.rodata


  2. RW标志段


    2.1。 ELF中的可变数据: .plt.got.data.bss



如果您关注第一段的组成及其访问标志,那么很明显,这种布局扩展了用于搜索用于代码重用技术的小工具的空间。 在大型ELF(例如libcrypto)中,服务表和其他不可变数据最多可占据可执行段的40%。 通过尝试在可执行文件段中没有节表和符号的情况下反汇编带有大量数据的此类二进制文件,可以确认此数据中是否存在类似于代码段的内容。 单个可执行段中的每个字节序列都可以视为对机器代码和跳板的攻击片段有用-是该字节序列,至少包含程序的调试消息行,符号表中的函数名称的一部分或密码算法的常数。


PE可执行报头

ELF映像第一段开头的可执行标头和表类似于大约15年前Windows的情况。 有许多病毒会感染文件,并将其代码写入PE标头中,该标头也可以在此处执行。 我设法在档案中找到了这样一个样本:


病毒.Win32.Haless.1127


如您所见,病毒主体被紧紧压在PE标头区域中的节表之后。 在将文件投影到虚拟内存上时,此处通常大约有3KB的可用空间。 在病毒体之后有一个空白区域,然后第一部分从程序代码开始。


但是,对于Linux,VX领域还有很多有趣的作品: Retaliation


解决方案


  • 上述问题早已为人所知。
  • 固定于2018年1月12日 :在对象中添加了`ld -z独立代码键:“创建单独的代码” PT_LOAD“段头。这指定了一个内存段,该段应仅包含指令,并且必须与其他任何数据完全不相连。如果使用鼻梁代码,则不要创建单独的代码“ PT_LOAD”段。”)。 该功能已在2.30版发布
  • 此外,此功能默认包含在下一版本2.31中
  • 存在于新的binutils软件包中,例如,在Ubuntu 18.10存储库中。 ElfMaster研究人员遇到并记录了许多具有此新功能的软件包

由于更改了布局算法,因此获得了新的ELF图片:


 $ readelf -l ls Elf file type is DYN (Shared object file) Entry point 0x41aa There are 11 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x00000034 0x00000034 0x00160 0x00160 R 0x4 INTERP 0x000194 0x00000194 0x00000194 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x00000000 0x00000000 0x01e6c 0x01e6c R 0x1000 LOAD 0x002000 0x00002000 0x00002000 0x14bd8 0x14bd8 RE 0x1000 LOAD 0x017000 0x00017000 0x00017000 0x0bf80 0x0bf80 R 0x1000 LOAD 0x0237f8 0x000247f8 0x000247f8 0x0096c 0x01afc RW 0x1000 DYNAMIC 0x023cec 0x00024cec 0x00024cec 0x00100 0x00100 RW 0x4 NOTE 0x0001a8 0x000001a8 0x000001a8 0x00044 0x00044 R 0x4 GNU_EH_FRAME 0x01c3f8 0x0001c3f8 0x0001c3f8 0x0092c 0x0092c R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 GNU_RELRO 0x0237f8 0x000247f8 0x000247f8 0x00808 0x00808 R 0x1 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt 03 .init .plt .plt.got .text .fini 04 .rodata .eh_frame_hdr .eh_frame 05 .init_array .fini_array .data.rel.ro .dynamic .got .data .bss 06 .dynamic 07 .note.ABI-tag .note.gnu.build-id 08 .eh_frame_hdr 09 10 .init_array .fini_array .data.rel.ro .dynamic .got 

现在,代码和数据之间的边界更加准确。 唯一的可执行段实际上只包含以下代码段:.init,.plt,.plt.got,.text和.fini。


ld内部到底发生了什么变化?

如您所知, 链接器脚本描述了输出ELF文件的结构。 您可以看到如下的默认脚本:


 $ ld --verbose GNU ld (GNU Binutils for Ubuntu) 2.26.1 * * * using internal linker script: ================================================== /* Script for -z combreloc: combine and sort reloc sections */ /* Copyright (C) 2014-2015 Free Software Foundation, Inc. * * * 

用于不同平台和选项组合的许多其他脚本位于ldscripts目录中。 已为“ separate-code选项创建了新脚本。


 $ diff elf_x86_64.x elf_x86_64.xe 1c1 < /* Default linker script, for normal executables */ --- > /* Script for -z separate-code: generate normal executables with separate code segment */ 46a47 > . = ALIGN(CONSTANT (MAXPAGESIZE)); 70a72,75 > . = ALIGN(CONSTANT (MAXPAGESIZE)); > /* Adjust the address for the rodata segment. We want to adjust up to > the same address within the page on the next page up. */ > . = SEGMENT_START("rodata-segment", ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1))); 

在这里,您可以看到已经添加了一条指令来声明一个新段,该段在代码段之后带有只读段。


但是,除了脚本之外,还对链接器源进行了更改。 即,在函数_bfd_elf_map_sections_to_segments -请参见commit 。 现在,当为段选择段时,如果段与上一个段的SEC_CODE标志不同,则将添加新段。


结论


以前一样 ,我们建议开发人员在开发软件时不要忘记使用内置在编译器和链接器中的安全标志。 如此小的更改会使攻击者的生活大大复杂化,并使您的冷静些。

Source: https://habr.com/ru/post/zh-CN433108/


All Articles