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”.

process killed after helloFig. 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.

libmsaoaidsec.so anti-fridaFig. 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.

hit but freezes after nopFig. 3

Hooking clone

Here I found a lower-level approach using clone to detect thread creation.

clone detectionFig. 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.

clone retrieves the thread function addressFig. 5

Hooking strstr

Let’s step into these thread functions to see what they actually do.

entering the thread functionsFig. 6

open system files, fgets, strstrFig. 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.

returning false bypasses the checksFig. 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.

rusda bypasses it directlyFig. 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