wtflol

By Arie and Israel

(This writeup is based on the wtflol_reflagged version, but we'll call it wtflol)

wtflol_reflagged

In this challenge, we were given one file, with only one instruction: Can you get the flag?.

wtflol is a 64bit Windows kernel driver, with very little code, but a huge (almost 2MB) .data segment.

Very few strings, among them:

.text:0000000140005060 \Driver\Null .text:0000000140005080 This challenge is fully compatible with windows 8 and above.\n\n .text:00000001400050E0 writing %d bytes

A quick overview of the first functions gives away some simple decoding functions. We get these strings, but it's not clear how they are used yet:

as /e fuckthat PROCESSOR_ARCHITECTURE; .block {.if($spat(@"${fuckthat}","x86")== 0) { .writemem C:\Windows\Temp\kd.dll %p l?%x;!C:'\Windows\Temp\kd.dll.aaaa %p;g} .else {.writemem C:\Windows\Temp\kd.dll %p l?%x;!C:\Windows\Temp\kd.dll.aaaa %p;g}}',0

and

!C:\Windows\Temp\kd.dll.bbbb

We also found with static analysis that the big data blob in fact contains two dlls, encrypted by repeating-key XOR with the key: \xFD\xED\xDD\xCD\xBD\xAD\x0D\x00.

These dlls look very similar, except that one is 32bits, and the other is 64bits. Two interesting functions are named aaaa and bbbb, but it's not clear at this stage how they are used or even how they are called.

The DriverEntry function has references to \\Driver\\Null and to This challenge is *fully* compatible with windows 8 and above.\n\n.

Another interesting thing about these dlls, both have the WinDbgExtensionDllInit function which means that they are WinDbg extension dlls.

Time for some debugging

We started debugging the driver using WinDbg (where the driver was running on a VM).

As the dlls were WinDbg extension dlls we tried to load them into the debugger and run aaaa and bbbb, without any result.

When checking which modules were loaded into WinDbg to see that the dll was loaded correctly we found something intersting, the dll was loaded twice. First from the folder that we manually loaded from and then from C:\Windows\Temp\kd.dll.

This reminded us of the string as /e fuckthat PROCESSOR_ARCHITECTURE; .block {.if($spat(@"${fuckthat}","x86")== 0) { .writemem C:\Windows\Temp\kd.dll %p l?%x;!C:'\Windows\Temp\kd.dll.aaaa %p;g} .else {.writemem C:\Windows\Temp\kd.dll %p l?%x;!C:\Windows\Temp\kd.dll.aaaa %p;g}}',0 this is a WinDbg script that was run.

Attacking the host via remote kernel debugger is not new, but somehow, has not received all the attention one could have expected. For the full explanation see the 2010 Recon presentation by Alex Ionescu and the article by Mateusz ‘j00ru’ Jurczyk.

The bottom line is that the Kernel Debugger communication protocol lets the target print some text into the host's debugger window, break the host's debugger session to query some info... or execute a Debug Command string (see the presentation for the gory details).

The script checks for the host architecture (32 or 64 bits), copies the kernel driver memory part where the relevant dll has been decrypted to a file in the host, and runs the function aaaa.

We thought that function bbbb needed to run in order to get the flag.

First, we tried to run bbbb manually in the same way aaaa was called !C:\Windows\Temp\kd.dll.aaaa %p, but wait, what is the pointer that we need to have there?

For getting the address we opened another instance of WinDbg to debug the first one and added a breakpoint at aaaa, and at bbbb to make sure it was not called earlier.

Indeed aaaa was called with some address and bbbb was not, we tried to run bbbb with the same address without success.

As we saw the following string on the dirver memory !C:\Windows\Temp\kd.dll.bbbb something told us that the script should be called by the driver the same way aaaa was called, we just needed to find a trigger.

We found that the memory used by this string was in some function 0x140003740. Looking carefully where this function is being used we perceived that in DriverEntry there is a call to ObReferenceObjectByName an undocumented function using \\Driver\\Null as a parameter and IoDriverObjectType as the expected object type.

The object recieved from this function is _DRIVER_OBJECT:

   +0x000 Type             : Int2B
   +0x002 Size             : Int2B
   +0x008 DeviceObject     : Ptr64 _DEVICE_OBJECT
   +0x010 Flags            : Uint4B
   +0x018 DriverStart      : Ptr64 Void
   +0x020 DriverSize       : Uint4B
   +0x028 DriverSection    : Ptr64 Void
   +0x030 DriverExtension  : Ptr64 _DRIVER_EXTENSION
   +0x038 DriverName       : _UNICODE_STRING
   +0x048 HardwareDatabase : Ptr64 _UNICODE_STRING
   +0x050 FastIoDispatch   : Ptr64 _FAST_IO_DISPATCH
   +0x058 DriverInit       : Ptr64     long 
   +0x060 DriverStartIo    : Ptr64     void 
   +0x068 DriverUnload     : Ptr64     void 
   +0x070 MajorFunction    : [28] Ptr64     long 

The offset 0xE0 was changed by the driver to the function 0x140003740, offset E0 is MajorFunction[0xe] (0x70 + 0xe*8) when 0xe is IRP_MJ_DEVICE_CONTROL, so the driver has changed the \\Driver\\Null IOCTL function to its own function. Clever! So we need to send an IOCTL to \\Driver\\Null.

The alias for \\Driver\\Null is the NUL file, it is similar to /dev/null on Linux, but after the IOCTL change it does more intersting things :-)

We got the IOCTL code from the function C07FC004, and we understood that the input buffer should between 0x19 and 0x400 bytes.

The IOCTL Handling function (0x140003740) also has a comparison between the IOCTL input (a.k.a "the password") and some deobfuscated buffer.

We just lifted the algorithm and ran it to find the right characters, one by one, of the 25 chars long password.

#include "stdio.h"
unsigned char str[25] ={0x0e, 0x47, 0xad, 0xa4, 0xe1, 0x13, 0x43, 0x3b, 
                        0xcd, 0x7b, 0xda, 0x2f, 0x78, 0xff, 0x24, 0x33, 
                        0xde, 0x6d, 0xb0, 0xcc, 0x1b, 0x14, 0x25, 0x6b, 
                        0xec};

void main(){
unsigned char temp;
for (int i = 0; i < 25; ++i ){
    for (int j=0;j<256;j++){
        temp = ((~((i ^ (((((((-(char)~((i ^ (i+ i + ~((i ^ ((i ^ ((i ^ (((~(i ^ (~(j + 60)- 54) ^ 0x1E)
                - i) ^ 0x71)  - 79))  + 1))  + 1))- i) - 28 - 1)) + 1) - 1) ^ 0x36)- i - 103) ^ 0xE6) + 32)
                 ^ 0x39) - i)) + 1) - 12 + 101) ^ 0xB1) - i;
        if (temp == str[i]){
            printf("%02x", j);
            break;            
        }
       }
  }
}

Then we sent the IOCTL with the password:

    unsigned char InputBuffer[25] = {
        0xE5, 0x37, 0x48, 0xD4, 0x4A, 0x97, 0x26, 0x41, 0x12, 0xFB, 0x3F, 0x51,
        0xF7, 0x03, 0xC9, 0xB1, 0x65, 0xD1, 0x21, 0x0C, 0x58, 0x82, 0xA4, 0xC1,
        0x1F
    };

    hDevice = CreateFile("\\\\.\\NUL",
        GENERIC_READ | GENERIC_WRITE,
        0,
        NULL,
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL);
    
    DeviceIoControl(hDevice,
        (DWORD)0xC07FC004,
        &InputBuffer,
        (DWORD)sizeof(InputBuffer),
        &OutputBuffer,
        sizeof(OutputBuffer),
        &bytesReturned,
        NULL);        

When the IOCTL is received, the driver uses the same mechanism as before to activate the function bbbb from the dll, using the second encrypted script. (BTW, bbbb is also the name KD protocol Break-in control packet :-)).

At this stage, we used DbgView to see the flag printed as a Kernel DbgPrint.

The DbgPrint is handled by function 0x140003E40, that also had a string deobfuscation for the format string Your Flag is: %s\n and of the input it got.

The returned flag is just gibberish.

But just after the print, there is another comparison at 0x140003BF2, with a value that should not have been changed by the driver itself.

That was suspiscous and looked like an anti-debugging mechanism, so we removed the break points, and now, a new message appeared on the console debug of WinDbg only, after the bad flag.

Please continue from here, the pointer to your flag is 00007ffb2de56010, remember to look at the bigger picture :)

This address is actually in the dll loaded by windbg in the host. From there we extracted ... an ELF file!

There was some ascii art of a cat. But nothing else apparent. Ascii steganography ;-D?

CAT ASCII

But most of the ELF file was in fact a huge ugly function at 0x8048913.

When we tried to see how it looks in graph view, IDA complained that the function had to many nodes to display the graph view.

We changed the graph options to raise the maximum to 10000, and by zooming out (to look at the bigger picture), we finally got the flag: BSidesTLV{Dont_Leak_This_One_Lol}

flag

All in all, an amazing challenge, with several surprises, especially for people who have never seen this attack through remote kernel debugging.

Success