下载unidbg
地址:https://github.com/zhkl0228/unidbg
unidbg需要在IDEA端进行调试一下,等待依赖自动安装后,运行unidbg-android/src/test/java/com/bytedance/frameworks/core/encrypt/TTEncrypt.java
文件。如果显示如下则代表运行正常。
使用一个基础的模板,后续可以根据此模板来进行修改
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);
}
其中需要注意的是:
- setProcessName为进程名,可以自己去赋值。
- setLibraryResolver有19、23两个SDK可以选,一般使用23。
- createDalvikVM的时候里面为APK的目录,可以为空。
- loadLibrary为需要加载到内存的so文件。
- resolveClass为调用了so加密函数的Java代码位置。
- 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代码所在类,但是并不需要实际添加进去。运行的结果如下所示。
APP
测试APP来源猿人学的一次活动
地址:http://download.python-spider.com/yuanrenxuem106.apk
WP:https://mp.weixin.qq.com/s/CXsbzt4IWyDaV006JdIYsQ
先找到这个包的包名com.yuanrenxue.match2022
,基本在这个目录下,这个app做了代码混淆,先不管。
根据提示找到/app2
这个接口对应的代码处。
其中sign为加密后的字符串,搜索这个字符串,在包下面找相关的字段。
只有两处相关com.yuanrenxue.match2022.fragment.challenge
和com.yuanrenxue.match2022.security
在类ChallengeTwoFragment
中可以看到明显的第二题代码,查看最后的sign的加密。加载了match02的so文件。
传入一个参数,类型为str,然后需要找个调用这个sign的地方,看上面的调用。
这里有两个需要注意,其中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();
}
}
然后再来看一下第四题,这个看完后发现基本跟第二题没太大区别。同样找到sign方法,里面有两个参数,去找这两个参数对应的值。
代码为:
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();
}
}
结果为:
备注:
这里是记录一些类型的描述符,方便后续修改查询
变量类型 | 类型描述符 | 包装类 | 包装类类型描述符(包含分号) |
---|---|---|---|
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