分组密码-逆向实践

xiaoeryu Lv5

之前几章已经对分组密码的原理以及填充进行了详细分析,本章使用一个demo来进行实战分析

  • 一个两层密钥的demo,第一层是java中的AES,第二层是native层的AES加密

第一层

  • 以32位安装方便进行分析测试

  • 一个简单demo,打开界面是输入框和按钮,点击按钮后校验密码错误的话,会弹窗提示错误

  • GDA打开看到有360的加固,先脱壳
  • 不想刷脱壳机使用frida_fart_hook 脚本脱壳也行,使用方法脚本里面写的很清楚,之前也讲过了这里不再赘述
分析脱壳后的dex
  • 直接查看onClick()函数,看到这里的校验是在test4()函数里面做的,那就继续分析test4()
  • 这里算法特征相关的函数和字符串都已经被隐藏了。这里只能看到一个key、IV以及要与之比较的一个值,不能看到是什么加密和工作以及填充模式
  • 所以如果想要搞清楚这些东西,需要使用hook的方式来获取
编写frida_hook脚本
  1. hook *loadClass()*获取它的参数看是加载了哪个类
  2. hook *contains()*获取它的参数查看前面获取了哪个method
function main(){
    if(Java.available){
        Java.perform(function(){
            Java.use("java.lang.ClassLoader").loadClass.overload('java.lang.String').implementation = function(arg0){
                console.log("java.lang.ClassLoader->loadClass: ", arg0);
                var result = this.loadClass(arg0);
                return result;
            }
            Java.use("java.lang.String").contains.implementation = function(arg0){
                var result = this.contains(arg0);
                // 这里因为dex中的代码是使用反射的方式来调用函数的,所以按函数逻辑加一个结果为true才打印,看起来会清楚点,不然打印的结果会非常多
                if(result == true){
                    console.log("java.lang.String->contains: ", arg0);
                }
                return result;
            }
        })
    }
}

setImmediate(main)
  • 让脚本以Attach附加的方式启动,在*onClick()*点击的时候触发。不然这种通用函数触发的位置太多了
  • 根据触发后的打印结果来看:
    • *loadClass()*加载了Cipher类
    • 然后通过反射的方式拿到了getInstance()、doFinal()、init()三个方法
  • 通过打印结果知道了它的调用方法之后,就直接hook这些函数就可以了
function hookCipher(){
    Java.perform(function(){
        Java.use('javax.crypto.Cipher').getInstance.overload('java.lang.String').implementation = function(arg0){
            console.log('javax.crypto.Cipher.getInstance is called!', arg0);
            var result = this.getInstance(arg0);
            return result;
        };
        // cipher.init(Cipher.ENCRYPT_MODE, getRawKey(key), iv);
        Java.use('javax.crypto.Cipher').init.overload('int', 'java.security.Key', 'java.security.spec.AlgorithmParameterSpec').implementation = function(arg0, arg1, arg2){
            // console.log("javax.crypto.Cipher.init is called!", arg0, arg1, arg2);
            var mode = arg0;
            var key = arg1;
            var iv = arg2;
            var KeyClass = Java.use('java.security.Key');
            var keyobj = Java.cast(key, KeyClass);
            var key_bytes = keyobj.getEncoded();
            // 把ASCII编码的字符数组转为字符打印出来,方便我们查看
            var StringClass = Java.use('java.lang.String');
            var key_string = StringClass.$new(key_bytes);
            console.log("key_string: ", key_string);

            var IVClass = Java.use('javax.crypto.spec.IvParameterSpec');
            var ivobj = Java.cast(iv, IVClass);
            var iv_bytes = ivobj.getIV();
            // 这里同样打印出来方便查看
            var iv_string = StringClass.$new(iv_bytes);
            console.log("iv_string: ", iv_string);

            console.log("javax.crypto.Cipher.init is called!", mode, JSON.stringify(key_bytes), JSON.stringify(iv_bytes));
            var result = this.init(arg0, arg1, arg2);
            return result;
        };
        // doFinal
        Java.use('javax.crypto.Cipher').doFinal.overload('[B').implementation = function(arg0){
            console.log("javax.crypto.Cipher.doFinal is called!", JSON.stringify(arg0));
            var data = arg0;
            var result = this.doFinal(arg0);
            console.log("javax.crypto.Cipher.doFinal is called!", JSON.stringify(data), "encrypt: ", JSON.stringify(result));
            return result;
        };
    })
}

现在就可以去CyberChef 中计算出AES的明文了:qazwsxedcrfvtgbyhnujm

第二层

分析native层AES加密
  • 第二层获取密钥需要分析这个*test3()*函数 ,它是一个静态注册的native函数,我们用ida打开so文件进行分析
  • 分析这个函数

    • 对这个函数经过分析之后,发现*sub_9744()*这个函数的返回值比较重要,要参与到后面的循环中让v5和v7两个字符数组进行比较,而且这个函数的接受了我们输入的字符串,和一串疑似是key的字符串。
    • 那等下就可以来hook这个函数看它的参数和返回值是什么,不出意外应该就是返回加密后的字符数组。然后让加密后的数组v5和v7进行对比,数组长度是32位
  • 但是,现在我们还没有在so中看出来这里用到了什么算法和什么工作模式、填充方式

    这里使用FindCrypt插件去分析匹配一下

    匹配到了AES的S盒跟S盒逆,通过这些S盒进行交叉引用就能跟踪到它最终在*sub_9744()*中进行了调用

编写frida_hook脚本

对*sub_9744()*函数进行主动调用和hook

在前面我们分析源码的时候看过在Android中对so的加载是通过*loadLibrary()*进行加载的

  1. 在之前的文章中已经分析过源码,这里不能直接对*loadLibrary()*进行hook,而要对loadelibrary()下面的加载函数进行hook
  • 在Android8.0的源码中是*loadLibrary0()*,这个要根据不同版本来hook不同的函数

  • 对于so的hook要选择一个时机,在*loadLibrary0()*加载了so文件之后,我们就可以对so文件中的函数进行hook了

    function hooksoMain(){
        if(Java.available){
            Java.perform(function(){
                Java.use("java.lang.Runtime").loadLibrary0.implementation = function(arg0, arg1){
                    console.log("java.lang.Runtime->loadLibrary0: ", arg1);
                    var result = this.loadLibrary0(arg0, arg1);
                    if(arg1.indexOf('native-lib') != -1){
                        hookso();
                    }
                    return result;
                }
            })
        }
    }
        
    
  1. 根据前面的分析,我们这里对*sub_9744()*进行hook

    function hookso() {
        var nativelibmodul = Process.getModuleByName("libnative-lib.so");
        var sub_9744_addr = nativelibmodul.base.add(0x9745);
        console.log("sub_9744:",sub_9744_addr);
        Interceptor.attach(sub_9744_addr, {
            onEnter: function (args) {
                console.log("sub_9744_addr arg0:", hexdump(args[0]));
                console.log("sub_9744_addr arg1:", hexdump(args[1]));
                console.log("sub_9744_addr arg2:", hexdump(args[2]));
            }, onLeave: function (retval) {
                console.log("sub_9744_addr retval:", hexdump(ptr(retval)));
            }
        })
    }
    
    • Spawn(生成模式)运行脚本frida -U -f 包名 -l 脚本名 --no-pause,需要先手动输入第一层密码,然后再尝试随便输入一个字符串当作第二层密码(这里输入的还是第一层的密码),查看被hook函数的返回值

      arg0是要加密的明文

      arg1是key

      arg2是IV

      返回值是加密后的串

    • 现在根据返回值和我们前面在ida中识别到的S盒,得知这是一个AES加密函数。但是对于工作模式和填充方式还是不知道。在有IV的情况下CBC、OFB、CFB都有可能可以在CyberChef中把这三种都尝试一下,看哪个结果能对上

      经过验证,CBC模式的加密结果和我们上面hook后得到的返回值是相同的,说明他就是CBC工作模式,加密后的结果长度是32位=明文的整数倍,可知它的填充模式是PKCS5Padding或者PKCS7Padding

  2. 最后在补充一点

    在测试的时候有时候需要一遍遍的重启来hook app,每次都手动输入第一层的密码比较麻烦,可以使用frida提供的*NativeFunction()*来直接主动调用目标函数,方便直接测试第二层的结果是否正确

    function activecallsub_9744() {
        var arg0=Memory.alloc(21);
        ptr(arg0).writeUtf8String('ujmyhntgbrfvedcwsxqaz');
        var arg1=Memory.alloc(21);
        ptr(arg1).writeUtf8String('i am encrypt key');
        var arg2=Memory.alloc(21);
        // 将十六进制字节序列写入到指定内存地址
        var byteArray = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f];
        var ptrArg2 = ptr(arg2);
        ptrArg2.writeByteArray(byteArray);
        console.log("activecallsub_9744",hexdump(ptr(retval)));
    }
    
验证结果
  • 在IDA中可以看到v7的值来自数组byte_17FA5

  • 接下来用cyberChef对这个值进行解密

    • ujmyhntgbrfvedcwsxqaz
  • 在编辑框中输入这个值

    • 成功通过了验证
附件

完整代码

  • 标题: 分组密码-逆向实践
  • 作者: xiaoeryu
  • 创建于 : 2024-03-05 01:31:05
  • 更新于 : 2024-03-22 20:36:55
  • 链接: https://github.com/xiaoeryu/2024/03/05/分组密码-逆向实践/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
此页目录
分组密码-逆向实践