Bypassing libmsaoaidsec.so Frida Anti-Debugging in an Unpacked App

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.

Bypassing libmsaoaidsec.so Frida Anti-Debugging in an Unpacked App

How it works

A lot of apps these days integrate libmsaoaidsec.so to implement Frida anti-debugging. The core logic: during startup, when libmsaoaidsec.so is loaded, it spins up an independent thread via pthread_create() and runs the anti-debug function inside that thread, actively scanning for Frida processes, ports, and in-memory signatures. If Frida is detected, it kills or crashes the process.

The bypass can go after the anti-debug thread itself — either its loading or its execution. The article tests different apps, but the idea is the same for any app that uses libmsaoaidsec.so for anti-debugging.

Locating the checks

Hook dlopen() to see, by the last SO loaded, which SO the program exits right after loading.

function hook_dlopen(){
    // on Android 8.0+ SOs are loaded via android_dlopen_ext
    var android_dlopen_ext = Module.findExportByName(null,"android_dlopen_ext");
    console.log("addr_android_dlopen_ext",android_dlopen_ext);
    Interceptor.attach(android_dlopen_ext,{
        onEnter:function(args){
            var pathptr = args[0];
            if(pathptr!=null && pathptr != undefined){
                var path = ptr(pathptr).readCString();
                console.log("android_dlopen_ext:",path);
            }
        },
        onLeave:function(retvel){
            console.log("leave!");
        }
    })
}
hook_dlopen()

As shown below, the program exits right after libmsaoaidsec.so finishes loading.

The app exits after libmsaoaidsec.so loads

Next we pick a moment — when libmsaoaidsec.so is loading — to print the threads it creates.

function hook_dlopen(){
    var android_dlopen_ext = Module.findExportByName(null,"android_dlopen_ext");
    console.log("addr_android_dlopen_ext",android_dlopen_ext);
    Interceptor.attach(android_dlopen_ext,{
        onEnter:function(args){
            var pathptr = args[0];
            if(pathptr!=null && pathptr != undefined){
                var path = ptr(pathptr).readCString();
                if(path.indexOf("libmsaoaidsec.so")!=-1){
                    console.log("android_dlopen_ext:",path);
                    hook_pthread()
                }
            }
        },
        onLeave:function(retvel){

        }
    })
}

function hook_pthread() {
    var pth_create = Module.findExportByName("libc.so", "pthread_create");
    console.log("[pth_create]", pth_create);
    Interceptor.attach(pth_create, {
        onEnter: function (args) {
            var module = Process.findModuleByAddress(args[2]);
            if (module != null) {
                console.log("address", module.name, args[2].sub(module.base));
            }
        },
        onLeave: function (retval) {}
    });
}

function main(){
    hook_dlopen()
}

main()

Here’s the output. If you’re curious, decompile libmsaoaidsec.so in IDA, jump to those addresses, and see what each function does.

The anti-debug thread addresses

Bypassing the anti-debugging

Now that we know which functions detect Frida, the key thing is to find a moment to replace or nop those functions.

If you analyze them in IDA — jump to those addresses and follow cross-references up to their callers — you’ll find they all converge on the init_proc function. That’s an initialization function called automatically when the SO loads, and it’s the very first thing to run during SO loading. So we need a moment earlier than init_proc to replace or nop the Frida-detection functions.

During the SO link process, the linker’s call_constructors() function triggers the constructors, registers the init functions, then runs them. The registered init functions live in the .init_array list. We can hook call_constructors() and, at that point, dynamically rewrite the addresses of the Frida-detection functions in the SO to point at our own empty function.

Approach 1: replacing the functions

function hook_dlopen(){
    // on Android 8.0+ SOs are loaded via android_dlopen_ext
    var android_dlopen_ext = Module.findExportByName(null,"android_dlopen_ext");
    console.log("addr_android_dlopen_ext",android_dlopen_ext);
    Interceptor.attach(android_dlopen_ext,{
        onEnter:function(args){
            var pathptr = args[0];
            if(pathptr!=null && pathptr != undefined){
                var path = ptr(pathptr).readCString();
                if(path.indexOf("libmsaoaidsec.so")!=-1){
                    console.log("android_dlopen_ext:",path);
                    hook_call_constructors()
                }
            }
        },
        onLeave:function(retvel){
            //console.log("leave!");
        }
    })
}

function hook_call_constructors() {
    let linker = null;
    if (Process.pointerSize === 4) {
        linker = Process.findModuleByName("linker");
    } else {
        linker = Process.findModuleByName("linker64");
    }
    let call_constructors_addr, get_soname
    let symbols = linker.enumerateSymbols();
    for (let index = 0; index < symbols.length; index++) {
        let symbol = symbols[index];
        if (symbol.name === "__dl__ZN6soinfo17call_constructorsEv") {
            call_constructors_addr = symbol.address;
        } else if (symbol.name === "__dl__ZNK6soinfo10get_sonameEv") {
            get_soname = new NativeFunction(symbol.address, "pointer", ["pointer"]);
        }
    }
    console.log(call_constructors_addr)
    var listener = Interceptor.attach(call_constructors_addr, {
        onEnter: function (args) {
            console.log("hooked call_constructors")
            var module = Process.findModuleByName("libmsaoaidsec.so")
            if (module != null) {
                Interceptor.replace(module.base.add(0x1c544), new NativeCallback(function () {
                    console.log("0x1c544: replaced")
                }, "void", []))
                Interceptor.replace(module.base.add(0x1b8d4), new NativeCallback(function () {
                    console.log("0x1b8d4: replaced")
                }, "void", []))
                Interceptor.replace(module.base.add(0x26e5c), new NativeCallback(function () {
                    console.log("0x26e5c: replaced")
                }, "void", []))
                listener.detach()
            }

        },
    })
}
function main(){
    hook_dlopen()
}

main()

And it works:

Anti-debugging bypassed

Approach 2: nop-ing the functions

function nop_addr(addr) {
    Memory.protect(addr, 4 , 'rwx');
    var w = new Arm64Writer(addr);
    w.putRet();
    w.flush();
    w.dispose();
}

function hook_dlopen(){
    // on Android 8.0+ SOs are loaded via android_dlopen_ext
    var android_dlopen_ext = Module.findExportByName(null,"android_dlopen_ext");
    console.log("addr_android_dlopen_ext",android_dlopen_ext);
    Interceptor.attach(android_dlopen_ext,{
        onEnter:function(args){
            var pathptr = args[0];
            if(pathptr!=null && pathptr != undefined){
                var path = ptr(pathptr).readCString();
                if(path.indexOf("libmsaoaidsec.so")!=-1){
                    console.log("android_dlopen_ext:",path);
                    hook_call_constructors()
                }
            }
        },
        onLeave:function(retvel){
            //console.log("leave!");
        }
    })
}

function hook_call_constructors() {
    let linker = null;
    if (Process.pointerSize === 4) {
        linker = Process.findModuleByName("linker");
    } else {
        linker = Process.findModuleByName("linker64");
    }
    let call_constructors_addr, get_soname
    let symbols = linker.enumerateSymbols();
    for (let index = 0; index < symbols.length; index++) {
        let symbol = symbols[index];
        if (symbol.name === "__dl__ZN6soinfo17call_constructorsEv") {
            call_constructors_addr = symbol.address;
        } else if (symbol.name === "__dl__ZNK6soinfo10get_sonameEv") {
            get_soname = new NativeFunction(symbol.address, "pointer", ["pointer"]);
        }
    }
    console.log(call_constructors_addr)
    var listener = Interceptor.attach(call_constructors_addr, {
        onEnter: function (args) {
            console.log("hooked call_constructors")
            var module = Process.findModuleByName("libmsaoaidsec.so")
            if (module != null) {
                nop_addr(module.base.add(0x1c544))
                console.log("0x1c544: replaced")
                nop_addr(module.base.add(0x1b8d4))
                console.log("0x1b8d4: replaced")
                nop_addr(module.base.add(0x26e5c))
                console.log("0x26e5c: replaced")
                listener.detach()
            }

        },
    })
}
function main(){
    hook_dlopen()
}

main()

This works too:

Nop bypass


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