Metadata-Version: 2.5
Name: emulite
Version: 0.4.0
Summary: Emulation framework for Android & iOS native libraries
Project-URL: Repository, https://github.com/retrowave3/emulite
Project-URL: Issues, https://github.com/retrowave3/emulite/issues
Requires-Python: >=3.10
Requires-Dist: capstone>=5.0.2
Requires-Dist: cryptography>=42
Requires-Dist: lief>=0.17
Requires-Dist: unicorn>=2.1.4
Provides-Extra: dev
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Description-Content-Type: text/markdown

# Emulite

[![PyPI](https://img.shields.io/pypi/v/emulite)](https://pypi.org/project/emulite/)
[![Python](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fpypi.org%2Fpypi%2Femulite%2Fjson&query=%24.info.requires_python&label=python)](https://pypi.org/project/emulite/)
[![Tests](https://github.com/retrowave3/emulite/actions/workflows/tests.yml/badge.svg)](https://github.com/retrowave3/emulite/actions/workflows/tests.yml)

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](https://github.com/zhkl0228/unidbg) and [Chomper](https://github.com/sledgeh4w/chomper).

## Requirements

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

## Installation

From PyPI:

```console
pip install emulite
```

From source:

```console
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](https://github.com/retrowave3/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.
Syscall hooks receive `(emu, original_function)`: call `original_function()` to continue through the hook chain, or return an integer directly to handle the syscall.

```python
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)`.

```python
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:

```python
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:

```python
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:

```python
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:

```python
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  | — |


<sub>✅ Supported · ⏳ Planned · — Not planned</sub>

## Resources

- [Unidbg](https://github.com/zhkl0228/unidbg)
- [Chomper](https://github.com/sledgeh4w/chomper)
- [Unicorn](https://github.com/unicorn-engine/unicorn)
- [LIEF](https://github.com/lief-project/LIEF)
- [Capstone](https://github.com/capstone-engine/capstone)
