Pwn iNt All! We find vulnerabilities in script engines and reveal the usermode-protection mechanisms of Windows. Part 1
Operating systems are a delicate matter. And very protected! Getting around their defense mechanisms is a complex and time-consuming affair, even slightly similar to magic. But they say that the mother’s son’s son has already found several RCE vulnerabilities , wrote an exploit, and bypassed all the defense mechanisms. We will try too!
According to legend, we have a binary program, as well as the address on which this service is spinning. It is also mentioned that the first step to success is a certain “key” function.
So let's get started!
Consider the source data. The target program is a kind of command interpreter for working with three types of data: numbers, vectors and matrices. We will use the help command - this will give us an idea of the set of supported commands. The interpreter allows you to:
- Create variables of vectors, matrices and numbers of various sizes.
- Reassign and copy variables.
- Work with individual cells of vector matrices as with numbers, perform simple arithmetic operations with them.
- Search for a value in vectors / matrices, find out the type of a variable and its value.
- Create loops with variable bounds and steps.
The program is implemented as an endless cycle of processing commands:
And each team is represented by a separate regular expression.
So, in search of the “key” function, we check the binary for the characteristic lines associated with the word “key” in all languages of the world. Quickly find the desired fragment:
It looks like there is no key in the binary (which is logical, damn it!). Apparently, the same binary is spinning on the server, and in the line of the argument aFirstKeyIs2c7f there is the correct key there! But how to get the contents of a modified row from a remote server?
Elementary analysis confirms the fact that the function we found is only necessary to call, and the line with the key will be printed in stdout. It seems that this function is a virtual method of some class, since:
- There are no direct calls to this function and sub_40E120 stub functions in the code.
- The sub_40E120 call lies in the list of allowed from the point of view of the Control Flow Guard mechanism , which is typical for virtual methods - these methods are called through indirect calls.
So, the front of work is clear: you need to learn how to call the desired virtual method in the binary on the server side. There are two scenarios:
- Try to find a legitimate opportunity to call the desired method. Theoretically, this may be an undocumented feature of the program.
- Search for vulnerabilities. If you find something that allows us to master the magic of executing arbitrary code, you can easily call the desired method and get the key.
Where to start?
Bookmarks are sweet
First, let's analyze the code for undocumented features. Apparently, the input line processing function does not contain them: the regular expressions used here apply only to the above commands. And there are no direct calls to this function in the code. Perhaps, among the documented teams there are hidden possibilities for indirectly calling the “key” function we need?
But no. Some commands (find, what) contain indirect calls of virtual methods, but it is impossible to influence the choice of the called method - offsets in the vtable are fixed everywhere.
Alas, failure! Now is the time to look for
He who seeks will always find what to seek
- First, a number of variables of various types and sizes are created. Names and types of variables are saved for later use.
- Next, each command is randomly generated in accordance with the list of available variables:
- An arbitrary command is selected (without taking into account the parameters) from the list: the command to create a new variable, the arithmetic operation, the assignment command, the find, print commands.
- Depending on the type of command, arbitrary arguments are added to it. For example, for the find command, an arbitrary name is selected from the list of available variables + an arbitrary number, which is the second argument. For an arithmetic operation command, the number of parameters is large (operation sign, variables, indexes of vectors and matrices), and each of them is also chosen arbitrarily.
- The generated command is added to the current sequence. Since some commands can create new variables and change the type of existing ones, the lists with variables are updated in these cases. This will allow the use of new / changed variables in the process of generating subsequent commands.
- The interpreter runs under the debugger. The generated sequence is transmitted to the input to the interpreter. The registration of possible crashes while processing transmitted commands is being carried out.
III ... Victory! After starting the fuzzer, it was quickly enough able to find vulnerable sequences of commands on which the interpreter “fell”. The result is more than satisfactory - as many as two different vulnerabilities!
Vulnerability No. 1 (UaF)
Having received the first sequence and clearing it of unnecessary commands (which do not affect the fall of the interpreter), we obtain the following Proof-Of-Concept:
Let's take a closer look. Do you recognize? Before us is a classic vulnerability like Use-After-Free ! Here's what happens in this sequence of commands:
- A large vec vector and an intval intval variable are created .
- A copy of the vec vector is created - the variable vec2 .
- The variable vec2 is assigned a variable of another type (integer).
- An attempt to work with the original vec vector leads to writing to the freed memory and, accordingly, to the program crash.
It seems that the interpreter implements the concept of Copy-On-Write, as a result of which the variables vec and vec2 contained a reference to the same buffer in memory. When reassigning one of the variables, the buffer was freed without taking into account the fact that it was referenced in another variable. After that, the vec variable contained a pointer to the freed memory, and had read and write access to it. As a result, this vulnerability allows to gain random access to a piece of memory in which other program objects may be located.
Vulnerability No. 2 (Integer Overflow)
The second problem is related to the constructor of the matrix object. When allocating memory for a numerical array, verification of the width W and the height H of the matrix is incorrectly implemented .
The designer pre-allocates memory for W * H integer cells. If the product W * H was greater than 2 32 -1, this leads to the allocation of a very small buffer due to integer overflow. This can be noticed directly in the binary, in the class constructor.
However, the possibility of subsequent access to the cells of this array is determined by the large boundaries of W and H , and not by the overflowing value of W * H :
This vulnerability, like the first one, allows reading and writing to memory beyond acceptable boundaries.
Each of the vulnerabilities found allows you to achieve the desired result - to become an RCE-god and execute arbitrary code. Consider this procedure in both cases.
I control you!
The possibility of random access to a free piece of memory on the heap is a strong primitive of exploitation. It is necessary to use the damaged vec object (a vector in the first case and a matrix in the second) for the controlled modification of some other object in the process heap (let's call it victim ). By properly changing the internal fields of the victim object , we will achieve the execution of arbitrary code.
First we understand what the objects of numbers, vectors and matrices are in memory. Let's look again at the binary and not far from the input command processing function we will find the constructor calls for the corresponding classes. All of them are inheritors of the base class and have similar constructors. For example, the constructor of a vector object looks like this:
When creating an object, the constructor of the base class is called, then the fields of the object are filled in according to its type. You can see the purpose and order of the fields: virtual function table address, vector size, buffer address, object type as a numerical identifier.
How can I make sure that the victim variable is in the freed memory area accessible with the damaged vec object ? We will create new objects in the heap of the process immediately after bringing the vec object into a damaged state. And to search for an object that will fall into the freed memory, we use the find interpreter command. Let's put some rare value in one of the fields of the created objects - a kind of magic number. Obviously, the buffer size is best suited for this, as it is set directly by the user when creating a new object. If the search for this value in the buffer of the damaged vec object is successful, then we have found the desired object!
We demonstrate the location of the victim object for both vulnerabilities. Actions at this stage will be slightly different from each other.
In the case of the first vulnerability of the UaF type, the find command searches in a limited area of memory - its size coincides with the size of the freed buffer of the vec object. Allocation of an object in a heap is performed by a system allocator and is not a deterministic process. We will choose the size of the buffer from the original vec object and from the created objects so that some of them get into the necessary memory in a small number of attempts:
Well, we successfully found the victim object victim (aka v1 in this case)!
When executing the same scenario using the second vulnerability, everything will be somewhat more complicated. In this case, the search will be potentially carried out in the entire memory, since the width W or the height H of the matrix will be large. However, if the required number is not in the immediate vicinity of the buffer, the search cycle will go beyond the boundaries of the memory mapped into the address space. This will lead to the fall of the interpreter process until it finds the coveted magic number. This problem is solved by allocating a large number of objects in memory - at least one of them gets into memory directly near the buffer.
Bingo! And in this case, we also found the victim object !
And now the most interesting part is that you need to understand how to change the victim object and how to start the execution of the code we need with it. So, we have successfully placed an object of the “vector” class into the freed memory. Schematically, it looks something like this:
The victim object is located in the heap area accessible to us using the vec object , and we can both read the fields of the victim object and change them. We also know the offset of the number bufsize in the buffer of the vec object . Now modify the victim object as follows:
- We read the address of the vtable, thereby revealing the executable address in memory (and defeating ASLR protection).
- Having studied the interpreter binary, we find the constant difference between the address of the table vtable for the vector object and the address of the “key” function. This will allow you to calculate the address of the “key” function on the server side:
- We will create a false table vtable directly in the buffer of the victim object , where we will write the just calculated address of the “key” function.
- Replace the address of the true table vtable of the victim object with the address of the false.
- Call the what command on the victim object . Thanks to the manipulation of the vtable, instead of the genuine virtual method, our “key” function will be called. Since we call the whole function, rather than individual ROP gadgets, Contol Flow Guard protection does not prevent the function from calling.
This scheme of calling the function we need is identical for both vulnerabilities.
We are a step away from success, it remains to put everything together and get the key! Below we consider both examples.
This is the code for a working exploit for a Use-After-Free vulnerability:
Exploit Code for Use-After-Free Vulnerability
var vec var m2:=vec var va=3 m2:=va va=find(vec,256) var v1 va=find(vec,256) var vtb=0 vtb=va-1 var buf=0 buf=va+1 var func=0 func=vec[vtb]-1547208 func=func+57632 v1=func vec[vtb]=vec[buf] what v1
And so - for an Integer Overflow type vulnerability:
Exploit Code for Integer Overflow Vulnerability
var m var v1 var v2 var v3 var v4 var v6 var v5 var v7 var v8 var v9 var v10 var v11 var v12 var v13 var v14 var v16 var v15 var v17 var v18 var v19 var v20 var v21 var v22 var v23 var v24 var v25 var v26 var v27 var v28 var v29 var v30 var v31 var v32 var v33 var v34 var v35 var v36 var v37 var v38 var v39 var v40 var varr=0 var varc=0 varr=find(m,256).row varc=find(m,256).col var vtb=0 vtb=varc-1 var buf=0 buf=varc+1 var func=0 func=m[varr][vtb]-1547208 func=func+57632 v1=func v2=func v3=func v4=func v5=func v6=func v7=func v8=func v9=func v10=func v11=func v12=func v13=func v14=func v15=func v16=func v17=func v18=func v19=func v20=func v21=func v22=func v23=func v24=func v25=func v26=func v27=func v28=func v29=func v30=func v31=func v32=func v33=func v34=func v35=func v36=func v37=func v38=func v39=func v40=func m[varr][vtb]=m[varr][buf] what v1 what v2 what v3 what v4 what v5 what v5 what v6 what v7 what v8 what v9 what v10 what v11 what v12 what v13 what v14 what v15 what v16 what v17 what v18 what v19 what v20 what v21 what v22 what v23 what v24 what v25 what v26 what v27 what v28 what v29 what v30 what v31 what v32 what v33 what v34 what v35 what v36 what v37 what v38 what v39 what v40
Hurrah! The key is received, and a bonus to it is important information. It turns out that there is a second key, but it lies in the file next to the binary on the server side. To solve this problem, you need to build an ROP chain, which means you have to bypass CFG. But more about that in the next article!
And we remind you that everyone who has completed at least one task at NeoQUEST-2018 , is entitled to a prize! Check your mail for a letter, and if it didn’t come to you, write to firstname.lastname@example.org !