CSAW CTF 2017 Revisiting Auir



This is a short post to (hopefully) answer some of the questions that I have received about my exploit auir.py and talk about a few interesting things that I have found.

Why Free?

In my exploit, I triggered the double free corruption error instead of allocating another chunk to invoke __malloc_hook because of the constraints on the one shot gadget that I used.

Does malloc actually get called when you trigger the double free corruption error?

Yes, it does invoke __malloc_hook. To be more exact, it does not directly call __malloc_hook but one of the functions that is used calls the malloc internally.

So, which function triggers the malloc?

This is where I was stuck because I genuinely did not know. I knew for sure that malloc gets called because some of the public ctf write-ups (sorry, can not exactly remember the link) talk about freeing the chunk twice triggers malloc. However, none of the write-ups (as far as I am aware) talks about the details on which function calls the malloc. Based on the advice I have received, it seems __fortify_fail was the function that calls the malloc internally.

Does __fortify_fail actually get called when you raise the double free corruption error?

I knew that __foritfy_fail gets triggered by __stack_check_fails:

extern char **__libc_argv attribute_hidden;

void 
__attribute__ ((noreturn))
__stack_chk_fail (void)
{
  __fortify_fail ("stack smashing detected");
}

This always gets triggered when you overwrite the stack canary (stack smashing, for instance). However, I was not quite sure if the function __fortify_fail actually gets triggered for the heap related error. In order to verify this, I created a small toy program that intentionally frees the same chunk twice:

int main(){

	//fit in fastbin	
	char * ptr = (char *)malloc(0x20);
	//double free	
	free(ptr);
	free(ptr);	

}

Using gdb, I set a breakpoint on __fortify_fail and ran the program. The result of the program as expected threw the double free corruption error. Looking through the backtrace, __fortify_fail did not seem to get called:

#0  0x00007ffff7a42428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
#1  0x00007ffff7a4402a in __GI_abort () at abort.c:89
#2  0x00007ffff7a847ea in __libc_message (do_abort=do_abort@entry=2, fmt=fmt@entry=0x7ffff7b9de98 "*** Error in `%"...) at ../sysdeps/posix/libc_fatal.c:175
#3  0x00007ffff7a8d37a in malloc_printerr (ar_ptr=<optimized out>, ptr=<optimized out>, str=0x7ffff7b9df60 "double free or "..., action=3) at malloc.c:5006
#4  _int_free (av=<optimized out>, p=<optimized out>, have_lock=0) at malloc.c:3867
#5  0x00007ffff7a9153c in __GI___libc_free (mem=<optimized out>) at malloc.c:2968
#6  0x0000000000400594 in main ()
#7  0x00007ffff7a2d830 in __libc_start_main (main=0x400566 <main>, argc=1, argv=0x7fffffffdf68, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffdf58) at ../csu/libc-start.c:291
#8  0x0000000000400499 in _start ()

Instead, malloc_printerr was called. This was good to know but there were still no traces of call to malloc. Since I did not want to read the source code line by line, I set a breakpoint at __malloc_hook and re-run the program to see which function actually calls the malloc internally:

pwndbg> b*&__malloc_hook
Breakpoint 6 at 0x7ffff7dd1b10
pwndbg> r 
--- snip --
pwndbg> bt
#0  0x00000000000000cc in ?? ()
#1  0x00007ffff7df3f5a in __strdup (s=0x7fffffffcb50 "/lib/x86_64-lin"...) at strdup.c:42
#2  0x00007ffff7def7df in _dl_load_cache_lookup (name=name@entry=0x7ffff7b99646 "libgcc_s.so.1") at dl-cache.c:311
#3  0x00007ffff7de0169 in _dl_map_object (loader=loader@entry=0x7ffff7ff74c0, name=name@entry=0x7ffff7b99646 "libgcc_s.so.1", type=type@entry=2, trace_mode=trace_mode@entry=0, mode=mode@entry=-1879048191, nsid=<optimized out>) at dl-load.c:2342
#4  0x00007ffff7dec577 in dl_open_worker (a=a@entry=0x7fffffffd240) at dl-open.c:237
#5  0x00007ffff7de7564 in _dl_catch_error (objname=objname@entry=0x7fffffffd230, errstring=errstring@entry=0x7fffffffd238, mallocedp=mallocedp@entry=0x7fffffffd22f, operate=operate@entry=0x7ffff7dec4d0 <dl_open_worker>, args=args@entry=0x7fffffffd240) at dl-error.c:187
#6  0x00007ffff7debda9 in _dl_open (file=0x7ffff7b99646 "libgcc_s.so.1", mode=-2147483647, caller_dlopen=0x7ffff7b22b81 <__GI___backtrace+193>, nsid=-2, argc=<optimized out>, argv=<optimized out>, env=0x7fffffffdf48) at dl-open.c:660
#7  0x00007ffff7b5056d in do_dlopen (ptr=ptr@entry=0x7fffffffd460) at dl-libc.c:87
#8  0x00007ffff7de7564 in _dl_catch_error (objname=0x7fffffffd450, errstring=0x7fffffffd458, mallocedp=0x7fffffffd44f, operate=0x7ffff7b50530 <do_dlopen>, args=0x7fffffffd460) at dl-error.c:187
#9  0x00007ffff7b50624 in dlerror_run (args=0x7fffffffd460, operate=0x7ffff7b50530 <do_dlopen>) at dl-libc.c:46
#10 __GI___libc_dlopen_mode (name=name@entry=0x7ffff7b99646 "libgcc_s.so.1", mode=mode@entry=-2147483647) at dl-libc.c:163
#11 0x00007ffff7b22b81 in init () at ../sysdeps/x86_64/backtrace.c:52
#12 __GI___backtrace (array=array@entry=0x7fffffffd4c0, size=size@entry=64) at ../sysdeps/x86_64/backtrace.c:105
#13 0x00007ffff7a2c9f5 in backtrace_and_maps (do_abort=<optimized out>, do_abort@entry=2, written=<optimized out>, fd=fd@entry=3) at ../sysdeps/unix/sysv/linux/libc_fatal.c:47
#14 0x00007ffff7a847e5 in __libc_message (do_abort=do_abort@entry=2, fmt=fmt@entry=0x7ffff7b9de98 "*** Error in `%"...) at ../sysdeps/posix/libc_fatal.c:172
#15 0x00007ffff7a8d37a in malloc_printerr (ar_ptr=<optimized out>, ptr=<optimized out>, str=0x7ffff7b9df60 "double free or "..., action=3) at malloc.c:5006
#16 _int_free (av=<optimized out>, p=<optimized out>, have_lock=0) at malloc.c:3867
#17 0x00007ffff7a9153c in __GI___libc_free (mem=<optimized out>) at malloc.c:2968
#18 0x0000000000400594 in main ()
#19 0x00007ffff7a2d830 in __libc_start_main (main=0x400566 <main>, argc=1, argv=0x7fffffffdf38, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffdf28) at ../csu/libc-start.c:291
#20 0x0000000000400499 in _start ()
pwndbg> x /10g $rip
0xcc:	Cannot access memory at address 0xcc

Looking at the backtrace again, I noticed that it succesfully hit __malloc_hook (the current rip is pointing at cc,which is the breakpoint I set earlier) and __strdup function was called before.

__strdup function returns the pointer to the duplicated string and sure enough it internally uses malloc to allocate the new buffer for a copy of the string:

char *
__strdup (const char *s)
{
  size_t len = strlen (s) + 1;
  void *new = malloc (len); //here!

  if (new == NULL)
    return NULL;

  return (char *) memcpy (new, s, len);
}

__strdup function gets called by _dl_load_cache_lookup, which is responsible for looking up the passed in name in ld.so and returning the stored filename there:

-- snip --
  char *temp;
  temp = alloca (strlen (best) + 1);
  strcpy (temp, best);
  return __strdup (temp);
}

Does __fortify_fail call malloc internally?

Since __fortify_fail calls backtrace_and_maps internally, it should theoretically reach the similar path. However, when I ran my toy bof program, it was aborted as soon as it printed out an error message. After reading through the source code, I realized the first argument to libc_message is the flag that tells whether it requires to do backtrace. In this case, it had the value of 1, which is to do_abort. The rest of the value can be found in stdio.h:

--- snip ---
enum __libc_message_action
{
	  do_message        = 0,                /* Print message.  */
	  do_abort        = 1 << 0,        /* Abort.  */
	  do_backtrace        = 1 << 1        /* Backtrace.  */
	
};
--- snip ---

Since the value of the flag was always 1, it made sense that it did not go through the same path (like printing out the traces).

But, I set a breakpoint at __malloc_hook again just to make sure and overwrote the canary to trigger __fortify_fail:

pwndbg> b*&__malloc_hook
Breakpoint 2 at 0x7ffff7dd1b10
pwndbg> c
Continuing.
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
  f 0     7ffff7a42428 raise+56
   f 1     7ffff7a4402a abort+362
   f 2     7ffff7a847ea
   f 3     7ffff7b2611c __fortify_fail+92
   f 4     7ffff7b260c0 __fortify_fail
   f 5           4005e1 main+75
   f 6    a616161616161
   f 7                0
Program received signal SIGABRT
pwndbg> bt
#0  0x00007ffff7a42428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
#1  0x00007ffff7a4402a in __GI_abort () at abort.c:89
#2  0x00007ffff7a847ea in __libc_message (do_abort=do_abort@entry=1, fmt=fmt@entry=0x7ffff7b9c45f "*** %s ***: %s "...) at ../sysdeps/posix/libc_fatal.c:175
#3  0x00007ffff7b2611c in __GI___fortify_fail (msg=<optimized out>, msg@entry=0x7ffff7b9c441 "stack smashing "...) at fortify_fail.c:37
#4  0x00007ffff7b260c0 in __stack_chk_fail () at stack_chk_fail.c:28
#5  0x00000000004005e1 in main ()
#6  0x000a616161616161 in ?? ()
#7  0x0000000000000000 in ?? ()
pwndbg>
 

Based on the result, I was able to confirm that malloc did not get called….

What’s next?

It will be interesting to dig in and see if there are any writable places in __fortify_fail because I did not look into it deeply. Even better, it will be very useful to discover the other writable places by reading through the glibc source code. Let me know if you find any :)

And…

That’s it for this challenge. I hope this answers the questions and gives a bit of explanation on whether malloc function actually gets called when you raise the doulbe free corruption error.