常见加密算法逆向特征分析
常见加密算法逆向特征分析
本博文秉持不求甚解的精神,不求理解,只求看出来是什么算法。
此外,本博文在AES、RC4部分有对b站up主:可厉害的土豆 视频的参考,并截取了其中的一些图片,因此本博客并非完全原创。
0x01 仿射加密
一、解密脚本
先上脚本
1 | #仿射密码加密与解密实现算法 |
二、程序情况
1、基本概况
仿射就是利用如:
的表达式,通过密钥a、b对密文x进行加密。
2、特征
一般来说,因为仿射加密加密的内容只能是英文的26个字母,因此一般程序一开始会对于输入做限制:
然后加密就这么写:
1 | for(i=0;i<len;++i){ |
0x02 DES
DES加密是一种经典的分组、对称加密算法。
一、解密脚本
1 | def desenc(key, plaintext): |
二、程序情况
des好就好在,虽然过程有点复杂,但是他的常量多的起飞。
1、基本情况:
密钥长 64 位,密钥事实上是 56 位参与 DES 运算(第 8、16、24、32、40、48、56、 64 位是校验位,使得每个密钥都有奇数个 1),分组后的明文组和 56 位的密钥按位替代或交换的方法形成密文组。
其主要加密流程有16轮:
2、常量使用
ip置换:
1 | const char IP_Table[64]= { |
S盒置换:
1 | const char S_Box[8][4][16] = { |
P盒置换:
1 | const char P_Table[32] = { |
0x03 AES
AES加密是一种经典的分组、对称加密算法,其出现是为了代替安全性不够的DES算法。
一、解密脚本
类型转化可以参考上文DES中的transform函数
1 | # -*- coding: utf-8 -*- |
二、程序情况
1、基本情况
在AES标准规范中,分组长度只能是128位,也就是说,每个分组为16个字节(每个字节8位)。密钥的长度可以使用128位、192位或256位。密钥的长度不同,推荐加密轮数也不同,如下表所示:
AES | 密钥长度(32位比特字) | 分组长度(32位比特字) | 加密轮数 |
---|---|---|---|
AES-128 | 4 | 4 | 10 |
AES-192 | 6 | 4 | 12 |
AES-256 | 8 | 4 | 14 |
2、加密流程
分组后,每一组的加密流程如下:
如图,前9轮每轮有四步,最后一轮不进行列混合。
3、特征情况
注意,这里不涉及到AES的加密流程讲解,只涉及到特征。
密钥拓展:
轮函数加密一定需要密钥,因此密钥拓展一定发生在一开始:
密钥拓展函数中,会进行两个循环:
第一个用于求w0-w3,第二个用于求w4-w43:
1 | /* W[0-3] */ |
两个循环的为代码中,都会有如上所示的移位和异或运算。
同时,如上代码块所示,还有重要常量rcon:
1 | static const unsigned int rcon[10] = { |
初始变换:
就是一顿的异或。
轮函数:
轮函数中,我们涉及到四个函数:
- subBytes(state); 字节代换
- shiftRows(state);行移位
- mixColumns(state); 列混合
- addRoundKey(state, rk); 轮密钥加
字节代换中我们会用到非常重要的常量S盒(S盒一样基本就实锤了):
1 | unsigned char S[256] = { |
字节代换步骤,会将状态矩阵与s盒进行代换处理。
行移位就是简单移位操作,没有特征;
列混合中,我们需要用到矩阵:
1 | M[4][4] = {{0x02, 0x03, 0x01, 0x01}, |
用于进行列混合操作,这个函数的逻辑相对比较复杂。
轮密钥加,就是一堆的异或。
0x04 SM4
SM4是中国特有的分组加密算法,分组长度为128bit,密钥为128bit,轮数为32
一、解密脚本
1 | import binascii |
二、程序情况
1、常量使用
1 | static const unsigned char SboxTable[16][16] = |
0x05 rc4
rc4是流密码,其逻辑比较简单,明文和密文一样长,通过简单的异或(明文XOR密钥)加密进行加密,其核心就是给定一个随意长度的密钥,生成伪随机数的密钥生成算法。
不好的是,rc4没有任何常量标识,这使得不熟悉的情况下难以辨认。
一、解密脚本
建议不要使用库函数,因为有时候编写者会稍微改变一点rc4的逻辑,这时候我们需要进行修正,库函数不能修正。
1 | def init_sbox(key) : |
二、程序情况
1、主程序思路
rc4加密主程序如下:
1 | init_sbox(); |
可以看到非常简单:
第一步:根据用户给出的不定长密钥,初始化密钥;
第二部:将密钥与明文进行异或,得到密文;
看到这样的异或加密,我们就可以猜测是一种流密码,但是我们依然需要进一步了解**init_box函数 和 generate_key函数 **的逻辑,这样才能判断是哪一种流密码,其逻辑如下
一、初始化S表 (init_sbox)
- Step1:对S表进行线性填充,⼀般为256个字节;
- Step2:用种子密钥填充另⼀个256字节的K表;
- Step3:用K表对S表进行初始置换。
二、密钥流的生成generate_key(为每个待加密的字节⽣成⼀个伪随机数,⽤来异或)
注:表S⼀旦完成初始化,种⼦密钥就不再被使用。
2、特征
由于没有特征值,所以我们只能对照逻辑硬看,感觉八九不离十的,应该就是rc4
init_sbox:
1 | //c参考实现思路 |
简单的说,这个函数就是初始化了一个大小256,记录从1到256的数组,称为s盒;
然后通过我们的输入(key)的值,循环对盒进行元素的换位置,将s盒中数字的顺序打乱
generate_key:
我们用打乱的S盒,可以进行密钥的生成
1 | unsigned char generate_key(){ |
这个函数的特征就是,没有循环,也不需要传参,直接就是一个逻辑运算,然后返回一个值。与此同时他就已经改变了S盒的构造,下一次运行时,又会返回一个新的值。
0x06 MD5
一、解密脚本
md5和hash都是哈希算法,都是单向的,所以不存在加解密,也不存在密钥,只有明文和密文。发现是md5或sha之后,直接就是一个爆破,一般来说题目出得都应该是四位数,因此脚本如下:
1 | import hashlib |
二、程序情况
1、输入输出:
输入输出:输入明文不定长,输出永远是16字节;
2、主要函数和加密思路:
先看md5的主程序:
1 | size_t filledLen; |
- 第一步,补位(filledLen),
- 使得输入的长度位512*n+448(单位为bit);补位通过形如10000000….的比特位进行。
- 然后再补64位,用于记录原始信息长度。
- 补位结束后,信息长度为512*n bit;
- 第二步,初始化(md5_init):
- 定义四个四字节标准幻数,总大小十六字节,作为循环运算的初始值;(固定的,后文会写);
- 四个标准幻数应以小端字节序存储;
- 第三步,混淆(64位一组):
- 使用FF、GG、HH、II四个函数,参与混淆过程;
- 最终输出结果位四个标准幻数混淆后的值。
3、特征
我们并不关系混淆的逻辑,只关心算法的特征。
补位操作:
补位操作中一般都可能会按照字节进行操作,那么我们就可以看到类似64和56(448/4)的常量,且补位时需要先补充一个1,因此如果按照字节补充,就会出现0x80;
初始化:
初始化操作不在循环中进行,同时,初始化的四个幻数是固定的:
1 | int A=0x67452301 |
就是1到E再从E到1,只不过是小端存储。
因此,有时候也会以数组的形式顺序定义,然后通过memcpy等函数进行进一步的小端存储。
混淆:
混淆肯定是循环,循环轮数是补位后的长度除以64,且其使用的函数FF、GG、HH、II逻辑非常有识别度。
其函数中会存在大量的调用如下图:
每个函数调用16次,且传参为七个。
进一步进入,FF、GG、HH、II的逻辑都是简单的位运算,不具有分支循环逻辑。
0x07 sha256
一、解密脚本
sha256脚本与md5通用,在上文中修改函数名即可,不多赘述。
二、程序情况
sha256是sha2下的分支哈希函数。
1、输入输出
对于任意长度的消息,SHA256 都会产生一个 256bit 长的哈希值,相当于是个长度为 32 个字节的数组,通常用一个长度为 64 的十六进制字符串来表示。
2、主要函数和加密思路:
主函数思路如下:
1 | sha_init(&A,&B,&C,&D,&E,&F,&G,&H); |
第一步,初始化sha_init:
- 和md5相似,sha256也需要初始化一个标准幻数,对其进行处理最后达成输出;
- 他们也是以小端进行存储,一共8个,每个四字节,是通过质数平方根的小数求得,但是不重要,因为是固定的(见下文);
第二步,修正输入sha_update:
- 和md5相同,sha修正的思路完全一致;
- 使得输入的长度位512*n+448(单位为bit);补位通过形如10000000….的比特位进行。
- 然后再补64位,用于记录原始信息长度。补位结束后,信息长度为512*n bit;
第三步,轮加密:
- 先使用sha_transform进行初步对于数据的类型转化处理;
- 轮加密的整体思路就是分块进行,先把输入分成定长大小的块,然后再把每个块在分块进行逻辑加密;
3、特征
初始化:
标准幻数是最容易识别的:
1 | a=0x67, 0xE6, 0x09, 0x6A, |
注意,实际顺序是小端,即上面的倒序;
轮加密:
轮加密中,我们会用到一个重要的巨大常量,即K表,由上代码中data_round调用:
看到这样的常量标志,就非常具有意义了,其值如下:
1 | unsigned int const K[64] ={ |
此外:
处理函数sha_transform应由嵌套循环组成,具体取决于书写者使用的数据结构。
轮函数data_round应该由循环次数为16 、64 、64的三个循环的主体构成。