
Android 네이티브 라이브러리를 에뮬레이션할 수 있으며, 실험적인 iOS 에뮬레이션도 지원합니다.
Android 네이티브 라이브러리 에뮬레이션을 지원하며, 실험적인 iOS 에뮬레이션도 지원합니다.
ELF/MachO 파일 형식과 ARM 어셈블리에 대해 더 배우기 위한 교육용 프로젝트입니다.
사용에 따른 책임은 본인에게 있습니다!
unidbg는 AI 지원 디버깅을 위해 Model Context Protocol (MCP)를 지원합니다. 디버거가 활성화되면 콘솔에 mcp를 입력하여 AI 도구(예: Cursor)가 연결할 수 있는 MCP 서버를 시작합니다.
unidbg MCP에는 두 가지 작동 모드가 있습니다.
모드 1: 중단점 디버그 — 디버거를 연결하고 코드를 실행합니다. 중단점에 도달하면 Breaker.debug()가 에뮬레이터를 일시 중지합니다. 콘솔에 mcp를 입력하여 MCP 서버를 시작하고 AI가 분석을 도와주게 하세요. 모든 디버깅 도구(레지스터, 메모리, 디스어셈블리, 스텝, 트레이스 등)를 사용할 수 있습니다. 재개한 후 다른 중단점에 도달하면 디버거가 다시 일시 중지됩니다. 중단점에 도달하지 않고 실행이 완료되면 프로세스가 종료되고 MCP가 종료됩니다.
Debugger debugger = emulator.attach();
debugger.addBreakPoint(address);
// 에뮬레이션 로직 실행 — 중단점에 도달하면 디버거가 일시 중지됩니다.
모드 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";
// 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();
// .so 로드, JNI_OnLoad 호출 등
}
@Override
public void destroy() {
emulator.close();
}
public byte[] doWork(byte[] input) {
// 네이티브 메서드 호출 및 결과 반환
}
}
// 워커 풀 생성 (max = CPU 코어 수, 지연 초기화)
WorkerPool pool = WorkerPoolFactory.create(MyWorker::new);
// 또는 최대 워커 수를 명시적으로 지정
// WorkerPool pool = WorkerPoolFactory.create(MyWorker::new, 4);
// 선택 사항: 유휴 타임아웃 사용자 지정 (기본 10분, 최소 1분)
pool.setIdleTimeout(30); // 30분 후 유휴 워커 제거
// 선택 사항: 최소 유지 워커 수 사용자 지정 (기본 1, 최소 1)
pool.setMinIdle(2); // 항상 최소 2개의 워커 유지
// 선택 사항: 워커를 미리 생성 (기본 0, 완전 지연)
pool.setInitialSize(4); // 시작 시 워커 4개 미리 생성
// 여러 스레드에서 동시 호출
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);
}
} // 워커는 풀에 자동으로 반환됩니다
});
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.MINUTES);
pool.close(); // 모든 워커 제거 및 리소스 해제
전체 예제는 TTEncryptWorker.java를 참조하세요.
src/test 디렉토리의 간단한 테스트:

추가 테스트:
| 도구 | 설명 |
|---|
check_connection | 에뮬레이터 상태: Family, 아키텍처, 백엔드 기능, isRunning, 로드된 모듈 |
list_modules / get_module_info | 로드된 모듈 목록, 내보낸 심볼 수 및 종속성을 포함한 세부 정보 |
list_exports | 선택적 필터 및 C++ demangling을 포함한 모듈의 내보낸/동적 심볼 목록 |
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 | 실행 재개. breakpoint_hit 또는 execution_completed를 기다리려면 poll_events 사용 |
step_over / step_into / step_out | 함수 step over, into (N개 명령어) 또는 out |
next_block | 다음 기본 블록에서 중단 (Unicorn 전용) |
step_until_mnemonic | 니모닉과 일치하는 다음 명령어(예: bl, ret)에서 중단 (Unicorn 전용) |
poll_events | breakpoint_hit, execution_completed, trace 이벤트 폴링 |
inspect_objc_msgobjc_msgSend 호출 검사: receiver 클래스 이름과 셀렉터 표시, 예: -[NSString length] |
get_objc_class_name | 주어진 주소의 객체 ObjC 클래스 이름 가져오기 (순수 메모리 파싱, 상태 변경 없음) |
dump_objc_class | ObjC 클래스 정의 덤프 (프로퍼티, 메서드, 프로토콜, ivars) |
dump_gpb_protobuf | GPB protobuf 메시지 스키마를 .proto 형식으로 덤프 (64비트 전용) |