样本:aHR0cHM6Ly9wYW4uYmFpZHUuY29tL3MvMUNtbzZHU1lOUmM4a2l4YkVsT3Rkc1E/cHdkPWxpbm4=
打开样本App 直接抓包
本次的受害参数为sign
打开jadx定位到sign
的加密位置
加载的so为libjdbitmapkit.so
有了这些信息 上frida hook入参和结果
打开frida 启动样本App发现 App卡死闪退
可能有frida检测 那我们就使用葫芦娃大佬的魔改frida
github: https://github.com/hluwa/strongR-frida-android
继续启动frida 发现还是崩溃
猜测可能还有其他检测方式
根据网上文章给出的frida检测点进行多次尝试
经过多次试错后最后可知 样本App对frida的默认端口27042
进行检测
那就需要让frida运行在非默认端口
/data/local/tmp/hluda-server-15.1.17-android-arm64 -l 127.0.0.1:8080
然后映射端口
adb forward tcp:8080 tcp:8080
最后启动frida
frida -H 127.0.0.1:8080
成功运行frida且不闪退后就可以对样本进行hook操作了
function hook_getSignFromJni() {
Java.perform(function () {
var Class = Java.use('com.xxxx.common.utils.BitmapkitUtils');
var Method = "getSignFromJni"
Class[Method].overload('android.content.Context', 'java.lang.String', 'java.lang.String', 'java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function () {
var result = this[Method]['apply'](this, arguments);
console.log('----------------------');
console.log('arg1:' + arguments[0]);
console.log('arg2:' + arguments[1]);
console.log('arg2:' + arguments[2]);
console.log('arg2:' + arguments[3]);
console.log('arg2:' + arguments[4]);
console.log('arg2:' + arguments[5]);
console.log('result:' + result);
console.log('----------------------');
return result;
}
})
}
setImmediate(hook_getSignFromJni)
通过hook 拿到了多个结果 选取其中一个进行固定分析
arg1:[email protected]
arg2:hotWords
arg2:{"originHotWord":"0","pageFrom":"1"}
arg2:3036c83c3c4b25a2
arg2:android
arg2:10.2.0
result:st=1664463515894&sign=7ce2045026643e68ea1639c5e291127f&sv=122
复制粘贴造个轮子运行
根据报错信息开始补环境
vm.resolveClass("android/app/Activity");
取getpackagemanager
所以需要在前面的基础上再完善一点环境
vm.resolveClass("android/app/Activity",vm.resolveClass("android/content/ContextWrapper", vm.resolveClass("android/content/Context"))).newObject(null);
取ApplicationInfo
也就是apk的存放位置 可以在RE文件管理器/data/app
中找到对应的位置
new StringObject(vm,"/data/app/com.xxxx.app.mall-cd4VeJ0b5yxrR0Zb-io_MA=/base.apk");
取unZip
看传入参数是什么传入了仨参数
arg1->"/data/app/com.xxxx.app.mall-cd4VeJ0b5yxrR0Zb-io_MA=/base.apk"
arg2->"META-INF/"
arg3->".RSA"
根据参数打开APK中中的META-INF
搜索RSA
结尾的文件根据这个文件名补即可
new ByteArray(vm,vm.unzip("META-INF/xxxx.RSA"));
vm.resolveClass("sun/security/pkcs/PKCS7").newObject(new PKCS7((byte[]) varArg.getObjectArg(0).getValue()));
PKCS7 pkcs7 = (PKCS7) dvmObject.getValue();
X509Certificate[] certificates = pkcs7.getCertificates();
return ProxyDvmObject.createObject(vm,certificates);
这里的objectToBytes
是java层的一个方法 直接去复制粘贴就行
new ByteArray(vm,objectToBytes(varArg.getObjectArg(0).getValue()));
补到这里 恭喜你完成初始化了
主动调用getSignFromJni
public void getSignFromJni(){
ArrayList<Object> args = new ArrayList<>(10);
args.add(vm.getJNIEnv());
args.add(0);
args.add(vm.addLocalObject(vm.resolveClass("android/content/Context").newObject(null)));
args.add(vm.addLocalObject(new StringObject(vm,"hotWords")));
args.add(vm.addLocalObject(new StringObject(vm,"{\"originHotWord\":\"0\",\"pageFrom\":\"1\"}")));
args.add(vm.addLocalObject(new StringObject(vm,"3036c83c3c4b25a2")));
args.add(vm.addLocalObject(new StringObject(vm,"android")));
args.add(vm.addLocalObject(new StringObject(vm,"10.2.0")));
Number number = module.callFunction(emulator, 0x28b5, args.toArray());
System.out.println(vm.getObject(number.intValue()).getValue().toString());
}
然后接着报错接着补环境
vm.resolveClass("java/lang/StringBuffer").newObject(new StringBuffer());
StringBuffer stringBuffer = (StringBuffer) dvmObject.getValue();
return vm.resolveClass("java/lang/StringBuffer").newObject(stringBuffer.append(vaList.getObjectArg(0).getValue()));
Integer integer = new Integer(vaList.getIntArg(0));
return vm.resolveClass("java/lang/Integer").newObject(integer);
Integer integer = (Integer) dvmObject.getValue();
return vm.resolveClass("java/lang/Integer").newObject(integer.toString());
StringBuffer stringBuffer = (StringBuffer) dvmObject.getValue();
return new StringObject(vm,stringBuffer.toString());
恭喜你 补到这里就能出结果了
小声bb 写到这里就1k字了还没开始看算法
多次调用sv变动 st变动 sign也变动
IDA打开直接搜索
看到直接是静态注册的
双击跳转过去
这一段全是拼接操作
st
生成位置sv
生成位置
固定随机项
idea双击shift搜gettimeofday
改为固定的时间戳固定了时间 不同的sv算出来的sign结果也不一致观察代码 sv是通过lrand48
生成的
固定lrand48
HookZz instance = HookZz.getInstance(emulator);
instance.wrap(module.findSymbolByName("lrand48"), new WrapCallback<HookZzArm32RegisterContext>() {
int count=0;
@Override
public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
}
@Override
public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
count+=1;
if(count==2){
ctx.setR0(0x1);
}
if(count==1){
ctx.setR0(0x1);
}
}
});
就是这俩处位置结果变动使sign
的结果随之改变
改了这些就可以为所欲为了
前面拼接了一堆东西的结果传入sub_126AC
先放着继续往下看
sub_126AC
传回的结果放入了sub_18B8
进入sub_18B8
看到这里似乎是一个Base64的码表那sub_18B8
可能就是一个Base64方法
sub_18B8
的结果由v66传出并且传入sub_227C
进入sub_227C
看到似曾相识的东西 这样看可能不是很明显 手动分割一下
这不就是MD5
从魔数上看 似乎并没有魔改
验证一下前面的Base64和MD5猜想
拿到sub_126AC
的结果进行Base64再MD5
验证成功 那重心就在sub_126AC
了
回到sub_126AC
这里有3个case分别代表三个算法
而算法的走向是由v24决定
v24是由sv2和sv3决定的
根据伪代码逻辑可以翻译为
unk_17440 = [0, 1, 2]
sv1 = 1
sv2 = 1
sv3 = 1
print(unk_17440[(sv2 + sv3) % 3])
根据结果得到下表
sv1 | sv2 | sv3 | sv | case |
---|---|---|---|---|
1 | 0 | 0 | 100 | 0 |
1 | 0 | 1 | 101 | 1 |
1 | 0 | 2 | 102 | 2 |
1 | 1 | 0 | 110 | 1 |
1 | 1 | 1 | 111 | 2 |
1 | 1 | 2 | 112 | 0 |
1 | 2 | 0 | 120 | 2 |
1 | 2 | 1 | 121 | 0 |
1 | 2 | 2 | 122 | 1 |
往下看这里先是内存拷贝了一个值然后根据v12 * 40的值进行偏移 其实实现的就是一个数组取值的操作这里根据伪代码可以翻译为
v11 = ['44e715a6e322ccb7d028f7a42fa55040', '7d0069660c9b5d32074facf37c3738a1', '80306f4370b39fd5630ad0529f77adb6']
v13 = v11[v12]
接下来的重点就是在
前面手动固定了lrand48
sv为111
所以走的是case 2
进入sub_10DE4
只有三个方法
算法在sub_12ECC
中 双击进来
根据hook入参得到以下结果
a2->80306f4370b39fd5630ad0529f77adb6
a3->0x1
a4->functionId=hotWords&body={"originHotWord":"0","pageFrom":"1"}&uuid=3036c83c3c4b25a2&client=android&clientVersion=10.2.0&st=1664689004670&sv=111
a5->0x8f
由入参可知a4为拼接后的明文 a5是a4的长度
所以这个if是必定成立的 else后面的那一块可以忽略不看
这一段主要在计算v21
这里的a3对应的是sv1 而sv1固定为1 所以同理 不用理else部分
这里就是sub_12ECC
的核心计算位置
其中
v16 = &v21[7] + (v15 & 0xF);
v18 =*(v16 - 20);
从汇编中可知R0=(SP+0x20-0x14)+(R3&0xf)
所以这段实现的操作是v21[v15&0xf]
v21结果为SP+0xC
也就是前面小端结果往下看
v17 = v15++ & 7;
result = ((v18 ^ *a4 ^ *(a2 + v17)) + v18);
此处为取值进行异或操作
LOBYTE(v18) = v18 ^ result;
*a4++ = v18;
*(a4 - 1) = v18 ^ *(a2 + v17);
将异或后的结果取低位然后再与a2[v17]
进行运算最后算出结果
整个循环翻译成py简简单单没有难点
v15 = 0
v21 = [0x37, 0x92, 0x44, 0x68, 0xA5, 0x3D, 0xCC, 0x7F, 0xBB, 0x0F, 0xD9, 0x88, 0xEE, 0x9A, 0xE9, 0x5A]
while v15 != a5:
v18 = v21[v15 % 16]
v17 = v15 & 7
result = (v18 ^ a4[v15] ^ a2[v17]) + v18
v18 = v18 ^ result % 256
a4[v15] = v18 ^ a2[v17]
v15 += 1
运算后的结果Base64再MD5即为sign值
这是简单的case 2
接下来将前面固定lrand48
的返回值改为0x2
使得sv为122
当sv为122时 走case 1
进入sub_10E18
和前面一样进来就是三个方法
但是不同的是出现了一个nullsub_1
那就分析不了吗?
并不然 从前面的分析结合这里可知 sub_125F0
可能为初始化 sub_12510
可能为计算核心方法 那nullsub_1
就可能是释放 所以并不需要理睬nullsub_1
进入sub_12510
入参和前面基本一致 a2变为7d0069660c9b5d32074facf37c3738a1
这里循环每次取8个字节进入sub_10EA4
计算 一共循环a5 >> 3
次
进入sub_10EA4
955行代码 有点多 不过大部分都能直接复制
这里一堆与操作的目的就是将传入的8位分别和0x80 0x40 0x20 0x10 8 4 2 1
与操作扩展至64位
接着就是一堆赋值 直接Ctrl+C Ctrl+V
这里为遍历a2
然后进行判断走不同分支
其中出现了一些goto操作
Python本身是没有的 但是可以依靠一个库goto-statement
来实现
pip install goto-statement
https://www.w3cschool.cn/article/3069641.html
这里就是将前面计算好并且重新赋值后的64位循环4次计算 每次取16位
每次循环更改2位 循环4次一共8位
实现了goto 其他操作只需要复制粘贴复制粘贴并稍微改改就能实现 反正全靠肝
回到上层 这里一共循环0x8f >> 3 = 17
但是似乎还有部分明文并没有参与计算 而最后得出的结果显示 全部都参与了计算
hook验证猜想
确实 只循环了17次 后面还会有&sv=122
没有参与计算 但是从最终结果来看 确实是计算到了 那是哪里计算了呢?
直接traceWrite
emulator.traceWrite(0x4021c080L,0x4021c080L+10L);
跳转0xfbd0
这里就是赋值位置 但是 这里居然有3509行 这谁顶得住
先不管 看一下sub_E7FC
的交叉引用
只一个 跳转过来
从(a4 & 7) - 1
可知
这。。这段不就是根据未参与计算的明文的长度走不同的方法 而且每个方法都有上千行 留给有肝的人还原吧
将之前还原好的做个验证没问题
接下来将前面固定lrand48
的返回值改为0x0
使得sv为100
当sv为100时 走case 0
进入sub_10E4C
一样的三个方法
进入sub_12580
看到核心方法是一样的sub_10EA4
不过16变成了32
其余的和前面分析case2的一致
到此整一个流程就基本走完了 最后再拼接成st=xxx&sign=xxx&sv=xxx
即可
-恭喜你 看完了这篇又水又长的东西-