App逆向百例|11|某咖啡App参数分析
2022-9-23 17:26:39 Author: 逆向lin狗(查看原文) 阅读量:77 收藏

观前提示:

本文章仅供学习交流,切勿用于非法通途,如有侵犯贵司请及时联系删除

样本:aHR0cHM6Ly9wYW4uYmFpZHUuY29tL3MvMXk0UVJ3UWVaNVJiVHBfbTBRQTFyUkE/cHdkPWxpbm4=

将样本App拖入JADX 发现有360的壳

使用FRIDA-DEXDUMP脱壳报错

经了解是双进程保护

解决方法就是使用spawn的方式就能成功hook上

App还拥有证书验证 Pass掉后即可正常抓包

本次的受害参数为signq

用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+1new 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+1new 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(015), (byte) randInt(00xff));//随机更改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狗


文章来源: http://mp.weixin.qq.com/s?__biz=MzUxMjU3ODc1MA==&mid=2247485135&idx=1&sn=2b69d0e74dbe195d5afe76d97c07d595&chksm=f963031ece148a08aadeae38fbf4cb846fbf672470e3ad07bf1944439418d140d2f617a270aa#rd
如有侵权请联系:admin#unsafe.sh