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` 自己能算 → **它是个已知数**。你只是要把「加密时加上的那个数」算出来,再减掉。
所以:
```c
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`。解密只能**沿链倒着剥**:
```c
v1 -= G(v0, sum); /* v0 已知(密文里就有),先把 v1 还原出来 */
v0 -= F(v1, sum); /* 这里的 v1 必须是【刚还原出的新值】 */
```
两个必须注意的点:
1. **两条语句换序**:加密 v0 在前,解密 v1 在前。
2. **第二行必须用第一行刚算出的新值**,缓存旧值是经典错误(`lab.c` 第 4 节有反例)。
轮与轮之间同理:加密 1→32 轮,解密 32→1 轮,所以 `sum` 要从**终值**往回走,而不是从 0 开始。
---
## 5. ARX 密码的统一套路
TEA / XTEA / XXTEA / ChaCha / Salsa 这类只用三种运算:**A**dd(模加)、**R**otate/Shift(移位)、**X**or(异或),再加"把密钥混进来"。
套第 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 的设计原则:把难逆的混洗函数作用在"已知量"上,再用可逆运算(`+` / `^`)把它混进状态。**
所以你以后看到任何
```c
x += f(其他字); x ^= f(其他字);
```
都能立刻写出解密:
```c
x -= f(其他字); x ^= f(其他字); /* f 原样 */
```
这是 Feistel 结构的思想;TEA 是它的"模加版本"。
---
## 7. TEA / XTEA 逐行推导
### TEA
加密(对应 chall6 `tea_encrypt` @ 0x401205):
```c
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 反向:
```c
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):
```c
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"。
**解密必须尊重这个不对称**:
```c
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` 的填空
```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`,已按小端还原):
```c
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 · 第二行用了旧值**
```c
uint old = v1;
v1 -= G(v0, sum);
v0 -= F(old, sum); /* 错!必须用刚算出的新 v1 */
```
**坑 3 · C 的有符号溢出与字面量类型**
```c
uint sum = -1640531527 * 32; /* int 溢出,UB:GCC 常给对,但不保证 */
uint sum = (uint)0x9E3779B9 * 32; /* 规范写法;或直接 0xC6EF3720u */
```
另外 `(sum >> 11) & 3` 里的 `sum` 必须是**无符号**,否则右移是算术移位,下标会算错。模板用 `uint` 正好避开了。
---
## 9. 实战流程(拿到新题照这五步走)
1. **抄** —— 把反编译的加密函数逐行抄成能编译的 C,一行都别省,标出状态变量。
2. **定** —— 确认轮数、块大小、`sum`/`delta` 演化方式、密钥、字节序(x86 小端)。
3. **推** —— 机械套用:**轮倒序、语句倒序、外层取逆、内层照抄、sum 反向**。
4. **验** —— 写 round-trip 测试:用抄来的 encrypt 加密任意数据,再用自己的 decrypt 解,必须完全一致。
**这一步比肉眼推理可靠得多**,第 8 节的三个坑它全能抓出来。
5. **对** —— 把解出的明文喂回**原始程序**验证:`echo -n 'flag' | ./chall6`。
---
## 10. 练习
1. 补完 `tea6solve.c` 的填空,跑出 flag。
2. 把 `lab.c` 第 4 节的"反例"改成正确版,确认全部 PASS。
3. **思考**:XTEA 两半分别用 `sum & 3` 和 `(sum >> 11) & 3` 两个不同的位段取密钥,这样做对扩散有什么好处?如果都用 `sum & 3` 会怎样?
4. **进阶**:自己写一个 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!!}
评论交流
欢迎留下你的想法