分组密码-逆向实践
之前几章已经对分组密码的原理以及填充进行了详细分析,本章使用一个demo来进行实战分析
- 一个两层密钥的demo,第一层是java中的AES,第二层是native层的AES加密
第一层
以32位安装方便进行分析测试
一个简单demo,打开界面是输入框和按钮,点击按钮后校验密码错误的话,会弹窗提示错误
- GDA打开看到有360的加固,先脱壳
- 不想刷脱壳机使用frida_fart_hook 脚本脱壳也行,使用方法脚本里面写的很清楚,之前也讲过了这里不再赘述
分析脱壳后的dex
- 直接查看onClick()函数,看到这里的校验是在test4()函数里面做的,那就继续分析test4()
- 这里算法特征相关的函数和字符串都已经被隐藏了。这里只能看到一个key、IV以及要与之比较的一个值,不能看到是什么加密和工作以及填充模式
- 所以如果想要搞清楚这些东西,需要使用hook的方式来获取
编写frida_hook脚本
- hook *loadClass()*获取它的参数看是加载了哪个类
- 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()*进行加载的
- 在之前的文章中已经分析过源码,这里不能直接对*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; } }) } }
根据前面的分析,我们这里对*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
最后在补充一点
在测试的时候有时候需要一遍遍的重启来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 进行许可。