一.前言
今天开始讲app逆向最后一个也是最重要的unidbg,这已经是从初级进阶到中级的了,我会讲unidbg,讲三节课,分为上中下来和大家讲(由简单到难逐步),这节课主要是和大家讲unidbg的介绍并且会结合之前讲的简单案例来让大家理解,如果过程中不太记得之前的位置定位,可以去看之前的课程,在学习工程中遗忘是难免的,这也是我做教学的初心,希望大家点赞关注加收藏。
二.unidbg的介绍和使用
2.1 unidbg 是什么
unidbg是一个Java开源项目,可以帮助我们去模拟一个安卓或IOS设备,用于去执行so文件中的算法,从而不需要再去逆向他内部的算法
它是一个基于 unicorn 的逆向工具,可以直接调用Android和iOS中的 so 文件Allows you to emulate an Android native library, and an experimental iOS emulation
允许您模拟 Android native library 和 实验性的 iOS 模拟
而在什么时候使用unidbg呢?
当加密位置是在so文件的时候,而so文件破解难度大,我们就会使用unidbg,我们之前也教过大家其他两种硬核破解so文件的,分别是frida-rpc和自己编写app来调用,但是问题就是我们不方便交付,特别的麻烦,所以我们可以使用unidbg,再把unidbg的java代码编写成jar包,通过python来调用jar包就好(当然今天我们讲不到这里),后面会讲的,这就有点类似于js逆向时python调用js文件一样!
2.2 下载和使用
这里给出下载地址
Releases · zhkl0228/unidbg · GitHubhttps://github.com/zhkl0228/unidbg/releases打开这个网址选择最新版下载就好,下载好解压完,使用idea打开
注意打开时解压后里面的这一层目录
然后我们运行一下
正常出值了就说明没问题,比如我最开始运行就发现报错,排除半天才发现jdk版本错误,当时用的版本是jdk1.7,后来换成1.8就ok了,这个代码是安居客sign的测试代码,运行成功就说明环境没问题了
2.3 关于unidbg的补环境
1 unidbg 模拟了手机设备--》so文件进行加密---》so文件加密时,有两种方式
- 加密算法,都在so层,全是用c实现的 大姨妈--》不需要补环境
- 加密算法,在so层--》so又调用了java层--》返回so层继续逻辑--》唯品会--》需要补环境
2 所谓的补环境--》就是补c层调用java时候的一些类
unidbg使用java写的---》c调用java的时候---》unidbg是没有对应app的java的代码---》c中调用java时,缺java的这些类---》我们需要把缺的java类补上
所以说,unidbg补环境只在so层调用java的时候才需要补上,也就是补上那些java中的类
三.车智赢案例
刚才我们介绍了一下unidbg,现在我就带大家讲一下之前的unidbg,忘了的话可以去看一下之前的案例
3.1 回顾当时so加密
APP逆向 day13 逆向某智赢登录-CSDN博客文章浏览阅读1k次,点赞23次,收藏17次。pwd 就是对密码进行md5加密udid就是将几个值拼接起来进行des加密,最后再第61个位置加上空格(可忽略)_sign 对载荷进行拼接后 前面加上一串字符,后面加上一串字符进行md5。https://blog.csdn.net/weixin_74178589/article/details/140250795?spm=1001.2014.3001.5501当时我们逆向udid的时候发现执行力des加密,而我们当时get3deskey是直接hook得来的
我们今天用unidbg带大家跑出来这个key是什么
3.2 unidbg的使用步骤
(1)在java/com下创建一个包和类,名字随便取
(2)设置初始化
这里先给大家截图,讲完思路之后会给大家代码的,这些基本都是固定的
可以发现我们还要单独创建一个目录来导入apk和so文件
这样就创建好了
这些初始化写好之后就运行一下,代码基本都是固定的,后续只要自己修改包名那些就好了
运行不报错了
package com.che_des3key;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import java.io.File;
//必须继承AbstractJni
public class CheZhiYing extends AbstractJni {
public static AndroidEmulator emulator; // 静态属性,以后对象和类都可以直接使用
public static Memory memory;
public static VM vm;
public static Module module;
// 1 构造方法--》用来初始化
public CheZhiYing(){
// 1.创建设备(32位或64位模拟器), 具体看so文件在哪个目录。 在armeabi-v7a就选择32位
// 传进设备时,如果是32位,后面so文件就要用32位,同理需要用64位的
// 这个名字可以随便写,一般写成app的包名 以后可能会动
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.che168.autotradercloud").build();
// 2.获取内存对象(可以操作内存)
memory = emulator.getMemory();
//3.设置安卓sdk版本(只支持19、23)
memory.setLibraryResolver(new AndroidResolver(23));
// 4.创建虚拟机(运行安卓代码需要虚拟机,就想运行py代码需要python解释器一样) 以后会动
vm = emulator.createDalvikVM(new File("apks/che/che3.32.1.apk"));
vm.setJni(this); // 后期补环境会用,把要补的环境,写在当前这个类中,执行这个代码即可,但是必须继承AbstractJni
//vm.setVerbose(true); //是否展示调用过程的细节
// 5.加载so文件
DalvikModule dm = vm.loadLibrary(new File("apks/che/libnative-lib.so"), false); // 以后会动
dm.callJNI_OnLoad(emulator); // jni开发动态注册,会执行JNI_OnLoad,如果是动态注册,需要执行一下这个,如果静态注册,这个不需要执行,车智赢案例是静态注册
// 6.dm代表so文件,dm.getModule()得到module对象,基于module对象可以访问so中的成员。
module = dm.getModule(); // 把so文件加载到内存后,后期可以获取基地址,偏移量等,该变量代指so文件
}
//2 sign 成员方法--》主要用来解密
public void sign(){
}
// 3 main方法---》右键直接运行
public static void main(String[] args) {
CheZhiYing che=new CheZhiYing();
che.sign();
}
}
(3)执行签名
jni签名
因为content是android中的方法,所以这个比较特殊,所以写成了android/content/Context,正常都是根据这个表对应来写就好
代码给出如下
public void sign(){
// 1 找到java类中jni的类 native方法,找的时候是固定写法
// com.autohome.ahkit.jni.CheckSignUtil-->.写成 / 形式
DvmClass CheckSignUtil = vm.resolveClass("com/autohome/ahkit/jni/CheckSignUtil");
// 2 找到类中的方法--》使用签名的方式找【参数签名和返回值签名】--》固定写法
// Landroid/content/Context; 就是参数,这个只有一个content类型 要用jni中对应的类型来写
// Ljava/lang/String 就是返回值,也要用jni中对应的类型来写
// 之前在jni开发中给过一张对应图了
String method = "get3desKey(Landroid/content/Context;)Ljava/lang/String;";
// 3 执行方法,传入参数
// 第一个参数是:设备对象
// 第二个参数是:方法
// 再往后的参数,就是这个方法的参数
StringObject obj = CheckSignUtil.callStaticJniMethodObject(
emulator,
method,
// 相当于 new 了个contex传入了,但是这个context是空的
// 具体类型一会讲
vm.resolveClass("android/content/Context").newObject(null)
);
// 4 打印结果
String result=obj.getValue();
System.out.println(result);
}
现在再右键
可以发现出值了,而这个值就是key,和我们之前的一结果一致
3.3 调用方法--传参和返回值
相信大家在刚才的传参和返回值那里应该都挺蒙蔽的,那我们现在就来具体讲一下,讲完这个就接着和大家讲案例让大家深入理解
使用unidbg调用 jni 中Navite方法时,传入的参数和返回值,不是java的类型,需要是unidgb提供的类型,我们反编译回来的代码是这样:
public static native String get3desKey(Context context);
而我们写unidbg的代码是这样:
StringObject obj = CheckSignUtil.callStaticJniMethodObject(
emulator,
method,
vm.resolveClass("android/content/Context").newObject(null)
);
因为在使用unidbg时要有包裹概念,要把参数进行包裹再传给unidbg使用,这里是传参包裹的规则
java类型 包裹
字符串:'justin' StringObject("justin")
字节数组:{11,12} ByteArray({11,22})
----------------------------布尔-数字-空-----------------------
布尔:True/False True/False
数字:19 19
空:null null
----------------------------自定义类型----------------------------------------
自定义类型:Info cls = vm.resolveClass("com/nb/utils/Info");
cls.newObject(对象)可以直接写成vm.resolveClass("com/nb/utils/Info").newObject(new出来的对象)
而我们刚才这么写:
vm.resolveClass("android/content/Context").newObject(null)
是因为content是安卓中的方法,java中没有,而我们这个方法恰巧没有使用content里面的值,如果有的案例需要这个值,那我们就要补环境了
返回值就比较简单
返回值--》被unidbg包裹的对象--》通过getValue得到真正的字符串对象
如果返回字符串 需要用 StringObject 类型接受---》拿到真正的字符串 需要 obj.getValue()
四.大姨妈案例
这里给出当时案例的地址,不记得的话可以去看看(前几天的案例要是看过了但是忘了就该打!)
当时我们传入三个参数,第一个参数是0,第二个参数是sb,第三个参数是sb的长度
这里我们随便拿之前hook到的模拟一下
j2=0
str=64e6176e45397c5989504e76f98ecf2e63b2679euser/login18953675221WA89qByLlDeaGjmVNzXm/w==
j3=85
那我们就来c一下代码嘛,改个包名和文件名就好了
c好的代码修改了如上面,我们运行一下
发现运行报错
还记得这里嘛,说明我们c的是64位的so文件,那我们改一下就好了
改好之后运行不报错了,我这里真的是想办法帮你们把所有坑都排掉,别太感动!!点赞关注加收藏就好
我们现在来编写sign中的内容,sign中的内容也可以复制然后修改就好,最后我会给出代码的
这个就是之前我们写的类似,注意的是包名中间的.都换成/,参数之间不用分割,中间那个;是因为jni中string对应的后面有分号
public void sign(){
// 1 找到java类中jni的类 native方法,找的时候是固定写法
// 找到包,找到类
DvmClass CheckSignUtil = vm.resolveClass("com/yoloho/libcore/util/Crypt");
// 2 找到类中的方法--》使用签名的方式找【参数签名和返回值签名】--》固定写法
// 第一个参数: J long类型对应
// 第二个参数: Ljava/lang/String; string类型对应
// 第三个参数: J long类型对应
// 返回值: Ljava/lang/String;
// 注意:参数和参数之间不用; 或者,分割 直接写就行了
String method = "encrypt_data(JLjava/lang/String;J)Ljava/lang/String;";
// 3 执行方法,传入参数
// 第一个参数是:设备对象
// 第二个参数是:方法
// 再往后的参数,就是这个方法的参数
StringObject obj = CheckSignUtil.callStaticJniMethodObject(
emulator,
method,
0,
new StringObject(vm,"64e6176e45397c5989504e76f98ecf2e63b2679euser/login18953675221WA89qByLlDeaGjmVNzXm/w=="),
85
);
// 4 打印结果
String result=obj.getValue();
System.out.println(result);
}
运行结果
五.得物案例
这里给出之前案例的地址
我们这里大致回忆一下,当时我们,当时他执行了getByteValues,然后再进行下面的操作,最后把生成的字符串传入 再执行encodeByte,这两个都是jni方法,当时我们只看了encodeByte的参数和返回值,并没有怎么读上面的逻辑,因为读so太麻烦了,而这次我们用jni完整的把这两个方法都搞出来,我们还是先c一下并修改初始化的代码
我们运行一下
这个不是报错,只是内部打印的日志
这个就比前两个要复杂,他要调用两次jni方法,但是殊途同归,可以发现,这样子的好处就是,遇到java中的逻辑代码,我们能够直接复制,稍微修改一点点了,记得当时我们硬核破解so的时候,就在想这个取反之后的字符串为什么没有用到,为啥会没有用,还会在想会不会用错了,但是当我们使用jni的时候完全不会觉得用错了,能够十分自信,而且更加的方便马,这就是用unidbg的好处,简单好破解,这里给出sign中的代码
public void sign(){
// 内部调用了两次jni方法
// 1 找到类
DvmClass AESEncrypt = vm.resolveClass("com/duapp/aesjni/AESEncrypt");
//2 找到方法
// 没有参数
String method = "getByteValues()Ljava/lang/String;";
// 3 调用
StringObject byteValues = AESEncrypt.callStaticJniMethodObject(
emulator,
method
);
// 4 拿到真正的字符串
String byteValuesString = byteValues.getValue();
// 5 按照反编译的逻辑执行java代码
// 直接复制就行了
StringBuilder sb = new StringBuilder();
for (int i2 = 0; i2 < byteValuesString.length(); i2++) {
if (byteValuesString.charAt(i2) == '0') {
sb.append('1');
} else {
sb.append('0');
}
}
String sbString = sb.toString();
System.out.println(sbString);
// 6 找到第二个方法
// 第一个参数: [B 对应字节数组
// 后面我就不多说了
String methodEncodeBytes = "encodeByte([BLjava/lang/String;)Ljava/lang/String;";
// 7 执行方法
String body="abRecReason0abRectagFengge0abTypesocial_brand_strategy_v454abValue1abVideoCover2deliveryProjectId0lastIdlimit20loginTokenplatformandroidtimestamp1715346585009uuidee13885e68d76ed4v4.74.5";
StringObject data = AESEncrypt.callStaticJniMethodObject(
emulator,
methodEncodeBytes,
// 按照前面说的包裹好
new ByteArray(vm, body.getBytes()),
new StringObject(vm, sbString)
);
//8 打印结果
System.out.println(data.getValue());
}
运行结果
六.海南航空案例
讲了几个之前的案例,我们这就来讲一个新的案例,担心大家记不清之前的案例了还不愿意回去看,这就完整的和大家讲一个
版本选择 v9.0.0
6.1 抓包分析
我们今天就不完全给他全部破出来,而是只用unidbg破解这一个参数:hnairSign
6.2 找到加密位置
我们搜索hnairSign
找到一个位置,我们点进去
发现在这里,我们再点进去
i.p().get(0)-->i.p执行结果是列表ArrayList,取了列表的第0个位置
HNASignature.getHNASignature()--》执行结果是字符串
然后对其进行分割取第0个位置,我们点进去
6.3 hook得到加密参数
发现在这里是个jni方法,那我们接下来就是hook参数并且找到其对应的so文件
这里给出hook代码
Java.perform(function () {
var HNASignature = Java.use("com.rytong.hnair.HNASignature");
HNASignature.getHNASignature.implementation = function (str,str2,str3,str4,str5) {
console.log("---------------------")
console.log("参数1",str);
console.log("参数2",str2);
console.log("参数3",str3);
console.log("参数4",str4);
console.log("参数5",str5);
var res = this.getHNASignature(str,str2,str3,str4,str5);
console.log("返回值=",res);
return res;
}
});
//frida -U -f com.rytong.hnair -l hook.js
我们清除数据执行一下,通过抓包的数据搜索
发现参数和返回值
参数1 {}
参数2 {}
参数3 {"abuild":"64249","akey":"184C5F04D8BE43DCBD2EE3ABC928F616","aname":"com.rytong.hnair","atarget":"standard","aver":"9.0.0","caller":"AD_H5","did":"d3e4ffe477e86d99","dname":"Google_Pixel 3","gtcid":"","hver":"9.0.0.35417.7ac
793f2e.standard","mchannel":"huawei","schannel":"AD","slang":"zh-CN","sname":"google\/blueline\/blueline:11\/RQ3A.211001.001\/7641976:user\/release-keys","stime":"1722284925093","sver":"11","system":"AD","szone":"+0800","riskToken":"66a7fb7adXzZGwi8Bf8b3o9WpWYvTtyQMM7ewnx3"}
参数4 21047C596EAD45209346AE29F0350491
参数5 F6B15ABD66F91951036C955CB25B069F
返回值= E3B7A590F71E7C8292165B26BE54814C7F56653E>>64249184C5F04D8BE43DCBD2EE3ABC928F616com.rytong.hnairstandard9.0.0AD_H5d3e4ffe477e86d99Google_Pixel 39.0.0.35417.7ac793f2e.standardhuawei66a7fb7adXzZGwi8Bf8b3o9WpWYvTtyQMM7ewnx3ADzh-CNgoogle/blueline/blueline:11/RQ3A.211001.001/7641976:user/release-keys172228492509311AD+0800>>F6B15ABD66F91951036C955CB25B069F
6.4 找到加密位置
这个发现找不到我们想要的so文件的位置,之前我们在b站案例中给我一个找到动态注册的so文件以及偏移量直接找到加密方法,这里再次给出
function hook_RegisterNatives() {
//1 加载安卓手机底层包,系统自带的库,我们hook的RegisterNatives在这个包中
var symbols = Module.enumerateSymbolsSync("libart.so");
//2 定义一个变量,用来接收一会找到的addrRegisterNatives的地址
var addrRegisterNatives = null;
// 3 循环找到RegisterNatives的地址,赋值给变量
//注意:此处可能找出多个RegisterNatives的地址,由于咱们是for循环,会把之前的覆盖掉,所有如果hook没反应,尝试加break,使用第一个找到的
for (var i = 0; i < symbols.length; i++) {
var symbol = symbols[i];
if (symbol.name.indexOf("art") >= 0 &&
symbol.name.indexOf("JNI") >= 0 &&
symbol.name.indexOf("RegisterNatives") >= 0 &&
symbol.name.indexOf("CheckJNI") < 0) {
addrRegisterNatives = symbol.address;
console.log("RegisterNatives is at ", symbol.address, symbol.name);
break
}
}
// 4 找到后开始hook
if (addrRegisterNatives != null) {
Interceptor.attach(addrRegisterNatives, {
// 4.1 当进入RegisterNatives时执行
// RegisterNatives(env, 类型, Java和C的对应关系,个数)
onEnter: function (args) {
// 4.2 第0个参数是env
var env = args[0];
// 4.3 第1个参数是类型
var java_class = args[1];
// 4.4 通过类型得到具体的类名
var class_name = Java.vm.tryGetEnv().getClassName(java_class);
//console.log(class_name);
// 只有类名为com.bilibili.nativelibrary.LibBili,才打印输出
var taget_class = "com.rytong.hnair.HNASignature";
if (class_name === taget_class) {
//4.5 只有类名为com.bilibili.nativelibrary.LibBili,再取出第四个参数
console.log("\n[RegisterNatives] method_count:", args[3]);
// 4.6 第2个参数是:Java和C的对应关系,我们转成指针
/*
static JNINativeMethod gMethods[] = {
{"add", "(III)I", (void *) plus},
{"add", "(II)I", (void *) plus},
{"add", "(II)I", (void *) plus},
};
*/
var methods_ptr = ptr(args[2]);
// 4.7 java和c函数对应关系的个数
var method_count = parseInt(args[3]);
// 4.8 我们循环这个个数,依次移动指针methods_ptr,通过readPointer,往后读取 {"add", "(III)I", (void *) plus},依次读出Java中函数名字,签名和C中的函数指针
for (var i = 0; i < method_count; i++) {
// 4.8.1 读取Java中函数名字的
var name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3));
// 4.8.2 读取签名, 参数和返回值类型
var sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize));
// 4.8.3 读取 C中的函数指针
var fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));
// 4.8.4 读取java中函数名 字符串名
var name = Memory.readCString(name_ptr);
// 4.8.5 参数和返回值类型 字符串名
var sig = Memory.readCString(sig_ptr);
// 4.5.6 根据C中函数指针获取模块
var find_module = Process.findModuleByAddress(fnPtr_ptr); // 根据C中函数指针获取模块
// 4.8.7 得到该函数的偏移量:ptr(fnPtr_ptr)函数在内存中的地址 减去 该so文件的基地址(find_module.base)====得到偏移量
// 地址:函数在内存中的地址
// 偏移量:后期单独打开so文件后,可以根据偏移量 定位到函数位置
// 基地址:当前so文件从那个位置开始算地址
var offset = ptr(fnPtr_ptr).sub(find_module.base)
// console.log("[RegisterNatives] java_class:", class_name);
// 4.8.8 输出 函数名 参数和返回值类型 模块 偏移量
console.log("name:", name, "sig:", sig, "module_name:", find_module.name, "offset:", offset);
}
}
}
});
}
}
setImmediate(hook_RegisterNatives);
// frida -U -f 包名 -l 16.通用脚本_获取动态注册对应关系.js
发现运行啥也没有,说明是静态注册,这里给出找到静态注册so文件的代码
Java.perform(function () {
var dlsymadd = Module.findExportByName("libdl.so", 'dlsym');
Interceptor.attach(dlsymadd, {
onEnter: function (args) {
this.info = args[1];
}, onLeave: function (retval) {
//那个so文件 module.name
var module = Process.findModuleByAddress(retval);
if (module == null) {
return retval;
}
// native方法
var funcName = this.info.readCString();
// 后期只需要改这里,对应的java方法名
if (funcName.indexOf("getHNASignature") !== -1) {
console.log(module.name);
console.log('\t', funcName);
}
return retval;
}
})
});
// frida -U -f 包名 -l 24.通用脚本_获取静态注册的so文件.js
// frida -U -f com.rytong.hnair -l 24.通用脚本_获取静态注册的so文件.js
发现是libsignature.so,那我们现在就不用去so中读逻辑了,直接把apk改成zip,解压后把这个so文件和apk一起放入unidbg中就好
6.5 unidbg跑
然后就是c一下那段代码了
我们把该c好的c好,运行发现不报错,现在就开始写sign
直接写入参数就好了,这里给出代码
public void sign(){
// 1 找到类
DvmClass HNASignature = vm.resolveClass("com/rytong/hnair/HNASignature");
// 2 找到方法
// 这里五个参数都是字符串,看起来有带你奇怪
String method = "getHNASignature(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;";
//3 执行方法
StringObject obj = HNASignature.callStaticJniMethodObject(
emulator,
method,
new StringObject(vm,"{}"),
new StringObject(vm,"{}"),
new StringObject(vm,"{\"abuild\":\"64249\",\"akey\":\"184C5F04D8BE43DCBD2EE3ABC928F616\",\"aname\":\"com.rytong.hnair\",\"atarget\":\"standard\",\"aver\":\"9.0.0\",\"caller\":\"AD_H5\",\"did\":\"d3e4ffe477e86d99\",\"dname\":\"Google_Pixel 3\",\"gtcid\":\"\",\"hver\":\"9.0.0.35417.7ac793f2e.standard\",\"mchannel\":\"huawei\",\"schannel\":\"AD\",\"slang\":\"zh-CN\",\"sname\":\"google\\/blueline\\/blueline:11\\/RQ3A.211001.001\\/7641976:user\\/release-keys\",\"stime\":\"1722284925093\",\"sver\":\"11\",\"system\":\"AD\",\"szone\":\"+0800\",\"riskToken\":\"66a7fb7adXzZGwi8Bf8b3o9WpWYvTtyQMM7ewnx3\"}"),
new StringObject(vm,"21047C596EAD45209346AE29F0350491"),
new StringObject(vm,"F6B15ABD66F91951036C955CB25B069F")
);
//4 打印结果
System.out.println(obj.getValue());
}
运行结果
和刚才抓包结果一致,至此破解成功
七.总结
今天讲的干活和内容很多,大家要仔细品味,发现unidbg也没有这么难,下一期会和大家讲中部分,稍微比这个案例难一点,但是也是之前讲过的,大家可以看看之前的案例,对比一下使用unidbg,多多体会
补充
有需要源码的看我主页签名名字私信我,有求必应
标签:逆向,day24unidbg,String,java,APP,参数,unidbg,so,com From: https://blog.csdn.net/weixin_74178589/article/details/140778060