/*
Userspace Virtual Filesystem
Copyright (C) 2015 Sebastian Herbord. All rights reserved.
This file is part of usvfs.
usvfs is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
usvfs is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with usvfs. If not, see .
*/
#include
#include
#include
#include
#include
#include
namespace fs = boost::filesystem;
#include "injectlib.h"
#include
#include
#include
#include
// local version of asmjit with warning suppression
#include "asmjit_sane.h"
#include
#include
using namespace asmjit;
using namespace usvfs::shared;
#if BOOST_ARCH_X86_64
#pragma message("64bit build")
using namespace x86;
#elif BOOST_ARCH_X86_32
#pragma message("32bit build")
using namespace asmjit::x86;
#else
#error "unsupported architecture"
#endif
typedef HMODULE(WINAPI* TLoadLibraryType)(LPCWSTR);
typedef FARPROC(WINAPI* TGetProcAddressType)(HMODULE, LPCSTR);
typedef DWORD(WINAPI* TGetLastErrorType)();
typedef BOOL(WINAPI* TSetXStateFeaturesMaskType)(PCONTEXT, DWORD64);
static const size_t MAX_FUNCTIONAME = 20;
struct TDataRemote
{
TLoadLibraryType loadLibrary;
TGetProcAddressType getProcAddress;
TGetLastErrorType getLastError;
REGWORD returnAddress;
char initFunction[MAX_FUNCTIONAME + 1];
WCHAR dllName[MAX_PATH];
};
#if BOOST_ARCH_X86_64
void pushAll(X86Assembler& assembler)
{
assembler.pushf();
assembler.push(rax);
assembler.push(rcx);
assembler.push(rdx);
assembler.push(rbx);
assembler.push(rbp);
assembler.push(rsi);
assembler.push(rdi);
assembler.push(r8);
assembler.push(r9);
assembler.push(r10);
assembler.push(r11);
assembler.push(r12);
assembler.push(r13);
assembler.push(r14);
assembler.push(r15);
}
void popAll(X86Assembler& assembler)
{
assembler.pop(r15);
assembler.pop(r14);
assembler.pop(r13);
assembler.pop(r12);
assembler.pop(r11);
assembler.pop(r10);
assembler.pop(r9);
assembler.pop(r8);
assembler.pop(rdi);
assembler.pop(rsi);
assembler.pop(rbp);
assembler.pop(rbx);
assembler.pop(rdx);
assembler.pop(rcx);
assembler.pop(rax);
assembler.popf();
}
#endif // BOOST_ARCH_X86_64
void addStub(size_t userDataSize, X86Assembler& assembler, bool skipInit,
TDataRemote* localData, TDataRemote* remoteData, LPCSTR initFunction)
{
Label Label_DLLLoaded = assembler.newLabel();
#if BOOST_ARCH_X86_64
pushAll(assembler);
// call load library for the actual injection
assembler.mov(rcx, imm(reinterpret_cast(&remoteData->dllName)));
assembler.mov(rax, imm((intptr_t)(void*)localData->loadLibrary));
assembler.sub(rsp, 32);
assembler.call(rax);
assembler.add(rsp, 32);
// cancel out of here if we failed to load the dll
// TODO: would be great to report this error. But how?
assembler.test(rax, rax);
assembler.jnz(Label_DLLLoaded);
/* this commented out code may seem pointless but it is a simple way to get at the
error code when debugging. assembler.mov(rax,
imm((intptr_t)(void*)localData->getLastError)); assembler.sub(rsp, 32);
assembler.call(rax);
assembler.add(rsp, 32);
assembler.int3();*/
popAll(assembler);
assembler.ret();
assembler.bind(Label_DLLLoaded);
// determine address of the init function
if (initFunction != nullptr) {
Label Label_SkipInit = assembler.newLabel();
assembler.mov(rcx, rax); // handle of the dll
assembler.mov(rdx, imm(reinterpret_cast(
remoteData->initFunction))); // name of init function
assembler.mov(rax, imm((intptr_t)(void*)localData->getProcAddress));
assembler.sub(rsp, 32);
assembler.call(rax);
assembler.add(rsp, 32);
if (skipInit) {
assembler.test(rax, rax);
assembler.jz(Label_SkipInit);
}
// call the init function with user data
assembler.mov(rcx,
imm(reinterpret_cast(remoteData) + sizeof(TDataRemote)));
assembler.mov(rdx, imm(static_cast(userDataSize)));
assembler.sub(rsp, 32);
assembler.call(rax);
assembler.add(rsp, 32);
assembler.bind(Label_SkipInit);
}
// restore registers
popAll(assembler);
#else
// save registers
assembler.push(eax);
assembler.pushf();
// call load library for the actual injection
assembler.push(imm(void_ptr_cast(remoteData->dllName)));
// assembler.call(ptr_abs(static_cast(remoteData->loadLibrary)));
assembler.mov(eax, imm(void_ptr_cast(localData->loadLibrary)));
assembler.call(eax);
assembler.test(eax, eax);
assembler.jnz(Label_DLLLoaded);
/* this commented out code may seem pointless but it is a simple way to get at the
error code when debugging. assembler.mov(eax,
imm((intptr_t)(void*)localData->getLastError)); assembler.call(eax);
assembler.int3();*/
assembler.popf();
assembler.pop(eax);
assembler.ret();
assembler.bind(Label_DLLLoaded);
// determine address of the init function
if (initFunction != nullptr) {
Label Label_SkipInit = assembler.newLabel();
assembler.push(imm(
void_ptr_cast(remoteData->initFunction))); // name of init function
assembler.push(eax); // handle of the dll
assembler.mov(eax, imm(void_ptr_cast(localData->getProcAddress)));
assembler.call(eax);
if (skipInit) {
assembler.cmp(eax, 0);
assembler.jz(Label_SkipInit);
} else {
assembler.cmp(eax, 0);
assembler.jnz(Label_SkipInit);
// heading for a crash! give an attached debugger a chance to analyse the error
assembler.mov(eax, imm(void_ptr_cast(localData->getLastError)));
assembler.call(eax);
assembler.int3();
assembler.bind(Label_SkipInit);
}
// call the init function with user data
assembler.push(userDataSize);
assembler.push(imm(void_ptr_cast(remoteData) + sizeof(TDataRemote)));
assembler.call(eax);
// init function is declared __cdecl so we have to remove parameters from the stack
assembler.pop(eax);
assembler.pop(eax);
if (skipInit) {
assembler.bind(Label_SkipInit);
}
}
// restore registers
assembler.popf();
assembler.pop(eax);
#endif
}
REGWORD WriteInjectionStub(HANDLE processHandle, LPCWSTR dllName, LPCSTR initFunction,
LPCVOID userData, size_t userDataSize, bool skipInit,
REGWORD returnAddress)
{
HMODULE k32mod = ::LoadLibrary(__TEXT("kernel32.dll"));
TDataRemote data = {0};
if (k32mod != nullptr) {
data.loadLibrary =
reinterpret_cast(GetProcAddress(k32mod, "LoadLibraryW"));
data.getProcAddress =
reinterpret_cast(GetProcAddress(k32mod, "GetProcAddress"));
data.getLastError =
reinterpret_cast(GetProcAddress(k32mod, "GetLastError"));
if ((data.loadLibrary == nullptr) || (data.getProcAddress == nullptr) ||
(data.getLastError == nullptr)) {
throw windows_error("failed to determine address for required functions");
}
} else {
throw windows_error("kernel32.dll not loaded?");
}
data.returnAddress = returnAddress;
if (initFunction != nullptr) {
strncpy_s(data.initFunction, MAX_FUNCTIONAME, initFunction, MAX_FUNCTIONAME);
data.initFunction[MAX_FUNCTIONAME] = '\0';
} else {
data.initFunction[0] = '\0';
}
wcsncpy_s(data.dllName, MAX_PATH, dllName, MAX_PATH - 1);
data.dllName[MAX_PATH - 1] = L'\0';
size_t totalSize = sizeof(TDataRemote) + userDataSize;
// allocate memory in the target process and write the data-block there
LPVOID remoteMem = VirtualAllocEx(processHandle, nullptr, totalSize,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (remoteMem == nullptr) {
throw windows_error("failed to allocate memory in target process");
}
SIZE_T written;
if (!WriteProcessMemory(processHandle, remoteMem, &data, sizeof(TDataRemote),
&written)) {
throw windows_error("failed to write control data to target process");
}
if (written != sizeof(TDataRemote)) {
throw windows_error("failed to write whole control data to target process");
}
// write user data to remote memory if necessary
if (userData != nullptr) {
if (!WriteProcessMemory(processHandle, AddrAdd(remoteMem, sizeof(TDataRemote)),
userData, userDataSize, &written)) {
throw windows_error("failed to write user data to target process");
}
if (written != userDataSize) {
throw windows_error("failed to write whole user data to target process");
}
}
TDataRemote* remoteData = reinterpret_cast(remoteMem);
// now for the interesting part: write a stub into the target process that is run
// before any code of the original binary.
JitRuntime runtime;
#if BOOST_ARCH_X86_64
X86Assembler assembler(&runtime);
if (returnAddress != 0) {
// put return address on the stack
// (this damages rax which hopefully doesn't matter)
assembler.mov(rax, imm((intptr_t)(void*)data.returnAddress));
assembler.push(rax);
} // otherwise no return address was specified here. It better be on the stack
// already
#else
X86Assembler assembler(&runtime);
if (returnAddress != 0) {
assembler.push(imm((intptr_t)(void*)data.returnAddress));
}
#endif
addStub(userDataSize, assembler, skipInit, &data, remoteData, initFunction);
assembler.ret(0);
size_t stubSize = assembler.getCodeSize();
// reserve memory for the stub
PBYTE stubRemote = reinterpret_cast(
VirtualAllocEx(processHandle, nullptr, stubSize, MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE));
if (stubRemote == nullptr) {
throw windows_error("failed to allocate memory for stub");
}
// almost there. copy stub to target process
if (!WriteProcessMemory(processHandle, stubRemote, assembler.getBuffer(), stubSize,
&written) ||
(written != stubSize)) {
throw windows_error("failed to write stub to target process");
}
return reinterpret_cast(stubRemote);
}
void InjectDLLEIP(HANDLE processHandle, HANDLE threadHandle, LPCWSTR dllName,
LPCSTR initFunction, LPCVOID userData, size_t userDataSize,
bool skipInit)
{
threadHandle =
OpenThread((THREAD_GET_CONTEXT | THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME),
FALSE, GetThreadId(threadHandle));
if (threadHandle == nullptr) {
throw windows_error("failed to open thread");
}
CONTEXT threadContext;
threadContext.ContextFlags = CONTEXT_CONTROL;
// documentation says starting with Win7 SP1 you HAVE to call SetXStateFeaturesMask
HMODULE k32mod = ::LoadLibrary(__TEXT("kernel32.dll"));
if (k32mod == nullptr) {
throw windows_error("failed to load kernel32.dll");
}
TSetXStateFeaturesMaskType sxsfm = reinterpret_cast(
GetProcAddress(k32mod, "SetXStateFeaturesMask"));
if (sxsfm != nullptr) {
sxsfm(&threadContext, 0);
}
::FreeLibrary(k32mod);
if (GetThreadContext(threadHandle, &threadContext) == 0) {
throw windows_error("failed to access thread context.");
}
#if BOOST_ARCH_X86_64
REGWORD returnAddress = threadContext.Rip;
#else
REGWORD returnAddress = threadContext.Eip;
#endif
REGWORD stubAddress =
WriteInjectionStub(processHandle, dllName, initFunction, userData, userDataSize,
skipInit, returnAddress);
// make the stub the new next thing for the thread to execute
#if BOOST_ARCH_X86_64
threadContext.Rip = stubAddress;
#else
threadContext.Eip = stubAddress;
#endif
if (SetThreadContext(threadHandle, &threadContext) == 0) {
throw windows_error("failed to overwrite thread context");
}
}
void InjectDLLRemoteThread(HANDLE processHandle, LPCWSTR dllName, LPCSTR initFunction,
LPCVOID userData, size_t userDataSize, bool skipInit)
{
REGWORD stubAddress = WriteInjectionStub(processHandle, dllName, initFunction,
userData, userDataSize, skipInit, 0);
DWORD threadId = 0UL;
HANDLE threadHandle = CreateRemoteThread(
processHandle, nullptr, 0, reinterpret_cast(stubAddress),
nullptr, 0, &threadId);
if (threadHandle == nullptr) {
throw windows_error("failed to start remote thread");
}
ResumeThread(threadHandle);
spdlog::get("usvfs")->info("waiting for {0:x} to complete",
GetThreadId(threadHandle));
::WaitForSingleObject(threadHandle, 100);
::CloseHandle(threadHandle);
}
void InjectLib::InjectDLL(HANDLE processHandle, HANDLE threadHandle, LPCWSTR dllName,
LPCSTR initFunction, LPCVOID userData, size_t userDataSize,
bool skipInit)
{
namespace bfs = boost::filesystem;
if (!exists(bfs::path(dllName))) {
USVFS_THROW_EXCEPTION(file_not_found_error()
<< ex_msg(string_cast(dllName)));
}
if (threadHandle == INVALID_HANDLE_VALUE) {
#pragma message( \
"doesn't seem to work as usvfs causes an exception in the first static initialization or pretty much on any function call. Because process is in different session? CRT related?")
/*
InjectDLLRemoteThread(processHandle, dllName,
initFunction, userData, userDataSize, skipInit);
*/
DWORD pid = GetProcessId(processHandle);
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
THREADENTRY32 threadInfo;
threadInfo.dwSize = sizeof(THREADENTRY32);
BOOL moreThreads = Thread32First(snapshot, &threadInfo);
std::vector threadHandles;
HANDLE injectThread = INVALID_HANDLE_VALUE;
FILETIME injectThreadTime;
spdlog::get("usvfs")->info("inject dll to process {0}", pid);
while (moreThreads) {
if (threadInfo.th32OwnerProcessID == pid) {
HANDLE thread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, threadInfo.th32ThreadID);
if (thread != nullptr) {
DWORD suspCount = SuspendThread(thread);
if (suspCount == 0) {
FILETIME creationTime, exitTime, kernelTime, userTime;
::GetThreadTimes(thread, &creationTime, &exitTime, &kernelTime, &userTime);
if ((injectThread == INVALID_HANDLE_VALUE) ||
(CompareFileTime(&creationTime, &injectThreadTime) < 0)) {
spdlog::get("usvfs")->info("candidate for oldest thread: {0}",
threadInfo.th32ThreadID);
injectThread = thread;
injectThreadTime = creationTime;
}
}
threadHandles.push_back(thread);
}
}
moreThreads = Thread32Next(snapshot, &threadInfo);
}
if (injectThread != INVALID_HANDLE_VALUE) {
spdlog::get("usvfs")->debug("going to inject dll");
InjectDLLEIP(processHandle, injectThread, dllName, initFunction, userData,
userDataSize, skipInit);
} else {
spdlog::get("usvfs")->critical("found no thread to use for injecting");
}
for (HANDLE hdl : threadHandles) {
spdlog::get("usvfs")->info("resuming thread {0}", ::GetThreadId(hdl));
ResumeThread(hdl);
CloseHandle(hdl);
}
CloseHandle(snapshot);
} else {
InjectDLLEIP(processHandle, threadHandle, dllName, initFunction, userData,
userDataSize, skipInit);
}
}