📜 ⬆️ ⬇️

Pwn iNt All! We find vulnerabilities in scripting engines and open usermode-mechanisms for protecting Windows. Part 1


Operating systems are a delicate matter. And very secure! To bypass their defense mechanisms is a complicated and time-consuming business, even a bit like magic. But they say that the son of my mother's friend has already found several RCE vulnerabilities , wrote an exploit, and bypassed all the defense mechanisms. We will try and we!

Let's do it on the example of task number 11 from the past NeoQUEST-2018 . The task is realistic - consider a “live” script of remote code execution, similar to the one that occurs during the operation of Javascript engines in browsers. We will look for vulnerabilities in some interpreter, implement them to get an ARW-primitive, and as a result we will bypass the protection of the Windows user-mode OS. The task turned out to be extremely difficult and interesting, therefore we will dedicate to it a whole cycle of articles with a detailed description of the path to success.

According to legend, we have a binary program, as well as the address where this service runs. It is also mentioned that the first step to success is a certain “key” function.
')
So let's get started!

"Key" search


Consider the original data. The target program is a kind of command interpreter for working with three data types: numbers, vectors, and matrices. Use the help command - this will give us an idea of ​​the set of supported commands. The interpreter allows you to:





The program is implemented as an infinite command processing loop:



And each team is represented by a separate regular expression.


So, in search of the “key” function, we will check the binary for characteristic strings associated with the word “key” in all languages ​​of the world. Quickly find the desired fragment:




It seems that in the binary key is missing (which is logical, damn it!). Apparently, the same binary is running on the server, and in the argument string aFirstKeyIs2c7f there is the correct key! But how to get the contents of a modified string from a remote server?

An elementary analysis confirms the fact that the function we found need only be called, and the string with the key will be printed in stdout. It seems that this function is a virtual method of some class, because:






So, the scope of work is clear: it is necessary to learn how to call the desired virtual method in the binary on the server side. There are two options for the development of events:

  1. Try to find a legitimate opportunity to call the desired method. Theoretically, this may be an undocumented possibility of the program.
  2. Search for vulnerabilities. If you find one that allows us to master the magic of executing arbitrary code, then 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 function of processing the input string does not contain them: the regular expressions used here refer only to the commands listed above. And there are no direct calls to this function in the code. Perhaps among the documented commands there are hidden opportunities to indirectly call the “key” function we need?

But no. Some commands (find, what) contain indirect calls to virtual methods, but it is impossible to influence the choice of the method being called - the offsets in the vtable table are fixed everywhere.





Alas, bad luck! Now is the time to look for excuses for vulnerabilities that will allow us to call a “key” function.

He who seeks will always find what to look for.


We are programmers at mom, so let's try to write a simple fuzzer that generates random, syntactically correct sets of commands for the interpreter. Javascript and DOM fuzzers are written on the same principle when searching for vulnerabilities in browsers. Our fuzzer will generate various command sequences in the hope of “dropping” the interpreter. The principle of operation of the fuzzer when generating the next sequence of commands is as follows:

  1. First, a number of variables of various types and sizes are created. Names and types of variables are saved for later use.
  2. Then each command is generated randomly according to the list of available variables:
    • An arbitrary command (without parameters) is selected from the list: the command to create a new variable, an arithmetic operation, an assignment command, the find, print command.
    • 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 (the sign of the operation, variables, indices of vectors and matrices), and each of them is also chosen arbitrarily.
    • The generated command is added to the current sequence. Since some teams can create new variables and change the type of existing ones, the lists with variables in these cases are updated. This will allow the use of new / changed variables in the process of generating subsequent commands.
  3. The interpreter runs under the debugger. The generated sequence is transmitted to the input of the interpreter. Possible crashes are recorded when processing transmitted commands.

Iii ... Victory! After launching the fuzzer, we quickly managed to find vulnerable sequences of commands on which the interpreter “fell”. The result is more than satisfactory - as many as two different vulnerabilities!

Vulnerability # 1 (UaF)


After receiving the first sequence and clearing it of unnecessary commands (which do not affect the interpreter’s fall), we obtain the following Proof-Of-Concept:



Let's take a closer look. Do you recognize? Before us is a classic vulnerability of type Use-After-Free ! Here is what happens in this sequence of commands:

  1. A large vector vec and an integer variable intval are created .
  2. A copy of the vector vec is created - the variable vec2 .
  3. The variable vec2 is assigned to a variable of another type (integer).
  4. An attempt to work with the source vector vec leads to writing to the freed memory and, accordingly, to the crash of the program.




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 variable vec contained a pointer to the freed memory, and had access to it for reading and writing. As a result, this vulnerability allows random access to a section of memory in which other program objects may be located.

Vulnerability # 2 (Integer Overflow)


The second problem is related to the constructor of the matrix object. When allocating memory for a numerical array, a check of the width W and the height H of the matrix is ​​incorrectly implemented.



In the constructor, memory is pre-allocated for W * H integer cells. If the product W * H was greater than 2 32 -1, then this results in the allocation of a very small buffer due to the integer overflow. This can be seen 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 the permissible limits.

Each of the found vulnerabilities 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 the free memory in the heap is a strong primitive operation. It is necessary to use the damaged object vec (the vector in the first case and the matrix in the second) for the controlled modification of some other object in the process heap (let's call it victim ). By appropriately changing the internal fields of the victim object, we will achieve the execution of an arbitrary code.
First, we understand what are the objects of number, vector and matrix in memory. Again, we will look at the binary and close to the input command processing function, we will find constructor calls for the corresponding classes. All of them are the heirs of the base class and have similar constructors. For example, the vector object constructor looks like this:



When an object is created, the constructor of the base class is called, then the object's fields are filled in according to its type. You can see the purpose and order of the fields: the address of the table of virtual functions, the size of the vector, the address of the buffer, the type of object in the form of a numerical identifier.
How to ensure that in the freed memory area accessible by the damaged object vec , the victim variable is found? We will create new objects in the process heap immediately after bringing the vec object into a damaged state. And to search for an object that does fall into the freed memory, use the find interpreter command . Place one of the rare objects in one of the fields of the created objects - a peculiar magic number. Obviously, the buffer size is best for this, since it is set directly by the user when creating a new object. If the search for this value in the buffer of the damaged object vec is crowned with success, then we have found the desired object!
We demonstrate the victim object for both vulnerabilities. Actions at this stage will differ slightly 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. Object allocation in the heap is performed by the system allocator and is not a deterministic process. We will select the buffer sizes of the initial object vec and of the created objects so that one of them gets into the right memory for a small number of attempts:



Great, we successfully found the victim object ( victim v1 in this case)!

If you execute 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 in the immediate vicinity of the buffer is not, then the search cycle will go beyond the boundaries of the memory mapped to the address space. This will lead to a drop in the interpreter process until the magic number is found. This problem is solved by allocating a large number of objects in memory - at least one of them will fall into memory directly near the buffer.





Bingo! And in this case, we also found the victim object!

And now the most interesting thing is to understand how exactly to change the victim object and how to start with it the execution of the code we need. So, we successfully placed an object of the “vector” class in the released 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 modify them. We also know the offset of the number bufsize in the buffer of the object vec . Now modify the victim object as follows:

  1. Read the address of the vtable table, thereby revealing the executable address in memory (and defeating the ASLR protection).
  2. Having studied the interpreter's binary, we find the constant difference between the address of the vtable table 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:


  3. We form a false table vtable directly in the buffer of the victim object, where we write the address of the key function just calculated.
  4. Replace the address of the true vtable table of the victim object with the address false.
  5. Call the command what the victim object. Due to the manipulation of the vtable table, instead of the genuine virtual method, our “key” function will be called. Since we are calling the whole function, rather than individual ROP gadgets, the Contol Flow Guard protection doesn’t prevent the function from being invoked.



This scheme of calling the function we need is identical for both vulnerabilities.

We are one step away from success, it remains to put everything together and get the key! Below we consider both examples.
This is the code of a working exploit for a vulnerability of type Use-After-Free:



Exploit Code for Use-After-Free Vulnerability
var vec[4096] var m2:=vec var va=3 m2:=va va=find(vec,256) var v1[256] 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[0]=func vec[vtb]=vec[buf] what v1 



And so for the Integer Overflow vulnerability:



Exploit Code for Integer Overflow Vulnerability
 var m[65537][65536] var v1[256] var v2[256] var v3[256] var v4[256] var v6[256] var v5[256] var v7[256] var v8[256] var v9[256] var v10[256] var v11[256] var v12[256] var v13[256] var v14[256] var v16[256] var v15[256] var v17[256] var v18[256] var v19[256] var v20[256] var v21[256] var v22[256] var v23[256] var v24[256] var v25[256] var v26[256] var v27[256] var v28[256] var v29[256] var v30[256] var v31[256] var v32[256] var v33[256] var v34[256] var v35[256] var v36[256] var v37[256] var v38[256] var v39[256] var v40[256] 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[0]=func v2[0]=func v3[0]=func v4[0]=func v5[0]=func v6[0]=func v7[0]=func v8[0]=func v9[0]=func v10[0]=func v11[0]=func v12[0]=func v13[0]=func v14[0]=func v15[0]=func v16[0]=func v17[0]=func v18[0]=func v19[0]=func v20[0]=func v21[0]=func v22[0]=func v23[0]=func v24[0]=func v25[0]=func v26[0]=func v27[0]=func v28[0]=func v29[0]=func v30[0]=func v31[0]=func v32[0]=func v33[0]=func v34[0]=func v35[0]=func v36[0]=func v37[0]=func v38[0]=func v39[0]=func v40[0]=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 


Hooray! 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, it is required to build a ROP chain, which means it will have to bypass the CFG. But about this - in the next article!

And we remind that everyone who completed at least one task on NeoQUEST-2018 is entitled to a prize! Check your mail for the presence of a letter, and if suddenly it did not come to you - write to support@neoquest.ru !

Source: https://habr.com/ru/post/353850/


All Articles