Skip to main content

CommandLine rewriting and Reflective loading

1. Overview

This article describes the following topics.
・Commandline rewriting technique
・Applying new command line to reflective loaded PE file's context

My goal was to develop loader that load PE file from URL and launch PE file in memory with new commandline context. This is stealth since it leaves no final payload on filesystem. Since this is a topic that has been described exhaustively, this post does not describe downloading PE file and reflective load. This article does not show the full code to prevent abuse.

2. Commandline rewriting

When this loader starts, loader's commandline is "loader.exe [c2url] [newcommand]". This loader needs to load the PE file into memory and patch memory so that [newcommand] is handled as the first argument. The commandline is included in RTL_USER_PROCESS_PARAMETERS structure, which is pointed to by ProcessParameters member of PEB structure. PEB is very important structure in the process.
I believe it will work even if only CommandLine member is patched, but this time I created new RTL_USER_PROCESS_PARAMETERS structure. RTLCreateProcessParametersEx function can be used to create new RTL_USER_PROCESS_PARAMETERS structure. C# code to create and patch new RTL_USER_PROCESS_PARAMETERS structure is shown as follows.
...
//Save old commandline context
System.Diagnostics.Process hProcess = System.Diagnostics.Process.GetCurrentProcess();
IntPtr handle = hProcess.Handle;
PROCESS_BASIC_INFORMATION pbi = new PROCESS_BASIC_INFORMATION();

IntPtr tmp = IntPtr.Zero;
NtQueryInformationProcess(handle, 0, ref pbi, (uint)(IntPtr.Size * 6), tmp);
IntPtr pebAddress = pbi.PebAddress;
PEB peb = PEB.Create(pebAddress);

RTL_USER_PROCESS_PARAMETERS paramter = RTL_USER_PROCESS_PARAMETERS.Create(peb.ProcessParameters);
Parameter = paramter.CommandLine;
DllPath = paramter.DllPath;
ImagePath = paramter.ImagePathName;
CurrentDir = paramter.CurrentDirectory.DosPath;
WindowTitle = paramter.WindowTitle;
DesktopInfo = paramter.DesktopInfo;
ShellInfo = paramter.ShellInfo;
RuntimeData = paramter.RuntimeData;
Flags = paramter.WindowFlags;
Environment = paramter.Environment;

ConsoleHandle = paramter.ConsoleHandle;
StandardInput = paramter.StandardInput;
StandardOutput = paramter.StandardOutput;
StandardError = paramter.StandardError;
...
//Create new commandline context
UNICODE_STRING patch_commandline = CreateUnicodeString(Marshal.PtrToStringUni(ImagePath.Buffer) + " " + param);
IntPtr pParamter = IntPtr.Zero;

uint status = RtlCreateProcessParametersEx(ref pParamter, ref ImagePath, ref DllPath, ref CurrentDir, ref patch_commandline, Environment, ref WindowTitle, ref DesktopInfo, ref ShellInfo, ref RuntimeData, Flags);
...
//Patch commandline
ulong writesize = 0;
long pacth = (long)pParamter;
WriteProcessMemory(handle, pebAddress + 0x20, ref pacth, 8, ref writesize);
...
I ran this code and it triggered a crash. What went wrong?
I checked RTL_USER_PROCESS_PARAMETERS structure created by RTLCreateProcessParametersEx function.
It appears that the pointer to buffer is wrong. After investigation, I noticed that this is an offset from the top of RTL_USER_PROCESS_PARAMETERS structure. Therefore, the top address of RTL_USER_PROCESS_PARAMETERS structure was added to buffer member of each unicode string. In addition, each handle was null, so this was patched.
public void RelocateUnicodeString(IntPtr Addr, long baseaddr)
{
    System.Diagnostics.Process hProcess = System.Diagnostics.Process.GetCurrentProcess();
    IntPtr handle = hProcess.Handle;
    ulong writesize = 0;

    IntPtr patch_ptr = (IntPtr)Marshal.PtrToStructure(Addr + 8, typeof(IntPtr));
    long patch = (long)patch_ptr + baseaddr;
    WriteProcessMemory(handle, Addr + 8, ref patch, 8, ref writesize);

    IntPtr patched_ptr = (IntPtr)Marshal.PtrToStructure(Addr + 8, typeof(IntPtr));
}

public void PatchMember(IntPtr Addr, long value, Int32 size)
{
    System.Diagnostics.Process hProcess = System.Diagnostics.Process.GetCurrentProcess();
    IntPtr handle = hProcess.Handle;
    ulong writesize = 0;
    WriteProcessMemory(handle, Addr, ref value, size, ref writesize);
}
...
RelocateUnicodeString(pParamter + 0x38, (long)pParamter); //Patch CurrentDirectory
RelocateUnicodeString(pParamter + 0x50, (long)pParamter); //Patch DllPath
RelocateUnicodeString(pParamter + 0x60, (long)pParamter); //Patch ImagePathName
RelocateUnicodeString(pParamter + 0x70, (long)pParamter); //Patch CommandLine
RelocateUnicodeString(pParamter + 0xB0, (long)pParamter); //Patch WindowTitle
RelocateUnicodeString(pParamter + 0xC0, (long)pParamter); //Patch DesktopInfo
RelocateUnicodeString(pParamter + 0xD0, (long)pParamter); //Patch ShellInfo
RelocateUnicodeString(pParamter + 0xE0, (long)pParamter); //Patch RuntimeData

PatchMember(pParamter + 0x20, (long)StandardInput, 8);    //Patch StandardInput
PatchMember(pParamter + 0x28, (long)StandardOutput, 8); //Patch StandardOutput
PatchMember(pParamter + 0x30, (long)StandardError, 8);    //Patch StandardError

When this loader set fully configured RTL_USER_PROCESS_PARAMETERS structure, this result was shown as follows. Looks like commandline rewriting is not working. In addition, some PE files triggered a crash.

Nonetheless, this commandline for process appears to be correctly modified. This means that the commandline changes are not applied to in-memory execution context.

3. Applying commandline to in-memory execution context

How are commandline arguments passed to main function? In net1.exe, it is retrieved by __getmainargs function in msvcrt.dll.
msvcrt.dll saves the commandline to global variable by GetCommandLineA or GetCommandLineW at load time. __getmainargs function gets the value from this global variable and returns it. In other words, the commandline at the time msvcrt.dll was loaded is passed to main function, not the rewritten command line. Therefore, function that is triggered at the time msvcrt.dll was loaded must be run again.
Furthermore, it is not enough to rerun the function that is triggered at the time msvcrt.dll. GetCommandLineA function is used when this function sets the commandline to global variable, but the return value of GetCommandLineA is not the current commandline.
GetCommandLineA function is implemented in similar style with __getmainargs function. KernelBase.dll saves the commandline from PEB to global variable at load time. GetCommandLineA simply returns the value of this global variable.

As conclusion, the following operations are required to apply the rewritten commandline to main function.
・Re-run dllmain function in kernelbase.dll to change the return values of CommandLineA and CommandLineW
・Re-run dllmain function in msvcrt.dll to change the commandline passed to main function

Unfortunately, these functions are fastcall and cannot use in C# delegate. Therefore, I created DLL that executes these two functions. This should correctly apply new commandline to in-memory execution context.
#include "pch.h"
#include "windows.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved){
    HANDLE hLib = LoadLibraryA("kernelbase.dll");
    HANDLE hLib2 = LoadLibraryA("msvcrt.dll");
    typedef void __fastcall dllmain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved);
    dllmain* func = NULL;

    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        IMAGE_DOS_HEADER* idh;
        idh = (IMAGE_DOS_HEADER*)hLib;

        IMAGE_NT_HEADERS64* inh;
        inh = (IMAGE_NT_HEADERS64*)((LONG64)hLib + idh->e_lfanew);

        func = (dllmain*)((LONG64)hLib + inh->OptionalHeader.AddressOfEntryPoint);
        func((HINSTANCE)hLib, 1, 0);

        
        idh = (IMAGE_DOS_HEADER*)hLib2;
        inh = (IMAGE_NT_HEADERS64*)((LONG64)hLib2 + idh->e_lfanew);
        func = (dllmain*)((LONG64)hLib2 + inh->OptionalHeader.AddressOfEntryPoint);
        func((HINSTANCE)hLib2, 1, 0);
        

    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
The loader operation was as shown follows. Depending on framework and programming language of the executable to be loaded, additional operations may be required.

It works well

Comments

Popular posts from this blog

.NET in JavaScript, Fake PDF Converter

1. Overview I found a site that distributes curious PDF converter. The executable file distributed at this site appears to be malicious and have several interesting features. ・ .NET Anti-Analysis ・ Execution of external JavaScript payload with WebView2 ・ .NET object manipulation from JavaScript code This post will mention these techniques. The execution flow of this executable is shown below. 2. .NET Anti-Analysis PdfConverters.exe analyzed in this article was created with .NET Core. .NET Core allows developer to embed runtime and libraries into a single executable file. This executable also contains a number of files, which are extracted at execution time into a folder under %TEMP%\PdfConverters. A good way to know the role of these files is to look at [AppName].deps.json. app.deps.json reveals that main functionality of this executable exists in app.dll. [app.deps.json] ... "app/1.0.0": { "dependencies": { "Microsoft.

ShimCache (AppCompatCache) Internals

1. Overview ShimCache (AppCompatCache) is artifact that exists in Windows SYSTEM registry. This artifact records program execution but not execution time. Nevertheless, it is valuable artifact on Windows Server hosts where prefetch is not recorded by default or Windows hosts where prefetch has been removed. This article describes the following topics. ・Information in ShimCache (Forensics) ・Reverse engineering on ShimCache mechanism (Redteaming) 2. Information in ShimCache Shimcache is recorded under following subkey. HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Session Manager\AppCompatCache Shimcache data is binary format and composed of 52 byte header and multiple entries in Windows 10 (ver 2004). The format of the entry is as follows. Field Type Offset Description Signature DWORD 0x00 31 30 74 73 (10ts) CRC32 Hash DWORD 0x04 Entry Size DWORD 0x08 Path Size WORD 0x0C Path field's data length Path WString 0x0E PE file path Modified Time FILETIME NTFS $SI mo