From aldeid
Jump to navigation Jump to search
You are here
Challenge 10


Uncompress (password is "flare") and you will get a file named loader with following properties:

MD5 b767bd2c7c29056505d6ca290330b919
SHA1 11c975a899bf81a152218c62c01acdf937007ad0
SHA256 403c319f2aa744d6cba81ea98881ac91d766b582c907ec5bf0d99f2312d9dbd9
File type PE32 executable (GUI) Intel 80386, for MS Windows



Uncompressed files

The file is an AutoIt based executable. Let's use Exe2aut to decompile the loader.exe file. It results in several files:

Driver for Windows 7
MD5sum dade1de693143d9ef28fe7ebe7a7fb22
SHA1sum 745ba710cf5d4a8cbb906f84e6096ca1b9a1bae3
SHA256sum 59dbf937021c7856bad4155440dbd2e0c5422f06589bb11e5ac0b3300aad629c
File type PE32 executable (native) Intel 80386, for MS Windows
Driver for Windows XP
MD5sum 399a3eeb0a8a2748ec760f8f666a87d0
SHA1sum 393f2aefa862701642f566cdaee01a278e2378c0
SHA256sum 57b1d7358d6af4e9d683cf004e1bd59ddac6047e3f5f787c57fea8f60eb0a92b
File type PE32 executable (native) Intel 80386, for MS Windows
Driver controller
MD5sum 205af3831459df9b7fb8d7f66e60884e
SHA1sum dfb2dc09eb381f33c456bae0d26cf28d9fc332e0
SHA256sum 44473274ab947e3921755ff90411c5c6c70c186f6b9d479f1587bea0fc122940
File type PE32 executable (console) Intel 80386, for MS Windows
The decompiled version of the AutoIt executable

Decrypt loader.exe

As the file names suggest, we probably have to do with a driver that is controlled by ioctl.exe. A dynamic analysis confirms it and we can see that the appropriate driver is chosen depending on whether it is run on Windows XP or Windows 7 before being renamed to challenge.sys and placed in \%WINDIR%\System32\ along with ioctl.exe.

The loader_.au3 file shows encrypted content that is passed to the dothis() function. This function accepts 2 parameters: the encoded string and a key.


To decode these strings, we will reuse the decryption functions already in the code. To do that, download the AutoIt program and modify the source code by replacing the Execute statement with MsgBox in the dothis() function:

Func dothis($data, $key)
	$exe = decrypt($data, $key)
	$exe = BinaryToString($exe)
	;Return Execute($exe)
        Return MsgBox(0, "DEBUG", $exe)

Run the modified executable and you will get this:


If @OSArch <> "X86" Then
	MsgBox(0, "Unsupported architecture", "Must be run on x86 architecture")
If @OSVersion = "WIN_7" Then
	FileInstall("challenge-7.sys", @SystemDir & "\challenge.sys")
ElseIf @OSVersion = "WIN_XP" Then
	FileInstall("challenge-xp.sys", @SystemDir & "\challenge.sys")
	MsgBox(0, "Unsupported OS", "Must be run on Windows XP or Windows 7")
FileInstall("ioctl.exe", @SystemDir & "\ioctl.exe")
$nret = _CreateService("", "challenge", @SystemDir & "\challenge.sys", "", "", $SERVICE_KERNEL_DRIVER, $SERVICE_DEMAND_START)
If $nret Then
	If _StartService("", "challenge") Then
		ShellExecute(@SystemDir & "\ioctl.exe", "22E0DC")

Code analysis:

  • first check that the executable is run on a 32 bits OS. If it's not the case, display an error message and exit.
  • Then check whether the OS is Windows 7 or Windows XP and rename the appropriate driver (respectively challenge-7.sys and challenge-xp.sys to challenge.sys in %WINDIR%\system32\). If the executable is run on a different OS, display an error message and exit.
  • Install ioctl.exe in %WINDIR%\system32\
  • Register the challenge.sys driver as a service named challenge and send the ID 22E0DC to the controler (ioctl.exe).


Now, let's have a look at ioctl.exe. Nothing special here. The DeviceIoControl function is just passing the argument to the IRP table of the driver. Now, let's open our driver to check what is called at this IRP.


Determine index

Let's use challenge-7.sys for our analysis. A quick analysis shows that the IRP code (22E0DC) is sent to sub_29CD20 which is the IRP dispatcher.

.text:0029CD20 ; int __stdcall sub_29CD20(int, PIRP Irp)
.text:0029CD20 sub_29CD20      proc near
.text:0029CD20 var_20          = dword ptr -20h
.text:0029CD20 var_1C          = byte ptr -1Ch
.text:0029CD20 var_15          = byte ptr -15h
.text:0029CD20 var_14          = dword ptr -14h
.text:0029CD20 var_5           = byte ptr -5
.text:0029CD20 var_4           = dword ptr -4
.text:0029CD20 Irp             = dword ptr  0Ch
.text:0029CD20                 mov     edi, edi
.text:0029CD22                 push    ebp
.text:0029CD23                 mov     ebp, esp
.text:0029CD25                 sub     esp, 20h
.text:0029CD28                 mov     [ebp+var_5], 41h
.text:0029CD2C                 mov     eax, [ebp+Irp]           ; IRP code saved to EAX
.text:0029CD2F                 mov     dword ptr [eax+18h], 0
.text:0029CD36                 mov     ecx, [ebp+Irp]
.text:0029CD39                 mov     dword ptr [ecx+1Ch], 0
.text:0029CD40                 mov     edx, [ebp+Irp]
.text:0029CD43                 push    edx
.text:0029CD44                 call    sub_29D7B0
.text:0029CD49                 mov     [ebp+var_14], eax       ; EAX saved to var_14
.text:0029CD4C                 mov     eax, [ebp+var_14]       ; var_14 saved to EAX
.text:0029CD4F                 mov     cl, [eax]
.text:0029CD51                 mov     [ebp+var_1C], cl
.text:0029CD54                 cmp     [ebp+var_1C], 0Eh
.text:0029CD58                 jz      short loc_29CD5F
.text:0029CD5A                 jmp     loc_29D46B      ; jumptable 0029CD91 default case
.text:0029CD5F ; ---------------------------------------------------------------------------
.text:0029CD5F loc_29CD5F:
.text:0029CD5F                 mov     edx, [ebp+var_14]
.text:0029CD62                 mov     eax, [edx+0Ch]
.text:0029CD65                 mov     [ebp+var_4], eax
.text:0029CD68                 mov     ecx, [ebp+var_4]
.text:0029CD6B                 mov     [ebp+var_20], ecx
.text:0029CD6E                 mov     edx, [ebp+var_20]
.text:0029CD71                 sub     edx, 22E004h           ; EDX = IRP code - 0x22E004
.text:0029CD77                 mov     [ebp+var_20], edx      ; 
.text:0029CD7A                 cmp     [ebp+var_20], 190h ; switch 401 cases
.text:0029CD81                 ja      loc_29D46B      ; jumptable 0029CD91 default case
.text:0029CD87                 mov     eax, [ebp+var_20]
.text:0029CD8A                 movzx   ecx, ds:byte_29D614[eax]
.text:0029CD91                 jmp     ds:off_29D480[ecx*4] ; switch jump
.text:0029CD98 ; ---------------------------------------------------------------------------

At offset 0x29CD71, we see that 0x22E004 is substracted from the IRP code to get an index. In our case, the index is:

0x22E0DC - 0x22E004 = 0x18

This index is used to get a value from byte_29D614[eax]. In our case, it returns:


This index is then used in a second table to jump to a new location (jmp ds:off_29D480[ecx*4]):


Here is the code at this location:

.text:0029D180 loc_29D180:             ; jumptable 0029CD91 case 216
.text:0029D180 movzx   eax, [ebp+var_5]
.text:0029D184 push    eax
.text:0029D185 call    sub_29C1A0
.text:0029D18A mov     [ebp+var_15], al
.text:0029D18D jmp     loc_29D46B


It only calls the sub_29C1A0 function. Let's jump to that function:

Flare-on-challenge-2015-l10-sub 29C1A0.png

This function is performing a binary check of each bit (value 0 or 1) of a sequence of bytes and jumps to different branches depending on the value of the probed bit. Below is the code corresponding to the checks performed on the 1st byte (8 tests because 1 byte = 8 bits):

.text:0029C1C7                 movzx   ecx, byte ptr [ebp+var_1C]
.text:0029C1CB                 and     ecx, 1
.text:0029C1CE                 jz      short loc_29C1D7 ; jump if 8th bit is 0
.text:0029C1D0                 xor     al, al
.text:0029C1D2                 jmp     loc_29CCE6
.text:0029C1D7 ; ---------------------------------------------------------------------------
.text:0029C1D7 loc_29C1D7:                             ; CODE XREF: sub_29C1A0+2E�j
.text:0029C1D7                 movzx   edx, byte ptr [ebp+var_1C]
.text:0029C1DB                 and     edx, 2
.text:0029C1DE                 jz      short loc_29C1E7 ; jump if 7th bit is 0
.text:0029C1E0                 xor     al, al
.text:0029C1E2                 jmp     loc_29CCE6
.text:0029C1E7 ; ---------------------------------------------------------------------------
.text:0029C1E7 loc_29C1E7:                             ; CODE XREF: sub_29C1A0+3E�j
.text:0029C1E7                 movzx   eax, byte ptr [ebp+var_1C]
.text:0029C1EB                 and     eax, 4
.text:0029C1EE                 jnz     short loc_29C1F7 ; jump if 6th bit is 1
.text:0029C1F0                 xor     al, al
.text:0029C1F2                 jmp     loc_29CCE6
.text:0029C1F7 ; ---------------------------------------------------------------------------
.text:0029C1F7 loc_29C1F7:                             ; CODE XREF: sub_29C1A0+4E�j
.text:0029C1F7                 movzx   ecx, byte ptr [ebp+var_1C]
.text:0029C1FB                 and     ecx, 8
.text:0029C1FE                 jz      short loc_29C207 ; jump if 5th bit is 0
.text:0029C200                 xor     al, al
.text:0029C202                 jmp     loc_29CCE6
.text:0029C207 ; ---------------------------------------------------------------------------
.text:0029C207 loc_29C207:                             ; CODE XREF: sub_29C1A0+5E�j
.text:0029C207                 movzx   edx, byte ptr [ebp+var_1C]
.text:0029C20B                 and     edx, 10h
.text:0029C20E                 jnz     short loc_29C217 ; jump if 4th bit is 1
.text:0029C210                 xor     al, al
.text:0029C212                 jmp     loc_29CCE6
.text:0029C217 ; ---------------------------------------------------------------------------
.text:0029C217 loc_29C217:                             ; CODE XREF: sub_29C1A0+6E�j
.text:0029C217                 movzx   eax, byte ptr [ebp+var_1C]
.text:0029C21B                 and     eax, 20h
.text:0029C21E                 jnz     short loc_29C227 ; jump if 3th bit is 1
.text:0029C220                 xor     al, al
.text:0029C222                 jmp     loc_29CCE6
.text:0029C227 ; ---------------------------------------------------------------------------
.text:0029C227 loc_29C227:                             ; CODE XREF: sub_29C1A0+7E�j
.text:0029C227                 movzx   ecx, byte ptr [ebp+var_1C]
.text:0029C22B                 and     ecx, 40h
.text:0029C22E                 jnz     short loc_29C237 ; jump if 2nd bit is 1
.text:0029C230                 xor     al, al
.text:0029C232                 jmp     loc_29CCE6
.text:0029C237 ; ---------------------------------------------------------------------------
.text:0029C237 loc_29C237:                             ; CODE XREF: sub_29C1A0+8E�j
.text:0029C237                 movzx   edx, byte ptr [ebp+var_1C]
.text:0029C23B                 and     edx, 80h
.text:0029C241                 jz      short loc_29C24A ; jump if 1st bit is 0
.text:0029C243                 xor     al, al
.text:0029C245                 jmp     loc_29CCE6

For the 1st byte, here is what we have:

bit # 8 7 6 5 4 3 2 1
expected value 0 0 1 0 1 1 1 0

wich results (with bits reordered) in 01110100 or 116 in decimal (ASCII char t). And so on for all bytes in the sequence.

Instead of manually performing the same task for all bytes, you can write a python script and execute it in IDA-Pro (Alt+F7):

tmp = ''
secret = []
loc = 0x29C1CE

while loc < 0x29CCE0:

    # Check whether instruction is jz
    if Byte(loc) == 0x74 and (Byte(loc + 1) == 0x7 or Byte(loc + 1) == 0x4):
        tmp += '0'
    # Check whether instruction is jnz
    elif Byte(loc) == 0x75 and (Byte(loc + 1) == 0x7 or Byte(loc + 1) == 0x4):
        tmp += '1'

    # When all 8 bits are tested, reverse bit order and get ascii char corresponding to byte
    if len(tmp) == 8:
        secret.append(chr(int(tmp[::-1], 2)))
        tmp = ''


print ''.join(secret)

This script displays the following message:

try this ioctl: 22E068

IRP 22E068

Let's do the same process with this new IRP. We can directly determine the location as follows:


At this location (offset 0x29cf5a), the code looks like this:

.text:0029CF5A                 call    sub_2D2E0
.text:0029CF5F                 jmp     loc_29D46B

Let's analyze the sub_2D2E0 function.


This function is also very complex as depicted on the following graph:

Flare-on-challenge-2015-l10-sub 2D2E0.png

It is actually a series of conditions and sub-conditions, but a deeper analysis of some of these branches shows that there are actually fake tests. Indeed, as shown on the below screenshot, the execution workflow will always be the same since EDX is set to 0.


At the very end of the function, we notice that there is a buffer (byte_29f210) sent to another function (sub_110F0) as follows:

Flare-on-challenge-2015-l10-byte 29f210.png

A quick analysis of sub_110F0 shows a very probable encryption routine:

Flare-on-challenge-2015-l10-sub 110F0.png

Without going further in the analysis of this function, let's try to debug the driver and stop after the call to this function to check what value the buffer has. Unfortunately, it only contains zeroes.

Patch the sub_2D2E0 function

At this stage, we can guess that we have to patch the sub_2D2E0 function in order to take the jumps where we had fake tests. The problem is that Windows won't let us load a modified driver because an integrity check is performed. For this reason, the best is to dump the memory segment that corresponds to the function, patch it, and reimport the segment. This way, we will be able to execute the function and put a breakpoint after the call to the encryption routine. Let's detail this process.

Once your kernel debugging environment is ready, let's open WinDbg on the debugger's side and put a breakpoint when the driver is loaded:

kd> sxe ld challenge
kd> g

On the debuggee's side, execute loader.exe. It will copy challenge.sys and ioctl.exe to C:\Windows\System32, register the driver and start the service as studied previously. When the driver will be loaded, it should freeze the debuggee and you should see the following output on the debugger's side:

ModLoad: 9aa16000 9acab000   challenge.sys
82a34578 cc              int     3

Now, we will dump the driver header to get the address of the entry point:

kd> !dh challenge
     14C machine (i386)
       6 number of sections
55B81359 time date stamp Wed Jul 29 01:42:17 2015

       0 file pointer to symbol table
       0 number of symbols
      E0 size of optional header
     102 characteristics
            32 bit word machine

     10B magic #
    9.00 linker version
  28E600 size of code
    2200 size of initialized data
       0 size of uninitialized data
  29203E address of entry point
    1000 base of code

Now, we can break at the entry point:

kd> bp challenge+29203E
Loading symbols for 9aa16000    challenge.sys ->   challenge.sys
*** ERROR: Module load completed but symbols could not be loaded for challenge.sys
kd> g
Breakpoint 0 hit
9aca803e 8bff            mov     edi,edi

It is important to take note of the offset 9aca803e because we will have to convert addresses in IDA-Pro to get the equivalent of the ones in WinDbg. You can use a small python snipet as follows:

# d = delta between EP in WinDbg and IDA Pro
def convaddr(a):
    return hex(a+d)

At this stage, we want to break at location 0x29cd71 (converted to 0x9aca2d71), just before the index is computed. It will enable us to modify the value of the IRP because the one by default is 22E0DC and we want to use 22E068 instead, as seen previously.

kd> bp 9aca2d71
kd> g
Breakpoint 1 hit
9aca2d71 81ea04e02200    sub     edx,22E004h

We need to change the value of EDX because it contains the default IRP:

kd> redx
kd> r @edx=22E068

And put a breakpoint at location 0x29cf5a (converted to 0x9aca2f5a) because this is the branch that will be taken with this IRP.

kd> bp 9aca2f5a
kd> g
Breakpoint 2 hit
9aca2f5a e88103d9ff      call    challenge+0x1d2e0 (9aa332e0)

Now, the program is stopped just before sub_2D2E0 is called. This is where we want to patch the function. Let's dump the memory segment corresponding to this function (In IDA Pro, right click on function and select Edit function..). Then convert the addresses as follows:

Flare-on-challenge-2015-l10-dump-sub 2D2E0.png

kd> .writemem sub_2D2E0.mem 9aa332e0 9aab3c3a
Writing 8095b bytes...

Now patch the dump with a small python snippet:

>cd "\program files (x86)\windows kits\8.1\debuggers\x86"
>>> fn = "sub_2D2E0.mem"
>>> buf = bytearray(open(fn, 'rb').read())
>>> with open(fn, 'wb') as f:
...     f.write(buf.replace("\xc6\x45\x9e\x00", "\xc6\x45\x9e\x01"))
>>> exit()

And reimport the patched memory segment into WinDbg:

kd> .readmem sub_2D2E0.mem 9aa332e0 9aab3c3a
Reading 8095b bytes............

Now we want to set a breakpoint just after sub_110F0 is called, at offset 0xADC36 (converted to 0x9aab3c36) and get the value of byte_29F210 (converted to 0x9aca5210).

kd> bp 9aab3c36
kd> g
Breakpoint 3 hit
9aab3c36 8be5            mov     esp,ebp
kd> da 9aca5210
9aca5210  "[email protected]"
9aca5230  ""


The solution is [email protected].


blog comments powered by Disqus

Keywords: reverse-engineering challenge flare fireeye autoit sys kernel driver windbg