最近半年打了很多比赛,也见了很多的逆向方向的题目。对于解决对比密文类的逆向题,往往绕不开的一个问题就是对于加密算法的识别。
下面就分类对于各种加密的特征以及识别方法进行总结。
作者:Sink
流密码
常见的有 RC4
、Salsa20
以及 ChaCha20
。但是识别出是指定加密是何种流密码其实并不是非常的重要,一般情况下只要识别出来是流密码就可以了,特征就是密文仅由明文与密钥流经过异或运算得到。只要识别出流密码,我们就可以选择动态调试获取密钥流或者直接把目标密文 patch 进去拿输出就可以了。
RC4
1 |
|
RC4 实现非常简单,特征也非常明显:
长度为 256 的 S 盒,且在每一步生成与加密过程中都伴随着 S 盒的 swap
Salsa20
Salsa20算法通过将 32 字节(或者 16 字节)的密钥 和 **8 字节的iv **扩展为伪随机密钥字节流,通过伪随机密钥字节流和异或操作实现加解密。
伪随机密钥字节流的生成
伪随机密钥字节流的生成其实是使用 密钥、iv、以及一些常量构成 64 字节数据,输入到核心函数中得到 64 字节的输出。
Salsa20 密钥拓展规则如下:
1 | key 为 32 字节时 |
** 核心函数实现: **
1 |
|
后面接着就是 xor 了,看了前面的实现代码,我相信这种加密的识别也并不困难
首先,构造核心函数输入时的参数,是最容易识别的。(当然也是最容易魔改的)
其次,核心函数中的标志性循环左移,以及每一位对应的位移数,不要看着复杂,其实就是 7、9、13、18,可以看到上面的代码中我把每4个分成了一组,因为在实现的时候有时候会把每四个作为一组来处理。
1 |
|
最后提醒下,Salsa20 核心函数中的 20 轮也是可以魔改的。
ChaCha20
ChaCha20 加密基本上和 Salsa20差不多,作者也是同一个人。ChaCha20 的 密钥为 32 字节,iv 为 12 字节, 计数器为 4 字节
伪随机密钥字节流的生成
ChaCha20 密钥拓展规则如下:
1 | c[0:4] + key[0:32] + 计数器(4 bytes) + iv |
核心函数的实现:
1 | static inline void u32t8le(uint32_t v, uint8_t p[4]) { |
和 Salsa20 整体来说基本一致,识别方法也基本一样,对于二者的区别主要是:
密钥拓展规则的不同
quarterround 也不同,其中做容易看出来的就是 ChaCha20 的移位为 16 12 8 7,其他的就要看具体的运算逻辑了
比如最近 RCTF2022 中的checkserver 中的加密,
单看密钥 64 字节,没有iv,也没有常量,看起来好像不是 ChaCha20,但是看后面,标志性的循环左移16 12 8 7,我们就可以很容易的识别出来,这是一个去除了密钥拓展的 ChaCha20.
分组密码
TEA
微型加密算法(Tiny Encryption Algorithm,TEA)是一种易于描述和执行的分组密码,密钥长度为128位,分组长度为 64 位,基于 Feistel 结构,流程图如下:
下面是 TEA 的实现
1 |
|
可以看出,TEA 的实现确实非常简单,对于TEA的识别,特征常量 delta 0x9e3779b9
是一个非常重要的特征,但是delta在赛题中往往会被魔改为其他数值,这种情况下就需要通过对 Feistel 结构的识别和移位操作的识别来识别了。
XTEA
1 |
|
和 TEA 很像,二者区别最简单的方法看 sum += delta
的位置
XXTEA
1 |
|
一般来说,识别可以通过,delta 以及 round = 6 + 52/n
、(sum >> 2) & 3
这种特殊的运算来判断。
DES
详细的实现介绍可以看之前组会分享的 PPT
CTF DES加密 .pdf
主要通过 S盒 以及各个置乱表来识别,可以使用插件来自动化识别这些特征。
AES
AES(Advanced Encryption Standard,高级加密标准),分组大小是 128 位,根据密钥长度和轮数可以分为 AES-128、AES-192、AES-256,具体区别如下表:
AES-128 | AES-192 | AES-256 | |
---|---|---|---|
密钥长度(bit) | 128 | 192 | 256 |
轮数 | 10 | 12 | 14 |
整体流程
整体来说AES加密有如下几步
密钥拓展,使用密钥拓展算法通过初始密钥获取轮密钥
初始轮密钥加
前 9 (或 11,或 13 ) 轮
- S 盒代换(SubBytes )
- 行移位 (ShiftRows )
- 列混合 (MixColumns )
- 轮密钥加(AddRoundKey)
最后一轮
SubBytes—通过一个非线性的替换函数,用查找表的方式把每个字节替换成对应的字节。
- ShiftRows—将矩阵中的每行进行循环移位。
- 第 1 行不变,第 2 行循环左移 1, 第 3 行 循环左移 2, 第 4 行循环左移 3
- MixColumns—使用可逆线性转换将状态每一列的四个字节混合在一起,输入输出都是 4 个字节(也就是1列)。
- 变换公式如下:
各个值在相加时使用的是模2加法(异或运算)
- 更一般的,可以认为是把每列看作 上的多项式,让其与一个固定的多项式相乘,然后模 ,通过运算可以证明此运算与上述矩阵运算等价
- AddRoundKey—矩阵中的每一个字节都与该轮的轮密钥(round key)做xor运算;每个子密钥由密钥生成方案产生。
轮密钥生成算法
每个轮密钥是 128 bit,
1 | uint8_t R[] = {0x02, 0x00, 0x00, 0x00}; |
Rcon 也可以用以下的表格来实现
优化算法
类似前面介绍的DES加密的SP盒,AES也有着类似的优化算法,除了轮密钥加,以下三个步骤
- S 盒代换(SubBytes )
- 行移位 (ShiftRows )
- 列混合 (MixColumns )
可以合并为一步表格置换叫做 Te 表
白盒 AES
只见过一次,2022年国赛分区赛逆向有个,解法可以参考下面的文章:
https://bbs.pediy.com/thread-254042.htm
SM4
SM4是国密算法,由国家密码局发布,分组长度为128比特,密钥长度为128比特。
这个加密在22年的比赛中还没遇到过,但是还是简单介绍下
轮函数
密钥拓展算法
T’ 和合成置换T基本类似,只是换了线性置换L为L’
1 | static const uint32_t FK[4] = {0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dc}; |
识别方法
由上面的介绍,可以看出,SM4也有着 S盒、FK、CK几个常量表,所以使用插件也可以自动化识别。
非对称密码
RSA
产生公钥和私钥:
- 随机选择两个大素数 和 ,且 ,计算
- 求得
- 选择一个小于的整数,使与互质。求,令
(e, N) 是公钥,(d,N) 是私钥
加密
解密
识别
对于RSA
的识别,关键是对于大数运算库函数的识别,常见的大数运算库有:GMP
、Miracl
或者一些密码学的库 OpenSSL
、Crypto++
、libtomcrypt(用的GMP)
也有着对应的实现。
单向散列函数
MD5
MD5消息摘要算法(MD5,Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16 bytes)的散列值。
下面是C语言具体实现:
1 | // Constants are the integer part of the sines of integers (in radians) * 2^32. |
识别
首先常数 0x67452301
, 0xefcdab89
, 0x98badcfe
, 0x10325476
,是识别MD5的一大关键,其次就是 k 和 r 两个表(这俩不一定会写成数组的格式,也有可能硬编码到代码里),一般来说MD5是可以通过插件识别的。
SHA
安全散列算法(Secure Hash Algorithm,缩写为SHA)是一个密码散列函数家族,是FIPS所认证的安全散列算法。
比较常见的有 SHA-1, SHA2-256,SHA2-512,下面是他们的特征:
1 | // SHA-1 的初始散列值 |
太多啦,实现细节就不展开了,SHA2中 224和256实现相似,384和512实现相似,但是这四个的初始哈希值都是不一样的,只不过较短的都是截断了最后的 h7
识别
与 md5 类似(也是有表的只不过没列出来),就不再赘述了。
BaseXX 系列
Base64
Base64是网络上最常见的用于传输字节码的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据的方法。它的工作方式可以简单的用wiki上的三张图来描述:
- 待加密数据字节数是 3 的整数倍,3 * 8 = 4 * 6,即最完美的情况,不需要Padding
- len % 3 = 2,2 * 8 + 2 = 3 * 6, 即需要补 2 个 0,此时后面要添加 1 个等号作为标志
len % 3 = 1,1 * 8 + 4 = 2 * 6, 即需要补 4 个 0,此时后面要添加 2 个等号作为标志
常见实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43unsigned int base64_encode(const unsigned char *in, unsigned int inlen,
char *out) {
int s;
unsigned int i;
unsigned int j;
unsigned char c;
unsigned char l;
s = 0;
l = 0;
for (i = j = 0; i < inlen; i++) {
c = in[i];
switch (s) {
case 0:
s = 1;
out[j++] = base64en[(c >> 2) & 0x3F];
break;
case 1:
s = 2;
out[j++] = base64en[((l & 0x3) << 4) | ((c >> 4) & 0xF)];
break;
case 2:
s = 0;
out[j++] = base64en[((l & 0xF) << 2) | ((c >> 6) & 0x3)];
out[j++] = base64en[c & 0x3F];
break;
}
l = c;
}
switch (s) {
case 1:
out[j++] = base64en[(l & 0x3) << 4];
out[j++] = BASE64_PAD;
out[j++] = BASE64_PAD;
break;
case 2:
out[j++] = base64en[(l & 0xF) << 2];
out[j++] = BASE64_PAD;
break;
}
out[j] = 0;
return j;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73char *b64_encode(const unsigned char *src, size_t len) {
int i = 0;
int j = 0;
char *enc = NULL;
size_t size = 0;
unsigned char buf[4];
unsigned char tmp[3];
// alloc
enc = (char *)b64_buf_malloc();
if (NULL == enc) {
return NULL;
}
// parse until end of source
while (len--) {
// read up to 3 bytes at a time into `tmp'
tmp[i++] = *(src++);
// if 3 bytes read then encode into `buf'
if (3 == i) {
buf[0] = (tmp[0] & 0xfc) >> 2;
buf[1] = ((tmp[0] & 0x03) << 4) + ((tmp[1] & 0xf0) >> 4);
buf[2] = ((tmp[1] & 0x0f) << 2) + ((tmp[2] & 0xc0) >> 6);
buf[3] = tmp[2] & 0x3f;
// allocate 4 new byts for `enc` and
// then translate each encoded buffer
// part by index from the base 64 index table
// into `enc' unsigned char array
enc = (char *)b64_buf_realloc(enc, size + 4);
for (i = 0; i < 4; ++i) {
enc[size++] = b64_table[buf[i]];
}
// reset index
i = 0;
}
}
// remainder
if (i > 0) {
// fill `tmp' with `\0' at most 3 times
for (j = i; j < 3; ++j) {
tmp[j] = '\0';
}
// perform same codec as above
buf[0] = (tmp[0] & 0xfc) >> 2;
buf[1] = ((tmp[0] & 0x03) << 4) + ((tmp[1] & 0xf0) >> 4);
buf[2] = ((tmp[1] & 0x0f) << 2) + ((tmp[2] & 0xc0) >> 6);
buf[3] = tmp[2] & 0x3f;
// perform same write to `enc` with new allocation
for (j = 0; (j < i + 1); ++j) {
enc = (char *)b64_buf_realloc(enc, size + 1);
enc[size++] = b64_table[buf[j]];
}
// while there is still a remainder
// append `=' to `enc'
while ((i++ < 3)) {
enc = (char *)b64_buf_realloc(enc, size + 1);
enc[size++] = '=';
}
}
// Make sure we have enough space to add '\0' character at end.
enc = (char *)b64_buf_realloc(enc, size + 1);
enc[size] = '\0';
return enc;
}识别
有
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
码表(或者类似的长度是64就行)基本上实现就是上面两种形式,对照一下就行
比如下面这个,显然就是上面的第一种形式
Base32
就是把 Base64 的 3 * 8 = 4 * 6 改成 5 * 8 = 8 * 5 而已,不再展开。
Base58
1 | static const char b58digits_ordered[] = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; |
关键特征为长度为 58 的码表 ,和 mod 58 、 除以 58 操作,以及奇怪的 * 138 / 100 + 1
比如下面这个
Base24
Base24实现非常简单,简单到没必要了解,因为只需要看一下加密,就能写出来解密。
但是还是给一下实现:
1 | const char base24code[] = { |