New bypass and protection techniques for ASLR on Linux (1)



http://www.openwall.com/lists/oss-security/2018/02/27/5

本文由机器翻译加简单人工润色得到.

========================================================================
Contents
========================================================================

I. Introduction
II. Problems with current implementation
II.1. Close proximity of memory location
II.2. Fixed method of loading libraries
II.3. Fixed order of execution
II.4. Holes
II.5. TLS and thread stack
II.6. malloc and mmap
II.7. MAP_FIXED and loading of ET_DYN ELF files
II.8. Cache of allocated memory
II.9. thread cache alignment
III. Solutions
III.1 Holes
III.2 Order of loading ELF file segments
III.3 Use of mmap_min_addr when searching for mmap allocation addresses
III.4 mmap
IV. Related Work
V. References

========================================================================
I. 介绍
========================================================================

开始前,版本信息:
$ uname -a
Linux blackzert-virtual-machine 4.13.0-36-generic #40-Ubuntu SMP Fri Feb 16
20:07:48 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
$ ldd –version
ldd (Ubuntu GLIBC 2.26-0ubuntu2.1) 2.26

今天说的这些东西对 Kernel - 4.16-rc3, GNU Libc 2.27 也一样有效

通过这个步骤开始我们的研究:
$ less /proc/self/maps

5607a1ae5000-5607a1b0b000 r-xp 00000000 08:01 1966152 /bin/less
5607a1d0a000-5607a1d0b000 r–p 00025000 08:01 1966152 /bin/less
5607a1d0b000-5607a1d0f000 rw-p 00026000 08:01 1966152 /bin/less
5607a1d0f000-5607a1d13000 rw-p 00000000 00:00 0
5607a3bf8000-5607a3c19000 rw-p 00000000 00:00 0 [heap]
7f4d147e7000-7f4d14bf4000 r–p 00000000 08:01 3016021 /usr/lib/locale/locale-archive
7f4d14bf4000-7f4d14dca000 r-xp 00000000 08:01 1179731 /lib/x86_64-linux-gnu/libc-2.26.so
7f4d14dca000-7f4d14fca000 —p 001d6000 08:01 1179731 /lib/x86_64-linux-gnu/libc-2.26.so
7f4d14fca000-7f4d14fce000 r–p 001d6000 08:01 1179731 /lib/x86_64-linux-gnu/libc-2.26.so
7f4d14fce000-7f4d14fd0000 rw-p 001da000 08:01 1179731 /lib/x86_64-linux-gnu/libc-2.26.so
7f4d14fd0000-7f4d14fd4000 rw-p 00000000 00:00 0
7f4d14fd4000-7f4d14ff9000 r-xp 00000000 08:01 1185166 /lib/x86_64-linux-gnu/libtinfo.so.5.9
7f4d14ff9000-7f4d151f8000 —p 00025000 08:01 1185166 /lib/x86_64-linux-gnu/libtinfo.so.5.9
7f4d151f8000-7f4d151fc000 r–p 00024000 08:01 1185166 /lib/x86_64-linux-gnu/libtinfo.so.5.9
7f4d151fc000-7f4d151fd000 rw-p 00028000 08:01 1185166 /lib/x86_64-linux-gnu/libtinfo.so.5.9
7f4d151fd000-7f4d15224000 r-xp 00000000 08:01 1179654 /lib/x86_64-linux-gnu/ld-2.26.so
7f4d1540b000-7f4d15410000 rw-p 00000000 00:00 0
7f4d15424000-7f4d15425000 r–p 00027000 08:01 1179654 /lib/x86_64-linux-gnu/ld-2.26.so
7f4d15425000-7f4d15426000 rw-p 00028000 08:01 1179654 /lib/x86_64-linux-gnu/ld-2.26.so
7f4d15426000-7f4d15427000 rw-p 00000000 00:00 0
7ffdeb3ec000-7ffdeb40d000 rw-p 00000000 00:00 0 [stack]
7ffdeb41e000-7ffdeb421000 r–p 00000000 00:00 0 [vvar]
7ffdeb421000-7ffdeb423000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

- 程序的起始地址是5607a1ae5000.
- 堆的起始地址是5607a3bf8000, 作为二进制应用程序结尾的地址加上一个随机值,在本例中,它等于0×1ee5000 (5607a3bf8000-5607a1d13000).
因为是x86-64架构,所以这个地址按2^12对齐了。

- 地址 7f4d15427000 被选为 mmap_base. 当通过mmap系统调用为任何内存分配选择随机地址时,该地址将作为上边界。
- 库 ld-2.26.so, libtinfo.so.5.9, and libc-2.26.so 被顺序定位。

如果将减法应用于相邻的内存区域,我们将注意到以下几点:二进制文件、堆栈、最低本地存档地址和最高ld地址之间有很大的差别。
加载的库(文件)之间没有任何空闲页面。无论我们重复这个过程几次,文件也都实际上将保持不变:页面之间的差异将有所不同,而库和文件在相对于其他页面的位置上保持相同。
这一事实对我们的分析至关重要。

当前的mmap实现是造成这种行为的原因。逻辑位于在do_mmap内核函数中,该函数实现内存在用户态(mmap syscall)和内核(在执行execve时)的分配。
在第一阶段,选择一个可用地址(get_unmapped_area);在第二阶段,将页面映射到该地址(mmap_region)。
我们将从第一阶段开始。

在选择地址时,可以使用以下选项:
1.如果设置MAP_FIXED 标志,系统将返回addr参数的值作为地址。
2.如果addr参数值不是零,则使用此值作为hint,在某些情况下,将直接选择此值。
3.只要可用区域的最大地址长度合适并且位于可选择地址的允许范围内,将选择它作为地址。
4.检查地址是否存在与安全相关的限制。
如果所有操作都成功,则将分配所选地址的内存区域。

地址选择算法的细节
进程虚拟内存管理器的基础结构是vm_area_struct(简称vma):

  struct vm_area_struct {
      unsigned long vm_start; /* Our start address within vm_mm. */
      unsigned long vm_end; /* The first byte after our end address
  within vm_mm. */
      ...
      /* linked list of VM areas per task, sorted by address */
      struct vm_area_struct *vm_next, *vm_prev;

      struct rb_node vm_rb;
      ...
      pgprot_t vm_page_prot; /* Access permissions of this VMA. */
      ...
  };

此结构描述虚拟内存区域的开始、区域结束和区域内页的访问标志。VMA按升序排列在双链接的区域起始地址列表中,也按升序排列在区域起始地址的增广红-黑树中。
内核开发人员自己给出了这个解决方案的一个很好的基本原理[1]。

红-黑树增强是特定节点的可用内存量。
节点可用内存量定义为:
-在升序双链接列表中,当前vma的开始与前面vma的结束之间的内存空间的差值。
-左子树的可用内存量。
-右手子树的可用内存量。

这种结构使快速搜索(在O(log n)时间内)可以对应于某个地址的VMA或选择某一长度的可用范围。
在地址选择过程中,还确定了两个重要的边界:最小下边界和最大上边界。

下边界由体系结构确定为最小允许地址或系统管理员允许的最小值。上边界即mmap_base,这个值被选择为基于栈的随机值。这里“栈”是指最大栈地址,而“随机”是一个随机值,熵为28到32位,这取决于相关的内核参数。
Linux内核不能选择高于mmap_base的地址。在地址进程的地址空间中,大型mmap_base值要么对应于堆栈和特殊系统区域(vvar和vdso),要么显式地标记为MMAP_FIXED标志,否则将永远不会使用。

因此,在整个方案中,下列值仍然未知:
-主线程栈的起始地址
-加载应用程序二进制文件的Base Address
-应用程序堆的起始地址以及mmap_base,它是使用mmap分配内存的起始地址。

=====================================================
ii.当前实施中的问题。
=====================================================
刚才描述的内存分配算法有一些缺点。
=====================================================
ii.1.接近内存位置。
=====================================================
应用程序使用虚拟RAM。应用程序对内存的常用用法包括加载模块、线程堆栈和加载文件的堆、代码和数据(.rodata,.bss)。
在处理这些页面中的数据时,任何错误都可能影响到附近的数据。当更多具有不同类型内容的页面位于近距离时,攻击区域会变得更大,成功利用的可能性也会增加。
此类错误的例子包括越界[2]、溢出(整数[3]或缓冲区[4])和类型混淆[5]。

这个问题的一个具体实例是,系统仍然容易受到offset2lib攻击,如[6]所述。
简而言之,程序加载的Base Address和库的加载地址都是连在一起的,但是内核却选择它作为了mmap_base。

如果应用程序包含漏洞,则更容易利用它们,因为库映像靠近二进制应用程序映像。
演示此问题的一个很好的示例是[7]中的PHP漏洞,该漏洞允许读取或更改相邻的内存区域。

=====================================================
ii.2. 加载库的固定方法。
=====================================================

在Linux中,动态库实际上是在不调用Linux内核的情况下加载的。LD库(来自GNU Libc)负责这个过程。内核参与的唯一方式是通过mmap函数(我们还不考虑open/stat和其他文件操作):这是将代码和库数据加载到进程地址空间所必需的。一个例外是ld库本身,它通常作为文件加载的解释器写入可执行ELF文件中。至于解释器,它是由内核加载的。

如果使用GNU Libc中的ld作为解释器,则库的加载方式与以下类似:

1.将程序ELF文件添加到文件队列中进行处理。
2.从队列(FIFO)中取出第一个ELF文件。
3.如果该文件尚未加载到进程地址空间,则在mmap的帮助下加载该文件。
4.该文件所需的每个库都被添加到文件队列中进行处理。
5.只要队列不是空的,就重复步骤2。

此算法意味着加载顺序总是确定的,如果所有需要的库(其二进制文件)都是已知的,则可以重复加载。如果知道任何单个库的地址,则允许恢复所有库的地址:

1.假设libc库的地址是已知的。
2.将libc库的长度添加到libc加载地址-这是在libc之前加载的库的加载地址。
3.以同样的方式继续,我们获得在libc之前加载的库的mmap_base值和地址。
4.从libc地址减去libc之后加载的库的长度。这是libc之后加载的库的地址。
5.以同样的方式迭代,我们获得在程序开始时加载的所有库的地址。

如果在程序运行时加载了库(例如,通过dlopen函数),在某些情况下,攻击者可能不知道它相对于其他库的位置。例如,如果存在攻击者不知道分配的内存区域大小的mmap调用,则可能发生这种情况。

在利用漏洞时,对库地址的了解非常有帮助:例如,在搜索用于构建ROP链的gadget时。此外,如果任何库包含允许相对于库地址进行读取或写入值的漏洞,则此类漏洞很容易被利用,因为库是连续的。

大多数Linux发行版都包含具有最广泛库(例如libc)的编译包。这意味着库的长度是已知的,在这种情况下,相当于就知道了进程的虚拟地址空间分布的部分细节。

理论上,可以为此建立一个大型数据库。对于Ubuntu,它将包含库的版本,包括ld、libc、libp线程和libm;对于每个版本的库,可以分析它所需的多个版本的库(依赖关系)。因此,通过了解一个库的地址,就可以知道描述部分进程地址空间分布的可能映射版本。

这类数据库的例子有libcdb.com和libc.blukat.me,它们用于根据已知函数的偏移量来标识libc版本。

这意味着加载库的固定方法是应用程序的安全问题。上一节描述的mmap的行为使问题复杂化。在Android中,这个问题在版本7和更高版本[8][9]中得到了解决。

=====================================================
ii.3.固定执行顺序
=====================================================

程序有一个有趣的属性:在执行线程中有一对特定的点,在这对点之间程序状态是可预测的。例如,一旦客户端连接到网络服务,服务就会向客户端分配一些资源。这些资源的一部分可以从应用程序堆中分配。在这种情况下,对象在堆中的相对位置通常是可预见的。

通过“生成”攻击者所需的程序状态,此属性对于利用应用程序非常有用。在这里,我们称这种状态为固定的执行顺序。

在此属性的某些情况下,执行线程中有某个不动点。此时,从执行开始,从启动到启动,除了一些变量之外,程序状态保持不变。例如,在执行主函数之前,ld解释器必须加载和初始化所有库,然后初始化程序。如第4.2节所述,库的相对位置差将永远相同。在执行主函数期间,差异将包括用于程序加载、库、堆栈、堆和分配在内存中的对象的特定地址。这些差异是由于第6节所述的随机化造成的。

因此,攻击者可以获得关于程序数据相对位置的信息。此位置不受进程地址空间随机化的影响。

在这个阶段,唯一可能的熵来源是线程之间的竞争:如果程序创建了几个线程,它们在处理数据时的竞争可能会将熵引入对象的位置。在本例中,通过程序全局构造函数或所需库的帮助,可以在执行主函数之前创建线程。

当程序开始使用堆并从堆中分配内存时(通常是在new/malloc的帮助下),在每次启动之前,堆中对象的相互位置将保持不变。

在某些情况下,线程和堆栈的位置相对于库地址也是可预测的。

如果需要,可以获得这些偏移量以用于开发。一种方法是简单地为这个应用程序执行“strace -e mmap”两次,并比较地址的差异。

=====================================================
ii.4. Holes
=====================================================

如果应用程序使用mmap分配内存,然后释放该内存的一部分,则会导致被占用区域包围的hole。如果此空闲内存(hole)再次分配给易受攻击的对象(应用程序在其处理过程中有漏洞的对象),则可能会出现问题。这就使我们回到了内存中对象位置挨得太紧的问题。

Linux内核的ELF文件加载代码中找到了这样一个Hole的说明性示例:加载ELF文件时,内核首先读取文件的大小,并尝试通过do_mmap将其完全映射。一旦文件被完全加载,第一个段之后的内存将被释放。之后的所有段都加载在相对于第一个段设置的固定地址(MAP_FIXED)处。
当然,肯定是要这么做的,因为如果你想把整个文件加载到选定的地址,并根据ELF文件中的描述按权限、偏移量给各个段分配内存,就只能这么做。
但问题在于,如果ELF文件段的描述就不是连续的,这么分配的话就会导致内存中段不连续,也就是出现了hole。

但对于ld解释器(glibc)来说,在加载ELF文件期间将不调用Unmap,而是将空闲页面(hole)的权限更改为PROT_NONE,这将禁止进程访问这些页面。这种方法更安全。

要显示该问题的影响,请从以下命令开始:
$ strace -e mmap,munmap,open,openat,arch_prctl -f cat /proc/self/maps

openat(AT_FDCWD, “/etc/ld.so.cache”, O_RDONLY|O_CLOEXEC) = 3
mmap(NULL, 79806, PROT_READ, MAP_PRIVATE, 3, 0) = 0×7fa5fd8bc000

mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
0×7fa5fd8ba000
arch_prctl(ARCH_SET_FS, 0×7fa5fd8bb500) = 0
munmap(0×7fa5fd8bc000, 79806) = 0

openat(AT_FDCWD, “/proc/self/maps”, O_RDONLY) = 3
mmap(NULL, 139264, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
0×7fa5fd898000

560d3c3e5000-560d3c3ed000 r-xp 00000000 08:01 1966104 /bin/cat
560d3c5ec000-560d3c5ed000 r–p 00007000 08:01 1966104 /bin/cat
560d3c5ed000-560d3c5ee000 rw-p 00008000 08:01 1966104 /bin/cat
560d3e00b000-560d3e02c000 rw-p 00000000 00:00 0 [heap]
7fa5fcebc000-7fa5fd2c9000 r–p 00000000 08:01 3016021 /usr/lib/locale/locale-archive
7fa5fd2c9000-7fa5fd49f000 r-xp 00000000 08:01 1179731 /lib/x86_64-linux-gnu/libc-2.26.so
7fa5fd49f000-7fa5fd69f000 —p 001d6000 08:01 1179731 /lib/x86_64-linux-gnu/libc-2.26.so
7fa5fd69f000-7fa5fd6a3000 r–p 001d6000 08:01 1179731 /lib/x86_64-linux-gnu/libc-2.26.so
7fa5fd6a3000-7fa5fd6a5000 rw-p 001da000 08:01 1179731 /lib/x86_64-linux-gnu/libc-2.26.so
7fa5fd6a5000-7fa5fd6a9000 rw-p 00000000 00:00 0
7fa5fd6a9000-7fa5fd6d0000 r-xp 00000000 08:01 1179654 /lib/x86_64-linux-gnu/ld-2.26.so
7fa5fd898000-7fa5fd8bc000 rw-p 00000000 00:00 0
7fa5fd8d0000-7fa5fd8d1000 r–p 00027000 08:01 1179654 /lib/x86_64-linux-gnu/ld-2.26.so
7fa5fd8d1000-7fa5fd8d2000 rw-p 00028000 08:01 1179654 /lib/x86_64-linux-gnu/ld-2.26.so
7fa5fd8d2000-7fa5fd8d3000 rw-p 00000000 00:00 0
7ffc0a6bc000-7ffc0a6dd000 rw-p 00000000 00:00 0 [stack]
7ffc0a730000-7ffc0a733000 r–p 00000000 00:00 0 [vvar]
7ffc0a733000-7ffc0a735000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
munmap(0×7fa5fd898000, 139264) = 0
+++ exited with 0 +++

这里“/etc/ld.so.cache”通过mmap映射到了0×7fa5fd8bc000,即ld-2.26.so内部的hole上。稍后调用mmap获取大小为8192的0×7fa5fd8ba000地址。此范围包含主线程的TCB(线程控制块)结构,设置为arch_prctl(ARCH_SET_FS, 0×7fa5fd8bb500)。在调用“cat”程序的“main”函数之前,0×7fa5fd8bc000未映射,在tcb和ld只读段之间留下了另一个hole。

这两个hole:
1.在ld.text和0×7fa5fd8ba000之间,TCB使用的是什么。
2.在0×7fa5fd8bc000(TCB的结束)和ld.rdOnly之间,如果攻击者在这些漏洞中执行mmap之后容易遭受攻击对象(只需指定大小)并使用读/写超出限制的漏洞来访问LD数据或TCB或任何其他库数据,则可能使用该漏洞绕过ASLR。

以‘/self/proc/map’本身为例,它被mmap到0×7fa5fd898000的hole中。
在用户态调用mmap的另一种方法是使用长度很大的malloc调用。本例中是glibc的malloc调用的mmap。

=====================================================
ii.5. TLS和线程栈
=====================================================

线程本地存储(TLS)是一种机制,在这种机制中,多线程进程中的每个线程都可以为数据存储分配位置[10]。该机制在不同的体系结构和操作系统上实现不同。在我们的示例中,这是x86-64下的glibc实现。对于x86来说,对于所讨论的mmap问题,任何差异都不会是实质性的。

对于glibc的代码来说,mmap也用于创建TLS。这意味着TLS也是按照这里已经描述的方式选择的。如果TLS接近易受攻击的对象,则可以对其进行更改。

TLS有什么有趣的?在glibc实现中,段寄存器fs(对于x86-64体系结构)指向TLS。其结构由glibc源文件中定义的tcbhead_t类型描述:

typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard;
uintptr_t pointer_guard;

} tcbhead_t;

这种类型包含字段Stack_看板,它包含一个所谓的canary——一个随机或伪随机的数字,用于保护应用程序免受栈溢出[11]的影响。
这种保护的工作方式如下:当输入一个函数时,从tcbhead_t.stack_guard 获得的一个canary被放置在栈上。在函数的末尾,将栈值与tcbhead_t.stack_guard中的引用值进行比较。如果两个值不匹配,应用程序将返回错误并终止。

可以通过几种方式绕过canary:

-如果攻击者不需要覆盖此值[12]。
-如果攻击者设法读取或预期该值,则有可能执行成功的攻击[12]。
-如果攻击者能够用已知值覆盖此值,则可能导致栈溢出[12]。
-攻击者可以在应用程序终止[13]之前进行控制。
-列出的旁路突出了保护TLS不被攻击者读取或覆盖的重要性。

我们的研究表明,glibc对于使用pthread_create创建的线程来说,在TLS实现中存在一个问题。假设需要为新线程选择TLS。为堆栈分配内存后,glibc。
在此内存的上层地址初始化TLS。在这里考虑的x86-64体系结构上,堆栈向下增长,将TLS放在堆栈的顶部。
从TLS中减去某个常量值,得到一个新线程用于栈寄存器的值。从TLS到参数传递给pthread_create的函数的栈帧的距离小于一页。

现在,一个可能的攻击者不需要猜测或窃取canary-攻击者只需将引用值与栈值一起覆盖,就可以完全绕过保护。在Intel ME[14]中也发现了类似的问题。

以下是概念的证明:

void pwn_payload() {
char *argv[2] = {”/bin/sh”, 0};
execve(argv[0], argv, 0);
}

int fixup = 0;
void * first(void *x)
{
unsigned long *addr;
arch_prctl(ARCH_GET_FS, &addr);
printf(”thread FS %pn”, addr);
printf(”cookie thread: 0x%lxn”, addr[5]);
unsigned long * frame = __builtin_frame_address(0);
printf(”stack_cookie addr %p n”, &frame[-1]);
printf(”diff : %lxn”, (char*)addr - (char*)&frame[-1]);
unsigned long len =(unsigned long)( (char*)addr - (char*)&frame[-1]) +
fixup;
// example of exploitation
// prepare exploit
void *exploit = malloc(len);
memset(exploit, 0×41, len);
void *ptr = &pwn_payload;
memcpy((char*)exploit + 16, &ptr, 8);
// exact stack-buffer overflow example
memcpy(&frame[-1], exploit, len);
return 0;
}

int main(int argc, char **argv, char **envp)
{
pthread_t one;
unsigned long *addr;
void *val;
arch_prctl(ARCH_GET_FS, &addr);
if (argc > 1)
fixup = 0×30;
printf(”main FS %pn”, addr);
printf(”cookie main: 0x%lxn”, addr[5]);
pthread_create(&one, NULL, &first, 0);
pthread_join(one,&val);
return 0;
}

运行它:

blackzert@…sher:~/aslur/tests$ ./thread_stack_tls 1
main FS 0×7f4d94b75700
cookie main: 0×2ad951d602d94100
thread FS 0×7f4d94385700
cookie thread: 0×2ad951d602d94100
stack_cookie addr 0×7f4d94384f48
diff : 7b8
$ ^D
blackzert@…sher:~/aslur/tests$

`Diff`这里是当前堆栈帧和TCB结构之间以字节为单位的大小。
这个等于0×7b8字节,少了一页。
缓冲区溢出可以绕过缓冲区溢出的保护。

=====================================================
ii.6.malloc和mmap。
=====================================================

在使用malloc时,如果请求的内存的大小大于某个值,则glibc有时会使用mmap来分配新的内存区域。在这种情况下,内存将在mmap的帮助下分配,因此,在内存分配之后,地址将接近使用mmap分配的库或其他数据。
攻击者密切关注堆对象处理中的错误,例如堆溢出、释放后使用[15]和类型混淆[5]。

当程序使用pthread_create时,会发现glibc库有一个有趣的行为。在第一次从用pthread_create创建的线程调用malloc时,glibc将调用mmap为这个堆栈创建一个新堆。因此,在这个线程中,通过malloc调用的所有地址都将位于同一个线程的堆栈附近。

一些程序和库使用mmap将文件映射到进程的地址空间。例如,这些文件可用作高速缓存或用于快速保存(改变)驱动器上的数据。

下面是一个抽象的示例:应用程序在mmap的帮助下加载MP3文件。让我们调用加载地址mmap_mp3。然后,应用程序从加载的数据读取到音频数据开始的偏移量。如果应用程序在其例行程序中包含验证该值长度的错误,则攻击者可以专门创建一个MP3文件,该文件能够访问mmap_mp3之后的内存区域。

Some PoC code:

int main(int argc, char **argv, char **envp)
{
int res;
system(”"); // call to make lazy linking
execv(”", NULL); // call to make lazy linking
unsigned long addr = (unsigned long)mmap(0, 8 * 4096 *4096, 3, MAP_ANON |
MAP_PRIVATE, -1, 0);
if (addr == MAP_FAILED)
return -1;
unsigned long addr_system = (unsigned long)dlsym(RTLD_NEXT, “system”);
unsigned long addr_execv = (unsigned long)dlsym(RTLD_NEXT, “execv”);
printf(”addr %lx system %lx execv %lxn”, addr, addr_system, addr_execv);
printf(”system - addr %lx execv - addr %lxn”, addr_system - addr,
addr_execv - addr);
return 0;
}

And results:
blackzert@…sher:~/aslur/tests$ ./mmap_libc
addr 7f02e9f85000 system 7f02f1fca390 execv 7f02f2051860
system - addr 8045390 execv - addr 80cc860

这显示了从mmap段到链接库数据的常量偏移(malloc也是这样)。

=====================================================
ii.7. MAP_FIXED的情况下ET_DYN ELF文件的加载。
=====================================================

mmap手册中关于MAP_FIXED式标志的说明如下:

MAP_FIXED

不要将addr解释为hint:一定要将映射放置在准确的地址。
addr必须是页大小的倍数。如果addr和len指定的内存区域与任何现有映射的页面重叠,则现有映射的重叠部分将被丢弃。如果无法使用指定的地址,mmap()将失败。由于映射需要固定地址的可移植性较低,因此不鼓励使用此选项。

如果带有MAP_FIXED标志的请求区域与现有区域重叠,则成功执行mmap将覆盖现有区域。

因此,如果程序员错误地使用MAP_FIXED,则可能会重新定义现有的内存区域。

在Linux内核和glibc中都发现了这样一个错误的有趣示例。
如[16]所述,ELF文件必须符合以下要求:在Phdr头中,ELF文件段必须按vaddr地址的升序排列:

PT_LOAD。

数组元素指定一个可加载的段,由p_filesz、p_memsz描述。文件中的字节映射到内存段的开头。
如果段的内存大小(p_memsz)大于文件大小(p_filesz),则定义“额外”字节来保存值0并跟踪段的初始化区域。文件大小不能大于内存大小。
程序头表中的可加载段条目按升序显示,在p_vaddr成员上排序。

但是,这一要求没有被检查。ELF文件加载的(作者写作时)最新代码如下:

case PT_LOAD:
struct loadcmd *c = &loadcmds[nloadcmds++];
c->mapstart = ALIGN_DOWN (ph->p_vaddr, GLRO(dl_pagesize));
c->mapend = ALIGN_UP (ph->p_vaddr + ph->p_filesz, GLRO(dl_pagesize));

maplength = loadcmds[nloadcmds - 1].allocend - loadcmds[0].mapstart;

for (const struct loadcmd *c = loadcmds; c < &loadcmds[nloadcmds]; ++c)
...
/* Map the segment contents from the file. */
if (__glibc_unlikely (__mmap ((void *) (l->l_addr + c->mapstart),
maplen, c->prot,
MAP_FIXED|MAP_COPY|MAP_FILE,
fd, c->mapoff)

所有段都按照以下算法处理:

1.计算加载的ELF文件的大小:最后一个段结束的地址减去第一个段的开始地址。
2.在mmap的帮助下,为这个大小的整个ELF文件分配内存,从而获得ELF文件加载的基本地址。
3.对于glibc,更改访问权限。如果从内核加载,释放造成漏洞的区域。这里,glibc和Linux内核的行为不同,如第4.4节所述。
4.借助mmap和MMAP_FIXED标志,通过使用通过隔离第一段获得的地址和添加从ELF文件头获得的偏移量来分配剩余段的内存。

这使入侵者能够创建一个恶意ELF文件,其中一个段可以完全覆盖现有的内存区域,例如线程的栈、堆或库代码。

易受攻击的应用程序的一个例子是LDD工具,该工具用于检查系统中是否存在所需的库:

blackzert@…sher:~/aslur/tests/evil_elf$ ldd ./main
linux-vdso.so.1 => (0×00007ffc48545000)
libevil.so => ./libevil.so (0×00007fbfaf53a000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0×00007fbfaf14d000)
/lib64/ld-linux-x86-64.so.2 (0×000055dda45e6000)

该工具使用ld解释器。利用刚才讨论的ELF文件加载问题,我们成功地使用LDD执行了任意代码:

blackzert@…sher:~/aslur/tests/evil_elf$ ldd ./main
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
blackzert@…sher:~/aslur/tests/evil_elf$

在之前的Linux社区中也提出过MAP_FIXED的问题[17]。

Sunday, July 1, 2018 by blast