The-FLARE-On-Challenge-2015/Challenge-10
You are here | Challenge 10
|
File
Uncompress DC682778F53E853B3188AC63EB376D8B.zip (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 |
Analysis
loader.exe
Uncompressed files
The file is an AutoIt based executable. Let's use Exe2aut to decompile the loader.exe file. It results in several files:
- challenge-7.sys
- Driver for Windows 7
MD5sum dade1de693143d9ef28fe7ebe7a7fb22 SHA1sum 745ba710cf5d4a8cbb906f84e6096ca1b9a1bae3 SHA256sum 59dbf937021c7856bad4155440dbd2e0c5422f06589bb11e5ac0b3300aad629c File type PE32 executable (native) Intel 80386, for MS Windows
- challenge-xp.sys
- Driver for Windows XP
MD5sum 399a3eeb0a8a2748ec760f8f666a87d0 SHA1sum 393f2aefa862701642f566cdaee01a278e2378c0 SHA256sum 57b1d7358d6af4e9d683cf004e1bd59ddac6047e3f5f787c57fea8f60eb0a92b File type PE32 executable (native) Intel 80386, for MS Windows
- ioctl.exe
- Driver controller
MD5sum 205af3831459df9b7fb8d7f66e60884e SHA1sum dfb2dc09eb381f33c456bae0d26cf28d9fc332e0 SHA256sum 44473274ab947e3921755ff90411c5c6c70c186f6b9d479f1587bea0fc122940 File type PE32 executable (console) Intel 80386, for MS Windows
- loader_.au3
- 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)
EndFunc
Run the modified executable and you will get this:
If @OSArch <> "X86" Then
MsgBox(0, "Unsupported architecture", "Must be run on x86 architecture")
Exit
EndIf
If @OSVersion = "WIN_7" Then
FileInstall("challenge-7.sys", @SystemDir & "\challenge.sys")
ElseIf @OSVersion = "WIN_XP" Then
FileInstall("challenge-xp.sys", @SystemDir & "\challenge.sys")
Else
MsgBox(0, "Unsupported OS", "Must be run on Windows XP or Windows 7")
Exit
EndIf
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")
EndIf
EndIf
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).
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.
challenge.sys
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
.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
.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
.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:
Python>hex(Byte(0x29D614+0xd8)) 0x36
This index is then used in a second table to jump to a new location (jmp ds:off_29D480[ecx*4]):
Python>hex(Dword(0x29D480+0x36*4)) 0x29d180L
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
sub_29C1A0
It only calls the sub_29C1A0 function. Let's jump to that function:
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
.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
.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
.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
.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
.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
.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
.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 = ''
loc+=1
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:
Python>hex(Dword(0x29D480+Byte(0x29D614+0x22E068-0x22E004)*4)) 0x29cf5aL
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.
sub_2D2E0
This function is also very complex as depicted on the following graph:
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:
A quick analysis of sub_110F0 shows a very probable encryption routine:
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 nt!DbgLoadImageSymbols+0x47: 82a34578 cc int 3
Now, we will dump the driver header to get the address of the entry point:
kd> !dh challenge File Type: EXECUTABLE IMAGE FILE HEADER VALUES 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 Executable 32 bit word machine OPTIONAL HEADER VALUES 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 [REMOVED]
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 challenge+0x29203e: 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
d=0x9aca803e-0x2a203e
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 challenge+0x28cd71: 9aca2d71 81ea04e02200 sub edx,22E004h
We need to change the value of EDX because it contains the default IRP:
kd> redx edx=0022e0dc 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 challenge+0x28cf5a: 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:
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" >python >>> 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 challenge+0x9dc36: 9aab3c36 8be5 mov esp,ebp kd> da 9aca5210 9aca5210 "unconditional_conditions@flare-o" 9aca5230 "n.com"
Solution
The solution is [email protected].
Comments
Keywords: reverse-engineering challenge flare fireeye autoit sys kernel driver windbg