Skip to main content

Emulite

PyPI Python Tests

Emulite is a Python framework for emulating native Android and iOS binaries. Strong emphasis is being put towards logging, configurable profiles, strong typing, and loud explicit errors. This project is currently in alpha, breaking API changes will occur while we aim for a stable v1.0.0 release

This project was inspired by Unidbg and Chomper.

Requirements

  • Python 3.10+
  • Unicorn 2.1.4+
  • Capstone 5.0.2+
  • LIEF 0.17+
  • Cryptography 42+

Installation

From PyPI:

pip install emulite

From source:

git clone --recurse-submodules https://github.com/retrowave3/emulite.git
cd emulite
pip install -e .

Examples

Examples has a different repository, click here to be redirected

Android Usage

Use AndroidEmulator32 or AndroidEmulator64 based on the target architecture. A default Android rootfs is included. Pass a path as the first argument to use a custom rootfs.

Every hook returns a HookHandle; call unhook() when the hook is no longer needed. Replacement, trace, and memory callbacks use explicit action enums, so callback behavior is visible in both code and type checkers. Syscall hooks receive (emu, original_function): call original_function() to continue through the hook chain, or return an integer directly to handle the syscall.

import logging
from collections.abc import Callable

from emulite import AndroidEmulator64, AndroidEmulatorBase, AndroidProfile, JniHandler, JniValue
from emulite import MemoryAccess, MemoryHookAction, ReplacementAction, TraceAction, TraceInfo
from emulite.abi.enums.native_type import NativeType
from emulite.abi.native_signature import NativeSignature
from emulite.android.java.lang.reflect.java_method import JavaMethod


class CustomJniHandler(JniHandler):
    @staticmethod
    def get_timeout() -> int:
        return 15_000

    def call_static_method(self, method: JavaMethod, args: list[object]) -> JniValue:
        if method.java_class.name == "com/example/Native" and method.name == "getValue" and method.signature == "(I)I":
            return 42
        return super().call_static_method(method, args)


def before_memcpy(emu: AndroidEmulatorBase) -> ReplacementAction:
    destination = emu.get_argument_pointer(0)
    print("memcpy destination:", destination, "size:", emu.get_argument(2))
    return ReplacementAction.CALL_ORIGINAL


def after_memcpy(emu: AndroidEmulatorBase) -> None:
    print("memcpy returned:", hex(emu.get_return_value()))


def replace_time(_emu: AndroidEmulatorBase) -> int:
    return 150000


def on_openat(emu: AndroidEmulatorBase, original_function: Callable[[], int]) -> int:
    print("openat syscall from", hex(emu.pc))
    return original_function()


def on_instruction(_emu: AndroidEmulatorBase, info: TraceInfo) -> TraceAction:
    print(info.format())
    return TraceAction.CONTINUE


def on_memory(_emu: AndroidEmulatorBase, access: MemoryAccess, address: int, size: int, value: int) -> MemoryHookAction:
    print(access.name.lower(), hex(address), size, hex(value))
    return MemoryHookAction.CONTINUE


logging.basicConfig(level=logging.DEBUG, format="%(levelname)s %(name)s: %(message)s")
profile = AndroidProfile(package_name="com.example.research", seed=1)
emu = AndroidEmulator64(profile=profile, jni_handler=CustomJniHandler())
module = emu.load_library("path/to/libnative.so")
module.call_jni_onload()

native = emu.java_class("com.example.Native")
memcpy_hook = module.hook_import("memcpy", before_memcpy, after_memcpy)
time_hook = emu.intercept("time", replace_time)
syscall_hook = emu.hook_syscall(56, on_openat)
trace = module.trace(on_instruction)

# Override a Java callback made by native code.
java_hook = emu.jni_handler.override_method("com/example/Config", "getTimeout", "()I", CustomJniHandler.get_timeout, static=True)
jni_result = native.call_static("getValue", "(I)I", 123)
java_hook.unhook()

buffer = emu.allocate_string("testing")
print(buffer.read_cstr())

watchpoint = emu.watchpoint(buffer, on_memory, length=8)
emu.map_file(guest_path="/data/local/tmp/config.json", host_path="config.json")
native_result = module.call_symbol("exported_symbol", buffer)

# For an export declared as uint64_t add(uint64_t, uint64_t):
signature = NativeSignature(NativeType.UINT64, (NativeType.UINT64, NativeType.UINT64))
typed_result = module.call_typed("add", signature, 20, 22, instruction_limit=10_000)
runtime_image = module.dump()

watchpoint.unhook()
trace.unhook()
syscall_hook.unhook()
time_hook.unhook()
memcpy_hook.unhook()
emu.free(buffer)
emu.close()

Guest Android threads run cooperatively, with their own TLS (thread-local storage) and JNI state.

iOS Usage

IOSEmulator64.load_app() accepts an unpacked .app or an .ipa, returns its native module, and keeps the emulator's configured profile. The bundled rootfs is used by default; system_runtime=True enables native Objective-C and system frameworks. Mach-O symbols retain their leading underscore. Darwin syscall hooks use non-negative BSD syscall numbers and negative Mach trap numbers, such as -int(MachTrap.HOST_SELF).

import logging
from collections.abc import Callable

from emulite import IOSEmulator64, IOSEmulatorBase, IOSProfile, MemoryAccess, MemoryHookAction
from emulite import ObjCObject, TraceAction, TraceInfo
from emulite.ios.enums.mach_trap import MachTrap


def replace_time(_emu: IOSEmulatorBase) -> int:
    return 1_700_000_000


def replace_shared_instance(emu: IOSEmulatorBase) -> ObjCObject:
    return emu.objc.require_class("Signer").call_method("alloc")


def replace_signer_value(_emu: IOSEmulatorBase) -> int:
    return 42


def on_host_self(emu: IOSEmulatorBase, original_function: Callable[[], int]) -> int:
    print("host_self Mach trap from", hex(emu.pc))
    return original_function()


def on_instruction(_emu: IOSEmulatorBase, info: TraceInfo) -> TraceAction:
    print(info.format())
    return TraceAction.CONTINUE


def on_memory(_emu: IOSEmulatorBase, access: MemoryAccess, address: int, size: int, value: int) -> MemoryHookAction:
    print(access.name.lower(), hex(address), size, hex(value))
    return MemoryHookAction.CONTINUE


logging.basicConfig(level=logging.DEBUG, format="%(levelname)s %(name)s: %(message)s")
profile = IOSProfile(bundle_id="com.example.research", executable_name="Example", seed=1)
emu = IOSEmulator64(profile=profile, system_runtime=True)
module = emu.load_app("path/to/Example.ipa")  # An unpacked .app directory also works.
objc = emu.objc

time_hook = emu.intercept("_time", replace_time)
mach_hook = emu.hook_syscall(-int(MachTrap.HOST_SELF), on_host_self)
trace = module.trace(on_instruction)
payload = emu.allocate_string("payload")
watchpoint = emu.watchpoint(payload, on_memory, length=8)
emu.map_file(guest_path="/tmp/config.json", host_path="config.json")
native_result = module.call_symbol("_sign", payload)

signer = objc.require_class("Signer")
instance_hook = objc.add_interceptor("+[Signer sharedInstance]", replace_shared_instance)
value_hook = objc.add_interceptor("-[Signer value]", replace_signer_value)
instance = signer.call_method("sharedInstance")
objc_result = instance.call_method("value")
instance_class_name = instance.class_name
state = instance.get_variable("_state")
class_header = signer.dump()
runtime_image = module.dump()

watchpoint.unhook()
trace.unhook()
value_hook.unhook()
instance_hook.unhook()
mach_hook.unhook()
time_hook.unhook()
emu.free(payload)
emu.close()

For standalone targets, use IOSEmulator64.load_library() for a dylib or load_framework() for a framework. Loading an app does not start its event loop.

Android and iOS share the same module, pointer, allocation, file, interception, tracing, watchpoint, and lifecycle primitives. String and byte arguments are borrowed for one call; use allocate_string() or allocate_bytes() when native code retains the pointer. module.call_address(offset, ...) uses a module-relative offset; emulator.call(address, ...) uses an absolute guest address.

Debugging

Debugging is disabled by default. Before a native call, use emu.attach_debugger() for the terminal console, or attach a GDB-compatible client:

from emulite import DebuggerType

debugger = emu.attach_debugger(DebuggerType.GDB, host="127.0.0.1", port=12345)
debugger.add_breakpoint(module.base + 0x1234)
result = module.call_symbol("exported_symbol")

Set host and port to your preferred listening address. The call pauses at entry. Connect GDB with target remote 127.0.0.1:12345 for the example above, or select Remote GDB in IDA, use the same host/port, and Start (F9). No extra IDA scripts are needed. Breakpoints take integer guest addresses; use runtime addresses in IDA. Call debugger.close() when finished debugging.

Crash Details

EmulatorCrashed retains a readable message and exposes pc, location, and an immutable frames tuple when that information can be collected:

from emulite import EmulatorCrashed

try:
    module.call_symbol("run")
except EmulatorCrashed as crash:
    print(crash.location)
    for frame in crash.frames:
        print(frame.format())

Virtual Filesystem Providers

Providers receive mount-relative POSIX paths:

import errno

from emulite import AndroidEmulator64
from emulite.filesystem import FileIO, FileStat, OpenFlag, RegularFileIO


class MemoryProvider:
    def __init__(self, data: bytes):
        self.data = bytearray(data)

    def open(self, path: str, flags: OpenFlag, *, mode: int = 0o666) -> FileIO | int:
        if path != "/message.txt":
            return -errno.ENOENT
        return RegularFileIO(path, self.data, writable=False, oflags=flags)

    def stat(self, path: str) -> FileStat | None:
        if path == "/":
            return FileStat.for_directory()
        if path == "/message.txt":
            return FileStat.for_file(len(self.data))
        return None

    def listdir(self, path: str) -> list[str] | None:
        return ["message.txt"] if path == "/" else None


emu = AndroidEmulator64()
provider = MemoryProvider(b"hello from Python")
emu.mount("/virtual", provider)
contents = emu.read_file("/virtual/message.txt")
emu.unmount("/virtual", provider)
emu.close()

Guest Snapshots

Android supports restoring guest checkpoints:

checkpoint = emu.snapshot()
first = module.call_symbol("exported_symbol")
emu.restore(checkpoint)
second = module.call_symbol("exported_symbol")

Snapshots restore CPU and guest memory on the emulator that created them. They do not restore Python objects, hooks, or open files. iOS snapshots support inspection only; restoring them is unsupported.

Project Status

Supported Platforms

Platform ARM32 ARM64
Android ✅ AndroidEmulator32 ✅ AndroidEmulator64
iOS — ✅ IOSEmulator64

Supported Engines

Backend Engine Status
Unicorn ✅
Dynarmic —
Apple Silicon Hypervisor —
Linux KVM Hypervisor —

✅ Supported · ⏳ Planned · — Not planned

Resources

Metadata

Release files for emulite 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for emulite 0.4.0
File Size Uploaded
emulite-0.4.0.tar.gz 90.2 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for emulite 0.4.0
File Interpreter ABI Platform
emulite-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 181.3 MB

Release files / emulite-0.4.0.tar.gz

Download URL emulite-0.4.0.tar.gz
Size 90.2 MB
Tags Source
SHA-256 checksum
How to use checksums
99802e3fc28bdb2c6b1218a29e4b72d32b0f046095882601489004ab55ad8202
BLAKE2b-256 checksum
How to use checksums
05c3a29acd6ed50c44874de47c62213292d5cdea7ea7bd151ec9323a6bea0ea5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 9, 2026.

Transparency log

Release files / emulite-0.4.0-py3-none-any.whl

Download URL emulite-0.4.0-py3-none-any.whl
Size 91.2 MB
Tags Python 3
SHA-256 checksum
How to use checksums
524ea5482850de3a9162c279066f899154b17c05b34640bb85e84ff7f3338f9b
BLAKE2b-256 checksum
How to use checksums
15dc8473312a8aaa42df186dc29c244a2e700fd7581d9e324dce17282dea3feb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 9, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page