Exploit Education | Phoenix | Final Two Solution

Remote heap overflow.

The description and source code can be found here:
http://exploit.education/phoenix/final-two/

Ok, so the title here is a little misleading as I don’t actually have a solution. What I will be doing, is explaining why this isn’t exploitable.

The predecessor to Phoenix is Protostar. This level is the same as Protostar’s Final Two level, yet that one IS exploitable despite the code being almost the same. I’ll discuss what makes Protostar different at the binary level.

Source Code Analysis

Let’s have a look at the source code to see what the program does (spoiler alert: Nothing useful).

There’s not much in main(). It just prints the banner, flushes anything in the input buffer (preventing you from piping anything to netcat), and calls get_requests().

int main(int argc, char **argv, char **envp) {
    printf("%s\n", BANNER);
    fflush(stdout);

    get_requests(0, 1);
    return 0;
}

The get_requests() function is where it gets interesting. It sets up a while loop that will break after 256 iterations in order to accept “requests” from a user (up to 256 of them). That loop does a few things:

  • Allocates 128 bytes of memory on the heap.
  • Reads from user input to save to the heap.
    • To continue making requests, the input has to be exactly 128 bytes long.
    • Anything more and the rest is used for the next iteration of the loop. Anything less, and the loop breaks.
  • Checks to make sure the first 4 characters are “FSRD” (and breaks out of the loop if it’s not).
  • Calls check_path(), supplying the user-provided input after “FSRD” as a parameter.
  • Increments the dll, the counter variable.
void get_requests(int in_fd, int out_fd) {
    char *buf;
    char *destroylist[256];
    int dll;
    int i;

    dll = 0;
    while (1) {
        if (dll >= 255) break;

        buf = calloc(REQSZ, 1);
        if (read(in_fd, buf, REQSZ) != REQSZ) break;

        if (strncmp(buf, "FSRD", 4) != 0) break;

        check_path(buf + 4);

        dll++;
    }
    ...

At this point, the get_requests() function isn’t over, but let’s jump to check_path() and come back to this later.

check_path() will do several things:

  • Looks for the last occurrence of the “/” character & saves a pointer to it called “p”
  • Saves the length of the string starting there
  • If the “/” character exists:
    • Look for the “ROOT” string and save a pointer to it called “start”
    • If the “ROOT” string exists:
      • Decrement the “start” pointer to the “ROOT” string until a “/” character is found
      • Copies the string starting at the “p” pointer to where the “start” pointer ends up
void check_path(char *buf) {
    char *start;
    char *p;
    int l;

    /*
    * Work out old software bug
    */

    p = rindex(buf, '/');
    l = strlen(p);
    if (p) {
        start = strstr(buf, "ROOT");
        if (start) {
            while (*start != '/') start--;
            memmove(start, p, l);
        }
    }
}

Finally, back at the get_requests() function, there’s a for loop that iterates for the number of 128 byte requests that were supplied by the user. The string “Process OK” is printed and free() is called on each pointer in the destroylist[] character pointer array.

    ...
    for (i = 0; i < dll; i++) {
        write(out_fd, "Process OK\n", strlen("Process OK\n"));
        free(destroylist[i]);
    }
}

If, at this point, you’re wondering, “when did we save any pointers to the destroylist[] character pointer array?” Then you’re on the right track. This whole “heap overflow” attack relies on the free() function. We would need to add an extra line of code to get_requests(), like so:

void get_requests(int in_fd, int out_fd) {
    char *buf;
    char *destroylist[256];
    int dll;
    int i;

    dll = 0;
    while (1) {
        if (dll >= 255) break;

        buf = calloc(REQSZ, 1);
        destroylist[dll] = buf;
        if (read(in_fd, buf, REQSZ) != REQSZ) break;

        if (strncmp(buf, "FSRD", 4) != 0) break;

        check_path(buf + 4);

        dll++;
    }
    ...

Running the Program

There’s not much to it, but I’ll show an example of what “valid” input looks like:

user@phoenix-amd64:~$ nc 127.1 64015
Welcome to phoenix/final-two, brought to you by https://exploit.education
FSRD/ROOT/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
FSRD/ROOT/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
FSRD/ROOT/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Process OK
Process OK
Process OK

To show what invalid input looks like, I’ll run the binary in GDB so we can see that it segfaults and at which instruction:

user@phoenix-amd64:~$ gdb -q /opt/phoenix/i486/final-two
GEF for linux ready, type `gef' to start, `gef config' to configure
71 commands loaded for GDB 8.2.1 using Python engine 3.5
[*] 2 commands could not be loaded, run `gef missing` to know why.
Reading symbols from /opt/phoenix/i486/final-two...(no debugging symbols found)...done.

gef> run
Starting program: /opt/phoenix/i486/final-two
Welcome to phoenix/final-two, brought to you by https://exploit.education
FSRDROOT/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Program received signal SIGSEGV, Segmentation fault.
0x08048959 in check_path ()
[... GEF output snipped ...]

gef> x/i $eip
=> 0x8048959 <check_path+84>:   mov    al,BYTE PTR [eax]

gef> x $eax
   0xf7e68fff:  Cannot access memory at address 0xf7e68fff

This happens because of the following line of code in the check_path() function.

while (*start != '/') start--;

Since there is no “/” character before the “ROOT” string, it keeps decrementing the point until it reaches a memory location it can’t read from. In this case, that’s at 0xf7e68fff, which is one byte before the start of the heap.

Now, if the free() function was being called on our allocated heap chunks, we could take advantage of this bug and overwrite the chunk metadata for one of the requests. For more details on how to exploit a heap overflow with this, see my solution to the Heap Three level.

Binary Analysis

If you look at the source code from the get_requests() function from Protostar’s Final Two level, it’s almost exactly the same. However, that one IS exploitable. There seems to be a difference between the source code and the binary. Let’s take a look at the binary on Protostar:

user@protostar:/opt/protostar/bin$ gdb -q final2
Reading symbols from /opt/protostar/bin/final2...done.

(gdb) set disassembly-flavor intel

(gdb) disas get_requests
Dump of assembler code for function get_requests:
...
0x0804bd6f <get_requests+40>:   call   0x804b4ee <calloc>
0x0804bd74 <get_requests+45>:   mov    DWORD PTR [ebp-0x14],eax
0x0804bd77 <get_requests+48>:   mov    eax,DWORD PTR [ebp-0x10]
0x0804bd7a <get_requests+51>:   mov    edx,DWORD PTR [ebp-0x14]
0x0804bd7d <get_requests+54>:   mov    DWORD PTR [ebp+eax*4-0x414],edx
0x0804bd84 <get_requests+61>:   add    DWORD PTR [ebp-0x10],0x1
0x0804bd88 <get_requests+65>:   mov    DWORD PTR [esp+0x8],0x80
0x0804bd90 <get_requests+73>:   mov    eax,DWORD PTR [ebp-0x14]
0x0804bd93 <get_requests+76>:   mov    DWORD PTR [esp+0x4],eax
0x0804bd97 <get_requests+80>:   mov    eax,DWORD PTR [ebp+0x8]
0x0804bd9a <get_requests+83>:   mov    DWORD PTR [esp],eax
0x0804bd9d <get_requests+86>:   call   0x8048e5c <read@plt>
...
0x0804be09 <get_requests+194>:  mov    eax,DWORD PTR [ebp+eax*4-0x414]
0x0804be10 <get_requests+201>:  mov    DWORD PTR [esp],eax
0x0804be13 <get_requests+204>:  call   0x804a9c2 <free>
...

When the calloc() function returns, it saves the pointer to the allocated heap space in EAX (get_requests+40). That pointer is saved to the stack at “ebp-0x14” (get_requests+45), which is where the buf variable is saved. This is then saved to EDX (get_requests+51). Finally, that pointer to the heap is stored in the character pointer array, with EAX containing the index number (get_requests+54). Each pointer in the array is 4 bytes in length, so the “eax*4” is a giveaway that it’s accessing the only array, destroylist[]. That value is later used as an argument to the free() function when it’s put onto the top of the stack (get_requests+194 & get_requests+201).

What this tells me, is that there is a line in the source code that looks something like destroylist[dll] = buf; My guess is that level was originally created with the source code you see, then updated when the author realized the level wasn’t exploitable and forgot to update the source code on the website. Now that Protostar has turned into Phoenix, the author of Phoenix used the non-exploitable version of the Final Two level.

I included all of the instructions up until the call to read() (get_requests+86) in that last disassembly because that’s what I’ll show you in the final-two binary from Phoenix.

user@phoenix-amd64:/opt/phoenix/i486$ gdb -q final-two
GEF for linux ready, type `gef' to start, `gef config' to configure
71 commands loaded for GDB 8.2.1 using Python engine 3.5
[*] 2 commands could not be loaded, run `gef missing` to know why.
Reading symbols from final-two...(no debugging symbols found)...done.

gef> disas get_requests
Dump of assembler code for function get_requests:
   ...
   0x0804899a <+35>:    call   0x804a53a <calloc>
   0x0804899f <+40>:    add    esp,0x10
   0x080489a2 <+43>:    mov    DWORD PTR [ebp-0x14],eax
   0x080489a5 <+46>:    sub    esp,0x4
   0x080489a8 <+49>:    push   0x80
   0x080489ad <+54>:    push   DWORD PTR [ebp-0x14]
   0x080489b0 <+57>:    push   DWORD PTR [ebp+0x8]
   0x080489b3 <+60>:    call   0x8048700 <read@plt>
   ...
   0x08048a1a <+163>:   mov    eax,DWORD PTR [ebp+eax*4-0x414]
   0x08048a21 <+170>:   sub    esp,0xc
   0x08048a24 <+173>:   push   eax
   0x08048a25 <+174>:   call   0x8049a12 <free>
   ...

As you can see, the pointer that’s returned by the calloc() function (stored in EAX) is saved to the buf variable, which is stored on the stack at “ebp-0x14” (get_requests+43). However, nothing else is done with that value until it’s pushed onto the stack (get_requests+54) as an argument for the call to read() (get_requests+60). Later in the function during the for loop, the values in the destroylist[] array are pushed onto the stack to be passed to free() as parameters (get_requests+163 & get_requests+173). However, nothing was ever stored there.

Conclusion

Maybe it’s just my lack of experience, but it doesn’t seem this level is exploitable. I believe the intended method is for the attacker to send 2 requests, the first one being a valid request while the second one is missing a “/” character before the “ROOT” string. After the “ROOT” string, you would have a “/” character followed by values to overwrite the prev_size and size metadata for the second chunk. After that, you could include pointer values to be able to arbitrarily overwrite (almost) any location with (almost) any value, 4 bytes long, of course. Everything after that last “/” character in the second request would get copied to the first request after the last “/” character.

Again, for more details on how this is done and why it works, see my solution to the Heap Three level.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.