观前提示:
本文章仅供学习交流,切勿用于非法通途,如有侵犯贵司请及时联系删除
样本:aHR0cHM6Ly9wYW4uYmFpZHUuY29tL3MvMXk0UVJ3UWVaNVJiVHBfbTBRQTFyUkE/cHdkPWxpbm4=
将样本App拖入JADX 发现有360的壳
使用FRIDA-DEXDUMP
脱壳报错
经了解是双进程保护
解决方法就是使用spawn的方式就能成功hook上
App还拥有证书验证 Pass掉后即可正常抓包
本次的受害参数为sign
和q
用JADX打开脱好的DEX 直接定位到加密位置
通过上面的位置最终定位到
可以看到sign的计算最终是调用so层的md5_crypt
方法
往上看加载so的文件名通过ApplicationC1081StubApp.getString2
给加密了
主动写个call方法调用
function StubApp(str){
Java.perform(function () {
var Class = Java.use('com.stub.StubApp');
var Method = "getString2";
var result = Class[Method](str);
console.log(result);
})
}
可以看到最终加载的so是cryptoDD
顺便frida hook入参和结果
function hook_a(){
Java.perform(function () {
var Class = Java.use('com.xxx.safeboxlib.CryptoHelper');
var Method = "a";
Class[Method].overload('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('result:'+result);
console.log('----------------------');
return result;
}
})
}
得到入参和结果方便分析
arg1:cid=210101;q=8EPO3SflRwrFi_pnFiANIO_hF_ztWu_3IwwLHqKqsId6ZW44ZebJ_ymIAGvQXtvRUHc4gsPQZOlbO0gH2GW6yAM66qNVLgAzyt-U8r9_sbGSUyrnslFmwLWcRHvFiYDX;uid=28167131-ac1d-4b66-810e-5071d8868fc61663316597190
arg2:1
result:1654061269899483325923884626986060448
可以看到这个结果的长度为37并非MD5输出的32位 还需要在so里面分析
将so文件拖入IDA 搜索导出表
发现并非静态注册
那就直接上Unidbg吧 也方便了后续调试
先把轮子复制粘贴过来 而且并不用补环境
public class luckin extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private String dfastring; luckin() {
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.xxx.xxx").build();
final Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\xxx\\xxx\\xxx.apk"));
DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\xxx\\xxx\\libcryptoDD.so"), true);
module = dm.getModule();
vm.setJni(this);
vm.setVerbose(true)
dm.callJNI_OnLoad(emulator);
}
public static void main(String[] args) {
luckin luckin = new luckin();
}
}
得到动态注册的偏移地址后在IDA上跳转过去跳转过来后可以看到视图这密密麻麻的区块 可以确定是一个ollvm
看伪代码看到一堆控制流 并不好找出加密的关键位置
这时刚刚造好的Unidbg就派上用场了 主动调用md5_crypt
public void md5_crypt() {
List<Object> args = new ArrayList<>(10);
args.add(vm.getJNIEnv());
args.add(0);
byte[] input = "cid=210101;q=8EPO3SflRwrFi_pnFiANIO_hF_ztWu_3IwwLHqKqsId6ZW44ZebJ_ymIAGvQXtvRUHc4gsPQZOlbO0gH2GW6yAM66qNVLgAzyt-U8r9_sbGSUyrnslFmwLWcRHvFiYDX;uid=28167131-ac1d-4b66-810e-5071d8868fc61663316597190".getBytes(StandardCharsets.UTF_8);
args.add(vm.addLocalObject(new ByteArray(vm, input)));
args.add(1);
Number number = module.callFunction(emulator, 0x1a981, args.toArray());
Inspector.inspect((byte[]) vm.getObject(number.intValue()).getValue(), "md5_crypt");
}
输出结果与hook的结果一致
根据输出的日志直接跳转到SetByteArrayRegion
调用处
跳转过来后可以看到 加密逻辑在doMD5sign
中
看上面的伪代码可知传入的initial_msg
还拼接了一个长度为20的字符串
盲猜后面部分dJLdCJiVnDvM9JUpsom9
就是盐
进入doMD5sign
继续观察
这里并没有经过ollvm污染 逻辑很明显 其中第一行就是MD5 让我们验证一下是否为无魔改的MD5
同样的hook住md5
输出mr2的返回结果
通过对比 结果一致 所以这就是一个无魔改的MD5
拿到MD5后经过四次bytesToInt
最后将所有返回的结果拼接在一起返回
进入bytesToInt
能看到这里还是被污染了 但是不要慌
这里返回的是v9 而v9的赋值处仅仅只有一处
所以剩下的根据伪代码逻辑照搬翻译即可
通过上面的代码 最终定位到
老规矩先hook入参和结果
arg1:{"supportTakeout":"1","implSource":"1","latitude":"23.099416","deptId":"324775","appversion":"4930","longitude":"113.477171"}
arg2:0
arg3:uATCFcK8LrUJHq4kOVZ8wvTMgcA4hx57kPtQeMgFKtaNn1swuJCl3QTm1P9xOnKNzTwzVjBK4y7WYDx2M4uexlld2rupEImTvN1Z9AWiFs-5C--eNSnif7SsT-yaUqQstV5SyB_woZdtCSi6NFirksZMEAuA8_nCcBlVjw5JB0w=
使用Unidbg主动调用
public void localAESWork4Api() {
List<Object> args = new ArrayList<>(10);
args.add(vm.getJNIEnv());
args.add(0);
byte[] input = "{\"supportTakeout\":\"1\",\"implSource\":\"1\",\"latitude\":\"23.099416\",\"deptId\":\"324775\",\"appversion\":\"4930\",\"longitude\":\"113.477171\"}".getBytes(StandardCharsets.UTF_8);
args.add(vm.addLocalObject(new ByteArray(vm, input)));
args.add(0);
Number number = module.callFunction(emulator, 0x1b1cd, args.toArray());
Inspector.inspect((byte[]) vm.getObject(number.intValue()).getValue(), "localAESWork4Api");
}
查看结果
对比一致
那接下来就可以为所欲为了
根据名字可以知道加密方式为AES 接着验证是CBC还是ECB模式
将input改为1234567890abcdef1234567890abcdef
运行观察结果
根据结果可以判断为ECB模式 后面部分应该是填充部分
知道了AES ECB 那就得找KEY了 回到IDA 跳转到过去
看到wbaes
一般就是白盒aes了
进入android_native_wbaes_jni
可以看到ECB的猜想是正确的
进入wbaes_encrypt_ecb
主要的逻辑在aes128_enc_wb_coff
进入aes128_enc_wb_coff
这里一堆查表操作 基本实锤白盒了
白盒情况下 就是要找轮和state 采用DFA攻击 需要在第九轮时进行修改state
先造个前提 将input改为123456
使其输出结果长度为32
然后记录下未进行攻击状态下的正确明文68fe8c552b93481754881068bbc3f96b
在wbShiftRows
处就是一个很好的位置
final Debugger debugger = emulator.attach();
debugger.addBreakPoint(module.base + 0x14F98+1, new BreakPointCallback() {//wbShiftRows
int count = 0;
@Override
public boolean onHit(Emulator<?> emulator, long address) {
count += 1;
System.out.println("count->" + count);
return true;
}
});
根据hook结果输出完全符合AES的10轮
继续写hook 修改第九轮的state
final Debugger debugger = emulator.attach();
debugger.addBreakPoint(module.base + 0x14F98+1, new BreakPointCallback() {//wbShiftRows
int count = 0;
@Override
public boolean onHit(Emulator<?> emulator, long address) {
count += 1;
System.out.println("count->" + count);
RegisterContext context = emulator.getContext();
final UnidbgPointer pointerArg = context.getPointerArg(0);
//onleave
debugger.addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
if (count % 9 == 0) {
pointerArg.setByte(randInt(0, 15), (byte) randInt(0, 0xff));//随机更改0-15的位置 然后附上0-0xff的差异值
}
return true;
}
});
return true;
}
});
运行得到一个错误密文
对比可以看到部分值被修改 此时我们就完全了一次DFA攻击
68fe8c552b93481754881068bbc3f96b//正确的
68b18c55ef93481754881019bbc3e46b//DFA攻击后的
单凭一次攻击并不能拿到KEY 这时Unidbg的好处就出来了 我们可以N次调用
这里我调用了300次 得到一堆错误密文再加上一个正确的密文 放到phoenixAES
跑出第十轮的KEY
得到第十轮的KEY后就能使用aes_keyschedule
逆推出KEY
Github https://github.com/SideChannelMarvels/Stark
最终算出AES的KEY为644A4C64434A69566E44764D394A5570
验证结果 结果一致 大功告成
相信有的人看到这里会出现一堆问号吧 为什么要这样做 为什么选择第九轮 为什么修改state 为什么能算出来KEY 这里 我就要推一推白龙的星球了
里面的白盒专题看完学完 让你解决以上疑惑 让我们一起来星球里面催龙龙更新吧!
感谢各位大佬观看
感谢大佬们的文章分享
如有错误 还请海涵
共同进步 带带弟弟
[完]
点赞 在看 分享是你对我最大的支持
逆向lin狗