本文包括
一. 修改测试
[ol]
[/ol]
二、Java层静态分析
[ol]
定位启动Activity
分析onCreate函数
LauncheActivity继承自BaseActivity,BaseActivity中调用了几个函数,经分析确定了w5()为关键函数:
@Override // android.support.v7.app.AppCompatActivity
protected void onCreate(@nullable Bundle arg2) {
this.Z5();
super.onCreate(arg2);
try {
this.a = android.databinding.f.l(this.O5(), this.Q5());
this.V5();
com.tencent.mm.base.f.c().a(this);
this.W5(); // 关键函数
this.U5();
this.b6();
}
catch(Exception v2) {
v2.printStackTrace();
}
}
跟踪w5()
在前面测试过程中有一个信息是:修改了签名后的app并不会直接闪退,而是在申请完相关使用权限后才会退出,如果没有通过权限的申请,会自动正常退出,而不是闪退;在w5()中找到了相关权限申请的函数T6()
@Override // com.tencent.mm.base.BaseActivity
public void W5() {
/* other code */
int v0_1 = this.checkSha1(this) ? 1 : 2;
com.tencent.mm.network.d.h = this.D6(this) + ":" + v0_1;
org.greenrobot.eventbus.c.f().t(this);
this.I = new LaunchModel(this);
this.z6();
this.T6(); // 在T6中进行权限申请
String v0_2 = i1.k().E();
if(!TextUtils.isEmpty(v0_2)) {
com.tencent.mm.l.j.d().v(((UserInfoBean)JSON.parseObject(v0_2, UserInfoBean.class)));
}
}
在T6()中如果没有赋予应用相关权限,则会结束应用,否则进入B6()
public void T6() {
/* other code */
// if no permission, return and eixt
LaunchActivity.this.B6();
跟踪B6()后续的一系列函数调用,最终定位到一个向服务器发送请求的函数
public void q(String arg6) {
d.D1().N4(arg6);
d.D1().d4("http://xxxx/.../xxx", d.D1().x1(), new b("/api/xxx/xxx") {
}
}
开启Fiddler抓报后,发现应用自启动到闪退没有发送任何请求,d4()函数中在发送请求前进行了一系列的数据操作
public void d4(String arg2, HttpParams arg3, com.tencent.mm.network.b arg4) {
((PostRequest)((PostRequest)((PostRequest)((PostRequest)((PostRequest)OkGo.post(arg2).tag(arg4.b())).upJson(this.s2(arg3).toJSONString())).headers("token", i1.k().w())).cacheKey(this.C0(arg4.a()))).cacheMode(CacheMode.FIRST_CACHE_THEN_REQUEST)).execute(arg4);
}
upJson()参数即为上传的数据,经过了s2()的处理,跟进s2(),最终数据的加密封装在com.szcx.lib.encrypt.c.k()中进行
public String k(String arg5) throws JSONException {
String v5 = this.e(arg5);
JSONObject v2 = new JSONObject();
v2.put("timestamp", "1663503240");
v2.put("_ver", "v1");
v2.put("data", v5);
v2.put("sign", this.j(a.e("_ver=v1&data=" + v5 + "×tamp=" + "1663503240" + this.e)));
return v2.toString();
}
经过对正常app运行时的抓包比较,此处的参数与实际一致,在e()中队数据进行了加密,最终调用native函数进行加密
public String f(String arg1, String arg2) {
return EncryptUtil.encrypt(arg1, arg2);
}
public static native String encrypt(String arg0, String arg1) {
}
同时在代码中发现了多个密钥,包括但不限于,第一个base64编码的密钥在跟踪流程中传递给了native函数
[/ol]
三、Java层动态跟踪、Hook分析
[ol]
将前面重新打包签名生成的可调试的apk安装到手机上,为防止应用直接闪退,拒绝其相关权限的申请,同时在程序判断权限申请结果处下断,动态修改权限申请的结果,使后续流程继续下去
调试跟踪函数,最终定位发现程序在加载上述native加密so库时闪退
.method static constructor ()V
.registers 1
00000000 const-string v0, "sojm"
00000004 invoke-static System->loadLibrary(String)V, v0
0000000A return-void
.end method
同时在上面的跟踪过程中还可以得到程序生成的一系列请求参数,包含了大量系统、设备信息,但没有hash相关的参数
使用 frida hook encrypt函数,主动调用其多次加密相同数据,可以发现每次得到的结果都不同,应该使用了某种随机量
[/ol]
四、so层静态分析
[ol]
使用ida pro分析sojm 库,通过观察函数名可以得到其是通过静态注册的,这里的四个参数也符合常规的jni函数
// JNIEnv* env
// jclass _clazz
// jstring a3
// jstring a4
int __fastcall Java_com_qq_lib_EncryptUtil_encrypt(int a1, int a2, int a3, int a4)
{
int v8; // r4
int v10[4]; // [sp+4h] [bp-2Ch] BYREF
int v11; // [sp+14h] [bp-1Ch]
v8 = cgo_wait_runtime_init_done();
v10[3] = a4; // jstring
v10[2] = a3; // jstring
v10[1] = a2; // _clazz
v10[0] = a1; // env
v11 = 0;
crosscall2(cgoexp_17c794619cba_Java_com_qq_lib_EncryptUtil_encrypt, v10, 20, v8);
cgo_release_context(v8);
return v11; // jstring
}
但是后续的操作就不太常规了,可以看出它把参数依次赋给了一个数组;同时调用了crosscall2,其参数为:
[ol]
[/ol]
值得注意的是,v10明明只有四个元素,但是传入的参数size却是20 = 5 * 4,同时v11被置0后又没有显式的赋值,最终却被返回,猜测应该是在cgoexp_17c794619cba_Java_com_qq_lib_EncryptUtil_encrypt中被赋值了
进入cgoexp_17c794619cba_Java_com_qq_lib_EncryptUtil_encrypt后发现参数个数很奇怪,而且sub_BC3C4658传入了很多重复的参数;
int __fastcall cgoexp_17c794619cba_Java_com_qq_lib_EncryptUtil_encrypt(int a1, int a2, int a3, int a4, int a5, int a6)
{
int v6; // r10
int v7; // lr
int v9; // [sp+14h] [bp-4h]
int v10; // [sp+14h] [bp-4h]
while ( (unsigned int)&a5
通过上面的观察分析,可以察觉到这不是常规的函数调用约定,而且肯定不是fastcall;注意到函数中出现了cgo字样,且在该so库的函数表中也有大量的cgo函数
分析:
[ol]
[/ol]
定位关键加密函数
虽然看起来有点奇怪,但是这并不妨碍定位到关键函数,跟进上面的sub_BC3C4658()函数:
int __fastcall sub_BC3C4658(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9, int a10)
{
int v10; // r10
int v11; // lr
int result; // r0
int v13; // [sp+Ch] [bp-2Ch]
_DWORD *v14; // [sp+10h] [bp-28h]
int v15; // [sp+10h] [bp-28h]
int v16; // [sp+14h] [bp-24h]
int v17; // [sp+20h] [bp-18h]
int v18; // [sp+24h] [bp-14h]
int v19[2]; // [sp+30h] [bp-8h] BYREF
while ( (unsigned int)&a5
跟进sub_BC3C0D0C(),发现了package_name,pakcage_hash字样,且进行了大量函数调用,将动态调试的目光先锁定在它身上
int __fastcall sub_BC3C0D0C(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9, int a10, _DWORD *a11, int a12, int a13, int a14)
{
while ( (unsigned int)&a5
[/ol]
五、so层动态调试、分析调用约定
[ol]
在jni接口处下断,符合fastcall调用约定
image-20220921175805969.png (102.07 KB, 下载次数: 1)
下载附件
2022-9-21 21:50 上传
传递给cgo的参数
image-20220921180034269.png (75.33 KB, 下载次数: 0)
下载附件
2022-9-21 21:51 上传
image-20220921180136723.png (40.66 KB, 下载次数: 1)
下载附件
2022-9-21 21:51 上传
进入cgo函数,首先观察栈平衡循环
image-20220921180451141.png (83.75 KB, 下载次数: 0)
下载附件
2022-9-21 21:52 上传
结束后
image-20220921180715293.png (109.45 KB, 下载次数: 1)
下载附件
2022-9-21 21:53 上传
观察从哪里取得参数
image-20220921181020664.png (104.08 KB, 下载次数: 0)
下载附件
2022-9-21 21:53 上传
在下一个函数调用前下断,观察参数传递
image-20220921181238897.png (132.09 KB, 下载次数: 0)
下载附件
2022-9-21 21:53 上传
f8步过,观察栈变化以及从哪里取的返回值
image-20220921181412980.png (103.19 KB, 下载次数: 0)
下载附件
2022-9-21 21:54 上传
总结得出函数调用:参数完全通过栈传递,返回值存储在参数往下的地址中
[/ol]
六、so层加密算法还原
[ol]
前置工作分析:可以跟踪调试sub_BC3C0D0C函数,发现这里只是进行了一些参数以及其他操作,真正的加密处理函数在这个函数的结尾处调用,即:sub_C2DC05EC
需要说明的是,这个函数中对java层传入的key进行了base64解码,并得到两个密钥:
[ol]
[/ol]
在sub_C2DC05EC处下断,分析参数
image-20220921182713971.png (33.09 KB, 下载次数: 0)
下载附件
2022-9-21 21:54 上传
首先对传入的两个key进行了异或得到一个新key
image-20220921182838936.png (70.15 KB, 下载次数: 0)
下载附件
2022-9-21 21:54 上传
再对key进行了两次转换,得到
第一次得到
image-20220921182913411.png (20.14 KB, 下载次数: 0)
下载附件
2022-9-21 21:54 上传
第二次得到,此时密钥已经成为一个不可读的字节序列,这也是最终加密算法使用的密钥
image-20220921183107882.png (20.25 KB, 下载次数: 0)
下载附件
2022-9-21 21:55 上传
之后生成了一个长度为0x10的随机串,这是最终加密算法使用的初始向量
image-20220921183027646.png (66.45 KB, 下载次数: 0)
下载附件
2022-9-21 21:55 上传
之后传入密钥,调用一个函数后返回了一个全局地址和一个指针
image-20220921183450895.png (76.12 KB, 下载次数: 1)
下载附件
2022-9-21 21:55 上传
其中指针的内容是
image-20220921183648512.png (103.95 KB, 下载次数: 0)
下载附件
2022-9-21 21:55 上传
到这里的话,因为前面已经猜测这是一个golang编写的so库,此时基本可以确定这是使用的go的crypto/cipher加密库了;
通过查看go加密的源码,能发现其newCipher最终会生成两个长度为0x3C即60的密钥,分别用来加密和解密;
又由于是对称加密,因此使用的是用一个密钥,这里生成的两个密钥刚好是逆序的关系,可能是因为方便实现的原因
image-20220921195430035.png (33.27 KB, 下载次数: 0)
下载附件
2022-9-21 21:56 上传
image-20220921195522728.png (17.1 KB, 下载次数: 1)
下载附件
2022-9-21 21:56 上传
image-20220921195620953.png (28.55 KB, 下载次数: 0)
下载附件
2022-9-21 21:56 上传
说明:这里usb断了一次连接,因此下面一些参数的地址可能和上面不同
接着,调用了一个保存在寄存器的地址,并将明文和前面密钥生成的结构当作参数传了进去
image-20220921190408627.png (136.95 KB, 下载次数: 0)
下载附件
2022-9-21 21:59 上传
函数返回后,那片内存空间里已经由全0填充为了字节序列,可以确定其为加密函数
image-20220921190708165.png (64.11 KB, 下载次数: 0)
下载附件
2022-9-21 21:57 上传
image-20220921190906871.png (90.51 KB, 下载次数: 1)
下载附件
2022-9-21 21:57 上传
最后,又对加密生成的序列进行了一次字母表映射,字母表为16进制的16个字符
image-20220921191342058.png (48.45 KB, 下载次数: 1)
下载附件
2022-9-21 21:57 上传
最终得到的密文为
image-20220921191405844.png (53.37 KB, 下载次数: 0)
下载附件
2022-9-21 21:57 上传
算法还原
找到最后一步密钥转换后生成的字节序列,这就是真正的加密密钥
确定加密算法,根据几个特征,推测应该是一种有初始向量的流加密,最终确定为CFB模式的aes加密
[ol]
[/ol]
之后,首先用go还原一下,验证加密算法无误
之后用python重现,这里要注意的是默认的python和go的CFB加密结果是不同的,需要在python加密中设置以下属性:
image-20220921191855157.png (13.15 KB, 下载次数: 0)
下载附件
2022-9-21 21:58 上传
最后验证python和go的加密结果一致即可
[/ol]
七、Java层签名算法分析还原
[ol]
接下来就是java层sign字段的生成了,这个比较简单,它依次调用了两个哈希算法
[ol]
sha256:根据post参数的格式及各个参数生成输入,经过sha256得到一个十六进制字符串
public static String e(String arg2) {
try {
MessageDigest v0 = MessageDigest.getInstance("SHA-256");
v0.update(arg2.getBytes("UTF-8"));
return a.b(v0.digest()); // 将字节数组转换为16进制字符串
}
catch(NoSuchAlgorithmException v2_1) {
v2_1.printStackTrace();
return "";
}
catch(UnsupportedEncodingException v2) {
v2.printStackTrace();
return "";
}
}
md5:将上面的到的sha256字符串经过md5变换得到最终的sign值
public static String b(String arg2) {
try { // a() 将字节数组转换为16进制字符串
return c.a(MessageDigest.getInstance("MD5").digest(arg2.getBytes("UTF-8")));
}
catch(Exception v2) {
v2.printStackTrace();
return "";
}
}
[/ol]
[/ol]
八、发包验证算法正确性
[ol]
用python实现它的数据加解密以及签名、封装过程,生成请求数据,并向其服务器的一个接口发起请求验证:
image-20220921194210696.png (385.57 KB, 下载次数: 0)
下载附件
2022-9-21 21:58 上传
服务器正常返回数据,解密得到数据:
[/ol]
image-20220921194419585.png (133.61 KB, 下载次数: 0)
下载附件
2022-9-21 21:58 上传