
Permet d'émuler une bibliothèque native Android, ainsi qu'une émulation iOS expérimentale.
Permet d'émuler une bibliothèque native Android, ainsi qu'une émulation iOS expérimentale.
Ceci est un projet pédagogique pour en apprendre davantage sur les formats de fichiers ELF/MachO et l'assembleur ARM.
À utiliser à vos risques et périls !
unidbg prend en charge le Model Context Protocol (MCP) pour le débogage assisté par IA. Lorsque le débogueur est actif, tapez mcp dans la console pour démarrer un serveur MCP auquel les outils d'IA (ex. Cursor) peuvent se connecter.
Le MCP d'unidbg a deux modes de fonctionnement :
Mode 1 : Débogage par point d'arrêt — Attachez le débogueur et exécutez votre code. Lorsqu'un point d'arrêt est atteint, Breaker.debug() met l'émulateur en pause — tapez mcp dans la console pour démarrer le serveur MCP et laisser l'IA vous assister dans l'analyse. Tous les outils de débogage sont disponibles (registres, mémoire, désassemblage, pas à pas, traçage, etc.). Après la reprise, si un autre point d'arrêt est atteint, le débogueur se remet en pause. Une fois l'exécution terminée sans atteindre de point d'arrêt, le processus se termine et le MCP s'arrête.
Debugger debugger = emulator.attach();
debugger.addBreakPoint(address);
// run your emulation logic — debugger pauses when breakpoint is hit
Mode 2 : Outils personnalisés (réutilisables) — Utilisez McpToolkit pour enregistrer des outils personnalisés et laisser l'IA réexécuter les fonctions cibles avec différents paramètres. La bibliothèque native est chargée une seule fois ; après chaque exécution, le processus reste actif et le MCP demeure disponible pour l'exécution suivante.
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());
Lorsque le débogueur s'arrête, tapez mcp (ou mcp 9239 pour spécifier le port) dans la console. Ajoutez ensuite aux paramètres MCP de Cursor :
{
"mcpServers": {
"unidbg-mcp-server": {
"url": "http://localhost:9239/sse"
}
}
}
Statut et informations
Registres et désassemblage
Mémoire
Points d'arrêt et exécution
Traçage
| Outil | Description |
|---|---|
trace_code | Tracer les instructions avec les valeurs de lecture/écriture des registres (regs_read, prev_write) |
trace_read / trace_write | Tracer les lectures/écritures mémoire dans une plage d'adresses |
Appels de fonctions
| Outil | Description |
|---|---|
call_function | Appeler une fonction native par adresse avec des arguments typés (hex, chaîne, octets, null). Renvoie la valeur avec résolution de symboles et aperçu mémoire |
call_symbol | Appeler une fonction exportée par module + nom de symbole, ex. libc.so + malloc |
iOS uniquement (disponible lorsque Family=iOS)
Utilisez McpToolkit pour enregistrer des outils personnalisés, chacun implémentant l'interface McpTool. Cela remplace la répartition manuelle if-else par des classes d'outils propres et autonomes. À ce stade, la bibliothèque native est entièrement chargée (JNI_OnLoad / point d'entrée déjà exécutés), donc le code dans le execute() de chaque outil correspond à la logique de la fonction cible à analyser. L'IA peut définir des points d'arrêt et des traces avant de déclencher un outil personnalisé, puis inspecter les résultats d'exécution avec différentes entrées sans redémarrer le processus.
Exemple Android — Voir Utilities64.java pour un exemple JNI Android avec des outils MCP personnalisés :
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());
Exemple iOS — Voir IpaLoaderTest.java pour un exemple de chargement d'IPA iOS avec des outils MCP personnalisés :
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());
Une fois le serveur MCP démarré, l'IA peut appeler ces outils via MCP pour exécuter des émulations avec des paramètres personnalisés, définir des points d'arrêt, tracer l'exécution et inspecter les résultats — le tout sans redémarrer le processus.
API de bas niveau : Vous pouvez également utiliser
Debugger.addMcpTool()+Debugger.run(DebugRunnable)directement pour un contrôle total.McpToolkitest une surcouche de plus haut niveau qui élimine la répartition if-else.
Suivez les allocations mémoire côté invité (mmap/munmap/brk) pour détecter les fuites dans le code natif émulé. Utilisez try-with-resources — le suivi démarre à la création, et le rapport de fuites est imprimé automatiquement à la fermeture.
try (MemoryTracker tracker = emulator.traceMemoryLeaks()) {
module.callFunction(emulator, "targetFunction", arg1, arg2);
}
Chaque bloc fuyé inclut une backtrace ARM invitée (module+offset+symbole) et une trace de pile Java hôte. Exemple de sortie :
=== 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)
...
Vous pouvez également accéder au rapport par programmation avant la fermeture :
try (MemoryTracker tracker = emulator.traceMemoryLeaks()) {
module.callFunction(emulator, "targetFunction", arg1, arg2);
List<AllocationRecord> leaks = tracker.getLeaks();
assert leaks.isEmpty() : "Memory leak detected!";
}
Un pool d'objets thread-safe pour réutiliser des instances d'émulateur sur plusieurs threads, évitant ainsi la surcharge d'une initialisation répétée.
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
Voir TTEncryptWorker.java pour un exemple complet.
Tests simples sous le répertoire src/test :

Autres tests :
| Outil | Description |
|---|
check_connection | Statut de l'émulateur : famille, architecture, capacités du backend, isRunning, modules chargés |
list_modules / get_module_info | Lister les modules chargés, obtenir les détails incluant le nombre de symboles exportés et les dépendances |
list_exports | Lister les symboles exportés/dynamiques d'un module avec filtre optionnel et démangling C++ |
find_symbol | Trouver un symbole par nom ou trouver le symbole le plus proche d'une adresse |
get_threads | Lister tous les threads/tâches de l'émulateur |
| Outil | Description |
|---|
get_registers / get_register / set_register | Lire/écrire les registres CPU |
disassemble | Désassembler les instructions à une adresse (les cibles de branchement sont automatiquement annotées avec les noms de symboles) |
assemble | Assembler le texte d'une instruction en code machine |
get_callstack | Obtenir la pile d'appels actuelle (backtrace) |
| Outil | Description |
|---|
read_memory / write_memory | Lire/écrire des octets mémoire bruts |
read_string / read_std_string | Lire une chaîne C ou une std::string C++ (avec détection SSO) |
read_pointer | Lire une chaîne de pointeurs avec résolution de symboles |
read_typed | Lire la mémoire sous forme de valeurs typées (int8–int64, float, double, pointeur) |
search_memory | Rechercher des motifs d'octets en mémoire avec filtres de portée/permissions |
list_memory_map | Lister tous les mappings mémoire avec leurs permissions |
allocate_memory / free_memory / list_allocations | Allouer (malloc/mmap) avec données initiales optionnelles, libérer et suivre les blocs mémoire |
patch | Écrire des instructions assemblées en mémoire |
| Outil | Description |
|---|
add_breakpoint / add_breakpoint_by_symbol / add_breakpoint_by_offset | Ajouter des points d'arrêt par adresse, symbole ou module+offset |
remove_breakpoint / list_breakpoints | Supprimer ou lister les points d'arrêt (avec désassemblage) |
continue_execution | Reprendre l'exécution. Utilisez poll_events pour attendre breakpoint_hit ou execution_completed |
step_over / step_into / step_out | Pas à pas au-dessus, à l'intérieur (N instructions) ou à l'extérieur d'une fonction |
next_block | S'arrêter au prochain bloc de base (Unicorn uniquement) |
step_until_mnemonic | S'arrêter à la prochaine instruction correspondant au mnémonique, ex. bl, ret (Unicorn uniquement) |
poll_events | Interroger les événements breakpoint_hit, execution_completed, trace |
| Outil | Description |
|---|
inspect_objc_msg | Inspecter un appel objc_msgSend : afficher le nom de la classe du récepteur et le sélecteur, ex. -[NSString length] |
get_objc_class_name | Obtenir le nom de classe ObjC d'un objet à une adresse donnée (pur parsing mémoire, sans changement d'état) |
dump_objc_class | Extraire la définition d'une classe ObjC (propriétés, méthodes, protocoles, ivars) |
dump_gpb_protobuf | Extraire le schéma d'un message protobuf GPB au format .proto (64 bits uniquement) |