Emulite
Emulite is a Python framework for emulating Android and iOS native binaries. It combines detailed logging, configurable device profiles, strong typing, and explicit failures to provide a direct path from a target binary to a working proof of concept.
Emulite is alpha software, so breaking API changes may occur before 1.0.0. It was inspired by Unidbg and Chomper.
Requirements
- Python 3.10+
- Unicorn 2.1.4+
- Capstone 5+
- LIEF 0.14+
- 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
Real application examples: emulite-examples
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.
import logging
from emulite import AndroidEmulator64, AndroidEmulatorBase, JniHandler, JniValue
from emulite import MemoryAccess, MemoryHookAction, ReplacementAction, TraceAction, TraceInfo
from emulite.android.java.lang.reflect.java_method import JavaMethod
class CustomJniHandler(JniHandler):
def call_static_method(self, method: JavaMethod, args: list[object]) -> JniValue:
if method.java_class.name == "com/example/Device" and method.name == "getValue":
return "value"
return super().call_static_method(method, args)
def before_memcpy(emu: AndroidEmulatorBase) -> ReplacementAction:
print("memcpy 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_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")
emu = AndroidEmulator64(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)
trace = module.trace(on_instruction)
jni_result = native.call_static("getValue", "(I)I", 123)
buffer = emu.allocate_string("testing")
print(buffer.read_cstr())
watchpoint = emu.watchpoint(buffer, on_memory, length=32)
emu.map_file("/data/local/tmp/config.json", "config.json")
native_result = module.call_symbol("exported_symbol", buffer)
runtime_image = module.dump()
watchpoint.unhook()
trace.unhook()
time_hook.unhook()
memcpy_hook.unhook()
emu.free(buffer)
emu.close()
iOS Usage
IOSApplication accepts an unpacked .app or an .ipa, creates its profile,
and uses the bundled rootfs. Mach-O symbols retain their leading underscore.
import logging
from emulite import IOSApplication, IOSEmulatorBase, MemoryAccess, MemoryHookAction
from emulite import TraceAction, TraceInfo
def replace_time(_emu: IOSEmulatorBase) -> int:
return 1_700_000_000
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")
app = IOSApplication.load("path/to/Example.app")
emu = app.emulator
module = app.module
time_hook = emu.intercept("_time", replace_time)
trace = module.trace(on_instruction)
payload = emu.allocate_string("payload")
watchpoint = emu.watchpoint(payload, on_memory, length=8)
emu.map_file("/tmp/config.json", "config.json")
native_result = module.call_symbol("_sign", payload)
signer = app.objc.require_class("Signer")
instance_hook = app.objc.add_interceptor("+[Signer sharedInstance]", lambda _emu: signer.call_method("alloc"))
value_hook = app.objc.add_interceptor("-[Signer value]", lambda _emu: 42)
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()
status = app.run()
runtime_image = module.dump()
watchpoint.unhook()
trace.unhook()
value_hook.unhook()
instance_hook.unhook()
time_hook.unhook()
emu.free(payload)
app.close()
For standalone targets, use IOSEmulator64.load_library() for a dylib or
load_framework() for a framework.
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.
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) -> 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
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.
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.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| emulite-0.3.0.tar.gz | 90.0 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| emulite-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 180.9 MB
Release files / emulite-0.3.0.tar.gz
| Download URL | emulite-0.3.0.tar.gz |
|---|---|
| Size | 90.0 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
122825344caad28ac645165bb0257608be531ea2b3c2c06678d95c7882d3436e
|
|
BLAKE2b-256 checksum How to use checksums |
1062da63915c22c2fa7b719776e9da62dd409e4dd79e909f1108377f07b42287
|
| 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 2, 2026.
Transparency logRelease files / emulite-0.3.0-py3-none-any.whl
| Download URL | emulite-0.3.0-py3-none-any.whl |
|---|---|
| Size | 90.9 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9a641656987a84468984cccd3613b796b2e98cbf4ac1bfc2e52fd30be1a8ebd3
|
|
BLAKE2b-256 checksum How to use checksums |
1af0db922eba9253691019441ce66ac148044210c92a3c54d85797d4b6b4d6f4
|
| 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 2, 2026.
Transparency log