libpeconv icon indicating copy to clipboard operation
libpeconv copied to clipboard

How to pass arguments to main payload in run-PE (or any program written by libPeConv)?

Open mhscXXl opened this issue 1 year ago • 2 comments

Issue: I have tried to pass arguments to a payload loaded by libpeconv but it wasn't possible directly. So i decided to go a little bit deeper and modified some stack related parts of the code, it was successful but this method is heavily relied on Non-standard methods and requires more or less complicated modifications on the main code.

Is there any possible ongoing features that are not yet published or any other methods that i could use for this matter?

mhscXXl avatar Jan 25 '24 13:01 mhscXXl

I've looked into this, you can't simply do EntryPoint(argc, argv), This happens due to the fact that normal PEs entry point is not actually int main(...) but rather int64_t crt_main() which is responsible of calling and _p___argc and __p___argv or __p___wargv and passing them to the real main function after setting up the CRT. seems like there is no straightforward way to approach this but the solution i went with was hooking the real main

here's a quick demo:

constexpr inline ptrdiff_t RealMainOffset = ...;

inline peconv::PatchBackup PayloadMainBackup;

const wchar_t* NewArgv[] = { L"....exe", L"--version" };
constexpr auto NewArgc = sizeof(NewArgv) / sizeof(NewArgv[0]);

inline auto RealEntryPoint = (int(*)(int , const wchar_t** , const char**))nullptr;

static int PayloadMainHook(int argc, const wchar_t** argv, const char** envp)
{
    PayloadMainBackup.applyBackup();

    argc = NewArgc;
    argv = NewArgv;

    return RealEntryPoint(argc, argv, envp);
}

int main()
{
    // Load the PE normally
   
    // ...

    auto EntryPointOffset = peconv::get_entry_point_rva((BYTE*)PayloadModuleBase);

    RealEntryPoint = (decltype(RealEntryPoint))(PayloadModuleBase + RealMainOffset);

    peconv::redirect_to_local(RealEntryPoint, &PayloadMainHook, &PayloadMainBackup);

    auto EntryPoint = (int(*)())(PayloadModuleBase + EntryPointOffset);

    return EntryPoint();
}

This for sure isn't convenient for a library, The way the library can approach this imo is doing some simple dynamic analysis on the crt main to find the real main and hook it, i think this is the best way instead of editing the args pushed to the host process.

kem0x avatar Aug 30 '24 07:08 kem0x

You need to hook GetCommandLineW & GetCommandLineA functions; CRT uses them for initialize the globals: __p___argc, __p___argv and __p___wargv: image Btw, some programs prefer not to use CRT, but to parse arguments via GetCommandLineA/W manually.

Sometimes you will also have to patch peb->ProcessParameters->CommandLine, because this is the field that Kernelbase uses to get information about command-line arguments and then issue them to GetCommandLineA/W, so that the programs can use peb->ProcessParameters to get information about command-line arguments image image

#include <Windows.h>
#include <peconv.h>
#include <string>
#include <cstdint>

std::string gCmdLineA;
std::wstring gCmdLineW;

static const uint8_t pe[] = {
//...
};

LPWSTR
WINAPI
hGetCommandLineW(
    VOID
  ) {
    return gCmdLineW.data();
}

LPSTR 
WINAPI
hGetCommandLineA(
    VOID
  ) {
    return gCmdLineA.data();
}

int main() {
  wchar_t selfFileNameW[MAX_PATH]{};
  char selfFileNameA[MAX_PATH]{};
  if (GetModuleFileNameW(NULL, selfFileNameW, MAX_PATH)) {
    gCmdLineW.push_back(L'"');
    gCmdLineW.append(selfFileNameW, wcslen(selfFileNameW)); // The first arg is always quoted application path
    gCmdLineW.push_back(L'"');
    gCmdLineW += L" NewArg1 NewArg2"; // New arguments
    gCmdLineW.push_back(L'\0'); // Null terminator
    std::wcout << gCmdLineW << std::endl;
  }
  if (GetModuleFileNameA(NULL, selfFileNameA, MAX_PATH)) {
    gCmdLineA.push_back('"');
    gCmdLineA.append(selfFileNameA, strlen(selfFileNameA)); // The first arg is always quoted application path
    gCmdLineA.push_back('"');
    gCmdLineA += " NewArg1 NewArg2";  // New arguments
    gCmdLineA.push_back('\0');  // Null terminator
    std::cout << gCmdLineA << std::endl;
  }
  
  peconv::hooking_func_resolver resolver;
  resolver.add_hook("GetCommandLineW", reinterpret_cast<FARPROC>(&hGetCommandLineW));
  resolver.add_hook("GetCommandLineA", reinterpret_cast<FARPROC>(&hGetCommandLineA));
  
  RtlAcquirePebLock();
  PPEB peb = NtCurrentPeb();
  RtlInitUnicodeString(&peb->ProcessParameters->CommandLine, gCmdLineW.data()); // Patching PEB
  RtlReleasePebLock();
  
  size_t vSize = 0;
  BYTE* loadedPe = peconv::load_pe_executable(pe, sizeof(pe), vSize, &resolver);
  // ...
}

And with RunPE everything is much simpler: you just need to pass the arguments to CreateProcessA/W directly.

NotCapengeR avatar Nov 30 '24 12:11 NotCapengeR