安卓逆向—AI跑通unidbg调用sgmain生成某ckey参数

admin 2026-09-18 06:29:41 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文介绍作者借助AI大模型辅助完成安卓逆向的过程,使用fridahook和unidbg框架调用sgmain生成ckey参数。文章详细描述了hook逻辑、递归打印函数、bytearray格式化工具及unidbg环境搭建等步骤,为逆向学习者提供了可参考的实战经验。 综合评分: 75 文章分类: 逆向分析,移动安全,安全工具


安卓逆向 — AI跑通unidbg调用sgmain生成某ckey参数

albao albao

逆向有你

2026年9月16日 09:33 河南

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

本人在安卓逆向方面属于小白,是跟着正己大佬《安卓逆向这档事》简单学习了一下基础知识,水平算是能够跟着大佬的攻略一步一步走下去的水平,因此本文可能存在大量解释有误的地方,欢迎大家指正。

最近恰逢春节在家无聊,同时刚好gemini 3.0 pro上线,想着通过大模型的能力,看是否能协助我完成“逆向”(本人的能力属实算不上)上的突破。之前学习过漁滒encryptR_client的生成教程,里面仅完成了encryptR_client“补环境”过程,最终没有完成ckey的生成。当时我就搭好了架子,但是同样一直存在一些卡点,最终一直没有完成ckey部分的生成。随着最近大模型能力的提升,我通过gemini(其他大模型大家可以自测)先后完成了某讯ckey、chacha20算法还原,以及本文要说的ckey的unidbg生成。(不得不感叹当前大模型能力的强大,能帮助我这样一个完全看不懂ida伪代码的人,完成算法还原)

回到正题,开工。

  1. 模拟器 or 真机 (安装frida-server)
  2. GetVideo 1.3.1(随便找一个应该都行)
  3. IDEA(unidbg项目)
  4. python (frida hook脚本调用)
  5. unidbg 0.9.8(最新版本)

一些基础知识,本文会一笔带过,大家可以通过其他文档学习,一定讲得比我好。另外遇到没讲清楚的地方,可以咨询大模型,本文涉及的所有代码,几乎全部由大模型完成。

二、frida hook,基础参数

需要hook的com.taobao.wireless.security.adapter.JNICLibrary下的doCommandNative方法。因为是动态加载的,所以普通的hook方法不好使,需要在BaseDexClassLoader加载class的时候进行hook。

Hook Logic

大致逻辑:hook BaseDexClassLoader,然后判断dexPath是否有sgmain,如果有,切换class loader之后,就可以hook到doCommandNative了。hook到方法之后,可以使用下面代码将入参和出参完整打印出来。这里就不贴代码了,大模型可以轻松搞定。

递归打印函数

 复制代码 隐藏代码
functionprintRecursive(obj, indent, prefix) {
    if (indent === undefined) indent = "";
    if (prefix === undefined) prefix = "";
    var currentIndent = indent + prefix;

    // Null Check
    if (obj === null || obj === undefined) {
        console.log(currentIndent + "null");
        return;
    }

    // JS Types
    var type = typeof obj;
    if (type === 'string') {
        console.log(currentIndent + "(JS-String) " + JSON.stringify(obj));
        return;
    }
    if (type === 'number') {
        console.log(currentIndent + "(JS-Number) " + obj);
        return;
    }
    if (type === 'boolean') {
        console.log(currentIndent + "(JS-Boolean) " + obj);
        return;
    }

    // JS Array (doCommandNative 的 n 参数)
    if (Array.isArray(obj)) {
        console.log(currentIndent + "(JS-Array) Length: " + obj.length + " {");
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;(var&nbsp;i =&nbsp;0; i < obj.length; i++) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;printRecursive(obj[i], indent +&nbsp;" &nbsp; &nbsp;",&nbsp;"["&nbsp;+ i +&nbsp;"] ");
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;console.log(indent +&nbsp;"}");
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return;
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;// Java Object
&nbsp; &nbsp;&nbsp;if&nbsp;(obj.getClass) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;clsName = obj.getClass().getName();

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(clsName ===&nbsp;"[B") {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// === Byte Array ===
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 直接调用上面的终极格式化函数
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;console.log(currentIndent +&nbsp;formatByteArray(obj));

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;elseif&nbsp;(clsName.startsWith("[L")) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// === Java Object Array ===
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 使用反射获取长度和元素
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;len =&nbsp;ReflectArray.getLength(obj);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;console.log(currentIndent +&nbsp;"("&nbsp;+ clsName +&nbsp;") Length: "&nbsp;+ len +&nbsp;" {");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;(var&nbsp;i =&nbsp;0; i < len; i++) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;subElem =&nbsp;ReflectArray.get(obj, i);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;printRecursive(subElem, indent +&nbsp;" &nbsp; &nbsp;",&nbsp;"["&nbsp;+ i +&nbsp;"] ");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;console.log(indent +&nbsp;"}");

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// === Ordinary Object ===
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;console.log(currentIndent +&nbsp;"("&nbsp;+ clsName +&nbsp;") "&nbsp;+ obj.toString());
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;catch&nbsp;(e) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;console.log(currentIndent +&nbsp;"[Error analysing Java Object]: "&nbsp;+ e);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return;
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;console.log(currentIndent +&nbsp;"(Unknown Type) "&nbsp;+ obj);
}

bytearray终极格式化工具:同时输出 Hex 和 ASCII

&nbsp;复制代码&nbsp;隐藏代码
functionformatByteArray(obj) {
&nbsp; &nbsp;&nbsp;try&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;len =&nbsp;ReflectArray.getLength(obj);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(len ===&nbsp;0)&nbsp;return"(byte[0])";

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;hexBuffer =&nbsp;"";
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;strBuffer =&nbsp;"";

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 阈值:如果在 Logcat 里打印太长会截断,这里限制一下预览长度
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 如果你想看全量,可以把 limit 调大,或者去掉
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;varPREVIEW_LIMIT&nbsp;=&nbsp;512;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;loopLen = (len >&nbsp;PREVIEW_LIMIT) ?&nbsp;PREVIEW_LIMIT&nbsp;: len;

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;(var&nbsp;i =&nbsp;0; i < loopLen; i++) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;b =&nbsp;ReflectArray.getByte(obj, i);

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 1. 处理 Hex
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;h = (b &&nbsp;0xFF).toString(16);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(h.length&nbsp;<&nbsp;2) h =&nbsp;"0"&nbsp;+ h;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; hexBuffer += h;

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 2. 处理 String (JS 硬解码)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 判断是否为可见 ASCII 字符 (32-126)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 0x20(空格) ~ 0x7E(~)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(b >=&nbsp;32&nbsp;&& b <=&nbsp;126) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; strBuffer +=&nbsp;String.fromCharCode(b);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 不可见字符用点代替,保持长度对齐,方便观察
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; strBuffer +=&nbsp;".";
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(len >&nbsp;PREVIEW_LIMIT) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; hexBuffer +=&nbsp;"...(truncated)";
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; strBuffer +=&nbsp;"...(truncated)";
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 格式化输出
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 如果数据很短,单行显示;很长,分行显示
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(len <&nbsp;32) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return"(byte["&nbsp;+ len +&nbsp;"]) hex: "&nbsp;+ hexBuffer +&nbsp;" | str: "&nbsp;+ strBuffer;
&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return"(byte["&nbsp;+ len +&nbsp;"])\n"&nbsp;+
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;" &nbsp; &nbsp; &nbsp;hex: "&nbsp;+ hexBuffer +&nbsp;"\n"&nbsp;+
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;" &nbsp; &nbsp; &nbsp;str: "&nbsp;+ strBuffer;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; }&nbsp;catch&nbsp;(e) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return"format_error: "&nbsp;+ e;
&nbsp; &nbsp; }
}

三、unidbg

unidbg架子

unidbg补环境主要会涉及JNI、文件以及系统调用,这里我将3种不同环境放到不同文件下。

JNICLibrary

&nbsp;复制代码&nbsp;隐藏代码
package&nbsp;com.taobao.wireless.security.adapter;

import&nbsp;com.github.unidbg.AndroidEmulator;
import&nbsp;com.github.unidbg.Module;
import&nbsp;com.github.unidbg.arm.backend.Unicorn2Factory;
import&nbsp;com.github.unidbg.file.linux.AndroidFileIO;
import&nbsp;com.github.unidbg.linux.android.AndroidARMEmulator;
import&nbsp;com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import&nbsp;com.github.unidbg.linux.android.AndroidResolver;
import&nbsp;com.github.unidbg.linux.android.dvm.*;
import&nbsp;com.github.unidbg.linux.android.dvm.array.ArrayObject;
import&nbsp;com.github.unidbg.linux.android.dvm.array.ByteArray;
import&nbsp;com.github.unidbg.linux.android.dvm.wrapper.DvmInteger;
import&nbsp;com.github.unidbg.linux.android.dvm.wrapper.DvmLong;
import&nbsp;com.github.unidbg.memory.Memory;
import&nbsp;com.github.unidbg.memory.SvcMemory;
import&nbsp;com.github.unidbg.unix.UnixSyscallHandler;

import&nbsp;java.io.File;
import&nbsp;org.apache.commons.logging.Log;
import&nbsp;org.apache.commons.logging.LogFactory;

publicclassJNICLibrary&nbsp;{
&nbsp; &nbsp;&nbsp;privatestaticfinalLoglog=&nbsp;LogFactory.getLog(JNICLibrary.class);
&nbsp; &nbsp;&nbsp;privatefinal&nbsp;AndroidEmulator emulator;
&nbsp; &nbsp;&nbsp;privatefinal&nbsp;VM vm;
&nbsp; &nbsp;&nbsp;privatefinal&nbsp;Memory memory;
&nbsp; &nbsp;&nbsp;privatefinal&nbsp;Module&nbsp;module;

&nbsp; &nbsp;&nbsp;privatefinal&nbsp;DvmClass myjniclass;

&nbsp; &nbsp;&nbsp;publicJNICLibrary()&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// syscall override
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;AndroidEmulatorBuilderbuilder=newAndroidEmulatorBuilder(false) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;@Override
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;public&nbsp;AndroidEmulator&nbsp;build()&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnnewAndroidARMEmulator(processName, rootDir, backendFactories) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;@Override
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;protected&nbsp;UnixSyscallHandler<AndroidFileIO>&nbsp;createSyscallHandler(SvcMemory svcMemory)&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnnewMySyscallHandler(svcMemory);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; };
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; };

&nbsp; &nbsp; &nbsp; &nbsp; emulator = builder
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .setProcessName("com.youku.phone")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .setRootDir(newFile("unidbg-android/src/main/java/com/taobao/wireless/security/adapter/rootfs"))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .addBackendFactory(newUnicorn2Factory(true))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .build();

&nbsp; &nbsp; &nbsp; &nbsp; emulator.getBackend().registerEmuCountHook(100000);
&nbsp; &nbsp; &nbsp; &nbsp; emulator.getSyscallHandler().setVerbose(true);
&nbsp; &nbsp; &nbsp; &nbsp; emulator.getSyscallHandler().setEnableThreadDispatcher(true);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 文件处理
&nbsp; &nbsp; &nbsp; &nbsp; emulator.getSyscallHandler().addIOResolver(newMyIOResolver());

&nbsp; &nbsp; &nbsp; &nbsp; memory = emulator.getMemory();
&nbsp; &nbsp; &nbsp; &nbsp; memory.setLibraryResolver(newAndroidResolver(23));
&nbsp; &nbsp; &nbsp; &nbsp; memory.setCallInitFunction(true);

&nbsp; &nbsp; &nbsp; &nbsp; vm = emulator.createDalvikVM();
&nbsp; &nbsp; &nbsp; &nbsp; vm.setVerbose(true);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 补环境
&nbsp; &nbsp; &nbsp; &nbsp; vm.setJni(newMyJni());

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;DalvikModuledalvikModule=&nbsp;vm.loadLibrary(newFile("unidbg-android/src/main/java/com/taobao/wireless/security/adapter/lib/libsgmainso-6.4.170.so"),&nbsp;true);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;module&nbsp;= dalvikModule.getModule();
&nbsp; &nbsp; &nbsp; &nbsp; vm.callJNI_OnLoad(emulator,&nbsp;module);
&nbsp; &nbsp; &nbsp; &nbsp; myjniclass = vm.resolveClass("com/taobao/wireless/security/adapter/JNICLibrary");
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;publicstaticvoidmain(String[] args)&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;JNICLibraryjnicLibrary=newJNICLibrary();
&nbsp; &nbsp; &nbsp; &nbsp; jnicLibrary.init();
&nbsp; &nbsp; }
}

(pkg名称,统一改成了com.youku.phone,没有使用getvideo)

MyJni

&nbsp;复制代码&nbsp;隐藏代码
package&nbsp;com.taobao.wireless.security.adapter;

import&nbsp;com.github.unidbg.linux.android.dvm.AbstractJni;
import&nbsp;org.apache.commons.logging.Log;
import&nbsp;org.apache.commons.logging.LogFactory;

publicclassMyJniextendsAbstractJni&nbsp;{
&nbsp; &nbsp;&nbsp;privatestaticfinalLoglog=&nbsp;LogFactory.getLog(MyJni.class);
}

MyIOResolver

&nbsp;复制代码&nbsp;隐藏代码
package&nbsp;com.taobao.wireless.security.adapter;

import&nbsp;com.github.unidbg.Emulator;
import&nbsp;com.github.unidbg.file.FileResult;
import&nbsp;com.github.unidbg.file.IOResolver;
import&nbsp;com.github.unidbg.file.linux.AndroidFileIO;
import&nbsp;org.apache.commons.logging.Log;
import&nbsp;org.apache.commons.logging.LogFactory;

publicclassMyIOResolverimplementsIOResolver<AndroidFileIO> {
&nbsp; &nbsp;&nbsp;privatestaticfinalLoglog=&nbsp;LogFactory.getLog(MyIOResolver.class);

&nbsp; &nbsp;&nbsp;@Override
&nbsp; &nbsp;&nbsp;public&nbsp;FileResult<AndroidFileIO>&nbsp;resolve(Emulator<AndroidFileIO> emulator, String pathname,&nbsp;int&nbsp;oflags)&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 打印所有文件访问请求,无论是否处理
&nbsp; &nbsp; &nbsp; &nbsp; log.info("[MyIOResolver] ========================> File open request: "&nbsp;+ pathname);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnnull;
&nbsp; &nbsp; }
}

MySyscallHandler

&nbsp;复制代码&nbsp;隐藏代码
package&nbsp;com.taobao.wireless.security.adapter;

import&nbsp;com.github.unidbg.Emulator;
import&nbsp;com.github.unidbg.arm.backend.Backend;
import&nbsp;com.github.unidbg.file.linux.AndroidFileIO;
import&nbsp;com.github.unidbg.linux.ARM32SyscallHandler;
import&nbsp;com.github.unidbg.memory.SvcMemory;
import&nbsp;com.github.unidbg.pointer.UnidbgPointer;
import&nbsp;org.apache.commons.logging.Log;
import&nbsp;org.apache.commons.logging.LogFactory;
import&nbsp;unicorn.ArmConst;

publicclassMySyscallHandlerextendsARM32SyscallHandler&nbsp;{
&nbsp; &nbsp;&nbsp;privatestaticfinalLoglog=&nbsp;LogFactory.getLog(MySyscallHandler.class);

&nbsp; &nbsp;&nbsp;publicMySyscallHandler(SvcMemory svcMemory)&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;super(svcMemory);
&nbsp; &nbsp; &nbsp; &nbsp; setVerbose(true);&nbsp;// 可按需开启详细日志
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;@Override
&nbsp; &nbsp;&nbsp;publicvoidhook(Backend backend,&nbsp;int&nbsp;intno,&nbsp;int&nbsp;swi, Object user)&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; Emulator<AndroidFileIO> emulator = (Emulator<AndroidFileIO>) user;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;UnidbgPointerpc=&nbsp;UnidbgPointer.register(emulator, ArmConst.UC_ARM_REG_PC);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;intNR=&nbsp;backend.reg_read(ArmConst.UC_ARM_REG_R7).intValue();
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 打印所有系统调用,无论是否处理
&nbsp; &nbsp; &nbsp; &nbsp; log.info("[MySyscallHandler] ========================>");
&nbsp; &nbsp; &nbsp; &nbsp; log.info("syscall intno=0x"&nbsp;+ Integer.toHexString(intno) +&nbsp;", swi="&nbsp;+ swi +&nbsp;", NR="&nbsp;+ NR +&nbsp;", pc="&nbsp;+ pc);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;super.hook(backend, intno, swi, user);
&nbsp; &nbsp; }
}

初始化

初始化代码

初始化流程其他很多文档都讲清楚了的,大致如下:10101 => 10102(sgmain) => 10102(securitybody) => 10102(avmp)。 (init代码和后面avmp调用方法大家可以参考别的文章,这里就不贴了)

补环境

日志等级调成INFO(DEBUG太慢了),从头开始看运行流程。

1. /dev/properties

创建文件夹即可,其中的内容如果没有似乎不影响。 暂时不用管,测试下来如果从真机pull下来这部分文件,后面补环境会少一些步骤,如果不补,也能够成功。

2. /proc/stat

目测unidbg已经补了?可以不用管。

3. getPackageCodePath

遇到第一个需要补的。

&nbsp;复制代码&nbsp;隐藏代码
case"com/youku/phone/App->getPackageCodePath()Ljava/lang/String;": {
&nbsp; &nbsp;&nbsp;returnnewStringObject(vm, dataAppPath +&nbsp;"/base.apk"); &nbsp; &nbsp;&nbsp;// dataAppPath = "/data/app/com.youku.phone"
}

同时,将base.apk复制到对应路径。(因为一开始设置了rootDir,设置的rootDir就是”/”目录,其他文件/目录可以直接往里面复制,不用处理所有文件) 对于不清楚的文件,可以到真机中去看一眼。

这里因为用的是getvideo,所以存在一个隐形的坑,base.apk需要用getvideo解压后里面的一个Youku_xxxx.apk。后续会从该文件中读取关键安全信息。

4. getFilesDir

&nbsp;复制代码&nbsp;隐藏代码
case"com/youku/phone/App->getFilesDir()Ljava/io/File;": {
&nbsp; &nbsp;&nbsp;return&nbsp;ProxyDvmObject.createObject(vm,&nbsp;newFile("/data/user/0/com.youku.phone/files"));
}

同样,相应的文件夹创建好。

5. getAbsolutePath

&nbsp;复制代码&nbsp;隐藏代码
case"java/io/File->getAbsolutePath()Ljava/lang/String;": {
&nbsp; &nbsp;&nbsp;returnnewStringObject(vm, dvmObject.getValue().toString());
}
6. getApplicationInfo nativeLibraryDir等

常规需要补的环境,可以上网搜,或者直接问大模型。本文后续只介绍一些可能会踩坑的。

7. /proc/self/status /proc/{PID}/status

这里强烈推荐看看正己大佬的《安卓逆向这档事》第二十五课、Unidbg之补完环境我就睡(中)。很多可能的坑大佬已经介绍了怎么绕过去。

&nbsp;复制代码&nbsp;隐藏代码
Stringpkg=&nbsp;emulator.getProcessName();
intpid=&nbsp;emulator.getPid();
if&nbsp;(pathname.equals("/proc/self/status") || pathname.equals("/proc/"&nbsp;+ pid +&nbsp;"/status")) {
&nbsp; &nbsp;&nbsp;// 返回一个包含 "TracerPid: 0" 的文件内容,表示未被调试
&nbsp; &nbsp;&nbsp;StringstatusContent="Name:\t"&nbsp;+ pkg +&nbsp;"\n"&nbsp;+
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"Umask:\t0077\n"&nbsp;+
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"State:\tS (sleeping)\n"&nbsp;+
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"Tgid:\t"&nbsp;+ pid +&nbsp;"\n"&nbsp;+
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"Pid:\t"&nbsp;+ pid +"\n"&nbsp;+
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"PPid:\t1\n"&nbsp;+
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"TracerPid:\t0\n";&nbsp;// 关键行
&nbsp; &nbsp;&nbsp;return&nbsp;FileResult.success(newByteArrayFileIO(oflags, pathname, statusContent.getBytes()));
}
8. /proc/{PID}/stat & /proc/{PID}/wchan

不懂的地方,优先google一下,看看有没有别人遇到过。github issues

&nbsp;复制代码&nbsp;隐藏代码
if&nbsp;(("/proc/"&nbsp;+ emulator.getPid() +&nbsp;"/stat").equals(pathname)) {
&nbsp; &nbsp;&nbsp;return&nbsp;FileResult.success(newByteArrayFileIO(oflags, pathname, (emulator.getPid() +
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;" (a.out) R 6723 6873 6723 34819 6873 8388608 77 0 0 0 41958 31 0 0 25 0 3 0 5882654 1409024 56 4294967295 134512640 134513720 3215579040 0 2097798 0 0 0 0 0 0 0 17 0 0 0\n").getBytes()));
}
if&nbsp;(("/proc/"&nbsp;+ emulator.getPid() +&nbsp;"/wchan").equals(pathname)) {
&nbsp; &nbsp;&nbsp;return&nbsp;FileResult.success(newByteArrayFileIO(oflags, pathname,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"sys_epoll".getBytes()));
}
9. registerAppLifeCyCleCallBack

参考安卓逆向小案例,很多本文需要补的环境,几乎都能从别人的文档中搜到。

需要注意的就是,尽量所有补的环境都加上log,方便后续debug。

&nbsp;复制代码&nbsp;隐藏代码
@Override
publicvoidcallStaticVoidMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg)&nbsp;{
&nbsp; &nbsp; log.info("callStaticVoidMethod signature="&nbsp;+ signature);
&nbsp; &nbsp;&nbsp;return&nbsp;;
}
10. SPUtility2->readFromSPUnified

参考安卓逆向小案例。

结论:在/data/user/0/{PKG}/files下面,,有个SGMANAGER_DATA2文件,里面JSON格式保存key-value。获取数据方法是通过arg1 + "_" + arg2作为key去取

&nbsp;复制代码&nbsp;隐藏代码
case"com/taobao/wireless/security/adapter/common/SPUtility2->readFromSPUnified(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;": {
&nbsp; &nbsp;&nbsp;Stringarg1=&nbsp;varArg.getObjectArg(0).getValue().toString();
&nbsp; &nbsp;&nbsp;Stringarg2=&nbsp;varArg.getObjectArg(1).getValue().toString();
&nbsp; &nbsp;&nbsp;Stringkey=&nbsp;arg1 +&nbsp;"_"&nbsp;+ arg2;
&nbsp; &nbsp; System.out.println("KEY==> "+ key);
&nbsp; &nbsp;&nbsp;JSONObjectdata=&nbsp;JSONObject.parseObject("{\"dynamicreid_dynamicreid\":\"xxxx\",\"dynamicrsid_dynamicrs这里还有很多,直接省略了\"}");
&nbsp; &nbsp;&nbsp;Stringresult=&nbsp;data.getString(key);
&nbsp; &nbsp; System.out.println("data ==> "&nbsp;+ result);
&nbsp; &nbsp;&nbsp;if&nbsp;(result !=&nbsp;null) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnnewStringObject(vm, result);
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;returnnull;
}
11. JNIBridge->registerInfoCallback

返回第二个参数。

12. UserTrackMethodJniBridge->utAvaiable

返回1。

13. SG_INNER_DATA & SG_USER_DATA
  1. SG_INNER_DATA: 从真机拿到文件,放到rootfs对应路径。这个文件会自动生成一个,替换他。
  2. SG_USER_DATA: 没看到,暂时空着没管。
14. SPUtility2->saveToFileUnifiedForNative

看起来是存到刚才的JSON里面,当你补了SG_INNER_DATA这个文件之后,这个方法就用不到了,所以不补。

15. UserTrackMethodJniBridge->addUtRecord

返回1就行。感兴趣可以将参数都打印出来看看,应该是用来记录设备行为的。

16. SGPluginExtras->slot

get方法时将值保存下来,set的时候返回值。

小结

至此,so的初始化就结束了,前面如果遇到乱七八糟的报错,很有可能是文件/目录访问没有处理好,相应的文件和目录都存在的情况下,基本没有什么大坑。

avmp计算ckey

avmp初始化 & ckey计算

流程:通过60901初始化avmp,然后60902获取ckey

经过测试,60901获取到avmp instance之后,应该可以复用。

_str输入为:ccode=01010101&client_ip=192.168.1.1&client_ts=1770000000&utid=xxxx&vid=XMjk4ODAyMzIyOA==

补环境

1. avmp补环境

上面已经讲了base.apk存在坑,如果没有踩这个坑,这里简单补几个环境应该就能完成avmp初始化了。后面很多不太常见的补环境,基本都是依靠gemini帮我补完的,大家也可以试试请教一下大模型。

2. [B->getClass()Ljava/lang/Class;

问大模型:

&nbsp;复制代码&nbsp;隐藏代码
case"[B->getClass()Ljava/lang/Class;": {
&nbsp; &nbsp;&nbsp;// 获取调用这个方法的对象,也就是 byte 数组本身
&nbsp; &nbsp;&nbsp;Objectvalue=&nbsp;dvmObject.getValue();

&nbsp; &nbsp;&nbsp;// 方法1:如果 value 本身就是 byte[],可以直接获取它的 Class 对象
&nbsp; &nbsp;&nbsp;if&nbsp;(value&nbsp;instanceofbyte[]) {
&nbsp; &nbsp; &nbsp; &nbsp; Class<?> clazz = value.getClass();
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 将 Java 的 Class 对象转换为 unidbg 的 DvmObject
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 这里可以直接使用 ProxyDvmObject.createObject 来封装
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;byte[] byteArray = (byte[]) value;
&nbsp; &nbsp; &nbsp; &nbsp; log.info("=== [B->getClass() 调用 ===");
&nbsp; &nbsp; &nbsp; &nbsp; log.info("Byte数组长度: "&nbsp;+ byteArray.length);
&nbsp; &nbsp; &nbsp; &nbsp; log.info("Byte数组内容(hex): "&nbsp;+ bytesToHex(byteArray));
&nbsp; &nbsp; &nbsp; &nbsp; log.info("Byte数组内容(ASCII): "&nbsp;+&nbsp;newString(byteArray).replaceAll("[^\\x20-\\x7E]",&nbsp;"."));
&nbsp; &nbsp; &nbsp; &nbsp; log.info("=========================");
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;ProxyDvmObject.createObject(vm, clazz);
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;// 方法2:或者直接返回一个代表 byte[] 类型的 Class 对象
&nbsp; &nbsp;&nbsp;// 这种方式更直接,不依赖于实际对象
&nbsp; &nbsp; Class<?> byteArrayClass = (newbyte[0]).getClass();
&nbsp; &nbsp;&nbsp;return&nbsp;ProxyDvmObject.createObject(vm, byteArrayClass);
}
3. DeviceInfoCapturer->doCommandForString

获取设备信息,大模型给了hook代码。需要什么参数,获取什么参数。

&nbsp;复制代码&nbsp;隐藏代码
functiondoCom(i) {
&nbsp; &nbsp;&nbsp;Java.perform(function() {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Java.enumerateClassLoaders({
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"onMatch":&nbsp;function(loader) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(loader.toString().indexOf("libsgmain.so") >=&nbsp;0&nbsp;) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Java.classFactory.loader&nbsp;= loader;&nbsp;// 将当前class factory中的loader指定为我们需要的
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;console.log("loader = ",loader.toString());
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; },
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"onComplete":&nbsp;function() {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;console.log("success");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; });
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;letDeviceInfoCapturer&nbsp;=&nbsp;Java.classFactory.use("com.taobao.wireless.security.adapter.datacollection.DeviceInfoCapturer");
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;var&nbsp;result =&nbsp;DeviceInfoCapturer.doCommandForString(i);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;console.log("result ==> "&nbsp;+ result)
&nbsp; &nbsp; })
}
4. ArtMethod 结构体检查(gemini这么说)

直接上图:

这个问题困扰了我很久,一直不知道怎么回事,直到最近问了gemini 3.0 pro,他给了我一个解释:

&nbsp;复制代码&nbsp;隐藏代码
发生了什么?
1.&nbsp;SO 想要获取设备的 IMEI,所以它去拿 TelephonyManager.getDeviceId() 的方法 ID。
2.&nbsp;Unidbg 作为一个模拟器,为了方便管理,把方法的 Hash 值(0x63bb9035)作为 MethodID 返回给了 SO。
3.&nbsp;关键来了:在真实的 Android 系统中,MethodID 其实是一个内存指针,指向底层的 ArtMethod 结构体。libsgmain 非常狡猾,它拿到这个 ID 后,并没有立刻去调用方法,而是把它当成内存指针,硬加上偏移量(0x63bb9035 + 5 = 0x63bb903a),试图去读取内存里面的方法的 Access Flags 或者入口点,以此来判断这个方法有没有被 Frida 或 Xposed Hook 掉!
4.&nbsp;因为 0x63bb9035 只是 Unidbg 算出来的一个 Hash 数字,这块内存地址在 Unicorn 引擎里根本没有被映射(Unmapped),所以 SO 一读就直接触发了内存访问异常,导致进程崩溃。

解决方案
要跑通这个逻辑,我们需要做两步:第一步是“骗”过它的 Hook 检测,第二步是给它返回一个假的 IMEI。

这个解释是否正确,我也不清楚,各位大佬如果有懂的可以评论回复。通过gemini给的方法,我发现压根不用在对应地址写入数据,只需要把内存空间开辟出来,读取内存不报错,这里就可以跑过去了。

当然,有可能是这里处理地不够好,导致后面生成的ckey长度会比frida hook出来的短上一些,好在可以正常使用。

5. svc number: 65

继续给大模型,但是这里他判断错了,他把NR=65的方法给我了。

这里显示NR=20,svc=65。按照正己大佬《安卓逆向这档事》第二十六课、Unidbg之补完环境我就睡(下)中的解释,这里应该是JNI调用(svcNumber 不等于 0x0),但是后面又没有UnsupportedOperationException。目前超出小白的能力范畴了,使用了最简单粗暴的方法,啥也不干,直接return。(留个作业后续再看)

&nbsp;复制代码&nbsp;隐藏代码
// MySyscallHandler中
if&nbsp;(NR ==&nbsp;20&nbsp;&& swi ==&nbsp;0x41) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return;
}
5. currentActivityThread

之前抄另一个文档,返回了Thread.currentThread(),后来发现这俩有区别,一个是java/lang/Thread,这里是android/app/ActivityThread,正常补就行。

6. android/app/ActivityThread->mActivities:Ljava/util/HashMap;

暂时new了个空的给过去。

四、总结

至此,大家应该和我一样,拿到最终的ckey了,但是因为很多地方处理并不是很完善,只能说跑出了可用的结果。另外如果想要跑encryptR_client,直接调用应该就能出,环境全部补好了的。

念念不忘,必有回响。前前后后开始->放弃->开始->放弃….重复了n次,最终借用gemini的能力跑通了,虽然在逆向学习中,自己仍然是小白,逆向的知识似乎也没有什么提升,但是通过使用大模型等能力完成多年未完成工程,心里还是十分开心的。

最后,如果还有精力,也会分享使用gemini + unidbg + ida还原算法的方法。整体来说本文难度还是比还原算法简单很多。

完结,撒花🎉🎉!!!

## 背景你现在是精通安卓逆向的工程师,需要你帮助完成unidbg调用一个so的签名。
现在我已经通过frida,hook到了签名函数ckey的输入和输出,现在我希望你帮助我使用unidbg模拟so的环境,并成功计算出ckey。其中我可以给你ida解析的so的伪代码,以及帮你使用frida hook更多参数供你分析。其他ida的脚本,我也可以尝试帮你使用。(我是小白,如果需要ida插件,需要详细说明一次)
## 要求1.&nbsp;unidbg补环境时,对于常见环境,可以直接补充,对于不常见或者拿不准的地方,可以使用frida hook到相应参数再补充。2.&nbsp;每次回复,可以简单解释你的分析,不要太多,因为我看不太懂,我不需要知道原理,我只需要协助你运行代码,帮你获取so的伪代码。
## 技能1.&nbsp;可以让我协助你hook函数参数,inline hook等各种frida支持的方式。2.&nbsp;可以让我给你提供so的伪代码。3.&nbsp;可以让我使用ida的插件协助分析(注意,我是小白,如果使用这个技能,需要你详细指导我一次使用方法)。4.&nbsp;其他可能有助于你分析的方法,可以教我使用。
## 目前进展
### unidbg脚本所有unidbg脚本 balabala
### frida hook脚本hook脚本
### frida hook结果hook结果
### 简单解释现有hook结果1.&nbsp;10101、10102都是初始化工作;2.&nbsp;60901初始化avmp,并拿到实例;3.&nbsp;60902是输入参数,并获取签名ckey;(重点调用)4.&nbsp;10601是获取R,可以暂时忽略。
## 任务基于目前frida hook的结果,协助我完成unidbg脚本补环境的过程,其中存在大量环境监测的地方,需要你凭借经验和frida hook,拿到真机环境数据,帮助我最终拿到ckey的模拟计算。

·今 日 推 荐·

| | | | — | — | | | |


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:逆向有你 albao albao《安卓逆向 — AI跑通unidbg调用sgmain生成某ckey参数》

评论:0   参与:  0