今天要分析的是某航空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_"
开头的函数就是静态注册,反之则为动态注册。
示例如图:
那如何判断 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