📜 ⬆️ ⬇️

Immersion in the driver: the general principle of the reverse example of the task NeoQUEST-2019


Like all programmers, you love code. You are with him - best friends. But sooner or later in life there comes a moment when there will be no code with you. Yes, it's hard to believe, but there will be a huge gulf between you: you are outside, and he is deep inside. From despair, you, like everyone, will have to go to the other side. On the side of reverse engineering.

Using the example of task number 2 from the online stage NeoQUEST-2019, we analyze the general principle of reverse Windows driver. Of course, the example is rather simplistic, but the essence of the process does not change from this - the only question is the amount of code that needs to be reviewed. Armed with experience and good luck, let's get started!

Given


According to legend, we were given two files: a traffic dump and a binary file, which this very traffic generated. First, look at the dump using Wireshark:


The dump contains a stream of UDP packets, each of which contains 6 bytes of data. These data, at first glance, represent some sort of random set of bytes — it is not possible to pull something out of the traffic. Therefore, we will turn our attention to the binary, which should tell you how to decipher everything.
Open it in IDA:
')

It seems that we face some kind of driver. Functions prefixed with WSK relate to Winsock Kernel, the Windows Kernel-Mode Network Programming Interface. See the MSDN for a description of the structures and functions used in the WSK.

For convenience, you can load the Windows Driver Kit 8 (kernel mode) - wdk8_km (or any newer) library into IDA to use the types defined there:


Caution, reverse!


As always, we start from the entry point:


Let's go in order. At first, Wsk is initialized, a socket is created and bind, we will not describe these functions in detail, they do not carry any useful information for us.

The sub_140001608 function sets 4 global variables. Let's call it InitVars. In one of them is written the value lying at the address 0xFFFFF78000000320. A little googling this address, you can make the assumption that it recorded the number of ticks of the system timer since the system was booted. For now let's name the variable TickCount.


Next, EntryPoint installs functions for processing IRP packets (I / O Request Packet). You can read more about them on MSDN. For all types of requests, a function is defined that simply passes the packet to the next driver in the stack.


But for the type IRP_MJ_READ (3) a separate function is defined; call it IrpRead.



It, in turn, sets CompletionRoutine.


CompletionRoutine fills an unknown structure with data obtained from the IRP and places it on the list. So far we do not know what is inside the package - we will return to this function later.
Look further into EntryPoint. After determining the IRP handlers, the sub_1400012F8 function is called. Let's look inside and immediately notice that a device is created in it (IoCreateDevice).


Let's call the function AddDevice. If we correctly type, we will see that the device name is "\\ Device \\ KeyboardClass0". This means that our driver interacts with the keyboard. Googling about IRP_MJ_READ in the context of the keyboard, you can find that the KEYBOARD_INPUT_DATA structure is transmitted in packets. Let's return to CompletionRoutine and see what data it transmits.


IDA is a poorly parsit structure here, but by offsets and further calls it can be understood that it consists of ListEntry, KeyData (the scan code of the key is stored here) and KeyFlags.
After AddDevice in EntryPoint, the sub_140001274 function is called. She creates a new thread.


Let's see what happens in ThreadFunc.


It gets the value from the list and processes them. Immediately pay attention to the function sub_140001A18.


It passes the processed data to the input of the sub_140001A68 function, along with a pointer to the WskSocket and the number 0x89E0FEA928230002. Having examined the number-parameter by bytes (0x89 = 137, 0xE0 = 224, 0xFE = 243, 0xA9 = 169, 0x2328 = 9000), we get exactly the same address and port from the traffic dump: 169.243.224.137:9000. It is logical to assume that this function sends a network packet to the specified address and port - we will not consider it in detail.
We will understand how data is processed before sending.

For the first two elements, perform an equivalent with the generated value. Since the number of ticks is used for the calculation, we can assume that we are generating a pseudo-random number.



After generating the number, it overwrites the value of the variable we previously named TickCount. Variables for the formula are set in InitVars. If we return to the call of this function, we will know the values ​​for these variables, and in the end we will get the following formula:

(54773 + 7141 * prev_value)% 259200

This is a linear congruential pseudo-random number generator . It is initialized in InitVars using TickCount. For each subsequent number, the previous value is used as the initial value (the generator returns a two-byte value, and the same is used for subsequent generation).


After an equivalence with a random number of two values ​​transmitted from the keyboard, a function is called that forms the remaining two bytes of the message. It simply produces the xor of the two already-encrypted parameters and some constant value. This is unlikely to allow somehow decrypt the data, so the last two bytes of the message for us do not carry any useful information, and they can not be considered. But what to do with the encrypted data?
Let's take a closer look at what is encrypted. KeyData is a scan code, it can take a fairly wide range of values, it is not easy to guess. KeyFlags is a bit field:


If you look at the table of scan codes, then you will notice that most often the flag will be either 0 (the key is omitted) or 1 (the key is raised). KEY_E0 will be set up quite rarely, but it may happen, but to meet KEY_E1 the chances are very small. Therefore, you can try to do the following: we go through the data from the dump, choose a value that is encrypted KeyFlags, produce an equivalent from 0, generate two successive PDHs. First, KeyData is a single byte, and we can check the correctness of the generated PN on the high byte. And secondly, the next encrypted KeyFlags, when making an equivalent with the correct PN, will take the same bit values. If this is not the case, then we assume that the KeyFlags that we initially considered was 1, and so on.
Let's try to implement our algorithm. To do this, use python:

Algorithm implementation
#  -   keymap = […] # ,   Wireshark traffic_dump = […] #  def bxnor(a, b): return ((~a & 0xffff) | b) & (a | (~b & 0xffff)) #   def brgen(a): return ((7141 * a + 54773) % 259200) & 0xffff def decode(): #     for i in range(0, len(traffic_dump) - 1): #   KeyFlags probe = traffic_dump[i][1] #   - scancode = traffic_dump[i+1][0] #    KeyFlags tester = traffic_dump[i+1][1] fail = True #     (  KEY_E1) for flag in range(4): rnd_flag = bxnor(flag, probe) rnd_sc = brgen(rnd_flag) next_flag = bxnor(tester, brgen(rnd_sc)) #   KeyFlags if next_flag in range(4): sc = bxnor(rnd_sc, scancode) if sc < len(keymap): sym = keymap[sc] if next_flag % 2 == 0: print(sym, end='') fail = False break #   -      KeyFlags   if fail: print('Something went wrong on {} pair'.format(i)) return print() if __name__ == "__main__": decode() 


Run our script on the data received from the dump:


And in the decrypted traffic, we discover our most desirable line!

NQ2019DABE17518674F97DBA393415E9727982FC52C202549E6C1740BC0933C694B3DE


Soon there will be articles with analyzes of other tasks, do not miss it!

PS And we remind that everyone who completed at least one task on NeoQUEST-2019 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/446462/


All Articles