今天要分析的是某航空app,版本号是8.19.0
,分析的样本在文章底部会提供,这次我们要借用unidbg
来辅助进行算法还原。
有关unidbg的介绍笔者就不做过多的描述,大家可自行百度查询。
该样本的so比较简单,但重点是记录分析的思路和过程。这里万分感谢鹏佬的指点。
老规矩,上来先抓个包。
1.抓包
经分析该app没有壳,里面有一个hnairSign
参数加密,hnairSign 就是本次样本研究的重点,我们不在意如何拿到数据,而是研究如何构造加密的。
2.jadx静态分析
把样本app 拖到jadx
里面,直接搜索关键词 "hnairSign"
,可以看到只有一条搜索结果,跟进去看看。
然后来到了这个,可以看到i6.b("hnairSign", signRequest);
,其中hnairSign 是该String signRequest = signRequest(aVar);
方法返回来的。
紧接着跟进 signRequest(aVar)
函数看一看,来到这里
可以看到 signRequest 函数的入参是v.a aVar
,返回值是String
,并且是由该(String) i.p(HNASignature.getHNASignature(headersForSign, queryForSign, requestBodyForSign, str, a9), new String[]{">>"}).get(0);
返回来的。
在这里我们看到了HNASignature.getHNASignature()
,根据函数命名规范,大胆的猜测下,这里就是最核心的加密代码。
getHNASignature()函数的入参是headersForSign, queryForSign, requestBodyForSign, str, a9
几个连续的String,具体是什么,目前不知道,不过先放置在这里。
继续跟进去,来到这里。
package com.rytong.hnair;
public class HNASignature {
public static native String getHNASignature(String str, String str2, String str3, String str4, String str5);
}
关键字native
,看来本次样本不是在java层那么简单了,本着学习的态度,进入so层看看代码是如何写的。
通常情况下,会有如下的代码:
System.loadLibrary("xxxxx")
会告诉我们,样本加密在那个so文件,但是这里并没有。
不过我们也可以用yang神的frida脚本 hook_RegisterNatives
( https://github.com/lasting-yang/frida_hook_libart) 来打印注册的native函数和so文件。
那它的脚本到底是如何做到的尼,点进去源码看看。
java层的静态分析到此就差不多结束了,接着要用frida动态分析下。
3. frida动态调试
打开frida服务,运行命令:
frida -U com.rytong.hnair -l hair_hook.js
hair_hook代码:
Java.perform(function () {
var HNASignature = Java.use("com.rytong.hnair.HNASignature");
HNASignature.getHNASignature.implementation = function (str1, str2, str3, str4, str5) {
console.log("---------------------------");
console.log('getHNASignature is called');
console.log("str1:" + str1);
console.log("str2:" + str2);
console.log("str3:" + str3);
console.log("str4:" + str4);
console.log("str5:" + str5);
var ret = this.getHNASignature(str, str2, str3, str4, str5);
console.log('getHNASignature加密值:' + ret);
return ret;
};
})
frida hook结果:
getHNASignature is called
str1:{}
str2:{}
str3:{"akey":"184C5F04D8BE43DCBD2EE3ABC928F616","aname":"com.rytong.hnair","atarget":"standard","aver":"8.19.0","did":"9908e1587f76bbd6","dname":"OnePlus_ONEPLUS A5010","gtcid":"0629cf96a1a94543d3b1db7625b97e44","mchannel":"official","schannel":"AD","slang":"zh-CN","sname":"OnePlus\/OnePlus5T\/OnePlus5T:8.1.0\/OPM1.171019.011\/1812111113:user\/release-keys","stime":"1671674896594","sver":"8.1.0","system":"AD","szone":"+0800","abuild":"63403","hver":"8.19.0.30995.c079e3182.standard","cms":[{"name":"cdnConfig"}],"h5Version":"8.19.0.30995.c079e3182.standard"}
str4:21047C596EAD45209346AE29F0350491
str5:F6B15ABD66F91951036C955CB25B069F
getHNASignature加密值:F3DCBDD8A2350604A7487FF97E3A709B331F3E51>>63403184C5F04D8BE43DCBD2EE3ABC928F616com.rytong.hnairstandard8.19.09908e1587f76bbd6OnePlus_ONEPLUS A50100629cf96a1a94543d3b1db7625b97e448.19.0.30995.c079e3182.standard8.19.0.30995.c079e3182.standardofficialADzh-CNOnePlus/OnePlus5T/OnePlus5T:8.1.0/OPM1.171019.011/1812111113:user/release-keys16716748965948.1.0AD+0800>>F6B15ABD66F91951036C955CB25B069F
连续hook多次发现,参数 str1 和 str2 固定为{}
,参数str3是一个json字符串
,参数str4为固定值21047C596EAD45209346AE29F0350491
,参数str5为固定值F6B15ABD66F91951036C955CB25B069F
,可以把它暂且理解为盐值吧!
4.so层分析
把样本app后缀修改成zip结尾,然后解压缩,在lib下的armeabi-v7a
32位目录下找到加密样本libsignature.so
。
把该so样本直接拖到ida里,全程选择默认,一路点击ok。
然后又是熟悉的页面,熟悉的晦涩难懂。。。硬着头皮上。。。。。
进入到这里,我们首先要确定两件事。
第一加密的native函数到底是静态注册
还是动态注册
的。
第二样本so用的是Thumb
指令 还是Arm
指令。
那么有问题的小明同学就想问一句,你说的到底是个啥,能简单的介绍下吗?
一般在Exports导出表能搜索到以"java_"
开头的函数就是静态注册,反之则为动态注册。
静态注册的so函数是这样命名的:
Java_包名_类名_函数名
动态注册通常你会看到JNI_OnLoad
。
示例如图:
那如何判断 Thumb 和 Arm 指令集尼?
ida里依次找到Options ----> General
然后 Number of opcode bytes(non-graph) 设置为4 ,点击ok.
然后在IDA View
中查看opcode 的长度, 如果出现 2 个字节和 4 个字节
的, 说明为 thumb 指令集。
如果都是 4
个字节的, 说明是 arm 指令集。
在 Thumb 指令集下, inline hook 的需要进行偏移地址 +1
操作;
示例如图:
继续之前的,刚才我们找到了"java_"
开头的函数 Java_com_rytong_hnair_HNASignature_getHNASignature
,点进去。
使用空格键
可以实现 文字图 和 竖形流程图 的切换(ps:这里可能表述不准确)。
然后再按 Fn5
健 就能看到 加密的 c/c++代码了。
这里就能看到加密的c函数HNASignature(&v19, &v25, &v24, &v23, &v22, &v21)
,
几个入参应该分别对应着java层的入参(形参)。
public static native String getHNASignature(String str, String str2, String str3, String str4, String str5);
}
同时我们在导出函数窗口还看到了CHMAC_SHA1::HMAC_SHA1()
根据经验,大胆的猜测下这里用的是hmac_sha1
算法。
好了,so层的分析到一段落。
5.unidbg分析
unidbg 最重要的就是补环境
环境搭建 可参考文章:https://blog.csdn.net/qq_41179280/article/details/121771586
第一步,先搭建基本框架
package com.rytong.hnair;
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.*;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class HNASignatureTest extends AbstractJni {
private final AndroidEmulator emulator;
private final Module module;
private final VM vm;
public HNASignatureTest() {
// 创建模拟器实例,进程名依照实际进程名填写,要模拟32位或者64位,在这里区分
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.rytong.hnair").build();
// 模拟器的内存操作接口
final Memory memory = emulator.getMemory();
// 设置系统类库解析,有19和23 两个版本选择
memory.setLibraryResolver(new AndroidResolver(23));
// 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/hair/hair_8.19.0.apk"));
// 设置是否打印Jni调用细节
vm.setVerbose(false);
// 加载目标SO
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/hair/armeabi_v7a/libsignature.so"), false);
//获取SO模块的句柄
module = dm.getModule();
System.out.println("baseAddr:"+ module.base);
// 设置JNI
vm.setJni(this);
// 调用JNI OnLoad
dm.callJNI_OnLoad(emulator);
}
初始化的大致逻辑就是先创建个模拟器对象emulator,然后去内存申请接口,再传入apk和so文件,最后再执行JNI OnLoad。
unidbg 是模拟执行so 的,因为是模拟而并不是真正的,所以有很多东西没有实现,所以会报各种各种的错误。
接下来,我们逐行的详细解释下,代码中的注释已经很清楚。
new AndroidResolver()
指定系统类库解析,那为什么只有2个版本可供选择尼?
点进源码,我们发现unidbg作者只实现了 19和23
两个版本。
emulator.createDalvikVM()
是创建Android虚拟机,为什么要传入apk文件尼?
点进源码看看
可以看到这里 Apk接口会自动获取版本号,版本名称,ManifestXml,签名,包名
等(建议写上,会帮我们做部分签名校验的工作)
vm.setJni(this); 是设置jni,那什么是jni
?
带着疑问,百度下?
详细可参考文章:https://blog.csdn.net/yaojingqingcheng/article/details/123497697
简单的说就是jni 是java世界和c/c++世界沟通的桥梁,java可以通过jni来调用c/c++封装好的函数,反之也可以,其中第⼀个参数JNIEnv *env,这个参数就是java环境。
执行so文件以后,紧接着执行JNI_OnLoad
,那JNI_OnLoad是个啥子尼?
JNI_OnLoad()
Java调用System.loadLibrary()加载一个库的时候,会首先在库中搜索JNI_OnLoad()函数,如果该函数存在,则执行它;
JNI_OnLoad()的作用主要有几点:
- 告诉JVM,这个库需要要求使用的JNI版本是什么
- 执行初始化操作
- 将JavaVM参数保存为全局对象,方便以后在任何地方获取JNIEnv对象
如果一个库不存在JNI_OnLoad()函数,那么JVM默认会使用最老版本的JNI,即1.1。
JNI_OnLoad方法在每一个库中只能存在一个。
也就是说我们加载了libsignature.so
库以后,就应该需要执行下JNI_OnLoad
,当然如果库没有JNI_OnLoad,那自然不用执行,但是不管有没有,执行就是了,反正不会错。
运行一下:
然后补call方法
public void callSign(String encryptData){
List<Object> list = new ArrayList<>(7);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0,一般用不到。
String str1 = "{}";
list.add(vm.addLocalObject(new StringObject(vm,str1)));
String str2 = "{}";
list.add(vm.addLocalObject(new StringObject(vm,str2)));
list.add(vm.addLocalObject(new StringObject(vm,encryptData)));
String str4 = "21047C596EAD45209346AE29F0350491";
list.add(vm.addLocalObject(new StringObject(vm,str4)));
String str5 = "F6B15ABD66F91951036C955CB25B069F";
list.add(vm.addLocalObject(new StringObject(vm,str5)));
Number number = module.callFunction(emulator,0xA49C+1, list.toArray());
DvmObject result = vm.getObject(number.intValue());
String value = (String) result.getValue();
System.out.println("result ->" + value);
System.out.println("result ------>" + value.split(">>")[0]);
}
需要注意几个点:
- 第一个参数是JNIEnv,
- 第二个参数,实例方法是
jobject
,静态方法是jclazz
,直接填0或者为空,这样做有小风险,一般用不到。 - 传入Native的JAVA参数,除了八个基本类型外(byte、char、short、int、long、float、double、boolean),都必须
vm.addLocalObjec
t添加到局部引用中去。
后面几个参数就是前面我们frida hook出 必要的传参。
运行一下:
对比下,发现unidbg跑出来的结果和抓包拿到的一样。
全部代码如下:
package com.rytong.hnair;
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.*;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class HNASignatureTest extends AbstractJni {
private final AndroidEmulator emulator;
private final Module module;
private final VM vm;
//
public HNASignatureTest() {
// 创建模拟器实例,进程名依照实际进程名填写,要模拟32位或者64位,在这里区分
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.rytong.hnair").build();
// 模拟器的内存操作接口
final Memory memory = emulator.getMemory();
// 设置系统类库解析,有19和23 两个版本选择
memory.setLibraryResolver(new AndroidResolver(23));
// 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/hair/hair_8.19.0.apk"));
// 设置是否打印Jni调用细节
vm.setVerbose(false);
// 加载目标SO
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/hair/armeabi_v7a/libsignature.so"), false);
//获取SO模块的句柄
module = dm.getModule();
System.out.println("baseAddr:"+ module.base);
// 设置JNI
vm.setJni(this);
// 调用JNI OnLoad
dm.callJNI_OnLoad(emulator);
}
public void callSign(String encryptData){
List<Object> list = new ArrayList<>(7);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0,一般用不到。
String str1 = "{}";
list.add(vm.addLocalObject(new StringObject(vm,str1)));
String str2 = "{}";
list.add(vm.addLocalObject(new StringObject(vm,str2)));
list.add(vm.addLocalObject(new StringObject(vm,encryptData)));
String str4 = "21047C596EAD45209346AE29F0350491";
list.add(vm.addLocalObject(new StringObject(vm,str4)));
String str5 = "F6B15ABD66F91951036C955CB25B069F";
list.add(vm.addLocalObject(new StringObject(vm,str5)));
Number number = module.callFunction(emulator,0xA49C+1, list.toArray());
DvmObject result = vm.getObject(number.intValue());
String value = (String) result.getValue();
System.out.println("result ->" + value);
System.out.println("result ------>" + value.split(">>")[0]);
}
public static void main(String[] args) throws IOException {
HNASignatureTest hnsign = new HNASignatureTest();
String encryptData1 = "{\"akey\":\"184C5F04D8BE43DCBD2EE3ABC928F616\",\"aname\":\"com.rytong.hnair\",\"atarget\":\"standard\",\"aver\":\"8.19.0\",\"did\":\"9908e1587f76bbd6\",\"dname\":\"OnePlus_ONEPLUS A5010\",\"gtcid\":\"0629cf96a1a94543d3b1db7625b97e44\",\"mchannel\":\"official\",\"schannel\":\"AD\",\"slang\":\"zh-CN\",\"sname\":\"OnePlus\\/OnePlus5T\\/OnePlus5T:8.1.0\\/OPM1.171019.011\\/1812111113:user\\/release-keys\",\"stime\":\"1671674896594\",\"sver\":\"8.1.0\",\"system\":\"AD\",\"szone\":\"+0800\",\"abuild\":\"63403\",\"hver\":\"8.19.0.30995.c079e3182.standard\",\"cms\":[{\"name\":\"cdnConfig\"}],\"h5Version\":\"8.19.0.30995.c079e3182.standard\"}";
System.out.println(encryptData1);
hnsign.callSign(encryptData1);
}
}
6.算法还原
经过对比发现,参数str3其实就是把请求的body里所有的key/values取出来
再看下frida返回值可以发现,其实就是把字典排个序,然后把value拼接到一起,尾部追加个固定盐值F6B15ABD66F91951036C955CB25B069F
,用的是hmac_sha1标准算法。
python代码就不贴了。
可以看到,unidbg,frida和抓包三者拿到的值都是一样的。
参考文章:
https://blog.csdn.net/qq_38851536/article/details/117418582
https://blog.csdn.net/qq_38851536/article/details/118115569
https://www.cnblogs.com/zhujiabin/p/10605745.html
https://blog.csdn.net/Qiled/article/details/124348705