Unidbg: Black-Box Magic for Android Native Reversing

About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.

This research is published for educational and defensive purposes. The target sample is anonymized throughout.

Unidbg: Black-Box Magic for Android Native Reversing

Course banner

Course goals

  1. Understand Unicorn and Unidbg.
  2. Learn Unidbg’s common APIs from the source.
  3. Understand Unidbg hooks.
  4. Understand Unidbg patches.

Tools

  1. The tutorial demo
  2. IDEA
  3. IDA

Unicorn & Unidbg

What is Unicorn?

Unicorn

Unicorn is a CPU emulator framework open-sourced in 2015 by a team at Nanyang Technological University in Singapore. It supports multiple architectures — x86/x64, ARM/ARM64, MIPS, and more. Its main characteristics:

  1. Multi-architecture support: it emulates CPU instructions from different architectures, useful for cross-platform analysis and testing.
  2. High performance: Unicorn uses JIT compilation to translate emulated instructions into native host instructions, improving execution efficiency.
  3. Rich bindings: it ships bindings for Python, Java, C#, and more, so it’s convenient across programming environments.
  4. Hook and intercept support: you can set hooks during emulation to intercept specific instructions or memory accesses — very useful for reverse engineering and dynamic analysis.
  5. CPU-focused: unlike emulators that simulate both CPU and OS, Unicorn focuses on CPU instruction emulation, providing simple hook, memory, and instruction-execution interfaces without OS-level simulation.

GitHub: https://github.com/unicorn-engine/unicorn

What is Unidbg?

Unidbg (Unicorn Debugger) is an open-source, lightweight emulator designed mainly for emulating Android native code. It was open-sourced in 2019, is Maven-based, written in Java, and runs inside an IDE. Unidbg can emulate Android native functions, letting reverse engineers and security researchers analyze a binary’s runtime behavior. It supports emulated syscalls and JNI calls, so code that depends on them can run in an emulated environment. Built on Unicorn, Unidbg’s advantage is a covert monitoring mechanism that can emulate a complex native environment for deep dynamic analysis. As open source, it’s widely supported and actively maintained, and has become a powerful tool in Android native reversing.

Competitors: AndroidNativeEmu and its successor ExAndroidNativeEmu (Unidbg’s edge: it implements more syscalls and JNI).

GitHub: https://github.com/zhkl0228/unidbg

Use cases, pros and cons

Aspect Description
Use cases
Emulated execution Run the function you care about in a target SO and get results equivalent to a real device; replace Frida/Xposed call for RPC.
Monitoring Observe how the sample reads/modifies its environment; monitor all external accesses — syscalls, library calls, JNI calls, file I/O.
Algorithm analysis & recovery Provides hook/debug/trace capabilities; combined with a time-travel debugger, it’s a powerful analysis tool for Android native code.
Pros
Low cost Cuts device and spoofing cost; no need to buy/maintain real devices or rent cloud phones.
Flexibility Can emulate or proxy every function-call interface, making it easy to simulate device-environment changes.
Monitoring Watches detailed native execution flow, including JNI calls and file access.
Analysis With a time-travel debugger, offers strong algorithm analysis/recovery.
Cons
Steep learning curve Especially environment completion (“filling the environment”); done poorly, results are unusable.
Slow Unicorn-based emulation is much slower than a real device; Dynarmic etc. can speed it up but sacrifice some algorithm-recovery ability.
Feature limits No optimization for specific scenarios, no config management; not all syscalls are well emulated, so some logic may fail.
Poor extensibility As a Java project, Unidbg can’t be an IDA/Ghidra plugin and is hard to embed elsewhere, unlike a Python project.

First steps with Unidbg

Setting up

  1. Download IDEA (Community edition).
  2. Download the source, open it in IDEA, and configure the SDK.
  3. Project structure:
├── README.md                     # project intro and usage guide
├── LICENSE                       # open-source license
├── .gitignore                    # git ignore config
├── pom.xml                       # Maven config: dependencies and build
├── mvnw                          # Maven Wrapper script (Linux/Mac)
├── mvnw.cmd                      # Maven Wrapper script (Windows)
├── test.sh                       # test script (Linux/Mac)
├── test.cmd                      # test script (Windows)
├── .mvn/                         # Maven config directory
│   └── wrapper/                  # Maven Wrapper config
├── assets/                       # resource files used during emulation
│   ├── *.dll                     # Windows dynamic-link libraries
│   └── *.so                      # Linux/Android shared libraries
├── backend/                      # backend logic, core emulation
├── unidbg-api/                   # core interfaces and abstract classes
│   └── src/                      # API module source
├── unidbg-ios/                   # iOS emulation module
│   └── src/                      # iOS emulation source
├── unidbg-android/               # Android emulation module
│   ├── pom.xml                   # Maven build file
│   ├── pull.sh                   # pull Android emulation dependencies
│   └── src/                      # unidbg-android source
│       ├── main/
│       │   ├── java/             # core Java source
│       │   │   └── com/github/unidbg/  # core emulator, filesystem, VM components
│       │   │   └── net/fornwall/jelf   # ELF parsing
│       │   └── resources/        # resources: JNI libs, Android system libs
│       ├── test/
│       │   ├── java/             # unit tests
│       │   ├── native/android/   # C/C++ source for Android native tests
│       │   └── resources/        # test resources: prebuilt binaries (log4j.properties controls logging; can trace syscalls like open/syscall)
└── .mvn/                         # Maven Wrapper config
log4j.logger.com.github.unidbg.linux.file=DEBUG // change INFO to DEBUG

Learning by project

package com.example.test2;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.DynarmicFactory;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.DvmObject;
import com.github.unidbg.linux.android.dvm.ProxyDvmObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;

import java.io.File;

public class MainActivity {

    private final AndroidEmulator emulator; // define the Android emulator instance
    private final VM vm; // define the Dalvik VM instance

    public MainActivity() {
        // create a 32-bit Android emulator using the Dynarmic backend
        emulator = AndroidEmulatorBuilder.for32Bit()
                .addBackendFactory(new DynarmicFactory(true))
                .build();

        // get the emulator's memory API
        Memory memory = emulator.getMemory();
        // set the system library resolver
        LibraryResolver resolver = new AndroidResolver(23);
        memory.setLibraryResolver(resolver);

        // create the Dalvik VM instance
        vm = emulator.createDalvikVM();
        // set whether to print verbose JNI logs
        vm.setVerbose(false);

        // load the SO from the given path without auto-calling JNI_OnLoad
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/example_binaries/armeabi-v7a/libnative-lib.so"), false);
        // manually call JNI_OnLoad
        dm.callJNI_OnLoad(emulator);
    }

    public void crack() {
        // create a vm object that proxies a Java object into the JNI layer
        DvmObject<?> obj = ProxyDvmObject.createObject(vm, this);

        // record the start time
        long start = System.currentTimeMillis();

        // iterate all possible 3-char combinations
        for (char a : LETTERS) {
            for (char b : LETTERS) {
                for (char c : LETTERS) {
                    String str = "" + a + b + c;
                    // call the JNI method with the current combo
                    boolean success = obj.callJniMethodBoolean(emulator, "jnitest(Ljava/lang/String;)Z", str);
                    if (success) {
                        // on success, print and return
                        System.out.println("Found: " + str + ", off=" + (System.currentTimeMillis() - start) + "ms");
                        return;
                    }
                }
            }
        }
    }

    private static final char[] LETTERS = {
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
        'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
        'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
        'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
    }; // the alphabet

    public static void main(String[] args) {
        // record start time
        long start = System.currentTimeMillis();
        // create the MainActivity instance
        MainActivity main = new MainActivity();
        // print the result
        System.out.println("load offset=" + (System.currentTimeMillis() - start) + "ms");
        // call the crack method
        main.crack();
    }
}
emulator = AndroidEmulatorBuilder.for32Bit()
            .addBackendFactory(new DynarmicFactory(true))
            .build();
  • for32Bit() creates a 32-bit Android emulator; for64Bit() creates a 64-bit one. If the APK only ships armeabi-v7a, you must use 32-bit; if only arm64-v8a, use 64-bit. Difference: 64-bit runs ~10% faster; Unidbg supports/refines ARM32 more than ARM64.
  • Unidbg supports several backends — currently five: Unicorn, Unicorn2, Dynarmic (faster), Hypervisor, KVM. The true in new DynarmicFactory(true) means fall back to the default Unicorn backend on exception.
.setRootDir() // set the VM's root directory for IO redirection; e.g. if the app reads /data/data/123.txt and rootDir is E:/unidbg, the real path is E:/unidbg/data/data/123.txt

Common emulator APIs

Method Return type Description
getMemory() Memory Get the memory API.
getPid() int Get the process PID.
createDalvikVM() VM Create a VM.
createDalvikVM(File apkFile) VM Create a VM with a specified APK path.
getDalvikVM() VM Get the created VM.
showRegs() void Show register state (optionally specific registers).
getBackend() Backend Get the backend CPU.
getProcessName() String Get the process name.
getContext() RegisterContext Get the register context.
traceRead(long begin, long end) void Trace memory reads.
traceWrite(long begin, long end) void Trace memory writes.
traceCode(long begin, long end) void Trace assembly execution.
isRunning() boolean Whether the emulator is running.
    LibraryResolver resolver = new AndroidResolver(23);
    memory.setLibraryResolver(resolver);

23 and 19 correspond to SDK 23 (Android 6.0) and SDK 19 (Android 4.4) runtime-library environments. For 64-bit SOs you can only pick SDK 23.

SDK resolver

Common memory APIs

Method Return type Description
setLibraryResolver(AndroidResolver resolver) void Set the Android SDK version resolver (supports 19 and 23).
getStackPoint() long Get the current stack pointer.
pointer(long address) UnidbgPointer Get a pointer to an address; manipulate memory through it.
getMemoryMap() Collection<MemoryMap> Get the current memory map.
findModule(String moduleName) Module Find a module by name.
findModuleByAddress(long address) Module Find a module by address.
loadLibrary(File file, boolean forceLoad) ElfModule Load an SO, calling Linker.do_dlopen().
allocatestack(int size) UnidbgPointer Allocate stack memory of a given size.
writestackstring(String value) UnidbgPointer Write a string to stack memory.
writestackBytes(byte[] value) UnidbgPointer Write a byte array to stack memory.
malloc(int size, boolean runtime) UnidbgPointer Allocate memory of a given size; return a pointer to it.
	// create the Dalvik VM instance
	vm = emulator.createDalvikVM();
	// set whether to print verbose JNI logs
	vm.setVerbose(false);
	// load the SO from the given path without auto-calling JNI_OnLoad
	DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/example_binaries/armeabi-v7a/libnative-lib.so"), false);
	// manually call JNI_OnLoad
	dm.callJNI_OnLoad(emulator);
Method Return type Description
createDalvikVM(File apkFile) VM Create a VM with a specified APK (file may be null).
setVerbose(boolean verbose) void Toggle JNI log output.
loadLibrary(File soFile, boolean callInit) DalvikModule Load an SO; second arg controls auto-calling the init function.
setJni(Jni jni) void Set the JNI interface; prefer implementing AbstractJni.
getJNIEnv() Pointer Get the JNIEnv pointer (passable as an argument).
getJavaVM() Pointer Get the JavaVM pointer (passable as an argument).
callJNI_OnLoad(Emulator<?> emulator, Module module) void Call JNI_OnLoad.
addGlobalObject(DvmObject<?> obj) int Add a global object; return its hash.
addLocalObject(DvmObject<?> obj) int Add a local object; return its hash.
getObject(int hash) DvmObject<?> Get an object by hash.
resolveClass(String className) DvmClass Resolve a class name into a DvmClass.
getPackageName() String Get the APK package name.
getVersionName() String Get the APK version name.
getVersionCode() String Get the APK version code.
openAsset(String assetName) InputStream Open an APK asset.
getManifestXml() String Get the AndroidManifest.xml text.
getSignatures() CertificateMeta[] Get APK signing info.
findClass(String className) DvmClass Get an already-loaded class by name.
getEmulator() Emulator<?> Get the emulator object.
  • VM dalvikVM = emulator.createDalvikVM(new File("apk file path")) — create a VM with an APK; Unidbg does some setup for you, like parsing APK basics (version name/code, package name, signatures), reducing environment work, and loading APK resources so you can openAsset files from assets/.
  • loadLibrary has three overloads:
/**
 * Load a library by name.
 * @param libname library name, excluding the "lib" prefix and ".so" suffix (e.g. "example" -> "libexample.so").
 * @param forceCallInit whether to force-call the library's init function (like JNI_OnLoad).
 * @return the loaded DalvikModule.
 */
DalvikModule loadLibrary(String libname, boolean forceCallInit);

/**
 * Load a library from a raw byte array.
 * @param libname library name, only used as an identifier.
 * @param raw the buffer, for easier ELF parsing.
 */
DalvikModule loadLibrary(String libname, byte[] raw, boolean forceCallInit);

/**
 * Load an ELF from a path.
 * @param elfFile a valid ELF file, e.g. new File("unidbg-android/src/test/resources/example_binaries/armeabi-v7a/libnative-lib.so")
 * @param forceCallInit whether to force-call the library's init function (like JNI_OnLoad).
 */
DalvikModule loadLibrary(File elfFile, boolean forceCallInit);

Calling JNI functions

JNI calls

// 1st arg: the emulator instance
// 2nd arg: the Java-layer method signature to call
// 3rd arg: varargs; older versions required lots of manual wrapping, newer versions handle it for you — see callJniMethod
boolean success = obj.callJniMethodBoolean(emulator, "jnitest(Ljava/lang/String;)Z", str);

/**
 * Helper to call a JNI method.
 *
 * @param emulator   the emulator instance.
 * @param vm         the Dalvik VM instance.
 * @param objectType the class of the method (DvmClass).
 * @param thisObj    the object instance (DvmObject).
 * @param method     the method signature (e.g. "methodName(paramTypes)returnType").
 * @param args       varargs.
 * @return           the return value (Number), integer or float.
 */
protected static Number callJniMethod(Emulator<?> emulator, VM vm, DvmClass objectType, DvmObject<?> thisObj, String method, Object... args) {
    // find the native function pointer
    UnidbgPointer fnPtr = objectType.findNativeFunction(emulator, method);
    // add the object to the local reference table to avoid GC
    vm.addLocalObject(thisObj);
    // build the argument list
    List<Object> list = new ArrayList<>(10);
    // add the JNI env pointer (JNIEnv*)
    list.add(vm.getJNIEnv());
    // add the this reference (jobject)
    list.add(thisObj.hashCode());
    // process the varargs
    if (args != null) {
        for (Object arg : args) {
            if (arg instanceof Boolean) {
                // boolean -> JNI_TRUE / JNI_FALSE
                list.add((Boolean) arg ? VM.JNI_TRUE : VM.JNI_FALSE);
                continue;
            } else if (arg instanceof Hashable) {
                // Hashable means DvmObject or subclass
                list.add(arg.hashCode()); // add object reference (jobject)
                if (arg instanceof DvmObject) {
                    // add DvmObject to the local reference table
                    vm.addLocalObject((DvmObject<?>) arg);
                }
                continue;
            } else if (arg instanceof DvmAwareObject ||
                    arg instanceof String ||
                    arg instanceof byte[] ||
                    arg instanceof short[] ||
                    arg instanceof int[] ||
                    arg instanceof float[] ||
                    arg instanceof double[] ||
                    arg instanceof Enum) {
                // DvmAwareObject, string, array, or enum -> create a proxy DvmObject
                DvmObject<?> obj = ProxyDvmObject.createObject(vm, arg);
                // add object reference (jobject)
                list.add(obj.hashCode());
                // add to the local reference table
                vm.addLocalObject(obj);
                continue;
            }

            // other types are added directly
            list.add(arg);
        }
    }
    // call the native function with the arg list and return the result
    return Module.emulateFunction(emulator, fnPtr.peer, list.toArray());
}

Primitive types pass through directly — int, long, boolean, double, etc.

Unidbg also wraps these object types for you:

  • String
  • byte array
  • short array
  • int array
  • float array
  • double array
  • Enum

Constructing special arguments

For other data types, use resolveClass to construct them — e.g. Context:

DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);

Symbol calls vs offset calls

Symbol call

Symbol symbol = module.findSymbolByName("exported symbol");
if (symbol != null){
	// 1st: emulator instance, 2nd: jnienv, 3rd: jclass, 4th: varargs
    Number numbers = symbol.call(emulator, vm.getJNIEnv(), vm.addLocalObject(instanceClass), varargs);
    int result = numbers.intValue();
    System.out.println(result);
    // if the return value is a string, use vm.getObject(retval)
    System.out.println(vm.getObject(result).getValue());
}else {
    System.out.println("symbol not found");
}

Offset call

// 1st: emulator instance, 2nd: offset address (add +1 for Thumb), 3rd: jnienv, 4th: jclass, 5th: varargs
Number number = module.callFunction(emulator, 0x11240, vm.getJNIEnv(),vm.addLocalObject(SecurityUtils),vm.addLocalObject(new StringObject(vm, "super")));
DvmObject<?> object = vm.getObject(number.intValue());
System.out.println("result:" + object.getValue());

Hooking in Unidbg

Category Description Pros Cons
Built-in third-party hook frameworks Dobby (formerly HookZz), Whale, xHook, etc. HookZz is better for 32-bit, Dobby for 64-bit; xHook can’t hook Sub_xxx sub-functions. Rich features: multiple hook modes like inline hook and PLT hook. Easy to use: concise API. Detectable: some apps detect these frameworks. Limitations: inline hook can fail on short or adjacent functions; PLT hook can’t hook non-exported functions.
Native Unicorn-engine hooks Instruction-level, block-level, memory read/write, and exception hooks via Unicorn; Unidbg also wraps a Console Debugger. Stealthier: native hooks are harder to detect. Flexible: hook any code location. More complex: requires understanding the low-level mechanism; harder to configure.

HookZz & Dobby

// get the HookZz instance for later hooking
IHookZz hookZz = HookZz.getInstance(emulator);
// inline-hook the exported function "ss_encrypt" with HookZz.wrap
hookZz.wrap(module.findSymbolByName("ss_encrypt"), new WrapCallback<RegisterContext>() {
    @Override
    public void preCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
        // runs before the function call
        // get the pointer of the 3rd argument
        Pointer pointer = ctx.getPointerArg(2);
        // get the integer value of the 4th argument
        int length = ctx.getIntArg(3);
        // read the memory the pointer points to and get the key data
        byte[] key = pointer.getByteArray(0, length);
        // use Inspector to dump the key for debugging
        Inspector.inspect(key, "ss_encrypt key");
    }

    @Override
    public void postCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
        // runs after the function call
        // print the return value
        System.out.println("ss_encrypt.postCall R0=" + ctx.getLongArg(0));
    }
});

1. Get the HookZz instance:

IHookZz hookZz = HookZz.getInstance(emulator);

2. wrap hook:

hookZz.wrap(functionAddress, new WrapCallback<RegisterContext>() {
    @Override
    public void preCall(Emulator<?> emulator, RegisterContext context, HookEntryInfo info) {
        // before the call
        UnidbgPointer input = ctx.getPointerArg(2);
			System.out.println("preCall:"+vm.getObject(input.toIntPeer()));

    }

    @Override
    public void postCall(Emulator<?> emulator, RegisterContext context, HookEntryInfo info) {
        // after the call
        // get the integer return value
        System.out.println("postCall:" + ctx.getLongArg(0));
        // get the jstr
			UnidbgPointer input = ctx.getPointerArg(0);
			System.out.println("postCall:"+vm.getObject(input.toIntPeer()));
    }
});

wrap has two overloads — by symbol or by address. They’re equivalent; symbol lookup ends up calling symbol.getAddress() anyway. WrapCallback’s generic interface has three variants: RegisterContext (function hook), HookZzArm32RegisterContext (ARM32), and HookZzArm64RegisterContext (ARM64). Because they can read register values, they’re suitable for inline hooks.

HookZzArm32RegisterContext

HookZzArm64RegisterContext reads registers via the following methods:

HookZzArm64RegisterContext

3. instrument inline hook

// inline-hook the instruction at an address with HookZz.instrument; add +1 for Thumb
hookZz.instrument(module.base + 0x11470, new InstrumentCallback<Arm64RegisterContext>() {
    @Override
    public void dbiCall(Emulator<?> emulator, Arm64RegisterContext ctx, HookEntryInfo info) {
	    // runs when the specified instruction executes
        // print the W0 register value
        System.out.println("W0=" + ctx.getXInt(0));
    }
});

4. replace

// use Dobby.replace to replace the implementation of "Java_com_zj_wuaipojie_util_SecurityUtil_diamondNum"
Dobby dobby = Dobby.getInstance(emulator);
dobby.replace(module.findSymbolByName("Java_com_zj_wuaipojie_util_SecurityUtil_diamondNum"), new ReplaceCallback() { // Dobby inline-hooks the exported function
    @Override
    public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
		// replace with an integer
        return HookStatus.LR(emulator,888888);
        // string
        return HookStatus.LR(emulator, vm.addLocalObject(new StringObject(vm, "super")));
    }
});

Unicorn hooks

emulator.getBackend().hook_add_new(new CodeHook() {
    @Override
    public void hook(Backend backend, long address, int size, Object user) {
        Arm64RegisterContext context = emulator.getContext();
        if (address == module.base + 0x11330) {
            int x0=emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X0).intValue();
            System.out.println("x0:"+vm.getObject(x0));
            emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_X0,vm.addLocalObject(new StringObject(vm, "super")));
        }
    }
    @Override
    public void onAttach(UnHook unHook) {
    }
    @Override
    public void detach() {
    }
}, module.base + 0x11240,module.base + 0x11340,null);
StringObject Result = SecurityUtils.callStaticJniMethodObject(emulator, "vipLevel(Ljava/lang/String;)Ljava/lang/String;", "123");
System.out.println("Result: " + Result.getValue());

Console Debugger

The Console Debugger is a powerful Unidbg tool that lets you set breakpoints, single-step, and inspect/modify memory and registers during emulation, for deep analysis of target behavior.

Debugger attach = emulator.attach();
attach.addBreakPoint(module.base + 0x11070); // breakpoint address
// call
Number number = module.callFunction(emulator, 0x11014, vm.getJNIEnv(),vm.addLocalObject(SecurityUtils),vm.addLocalObject(new StringObject(vm, "123456")));
System.out.println("result:" + number.intValue());

Replacing the return value

emulator.attach().addBreakPoint(module.findSymbolByName("verifyApkSign").getAddress(), new BreakPointCallback() {
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            System.out.println("replacing function verifyApkSign");
            RegisterContext registerContext = emulator.getContext();
            emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC, registerContext.getLRPointer().peer);
            emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0, 0);
            return true;
        }
    });
Command Description
c Continue execution
n Step over the current instruction
bt Backtrace
st hex Search the stack
shw hex Search the writable heap
shr hex Search the readable heap
shx-hex Search the executable heap
nb Break at the next block
s Step into the current instruction
s [decimal] Execute N instructions
s (blx) Execute until a blx mnemonic (slower)
m (op) [size] Show memory; default size 0x70, hex or decimal
mr0-mr7, mfp, mip, msp [size] Show memory at the given register
m (address) [size] Show memory at an address (must start with 0x)
wr0-wr7, wfp, wip, wsp <value> Write to the given register
wb(address), ws(address), wi(address) <value> Write (byte/short/int) to an address (must start with 0x)
wx (address) <hex> Write bytes to an address (must start with 0x)
b (address) Add a temporary breakpoint (must start with 0x; may be a module offset)
b Add a breakpoint at register PC
r Remove the breakpoint at register PC
blr Add a temporary breakpoint at register LR
p (assembly) Patch an assembly instruction at PC
where Show the Java stack trace
trace [begin-end] Set instruction tracing
traceRead [begin-end] Set memory-read tracing
traceWrite [begin-end] Set memory-write tracing
vm List loaded modules
vbs List breakpoints
d Show disassembly
d (0x) Show disassembly at an address
stop Stop emulation
run [arg] Run a test
gc Run System.gc()
threads List threads
cc size Convert an address range’s assembly to a C function

Patching in Unidbg

Patch is modifying the binary directly. Fundamentally there are two forms:

  • patching the binary file;
  • patching in memory.

Patching has many uses, and in some cases works better than hooking — hence this section. File patching is what most people know (the KeyPatch experience in IDA is friendly). Here I’ll focus on Unidbg’s in-memory patch, using the Keystone assembler:

UnidbgPointer pointer = UnidbgPointer.pointer(emulator,module.base + 0x1146C);
// before patching, make sure you've correctly located the target address and its instruction set (ARM or Thumb)
// for 32-bit, use: Keystone keystone = new Keystone(KeystoneArchitecture.Arm, KeystoneMode.ArmThumb);
Keystone keystone = new Keystone(KeystoneArchitecture.Arm64, KeystoneMode.LittleEndian);
String s = "MOV W0, #0x99";  // the assembly instruction to write
byte[] machineCode = keystone.assemble(s).getMachineCode();   // assemble to machine code
pointer.write(machineCode); // write to memory

Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).

Get in touch