Saturday, March 17, 2012

Some random observations on Linux ASLR

I've had cause to be staring at memory maps recently across a variety of systems. No surprise then that some suboptimal or at least interesting ASLR quirks have come to light.

1) Partial failure of ASLR on 32-bit Fedora

My Fedora is a couple of releases behind, so no idea if it's been fixed. It seems that the desire to pack all the shared libraries into virtual address 0x00nnnnnn has a catastrophic failure mode when there are too many libraries: something always ends up at 0x00110000. You can see it with repeated invocations of ldd /opt/google/chrome/chrome|grep 0x0011: => /lib/ (0x00110000) => /usr/lib/ (0x00110000) => /lib/ (0x00110000)

Which exact library is placed at the fixed address is random. However, any fixed address can be a real problem to ASLR. For example, in the browser context, take a bug such as Chris Rohlf's older but interesting CSS type confusion. Without a fixed address, a crash is a likely outcome. With a fixed address, the exact library mapped at the fixed address could easily be fingerprinted, and the BSS section read to leak heap pointers (e.g. via singleton patterns). Bye bye to both NX and ASLR.

Aside: in the 32-bit browser context with plenty of physical memory, a Javascript-based heap spray could easily fill most of the address space such that the attacker's deference has a low chance of failure.

Aside #2: my guess is that this scheme is designed to prevent a return-to-glibc attack vs. strcpy(), by making sure that all executable addresses contain a NULL byte. I'm probably missing something, but it seems like the fact that strcpy() NULL-terminates, combined with the little-endianness of Intel, makes this not so strong.

2) Missed opportunity to use more entropy on 64-bit

If you look at the maps of a 64-bit process, you'll see most virtual memory areas correspond to the formula 0x7fnnxxxxxxxx where all your stuff is piled together in xxxxxxxx and nn is random. At least, nothing is in or near a predictable location. One way to look at how this could be better is this: If you emit a 4GB heap spray, you have a ~1/256 chance of guessing where it is. Using the additional 7 bits of entropy might be useful, especially for the heap.

3) Bad mmap() randomization

Although the stack, heap and binary are placed at reasonably random locations, unhinted mmap() chunks are sort of just piled up adjacent, typically in a descending-vm-address fashion. This can lead to problems where a buffer overflow crashes into a sensitive mapping -- such a JIT mapping. (This is one reason JIT mappings have their own randomizing allocator in v8).

4) Heap / stack collision likely with ASLR binary

On a 32-bit kernel you might see:

b8105000-b8124000 rw-p 00000000 00:00 0 [heap]
bfae5000-bfb0a000 rw-p 00000000 00:00 0 [stack]

Or on a 64-bit kernel running a 32-bit process:

f7c52000-f7c73000 rw-p 00000000 00:00 0 [heap]
ff948000-ff96d000 rw-p 00000000 00:00 0 [stack]

In both cases, the heap doesn't have to grow too large before it cannot grow any larger. When this happens, most heap implementations fall back to mmap() allocations, and suffer the problems of 3) above. These things chained together with a very minor infoleak such as my cross-browser XSLT heap address leak could in fact leak the location of the executable, leading to a full NX/ASLR bypass.


A 32-bit address space just isn't very big any more, compared with todays large binaries, large number of shared library dependencies and large heaps. It's no surprise that everything is looking a little crammed in. The good news is that there are no obvious and severe problems with the 64-bit situation, although the full entropy isn't used. Applications (such as v8 / Chromium) can and do fix that situation for the most sensitive mappings themselves.


Jon Oberheide said...

FWIW, PaX addresses most of these issues.

A modern non-PaX kernel:

7f9cedfa5000-7f9cee12a000 r-xp 00000000 fe:00 2375596 /lib64/
7f9cee12a000-7f9cee329000 ---p 00185000 fe:00 2375596 /lib64/
7f9cee329000-7f9cee32d000 r--p 00184000 fe:00 2375596 /lib64/
7f9cee32d000-7f9cee32e000 rw-p 00188000 fe:00 2375596 /lib64/
7f9cee32e000-7f9cee333000 rw-p 00000000 00:00 0
7f9cee333000-7f9cee354000 r-xp 00000000 fe:00 2375907 /lib64/
7f9cee35f000-7f9cee524000 r--p 00000000 fe:00 658077 /usr/lib64/locale/locale-archive
7f9cee524000-7f9cee527000 rw-p 00000000 00:00 0
7f9cee552000-7f9cee553000 rw-p 00000000 00:00 0
7f9cee553000-7f9cee554000 r--p 00020000 fe:00 2375907 /lib64/
7f9cee554000-7f9cee555000 rw-p 00021000 fe:00 2375907 /lib64/
7f9cee555000-7f9cee556000 rw-p 00000000 00:00 0
7f9cee556000-7f9cee563000 r-xp 00000000 fe:00 1212040 /bin/cat
7f9cee762000-7f9cee763000 r--p 0000c000 fe:00 1212040 /bin/cat
7f9cee763000-7f9cee764000 rw-p 0000d000 fe:00 1212040 /bin/cat
7f9cf02be000-7f9cf02df000 rw-p 00000000 00:00 0 [heap]
7fffe4597000-7fffe45b8000 rw-p 00000000 00:00 0 [stack]
7fffe45ff000-7fffe4600000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

A modern PaX kernel:

407c7d9000-407c7e6000 r-xp 00000000 fe:00 1212040 /bin/cat
407c9e5000-407c9e6000 r--p 0000c000 fe:00 1212040 /bin/cat
407c9e6000-407c9e7000 rw-p 0000d000 fe:00 1212040 /bin/cat
407c9e7000-407ca0b000 rw-p 00000000 00:00 0 [heap]
30172765000-301728ea000 r-xp 00000000 fe:00 2375596 /lib64/
301728ea000-30172ae9000 ---p 00185000 fe:00 2375596 /lib64/
30172ae9000-30172aed000 r--p 00184000 fe:00 2375596 /lib64/
30172aed000-30172aee000 rw-p 00188000 fe:00 2375596 /lib64/
30172aee000-30172af3000 rw-p 00000000 00:00 0
30172af3000-30172b14000 r-xp 00000000 fe:00 2375907 /lib64/
30172b1d000-30172ce2000 r--p 00000000 fe:00 658077 /usr/lib64/locale/locale-archive
30172ce2000-30172ce5000 rw-p 00000000 00:00 0
30172d10000-30172d11000 rw-p 00000000 00:00 0
30172d11000-30172d13000 r-xp 00000000 00:00 0 [vdso]
30172d13000-30172d14000 r--p 00020000 fe:00 2375907 /lib64/
30172d14000-30172d15000 rw-p 00021000 fe:00 2375907 /lib64/
30172d15000-30172d16000 rw-p 00000000 00:00 0
3af4b980000-3af4b9a2000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 r--p 00000000 00:00 0 [vsyscall]

Chris Evans said...

@Jon Oberheide: that's really interesting / useful, thanks for adding it. One thing PaX seems to do "wrong" IMHO that the non-PaX doesn't, is to stack the heap directly after the binary. A heap address leak might lead someone to be able to infer the location of the program text for ROPping. Other than that, it looks much better.

Anonymous said...

be careful, if you call for PaX three times..spender might appear!!

Anonymous said...

On recent sudo exploit from vnsecurity, they made it work with PaX kernel as well

spender said...

I'd never want to disappoint such an ardent fan!

You should know that PaX doesn't do anything wrong -- that maps output is deceiving actually. PaX introduces a random offset to the beginning of the brk-managed heap. The easy way to verify this would be an sbrk(0) in a non-PIE binary (paxtest will do something equivalent). It currently randomizes bits 4-16, while upstream randomizes bits 12-25.

That PaX doesn't introduce an artificial gap between the binary image and the brk-managed heap is actually a benefit, as it guarantees an mmap-based allocation can't be located in such a gap where it would have the potential to overflow into the brk-managed heap. Though probably not an issue on 64-bit, this is a real concern on 32-bit.

Since userland has been able to withstand entropy added up to the 25th bit, PaX will likely increase its range to bits 4-25, giving it 21 bits of entropy specifically for the brk-managed heap. Vanilla, however, has no room to expand without changing their detached-brk implementation.


Jon Oberheide said...

@Chris: No prob, and sorry for the horrible looking comment formatting. Re: the heap placement, I'd much rather my RW heap run into a RX text than into my RW stack. Infoleak is better than code execution. :-)

@Anonymous: Everyone knows spender's google alerts are only watching for "grsecurity". Oh no, I mentioned it! It's too yourseeelllf!