MemoryModule icon indicating copy to clipboard operation
MemoryModule copied to clipboard

Starting Calc.exe fails (partial fix with code)

Open Elmue opened this issue 3 years ago • 2 comments

With the current fancycode you can load DLLs but starting Calc.exe or Notepad.exe from memory fails always. Calling the entry point hangs forever or crashes.

At least I know that adapting the PEB and the Loader Table is missing in fancycode. Here is the missing code:

// The following 3 structs have been verified for 32 bit and 64 bit compilation
// using the macro "offsetof()" with the relative offset information from Geoff Campbell.
#pragma pack(push, 8)

// Original name: LDR_DATA_TABLE_ENTRY
// https://www.geoffchappell.com/studies/windows/win32/ntdll/structs/ldr_data_table_entry.htm
struct OS_LDR_TABLE_ENTRY
{
    LIST_ENTRY InLoadOrderModuleList;           // LDR_DATA_TABLE_ENTRY by load order
    LIST_ENTRY InMemoryOrderModuleList;         // LDR_DATA_TABLE_ENTRY by memory location
    LIST_ENTRY InInitializationOrderModuleList; // LDR_DATA_TABLE_ENTRY by initialization order
    BYTE*          DllBase;                     // base address to which the module was loaded
    BYTE*          EntryPoint;                  // DllMain()
    ULONG          SizeOfImage;                 // size in memory
    UNICODE_STRING FullDllName;                 // full path
    UNICODE_STRING BaseDllName;                 // only file name
    ULONG          Flags;                       // LDRP_IMAGE_DLL, etc...
    USHORT         LoadCount;                   // reference count (not used anymore since Windows 8)
    USHORT         TlsIndex;                    // Thread Local Storage slot for this module
    union                                       // 3C
    {
        LIST_ENTRY HashLinks;                   // since Windows 8   sizeof(LIST_ENTRY) = 8 bytes
        PVOID SectionPointer;                   // NtClose(SectionPointer) when the DLL is unloaded
    };
    ULONG TimeDateStamp;                        // 44: Linker timestamp
    
    // The rest is not required here
};

// Original name: PEB_LDR_DATA 
// https://www.geoffchappell.com/studies/windows/win32/ntdll/structs/peb_ldr_data.htm
struct OS_LDR_DATA
{
    ULONG      Length;
    BOOLEAN    Initialized;    // Size = 1 !!
    PVOID      SsHandle;
    LIST_ENTRY InLoadOrderModuleList;
    LIST_ENTRY InMemoryOrderModuleList;
    LIST_ENTRY InInitializationOrderModuleList;
    PVOID      EntryInProgress;
    BOOLEAN    ShutdownInProgress;
    HANDLE     ShutdownThreadId;
};

// Original name: PEB
// https://www.geoffchappell.com/studies/windows/win32/ntdll/structs/peb/index.htm
struct OS_PEB 
{
    BOOLEAN       InheritedAddressSpace;
    BOOLEAN       ReadImageFileExecOptions;
    BOOLEAN       BeingDebugged;
    BYTE          BitField;
    HANDLE        Mutant;
    PVOID         ImageBaseAddress;
    OS_LDR_DATA*  Ldr;
    RTL_USER_PROCESS_PARAMETERS* ProcessParameters;
    
    // The rest is not required here
};

#pragma pack(pop)

#define LDRP_IMAGE_DLL  0x00000004 // This module is an image DLL (not a Data DLL or EXE)

typedef OS_PEB* (NTAPI* tRtlGetCurrentPeb)();

// define global variable
static tRtlGetCurrentPeb gf_RtlGetCurrentPeb = NULL;


// returns API error
DWORD MEMORYMODULE::CallEntryPoint()
{
    // Get the .NET Metadata directory (former name: COM descriptor)
    IMAGE_DATA_DIRECTORY* pk_ClrDir = &headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR];
    if (pk_ClrDict->Size > 0)
    {
        IMAGE_COR20_HEADER* pk_ClrHeader = (IMAGE_COR20_HEADER*)(codeBase + pk_ClrDir->VirtualAddress);
        
        // Since Windows XP the Windows loader does NOT call the entry point of a managed EXE anymore.
        // Microsoft did this by purpose. If a virus exists in the entry code of a .NET assembly it will never be executed.
        // Theoretically a .NET executable is started with the parameterless function _CorExeMain() in Mscoree.dll.        
        // But MsCoree.dll creates a filemapping from the real EXE file on disk.
        // If the file exists only as image in memory _CorExeMain() immediately terminates the process.

        // On the other hand loading a .NET DLL is ultra simple in managed code.
        // Simply use Assembly.Load(Byte[])

        // If you want to load managed assemblies into an unmanged process read these articles:
        // https://www.codeproject.com/Articles/607352/Injecting-NET-Assemblies-Into-Unmanaged-Processes
        // https://www.experts-exchange.com/questions/28909682/C-Loading-Managed-Assembly-From-Memory-in-Unmanaged-Process.html
        // https://modexp.wordpress.com/2019/05/10/dotnet-loader-shellcode/
        
        TRACE(L"ERROR: Loading managed executables is not implemented.");
        return ERROR_INVALID_FUNCTION;
    }
    
    if (isDLL)
    {
        // The entry point for a DLL is optional
        if (headers->OptionalHeader.AddressOfEntryPoint == 0)
            return 0;
    
        DllEntryProc f_DllEntry = (DllEntryProc)(codeBase + headers->OptionalHeader.AddressOfEntryPoint);

        TRACE(L"Send DLL_PROCESS_ATTACH to DLL entry point %p", f_DllEntry);
        
        if (!f_DllEntry((HINSTANCE)codeBase, DLL_PROCESS_ATTACH, 0)) 
            return ERROR_DLL_INIT_FAILED;

        initialized = TRUE;
        return 0;
    }
    else // EXE
    {
        // An EXE without entry point does not make sense. The Windows Loader crashes if EntryPoint == 0.
        if (headers->OptionalHeader.AddressOfEntryPoint == 0)
            return ERROR_BAD_FORMAT;
    
        if (!gf_RtlGetCurrentPeb)
        {
            HMODULE h_NtDll = GetModuleHandleA("ntdll.dll");
            if (!h_NtDll)
                return GetLastError();

            if (!(gf_RtlGetCurrentPeb = (tRtlGetCurrentPeb)GetProcAddress(h_NtDll, "RtlGetCurrentPeb")))
                return GetLastError();
        }
        
        ExeEntryProc f_ExeEntry = (ExeEntryProc)(codeBase + headers->OptionalHeader.AddressOfEntryPoint);

        OS_PEB* pk_PEB = gf_RtlGetCurrentPeb();
        pk_PEB->ImageBaseAddress = codeBase;

        LIST_ENTRY *pk_Head    = &pk_PEB->Ldr->InLoadOrderModuleList;
        LIST_ENTRY *pk_Current = pk_Head;
        // Iterate the modules of the current process.
        // The ModuleList is cyclic, which means that it has no end. 
        // The last element points back to the first.
        while ((pk_Current = pk_Current->Flink) != pk_Head)
        {
            OS_LDR_TABLE_ENTRY* pk_Module = CONTAINING_RECORD(pk_Current, OS_LDR_TABLE_ENTRY, InLoadOrderModuleList);
            // Search for the EXE in the module list (normally the first entry)
            if ((pk_Module->Flags & LDRP_IMAGE_DLL) == 0)
            {
                pk_Module->DllBase       = codeBase;
                pk_Module->SizeOfImage   = AlignValueUp(headers->OptionalHeader.SizeOfImage, pageSize);
                pk_Module->TimeDateStamp = headers->FileHeader.TimeDateStamp;
                pk_Module->EntryPoint    = (BYTE*)f_ExeEntry;
                break;
            }
        }

        TRACE(L"Call EXE main() at entry point %p", f_ExeEntry);

        // This function never returns.
        // When the user closes the main window, Windows kills our process with ExitProcess().
        // Therefore it is nonsense to run this code in an extra thread as I have seen in Github forks of fancycode.
        f_ExeEntry(); 
        
        // This "return" is just for the compiler not to complain. It will never execute.
        return 0; 
    }
}

Without the above code you can neither start Calc.exe nor Notepad.exe. It fails on ALL operating systems.

With the above code you can start Calc.exe and Notepad.exe from memory. But not on all operating systems.


1.) If an EXE file does not have a relocation table it is very probable that you cannot start it. Without relocation table the EXE MUST run at the predefined base address (mostly 40000). If this address area is already occupied by your starter process there is no way to start this EXE. In this case recompile the EXE that you want to start. Under "Linker Options" --> "Adavanced" --> "Fixed Base Address" enter "Generate a relocation section" This will add the linker commandline option /FIXED:NO After that the EXE will run at any base address.

2.) The Calc.exe on Windows 10 is not the real calculator anymore. It is only a launcher of 26 kB size which starts Calculator.exe in an AppContainer and then exits.

3.) With the above code I got the following results:

Windows XP 32 bit: Starting a 32 bit Calc.exe and Notepad.exe work perfectly (ONLY with the above code fix). Windows 7 64 bit: Starting a 32 bit Calc.exe and Notepad.exe work perfectly (ONLY with the above code fix). Windows 7 64 bit: Starting a 64 bit Calc.exe crashes immediately and Notepad.exe crashes when you chose a font. Windows 10 64 bit: Starting a 32 + 64 bit Calc.exe works (Calculator.exe is launched in AppContainer) Windows 10 64 bit: Starting a 32 + 64 bit Notepad.exe does not work

4.) But I can start my own MFC compiled GUI application either 32 bit or 64 bit perfectly on ALL operating systems. And even without the above PEB fix!

SUMMARY: My own EXE (with GUI) can be started, but Microsoft's EXE's not always. That's weird. I gave up getting this to work. If anybody can explain me that, please post a comment. BTW: I have no antivirus installed and Windows Defender is disabled.

Don't forget to implement the Activation Context! https://github.com/fancycode/MemoryModule/issues/100

and fix the execution bug: https://github.com/fancycode/MemoryModule/issues/101

P.D. There is many code out there using the process hollowing technique. But it has exactly the same problems. Notepad.exe can be started on XP but not on Windows 10.

Elmue

Elmue avatar Jul 15 '20 21:07 Elmue