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 代码的 Runtime
2. Native 线程(只能执行 C/C++):纯粹的 Linux 线程
3. Native 线程(还能执行 Java):既能执行 C/C++ 代码,也能执行 Java 代码

Java线程创建流程

java层:Thread.start()

// /libcore/libart/src/main/java/java/lang/Thread.java public 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.cc  static void 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.cc void 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.cc void* 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_t     pthread_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 参数传递给 clone     int 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 - 线程 ID     pid_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.do HTTP/1.1User-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)_c713c8b5b4e24af4840ab6f1e44f6d6fAccept-Language: zh_CN_#HansContent-Type: application/x-www-form-urlencodedContent-Length: 741Host: sso.chaoxing.comConnection: Keep-AliveAccept-Encoding: gzipCookie: 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_1760100738596data=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调用

    public final void y5() {        AccountManager.E().K0("https://sso.chaoxing.com/apis/login/userLogin4Uname.do", 3);        new Handler().postDelayed(new c(), 150L);    }}


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

      public void onChanged(@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 */    public void g5() {        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;        else if (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);            return result;        };                 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 jsonimport base64import timefrom Crypto.PublicKey import RSAfrom Crypto.Cipher import PKCS1_v1_5def generate_data(timestamp=None):    """生成SSO登录的data参数"""    if timestamp 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 = []         for i 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编码    return base64.b64encode(b''.join(encrypted_chunks)).decode('utf-8')


自动草稿

文章来源:看雪学苑

本文来源看雪学苑,经授权后由华盟君发布,观点不代表华盟网的立场,转载请联系原作者。

发表回复