malloc/malloc.c: Mitigate null-byte overflow attacks

Message ID 84f0d6ec-0e51-2f7a-db9c-3d3a8971fbc4@cs.ucsb.edu
State Superseded, archived
Headers

Commit Message

Moritz Eckert Oct. 17, 2017, 7:01 p.m. UTC
  Hi,

I want to propose a new check against one-null-byte-null overflow 
attacks in the malloc implementation.
I believe that the patch I am proposing completely fix this issue, 
without incurring in any additionally overhead.

Here are the technical details:

Multiple security checks have been introduced to assert metadata 
consistency in the past.
One particular committed by Chris Evans 
(17f487b7afa7cd6c316040f3e6c86dc96b2eec30) is targeting one-null-byte 
overflows [1]. This patch tries to prevent malicious chunk consolidation 
by verifying the a chunk's prev_size is equal to the corresponding 
previous chunk's size.
Specifically, this check was added into the unlink macro and it asserts 
that size and prev_size are equal. This check is absolutely correct and 
certainly adds additional constraints to any unlink attack.

However, one has to consider that it only aims for one-null-byte 
overflow. That is because any unlink against a fake chunk in controlled 
memory gives the attacker enough control to circumvent this and any 
similar check. (It's important to note that an attacker needs to control 
a particular memory location to set the prev_size, however it's 
relatively common to be able to craft the heap layout accordingly. 
Furthermore, already having an one-null-byte overflow, it's very likely 
that there is enough controlled memory to place the correct prev_size at 
the particular offset). Given a one-null-byte overflow, someone already 
figured out how to circumvent Chris Evan’s patch by setting the correct 
prev_size [4]
Ultimately, this means the current patch fails to prevent any currently 
known attack exploiting a one-null-byte overflow.

I tried to analyze why the patch failed to prevent this attack 
completely. The reason boils down to the fact that the unlink procedure 
will use the incorrect (malicious) prev_size to go back in memory and 
call unlink on the wrong (not previous anymore) chunk. The check now 
tries to verify that size and prev_size starting from the wrong chunk 
match, which will always hold true. The problem occurs because there is 
no check to verify prev_size and size before going back in memory. That 
means the check is correct, but happens when it's already too late.

The patch I propose contains a similar check, but before the unlink 
call. Since, this his highly performance critical code, I assumed people 
would refrain from adding more checks, so instead I placed my check as a 
replacement of the old one. The new check prevents any one-null-byte 
overflow attack and only falls short to unlink against fake chunks. 
Since the old check aims for the exact same goal, but fails to prevent 
it completely, the new check could be seen as a superset of the old one.
However, if the performance impact is not considered too high, my check 
would also be a good addition to the current one.

To sum up:
I think my patch protects completely against one-null-byte overflows, 
while Chris Evan’s one is bypassable. At the same time, I don’t think it 
increases performance overhead.

Cheers,
Moritz

[1] 
https://scarybeastsecurity.blogspot.com/2017/05/further-hardening-glibc-malloc-against.html
[2] 
https://googleprojectzero.blogspot.com/2014/08/the-poisoned-nul-byte-2014-edition.html
[3] https://github.com/shellphish/how2heap/blob/master/poison_null_byte.c
[4] 
https://github.com/shellphish/how2heap/commit/a666abbd1306fb3311144ca7ccfcdba939e77744#diff-196ce929986c83c72e376d5612656d3b


      BK = P->bk;                                      \
      if (__builtin_expect (FD->bk != P || BK->fd != P, 0))           \
@@ -4227,6 +4225,8 @@ _int_free (mstate av, mchunkptr p, int have_lock)
        prevsize = prev_size (p);
        size += prevsize;
        p = chunk_at_offset(p, -((long) prevsize));
+      if (__builtin_expect (chunksize(p) != prevsize, 0))
+        malloc_printerr ("corrupted size vs. prev_size");
        unlink(av, p, bck, fwd);
      }

@@ -4392,6 +4392,8 @@ static void malloc_consolidate(mstate av)
          prevsize = prev_size (p);
          size += prevsize;
          p = chunk_at_offset(p, -((long) prevsize));
+        if (__builtin_expect (chunksize(p) != prevsize, 0))
+          malloc_printerr ("corrupted size vs. prev_size");
          unlink(av, p, bck, fwd);
        }
  

Comments

Moritz Eckert Oct. 23, 2017, 5:29 p.m. UTC | #1
Hi,

Since I didn't receive any reply yet, I wanted to make sure that's not 
because there is something wrong with the form of my patch proposal in 
general, or this being the wrong mailinglist for it?

Thanks,

Moritz


On 10/17/2017 12:01 PM, Moritz Eckert wrote:
> Hi,
>
> I want to propose a new check against one-null-byte-null overflow 
> attacks in the malloc implementation.
> I believe that the patch I am proposing completely fix this issue, 
> without incurring in any additionally overhead.
>
> Here are the technical details:
>
> Multiple security checks have been introduced to assert metadata 
> consistency in the past.
> One particular committed by Chris Evans 
> (17f487b7afa7cd6c316040f3e6c86dc96b2eec30) is targeting one-null-byte 
> overflows [1]. This patch tries to prevent malicious chunk 
> consolidation by verifying the a chunk's prev_size is equal to the 
> corresponding previous chunk's size.
> Specifically, this check was added into the unlink macro and it 
> asserts that size and prev_size are equal. This check is absolutely 
> correct and certainly adds additional constraints to any unlink attack.
>
> However, one has to consider that it only aims for one-null-byte 
> overflow. That is because any unlink against a fake chunk in 
> controlled memory gives the attacker enough control to circumvent this 
> and any similar check. (It's important to note that an attacker needs 
> to control a particular memory location to set the prev_size, however 
> it's relatively common to be able to craft the heap layout 
> accordingly. Furthermore, already having an one-null-byte overflow, 
> it's very likely that there is enough controlled memory to place the 
> correct prev_size at the particular offset). Given a one-null-byte 
> overflow, someone already figured out how to circumvent Chris Evan’s 
> patch by setting the correct prev_size [4]
> Ultimately, this means the current patch fails to prevent any 
> currently known attack exploiting a one-null-byte overflow.
>
> I tried to analyze why the patch failed to prevent this attack 
> completely. The reason boils down to the fact that the unlink 
> procedure will use the incorrect (malicious) prev_size to go back in 
> memory and call unlink on the wrong (not previous anymore) chunk. The 
> check now tries to verify that size and prev_size starting from the 
> wrong chunk match, which will always hold true. The problem occurs 
> because there is no check to verify prev_size and size before going 
> back in memory. That means the check is correct, but happens when it's 
> already too late.
>
> The patch I propose contains a similar check, but before the unlink 
> call. Since, this his highly performance critical code, I assumed 
> people would refrain from adding more checks, so instead I placed my 
> check as a replacement of the old one. The new check prevents any 
> one-null-byte overflow attack and only falls short to unlink against 
> fake chunks. Since the old check aims for the exact same goal, but 
> fails to prevent it completely, the new check could be seen as a 
> superset of the old one.
> However, if the performance impact is not considered too high, my 
> check would also be a good addition to the current one.
>
> To sum up:
> I think my patch protects completely against one-null-byte overflows, 
> while Chris Evan’s one is bypassable. At the same time, I don’t think 
> it increases performance overhead.
>
> Cheers,
> Moritz
>
> [1] 
> https://scarybeastsecurity.blogspot.com/2017/05/further-hardening-glibc-malloc-against.html
> [2] 
> https://googleprojectzero.blogspot.com/2014/08/the-poisoned-nul-byte-2014-edition.html
> [3] https://github.com/shellphish/how2heap/blob/master/poison_null_byte.c
> [4] 
> https://github.com/shellphish/how2heap/commit/a666abbd1306fb3311144ca7ccfcdba939e77744#diff-196ce929986c83c72e376d5612656d3b
>
>
> diff --git a/ChangeLog b/ChangeLog
> index 5effbd7956..f7a76220d2 100644
> --- a/ChangeLog
> +++ b/ChangeLog
> @@ -1,3 +1,12 @@
> +2017-10-13  Moritz Eckert  <m.eckert@cs.ucsb.edu>
> +
> +    * malloc/malloc.c: (_int_free): Compare prev_size and prev->size
> +    before performing the backward unlink, instead of going back in
> +    memory first. This prevents forgotten update type of attacks.
> +
> +    * malloc/malloc.c: (unlink): Remove the current check to have no
> +    performance impact.
> +
>  2017-10-13  James Clarke  <jrtc27@jrtc27.com>
>
>      * sysdeps/powerpc/powerpc32/dl-machine.h (elf_machine_rela):
> diff --git a/malloc/malloc.c b/malloc/malloc.c
> index d3fcadd20e..c421e778da 100644
> --- a/malloc/malloc.c
> +++ b/malloc/malloc.c
> @@ -1395,8 +1395,6 @@ typedef struct malloc_chunk *mbinptr;
>
>  /* Take a chunk off a bin list */
>  #define unlink(AV, P, BK, FD) 
> {                                            \
> -    if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 
> 0))      \
> -      malloc_printerr ("corrupted size vs. prev_size");       \
>      FD = P->fd;                                      \
>      BK = P->bk;                                      \
>      if (__builtin_expect (FD->bk != P || BK->fd != P, 0))           \
> @@ -4227,6 +4225,8 @@ _int_free (mstate av, mchunkptr p, int have_lock)
>        prevsize = prev_size (p);
>        size += prevsize;
>        p = chunk_at_offset(p, -((long) prevsize));
> +      if (__builtin_expect (chunksize(p) != prevsize, 0))
> +        malloc_printerr ("corrupted size vs. prev_size");
>        unlink(av, p, bck, fwd);
>      }
>
> @@ -4392,6 +4392,8 @@ static void malloc_consolidate(mstate av)
>          prevsize = prev_size (p);
>          size += prevsize;
>          p = chunk_at_offset(p, -((long) prevsize));
> +        if (__builtin_expect (chunksize(p) != prevsize, 0))
> +          malloc_printerr ("corrupted size vs. prev_size");
>          unlink(av, p, bck, fwd);
>        }
>
>
  
Carlos O'Donell Oct. 23, 2017, 6:52 p.m. UTC | #2
On 10/23/2017 10:29 AM, Moritz Eckert wrote:
> Since I didn't receive any reply yet, I wanted to make sure that's
> not because there is something wrong with the form of my patch
> proposal in general, or this being the wrong mailinglist for it?

You are on the right list.

This is the place to send patches like this for glibc.

You just need to ping your patch and keep pining. Everyone is busy.

A few things that would help your patch:

* Provide real-world numbers to backup your claim that it has a
  neutral performance impact. Alternatively show in the disassembly
  that the instruction critical path length is equivalent.

* Provide a test case that fails with the fix in place?

* Provide a more detailed discussion about why the code changes you
  made are the correct ones (something a reviewer would have to do).

Please review the contribution checklist:
https://sourceware.org/glibc/wiki/Contribution%20checklist
  

Patch

diff --git a/ChangeLog b/ChangeLog
index 5effbd7956..f7a76220d2 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,12 @@ 
+2017-10-13  Moritz Eckert  <m.eckert@cs.ucsb.edu>
+
+    * malloc/malloc.c: (_int_free): Compare prev_size and prev->size
+    before performing the backward unlink, instead of going back in
+    memory first. This prevents forgotten update type of attacks.
+
+    * malloc/malloc.c: (unlink): Remove the current check to have no
+    performance impact.
+
  2017-10-13  James Clarke  <jrtc27@jrtc27.com>

      * sysdeps/powerpc/powerpc32/dl-machine.h (elf_machine_rela):
diff --git a/malloc/malloc.c b/malloc/malloc.c
index d3fcadd20e..c421e778da 100644
--- a/malloc/malloc.c
+++ b/malloc/malloc.c
@@ -1395,8 +1395,6 @@  typedef struct malloc_chunk *mbinptr;

  /* Take a chunk off a bin list */
  #define unlink(AV, P, BK, FD) 
{                                            \
-    if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 
0))      \
-      malloc_printerr ("corrupted size vs. prev_size");       \
      FD = P->fd;                                      \