
Androidネイティブライブラリのエミュレートと、実験的なiOSエミュレーションを可能にします。
Androidネイティブライブラリと、実験的なiOSエミュレーションをエミュレートできます。
これは、ELF/MachOファイル形式とARMアセンブリを学ぶための教育用プロジェクトです。
自己責任で使用してください!
unidbgは Model Context Protocol (MCP) に対応しており、AI支援デバッグが可能です。デバッガがアクティブなときにコンソールで mcp と入力するとMCPサーバーが起動し、AIツール(例: Cursor)が接続できます。
unidbg MCP には2つの動作モードがあります:
モード1: ブレークポイントデバッグ — デバッガをアタッチしてコードを実行します。ブレークポイントにヒットすると、Breaker.debug() がエミュレータを一時停止します。コンソールで mcp と入力するとMCPサーバーが起動し、AIが解析を支援します。レジスタ、メモリ、逆アセンブル、ステップ実行、トレースなど、すべてのデバッグツールが利用できます。再開後、別のブレークポイントにヒットすると、デバッガは再び一時停止します。ブレークポイントにヒットせずに実行が完了すると、プロセスは終了しMCPもシャットダウンします。
Debugger debugger = emulator.attach();
debugger.addBreakPoint(address);
// run your emulation logic — debugger pauses when breakpoint is hit
モード2: カスタムツール(繰り返し可能) — McpToolkit を使用してカスタムツールを登録し、AIが異なるパラメータで対象関数を再実行できるようにします。ネイティブライブラリは一度だけロードされ、各実行後もプロセスは生存し続け、MCPは次の実行のためにアクティブなままです。
McpToolkit toolkit = new McpToolkit();
toolkit.addTool(new McpTool() {
@Override public String name() { return "encrypt"; }
@Override public String description() { return "Run encryption"; }
@Override public String[] paramNames() { return new String[]{"input"}; }
@Override public void execute(String[] params) {
String input = params.length > 0 ? params[0] : "default";
// call encryption with input
}
});
toolkit.run(emulator.attach());
デバッガが停止したら、コンソールで mcp と入力します(ポートを指定する場合は mcp 9239)。次に、Cursor の MCP 設定に追加します:
{
"mcpServers": {
"unidbg-mcp-server": {
"url": "http://localhost:9239/sse"
}
}
}
ステータスと情報
レジスタと逆アセンブル
メモリ
ブレークポイントと実行
トレース
| ツール | 説明 |
|---|---|
trace_code | レジスタ読み書き値(regs_read、prev_write)付きで命令をトレース |
trace_read / trace_write | アドレス範囲内のメモリ読み書きをトレース |
関数呼び出し
| ツール | 説明 |
|---|---|
call_function | 型付き引数(hex、string、bytes、null)でアドレス指定のネイティブ関数を呼び出します。シンボル解決とメモリプレビュー付きの値を返します |
call_symbol | モジュール + シンボル名でエクスポートされた関数を呼び出します。例: libc.so + malloc |
iOSのみ (Family=iOS の場合に利用可能)
| ツール | 説明 |
|---|---|
McpToolkit を使用して、それぞれが McpTool インターフェースを実装するカスタムツールを登録します。これにより、手動のif-else分岐が、クリーンで自己完結型のツールクラスに置き換わります。この時点でネイティブライブラリは完全にロードされており(JNI_OnLoad / エントリポイントは既に実行済み)、各ツールの execute() 内のコードが解析対象の関数ロジックになります。AIはカスタムツールを起動する前にブレークポイントやトレースを設定でき、プロセスを再起動することなく異なる入力に対する実行結果を検査できます。
Androidの例 — カスタムMCPツールを使用したAndroid JNIの例については、Utilities64.java を参照してください:
DalvikModule dm = vm.loadLibrary(new File("libtmessages.29.so"), true);
dm.callJNI_OnLoad(emulator);
cUtilities = vm.resolveClass("org/telegram/messenger/Utilities");
McpToolkit toolkit = new McpToolkit();
toolkit.addTool(new McpTool() {
@Override public String name() { return "aesCbc"; }
@Override public String description() { return "Run AES-CBC encryption on input data"; }
@Override public String[] paramNames() { return new String[]{"input"}; }
@Override public void execute(String[] params) {
byte[] input = params.length > 0 ? params[0].getBytes() : new byte[16];
aesCbcEncryptionByteArray(input);
}
});
toolkit.addTool(new McpTool() {
@Override public String name() { return "aesCtr"; }
@Override public String description() { return "Run AES-CTR decryption on input data"; }
@Override public String[] paramNames() { return new String[]{"input"}; }
@Override public void execute(String[] params) {
byte[] input = params.length > 0 ? params[0].getBytes() : new byte[16];
aesCtrDecryptionByteArray(input);
}
});
toolkit.addTool(new McpTool() {
@Override public String name() { return "pbkdf2"; }
@Override public String description() { return "Run PBKDF2 key derivation"; }
@Override public String[] paramNames() { return new String[]{"password", "iterations"}; }
@Override public void execute(String[] params) {
String password = params.length > 0 ? params[0] : "123456";
int iterations = params.length > 1 ? Integer.parseInt(params[1]) : 100000;
pbkdf2(password.getBytes(), iterations);
}
});
toolkit.run(emulator.attach());
iOSの例 — カスタムMCPツールを使用したiOS IPAロードの例については、IpaLoaderTest.java を参照してください:
IpaLoader ipaLoader = new IpaLoader64(ipa, new File("target/rootfs/ipa"));
LoadedIpa loader = ipaLoader.load(this);
emulator = loader.getEmulator();
loader.callEntry();
module = loader.getExecutable();
McpToolkit toolkit = new McpToolkit();
toolkit.addTool(new McpTool() {
@Override public String name() { return "dumpClass"; }
@Override public String description() { return "Dump an ObjC class definition by name"; }
@Override public String[] paramNames() { return new String[]{"className"}; }
@Override public void execute(String[] params) {
String className = params.length > 0 ? params[0] : "AppDelegate";
IClassDumper classDumper = ClassDumper.getInstance(emulator);
System.out.println("dumpClass(" + className + "):\n" + classDumper.dumpClass(className));
}
});
toolkit.addTool(new McpTool() {
@Override public String name() { return "readVersion"; }
@Override public String description() { return "Read the TelegramCoreVersionString from the executable"; }
@Override public void execute(String[] params) {
Symbol sym = module.findSymbolByName("_TelegramCoreVersionString");
if (sym != null) {
Pointer pointer = UnidbgPointer.pointer(emulator, sym.getAddress());
if (pointer != null) {
System.out.println("_TelegramCoreVersionString=" + pointer.getString(0));
}
}
}
});
toolkit.run(emulator.attach());
MCPサーバーが起動すると、AIはMCP経由でこれらのツールを呼び出し、カスタムパラメータでエミュレーションを実行したり、ブレークポイントを設定したり、実行をトレースしたり、結果を検査したりできます。プロセスを再起動する必要はありません。
低レベルAPI: 完全な制御が必要な場合は、
Debugger.addMcpTool()+Debugger.run(DebugRunnable)を直接使用することもできます。McpToolkitはif-else分岐を排除する高レベルラッパーです。
ゲスト側のメモリ割り当て(mmap/munmap/brk)を追跡して、エミュレートされたネイティブコードのリークを検出します。try-with-resources を使用してください。追跡は作成時に開始され、リークレポートはクローズ時に自動的に出力されます。
try (MemoryTracker tracker = emulator.traceMemoryLeaks()) {
module.callFunction(emulator, "targetFunction", arg1, arg2);
}
リークした各ブロックには、ゲストARMバックトレース(モジュール+オフセット+シンボル)とホストJavaスタックトレースが含まれます。出力例:
=== Memory Leak Report ===
Tracking duration: 42ms
Total allocations: 5
Total deallocations: 3
Leaked blocks: 2
Total leaked size: 32768 bytes (32.0 KB)
--- Leak #1 ---
Address: 0x40001000, Size: 16384 (16.0 KB), Perms: rw-
Guest Backtrace:
#0 0x40123456 libexample.so+0x3456 (malloc+0x12)
#1 0x40124000 libexample.so+0x4000 (doSomething+0x48)
Host Stack Trace:
com.github.unidbg.linux.AndroidElfLoader.mmap2(AndroidElfLoader.java:785)
...
レポートにはクローズ前にプログラムからアクセスすることもできます:
try (MemoryTracker tracker = emulator.traceMemoryLeaks()) {
module.callFunction(emulator, "targetFunction", arg1, arg2);
List<AllocationRecord> leaks = tracker.getLeaks();
assert leaks.isEmpty() : "Memory leak detected!";
}
複数のスレッド間でエミュレータインスタンスを再利用するためのスレッドセーフなオブジェクトプールです。繰り返し初期化のオーバーヘッドを回避します。
public class MyWorker implements Worker {
private final AndroidEmulator emulator;
public MyWorker() {
emulator = AndroidEmulatorBuilder.for64Bit().build();
// load .so, call JNI_OnLoad, etc.
}
@Override
public void destroy() {
emulator.close();
}
public byte[] doWork(byte[] input) {
// call native methods and return the result
}
}
// Create a worker pool (max = CPU cores, lazy-initialized)
WorkerPool pool = WorkerPoolFactory.create(MyWorker::new);
// Or specify max workers explicitly
// WorkerPool pool = WorkerPoolFactory.create(MyWorker::new, 4);
// Optional: customize idle timeout (default 10 minutes, minimum 1 minute)
pool.setIdleTimeout(30); // idle workers destroyed after 30 minutes
// Optional: customize minimum kept-alive workers (default 1, minimum 1)
pool.setMinIdle(2); // always keep at least 2 workers alive
// Optional: pre-create workers eagerly (default 0, fully lazy)
pool.setInitialSize(4); // eagerly create 4 workers on startup
// Concurrent invocation from multiple threads
ExecutorService executor = Executors.newFixedThreadPool(100);
for (int i = 0; i < 100; i++) {
executor.submit(() -> {
try (WorkerLoan<MyWorker> loan = pool.borrow(1, TimeUnit.MINUTES)) {
if (loan != null) {
byte[] result = loan.get().doWork(input);
}
} // worker is automatically returned to the pool
});
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.MINUTES);
pool.close(); // destroy all workers and release resources
完全な例については TTEncryptWorker.java を参照してください。
src/test ディレクトリにある簡単なテスト:

その他のテスト:
| ツール | 説明 |
|---|
check_connection | エミュレータの状態: Family、アーキテクチャ、バックエンド機能、isRunning、ロード済みモジュール |
list_modules / get_module_info | ロード済みモジュールの一覧を取得し、エクスポートされたシンボル数や依存関係を含む詳細を取得 |
list_exports | オプションのフィルタとC++デマングル付きで、モジュールのエクスポート/動的シンボルを一覧表示 |
find_symbol | 名前でシンボルを検索するか、アドレスから最も近いシンボルを検索 |
get_threads | エミュレータ内のすべてのスレッド/タスクを一覧表示 |
| ツール | 説明 |
|---|
get_registers / get_register / set_register | CPUレジスタの読み書き |
disassemble | 指定アドレスの命令を逆アセンブル(分岐先はシンボル名で自動注釈) |
assemble | 命令テキストをマシンコードにアセンブル |
get_callstack | 現在のコールスタック(バックトレース)を取得 |
| ツール | 説明 |
|---|
read_memory / write_memory | 生のメモリバイトを読み書き |
read_string / read_std_string | C文字列またはC++ std::stringを読み取り(SSO検出付き) |
read_pointer | シンボル解決付きでポインタチェーンを読み取り |
read_typed | メモリを型付き値(int8–int64、float、double、pointer)として読み取り |
search_memory | スコープ/権限フィルタ付きでメモリ内のバイトパターンを検索 |
list_memory_map | 権限付きで全メモリマッピングを一覧表示 |
allocate_memory / free_memory / list_allocations | オプションの初期データ付きでメモリブロックを確保(malloc/mmap)し、解放、追跡 |
patch | アセンブル済みの命令をメモリに書き込む |
| ツール | 説明 |
|---|
add_breakpoint / add_breakpoint_by_symbol / add_breakpoint_by_offset | アドレス、シンボル、またはモジュール+オフセットでブレークポイントを追加 |
remove_breakpoint / list_breakpoints | ブレークポイントの削除または一覧表示(逆アセンブル付き) |
continue_execution | 実行を再開します。poll_events を使用して breakpoint_hit または execution_completed を待機します |
step_over / step_into / step_out | ステップオーバー、ステップイン(N命令)、または関数からのステップアウト |
next_block | 次の基本ブロックで停止(Unicornのみ) |
step_until_mnemonic | ニーモニックに一致する次の命令で停止。例: bl、ret(Unicornのみ) |
poll_events | breakpoint_hit、execution_completed、トレースイベントをポーリング |
inspect_objc_msgobjc_msgSend呼び出しを検査: レシーバのクラス名とセレクタを表示。例: -[NSString length] |
get_objc_class_name | 指定されたアドレスのオブジェクトのObjCクラス名を取得(純粋なメモリ解析で、状態は変更しません) |
dump_objc_class | ObjCクラス定義(プロパティ、メソッド、プロトコル、ivars)をダンプ |
dump_gpb_protobuf | GPB protobufメッセージスキーマを.proto形式でダンプ(64ビットのみ) |