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.

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.

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:

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:

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