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
:
{% highlight c %}
extern char **__libc_argv attribute_hidden;
void
attribute ((noreturn))
__stack_chk_fail (void)
{
__fortify_fail ("stack smashing detected");
}
{% endhighlight %}
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:
{% highlight c %}
int main(){
//fit in fastbin
char * ptr = (char *)malloc(0x20);
//double free
free(ptr);
free(ptr);
}
{% endhighlight %}
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:
{% highlight c %}
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=, ptr=, str=0x7ffff7b9df60 "double free or "..., action=3) at malloc.c:5006
4 _int_free (av=, p=, have_lock=0) at malloc.c:3867
5 0x00007ffff7a9153c in __GI___libc_free (mem=) at malloc.c:2968
6 0x0000000000400594 in main ()
7 0x00007ffff7a2d830 in __libc_start_main (main=0x400566 , argc=1, argv=0x7fffffffdf68, init=, fini=, rtld_fini=, stack_end=0x7fffffffdf58) at ../csu/libc-start.c:291
8 0x0000000000400499 in _start ()
{% endhighlight %}
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:
{% highlight c %}
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=) 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=, argv=, 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=, do_abort@entry=2, written=, 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=, ptr=, str=0x7ffff7b9df60 "double free or "..., action=3) at malloc.c:5006
16 _int_free (av=, p=, have_lock=0) at malloc.c:3867
17 0x00007ffff7a9153c in __GI___libc_free (mem=) at malloc.c:2968
18 0x0000000000400594 in main ()
19 0x00007ffff7a2d830 in __libc_start_main (main=0x400566 , argc=1, argv=0x7fffffffdf38, init=, fini=, rtld_fini=, stack_end=0x7fffffffdf28) at ../csu/libc-start.c:291
20 0x0000000000400499 in _start ()
pwndbg> x /10g $rip
0xcc: Cannot access memory at address 0xcc
{% endhighlight %}
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:
{% highlight c %}
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);
}
{% endhighlight %}
__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:
{% highlight c %}
-- snip --
char *temp;
temp = alloca (strlen (best) + 1);
strcpy (temp, best);
return __strdup (temp);
}
{% endhighlight %}
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:
{% highlight c %}
--- snip