西电CTF 2026 请你喝茶 wp
发布于 2026-10-19 02:20:13
die分析完后给IDA
int __fastcall main(int argc, const char **argv, const char **envp)
{
__int64 v4; // rdx
__int64 v5; // rcx
__int64 v6; // r8
__int64 v7; // r9
char s[8]; // [rsp+0h] [rbp-40h] BYREF
__int64 v9; // [rsp+8h] [rbp-38h]
__int64 v10; // [rsp+10h] [rbp-30h]
__int64 v11; // [rsp+18h] [rbp-28h]
int v12; // [rsp+20h] [rbp-20h]
int v14; // [rsp+2Ch] [rbp-14h]
char *v15; // [rsp+30h] [rbp-10h]
size_t v16; // [rsp+38h] [rbp-8h]
*(_QWORD *)s = 0;
v9 = 0;
v10 = 0;
v11 = 0;
LOWORD(v12) = 0;
fwrite("Input your flag: ", 1u, 0x11u, stdout);
if ( fgets(s, 34, stdin) )
{
v16 = strcspn(s, "\r\n");
s[v16] = 0;
if ( v16 == 32 && (v15 = s, (v14 = ((__int64 (__fastcall *)(char *))check_tea_part)(s)) != 0) )
{
if ( (unsigned int)check_xtea_part(v15 + 16, "\r\n", v4, v5, v6, v7, *(_QWORD *)s, v9, v10, v11, v12) )
puts("Correct!");
else
puts("Wrong!");
return 0;
}
else
{
puts("Wrong!");
return 0;
}
}
else
{
puts("Input error.");
return 1;
}
}
# 拿到加密代码,怎么写出解密代码
> 一套可迁移的方法,TEA / XTEA / XXTEA / ChaCha 这类 ARX 密码通吃。
> 配套可运行实验:同目录 `lab.c`(`gcc -O0 -o lab lab.c && ./lab`)。
---
## 0. 先记住一句话
> **解密 = 把加密的每一步「倒着放」,每一步用它的逆运算。**
就这一句。剩下全是怎么用。数学上就是 `(f∘g)⁻¹ = g⁻¹ ∘ f⁻¹`。
---
## 1. 为什么必须"倒着放"
加密做的事:
明文 –步骤1–> –步骤2–> … –步骤n–> 密文
状态一路被改写,每一步都依赖上一步的结果。要回到明文,只能从最后一步开始撤销:先撤步骤 n,再撤步骤 n-1……
就像穿衣服:先穿袜子再穿鞋,脱的时候必须**先脱鞋再脱袜子**。
**顺序反过来是本质,不是技巧。**
---
## 2. 逆运算速查表(背下这张表,大部分题就能做)
| 加密里的写法 | 解密里的写法 | 备注 |
|---|---|---|
| `x += e;` | `x -= e;` | 模 2³² 加法,减 = 加补数 |
| `x -= e;` | `x += e;` | |
| `x ^= e;` | `x ^= e;` | **XOR 自逆,原样写** |
| `x = e;` | 不能直接逆 | 覆盖赋值丢信息,**必须事先备份旧值** |
| `swap(a, b);` | `swap(a, b);` | 自逆 |
| `x = rotl(x, k);` | `x = rotr(x, k);` | 循环移位可逆 |
| `x <<= k;` | `x >>= k;`(**仅当高位没丢**) | 普通移位丢位,一般不可逆 |
| `x = T[x];` | `x = Tinv[x];` | 查表 |
三个要点:
1. 表里的 `e` 在解密时**必须和加密时算出同一个值** —— 这条引出第 3 节的核心判据。
2. **XOR 自逆**,所以它永远不用"翻"。
3. **普通移位丢位**,所以别指望靠反向移位去求逆。
---
## 3. 最重要的一节:什么时候"取逆",什么时候"照抄"
这就是"`+` 变 `-` 了,为什么 `>>` 不变"的根源。
**判据只有一个:看等号左边是不是你正在还原的那个未知量。**
- 左边是**待还原的状态** → **取逆**。
- 整个表达式其实是**由已知量组成的一个数** → **照抄**,一个字符都别改。
拿 TEA 那一行举例:
```c
v1 = v1 + G(v0, sum); /* 加密 */
- 左边
v1是待还原的 → 那个+要变-。 - 右边
G(v0, sum):v0在密文里就有,sum自己能算 → 它是个已知数。你只是要把「加密时加上的那个数」算出来,再减掉。 所以:
v1 = v1 - G(v0, sum); /* 解密:G 一字不改 */
用代数看更清楚:解方程 y = x + f(z)(z 已知)得 x = y - f(z)。
变的只有那个 +;f 从来不是“被求逆”,它只是被重新算了一遍。
由此推出所有细节:
| 位置 | 处理 | 原因 |
|---|---|---|
外层 v0 += … |
取逆 → -= |
它在造值,要撤销 |
内层 (x>>5) + k1 里的 + |
照抄 | 它在参与重算那个已知数 |
内层 <<4 / >>5 |
照抄 | 同上。写成 <<5 就等于算了个错的 G |
k[sum & 3] 的下标 |
照抄 | 下标也是“重算已知量” |
| 语句顺序、轮序 | 倒过来 | 依赖链要反向剥 |
一句话:要撤销的才取逆,要重算的必须照抄。
4. 为什么语句顺序和 sum 方向也要反过来
因为加密中的依赖是有方向的:
v0 先改 → v1 的更新读的是【新】v0
依赖链 v0 → v1。解密只能沿链倒着剥:
v1 -= G(v0, sum); /* v0 已知(密文里就有),先把 v1 还原出来 */
v0 -= F(v1, sum); /* 这里的 v1 必须是【刚还原出的新值】 */
两个必须注意的点:
- 两条语句换序:加密 v0 在前,解密 v1 在前。
- 第二行必须用第一行刚算出的新值,缓存旧值是经典错误(
lab.c第 4 节有反例)。 轮与轮之间同理:加密 1→32 轮,解密 32→1 轮,所以sum要从终值往回走,而不是从 0 开始。
5. ARX 密码的统一套路
TEA / XTEA / XXTEA / ChaCha / Salsa 这类只用三种运算:Add(模加)、Rotate/Shift(移位)、Xor(异或),再加“把密钥混进来”。 套第 3 节的判据,结构永远是这四句话:
外层 v0/v1 的更新 → 取逆(+= 变 -=) ← 翻转
内层混洗函数 F/G → 照抄 ← 不变
密钥下标表达式 → 照抄 ← 不变
轮序、sum 走向 → 倒过来 ← 反向
XTEA 的混洗函数比 TEA 更紧凑,同样原样照抄:
F(x) = ((x << 4) ^ (x >> 5)) + x
6. 为什么这些密码敢这么设计
TEA 更新 v1 时,混洗函数作用在 v0 上,而 v0 是已知的另一个字。
因为“已知”,所以永远不用求 F 的逆 —— 直接算出来减掉就行。
反面对比:如果某一轮写成 v0 = F(v0)(只依赖自己),解密就必须真的把 F 求逆,
要解开 <<4、>>5、^ 的纠缠,而移位还丢位,根本做不到。
ARX 的设计原则:把难逆的混洗函数作用在“已知量”上,再用可逆运算(
+/^)把它混进状态。 所以你以后看到任何
x += f(其他字); x ^= f(其他字);
都能立刻写出解密:
x -= f(其他字); x ^= f(其他字); /* f 原样 */
这是 Feistel 结构的思想;TEA 是它的“模加版本”。
7. TEA / XTEA 逐行推导
TEA
加密(对应 chall6 tea_encrypt @ 0x401205):
sum = 0;
for (i = 0; i < 32; i++) {
sum += D; /* D = 0x9E3779B9 */
v0 += ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
v1 += ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
}
解密(机械照做):轮序倒、v1 在前、外层取逆、内层照抄、sum 反向:
sum = D * 32; /* = 0xC6EF3720 */
for (i = 0; i < 32; i++) {
v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3); /* 先 v1 */
v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1); /* 用新的 v1 */
sum -= D;
}
XTEA
加密(对应 xtea_encrypt @ 0x4012d8):
sum = 0;
for (i = 0; i < 32; i++) {
v0 += (((v1<<4) ^ (v1>>5)) + v1) ^ (sum + k[sum & 3]);
sum += D; /* 注意:v0 算完才更新 sum */
v1 += (((v0<<4) ^ (v0>>5)) + v0) ^ (sum + k[(sum>>11) & 3]);
}
和 TEA 的关键差别:TEA 每轮“先加 sum 再两个都用”;XTEA 是“v0 用旧 sum,加完才更新,v1 用新 sum”。 解密必须尊重这个不对称:
sum = D * 32;
for (i = 0; i < 32; i++) {
v1 -= (((v0<<4) ^ (v0>>5)) + v0) ^ (sum + k[(sum>>11) & 3]); /* 对应"v1 用新 sum" */
sum -= D;
v0 -= (((v1<<4) ^ (v1>>5)) + v1) ^ (sum + k[sum & 3]); /* 对应"v0 用旧 sum" */
}
回到 tea6solve.c 的填空
uint delta = 0xC6EF3720; /* 原来写 0x0 */
for (int i = 1; i <= 32; i++) {
cipher[1] -= (((cipher[0] << 4) ^ (cipher[0] >> 5)) + cipher[0])
^ (key[(delta >> 11) & 3] + delta); /* 第一个 0x0 */
delta += 1640531527; /* 原来写 0x0;≡ delta -= 0x9E3779B9 */
cipher[0] -= (((cipher[1] << 4) ^ (cipher[1] >> 5)) + cipher[1])
^ (key[delta & 3] + delta); /* 两个 0x0 都在这 */
}
密文和密钥(chall6 .rodata,已按小端还原):
uint cipher2[4] = {0x60EC68AB, 0x940251CE, 0xB427534C, 0xDF435416};
uint key2[4] = {0xDEADBEEF, 0xCAFEBABE, 0x11223344, 0x55667788};
最终 flag:moectf{Wh4t_a_n1ce_cup_0f_TEA!!}
8. 三个经典坑(本次全踩过一遍)
坑 1 · sum 起始值
必须从加密结束时的 sum 开始。TEA/XTEA 都是 32 轮、每轮加一次 D,终值 = D*32 = 0xC6EF3720。写成 0 直接全乱。
不确定就跑一遍加密,把 sum 打印出来 —— 这比推理可靠。 坑 2 · 第二行用了旧值
uint old = v1;
v1 -= G(v0, sum);
v0 -= F(old, sum); /* 错!必须用刚算出的新 v1 */
坑 3 · C 的有符号溢出与字面量类型
uint sum = -1640531527 * 32; /* int 溢出,UB:GCC 常给对,但不保证 */
uint sum = (uint)0x9E3779B9 * 32; /* 规范写法;或直接 0xC6EF3720u */
另外 (sum >> 11) & 3 里的 sum 必须是无符号,否则右移是算术移位,下标会算错。模板用 uint 正好避开了。
9. 实战流程(拿到新题照这五步走)
- 抄 —— 把反编译的加密函数逐行抄成能编译的 C,一行都别省,标出状态变量。
- 定 —— 确认轮数、块大小、
sum/delta演化方式、密钥、字节序(x86 小端)。 - 推 —— 机械套用:轮倒序、语句倒序、外层取逆、内层照抄、sum 反向。
- 验 —— 写 round-trip 测试:用抄来的 encrypt 加密任意数据,再用自己的 decrypt 解,必须完全一致。 这一步比肉眼推理可靠得多,第 8 节的三个坑它全能抓出来。
- 对 —— 把解出的明文喂回原始程序验证:
echo -n 'flag' | ./chall6。
10. 练习
- 补完
tea6solve.c的填空,跑出 flag。 - 把
lab.c第 4 节的“反例”改成正确版,确认全部 PASS。 - 思考:XTEA 两半分别用
sum & 3和(sum >> 11) & 3两个不同的位段取密钥,这样做对扩散有什么好处?如果都用sum & 3会怎样? - 进阶:自己写一个 XXTEA 解密。它的结构是
x += f(邻居),套第 3 节判据试。
按以上逻辑以及c文件中的引导最终写成
#include <stdio.h> /**
- @brief 这是一个简短的 TEA 解密指南,依据 IDA 反编译的结果,
-
正确填充代码中的空缺处,运行后就可以获得 flag! - @attention 请先移步 main 函数,阅读此题拆解! */
#define uint unsigned int
/**
-
@brief TEA 解密一个 64 位数据块
-
@param cipher 待解密数据(2×32 bit)
-
@param key 128 位密钥(4×32 bit)
-
@param delta 常量值 / void tea_decrypt(uint cipher[2], uint key[4]) { /*
- @brief 解密是加密的逆过程。如果我们已知最终的密文,
-
并可以通过某种方式恢复上一轮的状态, -
那么一直恢复到最初状态,就可以得到明文。 -
在此例中,加密的逻辑是这样的: -
-> 先将 delta 减去 1640531527(或为加上 0x9E3779B9) -
-> 更新 cipher[0] -
-> 更新 cipher[1] -
-> 重复以上操作,循环 32 次 -
由于更新某一个数据时,另外两个数据和密钥的值都是固定的, -
于是在解密时,我们只需要: -
-> 反向更新 cipher[1] -
-> 反向更新 cipher[0] -
-> 将 delta 加上 1640531527(或为减去 0x9E3779B9) -
-> 重复以上操作,循环 32 次 - key
- @attention 根据 IDA 反编译展示的逻辑,修改下面代码中填 0x0 的部分。 */ uint delta = -1640531527 * 32;
for(int i = 1; i <= 32; i++) { cipher[1] -= (cipher[0] + delta) ^ (16 * cipher[0] + key[2]) ^ ((cipher[0] >> 5) + key[3]); cipher[0] -= (cipher[1] + delta) ^ (16 * cipher[1] + key[0]) ^ ((cipher[1] >> 5) + key[1]); delta += 1640531527; } }
/**
-
@brief XTEA 解密一个 64 位数据块
-
@param cipher 待解密数据(2×32 bit)
-
@param key 128 位密钥(4×32 bit)
-
@param delta 常量值 / void xtea_decrypt(uint cipher[2], uint key[4]) { /*
- @brief 这里的反编译可能会出现 *(_DWORD *)。
-
不用害怕,以 (4LL * (v4 & 3) + xtea_key_address) 为例, -
由于 uint 的存储空间为 4 字节,乘上几个 4 就代表偏移量是几。 - @attention 根据 IDA 反编译的结果,修改下面代码中填 0x0 的部分。 */ uint delta = -1640531527 * 32;
for(int i = 1; i <= 32; i++) { cipher[1] -= (((cipher[0] >> 5) ^ (16 * cipher[0])) + cipher[0]) ^ (key[(delta >> 11) & 3] + delta); delta += 1640531527; cipher[0] -= (((cipher[1] >> 5) ^ (16 * cipher[1])) + cipher[1]) ^ (key[(delta & 3)] + delta);
} }
int main() { /** * @brief 你看懂 main 函数的逻辑了吗? * 本题分为两个加密部分: * 第一,tea_encrypt 函数对 flag 前 16 字节进行加密。 * 第二,xtea_encrypt 函数对 flag 后 16 字节进行加密。 * 密文分别存储在 tea_cipher 和 xtea_cipher 中。 * * @attention 让我们先来解密 tea_encrypt 部分! * 请双击 check_tea_part 函数,找到这部分的密文以及密钥。 */
/**
* @brief 密文数组由 4 个 32 位无符号整数构成。
*
* @attention 双击 tea_cipher,填充以下数据!我已经帮你填好一个了。
*/
uint cipher1[4] = {
0xB3E7E33E, 0xB4114672, 0x8E088C0C, 0x14B2C329
};
/**
* @brief 密钥数组同样由 4 个 32 位无符号整数构成。
* 双击 get_tea_key_address,再双击 tea_key,填充...
* 等一下,我的 32 位整数呢?!实际上,呈现在你面前的 16 字节,
* 是密钥数组在内存中原始的存储形式。按每四个字节划分,
* 就可以得到 4 个密钥。那么,第一个密钥是 0x78563412 吗?
* 并不是!x86-64 机器一般采用小端序存储。记住一点:
* 低位字节存储在低地址,高位字节存储在高地址。因此,
* 第一个密钥应该是 0x12345678。
*
* @attention 完成剩下的填充!
*/
uint key1[4] = {
0x12345678,
0x87654321,
0x13572468,
0x24681357
};
/**
* @brief 加密算法 TEA 要求加密密钥为 128 比特,密文块分组长度为 64 比特。
* 这里密文长度为 128 比特,因此要分为两个块分别加密。
*
* @attention 请移步 tea_decrypt 函数,完成解密逻辑编写!
*/
tea_decrypt(cipher1, key1);
tea_decrypt(cipher1 + 2, key1);
/**
* @brief 祝贺你成功完成第一部分的解密!第二部分是 xtea 解密,
* 整体思路和 tea 差不多,不过在加密流程中有些许出入。
*
* @attention 填充对应密文。
*/
uint cipher2[4] = {
0x60EC68AB, 0x940251CE, 0xB427534C, 0xDF435416 }; /** * @attention 填充对应密钥。注意:别忘记小端序! / uint key2[4] = { 0xDEADBEEF, 0xCAFEBABE, 0x11223344, 0x55667788 }; /* * @attention 移步 xtea_decrypt,继续完成解密逻辑编写。 / xtea_decrypt(cipher2, key2); xtea_decrypt(cipher2 + 2, key2); /* * @attention 补全所有部分后,运行程序。如果解密正确,就能看到输出的 flag 啦! */ printf(“%.16s%.16s\n”, (char *)cipher1, (char *)cipher2);
return 0;
}
moectf{Wh4t_a_n1ce_cup_0f_TEA!!}