Bypassing bilibili's Frida Detection (7.26.1 → 7.76.0)

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 bilibili’s Frida Detection (7.26.1 → 7.76.0)

This is an analysis, study, and bypass of the Frida detection in bilibili versions 7.26.1 and 7.76.0.

bilibili 7.26.1

The older version’s Frida detection.

Frida detected on spawn

Launching in spawn mode, Frida gets detected immediately. Let’s hook dlopen to see which SO might contain the detection.

function hook_dlopen(soName = '') {
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
        onEnter: function(args) {
            var pathptr = args[0];
            if (pathptr) {
                var path = ptr(pathptr).readCString();
                console.log("Loading: " + path);
                if (path.indexOf(soName) >= 0) {
                    console.log("Already loading: " + soName);
                    // hook_system_property_get();
                }
            }
        }
    });
}
setImmediate(hook_dlopen, "libmsaoaidsec.so");

libmsaoaidsec.so is the last SO before detection

You can see that after libmsaoaidsec.so is dlopen’d, Frida gets detected — so the detection very likely lives in libmsaoaidsec.so. Before that, we need to figure out the most useful place to hook.

After dlopen finishes, hook the JNI_OnLoad function to check whether the detection runs before or after it. We can get JNI_OnLoad’s address from IDA. Here the Frida detection happens before JNI_OnLoad.

JNI_OnLoad address in IDA

function hook_JNI_OnLoad(){
    let module = Process.findModuleByName("libmsaoaidsec.so")
    Interceptor.attach(module.base.add(0xC6DC + 1), {
        onEnter(args){
            console.log("JNI_OnLoad")
        }
    })
}

By hooking thread creation, let’s find where the Frida-detection thread is started. During reproduction, the original author hooked __system_property_get, which is early in init_proc. This ties into the execution order of dlopen and the .init_xx functions in the AOSP source — I covered the details in my writeup on the SO loading process.

init_proc and the SO loading flow

init_proc and the SO loading flow

function hook_system_property_get() {
    var system_property_get_addr = Module.findExportByName(null, "__system_property_get");
    if (!system_property_get_addr) {
        console.log("__system_property_get not found");
        return;
    }
    
    Interceptor.attach(system_property_get_addr, {
        onEnter: function(args) {
            var nameptr = args[0];
            if (nameptr) {
                var name = ptr(nameptr).readCString();
                if (name.indexOf("ro.build.version.sdk") >= 0) {
                    console.log("Found ro.build.version.sdk, need to patch");
                    // hook_pthread_create();
                    // bypass()
                    // we can start hooking here
                }
            }
        }
    });
}

Since the Frida detection runs before JNI_OnLoad, we need to hook as early as possible in init_proc. The place to hook is where threads get created — pthread_create.

function hook_pthread_create() {
    var pthread_create = Module.findExportByName("libc.so", "pthread_create");
    var libmsaoaidsec = Process.findModuleByName("libmsaoaidsec.so");
    
    if (!libmsaoaidsec) {
        console.log("libmsaoaidsec.so not found");
        return;
    }
    
    console.log("libmsaoaidsec.so base: " + libmsaoaidsec.base);
    
    if (!pthread_create) {
        console.log("pthread_create not found");
        return;
    }
    
    Interceptor.attach(pthread_create, {
        onEnter: function(args) {
            var thread_ptr = args[2];
            if (thread_ptr.compare(libmsaoaidsec.base) < 0 || thread_ptr.compare(libmsaoaidsec.base.add(libmsaoaidsec.size)) >= 0) {
                console.log("pthread_create other thread: " + thread_ptr);
            } else {
                console.log("pthread_create libmsaoaidsec.so thread: " + thread_ptr + " offset: " + thread_ptr.sub(libmsaoaidsec.base));
            }
        },
        onLeave: function(retval) {}
    });
}

The hook chain: at the dlopen point, hook __system_property_get; when its argument is ro.build.version.sdk, hook pthread_create.

dlopen → __system_property_get → pthread_create

The thread offsets

This compares every thread created by pthread_create; when the thread’s address falls inside libmsaoaidsec.so’s range, it prints the address and offset. Two threads show up here.

Let’s look in IDA.

Thread 1 in IDA

Thread 2 in IDA

Thread offsets in IDA

These spots show strcmp, openat, strstr… — lots of Frida detection. Cross-reference pthread_create, then just NOP them.

Cross-references

Cross-references

The full script

function hook_dlopen(soName = '') {
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
        onEnter: function(args) {
            var pathptr = args[0];
            if (pathptr) {
                var path = ptr(pathptr).readCString();
                console.log("Loading: " + path);
                if (path.indexOf(soName) >= 0) {
                    console.log("Already loading: " + soName);
                    hook_system_property_get();
                }
            }
        }
    });
}

function hook_system_property_get() {
    var system_property_get_addr = Module.findExportByName(null, "__system_property_get");
    if (!system_property_get_addr) {
        console.log("__system_property_get not found");
        return;
    }
    
    Interceptor.attach(system_property_get_addr, {
        onEnter: function(args) {
            var nameptr = args[0];
            if (nameptr) {
                var name = ptr(nameptr).readCString();
                if (name.indexOf("ro.build.version.sdk") >= 0) {
                    console.log("Found ro.build.version.sdk, need to patch");
                    hook_pthread_create();
                    // bypass()
                }
            }
        }
    });
}

function hook_pthread_create() {
    var pthread_create = Module.findExportByName("libc.so", "pthread_create");
    var libmsaoaidsec = Process.findModuleByName("libmsaoaidsec.so");
    
    if (!libmsaoaidsec) {
        console.log("libmsaoaidsec.so not found");
        return;
    }
    
    console.log("libmsaoaidsec.so base: " + libmsaoaidsec.base);
    
    if (!pthread_create) {
        console.log("pthread_create not found");
        return;
    }
    
    Interceptor.attach(pthread_create, {
        onEnter: function(args) {
            var thread_ptr = args[2];
            if (thread_ptr.compare(libmsaoaidsec.base) < 0 || thread_ptr.compare(libmsaoaidsec.base.add(libmsaoaidsec.size)) >= 0) {
                console.log("pthread_create other thread: " + thread_ptr);
            } else {
                console.log("pthread_create libmsaoaidsec.so thread: " + thread_ptr + " offset: " + thread_ptr.sub(libmsaoaidsec.base));
            }
        },
        onLeave: function(retval) {}
    });
}
function nop_code(addr)
{
    Memory.patchCode(ptr(addr),4,code => {
        const cw =new ThumbWriter(code,{pc:ptr(addr)});
        cw.putNop();
        cw.putNop();
        cw.flush();
    })
}

function bypass()
{
    let module = Process.findModuleByName("libmsaoaidsec.so")
    nop_code(module.base.add(0x010AE4))
    nop_code(module.base.add(0x113F8))
}
setImmediate(hook_dlopen, "libmsaoaidsec.so");

Frida bypassed

That bypasses Frida. Alternatively, patch those two pthread_create sites directly in IDA and put the patched SO back into the APK.

bilibili 7.76.0

7.26.1:

7.26.1 pthread_create

In the newer version, pthread_create is hidden.

7.76.0:

7.76.0 obfuscated

But we can still see where pthread_create creates threads using the earlier method.

function hook_dlopen(soName = '') {
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
        onEnter: function(args) {
            var pathptr = args[0];
            if (pathptr) {
                var path = ptr(pathptr).readCString();
                console.log("Loading: " + path);
                if (path.indexOf(soName) >= 0) {
                    console.log("Already loading: " + soName);
                    hook_system_property_get();
                }
            }
        }
    });
}

function hook_system_property_get() {
    var system_property_get_addr = Module.findExportByName(null, "__system_property_get");
    if (!system_property_get_addr) {
        console.log("__system_property_get not found");
        return;
    }
    
    Interceptor.attach(system_property_get_addr, {
        onEnter: function(args) {
            var nameptr = args[0];
            if (nameptr) {
                var name = ptr(nameptr).readCString();
                if (name.indexOf("ro.build.version.sdk") >= 0) {
                    console.log("Found ro.build.version.sdk, need to patch");
                    hook_pthread_create();
                    // bypass()
                }
            }
        }
    });
}

function hook_pthread_create() {
    var pthread_create = Module.findExportByName("libc.so", "pthread_create");
    var libmsaoaidsec = Process.findModuleByName("libmsaoaidsec.so");
    
    if (!libmsaoaidsec) {
        console.log("libmsaoaidsec.so not found");
        return;
    }
    
    console.log("libmsaoaidsec.so base: " + libmsaoaidsec.base);
    
    if (!pthread_create) {
        console.log("pthread_create not found");
        return;
    }
    
    Interceptor.attach(pthread_create, {
        onEnter: function(args) {
            var thread_ptr = args[2];
            if (thread_ptr.compare(libmsaoaidsec.base) < 0 || thread_ptr.compare(libmsaoaidsec.base.add(libmsaoaidsec.size)) >= 0) {
                console.log("pthread_create other thread: " + thread_ptr);
            } else {
                console.log("pthread_create libmsaoaidsec.so thread: " + thread_ptr + " offset: " + thread_ptr.sub(libmsaoaidsec.base));
            }
        },
        onLeave: function(retval) {}
    });
}
setImmediate(hook_dlopen, "libmsaoaidsec.so");

Three new threads

Here you can see three new threads created inside libmsaoaidsec.so — most likely the Frida-detection spots.

pthread_create libmsaoaidsec.so thread: 0x76bcce0544 offset: 0x1c544
pthread_create libmsaoaidsec.so thread: 0x76bccdf8d4 offset: 0x1b8d4
pthread_create libmsaoaidsec.so thread: 0x76bcceae5c offset: 0x26e5c

We also hook at __system_property_get to find where the detection thread is. In practice that spot is already obfuscated, but the function itself isn’t — it’s directly findable in the import table. So cross-reference to see where it’s called.

Cross-referencing __system_property_get

Cross-referencing __system_property_get

It turns out it’s still at init_proc’s sub_123F0.

init_proc sub_123F0

The "ro.build.version.sdk" argument check is obfuscated, but since we could locate the thread this way, it clearly still calls __system_property_get("ro.build.version.sdk") at runtime. And since the code can directly detect the Frida-detection thread’s address, the detection point is still before __system_property_get("ro.build.version.sdk").

Let’s also cross-reference what pthread_create got obfuscated into.

pthread_create cross-reference

pthread_create cross-reference

pthread_create cross-reference

From the arguments, it’s almost certainly the obfuscated pthread_create.

Head-on bypass

For the old version, the bypass was to NOP the Frida-detection threads. In 7.26.1 there were two detection threads; in 7.76.0 there are three — so we can NOP all three.

Here I stick to the most direct approach: instead of NOP-ing pthread_create itself, I patch the actual detection functions. The timing I pick is right when we enter libmsaoaidsec.so and it starts creating the Frida-detection threads via pthread_create.

Patching the detection functions

function hook_pthread_create() {
    var pthread_create = Module.findExportByName("libc.so", "pthread_create");
    var libmsaoaidsec = Process.findModuleByName("libmsaoaidsec.so");
    
    if (!libmsaoaidsec) {
        console.log("libmsaoaidsec.so not found");
        return;
    }
    
    console.log("libmsaoaidsec.so base: " + libmsaoaidsec.base);
    
    if (!pthread_create) {
        console.log("pthread_create not found");
        return;
    }
    
    Interceptor.attach(pthread_create, {
        onEnter: function(args) {
            var thread_ptr = args[2];
            if (thread_ptr.compare(libmsaoaidsec.base) < 0 || thread_ptr.compare(libmsaoaidsec.base.add(libmsaoaidsec.size)) >= 0) {
                console.log("pthread_create other thread: " + thread_ptr);
            } else {
                console.log("pthread_create libmsaoaidsec.so thread: " + thread_ptr + " offset: " + thread_ptr.sub(libmsaoaidsec.base));
                Interceptor.replace(libmsaoaidsec.base.add(0x1c544),new NativeCallback(function(){
                    console.log("Interceptor.replace: 0x1c544")
                },"void",[]))
                Interceptor.replace(libmsaoaidsec.base.add(0x1b8d4),new NativeCallback(function(){
                    console.log("Interceptor.replace: 0x1b8d4")
                },"void",[]))
                Interceptor.replace(libmsaoaidsec.base.add(0x26e5c),new NativeCallback(function(){
                    console.log("Interceptor.replace: 0x26e5c")
                },"void",[]))
            }
        },
        onLeave: function(retval) {}
    });
}

Head-on bypass working

You can see the Frida detection is bypassed directly — and the Frida print function I wrote executes successfully.

The shortcut bypass

There’s another article that doesn’t fight head-on but takes a shortcut: hook dlsym, and once inside libmsaoaidsec.so, count the calls to pthread_create. On the first two calls, return a fake pthread_create so the Frida threads never start.

Here’s the shortcut code:

function create_fake_pthread_create() {
    const fake_pthread_create = Memory.alloc(4096)
    Memory.protect(fake_pthread_create, 4096, "rwx")
    Memory.patchCode(fake_pthread_create, 4096, code => {
        const cw = new Arm64Writer(code, { pc: ptr(fake_pthread_create) })
        cw.putRet()
    })
    return fake_pthread_create
}
 
function hook_dlsym() {
    var count = 0
    console.log("=== HOOKING dlsym ===")
    var interceptor = Interceptor.attach(Module.findExportByName(null, "dlsym"),
        {
            onEnter: function (args) {
                const name = ptr(args[1]).readCString()
                console.log("[dlsym]", name)
                if (name == "pthread_create") {
                    count++
                }
            },
            onLeave: function(retval) {
                if (count == 1) {
                    retval.replace(fake_pthread_create)
                }
                else if (count == 2) {
                    retval.replace(fake_pthread_create)
                    // done with 2 replacements, stop hooking dlsym
                    interceptor.detach()
                }
            }
        }
    )
    return Interceptor
}
 
function hook_dlopen() {
    var interceptor = 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)
                    if (path.indexOf("libmsaoaidsec.so") > -1) {
                        hook_dlsym()
                    }
                }
            }
        }
    )
    return interceptor
}
 
// create a fake pthread_create
var fake_pthread_create = create_fake_pthread_create()
var dlopen_interceptor = hook_dlopen()

Shortcut bypass working

This also bypasses it. But since we know libmsaoaidsec.so actually starts three threads, I think the count could be set to three:

onLeave: function(retval) {
                if (count == 1) {
                    retval.replace(fake_pthread_create)
                }
                else if (count == 2) {
                    retval.replace(fake_pthread_create)
                }
                else if (count == 2) {
                    retval.replace(fake_pthread_create)
                    // done with 2 replacements, stop hooking dlsym
                    interceptor.detach()
                }
            }

I tried it this way too — it also bypasses.


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