乐于分享
好东西不私藏

APP风控参数分析&Frida绕过

APP风控参数分析&Frida绕过

过Frida检测

先hook一下dlopen,也就是android_dlopen_ext

为什么要Hook dlopen呢?

因为App的Frida检测代码一般都在so层实现,这些检测代码会在对应的so加载时初始化。

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, {onEnterfunction (args) {      var pathptr = args[0];      if (pathptr != null && pathptr != undefined) {        var path = ptr(pathptr).readCString();        console.log("android_dlopen_ext:", path);      }    },onLeavefunction (retvel) {    }  })}function main() {   hook_dlopen()}setImmediate(main)

Frida进程会被杀死,同时手机也会卡死,而且也加载了特征so

这是为什么呢?

每隔几毫秒检查一次    ↓发现了Frida的痕迹    ↓执行反制措施:卡死界面 + 杀进程

我们的反制措施为Hook Clone函数

Clone函数为Linux创建线程的底层调用,Hook这个函数我们可以知道每个线程的详细信息,例如:谁创建的,线程函数在哪,什么时候创建的。

这样我们就可以定位到反调试线程,然后分析它,干掉它。

function hook_clone() {    var clone = Module.findExportByName('libc.so''clone');    Interceptor.attach(clone, {onEnterfunction (args) {        console.log("═══ Clone Called ═══");        console.log("args[0] (wrapper):", args[0]);  // __pthread_start        console.log("args[1] (stack)  :", args[1]);        console.log("args[2] (flags)  :", args[2]);        console.log("args[3] (tls)    :", args[3]);  //// 线程局部存储(TLS)        if (args[3] != 0) {          try {            // 读取真正的线程函数            var real_func = args[3].add(96).readPointer();            var module = Process.findModuleByAddress(real_func);            if (module) {              var offset = real_func.sub(module.base);              console.log(" 真正的线程函数:");              console.log("   SO名称:"module.name);              console.log("   函数地址:", real_func);              console.log("   偏移:"ptr(offset));              if (module.name.includes("DexHelper")) {                console.log(" 检测到目标so!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");              }            }          } catch(e) {            console.log("解析失败:", e);          }        }      }    });  }  setImmediate(hook_clone);

为什么要在var real_func = args[3].add(96).readPointer(); 读取?我们需要了解一下 pthread_internal_t 结构体也就是pthread_t

这是 Android Bionic 库中用来管理线程的内部结构:那么什么时候会创建这个结构体呢?肯定是线程被创建的时候,也就是pthread_create函数。

Android创建线程分析

安卓平台上总共有三种线程:1. Java 线程:Android 虚拟机线程,具有运行 Java 代码的 Runtime2. Native 线程(只能执行 C/C++):纯粹的 Linux 线程3. Native 线程(还能执行 Java):既能执行 C/C++ 代码,也能执行 Java 代码

Java线程创建流程

java层:Thread.start()

// /libcore/libart/src/main/java/java/lang/Thread.javapublic synchronized void start() {    checkNotStarted(); // 保证线程只启动一次    hasBeenStarted = true;    // 调用 native 方法创建线程    nativeCreate(this, stackSize, daemon);}

nativeCreate为JNI方法,对应C++层的Thread_nativeCreate

JNI方法映射

// /art/runtime/native/java_lang_Thread.cc// 宏定义#define NATIVE_METHOD(className, functionName, signature) \    { #functionName, signature, reinterpret_cast<void*>(className ## _ ## functionName) }// 方法注册NATIVE_METHOD(Thread, nativeCreate, "(Ljava/lang/Thread;JZ)V"),

展开后,nativeCreate 映射到 Thread_nativeCreate 函数。

Thread_nativeCreate

// /art/runtime/native/java_lang_Thread.ccstaticvoid Thread_nativeCreate(JNIEnv* env, jclass, jobject java_thread,                                jlong stack_size, jboolean daemon) {    // 创建 Native 线程    Thread::CreateNativeThread(env, java_thread, stack_size, daemon == JNI_TRUE);}

CreateNativeThread

// /art/runtime/thread.ccvoid Thread::CreateNativeThread(JNIEnv* env, jobject java_peer,                                 size_t stack_size, bool is_daemon) {    Thread* self = static_cast<JNIEnvExt*>(env)->self;    Runtime* runtime = Runtime::Current();    // 1. 创建 ART 的 Thread 对象    Thread* child_thread = new Thread(is_daemon);    // 2. 关联 Java 层的 Thread 对象(jpeer)    child_thread->tlsPtr_.jpeer = env->NewGlobalRef(java_peer);    // 3. 修正栈大小    stack_size = FixStackSize(stack_size);    // 4. 在 Java Thread 对象中设置 native peer 指针    env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer,                      reinterpret_cast<jlong>(child_thread));    // 5. 创建 JNI 环境    std::unique_ptr<JNIEnvExt> child_jni_env_ext(        JNIEnvExt::Create(child_thread, Runtime::Current()->GetJavaVM()));    // 6. 设置线程属性并创建 pthread    pthread_t new_pthread;    pthread_attr_t attr;    pthread_attr_init(&attr);    child_thread->tlsPtr_.tmp_jni_env = child_jni_env_ext.get();    // 7. 调用 pthread_create 创建线程    int pthread_create_result = pthread_create(        &new_pthread,            //返回线程句柄        &attr,        Thread::CreateCallback,  // 线程入口函数        child_thread             // 传递给线程的参数    );    if (pthread_create_result == 0) {        child_jni_env_ext.release();        return;    }    // 创建失败的处理...}

– 创建 ART 虚拟机的 Thread 对象– 关联 Java 和 Native 的 Thread 对象(双向引用)– 创建 JNI 环境,使线程能够调用 Java 代码– 调用 pthread_create 创建真正的操作系统线程

Thread::CreateCallback

// /art/runtime/thread.ccvoid* Thread::CreateCallback(void* arg) {    Thread* self = reinterpret_cast<Thread*>(arg);    Runtime* runtime = Runtime::Current();    // 1. 附加到 ART 虚拟机    self->Init(runtime->GetThreadList(), runtime->GetJavaVM());    // 2. 初始化线程相关资源    self->InitCardTable();    self->InitTid();    self->InitAfterFork();    // 3. 调用 Java 层的 run() 方法    {        ScopedObjectAccess soa(self);        self->NotifyThreadBirth();        // 获取 Thread.run() 方法        ArtMethod* run_method =             WellKnownClasses::java_lang_Thread_run->GetArtMethod();        // 反射调用 run 方法        JValue result;        run_method->Invoke(self                          reinterpret_cast<uint32_t*>(&self->tlsPtr_.jpeer),                          sizeof(void*),                           &result,                           "V");    }    // 4. 线程执行完毕,清理资源    self->NotifyThreadDeath();    return nullptr;}

– 线程启动后先初始化 ART虚拟机环境,通过反射调用 Java 层的 run() 方法执行完毕后进行资源清理

 pthread_create分析

pthread_create在CreateNativeThread时被调用

int pthread_create_result = pthread_create(        &new_pthread,            //返回线程句柄        &attr,        Thread::CreateCallback,  // 线程入口函数        child_thread             // 传递给线程的参数    );

pthread_create` 会先得到一个`pthread_internal_t`结构体

pthread_create会先得到一个pthread_internal_t结构体

// 1. 应用层调用pthread_t thread;pthread_create(&thread, NULL, my_function, my_arg);// 2. pthread_create 内部实现int pthread_create(pthread_t* thread_out, constpthread_attr_t* attr,void* (*start_routine)(void*), void* arg) {    // 分配并初始化 pthread_internal_tpthread_internal_t* thread =         reinterpret_cast<pthread_internal_t*>(            calloc(sizeof(pthread_internal_t), 1));    // 设置关键字段    thread->start_routine = start_routine;        thread->start_routine_arg = arg;                // 分配线程栈    thread->stack_base = mmap(...);    thread->stack_size = stack_size;    // 调用 clone 系统调用int flags = CLONE_VM | CLONE_FS | CLONE_FILES |                 CLONE_SIGHAND | CLONE_THREAD |                 CLONE_SYSVSEM | CLONE_SETTLS |                 CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID;    // 关键:thread 作为 TLS 参数传递给 cloneint tid = clone(__pthread_start,           // 包装函数                   thread->stack_top(),        // 栈顶                   flags,                       // 克隆标志                   thread,                      // TLS (args[3])                   &(thread->tid));             // parent_tidptr    // 将 pthread_internal_t 加入全局链表    __pthread_internal_add(thread);    // 返回线程句柄    *thread_out = reinterpret_cast<pthread_t>(thread);    return 0;}// 3. __pthread_start 包装函数staticint __pthread_start(void* arg) {pthread_internal_t* thread =         reinterpret_cast<pthread_internal_t*>(arg);    // 设置线程 ID    thread->tid = gettid();    // 调用真正的线程函数void* result = thread->start_routine(thread->start_routine_arg);    // 线程退出    pthread_exit(result);    return 0;}

这个结构体为核心数据结构,包含了线程的所有信息。

pthread_create是pthread库中的函数,通过syscall再调用到clone来请求内核创建线程。

Linux进程管理

Linux创建进程采用fork()和exec()– fork: 采用复制当前进程的方式来创建子进程,此时子进程与父进程的区别仅在于pid, ppid以及资源统计量(比如挂起的信号)

– exec:读取可执行文件并载入地址空间执行;一般称之为exec函数族,有一系列exec开头的函数,比如execl, execve等fork过程复制资源包括代码段,数据段,堆,栈。fork调用者所在进程便是父进程,新创建的进程便是子进程;在fork调用结束,从内核返回两次,一次继续执行父进程,一次进入执行子进程。进程创建– Linux进程创建: 通过fork()系统调用创建进程– Linux用户级线程创建:通过pthread库中的pthread_create()创建线程– Linux内核线程创建: 通过kthread_create()Linux线程,也并非”轻量级进程”,在Linux看来线程是一种进程间共享资源的方式,线程可看做是跟其他进程共享资源的进程。fork, vfork, clone根据不同参数调用do_fork:– pthread_create: flags参数为 CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND– fork: flags参数为 SIGCHLD– vfork: flags参数为 CLONE_VFORK, CLONE_VM, SIGCHLD

Fork流程图

进程/线程创建的方法fork(),pthread_create(),最终在linux都是调用do_fork方法。 当然还有vfork其实也是一样的, 通过系统调用到sys_vfork,然后再调用do_fork方法,该方法 现在很少使用,所以下图省略该方法。

fork执行流程:1. 用户空间调用fork()方法;2. 经过syscall陷入内核空间, 内核根据系统调用号找到相应的sys_fork系统调用;3. sys_fork()过程会在调用do_fork(), 该方法参数有一个flags很重要, 代表的是父子进程之间需要共享的资源; 对于进程创建flags=SIGCHLD, 即当子进程退出时向父进程发送SIGCHLD信号;4. do_fork(),会进行一些check过程,之后便是进入核心方法copy_process.

flags参数

进程与线程最大的区别在于资源是否共享,线程间共享的资源主要包括内存地址空间,文件系统,已打开文件,信号等信息, 如下图蓝色部分的flags便是线程创建过程所必需的参数。

fork采用Copy on Write机制,父子进程共用同一块内存,只有当父进程或者子进程执行写操作时会拷贝一份新内存。 另外,创建进程也是有可能失败,比如进程个数达到系统上限(32768)或者系统可用内存不足。

在安卓源码对应内容如上图所示而现在我们需要去分析pthread_internal_t*  结构体中,在哪里存储的线程函数

adb pull /system/lib64/libc.so ./libc64.so

搜索pthread_create

不要忘记了

int pthread_create_result = pthread_create(        &new_pthread,            //返回线程句柄        &attr,        Thread::CreateCallback,  // 线程入口函数        child_thread             // 传递给线程的参数    );

我们向下追踪

发现a3的值赋值给了v54

所以偏移为0x60的地方为咱们线程函数的基址

struct pthread_internal_t {void* next;                      // 0x00 - 链表指针void* prev;                      // 0x08 - 链表指针pid_t tid;                       // 0x10 - 线程 IDpid_t cached_pid;                // 0x14 - 缓存的进程 ID    // ... 省略一些字段 ...pthread_mutex_t startup_mutex;   // 0x88 - 启动互斥锁bool startup_flag;               // 0x8C - 启动标志void* mmap_base;                 // 0x90 (144) - mmap 分配的基地址size_t mmap_size;                // 0x98 (152) - mmap 分配的大小void* (*start_routine)(void*);   // 0x60 (96) - 线程入口函数(更正!)void* start_routine_arg;         // 0x68 (104) - 传递给线程函数的参数    // ... 其他字段 ...};  // 总大小:704 字节 (0x2C0)

我们再进入clone函数

这个函数只是clone函数的包装器,真正的clone为

如果返回值没问题,就调用__start_thread

在这个函数,会初始化tid,以及调用线程函数,线程函数执行后,就退出线程。

因此我们通过hook clone即可拦截线程!

console.log("启动反调试绕过...");var anti_debug_offsets = [    0x4c574,    0x56c10,    0x54584,    0x5c3c4];function waitForModule() {    var module = Process.findModuleByName("libDexHelper.so");    if (module) {        console.log("找到 libDexHelper.so 基址:"module.base);        hookAntidebugFunctions(module.base);    } else {        console.log("等待 libDexHelper.so 加载...");        setTimeout(waitForModule, 100);    }}function hookAntidebugFunctions(base) {    console.log("开始Hook反调试函数");    var dummy_func = new NativeCallback(function(arg) {        return 0;    }, 'int', ['pointer']);    anti_debug_offsets.forEach(function(offset, index) {        var func_addr = base.add(offset);        var hook_num = index + 1;        console.log("Hook 函数 #" + hook_num + " 偏移:" + ptr(offset) + " 地址:" + func_addr);        try {            Interceptor.replace(func_addr, dummy_func);            console.log("replace 替换成功");        } catch(e1) {            console.log("replace 失败,尝试 attach");            try {                Interceptor.attach(func_addr, {onEnterfunction(args) {                        console.log("函数 #" + hook_num + " 被调用");                        for (var i = 0; i < 8; i++) {                            try {                                args[i] = ptr(0);                            } catch(e) {}                        }                    },onLeavefunction(retval) {                        retval.replace(0);                        console.log("返回值已改为0");                    }                });                console.log("attach 拦截成功");            } catch(e2) {                console.log("attach 也失败:", e2.message);            }        }    });    console.log("所有函数Hook完成");}setTimeout(waitForModule, 500);

另一个so也是相同思路

参考资料:http://gityuan.com/2017/08/05/linux-process-fork/https://mp.weixin.qq.com/s/kZPYm_Ir-39cg7_Y7Ise3A

抓包

POST /apis/login/userLogin4Uname.doHTTP/1.1
User-Agent: Dalvik/2.1.0 (Linux; U; Android 10; Pixel 2 XL Build/QQ1A.191205.008) (schild:57e5cafb263d51fa7248f3c0ddcd93df) (device:Pixel 2 XL) Language/zh_CN_#Hans com.chaoxing.mobile/ChaoXingStudy_3_6.6.2_android_phone_10899_284 (@Kalimdor)_c713c8b5b4e24af4840ab6f1e44f6d6f
Accept-Language: zh_CN_#Hans
Content-Type: application/x-www-form-urlencoded
Content-Length: 741
Host: sso.chaoxing.com
Connection: Keep-Alive
Accept-Encoding: gzip
Cookie: fid=2785; _uid=297297071; _d=1760100738594; UID=297297071; vc3=Fk0zNvzdTe7%2BImSyEY%2BrftMrODXhTGfcFw3jxnljlduWgE5r8C0T5l8RYcOtv2RTydE2YMpSk%2FVOcFxUQu7neeNgPtUCQk7kK%2FojgewJg%2FZspamGvH5IwrE1jCexIBk8nVUKzescpiHIeBDqs9HHQQRDpoxE%2FgZmfdwBZ%2Ftvhs8%3Ddce0239082c19383c39d82cbf88df9af; uf=da0883eb5260151ec216b0d5ab4175f9f265e8f2d553141c7a06e39f91d78f9c6ab9b6f1074af198d088e79d3a8ff638a29d455a3bc484c1d807a544f7930b6aed1e6c11a143bb563b0339d97cdac4bab9fe7be6fa3211cd713028f1ec42bf71b1188854805578cc30efcb9395a6b42b9afeb38e675e28e312b47e51bb3d048117261b5a50f9432f0a0c3d0aedbb4bb14df7ff280fcb29d10d8a4c92b12beb4b9d97dfe5b26c691ce0022f71b6d9d4406250480410be0c44e7fafd565af53bf2; cx_p_token=af5660cce0baef49887ca0251893b508; p_auth_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIyOTcyOTcwNzEiLCJsb2dpblRpbWUiOjE3NjAxMDA3Mzg1OTYsImV4cCI6MTc2MDcwNTUzOH0.9TZcMPS1EjZdWKlNQ16MKaHN4pbe-drjpQZVSNw1kyM; xxtenc=55bd60e138e7351ede494fa388333930; DSSTASH_LOG=C_38-UN_1605-US_297297071-T_1760100738596
data=dshPLdgyOIBbdl0IOKx8rUlqOvtaCbUwO9yREsDb1HUICzSS%2FCPXlf86RK4hvVtSphjzNXiNUYPndvV9nnEAPdI%2B1nVqNHveTucKYJ34P%2F3fJ%2Fr0L5LMv2pQ2YrAtcexu5PPo2TfcOp%2FLe6msVfifZD9geolVYsfn3Ia%2Fu%2B2F5pF7hfFmbUllumZoYop5mSOHGTkRFX8puf4jUwBFbts1cI1TSYCv8DRuNOggFH3IYePBU5cHY4z%2BlFThw6%2F%2FL9oxs%2Bj0m9ajdK8hw2gJ62QByOJ2jMXyZZwLWm%2BBfaQdSxXRLxTuh3Qd27ZKVggKox4IFSucQoBgGDsttTQX0%2FIRn%2BEHAedMAQ1mCrt76WYz1FfWgBQENzLpp9QtKkotn9QgED4J3oMFJcU%2BdVrUlemkMMIIGV2VkpSbSHg6YBYeINSKS0VJEZXcodHnpiUhljadQJkC5dQnDH8rvrIPEmUD%2F2Nrf8NL61iGwK8mmSSVVDbM0CkAivVRMAlKjOmwyUFKOdF0QxsvYhGVQ0%2BNq%2BcmoSyeEADMIr2%2FQk2MThT1c69I%2FshWy7f05ZhBjBP08gCZDvSciMi2rrl%2FzJt9%2BnyOrqA4QdMXNpnNSly8ejZ4QBzDus8YWHz1%2FZ6DrtuhAMJuwqBIlh%2B23lKJsbmIfMiBFAynf20Is%2FrjhyfWflVFdc%3D

完整登录流程

1. 发送验证码
POST /api/sendcaptcha
2. 验证码登录
POST /v11/loginregister  ← 第一个请求
3. SSO登录
POST /apis/login/userLogin4Uname.do← 第二个请求
4. 登录成功

逆向分析

第一个方法是直接的SSO调用

publicfinal voidy5() {
AccountManager.E().K0("https://sso.chaoxing.com/apis/login/userLogin4Uname.do", 3);
newHandler().postDelayed(newc(), 150L);
}
}

第二个方法是SSO登录的触发条件

publicvoidonChanged(@Nullable be.b<ResponseResult> response) {
if(response.g()) {
ResponseResult responseResult = response.f4061c;
if(responseResult != null && responseResult.getResult() == 1) {
AccountManager.E().K0("https://sso.chaoxing.com/apis/login/userLogin4Uname.do", 3);
List list = (List) UnitListActivity.this.f54133f.getValue();
if(list.contains(this.f54144a)) {
list.remove(this.f54144a);
list.add(0, this.f54144a);
UnitListActivity.this.f54134g.notifyDataSetChanged();
UnitListActivity.this.f54133f.setValue(list);
}
UnitListActivity.this.f54138k = true;
return;
}
return;
}
response.d();
}
}

长度为512,应该是AES加密

现在我们还没有定位到关键代码,因为data被BASE编码了,这时候考虑hook一下base

[+] Call Details:
- Flags: 2 (Base64 flags)
- Thread: main
- Time: 2025-10-11T12:31:27.655Z
[CALL] com.chaoxing.android.crypto.Ciphertext.getBytes_Base64
[CALL] com.chaoxing.study.account.e.e
[CALL] com.chaoxing.study.account.AccountManager.p0
[CALL] com.chaoxing.study.account.AccountManager.w0
[CALL] com.chaoxing.study.account.AccountManager.v0
[CALL] com.chaoxing.mobile.study.account.LoginByCodeActivity.g5
[CALL] com.chaoxing.mobile.study.account.LoginByCodeActivity.a5
[CALL] com.chaoxing.mobile.study.account.LoginByCodeActivity$j.onClick

g5

/* JADX INFO: Access modifiers changed from: private */
publicvoidg5() {
EditText editText;
String strTrim = this.f73287c.getText().toString().trim();
String string = this.f73289e.getText().toString();
boolean z11 = true;
if(ge.f.c(strTrim)) {
com.chaoxing.android.widget.a.j(this, R.string.string_account_enter_phone_number).s();
editText = this.f73287c;
elseif(ge.f.c(string)) {
com.chaoxing.android.widget.a.j(this, R.string.string_account_enter_verification_code).s();
editText = this.f73289e;
else{
z11 = false;
editText = null;
}
if(z11) {
if(editText != null) {
editText.requestFocus();
}
else{
com.chaoxing.library.helper.a.a(getCurrentFocus());
if(this.f73295k.e()) {
AccountManager.E().v0(this, strTrim, this.f73297m, string, this.f73306v);
else{
n5();
}
}
}

HOOK一下AccountManager.E().v0(this, strTrim, this.f73297m, string, this.f73306v);  来看看参数都是什么。

============================================================
参数详情:
参数1 (this/owner): [object Object]
参数2 (strTrim/手机号): '1755038xxxx'
参数3 (f73297m/国家代码): '86'
参数4 (string/验证码): '296613'
参数5 (f73306v/回调): [object Object]

继续跟踪代码:

发现关键代码,hook一下这个方法,打印调用栈发现

– com.chaoxing.study.account.b (接口)– com.chaoxing.study.account.d0 (实现类)

/**
* Hook CxDevice.a方法打印返回值
*/
Java.perform(function() {
try{
var CxDevice = Java.use("com.chaoxing.securitylib.napi.CxDevice");
CxDevice.a.overload('android.content.Context''java.lang.String').implementation = function(context, str) {
var result = this.a(context, str);
console.log("CxDevice.a返回值: "+ result);
returnresult;
};
console.log("[+] Hook设置完成");
catch(e) {
console.log("[-] Hook失败: "+ e.message);
}
});
输出为
[Pixel 2 XL::com.chaoxing.mobile ]-> [+] Hook设置完成
[+] Hook设置完成
[+] Hook设置完成
CxDevice.a返回值: Tzkdtre+wwVXQLY/YcO+A6r9TLryLQV5ru0KoIlIuec9ehtd6JnTzoyJKAuh998iff8MVj+l+uA1/R6ZkF2hJvwd9KSjvYBqf8SBaE4rMcPtpYnUIcyqUlwzJQVdIX/jGVI+KTyaaJAO5UI8NI7P3Ott0/Kj8xW1UJmTxpzWhcqUwVFHQzZxkYExEd1G/O83whsKk1Kntw8E0MObLdiVy5Dz5KBMhV3E0HWmmLfVeNJKjNnggsQVQ4PMx9VnbPQMYmlYO0ovZ4dgjKTGsZSfbMS+zMoPVgS39kYvdDTItZFq2mbI50XJdYq88qxgj/2ewIYIuQJMT/hB6yU6L712JXKwKmcQsPBXXskVeC64VTwLYXYwW0kbKXHIjIp8txR4ImQWR8V5uJNF4PzwERBi1Q+FHQb50EgXoK1xHhlt/5kjtHfWolJ109kJomUntb6NudHgg83FF4+CFTYnYV/YUw2ZcvmYQyVolE/EHkfLtBsg+3wgrod6WC/RHVZGLSftXOyCqR4ZXOYyE40WB5QjRVAK0tqEPmOV7tjmAdGOWUnm2egthNkJtfVEKbx1zEatlaTAMqNc/6r6O1B4i8m0KFB7MeHtPhCBiZs1AXAO1ysXeHlIqlgTfZJ2ksJ53Mkm8SSNP2ACbMWRaqpTcCcuKdBkhRz3homkprZzmL6RXDI=

需要去分析so层,使用IDA

根据函数的参数调用可以简单分析逻辑,但是因为有混淆不能直接分析

我们随便以几个函数为例子,点开第一个函数 sub_4E1C0(v37, &v87);会发现

跟进

再跟

每个函数都是这样的,先尝试了一下inlinehook发现,最终函数都会指向

也就是这个函数,发现有点复杂,思路是HOOK偏移,来打印指定地址的函数调用的X17寄存器

不如直接Dump so

在dump后使用sofix修复即可

__int64__fastcall Java_com_chaoxing_securitylib_napi_CxDevice_getDeviceInfo(
JNIEnv *env,
jclass clazz,
jobject context,
jstring str,
jstring str2,
jstring str3)
{
StatusReg = _ReadStatusReg(TPIDR_EL0);
v103 = *(_QWORD *)(StatusReg + 40);
v84 = 0;
v85 = 0;
v86 = 0;
v81 = 0;
v82 = 0;
v83 = 0;
v79[0] = 0;
v79[1] = 0;
v80 = 0;
if( str )
{
v12 = (*env)->GetStringUTFChars(env, str, 0);
v13 = sub_4DFA0();
sub_4C800(&v84, v12, v13);
(*env)->ReleaseStringUTFChars(env, str, v12);
}
if( str2 )
{
v14 = (*env)->GetStringUTFChars(env, str2, 0);
v15 = sub_4DFA0();
sub_4C800(&v81, v14, v15);
(*env)->ReleaseStringUTFChars(env, str2, v14);
}
if( str3 )
{
v16 = (*env)->GetStringUTFChars(env, str3, 0);
v17 = sub_4DFA0();
sub_4C800(v79, v16, v17);
(*env)->ReleaseStringUTFChars(env, str3, v16);
}
*(_QWORD *)&v77[7] = 0;
v76 = 14;
v78 = 0;
strcpy(v77, "android");
sub_4D0B0(v91, env, context);
sub_4CC20("ro.product.model", v102);
sub_4CC20("ro.product.brand", v101);
sub_4CC20("ro.build.version.codename", v100);
sub_4CC20("ro.build.version.release", v99);
sub_4CC20("ro.product.locale.language", v98);
sub_4CC20("ro.product.cpu.abilist", v97);
sub_4CC20("ro.hardware", v96);
sub_4CC20("ro.product.board", v95);
sub_4BDA0(v72, env, context);
v69 = 0;
v70 = 0;
v71 = 0;
sub_4DDE0(v90, 0);
sub_4BC40(&v87, &v76);
v18 = sub_4C940(v90, "platform");
sub_4E1C0(v18, &v87);
sub_4CCE0(&v87);
sub_4BC40(&v87, v91);
v19 = sub_4C940(v90, "app_name");
sub_4E1C0(v19, &v87);
sub_4CCE0(&v87);
sub_4BC40(&v87, v93);
v20 = sub_4C940(v90, "app_ver");
sub_4E1C0(v20, &v87);
sub_4CCE0(&v87);
sub_4BC40(&v87, &v69);
v21 = sub_4C940(v90, "mediaDrmId");
sub_4E1C0(v21, &v87);
sub_4CCE0(&v87);
sub_4D610(&v87, v102);
v22 = sub_4C940(v90, "cdtype");
sub_4E1C0(v22, &v87);
sub_4CCE0(&v87);
sub_4D610(&v87, v100);
v23 = sub_4C940(v90, "os_name");
sub_4E1C0(v23, &v87);
sub_4CCE0(&v87);
sub_4D610(&v87, v99);
v24 = sub_4C940(v90, "os_ver");
sub_4E1C0(v24, &v87);
sub_4CCE0(&v87);
sub_4D610(&v87, v98);
v25 = sub_4C940(v90, "os_lang");
sub_4E1C0(v25, &v87);
sub_4CCE0(&v87);
sub_4D610(&v87, v97);
v26 = sub_4C940(v90, "cpu_ar");
sub_4E1C0(v26, &v87);
sub_4CCE0(&v87);
sub_4BC40(&v87, v72);
v27 = sub_4C940(v90, "resolution");
sub_4E1C0(v27, &v87);
sub_4CCE0(&v87);
sub_4D610(&v87, v101);
v28 = sub_4C940(v90, "brand");
sub_4E1C0(v28, &v87);
sub_4CCE0(&v87);
sub_4D610(&v87, v96);
v29 = sub_4C940(v90, "hardware");
sub_4E1C0(v29, &v87);
sub_4CCE0(&v87);
sub_4D610(&v87, v95);
v30 = sub_4C940(v90, "board");
sub_4E1C0(v30, &v87);
sub_4CCE0(&v87);
sub_4BC40(&v87, v74);
v31 = sub_4C940(v90, "dpi");
sub_4E1C0(v31, &v87);
sub_4CCE0(&v87);
sub_4BC40(&v87, &v84);
v32 = sub_4C940(v90, "device_id");
sub_4E1C0(v32, &v87);
sub_4CCE0(&v87);
sub_4BC40(&v87, v79);
v33 = sub_4C940(v90, "oaid");
sub_4E1C0(v33, &v87);
sub_4CCE0(&v87);
v67[0] = 0;
v67[1] = 0;
v68 = 0;
if( (v81 & 1) != 0 )
v34 = v82;
else
v34 = (unsigned __int64)(unsigned __int8)v81 >> 1;
if( v34 )
{
if( (v81 & 1) != 0 )
v35 = (char*)v83;
else
v35 = (char*)&v81 + 1;
}
else
{
if( (v69 & 1) != 0 )
v34 = v70;
else
v34 = (unsigned __int64)(unsigned __int8)v69 >> 1;
if( v34 )
{
if( (v69 & 1) != 0 )
v35 = (char*)v71;
else
v35 = (char*)&v69 + 1;
}
else
{
if( (v84 & 1) != 0 )
v35 = (char*)v86;
else
v35 = (char*)&v84 + 1;
if( (v84 & 1) != 0 )
v34 = v85;
else
v34 = (unsigned __int64)(unsigned __int8)v84 >> 1;
}
}
sub_4C800(v67, v35, v34);
sub_4BC40(&v87, v67);
v36 = sub_4C940(v90, "cdid");
sub_4E1C0(v36, &v87);
sub_4CCE0(&v87);
v65 = 0;
v66 = 0;
sub_4D1B0(&v65, 0);
sub_4BE00(&v87, v66 / 1000 + 1000 * v65);
v37 = sub_4C940(v90, "time_stamp");
sub_4E1C0(v37, &v87);
sub_4CCE0(&v87);
sub_4D850(&v87);
sub_4C5D0(&v63, &v87, v90);                   // 关键加密调用:sub_4C5D0(&v63, &v87, v90) - v87包含所有设备信息,v90是加密参数
v61 = 0u;

因为篇幅问题,部分代码省略

解密后可以知道输入都是封装进JSON里面了,通过HOOK 每次写入字段进JSON字符串的函数发现。

第一次:
返回值JSON字符串: {"app_name":"com.chaoxing.mobile","app_ver":"6.6.2","board":"taimen","brand":"google","cdid":"c713c8b5b4e24af4840ab6f1e44f6d6f","cdtype":"Pixel 2 XL","cpu_ar":"arm64-v8a,armeabi-v7a,armeabi","device_id":"c713c8b5b4e24af4840ab6f1e44f6d6f","dpi":"560","hardware":"taimen","mediaDrmId":"","oaid":"1004","os_lang":"","os_name":"REL","os_ver":"10","platform":"android","resolution":"1440*2712","time_stamp":1760434775772}
第二次:
返回值JSON字符串: {"app_name":"com.chaoxing.mobile","app_ver":"6.6.2","board":"taimen","brand":"google","cdid":"c713c8b5b4e24af4840ab6f1e44f6d6f","cdtype":"Pixel 2 XL","cpu_ar":"arm64-v8a,armeabi-v7a,armeabi","device_id":"c713c8b5b4e24af4840ab6f1e44f6d6f","dpi":"560","hardware":"taimen","mediaDrmId":"","oaid":"1004","os_lang":"","os_name":"REL","os_ver":"10","platform":"android","resolution":"1440*2712","time_stamp":1760434938312}

可以发现JSON内容只有一个会变,就是时间戳

先分析一下加密函数吧

这个函数会获取PackageManager,应用包名,PackageInfo

从PackageInfo提取:– versionName (版本名称)– versionCode (版本号)

这个会获取屏幕的宽度,高度,DPI

格式转为分辨率字符串  宽度 * 高度以及DPI值

 下面分析rsa_util::public_key_encrypt(v73, v76, &v77); 

-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC79d8Ot0hCbxxSISC6x8SCwTBspFSzlLKHJUYqoFNu1TSRaw4hEYkOnvEaL1VyoxV6HXcDrzwYvaFZaZaPQPFnfCHZy5dQwxcmifgSHqS+oKXw40Ys4cVIqnU5d90S7EWSRdBglX489jlqVaNcQSkDx2TYmC+DbAq9FV/BU09ISQIDAQAB
-----END PUBLIC KEY-----

这个函数就是处理前面的

strcpy(v65->__size, "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC79d8Ot0hCbxxSISC6x8SCwTBspFSzlLKHJUYqoFNu1TSRaw4hEYkOnvEaL1VyoxV6HXcDrzwYvaFZaZaPQPFnfCHZy5dQwxcmifgSHqS+oKXw40Ys4cVIqnU5d90S7EWSRdBglX489jlqVaNcQSkDx2TYmC+DbAq9FV/BU09ISQIDAQAB");
rsa_util::format_public_key(v76, &v74);

strcpy 将原始Base64公钥数据复制到内存,每64个字符插入换行符

rsa_util::format_public_key 将其格式化为完整的PEM格式这里的内容

再分析rsa_util::public_key_encrypt(v73, v76, &v77); 

先解析传入的公钥形式,获取密钥的大小,大小为1024个字节,RSA-1024,对于1024位RSA: max_block_size = 128 – 11 = 117字节。

计算最大的加密块大小,也就是v17 = v9 – 11;    这是PKCS#1 v1.5 (RSA_PKCS1_PADDING = 1)的标志性特征!

完整加密块格式 (128字节,对于1024位RSA):
┌────────┬────────┬────────────────────┬────────┬──────────────┐
│ 0x00   │ 0x02   │   PS (随机非零)     │ 0x00   │   数据       │
│ 1字节  │ 1字节  │   至少8字节         │ 1字节  │ 最多117字节  │
└────────┴────────┴────────────────────┴────────┴──────────────┘
固定       随机填充              分隔符    实际明文
总计: 1 + 1 + 8 + 1 = 11字节开销

填充方式为:PKCS#1 v1.5 (RSA_PKCS1_PADDING = 1)

使用while循环来逐块加密,每块最多117字节。

这个syslog函数根据参数来分析就是调用SSL的加密库,点击验证,显示是调用的外部函数,和我们猜测一样。剩下的就是base64

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_

这样就完成了data参数的分析

import json
import base64
import time
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
def generate_data(timestamp=None):
"""生成SSO登录的data参数"""
iftimestamp is None:
timestamp = int(time.time() * 1000)
# 1. 设备信息
device_info = {
"app_name""com.chaoxing.mobile",
"app_ver""6.6.2",
"board""taimen",
"brand""google",
"cdid""",
"cdtype""Pixel 2 XL",
"cpu_ar""arm64-v8a,armeabi-v7a,armeabi",
"device_id""",
"dpi""560",
"hardware""taimen",
"mediaDrmId""",
"oaid""1004",
"os_lang""",
"os_name""REL",
"os_ver""10",
"platform""android",
"resolution""1440*2712",
"time_stamp": timestamp
}
# 2. RSA分段加密
public_key_pem = """-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC79d8Ot0hCbxxSISC6x8SCwTBspFSzlLKHJUYqoFNu1TSRaw4hEYkOnvEaL1VyoxV6HXcDrzwYvaFZaZaPQPFnfCHZy5dQwxcmifgSHqS+oKXw40Ys4cVIqnU5d90S7EWSRdBglX489jlqVaNcQSkDx2TYmC+DbAq9FV/BU09ISQIDAQAB
-----END PUBLIC KEY-----"""
json_string = json.dumps(device_info, separators=(','':'))
public_key = RSA.import_key(public_key_pem)
cipher = PKCS1_v1_5.new(public_key)
max_chunk = public_key.size_in_bytes() - 11  # 117字节
encrypted_chunks = []
fori in range(0, len(json_string), max_chunk):
chunk = json_string[i:i + max_chunk].encode('utf-8')
encrypted_chunks.append(cipher.encrypt(chunk))
# 3. Base64编码
returnbase64.b64encode(b''.join(encrypted_chunks)).decode('utf-8')

文章来源:看雪学苑

黑白之道发布、转载的文章中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,任何人不得将其用于非法用途及盈利等目的,否则后果自行承担!

如侵权请私聊我们删文

END

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » APP风控参数分析&Frida绕过

评论 抢沙发

9 + 5 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮