Article Index

While this challenge was under the "Baby's First" section, I think it's a great teaching example of a basic heap exploitation technique. File

 

$ file beatmeonthedl
beatmeonthedl: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, not stripped

 

Giving it a run we get:

 

$ ./beatmeonthedl 
         __      __       .__                                  __          
        /  \    /  \ ____ |  |   ____  ____   _____   ____   _/  |_  ____  
        \   \/\/   // __ \|  | _/ ___\/  _ \ /     \_/ __ \  \   __\/  _ \ 
         \        /\  ___/|  |_\  \__(  <_> )  Y Y  \  ___/   |  | (  <_> )
          \__/\  /  \___  >____/\___  >____/|__|_|  /\___  >  |__|  \____/ 
               \/       \/          \/            \/     \/                
      __  .__             .__         .__                 _____    __  .__           
    _/  |_|  |__   ____   |  | _____  |__|______    _____/ ____\ _/  |_|  |__   ____ 
    \   __\  |  \_/ __ \  |  | \__  \ |  \_  __ \  /  _ \   __\  \   __\  |  \_/ __ \
     |  | |   Y  \  ___/  |  |__/ __ \|  ||  | \/ (  <_> )  |     |  | |   Y  \  ___/
     |__| |___|  /\___  > |____(____  /__||__|     \____/|__|     |__| |___|  /\___  >
               \/     \/            \/                                      \/     \/
  _________.__                .___              __________                __
 /   _____/|  |__ _____     __| _/______  _  __ \______   \_______  ____ |  | __ ___________  ______
 \_____  \ |  |  \\__  \   / __ |/  _ \ \/ \/ /  |    |  _/\_  __ \/  _ \|  |/ \// __ \_  __ \/  ___/
 /        \|   Y  \/ __ \_/ /_/ (  <_> )     /   |    |   \ |  | \(  <_> )    <\  ___/|  |  \/\___ \
/_______  /|___|  (____  /\____ |\____/ \/\_/    |______  / |__|   \____/|__|_ \\___  >__|    /____ >
        \/      \/     \/      \/                       \/                    \/    \/             \/
Enter username: test
Invalid user: test

 

It's apparently looking for a correct username. As is a usual next step, let's run ltrace on it to see if we can have an easy win. I'm cutting the output to the important part.

 

[pid 34323]           SYS_read(0test
, "test\n", 16)                                                                                  = 5
[pid 34323]      <... read resumed> , "test\n", 16)                                                                              = 5
[pid 34323]      strlen("mcfly")                                                                                                 = 5
[pid 34323]      memcmp(0x7ffd6141c6d0, 0x4085d8, 5, 0x4085d8)                                                                   = 7
[pid 34323]      printf("Invalid user: %s\n", "test\n" <unfinished ...>

 

So it reads in our "test" string, then checks the string length of "mcfly". Let's see if that's correct.

 

[pid 34348]      strlen("mcfly")                                                                                                 = 5
[pid 34348]      memcmp(0x7ffdc291f8f0, 0x4085d8, 5, 0x4085d8)                                                                   = 0
[pid 34348]      memset(0x7ffdc291f8e0, '\0', 24)                                                                                = 0x7ffdc291f8e0
[pid 34348]      printf("Enter Pass: " <unfinished ...>
[pid 34348]           SYS_write(1, "Enter Pass: ", 12Enter Pass: )                                                                           = 12
[pid 34348]      <... printf resumed> )                                                                                          = 12
[pid 34348]      read(0 <unfinished ...>
[pid 34348]           SYS_read(0test
, "test\n", 24)                                                                                  = 5
[pid 34348]      <... read resumed> , "test\n", 24)                                                                              = 5
[pid 34348]      sysconf(30, 32, 0, 4)                                                                                           = 4096
[pid 34348]      time(0)                                                                                                         = 1493773626
[pid 34348]      sbrk(0 <unfinished ...>
[pid 34348]           SYS_brk(0)                                                                                                 = 0x1f8c000
[pid 34348]      <... sbrk resumed> )                                                                                            = 0x1f8c000
[pid 34348]      sbrk(4096 <unfinished ...>
[pid 34348]           SYS_brk(0x1f8d000)                                                                                         = 0x1f8d000
[pid 34348]      <... sbrk resumed> )                                                                                            = 0x1f8c000
[pid 34348]      strlen("awesnap")                                                                                               = 7
[pid 34348]      memcmp(0x1f8c010, 0x4085de, 7, 0x4085de)                                                                        = 19
[pid 34348]      printf("Invalid pass: %s\n", "test\n" <unfinished ...>
[pid 34348]           SYS_write(1, "Invalid pass: test\n\n", 20Invalid pass: test

 

So it accepted the username. Next, it asked for a password and compared it against "awesnap". If we go ahead and try that as the password we will get past the login.

 

Enter username: mcfly
Enter Pass: awesnap
I) Request Exploit.
II) Print Requests.
III) Delete Request.
IV) Change Request.
V) Go Away.
|

 

It's a menu system. Let's try a few inputs:

 

I) Request Exploit.
II) Print Requests.
III) Delete Request.
IV) Change Request.
V) Go Away.
| 1
Request text > test
I) Request Exploit.
II) Print Requests.
III) Delete Request.
IV) Change Request.
V) Go Away.
| 1
Request text > test2
I) Request Exploit.
II) Print Requests.
III) Delete Request.
IV) Change Request.
V) Go Away.
| 1
Request text > %x
I) Request Exploit.
II) Print Requests.
III) Delete Request.
IV) Change Request.
V) Go Away.
| 2
0) test

1) test2

2) %x

I) Request Exploit.
II) Print Requests.
III) Delete Request.
IV) Change Request.
V) Go Away.
| 4
0) test

1) test2

2) %x

choice: 0
data: test3
I) Request Exploit.
II) Print Requests.
III) Delete Request.
IV) Change Request.
V) Go Away.
| 2
0) test3

1) test2

2) %x

 

So it lets us give it some input. It then gives us the ability to print that input out, modify it, and delete it. Already this is feeling like a heap challenge. Also, sadly (gladly?) the "%x" didn't get interpreted as a format string, so that's off the table.

 

At this point, I jumped into the code. Here's main:

 

 

Unfortunately, radare2 had a bit of a problem displaying that jump table at the bottom. However, this binary isn't stripped and there are helpful names for the functions.

 

 

Let's go through the functions.


Request Exploit

 

 

In this first part, we can tell there's a global named "reqlist". It is being iterated over 32 times looking for an entry with 0. Basic idea here is that this program will fill in holes in the global array with something (likely pointers). Let's look at the next part.

 

 

This is where it gets interesting. We're making a call to malloc in block 0x401035 for 0x38 bytes. Then in the very next block we're asking for input from the user, reading into this block 0x80 bytes. So this is effectively a blatant heap overflow. However, the question is how you turn that into an exploit. This function by itself would be difficult to fully exploit since you're going to be overwriting the wilderness value if you use the overwrite here. Let's keep looking.


Print Requests

Here's the print requests disassembly:

 

 

This one is pretty self explanatory. It's instantiating a counter value, iterating through the reqlist global array, and if the value is non zero, it's printing it out as a string. Moving on.


Change Request

Here is change requests disassembly:

 

 

So this function starts off getting user input and making sure that the pointer at that index is valid and within range. Once it has done that, it prompts for user input and reads in at most 0x80 characters. Remember that the original function that malloc'd the space malloc'd 0x38 bytes, so this is again an overwrite. This is more interesting now, since we have the ability to go back and overflow a buffer with a known malloc space after it. This means we have the opportunity to overwrite malloc control structures.


Delete Request

 

This one is also fairly strait forward. It prints a message to the user, grabs input, validates that there is indeed something to free at that index in the global array, and then proceeds to free it. Note that after freeing, it does zero out the location in the array, so they're not allowing a use after free or double free here.


The Vulnerability

I already hinted at what the problem is. The idea is that we have a blatant heap overwrite. To add on to that, we can create up to 32 heap chunks (we don't need that), and can overwrite any of them from the one previous. We can print out all the values. Finally, we can cause the program to delete any given entry.

 

The key point to add to all of this is the fact that we have a pointer to our heap address in a known static place. Specifically, the array named "reqlist". This becomes important because it opens up the opportunity for us to utilize the unsafe unlink attack.

 

Unsafe Unink

The idea behind the unsafe unlink attack is that we can overwrite a pointer somewhere by abusing the unlink method of the free call. This method is called when we want to merge two adjacent, now free, memory chunks together so that we can have one large one instead of two small ones. There was an unlink vulnerability in earlier versions of libc that allowed almost arbitrary write what where. However, modern versions of libc added a specific check to defeat this.

 

At its core, dlmalloc can be thought of similar to a doubly linked list. Each free malloc chunk contains a pointer to the next chunk, as well as to the previous chunk. The check that libc implemented to stop the previous unlink attack was simply to verify that the current chunk's forward pointer had a corresponding backwards pointer that pointed to the original chunk. The same concept with the backwards pointer. In code, it looks like (P->fd->bk != P || P->bk->fd != P) != False.

 

While this check does stop a write-what-where, clever people discovered a new way to attack it. The new attack utilizes the fact that malloc doesn't inherently know where the malloc'd blocks are. It utilizes pointers and sizes to discover this. That means, we can create our own fake malloc structures that malloc will believe are real, because it has no way of really determining they're fabricated. The only real requirement for this attack, aside from the ability to overwrite, is that we know where a pointer to our malloc chunk is. This is needed so that we can satisfy the unlink checks as described previously.

 

In our case, we do happen to know where our pointer is because it's being held in a global array on a non Position Independent Executable. Specifically, it resides at 0x609e80. That means, our game plan is:

 

  1. Allocate two blocks (don't overflow them)
  2. Edit the first block, creating a fake malloc structure and overwriting the control structure of the second
  3. Delete the second, causing the unlink
  4. We now have a pointer directly before our global array of pointers. We have arbitrary read/write ability.

 

The Fake Chunk

The fake chunk we are creating will look something like this:

 

----------------
- 8-bytes of 0 - <-- Previous Size
----------------
- 8-bytes of 0 - <-- Size and AMP flags
----------------
- Forward ptr  - <-- Global Pointer Address - (8 * 3)
----------------
- Backward ptr - <-- Global Pointer Address - (8 * 2)
----------------
-   Padding    - <-- Up to next chunk header
----------------
-     0x30     - <-- Previous size (back to our fake chunk)
----------------
-     0x42     - <-- Original chunk size minux in-use flag
----------------

 

Now when we attempt to free the second block, malloc will believe that our first block is free. It will read our fake malloc header structure, discover that the forward and back pointers point to a valid place, and update it for us. The net effect is that the next time we choose the menu option to edit our first block, we will instead be editing directly before it. If we're careful about how we write, we can leverage this for arbitrary writes and reads.


Exploit

Before we exploit, let's look at what protections are enabled on this binary:

 

$ checksec beatmeonthedl
[*] '/home/franklin/defcon/babys_first/beatmeonthedl/beatmeonthedl'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)

 

Absolutely nothing is enabled. Almost a shame really. This means we have no need for that print function, since we don't actually need to leak anything. There are many approaches you could take to exploit it from here. Since NX is disabled and the binary isn't PIE, I figured the easy win would be to do the following:

 

  1. Write shellcode into some place in the .bss section
  2. Overwrite a function's GOT entry (I chose memset for convenience)
  3. Done

 

To do these overwrites, simply line up the pointers in your second call to edit the note, then use the appropriate indexes.

 

My full exploit can be found here.

 

# The flag is: 3asy p33zy h3ap hacking!!