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



机器翻译加简单人工润色,翻译自:http://www.openwall.com/lists/oss-security/2018/02/27/5

相关讨论:https://lkml.org/lkml/2018/3/22/484

==========================================
II.8. 已分配内存缓存。
==========================================

glibc有许多不同的缓存,其中两个缓存在ASLR的上下文中很有趣:新创建线程的栈的缓存和栈。栈缓存的工作方式如下:在线程终止时,不会释放栈内存,而是将其传输到相应的缓存中。创建线程栈时,glibc首先检查缓存。如果缓存包含所需的内容,glibc使用该区域。在这种情况下,mmap将不会被访问,新线程将使用具有相同地址的以前使用的区域。如果攻击者成功地获得了线程堆栈地址,并且能够控制程序线程的创建和删除,则入侵者可以利用该地址的知识进行漏洞攻击。此外,如果应用程序包含未初始化的变量,它们的值也可能受攻击者的控制,这在某些情况下可能会导致攻击。

堆缓存的工作方式如下:在线程终止时,其堆移动到相应的缓存。当为新线程再次创建堆时,首先检查缓存。如果缓存具有可用区域,则将使用此区域。在本例中,上一段中有关堆栈的所有内容在这里也适用。

Here is PoC:

void * func(void *x)
{
long a[1024];
printf(”addr: %pn”, &a[0]);
if (x)
printf(”value %lxn”, a[0]);
else
{
a[0] = 0xdeadbeef;
printf(”value %lxn”, a[0]);
}
void * addr = malloc(32);
printf(”malloced %pn”, addr);
free(addr);
return 0;
}

int main(int argc, char **argv, char **envp)
{
int val;
pthread_t thread;
printf(”thread1n”);
pthread_create(&thread, NULL, func, 0);
pthread_join(thread, &val);
printf(”thread2n”);
pthread_create(&thread, NULL, func, 1);
pthread_join(thread, &val);
return 0;
}

blackzert@…sher:~/aslur/tests$ ./pthread_cache
thread1
addr: 0×7fd035e04f40
value deadbeef
malloced (b) 0×7fd030000cd0
thread2
addr: 0×7fd035e04f40
value deadbeef
malloced 0×7fd030000cd0

可以清楚地看到,连续创建的线程堆栈中局部变量的地址保持不变。同样,通过malloc为它们分配的变量的地址也是一样的;第二个线程仍然可以访问第一个线程的本地变量的一些值。攻击者可以利用此漏洞攻击未初始化变量的漏洞[18]。尽管缓存加快了应用程序的速度,但它也使攻击者能够绕过ASLR并执行攻击。

==========================================
II.9.线程缓存对齐。
==========================================

现在让我们创建一个线程,使用malloc分配一些内存,并计算与这个线程中的局部变量的差值。源代码:

void * first(void *x)
{
int a = (int)x;
int *p_a = &a;
void *ptr;
ptr = malloc(8);
printf(”%lxn%p, %pn”, (unsigned long long)ptr - (unsigned long long)p_a,
ptr, p_a);
return 0;
}

int main()
{
pthread_t one;
pthread_create(&one, NULL, &first, 0);
void *val;
pthread_join(one,&val);
return 0;
}

The first launch:

blackzert@…sher:~/aslur/tests$ ./thread_stack_small_heap
fffffffff844e98c
0×7f20480008c0, 0×7f204fbb1f34

And the second launch:

blackzert@…sher:~/aslur/tests$ ./thread_stack_small_heap
fffffffff94a598c
0×7fa3140008c0, 0×7fa31ab5af34

在这种情况下,差别是不一样的。每次执行它也不会保持不变。让我们考虑一下原因。

首先要注意的是:malloc派生的指针地址与进程堆地址不对应。

glibc为使用pthread_create创建的每个新线程创建一个新堆。指向此堆的指针位于TLS中,因此任何线程都会从自己的堆中分配内存,这会提高性能,因为在并发使用malloc时不需要同步线程。但为什么地址是“随机的”呢?

分配新堆时,glibc使用mmap;大小取决于配置。在这种情况下,堆大小是64 MB。堆起始地址必须与64 MB对齐。因此,系统首先分配128 MB,然后在此范围内对齐64 MB的块,同时释放未对齐的块,并在堆地址和先前分配mmap的最近区域之间创建一个“hole”。
当选择mmap_based时,内核本身带来了随机性:这个地址没有与64 MB对齐,在调用有关malloc之前的mmap内存分配也是如此。
不管为什么需要地址对齐,这都会导致一个非常有趣的效果:可以暴力破解。

Linux内核将x86-64的进程地址空间定义为“48 bit - 1 protect page”,为了简单起见,我们将其舍入到2^48(在计算大小时省略减1页)。64 MB是2^26,所以有效位等于48-26=22,总共给我们提供了2^22个不同的次线程堆。

这大大缩小了强制范围。

因为mmap地址是以一种已知的方式选择的,所以我们可以假定,使用pthread_create创建的第一个线程的堆将被选择为64 MB,接近上地址范围。更准确地说,它将接近所有加载的库、加载的文件等等。
在某些情况下,可以计算在调用有关malloc之前分配的内存量。在本例中,我们只加载了glibc和ld,并为线程创建了一个栈。所以这个值很小。

mmap_base是根据编译时的内核设置(默认为28位)选择的,熵为28到32位。所以一些上边界被相同的数量所抵消。
因此,在许多情况下,地址的上8位将等于0×7f,而在少数情况下,它们将是0×7e。这给了我们另外8 bit的确定性。为第一个线程选择堆总共有2^14种可能的选项。
创建的线程越多,用于下一个堆选择的值就越少。

让我们用以下C代码演示此行为:

void * first(void *x)
{
    int a = (int)x;
    void *ptr;
    ptr = malloc(8);
    printf("%pn", ptr );
    return 0;
}

int main()
{
    pthread_t one;
    pthread_create(&one, NULL, &first, 0);
    void *val;
    pthread_join(one,&val);
    return 0;
}

然后,让我们用Python代码启动足够多的程序来收集地址统计信息:

import subprocess
d = {}
def dump(iteration, hysto):
    print 'Iteration %d len %d'%(iteration, len(hysto))
    for key in sorted(hysto):
        print hex(key), hysto[key]
i = 0
while i < 1000000:
    out = subprocess.check_output(['./t'])
    addr = int(out, 16)
    #omit page size
    addr >>= 12
    if addr in d:
        d[addr] += 1
    else:
        d[addr] = 1
    i += 1
dump(i,d)

此代码启动简单的“./t”程序,该程序创建一个新线程足够多次。
代码借助malloc分配缓冲区并显示缓冲区地址。一旦程序完成此操作,将读取地址并计算在程序操作期间遇到地址的次数。该脚本总共收集了16,385个不同的地址,等于2^14+1。在最坏的情况下,这是攻击者可以尝试猜测所述程序的堆地址的次数。

=========================================
iii. 解决方案
=========================================

在本文中,我们回顾了几个问题-其中一些涉及到Linux内核,一些涉及到GNU Libc;现在我们可以考虑对Linux内核进行修复。
您可以通过以下链接跟踪GNU Libc问题:

- Stack protector easy to bypass
https://sourceware.org/bugzilla/show_bug.cgi?id=22850
- ld library ELF load error
https://sourceware.org/bugzilla/show_bug.cgi?id=22851
- Thread stack and heap caches
https://sourceware.org/bugzilla/show_bug.cgi?id=22852
- Heap address of pthread_create thread is aligned
https://sourceware.org/bugzilla/show_bug.cgi?id=22853

=========================================
iii.1 holes
=========================================

如第II.4节所示,Linux内核中的ELF解释器加载程序包含一个错误,并允许释放部分解释器库内存。
社区提出了一项相关的解决办法,但没有采取行动:
https://lkml.org/lkml/2017/7/14/290

=========================================
III.2加载ELF文件段的顺序。
=========================================

如上所述,在内核和glibc库的代码中,没有检查文件ELF段:代码只是相信它们的顺序是正确的。随函附上概念证明代码和修复程序:
https://github.com/blackzert/aslur

修复非常简单:我们遍历各个段并确保当前段与下一个段重叠,并且这些段按vaddr的升序排序。

https://lkml.org/lkml/2018/2/26/571

=========================================
iii.3在搜索mmap分配地址时使用mmap_min_addr。
=========================================

一旦为mmap编写了修复程序,为了以足够的熵返回地址,就会出现一个问题:一些mmap调用失败,导致访问权限错误。在执行execve时,即使是作为根用户或内核请求时也会发生这种情况。

在地址选择算法(前面介绍)中,列出的选项之一是检查地址是否有安全限制。在当前实现中,此检查验证所选地址是否大于mmap_min_addr。这是一个系统变量,管理员可以通过sysctl对其进行更改。系统管理员可以设置任何值,并且。
进程不能以小于此值的地址分配页。默认值是65536。

问题是,当在x86-64上调用mmap的地址函数时,Linux内核使用4096作为最小下限的值,这个值小于mmap_min_addr的值。如果所选地址介于4096和mmap_min_addr之间,函数capmmap_addr禁止此操作。

capmmap_addr被隐式调用;这个函数被注册为一个用于安全检查的钩子。这种体系结构解决方案提出了一些问题:首先,我们在没有能力用外部标准测试地址的情况下选择地址,然后根据当前的系统参数检查它的允许性。如果该地址没有通过检查,那么即使该地址是由内核选择的,也会被“禁止”,整个操作将以EPERM错误结束。

攻击者可以利用此问题在整个系统中造成拒绝服务:
如果攻击者能够指定一个非常大的值,系统就无法启动任何用户进程。此外,如果攻击者设法将该值存储在系统参数中,则即使重新引导也不会有任何帮助-所有创建的进程都将因EPERM错误而终止。

目前,修复方法是在向地址搜索函数发出请求时使用mmap_min_addr值作为允许的最低地址。这样的代码已经用于所有其他体系结构。
如果系统管理员开始在运行中的计算机上更改此值,会发生什么情况?这个问题仍然没有得到回答,因为更改之后的所有新分配都可能以EPERM错误结束;没有程序代码希望出现这样的错误,也不知道如何处理它。mmap文档说明如下:

“EPERM的处理函数阻止了这项操作;见fcntl(2)。”

也就是说,内核不能将EPERM返回到MAP_ANONYMOUS,尽管事实上并非如此。

https://lkml.org/lkml/2018/2/26/1053

=========================================
iii.4 mmap
=========================================

这里讨论的主要mmap问题是地址选择中缺少熵。
理想情况下,逻辑上的解决办法是随机选择内存。要随机选择它,必须首先建立一个具有适当大小的所有自由区域的列表,然后从该列表中选择满足搜索标准(请求区域的长度和允许的上下边界)的随机区域和该区域的地址。

为了实现这一逻辑,可以采用以下方法:

1.将空隙列表保存在降序数组中。在这种情况下,随机元素的选择是在单个操作中进行的,但是维护这个数组需要很多操作,以便在进程的当前虚拟地址空间映射发生变化时释放(分配)内存。
2.将空隙列表保存在树和列表中,以便找到满足长度要求的外部边界,并从数组中选择一个随机元素。如果该元素不符合最小/最大地址限制,则选择下一个,依此类推,直到找到一个(或不保留)。这种方法涉及复杂的列表和树结构,类似于VMA在地址空间更改方面已经存在的结构。
3.使用增广红黑VMA树的现有结构来绕过允许的空隙列表,并选择一个随机地址。在最坏的情况下,每个选择都必须绕过所有的峰值,但是重建树不会导致性能的任何额外的下降。

我们的选择走到了最后一条路。我们可以使用现有的VMA。
不添加冗余的组织结构,并使用以下算法选择地址:
1.使用现有算法查找具有最大有效地址的可能空白。同时,记录下VMA的结构。如果没有这样的结构,返回ENOMEM。
2.将发现的间隙记录为结果,将VMA记录为最大上边界。
3.从双链接列表中获取第一个VMA结构。它将是红-黑树上的一片叶子,因为它有最小的地址.。
4.从选定的VMA中对树进行左遍历,检查有关VMA与其前身之间的空闲区域的允许性。如果自由区域被限制所允许,则获得另一位熵。如果熵位为1,则重新定义空隙的当前值。
5.从选定的间隙空区返回随机地址。

优化算法的第四步的一种方法是不进入间隙扩展大小小于所需长度的子树。

此算法选择一个具有足够熵的地址,尽管它比当前的实现要慢。

至于明显的缺点,有必要绕过所有具有足够空隙长度的VMA结构。但是,在更改地址空间时,不会出现任何性能下降,从而抵消了这一点。

下面是修补程序之后的`less /proc/self/maps‘的输出:

314a2d0da000-314a2d101000 r-xp /lib/x86_64-linux-gnu/ld-2.26.so
314a2d301000-314a2d302000 r–p /lib/x86_64-linux-gnu/ld-2.26.so
314a2d302000-314a2d303000 rw-p /lib/x86_64-linux-gnu/ld-2.26.so
314a2d303000-314a2d304000 rw-p

3169afcd8000-3169afcdb000 rw-p

316a94aa1000-316a94ac6000 r-xp /lib/x86_64-linux-gnu/libtinfo.so.5.9
316a94ac6000-316a94cc5000 —p /lib/x86_64-linux-gnu/libtinfo.so.5.9
316a94cc5000-316a94cc9000 r–p /lib/x86_64-linux-gnu/libtinfo.so.5.9
316a94cc9000-316a94cca000 rw-p /lib/x86_64-linux-gnu/libtinfo.so.5.9

3204e362d000-3204e3630000 rw-p

4477fff2c000-447800102000 r-xp /lib/x86_64-linux-gnu/libc-2.26.so
447800102000-447800302000 —p /lib/x86_64-linux-gnu/libc-2.26.so
447800302000-447800306000 r–p /lib/x86_64-linux-gnu/libc-2.26.so
447800306000-447800308000 rw-p /lib/x86_64-linux-gnu/libc-2.26.so
447800308000-44780030c000 rw-p

509000396000-509000d60000 r–p /usr/lib/locale/locale-archive

56011c1b1000-56011c1d7000 r-xp /bin/less
56011c3d6000-56011c3d7000 r–p /bin/less
56011c3d7000-56011c3db000 rw-p /bin/less
56011c3db000-56011c3df000 rw-p

56011e0d8000-56011e0f9000 rw-p [heap]

7fff6b4a4000-7fff6b4c5000 rw-p [stack]
7fff6b53b000-7fff6b53e000 r–p [vvar]
7fff6b53e000-7fff6b540000 r-xp [vdso]
ffffffffff600000-ffffffffff601000 r-xp [vsyscall]

https://lkml.org/lkml/2018/2/27/267

========================================================================
IV. Related Work
========================================================================

The problem with current mmap behavior was also found by other researches:

Hector Marco-Gisbert, Ismael Ripoll-Ripoll. ASLR-NG: ASLR Next Generation. 2016
https://cybersecurity.upv.es/solutions/aslr-ng/ASLRNG-BH-white-paper.pdf

Julian Kirsch, Bruno Bierbaumer, Thomas Kittel and Claudia Eckert Dynamic
Loader Oriented Programming on Linux. 2017
https://github.com/kirschju/wiedergaenger/blob/master/kirsch-roots-2017-paper.pdf

========================================================================
V. References
========================================================================

1. https://lkml.org/lkml/2012/11/5/673
2. https://cwe.mitre.org/data/definitions/119.html
3. https://cwe.mitre.org/data/definitions/190.html
4. https://cwe.mitre.org/data/definitions/120.html
5. https://cwe.mitre.org/data/definitions/704.html
6. https://cybersecurity.upv.es/attacks/offset2lib/offset2lib.html
7. https://www.cvedetails.com/cve/CVE-2014-9427/
8. https://source.android.com/security/enhancements/enhancements70
9. https://android-review.googlesource.com/c/platform/bionic/+/178130/2
10. http://gcc.gnu.org/onlinedocs/gcc-3.3/gcc/Thread-Local.html
11. http://www.phrack.org/issues/49/14.html#article
12.
http://www.blackhat.com/presentations/bh-europe-09/Fritsch/Blackhat-Europe-2009-Fritsch-Buffer-Overflows-Linux-whitepaper.pdf
13. https://crypto.stanford.edu/cs155old/cs155-spring05/litch.pdf
14.
https://www.blackhat.com/docs/eu-17/materials/eu-17-Goryachy-How-To-Hack-A-Turned-Off-Computer-Or-Running-Unsigned-Code-In-Intel-Management-Engine-wp.pdf
15. https://cwe.mitre.org/data/definitions/416.html
16. http://www.skyfree.org/linux/references/ELF_Format.pdf
17. https://lwn.net/Articles/741335/
18. https://cwe.mitre.org/data/definitions/457.html
19. http://blog.ptsecurity.com/2018/02/new-bypass-and-protection-techniques.html

All sources and patches could be found at https://github.com/blackzert/aslur
repo.

Sunday, July 1, 2018 by blast