2
\$\begingroup\$

https://github.com/speedrun-program/load_extender

This is something I made and posted here about a year ago, but I decided to remake it.

To compile this on Windows, you need to install EasyHook.

load_extender_injector.cpp (only used on Windows because LD_PRELOAD and DYLD_INSERT_LIBRARIES can be used on Linux and Mac OS)

#include <tchar.h>
#include <cstdio>
// easyhook.h installed with NuGet
// https://easyhook.github.io/documentation.html
#include <easyhook.h>
#include <Windows.h>

void getExitInput(char ch)
{
    for (; ch != '\n'; ch = std::getchar());

    printf("Press Enter to exit\n");
    ch = std::getchar();
}

int _tmain(int argc, _TCHAR* argv[])
{
    WCHAR* dllToInject32 = nullptr;
    WCHAR* dllToInject64 = nullptr;
    _TCHAR* lpApplicationName = argv[0];
    DWORD lpBinaryType = 0;

    if (GetBinaryType(lpApplicationName, &lpBinaryType) == 0 || (lpBinaryType != 0 && lpBinaryType != 6))
    {
        std::printf("ERROR: This exe wasn't identified as 32-bit or as 64-bit\n");
        getExitInput('\n');
        return 0;
    }
    else if (lpBinaryType == 0)
    {
        dllToInject32 = (WCHAR*)L"load_extender_32.dll";
    }
    else
    {
        dllToInject64 = (WCHAR*)L"load_extender_64.dll";
    }

    std::printf("Enter the process Id: ");
    long long int PIDLongLong = 0;
    char ch = std::getchar();

    for (; ch != '\n'; ch = std::getchar())
    {
        if (ch >= '0' && ch <= '9')
        {
            ch = ch - '0'; // this prevents a warning message
            PIDLongLong *= 10;
            PIDLongLong += ch;
        }

        if (PIDLongLong > 4294967295)
        {
            std::printf("PID too large\n");
            getExitInput(ch);
            return 0;
        }
    }

    NTSTATUS nt = RhInjectLibrary(
        (DWORD)PIDLongLong,      // The process to inject into
        0,                       // ThreadId to wake up upon injection
        EASYHOOK_INJECT_DEFAULT,
        dllToInject32,           // 32-bit
        dllToInject64,           // 64-bit
        nullptr,                 // data to send to injected DLL entry point
        0                        // size of data to send
    );

    if (nt != 0)
    {
        std::printf("RhInjectLibrary failed with error code = %d\n", nt);
        PWCHAR err = RtlGetLastErrorString();
        std::printf("%ls\n", err);
        getExitInput(ch);
        return 0;
    }

    std::printf("Library injected successfully.\n");
    getExitInput(ch);
    return 0;
}

load_extender.cpp

#include <cstdio>
#include <climits>
#include <mutex>
#include <thread>
#include <chrono>
#include <vector>
#include <string>
#include <string_view>
#include <unordered_map>

#ifdef _WIN32
// easyhook.h installed with NuGet
// https://easyhook.github.io/documentation.html
#include <easyhook.h>
#include <Windows.h>
using wcharOrChar = wchar_t; // file paths are UTF-16LE on Windows
using strType = std::wstring;
using svType = std::wstring_view;
#else
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <dlfcn.h>
using wcharOrChar = char;
using strType = std::string;
using svType = std::string_view;
#endif

// using multiple cpp files made exe bigger, so definitions are in this header
#include "shared_stuff.h"

static myMapType m;
static std::mutex mutexForMap = setupMap(m);

#ifdef _WIN32
static NTSTATUS WINAPI NtCreateFileHook(
    PHANDLE            FileHandle,
    ACCESS_MASK        DesiredAccess,
    POBJECT_ATTRIBUTES ObjectAttributes,
    PIO_STATUS_BLOCK   IoStatusBlock,
    PLARGE_INTEGER     AllocationSize,
    ULONG              FileAttributes,
    ULONG              ShareAccess,
    ULONG              CreateDisposition,
    ULONG              CreateOptions,
    PVOID              EaBuffer,
    ULONG              EaLength)
{
    const wchar_t* path = (const wchar_t*)(ObjectAttributes->ObjectName->Buffer);
    int pathEndIndex = (ObjectAttributes->ObjectName->Length) / sizeof(wchar_t);
    int filenameIndex = pathEndIndex;

    for (; filenameIndex >= 0 && path[filenameIndex] != '\\'; filenameIndex--);

    filenameIndex++; // moving past '\\' character or to 0 if no '\\' was found
    auto it = m.find(svType(path + filenameIndex, (size_t)pathEndIndex - filenameIndex));

    if (it != m.end())
    {
        delayFile(m, it->second, mutexForMap);
    }

    return NtCreateFile(
        FileHandle,
        DesiredAccess,
        ObjectAttributes,
        IoStatusBlock,
        AllocationSize,
        FileAttributes,
        ShareAccess,
        CreateDisposition,
        CreateOptions,
        EaBuffer,
        EaLength
    );
}

extern "C" void __declspec(dllexport) __stdcall NativeInjectionEntryPoint(REMOTE_ENTRY_INFO * inRemoteInfo);

void __stdcall NativeInjectionEntryPoint(REMOTE_ENTRY_INFO* inRemoteInfo)
{
    HOOK_TRACE_INFO hHook1 = { nullptr };
    HMODULE moduleHandle = GetModuleHandle(TEXT("ntdll"));

    if (moduleHandle)
    {
        LhInstallHook(
            GetProcAddress(moduleHandle, "NtCreateFile"),
            NtCreateFileHook,
            nullptr,
            &hHook1
        );
    }

    ULONG ACLEntries[1] = { 0 };
    LhSetExclusiveACL(ACLEntries, 1, &hHook1);
}
#else
static auto originalFopen = reinterpret_cast<FILE * (*)(const char* path, const char* mode)>(dlsym(RTLD_NEXT, "fopen"));

FILE* fopen(const char* path, const char* mode)
{
    int filenameIndex = -1;
    int pathEndIndex = 0;

    for (; path[pathEndIndex] != '\0'; pathEndIndex++)
    {
        if (path[pathEndIndex] == '/')
        {
            filenameIndex = pathEndIndex;
        }
    }
    
    filenameIndex++; // moving past '/' character or to 0 if no '/' was found
    auto it = m.find(svType(path + filenameIndex, (size_t)pathEndIndex - filenameIndex));
    
    if (it != m.end())
    {
        delayFile(m, it->second, mutexForMap);
    }

    return original_fopen(path, mode);
}
#endif

shared_stuff.h

#ifndef DEBUG
#define printf(...) (0)
#endif

class FileHelper
{
public:
    FileHelper(const FileHelper& fh) = delete;
    FileHelper& operator=(FileHelper other) = delete;
    FileHelper(FileHelper&&) = delete;
    FileHelper& operator=(FileHelper&&) = delete;

    FileHelper() {}

    ~FileHelper()
    {
        if (_f)
        {
            std::fclose(_f);
        }

        if (_fcheck)
        {
            std::fclose(_fcheck);
        }
    }

    void tryToOpenFile(const char* filename)
    {
#ifdef _WIN32
        if (fopen_s(&_f, filename, "rb") != 0 || !_f)
#else
        if (!(_f = std::fopen(filePath, "rb")))
#endif
        {
            throw "FileHelper fopen failure in tryToOpenFile";
        }
    }

    bool checkIfFileExists(const char* filename)
    {
#ifdef _WIN32
        bool exists = fopen_s(&_fcheck, filename, "rb") == 0;
#else
        bool exists = (_fcheck = std::fopen(filePath, "rb"));
#endif

        if (_fcheck)
        {
            std::fclose(_fcheck);
            _fcheck = nullptr;
        }

        return exists;
    }

    bool getCharacter(wcharOrChar& ch)
    {
        if (_bufferPosition == _charactersRead)
        {
            _bufferPosition = 0;
            _charactersRead = (int)std::fread(_buffer.data(), sizeof(wcharOrChar), _buffer.size(), _f);

            if (!_charactersRead)
            {
                return false;
            }
        }

        ch = _buffer.at(_bufferPosition);
        _bufferPosition++;

        return true;
    }

    void resetFile() // used in tests
    {
        if (std::fseek(_f, 0, SEEK_SET) != 0)
        {
            throw "FileHelper fseek failure in resetFile";
        }

        _bufferPosition = 0;
        _charactersRead = 0;
    }

private:
    FILE* _f = nullptr;
    FILE* _fcheck = nullptr;
    std::vector<wcharOrChar> _buffer = std::vector<wcharOrChar>(8192 / sizeof(wcharOrChar));
    int _bufferPosition = 0;
    int _charactersRead = 0;
};

struct MapValue
{
    std::vector<int> delays;
    size_t position = 0;
    unsigned int fullResetCheckNumber = 0;
    bool reset = false;
    bool resetAll = false;
};

struct KeyCmp
{
    using is_transparent = void;

    bool operator()(const svType sv1, const svType sv2) const
    {
        return sv1 == sv2;
    }
};

struct KeyHash
{
    using is_transparent = void;

    size_t operator()(const svType sv) const
    {
        return _hashObject(sv);
    }

private:
    std::hash<svType> _hashObject = std::hash<svType>();
};

using myMapType = std::unordered_map<strType, MapValue, KeyHash, KeyCmp>;

static bool makeMapValue(std::vector<int>& delaysVector, MapValue& mv, FileHelper& fh)
{
    wcharOrChar ch = '\0';
    bool textRemaining = false;
    long long int delay = 0;

    for (
        textRemaining = fh.getCharacter(ch);
        ch != '\n' && textRemaining;
        textRemaining = fh.getCharacter(ch))
    {
        if (ch >= '0' && ch <= '9')
        {
            ch = ch - '0'; // this prevents a warning message
            delay *= 10;
            delay += ch;

            if (delay > INT_MAX)
            {
                throw "delays can't be larger than INT_MAX";
            }
        }
        else if (ch == '-')
        {
            if (delaysVector.empty())
            {
                mv.resetAll = true;
            }
            else
            {
                mv.reset = true;
            }

            break;
        }
        else if (ch == '/')
        {
            delaysVector.push_back((int)delay);
            delay = 0;
        }
    }

    if (delay != 0 && !mv.reset && !mv.resetAll)
    {
        delaysVector.push_back((int)delay);
    }

    // make sure to go to end of line
    for (; ch != '\n' && textRemaining; textRemaining = fh.getCharacter(ch));

    return textRemaining;
}

static bool addMapPair(myMapType& m, strType& keyStr, std::vector<int>& delaysVector, FileHelper& fh)
{
    keyStr.clear();
    delaysVector.clear();
    MapValue mv;
    wcharOrChar ch = '\0';
    bool keepWhitespace = false;
    bool textRemaining = fh.getCharacter(ch);

    if (ch == '\n' || !textRemaining)
    {
        return textRemaining;
    }
    else if (ch == '/')
    {
        keepWhitespace = true;
    }
    else if (ch != ' ' && ch != '\f' && ch != '\r' && ch != '\t' && ch != '\v')
    {
        keyStr.push_back(ch);
    }

    for (
        textRemaining = fh.getCharacter(ch);
        ch != '\n' && ch != '/' && textRemaining;
        textRemaining = fh.getCharacter(ch))
    {
        if (keepWhitespace || (ch != ' ' && ch != '\f' && ch != '\r' && ch != '\t' && ch != '\v'))
        {
            keyStr.push_back(ch);
        }
    }

    if (textRemaining && ch == '/')
    {
        textRemaining = makeMapValue(delaysVector, mv, fh);
    }

    if (!keyStr.empty() && (!delaysVector.empty() || mv.resetAll))
    {
        mv.delays = delaysVector;
        strType keyStrCopy(keyStr);
        mv.delays.shrink_to_fit();
        keyStrCopy.shrink_to_fit();
        m.emplace(std::move(keyStrCopy), std::move(mv));
    }

    return textRemaining;
}

static std::mutex setupMap(myMapType& m)
{
    try
    {
        std::wstring keyStr;
        std::vector<int> delaysVector;
        FileHelper fh;

        if (!fh.checkIfFileExists("files_and_delays0.txt"))
        {
            fh.tryToOpenFile("files_and_delays.txt");
            printf("opened files_and_delays.txt\n");
        }
        else
        {
            std::string name("files_and_delays1.txt");
            unsigned int n = 1;

            while (fh.checkIfFileExists(name.c_str()))
            {
                n++;
                name = std::string("files_and_delays" + std::to_string(n) + ".txt");
            }

            name = std::string("files_and_delays" + std::to_string(n - 1) + ".txt");
            fh.tryToOpenFile(name.c_str());
            printf("opened %s\n", name.c_str());
        }

#ifdef _WIN32
        wchar_t byteOrderMark = '\0';

        if (!fh.getCharacter(byteOrderMark))
        {
            printf(
                "files_and_delays.txt byte order mark is missing\n\
                make sure files_and_delays.txt is saved as UTF-16 LE\n"
            );
        }
        else if (byteOrderMark != 0xFEFF) // not 0xFFFE because of how wchar_t is read
        {
            printf(
                "files_and_delays.txt byte order mark isn't marked as UTF-16 LE\n\
                make sure files_and_delays.txt is saved as UTF-16 LE\n"
            );
        }
#endif

        while (addMapPair(m, keyStr, delaysVector, fh));
    }
    catch (char const* e)
    {
        char const* resolveC4101Warning = e;
        printf("%s\n", resolveC4101Warning);
        m.clear(); // clear map so failure is more obvious
    }

    return std::mutex();
}

static void delayFile(myMapType& m, MapValue& mv, std::mutex& mutexForMap)
{
#ifndef DEBUG // this needs to be reset in the test, so it's a global variable instead
    static unsigned int fullResetCount = 0;
#endif

    printf("fullResetCount: %zu\n", fullResetCount);
    int delay = 0;

    {
        std::lock_guard<std::mutex> mutexForMapLock(mutexForMap);

        if (mv.fullResetCheckNumber < fullResetCount)
        {
            mv.position = 0;
            mv.fullResetCheckNumber = fullResetCount;
            printf("this delay sequence reset due to prior full reset\n");
        }

        if (mv.resetAll)
        {
            if (fullResetCount == UINT_MAX) // this probably won't ever happen
            {
                fullResetCount = 0;

                for (auto& it : m)
                {
                    it.second.fullResetCheckNumber = 0;
                }

                printf("fullResetCount reset\n");
            }

            fullResetCount++;
            printf("fullResetCount set to %zu, all sequences will be reset\n", fullResetCount);
        }
        else if (mv.position == mv.delays.size())
        {
            if (mv.reset)
            {
                mv.position = 0;
                printf("this delay sequence reset\n");
            }
            else
            {
                printf("delay sequence already finished\n");
            }
        }

        if (mv.position < mv.delays.size())
        {
            delay = mv.delays.at(mv.position);
            mv.position++;
        }

        printf("delay is %d millisecond(s)\n\n", delay);
    }

    if (delay > 0)
    {
#ifndef DEBUG
        std::this_thread::sleep_for(std::chrono::milliseconds(delay));
#endif
    }
}

load_extender_test.cpp

#include <cstdio>
#include <climits>
#include <mutex>
#include <vector>
#include <string>
#include <unordered_map>

#ifdef _WIN32
using wcharOrChar = wchar_t; // file paths are UTF-16LE on Windows
using strType = std::wstring;
using svType = std::wstring_view;
#else
using wcharOrChar = char;
using strType = std::string;
using svType = std::string_view;
#endif

#define DEBUG
unsigned int fullResetCount = 0;

// using multiple cpp files made exe bigger, so definitions are in this header
#include "shared_stuff.h"

void windowsHookFunction(myMapType& m, strType& pathStr, std::mutex& mutexForMap)
{
    if (!pathStr.empty() && pathStr.back() == '\r')
    {
        pathStr.pop_back();
    }

    const wcharOrChar* path = pathStr.c_str();

    int pathEndIndex = (int)pathStr.size();
    int filenameIndex = pathEndIndex;

    for (; filenameIndex >= 0 && path[filenameIndex] != '\\'; filenameIndex--);

    filenameIndex++; // moving past '\\' character or to 0 if no '\\' was found
    auto it = m.find(svType(path + filenameIndex, (size_t)pathEndIndex - filenameIndex));

    if (it != m.end())
    {
#ifdef _WIN32
        printf("\n%ls found in map\n", pathStr.c_str());
#else
        printf("\n%s found in map\n", pathStr.c_str());
#endif
        delayFile(m, it->second, mutexForMap);
    }
    else
    {
#ifdef _WIN32
        printf("\n%ls not found in map\n", pathStr.c_str());
#else
        printf("\n%s not found in map\n", pathStr.c_str());
#endif
    }
}

void unixHookFunction(myMapType& m, strType& pathStr, std::mutex& mutexForMap)
{
    if (!pathStr.empty() && pathStr.back() == '\r')
    {
        pathStr.pop_back();
    }

    const wcharOrChar* path = pathStr.c_str();

    int filenameIndex = -1;
    int pathEndIndex = 0;

    for (; path[pathEndIndex] != '\0'; pathEndIndex++)
    {
        if (path[pathEndIndex] == '/')
        {
            filenameIndex = pathEndIndex;
        }
    }

    filenameIndex++; // moving past '/' character or to 0 if no '/' was found
    auto it = m.find(svType(path + filenameIndex, (size_t)pathEndIndex - filenameIndex));

    if (it != m.end())
    {
#ifdef _WIN32
        printf("\n%ls found in map\n", pathStr.c_str());
#else
        printf("\n%s found in map\n", pathStr.c_str());
#endif
        delayFile(m, it->second, mutexForMap);
    }
    else
    {
#ifdef _WIN32
        printf("\n%ls not found in map\n", pathStr.c_str());
#else
        printf("\n%s not found in map\n", pathStr.c_str());
#endif
    }
}

void printMap(const myMapType& m)
{
    for (auto& it : m)
    {
        printf(
#ifdef _WIN32
            "%ls : fullResetCheckNumber %zu : position %zu : ",
#else
            "%s : fullResetCheckNumber %zu : position %zu : ",
#endif
            it.first.c_str(),
            it.second.fullResetCheckNumber,
            it.second.position
        );

        for (size_t i = 0; i < it.second.delays.size(); i++)
        {
            printf("%d / ", it.second.delays.at(i));
        }

        if (it.second.reset)
        {
            printf("RESET");
        }
        else if (it.second.resetAll)
        {
            printf("RESET ALL");
        }

        printf("\n");
    }
}

void testFunctions(myMapType& m, FileHelper& fh, std::mutex& mutexForMap, bool testUnix)
{
    strType pathStr;
    wcharOrChar ch = '\0';

    while (fh.getCharacter(ch))
    {
        if (ch == '\n')
        {
            if (testUnix)
            {
                unixHookFunction(m, pathStr, mutexForMap);
            }
            else
            {
                windowsHookFunction(m, pathStr, mutexForMap);
            }

            printMap(m);
            pathStr.clear();
        }
        else
        {
            pathStr.push_back(ch);
        }
    }

    if (testUnix)
    {
        unixHookFunction(m, pathStr, mutexForMap);
    }
    else
    {
        windowsHookFunction(m, pathStr, mutexForMap);
    }
}

void testInputs(myMapType& m, std::mutex& mutexForMap)
{
    FileHelper fh;
    fh.tryToOpenFile("test_input.txt");
    wcharOrChar byteOrderMark = '\0';
#ifdef _WIN32

    if (!fh.getCharacter(byteOrderMark))
    {
        printf(
            "test_input.txt byte order mark is missing\n\
            save test_input.txt as UTF-16 LE\n\n"
        );

        return;
    }
    else if (byteOrderMark != 0xFEFF) // not 0xFFFE because of how wchar_t is read
    {
        printf(
            "test_input.txt byte order mark isn't marked as UTF-16 LE\n\
            make sure files_and_delays.txt is saved as UTF-16 LE\n\n"
        );
    }
#endif
    printf("\ntesting UNIX\n\n - - - - - - - - - -\n\n");
    printMap(m);
    testFunctions(m, fh, mutexForMap, true);
    printMap(m);
    fullResetCount = 0;
    fh.resetFile();
#ifdef _WIN32
    fh.getCharacter(byteOrderMark);
#endif

    for (auto& it : m)
    {
        it.second.position = 0;
        it.second.fullResetCheckNumber = 0;
    }

    printf("\n\ntesting Windows\n\n - - - - - - - - - -\n\n");
    printMap(m);
    testFunctions(m, fh, mutexForMap, false);
    printMap(m);
}

int main()
{
    try
    {
        printf("\ntest start\n\n");
        myMapType m;
        std::mutex mutexForMap = setupMap(m);
        testInputs(m, mutexForMap);
    }
    catch (char const* e)
    {
        printf("%s", e);
    }

    printf("\ntest finished, press Enter to exit\n");
    char ch = getchar();

    return 0;
}
\$\endgroup\$
1

1 Answer 1

1
\$\begingroup\$

Use more C++

You tagged your question with C++20, but you are not making full use of it. In fact, you even rely a lot on C functions when there are much better C++ equivalents.

Prefer <iostream> over <cstdio>. Since C++17, you have std::format() so you can format strings in a way similar to C's printf(), albeit with a slightly different syntax. For example, you can write:

auto name = std::format("files_and_delays{}.txt", n);
...
std::cout << std::format("opened {}\n", name);

Note that this is type safe and understands many C++ types, like std::strings. In the future you might even be able to use std::print().

You can also use C++17's filesystem library. For example, if you want to get the filename component from a path:

std::filesystem::path path{(const wchar_t*)(ObjectAttributes->ObjectName->Buffer)};

if (auto it = m.find(path.filename()); it != m.end()) {
    delayFile(m, it->second, mutexForMap);
}

There are even things that are provided by C++ from the previous millenium, like reading an integer from the standard input:

std::cout << "Enter the process ID: ";
long long int PID;
std::cin >> PID;

Naming things

Some of the names of variables, types or even files are badly chosen. Make sure things have concise and descriptive names, but are not abbreviated too much. Give a bit more thought to what a good name is; it will be nice for others that have to work with your code, but also for yourself in the future. Some examples of names that can be improved:

  • "shared_stuff.h": words like "stuff" and "thing" have almost no meaning. I see this file contains FileHelper and a lot of map related functions. Perhaps it can be split into two files: "filehelper.h" and "map.h". If you really do just want a bucket of shared stuff, I would just name it "shared.h".
  • PIDLongLong: I would avoid adding the type of a variable into the name of a variable if possible.
  • lpBinaryType: I know the Windows API uses Hungarian notation, but even Microsoft discourages it. I would avoid using it, but if you really do want to use it, at least follow its rules. The lp prefix is for long pointers, not for DWORDs which should use the dw prefix.
  • m: avoid single-letter names, unless it's something that is in very common use, like i for loop indices, or x and y for coordinates. I would choose map or fileMap instead as the name for this variable.

Create a class for managing the file map

You have a separate std::unordered_map and a std::mutex. However, these are tightly coupled. There are also some helper functions that set up the map and add items to it. I would create a new class for that and group everything in there. You can turn setupMap() into the constructor for this class.

Add a constructor to struct MapValue

Turn makeMapValue() into the constructor of MapValue.

\$\endgroup\$
3
  • \$\begingroup\$ Thanks for the review. I tried adding all your suggestions except for std::format, because for some reason it increased the exe size from 12 KB to 178 KB, so instead I put a note saying to replace std::printf and std::cout with std::print once it's available. I put the new code on the github page. Does it seem okay now? \$\endgroup\$ Commented Apr 29, 2022 at 6:08
  • 1
    \$\begingroup\$ Great! Just create a new question here on CodeReview with the new version, so we can review it again. The code size increase is unfortunate, and I'm afraid std::print() will have the same issue. So if you want a small executable, I guess it's better to stay with printf() then. \$\endgroup\$
    – G. Sliepen
    Commented Apr 29, 2022 at 6:52
  • 1
    \$\begingroup\$ Okay, thank you. I posted it again: codereview.stackexchange.com/questions/276167/… \$\endgroup\$ Commented Apr 29, 2022 at 21:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.