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 代码的 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, {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.doHTTP/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调用
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.onClickg5
/* 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;elsev34 = (unsigned __int64)(unsigned __int8)v81 >> 1;if( v34 ){if( (v81 & 1) != 0 )v35 = (char*)v83;elsev35 = (char*)&v81 + 1;}else{if( (v69 & 1) != 0 )v34 = v70;elsev34 = (unsigned __int64)(unsigned __int8)v69 >> 1;if( v34 ){if( (v69 & 1) != 0 )v35 = (char*)v71;elsev35 = (char*)&v69 + 1;}else{if( (v84 & 1) != 0 )v35 = (char*)v86;elsev35 = (char*)&v84 + 1;if( (v84 & 1) != 0 )v34 = v85;elsev34 = (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参数"""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
夜雨聆风
