Skip to content

Commit

Permalink
Make RewriteRemoteEntryPointW report IErrorInfo, VirtualProtectEx bef…
Browse files Browse the repository at this point in the history
…ore WriteProcessMemory
  • Loading branch information
Soreepeong committed Feb 12, 2024
1 parent 0cc28fb commit 7e78b62
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 117 deletions.
2 changes: 1 addition & 1 deletion Dalamud.Boot/Dalamud.Boot.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,4 @@
<Delete Files="$(OutDir)$(TargetName).lib" />
<Delete Files="$(OutDir)$(TargetName).exp" />
</Target>
</Project>
</Project>
8 changes: 4 additions & 4 deletions Dalamud.Boot/dllmain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
HMODULE g_hModule;
HINSTANCE g_hGameInstance = GetModuleHandleW(nullptr);

DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
g_startInfo.from_envvars();

std::string jsonParseError;
Expand Down Expand Up @@ -114,7 +114,7 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
logging::I("Calling InitializeClrAndGetEntryPoint");

void* entrypoint_vfn;
int result = InitializeClrAndGetEntryPoint(
const auto result = InitializeClrAndGetEntryPoint(
g_hModule,
g_startInfo.BootEnableEtw,
runtimeconfig_path,
Expand All @@ -124,7 +124,7 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
L"Dalamud.EntryPoint+InitDelegate, Dalamud",
&entrypoint_vfn);

if (result != 0)
if (FAILED(result))
return result;

using custom_component_entry_point_fn = void (CORECLR_DELEGATE_CALLTYPE*)(LPVOID, HANDLE);
Expand Down Expand Up @@ -156,7 +156,7 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
entrypoint_fn(lpParam, hMainThreadContinue);
logging::I("Done!");

return 0;
return S_OK;
}

extern "C" DWORD WINAPI Initialize(LPVOID lpParam) {
Expand Down
3 changes: 3 additions & 0 deletions Dalamud.Boot/pch.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
// MSVC Compiler Intrinsic
#include <intrin.h>

// COM
#include <comdef.h>

// C++ Standard Libraries
#include <cassert>
#include <chrono>
Expand Down
133 changes: 110 additions & 23 deletions Dalamud.Boot/rewrite_entrypoint.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#include "pch.h"

#include "logging.h"
#include "utils.h"

DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue);
HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue);

struct RewrittenEntryPointParameters {
char* pEntrypoint;
Expand Down Expand Up @@ -102,6 +103,7 @@ void read_process_memory_or_throw(HANDLE hProcess, void* pAddress, T& data) {

void write_process_memory_or_throw(HANDLE hProcess, void* pAddress, const void* data, size_t len) {
SIZE_T written = 0;
const utils::memory_tenderizer tenderizer(hProcess, pAddress, len, PAGE_EXECUTE_READWRITE);
if (!WriteProcessMemory(hProcess, pAddress, data, len, &written))
throw std::runtime_error("WriteProcessMemory failure");
if (written != len)
Expand Down Expand Up @@ -227,15 +229,18 @@ void* get_mapped_image_base_address(HANDLE hProcess, const std::filesystem::path
/// @brief Rewrite target process' entry point so that this DLL can be loaded and executed first.
/// @param hProcess Process handle.
/// @param pcwzPath Path to target process.
/// @param pcszLoadInfo JSON string to be passed to Initialize.
/// @return 0 if successful; nonzero if unsuccessful
/// @param pcwzLoadInfo JSON string to be passed to Initialize.
/// @return null if successful; memory containing wide string allocated via GlobalAlloc if unsuccessful
///
/// When the process has just been started up via CreateProcess (CREATE_SUSPENDED), GetModuleFileName and alikes result in an error.
/// Instead, we have to enumerate through all the files mapped into target process' virtual address space and find the base address
/// of memory region corresponding to the path given.
///
extern "C" DWORD WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath, const wchar_t* pcwzLoadInfo) {
extern "C" HRESULT WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath, const wchar_t* pcwzLoadInfo) {
std::wstring last_operation;
SetLastError(ERROR_SUCCESS);
try {
last_operation = L"get_mapped_image_base_address";
const auto base_address = static_cast<char*>(get_mapped_image_base_address(hProcess, pcwzPath));

IMAGE_DOS_HEADER dos_header{};
Expand All @@ -244,46 +249,86 @@ extern "C" DWORD WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t*
IMAGE_NT_HEADERS64 nt_header64{};
};

last_operation = L"read_process_memory_or_throw(base_address)";
read_process_memory_or_throw(hProcess, base_address, dos_header);

last_operation = L"read_process_memory_or_throw(base_address + dos_header.e_lfanew)";
read_process_memory_or_throw(hProcess, base_address + dos_header.e_lfanew, nt_header64);
const auto entrypoint = base_address + (nt_header32.OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC
? nt_header32.OptionalHeader.AddressOfEntryPoint
: nt_header64.OptionalHeader.AddressOfEntryPoint);

auto standalone_rewrittenentrypoint = thunks::create_standalone_rewrittenentrypoint(get_path_from_local_module(g_hModule));
last_operation = L"get_path_from_local_module(g_hModule)";
auto local_module_path = get_path_from_local_module(g_hModule);

last_operation = L"thunks::create_standalone_rewrittenentrypoint(local_module_path)";
auto standalone_rewrittenentrypoint = thunks::create_standalone_rewrittenentrypoint(local_module_path);

last_operation = L"thunks::create_entrypointreplacement()";
auto entrypoint_replacement = thunks::create_entrypointreplacement();

last_operation = L"unicode::convert<std::string>(pcwzLoadInfo)";
auto load_info = unicode::convert<std::string>(pcwzLoadInfo);
load_info.resize(load_info.size() + 1); //ensure null termination

std::vector<uint8_t> buffer(sizeof(RewrittenEntryPointParameters) + entrypoint_replacement.size() + load_info.size() + standalone_rewrittenentrypoint.size());
const auto bufferSize = sizeof(RewrittenEntryPointParameters) + entrypoint_replacement.size() + load_info.size() + standalone_rewrittenentrypoint.size();
last_operation = std::format(L"std::vector alloc({}b)", bufferSize);
std::vector<uint8_t> buffer(bufferSize);

// Allocate buffer in remote process, which will be used to fill addresses in the local buffer.
last_operation = std::format(L"VirtualAllocEx({}b)", bufferSize);
const auto remote_buffer = static_cast<char*>(VirtualAllocEx(hProcess, nullptr, buffer.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE));

auto& params = *reinterpret_cast<RewrittenEntryPointParameters*>(buffer.data());
params.entrypointLength = entrypoint_replacement.size();
params.pEntrypoint = entrypoint;

// Backup original entry point.
last_operation = std::format(L"read_process_memory_or_throw(entrypoint, {}b)", entrypoint_replacement.size());
read_process_memory_or_throw(hProcess, entrypoint, &buffer[sizeof params], entrypoint_replacement.size());

memcpy(&buffer[sizeof params + entrypoint_replacement.size()], load_info.data(), load_info.size());

last_operation = L"thunks::fill_placeholders(EntryPointReplacement)";
thunks::fill_placeholders(standalone_rewrittenentrypoint.data(), remote_buffer);
memcpy(&buffer[sizeof params + entrypoint_replacement.size() + load_info.size()], standalone_rewrittenentrypoint.data(), standalone_rewrittenentrypoint.size());

// Write the local buffer into the buffer in remote process.
last_operation = std::format(L"write_process_memory_or_throw(remote_buffer, {}b)", buffer.size());
write_process_memory_or_throw(hProcess, remote_buffer, buffer.data(), buffer.size());

last_operation = L"thunks::fill_placeholders(RewrittenEntryPoint_Standalone::pRewrittenEntryPointParameters)";
thunks::fill_placeholders(entrypoint_replacement.data(), remote_buffer + sizeof params + entrypoint_replacement.size() + load_info.size());

// Overwrite remote process' entry point with a thunk that will load our DLLs and call our trampoline function.
last_operation = std::format(L"write_process_memory_or_throw(entrypoint={:X}, {}b)", reinterpret_cast<uintptr_t>(entrypoint), buffer.size());
write_process_memory_or_throw(hProcess, entrypoint, entrypoint_replacement.data(), entrypoint_replacement.size());

return 0;
return S_OK;
} catch (const std::exception& e) {
OutputDebugStringA(std::format("RewriteRemoteEntryPoint failure: {} (GetLastError: {})\n", e.what(), GetLastError()).c_str());
return 1;
const auto err = GetLastError();
const auto hr = err == ERROR_SUCCESS ? E_FAIL : HRESULT_FROM_WIN32(err);
auto formatted = std::format(
L"{}: {} ({})",
last_operation,
unicode::convert<std::wstring>(e.what()),
utils::format_win32_error(err));
OutputDebugStringW((formatted + L"\r\n").c_str());

ICreateErrorInfoPtr cei;
if (FAILED(CreateErrorInfo(&cei)))
return hr;
if (FAILED(cei->SetSource(const_cast<LPOLESTR>(L"Dalamud.Boot"))))
return hr;
if (FAILED(cei->SetDescription(const_cast<LPOLESTR>(formatted.c_str()))))
return hr;

IErrorInfoPtr ei;
if (FAILED(cei.QueryInterface(IID_PPV_ARGS(&ei))))
return hr;

(void)SetErrorInfo(0, ei);
return hr;
}
}

Expand All @@ -300,30 +345,72 @@ static void AbortRewrittenEntryPoint(DWORD err, const std::wstring& clue) {
nullptr);

if (MessageBoxW(nullptr, std::format(
L"Failed to load Dalamud. Load game without Dalamud(yes) or abort(no)?\n\nError: 0x{:08X} {}\n\n{}",
err, pwszMsg ? pwszMsg : L"", clue).c_str(),
L"Failed to load Dalamud. Load game without Dalamud(yes) or abort(no)?\n\n{}\n\n{}",
utils::format_win32_error(err),
clue).c_str(),
L"Dalamud.Boot", MB_OK | MB_YESNO) == IDNO)
ExitProcess(-1);
}

/// @brief Entry point function "called" instead of game's original main entry point.
/// @param params Parameters set up from RewriteRemoteEntryPoint.
extern "C" void WINAPI RewrittenEntryPoint_AdjustedStack(RewrittenEntryPointParameters & params) {
const auto pOriginalEntryPointBytes = reinterpret_cast<char*>(&params) + sizeof(params);
const auto pLoadInfo = pOriginalEntryPointBytes + params.entrypointLength;
HANDLE hMainThreadContinue = nullptr;
auto hr = S_OK;
std::wstring last_operation;
std::wstring exc_msg;
SetLastError(ERROR_SUCCESS);

// Restore original entry point.
// Use WriteProcessMemory instead of memcpy to avoid having to fiddle with VirtualProtect.
if (SIZE_T written; !WriteProcessMemory(GetCurrentProcess(), params.pEntrypoint, pOriginalEntryPointBytes, params.entrypointLength, &written))
return AbortRewrittenEntryPoint(GetLastError(), L"WriteProcessMemory(entrypoint restoration)");
try {
const auto pOriginalEntryPointBytes = reinterpret_cast<char*>(&params) + sizeof(params);
const auto pLoadInfo = pOriginalEntryPointBytes + params.entrypointLength;

// Restore original entry point.
// Use WriteProcessMemory instead of memcpy to avoid having to fiddle with VirtualProtect.
last_operation = L"restore original entry point";
write_process_memory_or_throw(GetCurrentProcess(), params.pEntrypoint, pOriginalEntryPointBytes, params.entrypointLength);

const auto hMainThreadContinue = CreateEventW(nullptr, true, false, nullptr);
if (!hMainThreadContinue)
return AbortRewrittenEntryPoint(GetLastError(), L"CreateEventW");
hMainThreadContinue = CreateEventW(nullptr, true, false, nullptr);
last_operation = L"hMainThreadContinue = CreateEventW";
if (!hMainThreadContinue)
throw std::runtime_error("CreateEventW");

last_operation = L"InitializeImpl";
hr = InitializeImpl(pLoadInfo, hMainThreadContinue);
} catch (const std::exception& e) {
if (hr == S_OK) {
const auto err = GetLastError();
hr = err == ERROR_SUCCESS ? E_FAIL : HRESULT_FROM_WIN32(err);
}

ICreateErrorInfoPtr cei;
IErrorInfoPtr ei;
if (SUCCEEDED(CreateErrorInfo(&cei))
&& SUCCEEDED(cei->SetDescription(const_cast<wchar_t*>(unicode::convert<std::wstring>(e.what()).c_str())))
&& SUCCEEDED(cei.QueryInterface(IID_PPV_ARGS(&ei)))) {
(void)SetErrorInfo(0, ei);
}
}

if (FAILED(hr)) {
const _com_error err(hr);
auto desc = err.Description();
if (desc.length() == 0)
desc = err.ErrorMessage();
if (MessageBoxW(nullptr, std::format(
L"Failed to load Dalamud. Load game without Dalamud(yes) or abort(no)?\n\n{}\n{}",
last_operation,
desc.GetBSTR()).c_str(),
L"Dalamud.Boot", MB_OK | MB_YESNO) == IDNO)
ExitProcess(-1);
if (hMainThreadContinue) {
CloseHandle(hMainThreadContinue);
hMainThreadContinue = nullptr;
}
}

if (const auto result = InitializeImpl(pLoadInfo, hMainThreadContinue))
return AbortRewrittenEntryPoint(result, L"InitializeImpl");
if (hMainThreadContinue)
WaitForSingleObject(hMainThreadContinue, INFINITE);

WaitForSingleObject(hMainThreadContinue, INFINITE);
VirtualFree(&params, 0, MEM_RELEASE);
}
44 changes: 36 additions & 8 deletions Dalamud.Boot/utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -408,22 +408,28 @@ utils::signature_finder::result utils::signature_finder::find_one() const {
return find(1, 1, false).front();
}

utils::memory_tenderizer::memory_tenderizer(const void* pAddress, size_t length, DWORD dwNewProtect) : m_data(reinterpret_cast<char*>(const_cast<void*>(pAddress)), length) {
utils::memory_tenderizer::memory_tenderizer(const void* pAddress, size_t length, DWORD dwNewProtect)
: memory_tenderizer(GetCurrentProcess(), pAddress, length, dwNewProtect) {
}

utils::memory_tenderizer::memory_tenderizer(HANDLE hProcess, const void* pAddress, size_t length, DWORD dwNewProtect)
: m_process(hProcess)
, m_data(static_cast<char*>(const_cast<void*>(pAddress)), length) {
try {
for (auto pCoveredAddress = &m_data[0];
pCoveredAddress < &m_data[0] + m_data.size();
pCoveredAddress = reinterpret_cast<char*>(m_regions.back().BaseAddress) + m_regions.back().RegionSize) {
for (auto pCoveredAddress = m_data.data();
pCoveredAddress < m_data.data() + m_data.size();
pCoveredAddress = static_cast<char*>(m_regions.back().BaseAddress) + m_regions.back().RegionSize) {

MEMORY_BASIC_INFORMATION region{};
if (!VirtualQuery(pCoveredAddress, &region, sizeof region)) {
if (!VirtualQueryEx(hProcess, pCoveredAddress, &region, sizeof region)) {
throw std::runtime_error(std::format(
"VirtualQuery(addr=0x{:X}, ..., cb={}) failed with Win32 code 0x{:X}",
reinterpret_cast<size_t>(pCoveredAddress),
sizeof region,
GetLastError()));
}

if (!VirtualProtect(region.BaseAddress, region.RegionSize, dwNewProtect, &region.Protect)) {
if (!VirtualProtectEx(hProcess, region.BaseAddress, region.RegionSize, dwNewProtect, &region.Protect)) {
throw std::runtime_error(std::format(
"(Change)VirtualProtect(addr=0x{:X}, size=0x{:X}, ..., ...) failed with Win32 code 0x{:X}",
reinterpret_cast<size_t>(region.BaseAddress),
Expand All @@ -436,7 +442,7 @@ utils::memory_tenderizer::memory_tenderizer(const void* pAddress, size_t length,

} catch (...) {
for (auto& region : std::ranges::reverse_view(m_regions)) {
if (!VirtualProtect(region.BaseAddress, region.RegionSize, region.Protect, &region.Protect)) {
if (!VirtualProtectEx(hProcess, region.BaseAddress, region.RegionSize, region.Protect, &region.Protect)) {
// Could not restore; fast fail
__fastfail(GetLastError());
}
Expand All @@ -448,7 +454,7 @@ utils::memory_tenderizer::memory_tenderizer(const void* pAddress, size_t length,

utils::memory_tenderizer::~memory_tenderizer() {
for (auto& region : std::ranges::reverse_view(m_regions)) {
if (!VirtualProtect(region.BaseAddress, region.RegionSize, region.Protect, &region.Protect)) {
if (!VirtualProtectEx(m_process, region.BaseAddress, region.RegionSize, region.Protect, &region.Protect)) {
// Could not restore; fast fail
__fastfail(GetLastError());
}
Expand Down Expand Up @@ -654,3 +660,25 @@ std::wstring utils::escape_shell_arg(const std::wstring& arg) {
}
return res;
}

std::wstring utils::format_win32_error(DWORD err) {
wchar_t* pwszMsg = nullptr;
FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr,
err,
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
reinterpret_cast<LPWSTR>(&pwszMsg),
0,
nullptr);
if (pwszMsg) {
std::wstring result = std::format(L"Win32 error ({}=0x{:X}): {}", err, err, pwszMsg);
while (!result.empty() && std::isspace(result.back()))
result.pop_back();
LocalFree(pwszMsg);
return result;
}

return std::format(L"Win32 error ({}=0x{:X})", err, err);
}
5 changes: 5 additions & 0 deletions Dalamud.Boot/utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,13 @@ namespace utils {
};

class memory_tenderizer {
HANDLE m_process;
std::span<char> m_data;
std::vector<MEMORY_BASIC_INFORMATION> m_regions;

public:
memory_tenderizer(HANDLE hProcess, const void* pAddress, size_t length, DWORD dwNewProtect);

memory_tenderizer(const void* pAddress, size_t length, DWORD dwNewProtect);

template<typename T, typename = std::enable_if_t<std::is_trivial_v<T>&& std::is_standard_layout_v<T>>>
Expand Down Expand Up @@ -275,4 +278,6 @@ namespace utils {
void wait_for_game_window();

std::wstring escape_shell_arg(const std::wstring& arg);

std::wstring format_win32_error(DWORD err);
}
Loading

0 comments on commit 7e78b62

Please sign in to comment.