This challenge contains a large Windows Portable Executable File which is nearly 3.4 MB. It is usually difficult to reverse engineering such a large file. However, if you have noticed the special resource embedded into this file, things will become easier:
This file is actually a compiled AutoIT program and we can extract the embedded AutoIT script with many tools like exe2aut. With the help of exe2aut I have extracted one AutoIT script and three executable files:
File Name: ioctl.exe
Size: 46,080 bytes
File Name: challenge-xp.sys
Size: 2,688,640 bytes
File Name: challenge-7.sys
Size: 2,689,536 bytes
Apparently, there are two driver files, one is for Windows XP and the other one is for Windows 7. And the ioctl.exe, from the file name, we can guess that it is used to communicate with the driver.
Let’s see what will the AutoIT script do at first. The AutoIT script contains a series of functions used to manage the service like _startservice(), _stopservice(), _serviceexists(), _servicerunning(), _createservice(), _deleteservice(), and the core logic is shown as below:
Firstly, it will check if the system architecture is x86, if so it will drop a driver file to the system directory based on the OS version, after this it will also drop the ioctl.exe to the system directory. Next, it executes the function dothis() with two arguments, one is a hex string and the other one is an ASCII string “flarebearstare”. If the return value of that function is true, it will execute the dothis() function again with a different hex string. If the second dothis() returns true, it will then execute the third dothis() function. So what does the dothis() function do?
The dothis() function actually receives two arguments: data and key. It will decrypt the data with the key and execute the decrypted data. In the decrypt() function, it will execute an opcode by calling API CallWindowProc() and pass the data and key as arguments.
As we already know that the API CallWindowProc() will be called, we can just set a breakpoint on this API in a debugger and follow the API to execute the opcode, and then get the decryption result. However, if you run this program in a debugger, you may probably receiver this error:
The first arguments pass to the CallWindowProc() contains the address of the opcode, now we can put a breakpoint on the first instruction of the opcode and continue the execution. When the breakpoint on the opcode triggered, we can then execute to the “retn” instruction and here the decryption result can be found from the stack:
Repeat the above process for the other two dothis() function, we can have all the decryption code:
_CreateService("", "challenge", "challenge", @SystemDir & "\challenge.sys", "","", $SERVICE_KERNEL_DRIVER, $SERVICE_DEMAND_START)
ShellExecute(@SystemDir & "\ioctl.exe", "22E0DC")
From those codes we know that the driver dropped by this program is registered as a service named “challenge” and then an IoControlCode 0x22E0DC is send to this driver through ioctl.exe.
Now it is the time to analyze the driver. As only one of the driver will be chosen (based on the OS version), so the two drivers must have same functionality and we only need look at one of them. I chose the one for Windows 7. The Entry Pointer of this driver seems normal, it will create a device named “challenge” and then register the unload routine and the dispatch routines. All the dispatch routines are pointing to a same function located at address 0x0029CD20, and if you look at the Graph Overview of this function, you may see something like this:
Terrible, right? In the beginning of this function, it will retrieve the IoControlCode sent to this driver and sub it with 0x22E004, then it will use the result of the subtraction as an index to retrieve another index from a table located at address 0x0029D614, then the second index will be used to retrieve the jump destination address from a jump table located at address 0x0029D480:
So what is the first IoControlCode passed to this driver? Yes, it’s 0x22E0DC. So we can calculate the first index: 0x22E0DC – 0x22E004 = 0xD8, and the we can have the second index which is 0x36, and with the second index, we can finally found the jump address which should be 0x0029D180:
The functionality of this function is actually easy to understand, it will test each bit of a input sequence to see if that bit is 0 or 1, and then jump to different branch based on the test result. Basically, if all the bit test except the last one do not jump to the exit branch, we will get what we want. The following IDA Python script can be used to calculate the correct input sequence:
from idaapi import * def bit_reverse(c): out = 0 for i in range(0, 8): out <<= 1 c1 = c & 0x01 out |= c1 c >>= 1 return out begin_addr = 0x0029C1C7 out = '' addr = begin_addr while Byte(addr) == 0x0f: c = 0 for i in range(0, 8): c <<= 1 if Byte(addr) == 0x0f: for j in range(0, 2): addr = NextHead(addr) if GetMnem(addr) == 'jz': c |= 0 elif GetMnem(addr) == 'jnz': c |= 1 else: print hex(addr) raise Exception('unknown instruction') for j in range(0, 3): addr = NextHead(addr) else: print hex(addr) raise Exception('unknown instruction') out += chr(bit_reverse(c)) print out
Ok, it asks us to try another IoControlCode 0x22E068. We can use the same way described before to calculate the jump destination and the function we should care about this time is located at address 0x0002D2E0, another terrible function:
There are a lot of branches in this function which makes it difficult for us to understand the logic. But one good thing is that no matter what code path the function will walk through, a call at the end of this function will be executed:
This call received three arguments, and a quick analysis on it suggests that it is possibly uses the Tiny Encryption Algorithm (TEA) to decrypt some data. So how do we know what data will be decrypted? or what is the decryption result? I believe the easiest way to answer this is through debugging.
Let’s setup the kernel debugging environment at first since we are going to debug a driver. Here I use two machines, one is my physical machine (running Windows 8) and the other one is a Virtual Machine with Windows 7 installed. There is a wonderful tool named VirtualKD can make things easier. And there are a lot of tutorials online to teach you how to setup a kernel debugging environment, so I will not talk about it in details here, you may refer to the following article if you have not try this:
Now let’s set a breakpoint on the call at 0x000ADC31 and a breakpoint at the beginning of the dispatch routine at 0x0029CD20, then run the executable file of this challenge in the VM. If nothing goes wrong, we will stop at the dispatch routine like below:
However, after executes this function, we get nothing we want, the decrypted data seems meaningless. So what is the problem here? As this call aims at decrypting some data, let’s see what data is being decrypted. The data to be decrypted is passed as the first argument which should be a byte array, and if you look at this argument during debugging, you may be surprised that the array is all zeros when the decryption function is called, that is to say, the data to be decrypted is not initialized! Why?
If you look at the data reference of each byte in that array, you will find that each byte will be assigned a value by a separate function. However, those functions may not get executed when we reach the decryption function and that’s may be the reason why we have an empty array. Knowing this, we can try to initialize the array manually before we execute the decryption function. This can be done by the following IDA Python script:
from idaapi import * begin_addr = 0xA22A0210 out = '' addr = begin_addr for i in range(0, 0x28): found = False for ref in DataRefsTo(addr): if GetMnem(ref) == 'mov': prev_addr = ref for i in range(0, 2): prev_addr = PrevHead(prev_addr) if GetMnem(prev_addr) == 'mov': print hex(addr), GetOpnd(prev_addr, 1) PatchByte(addr, GetOperandValue(prev_addr, 1)) else: print hex(addr) raise Exception('unknown instruction') found = True break else: print hex(addr) raise Exception('unknown instruction') if found == False: print hex(addr) break addr += 1