Skip to content
Ukine9999Public

About

Provider-driven Windows x64 C++20 emulation library built on Unicorn: remote memory, recursive calls, import/syscall interception, overlays, and execution control.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Latest commit

 

History

7 Commits

Folders and files

Repository files navigation

Emul8

Emul8 is a Windows x64 C++20 library for executing machine code through Unicorn while routing guest memory, imports, system calls, and execution controls through replaceable providers.

It is intentionally small: one CMake target, a public API, provider interfaces, and a Unicorn backend. The host decides which process, memory view, environment data, and execution rules are visible to each invocation.

Capabilities

  • Windows x64 execution through Unicorn and Zydis
  • Static or shared library builds
  • Current-process, remote-process, and custom memory providers
  • Explicit PEB and TEB providers with no implicit remote discovery
  • PE module and import catalogues backed by IMemoryProvider
  • Recursive direct, indirect, nested, and tail-call execution
  • Reusable component profiles and per-invocation overrides
  • Replacement, sparse overlay, copy-on-write, and alias memory bindings
  • Local-only, forwarded, and denied writes
  • Instruction, call, return, memory, and syscall rules
  • Conditions, register changes, memory patches, pause, resume, stop, skip, redirect, and call replacement
  • External call and system call interception
  • Explicit register, stack, scratch, and return-sentinel configuration

Requirements

  • Windows x64
  • CMake 3.24 or newer
  • C++20 compiler: MSVC or clang-cl

Build

cmake -S . -B build\clang-x64 -G Ninja -DCMAKE_CXX_COMPILER=clang-cl
cmake --build build\clang-x64 --parallel
ctest --test-dir build\clang-x64 --output-on-failure

Build a shared library with the same target:

cmake -S . -B build\clang-x64-shared -G Ninja -DBUILD_SHARED_LIBS=ON -DCMAKE_CXX_COMPILER=clang-cl
cmake --build build\clang-x64-shared --parallel
ctest --test-dir build\clang-x64-shared --output-on-failure

The public target is Emul8::Emul8. The current build also produces a deterministic remote-process integration fixture used by the tests.

Minimal invocation

#include <Emul8/Api/Emul8.h>

#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <span>
#include <utility>

int main()
{
    auto memoryProvider = std::make_shared<Emul8::Adapters::Windows::CurrentProcessMemoryProvider>();
    auto profileResult = Emul8::Api::ComponentProfile::Create(memoryProvider);
    if (!profileResult)
        return 1;

    auto memoryRange = Emul8::Api::AddressRange(0x70000000, 0x10000);
    auto memoryLayout = Emul8::Api::InvocationMemoryLayout(memoryRange, std::nullopt, 0x71000000);
    auto executionLimits = Emul8::Api::ExecutionLimits(100000, 0);
    auto configuration = Emul8::Api::CpuConfiguration(std::move(profileResult).TakeValue(), memoryLayout, executionLimits);

    auto cpuResult = Emul8::Api::CreateCpu(configuration);
    if (!cpuResult)
        return 1;

    auto result = Emul8::Api::InvokeWin64<std::uint64_t>(
        *cpuResult.Value(),
        0x0000000140001000,
        std::uint64_t{7});
    return result && result.Value() == 42 ? 0 : 1;
}

The target address and every memory range must be valid in the selected provider or an explicit profile. The stack and return sentinel are local invocation ranges; the library does not allocate hidden guest memory.

Provider model

IMemoryProvider is the only required memory contract:

class MyMemoryProvider final : public Emul8::Spi::IMemoryProvider
{
public:
    Emul8::Api::Result<Emul8::Api::MemoryRegion> QueryRegion(std::uint64_t address) const override;
    Emul8::Api::Result<void> ReadMemory(std::uint64_t address, std::span<std::byte> destination) const override;
    Emul8::Api::Result<void> WriteMemory(std::uint64_t address, std::span<const std::byte> source) override;
};

Use CurrentProcessMemoryProvider for the host process. Use RemoteProcessMemoryProvider with an already opened borrowed HANDLE for another process. The provider never opens, suspends, or discovers a remote process on its own.

Environment data is a separate contract:

Emul8::Adapters::Windows::CurrentProcessWindowsEnvironmentProvider currentEnvironment;
Emul8::Adapters::Windows::RemoteProcessWindowsEnvironmentProvider remoteEnvironment(remotePeb, remoteTeb);

WindowsModuleCatalog and WindowsImportCatalog consume an explicit provider and address. Remote PEB, TEB, TLS, and module metadata therefore remain under host control.

Memory profiles

Profiles are immutable and reusable. Each root invocation receives a fresh overlay state.

Emul8::Api::MemoryProfileBuilder builder;
builder.AddReplacement(100, dataAddress, replacementBytes);
builder.AddSparseOverlay(90, Emul8::Api::AddressRange(dataAddress, 0x100), sparseBytes);
builder.AddCopyOnWrite(80, Emul8::Api::AddressRange(dataAddress, 0x1000));
builder.AddAlias(70, Emul8::Api::AddressRange(aliasAddress, 0x100), otherProvider, sourceAddress);
auto profileResult = builder.Build();

Higher priority wins. Equal-priority overlapping ranges are rejected. EMirrorWriteMode::LOCAL_ONLY changes only the invocation view, EMirrorWriteMode::FORWARD writes through to the selected provider, and EMirrorWriteMode::DENY rejects the write.

Rules and interception

Rules are attached to a component profile and inherited by descendant calls. A rule selects an event, evaluates conditions, and applies ordered actions. Actions can pause or stop execution, edit registers, patch memory, skip an instruction, redirect a call, or replace a call.

IExternalCallProvider receives a resolved call target before the guest call executes. It can let the call continue, return directly to the caller, or redirect it while preserving the Win64 call return address. ISystemCallProvider receives guest syscall requests; the core never executes a host syscall implicitly.

Remote execution semantics

When a remote provider is used, Unicorn receives an internal page backing for translation, but every guest fetch, read, and write is checked against the effective MemorySpace. Data reads are refreshed from the provider. Guest writes are forwarded to the provider without creating a hidden local mirror.

Remote processes are not suspended. Reads can be torn if the source process mutates memory concurrently. Code pages freeze on first fetch; call IInvocation::InvalidateCode after an intentional remote code change.

Scope

Each invocation is single-threaded and owns its Unicorn engine, registers, stack, scratch ranges, mappings, rules, and overlay state. Parallelism is achieved with independent CPU and invocation objects.

The current backend supports Windows x64 only.

About

Provider-driven Windows x64 C++20 emulation library built on Unicorn: remote memory, recursive calls, import/syscall interception, overlays, and execution control.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Contributors

Languages