帮我看下这道逆向题,题解说sub_557E8B201180是tea加密,我当时没看出来,我只完成了s...
Created on: March 3, 2026
Answered using GPT-5.2 Pro by Chat01
Created on: March 3, 2026
Answered using GPT-5.2 Pro by Chat01
帮我看下这道逆向题,题解说sub_557E8B201180是tea加密,我当时没看出来,我只完成了sub_557E8B200C00的分析,是根据输入生成十六进制,后面还说sub_557E8B2013D0(v5, v9) == 27106是自制的crc校验,我不太懂,原题有个提示就是给了md5值,hint md5('rctf{'+input+'}').digest.hex() == '5f8243a662cf71bf31d2b2602638dc1d'
不用提示能写出来吗?
我没分析出来这个是tea加密,虽然IDA的findcrypt插件识别出来了,但是我觉得不能依赖这个,万一是变种就不好了,这个算法识别有什么诀窍吗,或者说我对一个sub_557E8B201180(ptr, v4, &unk_557E8B402010, 16, &v9)这样的函数分析应该怎么做更好呢,后面的crc校验看着也挺复杂的,我该怎么做呢
__int64 __fastcall main(int a1, char **a2, char **a3)
{
char *v3; // kr00_8
int v4; // eax
char *v5; // rax
char *v6; // rbx
__int64 i; // rax
int v9; // [rsp+4h] [rbp-124h] BYREF
void *ptr; // [rsp+8h] [rbp-120h] BYREF
char flag[264]; // [rsp+10h] [rbp-118h] BYREF
unsigned __int64 v12; // [rsp+118h] [rbp-10h]
v12 = __readfsqword(0x28u);
setbuf(MEMORY[0x7F58EA1AE6B0], 0LL);
setbuf(MEMORY[0x7F58EA1AE6A8], 0LL);
setbuf(MEMORY[0x7F58EA1AE6A0], 0LL);
ptr = 0LL;
v9 = 0;
memset(flag, 0, 0x100uLL);
__printf_chk(1LL, "Input right flag you can got 'Bingo!' :");
__isoc99_scanf("%31s", flag);
v3 = &flag[strlen(flag)];
if ( (v3 - flag) > 16 )
{
puts("input is too long!");
}
else if ( v3 - flag == 16 ) // 长度正确
{
v4 = sub_557E8B200C00(flag, 16, &ptr);
if ( v4
&& (v5 = sub_557E8B201180(ptr, v4, &unk_557E8B402010, 16, &v9), (v6 = v5) != 0LL)
&& v9 > 0
&& sub_557E8B2013D0(v5, v9) == 27106 )
{
for ( i = 0LL; v9 > i; ++i )
v6[i] ^= 0x17u;
puts(v6);
if ( ptr )
free(ptr);
free(v6);
}
else
{
puts("input flag is wrong!");
}
} // 长度正确
else
{
puts("input is too short!");
}
return 0LL;
}
你这题其实不需要依赖 FindCrypt也能稳稳写出来,而且题解里说 “TEA” 其实更准确地说是 XXTEA / Block TEA(btea) 那一支(TEA 家族),你没看出来很正常——但它在汇编里有非常“露骨”的指纹。
下面我按你给的 main 流程,把关键点和“怎么更好分析这种函数”的套路都写清楚,并且给出不靠提示也能直接推出 input 的解法(最后再用 MD5 hint 验证大小写)。
你 main 里关键逻辑是:
sub_200C00(flag,16,&ptr)sub_201180(ptr, v4, key, 16, &v9) 得到 v5/v6crc(v5, v9) == 27106v6[i] ^= 0x17 然后 puts(v6) 输出所以这是典型的:
输入(16字节字符串) → 变换A → 解密/解码B → CRC校验 → XOR解码 → 打印
这类题最爽的解法就是从打印内容倒推:
sub_200C00:这里其实是“hex 解码”,不是生成 hex看你贴的 main,sub_200C00(flag, 16, &ptr) 返回的 v4 会作为长度传进 sub_201180(ptr, v4, ...)。
这题二进制里 sub_200C00 的行为是:
len/2 字节"0"-"9","A"-"F","a"-"f" 映射为 nibble也就是说:输入 16 个 hex 字符 → 输出 8 个字节(v4=8)。
这点非常关键:后面 TEA/XXTEA 那坨就是按 32-bit word 操作的,8 字节正好 2 个 word。
sub_201180(ptr, v4, key, 16, &v9) 为啥能认出是 XXTEA/BTEA(不靠插件)你不想靠 FindCrypt,这思路对。那怎么“人工识别”?
看这种函数你可以按下面顺序做:
它检查了这些东西:
keylen == 16 ✅(128-bit key,非常像 TEA/XXTEA)len > 0len % 4 == 0 ✅(按 32-bit word 对齐)malloc(len+1) + memcpy + 末尾补 0 ✅(当作字符串返回)这就很像:decrypt(bytes, len, key[16]) -> printable string
你在内层(它调用的那个大函数)里会看到:
0x9E3779B9(TEA 家族的 delta)0x61C88647(实际上就是 -0x9E3779B9 mod 2^32)这俩一出现,基本就可以锁定是 TEA-family。
经典 TEA 是每块 64-bit、固定 32 轮,常见结构是两条 32-bit 来回加减。
但这题的核心循环有两个非常“XXTEA”的特征:
轮数公式:6 + 52/n
你会在汇编里看到用 0x34(=52) 去除 n,再加 6 的影子。
key 索引形态:k[(p & 3) ^ e],其中 e = (sum >> 2) & 3
这是 XXTEA/BTEA 的标配写法。
另外还有一个超级关键点:wrapper 里把 len/4 取负传进去(n = -(len/4))
这正是 btea 参考实现的约定:
n > 1:加密n < -1:解密所以 sub_201180 本质是:
拿输入当密文 → XXTEA 解密 → 根据最后一个字节做 padding 截断 → 返回明文 + out_len
它解密完会读最后一个字节 pad = buf[len-1],要求 pad <= 4 且 pad < len,然后:
out_len = len - padbuf[out_len] = 0这就是“去 padding”的逻辑(虽然它没有校验 padding 内容,算个小 bug)。
sub_2013D0(v5, v9) == 27106
你看 CRC 那坨如果觉得乱,其实抓一个点就够:
0x1021、0xA001、0x80050x1021 → CRC-16/CCITT 系。另外它还调用了两个很像“bit reverse”的小函数(一个对 byte,一个对 word),说明这是 RefIn/RefOut 都开启的版本。
配上 init=0、xorout=0 的行为,很像 CRC-16/KERMIT 那个配置。
你不用背名字,写脚本照着逻辑实现即可。
关键观察:
puts(v6)^= 0x17'Bingo!'所以我们直接假设输出就是 "Bingo!"(这不是 md5 hint,是程序字符串在明示)。
打印前做 v6[i] ^= 0x17
所以 XOR 前(也就是 CRC 校验的对象)应该是:
textpre = b"Bingo!" ^ 0x17 = b"U~ypx6"
把 b"U~ypx6" 按它的 CRC 算法算一下,结果就是 0x69E2 = 27106
这一步等于“自校验”:说明你对输出的推断完全正确。
这就是你问的“不用提示能写出来吗”:
可以,因为 CRC 常量本身就能反证你是否猜对了明文。
解密输出原本是 8 字节(len=8),但它会按最后一个字节 pad 截短。
"U~ypx6" 长度 6,所以 pad 应该是 2 → 最后一个字节放 0x02,并且标准 padding 会再补一个 0x02:
textplain8 = b"U~ypx6" + b"\x02\x02" = 55 7e 79 70 78 36 02 02
(注意:程序实际上没校验倒数第二个 padding 字节,但按常见实现应当是 02 02,这也让解唯一,并且刚好匹配题目的 md5 hint。)
key 在 .data 里那 16 字节:
c7e0c7e0 d7d3f1c6 d3c6d3c6 ced2d0c4
把 plain8 用 btea(n=2) 加密,得到密文:
05 e8 a3 76 e4 e0 44 6e
转成 hex 输入就是:
05E8A376E4E0446E05e8a376e4e0446e二进制两者都接受,但 md5 hint 指向小写,所以最终 flag 是:
✅ rctf{05e8a376e4e0446e}
pythonimport hashlib DELTA = 0x9E3779B9 def reflect_bits(x, width): r = 0 for i in range(width): if (x >> i) & 1: r |= 1 << (width - 1 - i) return r def crc16_like_bin(data: bytes) -> int: # 对应题目 sub_2013D0:byte 反转 + poly 0x1021 + 最终 16bit 反转 crc = 0 for b in data: b = reflect_bits(b, 8) crc ^= (b << 8) for _ in range(8): if crc & 0x8000: crc = ((crc << 1) & 0xFFFF) ^ 0x1021 else: crc = (crc << 1) & 0xFFFF crc = reflect_bits(crc, 16) return crc def btea(v, n, k): # 标准 Block TEA / XXTEA 参考写法 def MX(z, y, sum_, p, e): return (((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4)) ^ ((sum_ ^ y) + (k[(p & 3) ^ e] ^ z))) & 0xFFFFFFFF if n > 1: rounds = 6 + 52 // n sum_ = 0 z = v[n - 1] while rounds: sum_ = (sum_ + DELTA) & 0xFFFFFFFF e = (sum_ >> 2) & 3 for p in range(n - 1): y = v[p + 1] v[p] = (v[p] + MX(z, y, sum_, p, e)) & 0xFFFFFFFF z = v[p] y = v[0] v[n - 1] = (v[n - 1] + MX(z, y, sum_, n - 1, e)) & 0xFFFFFFFF z = v[n - 1] rounds -= 1 elif n < -1: n = -n rounds = 6 + 52 // n sum_ = (rounds * DELTA) & 0xFFFFFFFF y = v[0] while rounds: e = (sum_ >> 2) & 3 for p in range(n - 1, 0, -1): z = v[p - 1] v[p] = (v[p] - MX(z, y, sum_, p, e)) & 0xFFFFFFFF y = v[p] z = v[n - 1] v[0] = (v[0] - MX(z, y, sum_, 0, e)) & 0xFFFFFFFF y = v[0] sum_ = (sum_ - DELTA) & 0xFFFFFFFF rounds -= 1 return v # key: 从 .data: c7e0c7e0 d7d3f1c6 d3c6d3c6 ced2d0c4 key_bytes = bytes.fromhex("c7e0c7e0d7d3f1c6d3c6d3c6ced2d0c4") k = [int.from_bytes(key_bytes[i:i+4], "little") for i in range(0, 16, 4)] # 目标输出:Bingo! out = b"Bingo!" pre = bytes([c ^ 0x17 for c in out]) # XOR 还原 assert crc16_like_bin(pre) == 27106 # 0x69E2 plain8 = pre + b"\x02\x02" # padding(8字节) v = [int.from_bytes(plain8[i:i+4], "little") for i in range(0, 8, 4)] c = btea(v, 2, k) cipher = b"".join(x.to_bytes(4, "little") for x in c) inp = cipher.hex() # 小写 hex print("input =", inp) print("flag =", f"rctf{{{inp}}}") print("md5 =", hashlib.md5(f"rctf{{{inp}}}".encode()).hexdigest())
运行会得到:
input = 05e8a376e4e0446eflag = rctf{05e8a376e4e0446e}5f8243a662cf71bf31d2b2602638dc1d以 sub_201180(ptr, v4, &unk_key, 16, &v9) 为例:
keylen==16 → key[16]len%4==0 → 32-bit word 算法malloc(len+1) + 写 0 → 输出是字符串/可打印数据outlen 指针被写 → 返回 buffer + length这种函数你就可以先把它重命名成:
decrypt_buf(in, in_len, key, keylen, out_len)
wrapper 里只做参数检查和内存管理,真正算法在它调用的那个大函数里。
而大函数里:
再看它是左移还是右移、有没有 bit reverse,就能判断 refin/refout。
你这题就是完美例子:
这比你从输入空间硬推强太多。
程序可通过的 input(16 hex 字符,小写)是:
05e8a376e4e0446e
对应 flag(结合 hint 的格式):
✅ rctf{05e8a376e4e0446e}
如果你愿意,我也可以把 sub_201180 和 sub_2013D0 对应的伪代码按你 IDA 的风格写成几乎一模一样的 C(便于你对照反编译输出、练“看出算法”的感觉)。
sub_55A507E01180之所以卡住我是因为这个函数在我输入16个1时会返回0,然后跳转到flag错误,我就觉得这是判断函数而不是加密或者解密函数,这是为什么
你这个“卡住”的点,其实是非常典型的误判:sub_55A507E01180 不是“判断函数”,它更像一个 decrypt/解码 wrapper,返回 指针(成功时指向解密后的缓冲区),失败时返回 0 = NULL。你输入 16 个 1 时返回 0,原因是它里面做了一个很“苛刻”的padding/格式合法性检查,随机密文几乎都会过不了,所以你一跑就一直是 NULL。
我把你这次遇到的现象,用更贴近反编译/汇编的角度解释清楚。
1,实际传进去的不是 “16字节 0x31”main 里先走了 sub_200C00(flag, 16, &ptr),它是 hex 解码:
"1111111111111111"(16 个字符)"11" -> 0x11ptr 实际是 8 字节:0x11 0x11 0x11 0x11 0x11 0x11 0x11 0x11v4 就是 8(长度 8)然后 sub_...1180(ptr, 8, key, 16, &v9) 才开始工作。
sub_55A507E01180 返回 0 的真正原因:它在解密后做了 padding 检查我看了你上传的 babyre 里 .text 段(这个函数就在 RVA 0x1180),它的逻辑非常清晰:
if (!in) return 0;if (len <= 0) return 0;if (!key) return 0;if (keylen != 16) return 0;if (outlen_ptr) *outlen = 0;if (len % 4 != 0) return 0;(按 32-bit word 对齐)这些都过了才会继续。
malloc(len+1),拷贝密文,然后调用真正的解密核心关键片段你应该能在 IDA 里看到:
buf = malloc(len + 1)memcpy(buf, in, len)n = -(len/4)btea_xxtea(buf, n, key) ← 真正的 TEA-family 解密在这里然后才是你遇到的“为什么经常返回 0”的核心:
它做了类似:
cpad = buf[len - 1]; buf[len] = 0; // 先塞一个0当临时字符串结尾 *outlen = len; if (pad >= len || pad > 4) { free(buf); *outlen = 0; return NULL; } *outlen = len - pad; buf[*outlen] = 0; return buf;
也就是说:
pad < len(不能比总长还大)free() 然后返回 NULL1)几乎必定失败?因为对一个固定 key 的 “XXTEA/BlockTEA 解密” 来说:
pad 在 0~255 上近似均匀pad ∈ {0,1,2,3,4}(从汇编看它允许 0)也就是说:你随便试一个输入,98% 的概率它都会直接返回 NULL
你看到“输入 16 个 1 返回 0”完全正常。
这也解释了你当时的直觉误区:
你看到函数返回 0,就以为“这是在判断对不对”。
但实际上它返回的是 NULL 指针,代表“解密结果不符合它自定义的 padding/格式规则”。
你 main 里是这样用的:
cv5 = sub_201180(...); if (v5 && v9 > 0 && crc(v5, v9) == 27106) { ... puts(v5 ^ 0x17); }
注意:
char* 来 XOR、来 puts__int64,你也应该手动改函数原型,否则你会一直把 NULL 当 “false”。你可以在 IDA 里把它改成类似:
cchar* __fastcall sub_201180(void* in, int len, void* key, int keylen, int* outlen);
改完之后,你再看 return 0 的语义就会立刻变成:返回 NULL,不是“校验失败返回 false”。
如果你想观察它到底解密成啥(方便你识别 XXTEA 特征),最实用的办法是:
btea 调用返回后下断点(或者在读取 buf[len-1] 那行下断点)pad 是多少pad 改成 0/2/4 之一,让它通过把 cmp eax, 4; jle ok 这一段改成“无条件跳转 ok”(或者直接 NOP 掉失败分支),让它永远不因为 pad 失败而 return NULL。
这样你用任意输入都能拿到解密后的 buffer,方便继续做算法识别。
它把 “解密 + padding 检查” 包在一起,导致:
很多 CTF babyre 都会用这种招数来“卡新手”。
如果你愿意,你把 sub_55A507E01180 里调用的那个核心函数(在你二进制里是 0xCE0 那个)反编译贴出来,我可以教你怎么从那一坨运算里不靠 FindCrypt,稳定抓出 XXTEA/BTEA 的三大指纹(0x9E3779B9、6+52/n、k[(p&3)^e]),以后遇到变种也不慌。