下载unidbg

地址:https://github.com/zhkl0228/unidbg

unidbg需要在IDEA端进行调试一下,等待依赖自动安装后,运行unidbg-android/src/test/java/com/bytedance/frameworks/core/encrypt/TTEncrypt.java文件。如果显示如下则代表运行正常。

image-20220708143215761

使用一个基础的模板,后续可以根据此模板来进行修改

public SignUtil() {
       emulator = AndroidEmulatorBuilder.for32Bit()
               .setProcessName("com.anjuke.android.app")
               .build();
       Memory memory = emulator.getMemory();
       memory.setLibraryResolver(new AndroidResolver(23));
       vm = emulator.createDalvikVM();
       vm.setDvmClassFactory(new ProxyClassFactory());
       vm.setVerbose(false);
       DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/example_binaries/armeabi-v7a/libsignutil.so"), false);
       cSignUtil = vm.resolveClass("com/anjuke/mobile/sign/SignUtil");
       dm.callJNI_OnLoad(emulator);
   }

其中需要注意的是:

  1. setProcessName为进程名,可以自己去赋值。
  2. setLibraryResolver有19、23两个SDK可以选,一般使用23。
  3. createDalvikVM的时候里面为APK的目录,可以为空。
  4. loadLibrary为需要加载到内存的so文件。
  5. resolveClass为调用了so加密函数的Java代码位置。
  6. callJNI_OnLoad为调用JNI_load,有时候这个方法会报错Illegal JNI version,这是文件修不不正常导致。

so文件测试

先用一个吾爱老哥的文件进行一下测试使用

地址:https://www.52pojie.cn/thread-1322512-1-1.html

编写一个TestJni.java,需要注意的是,这里做了一点修改,由于AndroidARMEmulator为受保护的方法,并不能直接调用,可能是unidbg做了变化,修改为AndroidEmulatorBuilder,代码为:

package com.misaki;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.ARMEmulator;
import com.github.unidbg.arm.backend.DynarmicFactory;
import com.github.unidbg.linux.android.AndroidARMEmulator;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.jni.ProxyClassFactory;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;

import java.io.File;
import java.io.IOException;

public class TestJni extends AbstractJni {
    // ARM模拟器
    private final AndroidEmulator emulator;
    // vm
    private final VM vm;
    // 载入的模块
    private final Module module;
    
    private final DvmClass TTEncryptUtils;

    /**
     *
     * @param soFilePath   需要执行的so文件路径
     * @param classPath    需要执行的函数所在的Java类路径
     * @throws IOException
     */
    public TestJni(String soFilePath, String classPath) throws IOException {
        // 创建app进程,包名可任意写
        emulator = AndroidEmulatorBuilder
                        .for32Bit()
                        .addBackendFactory(new DynarmicFactory(true))
                        .setProcessName("com.rs")
                        .build();
        Memory memory = emulator.getMemory();
        // 作者支持19和23两个sdk
        memory.setLibraryResolver(new AndroidResolver(23));
        // 创建DalvikVM,利用apk本身,可以为null
        vm = emulator.createDalvikVM((File) null);
        vm.setDvmClassFactory(new ProxyClassFactory());
        vm.setVerbose(true);
//        vm.setJni(this);
        // (关键处1)加载so,填写so的文件路径
        DalvikModule dm = vm.loadLibrary(new File(soFilePath), false);
        // 调用jni, 加入此代码有可能会报错 Illegal JNI version,环境原因
//        dm.callJNI_OnLoad(emulator);

        module = dm.getModule();
        // (关键处2)加载so文件中的哪个类,填写完整的类路径
        TTEncryptUtils = vm.resolveClass(classPath);
    }

    /**
     * 调用so文件中的指定函数
     * @param methodSign 传入你要执行的函数信息,需要完整的smali语法格式的函数签名
     * @param args       是即将调用的函数需要的参数
     * @return 函数调用结果
     */
    private String myJni(String methodSign, Object ...args) {
        // 使用jni调用传入的函数签名对应的方法()
        Object value = TTEncryptUtils.callStaticJniMethodObject(emulator, methodSign, args).getValue();
        return value.toString();
    }


    /**
     * 关闭模拟器
     * @throws IOException
     */
    private void destroy() throws IOException {
        emulator.close();
        System.out.println("emulator destroy...");
    }

    public static void main(String[] args) throws IOException {
        // 1、需要调用的so文件所在路径
        String soFilePath = "unidbg-android/src/test/resources/myso/libinyu-lib.so";
        // 2、需要调用加密函数所在的Java类完整路径,比如a/b/c/d等等,注意需要用/代替点,只需要填写即可。
        String classPath = "water/android/io/inyustring/InyuString";
        // 3、需要调用方法,再jadx中找到对应的方法,然后点击下面的Smail,复制方法的Smail代码。
        String methodSign = "getUrlSign(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;";
        TestJni testJni = new TestJni(soFilePath, classPath);
        // 输出getGameKey方法调用结果
        System.err.println(testJni.myJni(methodSign,"/v1/login/mobile/code?mobile=13888888888&country_code=0086&__plat=android&__version=2.21.0&__app=inyu","1607237431"));
        testJni.destroy();
    }
}

其中大部分都不需要修改,只需要修改main中的参数,classPath是加密so函数的Java代码所在类,但是并不需要实际添加进去。运行的结果如下所示。

image-20220708164633607

APP

测试APP来源猿人学的一次活动

地址:http://download.python-spider.com/yuanrenxuem106.apk

WP:https://mp.weixin.qq.com/s/CXsbzt4IWyDaV006JdIYsQ

先找到这个包的包名com.yuanrenxue.match2022,基本在这个目录下,这个app做了代码混淆,先不管。

根据提示找到/app2这个接口对应的代码处。

image-20220709123156188

其中sign为加密后的字符串,搜索这个字符串,在包下面找相关的字段。

只有两处相关com.yuanrenxue.match2022.fragment.challengecom.yuanrenxue.match2022.security

在类ChallengeTwoFragment中可以看到明显的第二题代码,查看最后的sign的加密。加载了match02的so文件。

image-20220709130744364

image-20220709130754983

传入一个参数,类型为str,然后需要找个调用这个sign的地方,看上面的调用。

image-20220709140354427

这里有两个需要注意,其中v0代表的是获取string中的资源,根据对应的查找发现是%d:%d

还有一个是int类型的v1,其中 this.OooO0O0代表是page字段,这个在下面也有定义,arg5.OooO00o()赋值为long类型的v5,也就是这个是ts字段。所以v1就是一个page和ts的数组。

最后返回的时候,可以看到这个函数和一开始的是一致的。sign中的字符串格式化就是String.format("%d:%d", {page, ts})

知道传入sign的字符串参数的形式后,我们自己来调用so来输出。

package com.misaki;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.ARMEmulator;
import com.github.unidbg.arm.backend.DynarmicFactory;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.linux.android.AndroidARMEmulator;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.jni.ProxyClassFactory;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;

import java.io.File;
import java.io.IOException;

public class TestJni extends AbstractJni {
    // ARM模拟器
    private final AndroidEmulator emulator;
    // vm
    private final VM vm;
    // 载入的模块
    private final Module module;
    
    private final DvmClass TTEncryptUtils;


    public TestJni(String soFilePath, String classPath) throws IOException {
        emulator = AndroidEmulatorBuilder
                        .for64Bit()
                        .addBackendFactory(new Unicorn2Factory(true))
                        .setProcessName("com.yuanrenxue.match2022")
                        .build();
        Memory memory = emulator.getMemory();
        memory.setLibraryResolver(new AndroidResolver(23));
        vm = emulator.createDalvikVM((File) null);
        vm.setDvmClassFactory(new ProxyClassFactory());
        vm.setVerbose(true);
        vm.setJni(this);
        DalvikModule dm = vm.loadLibrary(new File(soFilePath), true);
        dm.callJNI_OnLoad(emulator);
        module = dm.getModule();
        TTEncryptUtils = vm.resolveClass(classPath);
    }

    private String myJni(String methodSign, Object ...args) {
        Object value = TTEncryptUtils.callStaticJniMethodObject(emulator, methodSign, args).getValue();
        return value.toString();
    }

    private void destroy() throws IOException {
        emulator.close();
        System.out.println("emulator destroy...");
    }

    public static void main(String[] args) throws IOException {
        String soFilePath = "unidbg-android/src/test/resources/myso/libmatch02.so";
        String classPath = "com/yuanrenxue/match2022/fragment/challenge/ChallengeTwoFragment";
        String methodSign = "sign(java/lang/String;)java/lang/String;";
        TestJni testJni = new TestJni(soFilePath, classPath);
        System.err.println(testJni.myJni(methodSign,"1:1657348328"));
));
        testJni.destroy();
    }
}

image-20220709144802020

然后再来看一下第四题,这个看完后发现基本跟第二题没太大区别。同样找到sign方法,里面有两个参数,去找这两个参数对应的值。

image-20220709152257853

代码为:

private void lambda$initListeners$0(o0000O arg8) {
        this.OooO0O0 = 1;
        long v1 = System.currentTimeMillis();
        String v3 = this.getResources().getString(0x7F100053);  // string:format_match_04_sign "%d:%d"
        Object[] v4 = {((int)this.OooO0O0), ((long)v1)};
        oOO00O.OooO0O0.OooO0O0 v3_1 = oOO00O.OooO0O0.OooO0oO().OooO00o(this.OooO0O0);
        OooO0O0 v1_1 = this.OooO0Oo;
        com.yuanrenxue.match2022.fragment.challenge.ChallengeFourFragment.OooO00o v2 = new o00O0.OooO0O0(arg8) {
            public final o0000O OooO0O0;
            public final ChallengeFourFragment OooO0OO;
            public static final o00o00o.OooOo00.OooO00o OooO0Oo;

            {
                com.yuanrenxue.match2022.fragment.challenge.ChallengeFourFragment.OooO00o.OooO0OO();
            }

            {
                ChallengeFourFragment.this = arg1;
                this.OooO0O0 = arg2;
                super();
            }

            @Override  // o00O0.OooO0O0
            public void OooO0O0() {
                this.OooO0O0.OooO0O0();
            }

            @Override  // o00O0.OooO0O0
            public static void OooO0OO() {
                OooO v8 = new OooO("ChallengeFourFragment.java", com.yuanrenxue.match2022.fragment.challenge.ChallengeFourFragment.OooO00o.class);
                com.yuanrenxue.match2022.fragment.challenge.ChallengeFourFragment.OooO00o.OooO0Oo = v8.OooO0oO("method-execution", v8.OooO0o("1", "onError", "com.yuanrenxue.match2022.fragment.challenge.ChallengeFourFragment$1", "java.lang.Throwable", "t", "", "void"), 103);
            }

            public static final void OooO0o(com.yuanrenxue.match2022.fragment.challenge.ChallengeFourFragment.OooO00o arg0, Throwable arg1, OooOo00 arg2) {
                arg0.super.onError(arg1);
                arg1.printStackTrace();
                arg0.OooO0O0.OooO0O0();
            }

            @Override  // o00O0.OooO0O0
            public void OooO0o0(Object arg1) {
                this.OooO0oO(((oOO00O.OooO0OO)arg1));
            }

            public void OooO0oO(oOO00O.OooO0OO arg5) {
                ArrayList v0 = new ArrayList();
                v0.add(new o00O000.OooO0OO("padding"));
                Iterator v5 = arg5.OooO0O0().iterator();
                while(v5.hasNext()) {
                    v5.next();
                    v0.add(new o00O000.OooO0OO(""));
                }

                ChallengeFourFragment.OooO(ChallengeFourFragment.this).OooO(v0);
                this.OooO0O0.OooO0O0();
            }

            @Override  // o00O0.OooO0O0
            public void onError(Throwable arg5) {
                OooOo00 v0 = OooO.OooO0OO(com.yuanrenxue.match2022.fragment.challenge.ChallengeFourFragment.OooO00o.OooO0Oo, this, this, arg5);
                o000O0.OooO0OO().OooO0O0(new OooOOO0(new Object[]{this, arg5, v0}).OooO0O0(0x11010));
            }
        };
        v1_1.OooO0OO(((oOO00O.OooO0O0)v3_1.OooO0O0(this.sign(String.format(v3, v4), v1)).OooO0OO(v1).build()), v2);
    }

还是字符串格式化,还是%d:%d类型的格式化,但是参数变了。v4是{((int)this.OooO0O0), ((long)v1)},而this.OooO0O0上面有赋值,应该也还是page,v1是System.currentTimeMillis(),获取当前的总毫秒数。所以第一页的时候,v4就是{1:1657351690000}

所以sign的传参就是this.sign(String.format("%d:%d", {1,1657351690000}), 1657351690000)

修改上面的Java代码来调用。

package com.misaki;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.DynarmicFactory;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.jni.ProxyClassFactory;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;

import java.io.File;
import java.io.IOException;

public class TestJni extends AbstractJni {
    // ARM模拟器
    private final AndroidEmulator emulator;
    // vm
    private final VM vm;
    // 载入的模块
    private final Module module;
    private final DvmClass TTEncryptUtils;


    public TestJni(String soFilePath, String classPath) throws IOException {
        emulator = AndroidEmulatorBuilder
                        .for64Bit()
                        .addBackendFactory(new DynarmicFactory(true))
                        .setProcessName("com.yuanrenxue.match2022")
                        .build();
        Memory memory = emulator.getMemory();
        memory.setLibraryResolver(new AndroidResolver(23));
        vm = emulator.createDalvikVM((File) null);
        vm.setDvmClassFactory(new ProxyClassFactory());
        vm.setVerbose(true);
        vm.setJni(this);
        DalvikModule dm = vm.loadLibrary(new File(soFilePath), true);
        dm.callJNI_OnLoad(emulator);

        module = dm.getModule();
        TTEncryptUtils = vm.resolveClass(classPath);
    }

    private String myJni(String methodSign, String data1, Long data2) {
        Object value = TTEncryptUtils.callStaticJniMethodObject(emulator, methodSign, data1, data2).getValue();
        return value.toString();
    }

    private void destroy() throws IOException {
        emulator.close();
        System.out.println("emulator destroy...");
    }

    public static void main(String[] args) throws IOException {
        String soFilePath = "unidbg-android/src/test/resources/myso/libmatch04.so";
        String classPath = "com/yuanrenxue/match2022/fragment/challenge/ChallengeFourFragment";
        String methodSign = "sign(java/lang/String;J)java/lang/String;";
        TestJni testJni = new TestJni(soFilePath, classPath);
        System.err.println(testJni.myJni(methodSign,"1:1657351690000", 1657351690000L));
));
        testJni.destroy();
    }
}

结果为:

image-20220709154136336

备注:

这里是记录一些类型的描述符,方便后续修改查询

变量类型 类型描述符 包装类 包装类类型描述符(包含分号)
int I(大写i) Integer Ljava/lang/Integer;
short S Short Ljava/lang/Short;
long J Long Ljava/lang/Long;
boolean Z Boolean Ljava/lang/Boolean;
char C Character Ljava/lang/Character;
byte B Byte Ljava/lang/Byte;
float F Float Ljava/lang/Float;
double D Double Ljava/lang/Double;
void V Void Ljava/lang/Void;
Object L+类名(使用’/‘作为分隔符)+;如: Ljava/lang/Object;Lorg/objectweb/asm/MethodVisitor; / /
String Ljava/lang/String; / /
————– 数组写法: ——– —————-
X的N维数组 N个[+X的类型描述符 / /
int[] [I(大写的i) / /
byte[][] [[B / /
String[] [Ljava/lang/String; / /
Object[][] [[Ljava/lang/Object; / /

方法描述符

源文件中的方法声明 方法描述符 说明
void m(int i, float f) (IF)V 接收一个int和float型参数且无返回值
int m(Object o) (Ljava/lang/Object;)I 接收Object型参数返回int
int[] m(int i, String s) (ILjava/lang/String;)[I 接受int和String返回一个int[]
Object m(int[] i) ([I)Ljava/lang/Object; 接受一个int[]返回Object

参考文章

https://zhuanlan.zhihu.com/p/407839659

https://zhuanlan.zhihu.com/p/425355837

https://mp.weixin.qq.com/s/CXsbzt4IWyDaV006JdIYsQ

https://www.52pojie.cn/thread-1322512-1-1.html





# Android逆向  

tocToc: