请问你今天要来杯茶吗?🍵
1. TEA 加密简介
TEA(Tiny Encryption Algorithm)由剑桥计算机实验室的 Roger Needham 和 David Wheeler 于 1994 年发明。它是一种分组加密算法,速度快,实现简单,占用资源少,常见于嵌入式系统等。一个具体应用是 QQ 和微信的一部分加密协议。
TEA 使用 64 bits (8 bytes) 的明文分组和 128 bits (16 bytes) 的密钥进行加密。采用 Feistel 分组密码结构和迭代形式,将明文分为两部分,每一轮迭代后 “交换” 位置再执行运算。该算法推荐的迭代次数为 64 轮,最少不少于 32 轮。下图示意 TEA 的两轮迭代(或计为一个循环 (Cycle)),其中密钥被分为四个子密钥 :
(来源:Tiny Encryption Algorithm - Wikipedia)
阅读说明:(绿色)田字格表示 “加法”,(红色)田字圆表示异或。
TEA 引入了一个神秘常数 ,每轮迭代使用 的不同倍数以保证每一轮加密都不相同。这个神秘常数的推荐数值为 ,以十六进制表示为 0x9E3779B9
。这个常数的值可以替换,但采用推荐值可以避免一些算法问题。另外,这个常数在迭代过程中不能更改。
TEA 存在密钥表攻击的问题。为了解决算法的缺陷,设计者提出了更先进的算法:从 XTEA 和 Block TEA,到 XXTEA。
-
XTEA:使用与 TEA 相近的运算,但四个子密钥采取不太正规的方式进行混合,密钥的移位、异或与加法运算也经过重新组合。
(来源:XTEA - Wikipedia)
-
Block TEA:和 XTEA 同时提出,可以对 32 位的任意整数倍长度的明文块进行加解密操作。该算法将 XTEA 中的轮循函数依次应用于块中的每个字 (32 bits),并且将它与最左侧的邻字进行加法组合。
-
XXTEA:是 Block TEA 的改良版(所以它的英文全称为 “Corrected Block TEA”),换用具有两个输入量的 MX 函数作为轮循函数,在处理块中的每个字时利用了相邻的两个字。
(来源:XXTEA - Wikipedia)
2. TEA 系列算法加密流程分析
强调一下变量的类型一定要是无符号的,如果是有符号的话数字的大小是不正确的(会考虑符号位),所以一定要无符号才能跑的出结果
——[原创] 逆向魔改 tea + 简单异或 + z3 运算
2.1 TEA
以下是 David Wheeler 和 Roger Needham 公开发布的 TEA 加密和解密的 C 语言参考程序的改编:
1 |
|
(来源:Tiny Encryption Algorithm - Wikipedia)
- 在加密函数
encrypt
中,- 数组
v
是明文,分为v[0]
和v[1]
两部分。数组k
是密钥,分为k[0]
~k[3]
四个子密钥。sum
对应delta
的不同倍数。 - 整数类型使用
uint32_t
,即无符号的 32 位整数。由于范围限制,sum
会遇到整数溢出的情况,程序截断数值后会改变sum
的值,对sum
进行混淆。 - 接下来是 32 次循环。
- 循环结束,将密文写回数组
v
。
- 数组
- 在解密函数
decrypt
中,- 数组
v
是密文,分为v[0]
和v[1]
两部分。数组k
是密钥,分为k[0]
~k[3]
四个子密钥。 - 解密是加密的逆操作。此时要从最后一次迭代开始逆向迭代,逐步获取明文。这里
sum
的初始值由(delta << 5) & 0xFFFFFFFF
确定,计算得到0xC6EF3720
。(也可以直接用delta * 32
得到)
- 数组
2.2 XTEA
此 C 语言源代码,改编自 David Wheeler 和 Roger Needham 发布到公共领域的参考代码,使用 XTEA 进行加密和解密:
1 |
|
(来源:XTEA - Wikipedia)
2.3 XXTEA
以下 C 语言源代码根据 David Wheeler 和 Roger Needham 发布的原始源码改进而来,使用 XXTEA 进行加密和解密:
1 |
|
(来源:XXTEA - Wikipedia)
PS:也不知道是哪个神仙发明的算法,自学起来如此晦涩难懂。
——Tea 系列算法学习 - St4rdd3n’s blog
😂😂😂
TEA 算法及其背后的结构原理是很经典的,因此你能看到一些千奇百怪的加密算法都有 TEA 系列的影子。进阶一点的 CTF 比赛有很多就是拿 TEA 算法魔改的。所以…… 加油吧💪
3. TEA 加密特征识别
-
可能存在针对 64 bits 以及 128 bits 数字的操作(分别是输入的
message
和key
),一般会以uint32_t
的数组表示 -
存在先移位、后异或的类似操作。比如
((z >> 5 ^ x <<4) ^ (y << 4 & q >> 5))
等混合变换就是 XXTEA 加密的特征 -
前面一个复杂的混合变换的结果可能会叠加到另一个值上,两者相互叠加(即 Feistel 结构)
-
获取密钥的时候,会使用某一个常量值作为下标(如
key[(sum >> 11) & 3]
),并且出现轮换形式,可以猜测是 XTEA 或 XXTEA。 -
一开始会在算法定义一个
delta
(特别是值为0x9E3779B9
时),并且不断参与算法,但是不会受到输入的影响。 -
常见代码片段:
-
TEA
1
2
3sum += delta;
v0 += ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
v1 += ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3); -
XTEA
1
2
3v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
sum += delta;
v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum >> 11) & 3]); -
XXTEA
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
sum += delta;
e = (sum >> 2) & 3;
``
## 4. TEA 常见魔改方法
1. 更改 `delta` 的值,不再是 `0x9E3779B9`
2. 在每轮的迭代加密中添加可逆运算
3. 在迭代完成后赋值时添加可逆运算
## 5. TEA 逆向方法
基本思路为通过提取关键数据编写脚本进行解密。
如果反编译出来的算法不是那么标准,那么尽力写一个正向的加密算法出来,然后将测试数据借用目标程序进行加密,通过比对结果来检验我们写的算法的正确性。由于解密是加密的逆运算,因此正向的加密解决了,逆向的解密也不成问题。
在解密 TEA 时,我们需要:
- 提取常数 `delta` 的值,一般是一个 4 字节(32 位)大小的整数
- 查看每一轮的迭代加密是否添加了其他可逆运算
- 循环结束赋值回去前对密文有没有进一步加密
TEA 加密最好准备一个解密模板,当遇到 TEA 加密时就可以直接套数据,而不是复现整个解密流程。
## 6. 习题示例
> [西电 CTF 终端 - 练习场 - 强壮逆向人 - ezTea](https://ctf.xidian.edu.cn/training/2?challenge=521)
本题为源码逆向,要求借助加密函数写出与之配套的解密函数。
源码:
```c
void encrypt (uint32_t* v, uint32_t* k) { // 主要加密函数,试着搞定它
uint32_t v0 = v[0], v1 = v[1], sum = 0;
uint32_t delta = 0xd33b470;
for (int i = 0; i < 32; i++) {
sum += delta;
v0 += ((v1<<4) + k[0]) ^ (v1 + sum) ^ ((v1>>5) + k[1]);
v1 += ((v0<<4) + k[2]) ^ (v0 + sum) ^ ((v0>>5) + k[3]);
}
v[0] = v0;
v[1] = v1;
}
int main() {
uint32_t k[4] = {1, 2, 3, 4};
int8_t input[33] = {0};
scanf("%32s", input);
for (int i = 0; i < 32; i+=8) {
uint32_t v[2] = {*(uint32_t *)&input[i], *(uint32_t *)&input[i+4]};
encrypt(v, k);
for (int j = 0; j < 2; j++) { // 这一段主要是把 v 按单字节输出,另外可以了解一下 “大小端序” 在这题是如何体现的
for (int k = 0; k < 4; k++) {
printf("%#x, ", v[j] & 0xff); // x:将无符号整数以十六进制形式输出,字母使用小写;#:表示输出时添加前缀 0x
v[j] >>= 8;
}
}
}
return 0;
}
-
原题所给 tutorial:
你需要做的是看看
main
函数中给encrypt
传入的参数是什么,然后去分析加密的行为,并试着写出解密函数。当你输入正确的 flag 时会输出这样的数据:
1
2
3 0x17, 0x65, 0x54, 0x89, 0xed, 0x65, 0x46, 0x32, 0x3d, 0x58, 0xa9, 0xfd, 0xe2, 0x5e,
0x61, 0x97, 0xe4, 0x60, 0xf1, 0x91, 0x73, 0xe9, 0xe9, 0xa2, 0x59, 0xcb, 0x9a, 0x99,
0xec, 0xb1, 0xe1, 0x7d
脚本:
1 |
|
解密函数完全是加密函数的逆运算,所有的运算顺序都倒置了。
先用 DIE 看看

64 位,无壳
然后用 IDA,直接跳到 main
函数处
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
我们分析一下这个程序
-
sub_140001120
函数后面出现了很明显的格式化字符串,应该是scanf
。 -
aPleaseInput
点进去对应字符串Please Input:
。运行附件程序,发现这个是欢迎语,因此sub_140001070
函数应为printf
。 -
byte_140004700
是用户输入。dword_140004078
点进去发现其数值为 32。结合for
语句的条件i += 8
,可以知道这里将明文分成四等份(每份 8 字节)来处理。 -
unk_140004080
点进去发现疑似为一个数组,用 LazyIDA 提取后发现其长度为 32 -
重头戏是函数
sub_140001180
。点进去:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22__int64 __fastcall sub_140001180(unsigned int *a1, _DWORD *a2)
{
__int64 result; // rax
unsigned int v3; // [rsp+0h] [rbp-38h]
unsigned int v4; // [rsp+4h] [rbp-34h]
int v5; // [rsp+8h] [rbp-30h]
unsigned int i; // [rsp+Ch] [rbp-2Ch]
v3 = *a1;
v4 = a1[1];
v5 = 0;
for ( i = 0; i < 32; ++i )
{
v5 -= 0x61C88647;
v3 += (a2[1] + (v4 >> 5)) ^ (v5 + v4) ^ (*a2 + 16 * v4);
v4 += (a2[3] + (v3 >> 5)) ^ (v5 + v3) ^ (a2[2] + 16 * v3);
}
*a1 = v3;
result = 4i64;
a1[1] = v4;
return result;
}发现先移位、后异或的操作,并且使用 32 次循环。
这里的
delta
似乎是0x61C88647
,但这里是递减。通过计算,发现v5 -= 0x61C88647
和v5 += 0x9E3779B9
等价,所以真正的delta
仍然是0x9E3779B9
。推理:
0x61C88647 + 0x9E3779B9 = 0xFFFFFFFF + 1 = 0x100000000
由于
delta
是一个uint32_t
型常量,因此计算结果被截断,实际存储为0
。则
0x9E3779B9 = -0x61C88647
则
v5 = v5 - 0x61C88647
等价于v5 = v5 + 0x9E3779B9
这里用到的
a2
数组应该为密钥(因为被分成四份了),对应到main
函数里是aWelcometonewst
,也就是WelcomeToNewStar
(16 字节)。这里还需要将字符串转成十六字节来让脚本处理。可以初步确定是 TEA 算法, 因为在对明文处理时没有在下标计算时使用
v5
。
如果使用 IDA 的 Findcrypt 插件,可以直接 一眼丁真,鉴定为:
TEA_DELTA_1400011C3
接下来着手写解密脚本(官方 WriteUp)
1 |
|
得到
1 | flag{There_R_TEA_XTEA_and_XXTEA} |