参考文章:

RC5

RC5 分组密钥算法 C语言实现

背景与历史

RC5是Ron Rivest开发的对称密钥块加密算法。RC5的主要特性是很快,只是用基本的计算机运算(加、异或、移位等),轮数可变,密钥位数可变,从而大大增加灵活性。需要不同安全性的应用程序可以相应设置这些值。RC5的另一个重要特性是执行所需的内存更少。

RC5工作的基本原理

RC5的字长(即输入明文块的长度)、轮数和密钥8位字节数都是可变的。一旦确认之后,这些值在执行特定的加密算法时保持不变。这些变量在执行特定的RC5实例之前是可变的,可以从取值范围中选择。而DES的块长只能是64位,密钥长度只能是56位;IDEA的块长只能是64位,密钥长度只能是128位。下表列出了RC5一些参数的取值范围:

(1)明文块长可以是32、64或者128位(使用2个块)

(2)密钥长度为0~2040位(指定了8位密钥的取值)

RC5的输出是密文,与输入明文具有相同的参数。由于RC5算法的三个参数可以改变数值,因此RC5算法的特定实例记作RC5-w/r/b,其中w为字长(位),r为轮数,b为密钥的8位字节数。这样,RC5-32/16/16表示为RC5的块长为64位(注:RC5使用两个字块)、16轮加密和16字节密钥。Ron Rivest推荐的最低安全版本为RC5-32/16/16。

RC5 算法流程概述

  • 分组大小:两个 w-bit(32-bit)字 = 一共64位
  • 输入:两个32-bit字 (A,B)
  • 密钥扩展:生成 2r+2 个32-bit子密钥
  • 加密轮数:r轮

每一轮:

  1. A = ((A ⊕ B) <<< B) + S[2i]
  2. B = ((B ⊕ A) <<< A) + S[2i+1]

解密就是逆过程。

操作原理(这边以RC5-32/12/16为例)

由于使用上述记法,RC5初看起来比较复杂。首先介绍RC5的工作原理,为了便于理解RC5算法,假设输入明文块的长度为64位,其他块长的操作原理是一样的。

在一次性初始操作中,输入明文块分成两个32位块A和B,前两个子密钥(稍后会介绍如何生成)S[0]和S[1]分别加进A和B,分别产生C和D,表示一次性操作结束。

然后开始各轮,每一轮完成系列操作:

(1)位异或运算;

(2)循环左移;

(3)对C和D增加下一个子密钥,先是加法运算,然后将结果用2的w次方求模(由于这里w=32,因此为2的32次方)。

RC5 的工作原理,如下图所示:

从上图可以看出,初始操作有两步,然后是几轮操作,轮数可以取0~255。也可以从中看出,一个块的输出是另一个块的输入,是整个逻辑很难破译。

加密代码如下:

1
2
3
//循环位移操作
#define ROTL(x, y) (((x) << ((y) & (w - 1))) | ((x) >> (w - ((y) & (w - 1)))))
#define ROTR(x, y) (((x) >> ((y) & (w - 1))) | ((x) << (w - ((y) & (w - 1)))))
1
2
3
4
5
6
7
8
uint32_t A = in[0] + S[0];
uint32_t B = in[1] + S[1];
for (int i = 1; i <= r; i++) {
A = ROTL(A ^ B, B) + S[2 * i];
B = ROTL(B ^ A, A) + S[2 * i + 1];
}
//in[2]是明文被分为2个块
//ROTL是循环位移

扩展密钥

子密钥生成

这一步使用两个常量P和Q。生成的子密钥数组称为S,第一个子密钥为S[0]用P值初始化。

每个后续子密钥(S[1],S[2],…)根据前面的子密钥和常量Q求出,用2的32次方

求模。这个过程要进行2(r+1)-1次,其中r位轮数。下图显示了子密钥生成的数学形式:

这个过程把主密钥 K 扩展为 S[0..25] 子密钥。

分步讲解:

1
void generateSubkeys(uint8_t* K, uint32_t* S)

将字节数组 K 转换成 4个32位整数 L[0..3]

1
2
3
4
5
6
  uint32_t L[c] = { 0 };

// 将密钥转换为小端序的32位字
for (int i = 0; i < b; i++) {
L[i / 4] |= ((uint32_t)K[i]) << (8 * (i % 4));
}

示意:

1
2
K[0..15] --> L[0..3]
4字节组合成一个32位整数

例如:

1
2
K[0]=0xAA K[1]=0xBB K[2]=0xCC K[3]=0xDD
L[0]=0xDDCCBBAA

初始化子密钥数组 S[0..25]

1
2
3
4
S[0] = 0xB7E15163;
for (int i = 1; i < t; i++) {
S[i] = S[i-1] + 0x9E3779B9;
}

这里用两个“幻数”:

  • PW = 0xB7E15163 (2^w * (e - 2))
  • QW = 0x9E3779B9 (2^w * (Golden Ratio - 1)) 这是RC5官方定义。

这一部分按照原理来说的话应该是(S[i-1] + 0x9E3779B9)% (1<<32)

(% (1<<32)就是取2的32次方的mod)

但这个代码里面没有,为什么呢?

因为我用的是uint32_t类型

  • 在 C 语言里 uint32_t 类型是无符号32位整数

  • 当超过 2^32 时,会自动溢出并回绕到模 2^32 的余数

子密钥混合

子密钥混合阶段将子密钥S[],S[],…与原密钥L[],L[],…混合。其数学形式如下图所示:

如上图所示,首先初始化i,j,A和B;其次将子密钥与A和B相加,并左移3位,赋给A和该子密钥,将原密钥与A和B相加,并左移A加B位,赋给B和该原密钥;最后,将i加1,并用2(r+1)求模,将j加1,并用c求模,循环3×n次结束。

注:c是原密钥中最后一个子密钥部分,<<<表示循环左移。

代码实现

1
2
3
4
5
6
7
8
uint32_t A = 0, B = 0;
int i = 0, j = 0;
for (int k = 0; k < 3 * t; k++) {
A = S[i] = ROTL(S[i] + A + B, 3);
B = L[j] = ROTL(L[j] + A + B, A + B);
i = (i + 1) % t;
j = (j + 1) % c;
}

整体加解密代码实现(以RC5-32/12/16为例)

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#include <stdio.h>
#include <stdint.h>
#include <math.h>


#define w 32 // 字长
#define r 12 // 加密轮数
#define b 16 // 密钥长度
#define t (2 * r + 2) // 子密钥数
#define c (b * 8 / w) // 主密钥分租数

// 循环位移
#define ROTL(x, y) (((x) << ((y) & (w - 1))) | ((x) >> (w - ((y) & (w - 1)))))
#define ROTR(x, y) (((x) >> ((y) & (w - 1))) | ((x) << (w - ((y) & (w - 1)))))

// 生成一个 16 字节 (128 位) 的密钥数组 K
//void InitialKey(uint8_t* K)
//{
// K[0] = 3;
// for (int j = 1; j < b; j++) {
// K[j] = (uint8_t)((int)pow(3, j) % (255 - j));
// }
//}

// 密钥扩展
void generateSubkeys(uint8_t* K, uint32_t* S)
{
uint32_t L[c] = { 0 };

// 将密钥转换为大端序的32位字
for (int i = 0; i < b; i++) {
L[i / 4] |= ((uint32_t)K[i]) << (8 * (3 - (i % 4)));
}

// 子密钥生成
S[0] = 0xB7E15163;//常量Q,0x9E3779B9就是常量p
for (int i = 1; i < t; i++) {
S[i] = S[i - 1] + 0x9E3779B9;
}

// 主,子密钥混合
uint32_t A = 0, B = 0;
int i = 0, j = 0;
for (int k = 0; k < 3 * t; k++) {
A = S[i] = ROTL(S[i] + A + B, 3);
B = L[j] = ROTL(L[j] + A + B, A + B);
i = (i + 1) % t;
j = (j + 1) % c;
}
}

// 加密函数
void Encrypt(uint32_t* in, uint32_t* out, uint32_t* S)
{
uint32_t A = in[0] + S[0];
uint32_t B = in[1] + S[1];
for (int i = 1; i <= r; i++) {
A = ROTL(A ^ B, B) + S[2 * i];
B = ROTL(B ^ A, A) + S[2 * i + 1];
}
out[0] = A;
out[1] = B;
}

// 解密函数
void Decrypt(uint32_t* in, uint32_t* out, uint32_t* S)
{
uint32_t A = in[0];
uint32_t B = in[1];
for (int i = r; i >= 1; i--) {
B = ROTR(B - S[2 * i + 1], A) ^ A;
A = ROTR(A - S[2 * i], B) ^ B;
}
out[0] = A - S[0];
out[1] = B - S[1];
}

int main()
{

uint8_t K[b] = {1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2};
uint32_t S[t];
uint8_t plain[] = "yyyspark";
uint32_t ciphertext[2];
uint32_t decrypted[2];
uint32_t plaintext[2];

// 将uint8_t类型明文转换为小端序的32位字,vs2022系统是采用小端序架构,这里统一处理字节序转换
for (int i = 0; i < 2; i++) {
plaintext[i] =
(plain[4 * i + 3] << 24) |
(plain[4 * i + 2] << 16) |
(plain[4 * i + 1] << 8) |
plain[4 * i];
}

generateSubkeys(K, S);

printf("Plaintext: %08X %08X\n", plaintext[0], plaintext[1]);

// 加密
Encrypt(plaintext, ciphertext, S);
printf("Ciphertext: %08X %08X\n", ciphertext[0], ciphertext[1]);

// 解密
Decrypt(ciphertext, decrypted, S);
printf("Decrypted: %08X %08X\n", decrypted[0], decrypted[1]);
printf("%s", plaintext);
return 0;
}

数据输入转化为小端序

1
2
3
4
5
6
7
for (int i = 0; i < 2; i++) {
plaintext[i] =
(plain[4 * i + 3] << 24) |
(plain[4 * i + 2] << 16) |
(plain[4 * i + 1] << 8) |
plain[4 * i];
}

为什么要转化为小端序呢?大端序不行嘛?

vs2022系统是采用小端序架构,这里统一处理字节序转换,不然字符串输出是按照内存里面的小端序输出

RC5部分魔改方式

魔改方式一:写死 S[] 常量

绕过 密钥扩展,直接在代码里写死 S[] 的值。

示例代码(以RC5-32/12/16为例):

1
2
3
4
5
6
7
8
9
10
uint32_t S[T] = {
0xB7E15163, 0x5618CB1C, 0xF45044D5, 0x9287BE8E,
0x30BF3857, 0xCEF6B210, 0x6D2E2BC9, 0x0B65A582,
0xA99D1F3B, 0x47D498F4, 0xE60C12AD, 0x84338C66,
0x226B061F, 0xC0A27FD8, 0x5EDA0991, 0xFD11934A,
0x9B48ED03, 0x398066BC, 0xD7B7E075, 0x75EF5A2E,
0x1436D3E7, 0xB26E4DA0, 0x50A5C759, 0xEEDD4102,
0x8D14BAAB, 0x2B4C3464
};

  • 这些值是某一密钥对应的扩展值,但看不到 key。

  • 但是解密者依旧可以直接利用S盒直接进行解密,建议将S盒藏在栈或者其他地方

魔改方式二:使用伪随机数生成 S[]

用 PRNG 伪随机生成 S[],种子也可以隐藏在别处。

示例代码:

1
2
3
4
5
6
7
void generate_s_with_prng(uint32_t seed) {
for (int i = 0; i < T; ++i) {
seed = seed * 1103515245 + 12345;
S[i] = seed ^ 0xDEADBEEF;
}
}

其中

1
seed = seed * 1103515245 + 12345;

这一整行其实是一个经典的伪随机数算法:

Linear Congruential Generator(线性同余生成器,LCG)

其形式为:

1
seed = (a * seed + c) % m
  • S[] 看起来是“乱数”,但如果你知道 seed 和算法,就能复现。

  • 但逆向时要花大量时间分析 PRNG 逻辑,可能隐藏在多个函数中。

  • 有时 PRNG 逻辑是经过混淆的,如乘数/模数换成了奇怪的位运算。

魔改方式三:key 藏在数据中运行时提取

将 key(或 S[])不直接写在代码中,而是藏在数据、输入或解密 blob 中。

示例代码:

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
uint32_t dynamic_key(uint8_t* key, int round, int offset) {
int key_len = b;
uint32_t val = 0;

for (int i = 0; i < 4; i++) {
int index = (round * 2 + offset + i) % key_len;
val |= (key[index] << (i * 8));
}
return val;
}

void Encrypt(uint32_t* in, uint32_t* out, uint8_t* Key)
{
uint32_t A = in[0];//没有进行相加
uint32_t B = in[1];
for (int i = 1; i <= r; i++)
{
uint32_t K1 = dynamic_key(Key, i, 0);
uint32_t K2 = dynamic_key(Key, i, 1);
A = ROTL(A ^ B, B) + K1;
B = ROTL(B ^ A, A) + K2;
}
out[0] = A;
out[1] = B;
}
  • 你需要在调试器里看到 data 具体内容,才能提取 key。
  • 有时 key 会被 RC4 或 Base64 混淆,还要先解密才行。

魔改方式四:固定的 RC5 解密逻辑,无 S盒 和 key

将 RC5 的轮函数结果写死,作为“定值函数”。

1
2
3
4
5
6
7
8
9
10
11
void fixed_rc5_decrypt(uint32_t *in, uint32_t *out) {
uint32_t A = pt[0] + 0xB7E15163;
uint32_t B = pt[1] + 0x5618CB1C;

A = ((A ^ B) << (B & 0x1F)) + 0xF45044D5;
B = ((B ^ A) << (A & 0x1F)) + 0x9287BE8E;

out[0] = A;
out[1] = B;
}

  • 相当于只保留了部分轮数,或者所有轮都展开成常量。

  • 没有 key 也没有 S[],你只能通过解密路径还原明文。