[AngstromCTF 2020] bookface (pwn) writeup

Github link here.

bookface

  • Given files: bookface.tar.gz.
  • The binary has: Full RELRO, Canary found, NX enabled and PIE enabled.
  • The source code of the binary is given.
  • Libc version is 2.23.
  • Hints:
    -- sysctl vm.mmap_min_addr=0 was run on the host system. (more a reminder than a hint)
    -- Brute forcing is not required.
  • Small note: calls to read() in the binary read from stdout instead of stdin, so you may have to modify the binary, or set stdin=PTY for it to work with pwntools.

Functionalities

  • This program is like a very simple social media app where you can login and add/remove friends.
  • When logging in, you are asked for an ID. If it's a new ID, you will be asked for a name. If the ID already exist, it will be a relogin and you will be asked for a survey.
  • After logging in, you have the options to: add a number of friends, remove a number of friends, remove your account, logout.
  • Logging out will create a file in the users directory corresponding to the account ID.
  • For the survey, you will be asked to rate 4 aspects on a scale of 0 to 10. If you don't rate them all 10, you will have to rate again, and if you don't rate them all 10 in the second time, your friends will be set to 0.
  • The user's info will be saved in a struct, which will be stored in a separate mmapped page.

Vulnearabilities

(1) As the hint and the Dockerfile says, sysctl vm.mmap_min_addr=0 was run on the host system. There is a reason why systems set this to 4096 instead of 0, and this is a vulnearability. This makes so that mmap() can return a page at address 0, making NULL pointer deferences valid.

(2) When you do the survey the second time, your first rating will be passed directly into printf() -> format string bug. But the program will check if there is the character n in the string, so this format string in only for leaking and not overwriting.

(3) The number of friends in the struct is defined as a pointer, but treated as a number in the add/remove friend options, and treated as a pointer again when the user's friend get set to 0 -> we can write 0 to any arbitrary address.

(4) The page that contains user's info is mmapped with its address generated by rand().

Exploit plan

Step 1: Login, logout and login again to take the survey.

Step 2: Use the format string bug in the survey to leak libc's address.

Step 3: Use the arbitrary write to 0 bug to overwrite the randtbl of rand() to all 0 so that rand() will always return 0. (see explanation below)

Step 4: Logging in again will make the user's info mmapped at page 0. We spray a lot of one_gadget there to fake a _IO_file_jmp that contains only one_gadget.

Step 5: Overwrite the pointer to _IO_file_jmpof stdout to 0, which is now a fake _IO_file_jmp. Therefore, the next call to any of the function in _IO_file_jmp will pop a shell.

Full exploit

See solve.py.

About rand()

  • At first, I didn't think about exploiting the rand() function. Instead, I try to login and logout a lot to spray the memory with a lot of pages and hope a page will be mmapped at 0, which is a very bruteforcy and luck-based solution, and that doesn't work so well on the slow remote server (also the hint says bruteforcing is not required).
  • Therefore, I thought of finding a way to make mmap() always maps a page at 0. And to do that, I tried to make the first parameter of mmap(), which is generated by rand(), to always be 0.
  • In short, this is how rand() works: in the writable region of libc, there is a table of random values called randtbl. When rand() is called, it takes 2 "random" numbers in randtbl, adds them together and returns it as the result, it also uses that result to update the current randtbl.
  • So if we keep overwriting entries in randtbl to 0, it will make rand() always return 0.
  • Here is the code snippet of what I explained above (link):
int
__random_r (struct random_data *buf, int32_t *result)
{
    ...
    int32_t *fptr = buf->fptr;
    int32_t *rptr = buf->rptr;
    int32_t *end_ptr = buf->end_ptr;
    uint32_t val;
    val = *fptr += (uint32_t) *rptr;
    /* Chucking least random bit.  */
    *result = val >> 1;
    ++fptr;
    if (fptr >= end_ptr)
    {
        fptr = state;
        ++rptr;
    }
    else
    {
        ++rptr;
        if (rptr >= end_ptr)
            rptr = state;
    }
    buf->fptr = fptr;
    buf->rptr = rptr;
    ...
}
Show Comments

Get the latest posts delivered right to your inbox.