各位聪明绝顶,才高八斗的读者们你们好!今天我们主要讨论编译之后的RC4算法识别。
题外话,之前看到一个蛋疼的小知识,说“势”这个字最好不好查词典释义。我是很好奇的,果然后来无法直视势不可挡这个成语。
言归正传,我们将上一篇的Java与C的代码都编译一下,分别反编译看看。
Java版
Java版的就很好识别,几乎与源码没啥区别。
C版
这里我们先给出RC4加密算法在逆向分析过程中的快速识别经验:
-
首先判断明文和密文长度是否相等,等长则代表是序列密码。
-
接下来判断是否是RC4。RC4算法中的初始化算法(KSA)中有两轮非常显著的长度为256的循环体,用于根据给定的key生成s盒。伪随机子密码生成算法(PRGA)会根据上一步得到的扰乱的s盒,进一步生成子密钥流,最终和给定的明文进行逐字节的异或。
由于 so 是可以加 ollvm 的,而且还可以做字符混淆,所以逆向起来不会像Java那么容易,在我们找到一个怀疑对象函数之后,就可以通过编写frida脚本或xposed插件完成对相应函数的主动调用,判断当输入明文为任意长度时的密文长度。
关于 ndk 的混淆是有很多文章的,自己可以尝试一下控制流混淆与字符串混淆。
我们看一个混淆之后的例子:
sub_F658(&unk_38008, dest);
unk_38008
实际上是一个字符串,不过这个字符串被混淆了,在 init
阶段后才会被解密。
看里面的逻辑:
sub_F0AC((__int64)v7, (__int64)a1, v4);v2 = strlen(a2);return sub_F3C4(v7, a2, v2);
有2个函数比较可疑,先看 sub_F0AC
:
先看1处,有一个很明显的交换数组元素的逻辑。
再看2处,只有当 v7 = 915845509 的时候才会交换逻辑,循环次数是 256 次,这些都与 RC4 的 init 阶段是一样。
当然这里也只是大胆猜测,而且混淆级别开的没那么高,所以还能看清楚一定的逻辑。实际上这里就已经很难看出来第一个给 S 盒赋值的循环在哪里了,只能根据 result 的位置来看。
后面我们可以使用hook来验证我们的想法。
再看 sub_F3C4
函数:
看 1,2处,发现循环会执行的次数与第3个参数一样,看前面的逻辑:
这就是说,循环次数是第二个字符串的长度。配合第3处的异或,也可以合理怀疑这里就是加密函数。
Hook验证
sub_F0AC((__int64)v7, (__int64)a1, v4);
通过上面的分析,第一个参数是返回值,第二个参数是密钥,第三个参数是密钥长度。所以返回的是一个密钥流。
sub_F3C4(v7, a2, v2);
第一个参数是密钥流,第二个参数是明文,第三个参数是明文的长度。返回值(第一个参数)是加密后的结果。
sub_F658()
所以,这个函数就是一个 RC4 算法,我们 hook 这些函数,观察参数与结果。
export function invoke_sub_F658(arg1: string, arg2: string) {var offset = 0xF658;var module = Process.getModuleByName("libnative-lib.so");var sub_address = module.base.add(offset);var arg1_address = Memory.alloc(10);let arg2_address = Memory.alloc(10);arg1_address.writeUtf8String(arg1);arg2_address.writeUtf8String(arg2);var hooked_sub_f658 = new NativeFunction(sub_address, 'pointer', ['pointer', 'pointer']);var result = hooked_sub_f658(arg1_address, arg2_address);console.log("result:", hexdump(result));
}function hook_libnativelib() {var nativelibmodule = Process.getModuleByName("libnative-lib.so");var subf0ac = nativelibmodule.base.add(0xf0ac);var subf3c4 = nativelibmodule.base.add(0xf3c4);Interceptor.attach(subf0ac, {onEnter: function (args) {console.log("secret key: ", hexdump(args[1]), '\n length:', args[2]);}, onLeave: function () {}});Interceptor.attach(subf3c4, {onEnter: function (args) {this.arg1 = args[1];console.log("input: ", hexdump(args[1]), args[2]);}, onLeave: function () {console.log("output: ", hexdump(this.arg1));}});}function main() {if (Java.available) {Java.perform(function () {console.log("go into main");var RuntimeClass = Java.use('java.lang.Runtime');RuntimeClass.loadLibrary0.implementation = function (arg0: object, arg1: string) {var result = this.loadLibrary0(arg0, arg1);console.log("Runtime.loadLibrary0:", arg1);if (arg1.indexOf('native-lib') != -1) {hook_libnativelib();}return result;}})}
}rpc.exports = {invokesubf658: invoke_sub_F658
};setImmediate(main);
注意,我们hook so 的加载选择的是 Runtime
类,这是因为直接hook System
类会导致 Reflection.getCallerClass()
的值出问题。还有就是我的手机系统换到了 Android 10,注意自行查看源码区别。
hook 逻辑也很简单,就不细说了,看一下就明白。
注入脚本,看一下输出:
┌──(root㉿kali)-[~/workspace/frida-agent-example]
└─# frida -U -f com.kanxue.rc4 --no-pause -l _agent.js
secret key:0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
724f339008 72 63 34 74 65 73 74 00 31 32 33 34 35 36 37 38 rc4test.12345678
可以看到,secret key 是 rc4test
。
input:0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7fe568aa10 31 32 33 34 35 36 37 38 00 00 00 00 00 00 00 00 12345678........
加密的明文是 12345678
。
加密后的结果是:
output:0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7fe568aa10 a6 df 7a 5d dd ea 97 47 00 00 00 00 00 00 00 00 ..z]...G........
从 a6 到 47 这8个字节。
我们也可以主动调用函数,反复确认输入与输出是否一样长。