Metadata-Version: 2.5
Name: emulite
Version: 0.3.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
Requires-Dist: cryptography>=42
Requires-Dist: lief>=0.14
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/)

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

## Requirements

- Python 3.10+
- Unicorn 2.1.4+
- Capstone 5+
- LIEF 0.14+
- 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

Real application examples: [emulite-examples](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.

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

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

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

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

## 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)
