系列目标
- 常见加解密算法的基本原理
- 在App逆向分析过程中快速识别其使用的算法
基本原理
加密的工作原理是将“明文”编码为“密文”,通常通过使用被称为算法的密码学数学模型来实现。要将数据解码回明文,需要使用解密密钥,也就是由某种算法创建的一串数字或密码。安全的加密方法会有大量的加密密钥,因此未经授权的人员既无法猜出正确的密钥,也无法使用计算机通过尝试每个可能的组合(称为暴力破解)轻松计算出正确的字符串。
解密的工作原理就是加密的逆过程。

分类
- 对称密码:即加密与解密时使用相同密钥典型的有RC4、DES、AES等。
- 序列密码(流密码):将明文消息按字符逐位进行加密。明文与密文长度一致。RC4是序列密码。
- 分组密码:将明文消息分组(每组有多个字符),逐组进行加密。由于进行了分组,最后一组可能长度不够而进行填充,所以密文可能比明文要长。
- 非对称密码:即加密与解密时使用不用密钥典型的有RSA、ECC椭圆曲线等。
- 散列算法:又称哈希函数,对不同长度的输入消息,产生固定长度的输出。这个固定长度的输出称为原输入消息的"散列"或"消息摘要"(Messagedigest)。
BASE64编码
BASE64不是一个加密算法,只是一个编码,它没有密钥。只要得到其编码算法,就能还原。
我们以字符串"Hello"为例,解释Base64算法:
- 将字符串"Hello"转换为二进制数据:
- 将这些二进制数据拼接在一起:
- 将这些二进制数据按照每3个字节(24位)进行分组:
- 将每个分组的24位数据分割成4个6位的小组:
- 将每个6位的小组转换为对应的Base64字符:
- 将这些Base64字符拼接在一起:
在这个例子中,
Hello经过Base64编码后的结果是SGVsbG8=。请注意,由于Hello的字节数不是3的倍数,因此会在编码结果中添加额外的=字符进行填充。可以想一下,编码完成前后字符长度的关系:
上面的第5步是核心,它在转换的时候是有一个编码表的:

所以,如果我们改变这个表,比如交换一些字符的位置,就能实现一些base64的变种。
了解了BASE64的原理,那BASE32也就好理解了,它就是使用32个可见字符做编码表,然后是5个bit作为一组,也有填充。

同理,BASE16也好理解。

例子1
base64_ollvm.apk
查看JNI函数:
第一个参数改成
JNIEnv 指针类型,可读性会好一些。so里面方法不多,可以一个一个看过去,看谁比较可疑,也可以直接搜索编码表字符串,当然也有可能遇到字符串加密的情况。
找到
sub_72c 函数,混淆的非常严重:
看反汇编还是可以看到编码表的:
使用 frida 来主动调用一下这个函数,看看它的输入与输出:
注意,这里使用 12.8.0 版本的 frida 来执行,15.2.2的在 cli 里面找不到函数,需要再研究研究。
看结果:
可以看到 sub_72C 这个函数就是做了一个 base64 编码操作。
后来研究了一下 的使用,发现要对函数进行导出,导出名不可以有大写字母或者下划线。需要将代码写在 agent/index.ts 里面,使用 typescript 编写,体验也还不错。
加载脚本,使用 npm run watch,脚本更新后自动编译到 _agent.js:
不加
-f 参数就不会重新启动,但是需要传递进程 id:脚本如下:
输出结果如下:
例子2
base64_ollvm_packer.apk
这个就是上面的包加了个壳而已,使用 frida-dexdump dump一下就可以了。
frida直接进行hook与调用,不需要考虑classLoader:
当然如果想兼容性更好,还是要遍历 classLoader:
查看输出:










