Common Ways to Bypass libmsaoaidsec.so's Anti-Frida Protection
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.
Common Ways to Bypass libmsaoaidsec.so’s Anti-Frida Protection
While studying Android reverse engineering, I kept hitting libmsaoaidsec.so blocking Frida injection. Here I’ve collected a few bypass methods. Corrections welcome.
Hooking pthread_create
After injecting with Frida, the process gets killed right after printing “hello”.
Fig. 1
Let’s see which .so was loaded before it exited:
function hook_dlopen() {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
{
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
console.log("load " + path);
}
}
}
);
}
It’s easy to guess that libmsaoaidsec.so is the one doing the anti-Frida.
Fig. 2
There are many bypass methods online; most hook pthread_create and NOP out the thread function it creates.
function replace(addr) {
Interceptor.replace(addr, new NativeCallback(function () {
console.log(`replace ${addr}`)
}, 'void', []));
}
function nop_64(addr) {
Memory.protect(addr, 4 , 'rwx');
var w = new Arm64Writer(addr);
w.putRet();
w.flush();
w.dispose();
}
function hook_pthread_create(soname) {
let replaces = []; // records function offsets that have already been replaced
// int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
let pthread_create = Module.findExportByName(null, "pthread_create");
if (!pthread_create) {
console.log("pthread_create not found in libc.so");
return;
}
Interceptor.attach(pthread_create, {
onEnter: function (args) {
//console.log("hooking pthread_create ", pthread_create)
let start_routine = args[2];
//console.log(start_routine)
let libmsaoaidsec = Process.findModuleByAddress(start_routine);
if (libmsaoaidsec && libmsaoaidsec.name === soname) {
if (!replaces.includes(start_routine.toString())) {
let libmsaoaidsec_addr = libmsaoaidsec.base;
let func_offset = start_routine.sub(libmsaoaidsec_addr);
console.log("The thread function offset address in libmsaoaidsec.so(" + libmsaoaidsec_addr + ") is " + func_offset);
console.log("replace: " + func_offset);
//replaces.push(start_routine.toString());
//nop_64(start_routine)
}
}
}
});
}
setImmediate(hook_pthread_create, "libmsaoaidsec.so")
It successfully hit, but after NOP-ing those three places the app freezes for some reason.
Fig. 3
Hooking clone
Here I found a lower-level approach using clone to detect thread creation.
Fig. 4
function hook_clone(soname)
{
var clone = Module.findExportByName('libc.so', 'clone');
Interceptor.attach(clone, {
onEnter: function(args) {
// args[3] is the child thread's stack address. If 0, it may mean no stack was specified
if(args[3] != 0){
var addr = args[3].add(96).readPointer()
var so_name = Process.findModuleByAddress(addr).name;
var so_base = Module.getBaseAddress(so_name);
var offset = (addr - so_base);
console.log("===============>", so_name, addr,offset, offset.toString(16));
}
},
onLeave: function(retval) {
}
});
}
setImmediate(hook_clone, "libmsaoaidsec.so")
This also successfully retrieves the thread function’s address.
Fig. 5
Hooking strstr
Let’s step into these thread functions to see what they actually do.
Fig. 6
Fig. 7
We can infer that they open some system files, read their contents with fgets, then run key-string detection via strstr.
function hook_strstr_strcmp(soname) {
var pt_strstr = Module.findExportByName("libc.so", 'strstr');
var pt_strcmp = Module.findExportByName("libc.so", 'strcmp');
Interceptor.attach(pt_strstr, {
onEnter: function (args) {
var str1 = args[0].readCString();
var str2 = args[1].readCString();
if (
str2.indexOf("REJECT") !== -1 ||
str2.indexOf("tmp") !== -1 ||
str2.indexOf("frida") !== -1 ||
str2.indexOf("gum-js-loop") !== -1 ||
str2.indexOf("gmain") !== -1 ||
str2.indexOf("linjector") !== -1
) {
//console.log("strstr-->", str1, str2);
this.hook = true;
}
}, onLeave: function (retval) {
if (this.hook) {
retval.replace(0);
}
}
});
Interceptor.attach(pt_strcmp, {
onEnter: function (args) {
var str1 = args[0].readCString();
var str2 = args[1].readCString();
if (
str2.indexOf("REJECT") !== -1 ||
str2.indexOf("tmp") !== -1 ||
str2.indexOf("frida") !== -1 ||
str2.indexOf("gum-js-loop") !== -1 ||
str2.indexOf("gmain") !== -1 ||
str2.indexOf("linjector") !== -1
) {
//console.log("strcmp-->", str1, str2);
this.hook = true;
}
}, onLeave: function (retval) {
if (this.hook) {
retval.replace(0);
}
}
})
}
setImmediate(hook_strstr_strcmp, "libmsaoaidsec.so")
Simply returning false for these detection points bypasses it just the same.
Fig. 8
Modified Frida → rusda
For frida and frida-tools, use the official sources. On the phone, use the release rusda-server-android-arm64 — no need to compile manually.
Using rusda, it bypasses the protection outright.
Fig. 9
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