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, { onEnter: function (args) { var pathptr = args[0]; if (pathptr != null && pathptr != undefined) { var path = ptr(pathptr).readCString(); console.log("android_dlopen_ext:", path); } }, onLeave: function (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, { onEnter: function (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, { onEnter: function(args) { console.log("函数 #" + hook_num + " 被调用"); for (var i = 0; i < 8; i++) { try { args[i] = ptr(0); } catch(e) {} } }, onLeave: function(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')

文章来源:看雪学苑
华盟君