(本文是新手 re 同学的学习笔记,所以可能比较啰唆,见谅~)
审题#
审题可知,flower 代表花指令,tea 代表加密算法是 TEA 或者某种 TEA 的衍生算法。
分析加密算法#
首先导航到 Strings 发现一个很明显的 base 64 编码数据 bTRwbGUxc3hubg==
,解密后发现 主席是男娘 得到了 m4ple1sxnn
,猜想应该是密钥。
再使用 FindCrypt 插件查看是否有明显的常见加密算法迹象:
没有看到有用信息,重新根据 Strings 中保留的字符串,定位到跳转点。整理后代码如下(其中包含对我的一些疑问点的注释,感谢 GPT 5 的解答):
__int64 sub_140001A79()
{
_QWORD v1[2]; // 实际扮演的是 结构体 而不是单纯数组。
// v1[0] 保存了地址值(这里是 &unk_1400C82E0)。
// v1[1] 保存了长度值(15)。
// sub_1400AB3F0 接收到的第二个参数本质上就是“指向一块数据的指针 + 这块数据的长度”。
_BYTE input[32]; // [rsp+30h] [rbp-70h] BYREF
_BYTE key[32]; // [rsp+50h] [rbp-50h] BYREF
_BYTE cipher[30]; // [rsp+70h] [rbp-30h] BYREF
char v5; // [rsp+8Eh] [rbp-12h] BYREF
char v6; // [rsp+8Fh] [rbp-11h] BYREF
char *v7; // [rsp+90h] [rbp-10h]
char *v8; // [rsp+98h] [rbp-8h]
sub_14000DC40();
v8 = &v5;
v1[0] = &unk_1400C82E0;
v1[1] = 15;
sub_1400AB3F0(cipher, v1, &v5);
sub_1400A1160(&v5);
v7 = &v6;
base64_decode(key, "bTRwbGUxc3hubg==", &v6); // 这里明显是base64处理函数,外部做解码即可。
sub_1400A10A0(&v6);
printf(&unk_1400C6BC0, "plz input your flag: "); // 这里应该是单纯打印字符串的函数
// 但是读取输入为何要顺带读一个"全局变量?"
// 绝大多数 C 程序在调用 `printf` 的时候,编译器最终会把它翻译成 `fprintf(stdout, "msg")`。
// `stdout` 在 Windows CRT 里就是一个全局变量,编译时常常会被放在 `.data` 里,反编译看上去就是 `&unk_1400C6BC0`。
get_input(input);// 显然,获取输入
trim_input(&unk_1400C6860, input); // 这里读取一个全局变量并对input做处理
// 可能的处理方式:1. 标准化字符串,删除空格和特殊字符之类。 2. 对字符串和读入的全局变量做变换,使之以某种方式"混合"或者"发生作用关系"。这里我们就认为是普通的处理输入。
if ( (unsigned __int8)xxtea_encrypt(input, cipher, key) )// 同时出现密文,密钥和输入,显然是加密比较函数了
printf(&unk_1400C6BC0, "Right!let_us_have_a_cup_of_tea!!!\n");
else
printf(&unk_1400C6BC0, "oh_no_no_no!!!\n");
sub_1400AFEB0(input);
sub_1400AFEB0(key);
sub_1400AB510(cipher);
// 对那几个栈上对象做收尾工作:释放可能的堆内存、把长度归零等。
return 0;
// 标准返回函数
}
c于是我们清理 sub_1400018CA
(在上述清理过的代码中为 xxtea_encrypt )代码:
__int64 __fastcall xxtea_encrypt(__int64 input, __int64 cipher, __int64 key)
{
size_t n16; // rbx
const void *v4; // rax
size_t Size; // rsi
const void *v6; // rbx
void *v7; // rax
__int64 v8; // rbx
__int64 v9; // rax
_BYTE v11[32]; // [rsp+20h] [rbp-70h] BYREF
_QWORD v12[2]; // [rsp+40h] [rbp-50h] BYREF
_BYTE v13[43]; // [rsp+50h] [rbp-40h] BYREF
char v14; // [rsp+7Bh] [rbp-15h] BYREF
int v15; // [rsp+7Ch] [rbp-14h] BYREF
char *v16; // [rsp+80h] [rbp-10h]
unsigned __int64 v17; // [rsp+88h] [rbp-8h]
sub_140001450(v13, key);
// 下面是处理 key 的函数
v12[0] = 0;
v12[1] = 0;
if ( (unsigned __int64)sub_14002F0A0(v13) > 0x10 )
n16 = 16;
else
n16 = sub_14002F0A0(v13);
v4 = (const void *)sub_1400AD570(v13);
memcpy(v12, v4, n16);
// 将10位的小暖男(确信)密钥扩展成16位,空缺处补0
// eg: maple1sxnn -> maple1sxnn000000
v17 = (unsigned __int64)(sub_14002F0A0(input) + 3) >> 2; // XXTEA 所需的"按4字节对齐后的词数"
v16 = &v14;
v15 = 0;
sub_1400AB490(v11, v17, &v15, &v14);
sub_1400A1160(&v14);
Size = sub_14002F0A0(input);
v6 = (const void *)sub_14002EEB0(input);
v7 = (void *)sub_1400AB3C0(v11);
memcpy(v7, v6, Size);
// 准备工作,实现一些拷贝,缓冲之类的操作
v8 = sub_14002D4C0(v11); // 当前数据长度(字节数)
v9 = sub_1400AB3C0(v11); // 数据指针
xxtea_encrypt(v9, v8, v12); // XXTEA 加密的核心函数
// 这里把 buf[0..len-1] 按小端 32-bit 词视图、长度 n = (len+3)>>2,用 key(四个 32-bit 子键)做循环加密。
LODWORD(v8) = sub_1400BF530(v11, cipher);
sub_1400AB510(v11);
sub_1400AFEB0(v13);
return (unsigned int)v8;
}
c可知:
- 使用
m4ple1sxnn
作为密钥材料(16 字节,因此填 0) - 对用户输入的flag进行加密(通过
sub_1400016DC
函数) - 将加密结果与预存的数据进行比较
花指令处理#
于是定位到 sub_1400016DC
函数,发现是一个跳转。显然这里 JUMPOUT 出的地址才是真正的实现函数。但是由于它没有被 IDA 识别为函数,我们只能强行看汇编了。
void __fastcall encrypt_impl(__int64 a1, unsigned __int64 a2)
{
if ( a2 > 1 && 0x34 / (unsigned int)a2 != -6 )
JUMPOUT(0x140001744LL);
}
c(AI 注释: 你不一定要把 JUMPOUT 目标当“新函数”看。 JUMPOUT 往往只是跳到一个代码块(可能在同一函数里,或落在别的函数的中间/尾块),反编译器因此不把它认作函数入口。先把它当作“落点基本块”去读即可;若你确认那儿才是一个独立入口,再手动建函数。 )
; 噪音 / 误解码(建议当作 db)
; xor eax, [rcx+4514F845h]
; adc [rax], eax
; xor eax, [rcx+4514F845h] 带巨大的位移,对未定义的 RCX 取内存;adc [rax], eax 会对 EAX 指向的地址写内存,若 EAX 未设定几乎必崩——它们既不保存现场也不建立栈框,和后续“规整的逻辑”断层。
; loc_14000174B ← 以这里作为代码起点更合理
mov eax, [rbp+var_8] ; eax = sum !!!注意,这里是读取sum的地方!!初始的Sum就是我们需要的Delta
shr eax, 2
and eax, 3 ; eax = (sum >> 2) & 3 → e ∈ {0,1,2,3}
mov [rbp+var_18], eax ; e
mov [rbp+var_C], 0 ; i = 0
jmp loc_140001806 ; 进入主循环
asm如果我们将上面两条花指令 NOP 掉,其实可以看见部分 XXTEA 的实现(Delta 累加部分被更多花指令隐藏掉了):
void __fastcall encrypt_impl(_DWORD *a1, unsigned __int64 a2, __int64 a3)
{
unsigned int *v3; // rax
unsigned int *v4; // rax
unsigned int v6; // [rsp+4h] [rbp-1Ch]
unsigned int v7; // [rsp+10h] [rbp-10h]
unsigned int i; // [rsp+14h] [rbp-Ch]
unsigned int v9; // [rsp+1Ch] [rbp-4h]
if ( a2 > 1 )
{
v9 = a1[(unsigned int)(a2 - 1)];
v7 = 0x34 / (unsigned int)a2 + 6;
while ( v7-- )
{
for ( i = 0; i < (int)a2 - 1; ++i )
{
v6 = a1[i + 1];
v3 = &a1[i];
*v3 += (v6 + (v9 ^ *(_DWORD *)(4LL * (i & 3) + a3))) ^ (((4 * v6) ^ (v9 >> 5)) + ((v6 >> 3) ^ (16 * v9)));
v9 = *v3;
}
v4 = &a1[(unsigned int)(a2 - 1)];
*v4 += (*a1 + (v9 ^ *(_DWORD *)(4LL * (i & 3) + a3))) ^ (((4 * *a1) ^ (v9 >> 5)) + ((*a1 >> 3) ^ (16 * v9)));
v9 = *v4;
}
}
}
c进入主循环(太长了不贴),可以发现是一个标准的”判断是主循环还是尾循环,并执行符合 XXTEA 规则的轮加密”的函数。自此我们确定了该函数是 XXTEA 加密。(这一步需要有密码学基础,至少得熟悉常见的对称加密的代码实现,可以选择问AI)下一步就是找 Delta 了。
然而,上面 FindCrypt 的结果已经指出:TEA 类算法中经典的 Delta 值(0x9E3779B9)并没有出现在我们的程序中(表现为检测不出 TEA 类算法),于是我们猜测该程序有自定义的 Delta 值。结合上述内容可知 Delta 被花指令隐藏,该部分处理比较困难,于是考虑通过动态调试获取之(由于笔者使用 Linux 环境,这里使用 winedbg + gdb,选用x64dbg 等亦可):
动态调试#
首先,我们需要解决一个问题:去哪找?
根据上述内容,注意到:
mov eax, [rbp+var_8] ; eax = sum !!!注意,这里是读取sum的地方!!初始的Sum就是我们需要的Delta
asm这里就是我们需要的函数,它的地址在 IDA 中可以发现是 0x14000174b
,于是启动 gdb 并下断:
pwndbg> winedbg --gdb another_flower_tea.exe
(Some Output)
pwndbg> b *0x14000174b # 下断点
pwndbg> c # continue,表示执行程序到断点
# 等待程序停在断点处,中间可能需要按照源程序要求执行输入操作,随便输入些东西即可
pwndbg> set $sum_addr = $rbp - 8 # 源程序显示采用的是负偏移,所以我们先提前算出负偏移的 offset
pwndbg> x/wx $sum_addr
0x21fd68: 0x00114514
bash好了!我们拿到了初始的 Delta——0x00114514,一个非常符合网安学长精神状态的数。于是根据标准的 XXTEA 加密过程,可以开始写解密脚本了。
写出解密脚本如下(可以求助 AI 或翻阅 Github 现成实现并修改 Delta 值):
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#define DELTA 0x00114514
void xxtea_decrypt(uint32_t *v, uint32_t len, uint32_t *k) {
uint32_t n = len - 1;
uint32_t z = v[n], y = v[0], sum = 0, e;
uint32_t p, q;
q = 6 + 52 / (n + 1);
sum = q * DELTA; // 初始sum值
while (sum != 0) {
e = (sum >> 2) & 3;
for (p = n; p > 0; p--) {
z = v[p - 1];
y = v[p] -= (((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^
((sum ^ y) + (k[(p & 3) ^ e] ^ z));
}
z = v[n];
y = v[0] -= (((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^
((sum ^ y) + (k[(p & 3) ^ e] ^ z));
sum -= DELTA;
}
}
// 将密钥扩展为4个uint32_t,不足部分填0
void key_setup(const char *key_str, uint32_t *key) {
memset(key, 0, 16); // 先清零
memcpy(key, key_str, strlen(key_str) < 16 ? strlen(key_str) : 16);
}
int main() {
// 加密数据(60字节,15个uint32_t)
uint8_t enc_data[] = {
0x92, 0x37, 0x53, 0x0B, 0x48, 0x37, 0x0A, 0x7D, 0x08, 0xF6, 0x91, 0x5F,
0xA2, 0x4B, 0x3C, 0xAD, 0x06, 0xFB, 0x6C, 0x8B, 0xF3, 0x51, 0x74, 0x6D,
0xF3, 0x8F, 0x6F, 0x58, 0x20, 0x75, 0xFE, 0x81, 0xE0, 0x46, 0x0A, 0x88,
0x0E, 0x80, 0x04, 0xBD, 0xBE, 0xCB, 0x4B, 0x74, 0xC4, 0x58, 0x12, 0x30,
0x91, 0x29, 0x4D, 0x12, 0x1E, 0xCE, 0x38, 0x01, 0xD5, 0xF3, 0x0D, 0x23};
uint32_t data[15];
memcpy(data, enc_data, sizeof(data));
// 密钥
const char *key_str = "m4ple1sxnn";
uint32_t key[4];
key_setup(key_str, key);
// 解密
xxtea_decrypt(data, 15, key);
// 以字符串形式输出(可读明文)
printf("\n解密结果 (string):\n");
char *str = (char *)data;
for (int i = 0; i < 60; i++) {
if (str[i] >= 32 && str[i] <= 126) {
putchar(str[i]);
} else if (str[i] == 0) {
putchar('\\');
putchar('0');
} else {
printf("\\x%02X", (unsigned char)str[i]);
}
}
putchar('\n');
return 0;
}
c