[AngstromCTF2020] bookface writeups
All files can be found here
- Given files:
bookface.tar.gz
. - The binary has:
Full RELRO
,Canary found
,NX enabled
andPIE 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.
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_jmp
of 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 ofmmap()
, which is generated byrand()
, to always be 0. - In short, this is how
rand()
works: in the writable region of libc, there is a table of random values calledrandtbl
. Whenrand()
is called, it takes 2 “random” numbers inrandtbl
, adds them together and returns it as the result, it also uses that result to update the currentrandtbl
. - So if we keep overwriting entries in
randtbl
to 0, it will makerand()
always return 0. - Here is the code snippet of what I explained above:
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;
...
}