Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Heap hardening #14083

Open
1 of 5 tasks
jvoisin opened this issue Apr 30, 2024 · 7 comments
Open
1 of 5 tasks

Heap hardening #14083

jvoisin opened this issue Apr 30, 2024 · 7 comments

Comments

@jvoisin
Copy link
Contributor

jvoisin commented Apr 30, 2024

Description

Currently, PHP's heap implementation is ~trivial to exploit:

There are several hardening techniques that could/should be implemented, listed here in order of difficulty:

  • prevent unlink abuse: Add two checks for zend_mm_heap's integrity #13943
  • prevent freelist-corruption via shadow-pointer à la XNU/linux/partitionAlloc/suhosin/…: Detect heap freelist corruption #14054
  • surround large allocations with guard-pages, as done in partitionAlloc, scudo, … @jvoisin will try to look at implementing this.
  • Freelist randomisation, see Linux' SLAB_FREELIST_RANDOM
  • isolate types in different heaps to make type confusion/use after free more difficult, as in kalloc_type. An additional benefit of type isolation + freelist bitmap is that we wouldn't need the object_store anymore to enumerate objects. A less invasive approach would be to simply isolate by size as done in partitionAlloc and Webkit's libpas
    • once this is done, heaps need to be pinned by size/type, to prevent an attacker from re-using the memory space of a destructed heap with one of different type/size.
    • a low-hanging fruit would be to allocate GET/POST/COOKIES into a separated heap, to reduce the reach of heap feng-shui: Remote heap feng shui / heap spraying protection  #14304
  • allocate strings and array buckets in GigaCages so that a corrupt length doesn't allow to access anything else than other strings/array buckets. This will significantly increase the virtual-memory usage though. this isn't doable since those structures have a maximum size of SIZE_MAX

cc @arnaud-lb @cfreal @therealcoiffeur

@arnaud-lb
Copy link
Member

Thank you for creating this issue! I though a bit more about this since #13943 (comment):

  • Type isolation requires considerable changes to the allocator, as addresses are reused in various scenarios. In particular we need to change zend_mm_gc() so it doesn't release address space, large slots must be allocated in fixed size bins like small ones, and for huge ones I'm not sure. I'm considering the layout used by mimalloc for small/large bins.
  • GigaCages may not be practicable, or may not be effective, for multiple reasons. One is that the maximum string size is SIZE_MAX.
  • ASLR: On Linux the mmap base is randomized by 28 bits by default, but we align chunks to 2MiB and the layout is predictable inside chunks, so we only get 19 bits or randomness in practice. We can improve that without too much effort, so even though ASLR is not a panacea it would still be worth it. Having a different base in every child process, and re-basing from time to time would also help a bit (literally, according to this paper).
  • Under the threat model of a remote attacker, in some scenarios GET/POST may be the only way to heap feng chui. Allocating user inputs in a separate heap would be efficient against heap feng shui in this case.

Longer term, we should check if replacing refcounting+cycle GC by a full tracing GC is practicable, because it would help. Although refcounting can not be entirely removed because CoW semantics rely on it.

@jvoisin
Copy link
Contributor Author

jvoisin commented Apr 30, 2024

  • mimalloc is pretty neat and performant, and I'd recommend looking at isoalloc as well. I spent some time last year trying to produce some easily digestible mitigation/design comparison between userland allocators which might be relevant here, as well as benchmarking the performances of the different allocators, even gave a small talk on the topic
  • Err, indeed data with SIZE_MAX won't fix in a GigaCage, sigh.
  • Unfortunately, having a different base means re-executing the process after the fork, which might significantly impact performances wrt. CoW. It's one of the reasons Android's Zygote doesn't do it. Moreover, I think that the threat model here is "an attacker with (limited) PHP code execution", meaning that ASLR can usually be inferred/ignored in some ways. Randomization applied to freelist would/could help though.
  • Isolating GET/POST is a great idea indeed!

@arnaud-lb
Copy link
Member

Great, thank you!

  • Unfortunately, having a different base means re-executing the process after the fork, which might significantly impact performances wrt. CoW. It's one of the reasons Android's Zygote doesn't do it. Moreover, I think that the threat model here is "an attacker with (limited) PHP code execution", meaning that ASLR can usually be inferred/ignored in some ways. Randomization applied to freelist would/could help though.

Agreed with changing the base entirely. What I had in mind was to use a random mmap hint in zend alloc, and allocate contiguously from that hint (to avoid splitting the address space too much). After that we can randomize bin placement inside chunks (but I feel this can be easily defeated with heap feng shui) and freelists inside bins indeed.

Regarding the threat model, I'm focusing more on the remote attacker model for now, as I feel this is the most critical.

@jvoisin
Copy link
Contributor Author

jvoisin commented Apr 30, 2024

Agreed with changing the base entirely. What I had in mind was to use a random mmap hint in zend alloc, and allocate contiguously from that hint (to avoid splitting the address space too much). After that we can randomize bin placement inside chunks (but I feel this can be easily defeated with heap feng shui) and freelists inside bins indeed.

Oh, I see. Yes, having a randomized per-child base would help a bit, as an attacker wouldn't be able to use forks to bruteforce the randomization, albeit memory allocated before the fork would still be at the same offset across processes. As for periodic rebasing, I guess having the master process re-executing itself once in a while would be an acceptable hack tradeoff.

Remote PHP exploitation is pretty exotic, to my knowledge, to my knowledge, the only person to do it (publicly) is @cfreal. Local exploitation is much more common, usually to bypass open_basedir and disable_functions.

@devnexen
Copy link
Member

...

@jvoisin, just curious ; would you recommend using the userfaultfd api in that case ?

@jvoisin
Copy link
Contributor Author

jvoisin commented Apr 30, 2024

@jvoisin, just curious ; would you recommend using the userfaultfd api in that case ?

I'd rather keep things simple and portable: map two pages PROT_NONE and let the process violently crash in case of violation. I'm under the impression that userfaultfd adds a lot of complexity, which is never a good thing for security-related features.

@devnexen
Copy link
Member

Oh not so much complexity it allows to handle the violation more smoothly than the usual technique you re referring to. But ... that s just linux :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants