Out of bounds write in trustlet ‘1’, allows us to write random bytes at an address of
our choice. We can write our shellcode to an rwx region with this, without any bruteforce.
Note: During the CTF we used a 1 byte brute-force to get write shellcode in the rwx segment and get shell. It was only afterwards that we realised that no bruteforce was required!
The given binary provided is a 64-bit stripped ELF. It took us quite some time to reverse it and find the bug.
Essentially, the binary has 2 options - ‘l’ and ‘c’.
l allows the user to create trustlets that can be of 2 types - ‘1’ or ‘0’. Each trustlet has the following structure:
We can create a maximum of 10 trustlets & they are stored in an array in the following stack based structure -
l option also copies the code of a couple of functions from the text segment to the segment pointed to by the func* pointers in the trustlet structure (we’ll come to details of these functions shortly).
Thus each trustlet has 2 functions associated with it, lets name them ‘g’ and ‘s’ (these are the keys with which we trigger the calls).
By the way there is also a global array, say
offset_array, which contains a randomly assigned integer value
corresponding to each trustlet created.
c functionality executes the functions
s that are associated with the trustlets. It takes the index of a trustlet, asks us whether to execute
s and based on this, executes
Lets talk about these functions now.
For trustlet ‘1’ -
- Function g : It takes our input and an offset (provided by the user). The
function_datais offsetted by the value
corresponding to the specific trustlet in the global
offset_array. Our offset is added to this value (lets call this
target). A byte from
targetis xored with a random value and saved in
targetas well as our input string
and our updated input string is returned. (I skipped some details here, but we’ll get back to it.)
- Function s : Takes an input buffer with it’s size, xor’s them and returns the result.
For trustlet ‘0’ -
- Function g : Takes a page offset (
pgoff) and a page count (
pgcnt) and “hashes” the content of
one by one, starting from the
pgoffpage. The “hash” for each page is stored as in an
intarray in the last page, and this is returned by the function.
- Function s : Sets the bytes in
trustlet->data, which lies in stack.
s function of the trustlet with type
1, the offset that we provide can be negative. Thus we can write to addresses that lie before the original
function_data segment of the corresponding trustlet.
Here’s how the various trustlets are arranged in the memory -
0x00007fffa7a0c000 0x00007fffa7a14000 rwx # func_g and func_s of trustlet at offset 0
So if we have the OOB write in trustlet at offset 1, we can write to the data of
func_s of this
trustlet. But here comes the issue that we can’t predictively tell the address that we will write to as the
function_data is offsetted by a random value.
So we first have to somehow leak the value of this random value before we go on to exploiting this. Lets move on…
First we need some way to get the random value corresponding to a trustlet, stored in
offset_array. Since we can give a large negative value as the offset, we can write to the
function_data section of the previous
trustlet. So what is the use of this? Recall what the
g function of a trustlet with type
1 does. It hashes the
content of a page and saves this in an int array that is printed out. So we first create a trustlet of type
then another with type
1. After that print the hashes of all the pages of the (currently empty)
function_data section (basically call
g on trustlet with index 0, with offset=0 and count=0x7fd8). Now we give such an
offset in the
s function of the trustlet with
type 1 that the random value will definitely be written in the
function_data section of the previous
type 1 trustlet. After this again call
g function of
type 0 with
count=0x7fd8 and offset=0. This will again print out the hashes of all the pages. Now all we have to do is see which page a different hash from the initial one and we can predict the random value.
Now we have to calculate the offset to give in the
s function of trustlet 1.
0x00007fffa7a14000 is the start of
function_data of trustlet 0 and
0x00007fffafa14000 is that of trustlet 1. If we subtract these we get
0x8000000. If the offset is
-0x8000000 then the target address is sure to lie in the function_data of the
previous trustlet. Moreover, with this as the index, the page_offset we get from comparing the mismatched
page hash in
g function of trustlet 0 will be the page index of the offsetted
function_data segment of type 1. Thus the random value can be obtained by multiplying this with 1024.
Right so now that we have the value of the
offset_array of the trustlet with index 1, we can write to any
address that lies before the
function_data of this trustlet. But where do we write? The obvious choice is the
func_s segment of this trustlet, as they are
rwx regions and we can call them at our choice.
But here comes the next problem; we can’t write data of our choice. What is written at the target address is
the byte at the address xor’ed with a random value. One obvious solution is to use bruteforce to get a
shellcode in the
func_g segment. But the smallest shellcode (stub) that we could think of was around
12-15 bytes. Given the network speed in our locality, this could take ages. So we had to optimize.
Lets take a look at the code that actually creates the array from which the random value
that is used in the xoring comes.
for ( j = 0; j <= 0xFF; ++j )
i. After this using a buffer with random bytes, we calculate an index and
swap the value in
rand_val_array at this index.
So what if we bruteforce the termination value of the second
for loop to make it
0? Then the for loop would become -
for ( k = 0; k <= 0; ++k )
Thus now the code within this loop will never be executed.
Do remember that the target we are bruteforcing is the
func_s section of the trustlet at index 1.
So we bruteforced the 1 byte here.
It was afterwards that it struck us that this can be done without any bruteforce, by overwriting the initialization value of
gef➤ x/i 0x000555555555559
As you can see, the instruction is ‘mov DWORD PTR’ (makes sense as
k is an int variable).
So we can overwrite the second last byte with any number (other than 0 of course) and k is larger than 0xff!
Here we have a 1/256 probablity of being wrong! Thus no bruteforce required…
out = write_val(0x1de, 0)
So what did we gain from this? Well now
rand_val_array is not random anymore. It is just an array with
array[i]=i. Now lets take a look at the code that does the xoring…
a = 0;
Lets assume that our
input_size is always 1 (we give only one bytes at a time). Thus the loop will run only
once. The snippet updates the value of
rand_val_array[b] and xors a byte from the target address with
rand_val_array[(rand_val_array[a] + rand_val_array[b])]. Since the loop
runs only once, the swapping has no effect
rand_val_array[a] + rand_val_array[b] =
rand_val_array[b] + rand_val_array[a])
Also we can calculate
- a = (a+1) = 1
- b = (rand_val_array[a] + b) = (rand_val_array + 0) = 1
- v9 = (rand_val_array[a] + rand_val_array[b]) = 1+1 = 2 (a and b are 1)
So our target value will be xored with 2. Lets set our target address to the place where the initial value of b is
set to zero.
gef➤ x/i 0x000555555555649
If we xor that
0 with 2, the updated instruction reads -
0x555555555649: mov DWORD PTR [rbp-0xc0],0x2
Thus initial value of
b is now 2. Doing this 5 times sets the initial value of
b to 62 (bear with me, everything will be clear in a minute :P) and the code looks like
a = 0;
Thus value of
v9 = (62+1)+1 = 64.
So whatever target address we give now,
*target = *target ^ 64.
This piece of code is present at the start of the
s function of trustlet 1
offsetted_function_data = (offset_array_value << 12) + *(struct_1 + 16);
The second line sets the
offsetted_function_data + 64. Looking at the assembly -
0000555555555492 : mov rax, [rbp+offsetted_function_data]
64 ^ 64 = 0. So we can change that second line to
save_area = offsetted_function_data + 0;
offsetted_function_data ! Also take a look at the third line of code (the for loop). It copies our input to the
And now for the finale; the final part of the puzzle with which all the above stuff will make sense. Look at the
part how data at the target is written -
for ( m = 0; m < input_size; ++m )
Ahhh! The target address is set using the
save_area, but the
save_area is the
offsetted_function_data which has our input ! So we can write our (uncorrupted) input at an arbitrary address. But one last snag still remains -
a = 0;
This corrupts the
save_area. But this is not an issue now. We just xor the initial value of
l (the loop variable) with 64 to get the loop as -
for ( l = 64; input_size > l; ++l )
So provided that our
input_size is less than 64, we are done !
All that is left is to give a shellcode as an input and decide on a target address. We gave the target address as the
func_g of this trustlet (index = 1). So our shellcode got copied to
func_g. Now on running
func_g we get shell !!!
Here is the full exploit -
from pwn import *
A simpler way to solve this would be to forge, by bruteforcing 12 bytes, a shellcode to read more data into the
rwx region. This was our initial idea, but due to a slow network we spent some time trying to optimize the
bruteforce before stumbling on to the solution that only uses a 1-byte bruteforce. Afterwards we realised that this could have been done without bruteforce :)
This was the only pwn we solved in this CTF. All the
sandbox challenges of this CTF were really cool
and creative !