这道逆向题的flag是flag{d07abccf8a410cb37a},后面6位也是异或加密只是没有...
تم الإنشاء في: ٢٣ مارس ٢٠٢٦
تمت الإجابة باستخدام GPT-5.4 Thinking بواسطة Chat01
تم الإنشاء في: ٢٣ مارس ٢٠٢٦
تمت الإجابة باستخدام GPT-5.4 Thinking بواسطة Chat01
这道逆向题的flag是flag{d07abccf8a410cb37a},后面6位也是异或加密只是没有在程序中体现,是一道错题(输入正确flag过不了检查)。
.text:00401824 cmp [ebp+var_C], 0BAh
.text:0040182B jle short loc_401807
.text:0040182D lea eax, [ebp+Destination]
.text:00401830 mov [esp], eax
.text:00401833 call near ptr sub_401500
.text:00401838 test eax, eax
.text:0040183A setnz al
.text:0040183D test al, al
.text:0040183F jz short loc_40184C
.text:00401841 lea eax, [ebp+Destination]
.text:00401844 mov [esp], eax ; char *
.text:00401847 call sub_40159A
这个最终判断函数sub_40159A,反编译显示有很多参数,点进去显示只有一个参数,我看汇编却没有看到ecx,应该是ecx是传入参数啊,怎么回事
不是 ecx 传参,这里 sub_40159A 实际上就是 1 个参数,而且是 栈传参。
你这题最关键的点是:0x401500 ~ 0x4015BA 这段代码在调用前被异或解密了。
你贴的这段:
asm.text:00401807 ... .text:00401824 cmp [ebp+var_C], 0BAh .text:0040182B jle short loc_401807 .text:0040182D lea eax, [ebp+Destination] .text:00401830 mov [esp], eax .text:00401833 call near ptr sub_401500 ... .text:00401841 lea eax, [ebp+Destination] .text:00401844 mov [esp], eax .text:00401847 call sub_40159A
前面循环把 0x401500 开始的 0xBB 个字节全部 xor 0x41 了,所以:
调用 sub_40159A 前只有:
asmlea eax, [ebp+Destination] mov [esp], eax call sub_40159A
这说明调用者把参数放到栈顶 esp,也就是:
csub_40159A(Destination);
如果真是 thiscall,常见形式应该是类似:
asmlea ecx, [obj] call xxx
但这里根本没有。
sub_40159A这段解密后其实是:
asm40159a: push ebp 40159b: mov ebp, esp 40159d: sub esp, 28h ... 40160f: mov eax, [ebp+8]
注意这里直接用的是:
asm[ebp+8]
这就是 第一个栈参数。
如果是 ecx 传进来的 this,函数体里一般会先把 ecx 存起来,或者直接围绕 ecx 用,不会把“唯一参数”写成 [ebp+8]。
所以这已经坐实了:
sub_40159A 不是靠 ecx 传参,而是普通的 32 位栈传参函数。
因为这段函数在静态状态下前半截是加密字节。反编译器会发生几种典型误判:
错误切分函数边界
把中间某个位置误认成函数入口。
把垃圾指令里的栈访问误识别成参数
比如 [ebp+0Ch] [ebp+10h] [ebp+14h] 这种本来只是错误反汇编产物,却被伪代码当成“第 2、第 3、第 4 个参数”。
错误推断调用约定
有时会被推成 stdcall / fastcall / 甚至奇怪的自定义签名。
你这个文件还带了 C++ 符号名,40159A 对应的是:
c__Z7finallyPc
解出来就是:
cfinally(char *)
也正好说明它就一个 char * 参数。
结论就是:
不是。这里的 ecx 不是传参寄存器,只是普通临时寄存器。
在 sub_40159A 里,ecx 后面这几句:
asm4015cd: mov ecx, eax ... 4015ed: sub ecx, eax
只是拿来算 rand() % 100 的中间值,和参数无关。
解密后逻辑大概是:
cvoid finally(char *s) { char v[5] = {0x25, 0x74, 0x70, 0x26, 0x3a}; // "%tp&:" srand(time(0)); int r = rand() % 100; for (int i = 0; i <= 4; i++) { if ( (s[i] != v[i]) != r ) { puts("wrong"); break; } else { puts("..."); } } }
这里明显有 bug:
(s[i] != v[i]) 的结果只有 0 或 1r = rand()%100 是 0~99所以除非 r 恰好等于 0 或 1,否则永远过不了。
这也印证了你说的“错题”。
你这里不是 “ecx 隐式传参没显示出来”,而是:
sub_40159A 实际只有 一个 char * 参数[esp] / [ebp+8] 栈传递如果你愿意,我可以直接把 sub_401500 和 sub_40159A 都还原成完整 C 伪代码,顺手把“后 6 位该怎么补”也一起推出来。
__cdecl sub_40159A(int),32位程序的cdecl和__fastcall传入参数是不一样的是吧。
int __cdecl sub_40159A(int a1)
{
unsigned int v1; // eax
char v3[9]; // [esp+13h] [ebp-15h] BYREF
int v4; // [esp+1Ch] [ebp-Ch]
strcpy(v3, "%tp&:");
v1 = time(0);
srand(v1);
v4 = rand() % 100;
v3[6] = 0;
*&v3[7] = 0;
if ( (v3[v3[5]] != *(v3[5] + a1)) == v4 )
return puts("Really??? Did you find it?OMG!!!");
else
return puts("I hide the last part, you will not succeed!!!");
}
这里v3[5]我每次动态调试时总是0,为什么,没有给他赋值啊
pythonimport pefile, binascii, struct, os, sys, textwrap, math, json pe=pefile.PE('/mnt/data/jocker.exe') base=pe.OPTIONAL_HEADER.ImageBase rva=0x40159a-base off=pe.get_offset_from_rva(rva) data=pe.__data__[off:off+0x30] data[:0x21], len(data)
对,32 位里 __cdecl 和 __fastcall 传参方式确实不一样。
最常见的区别可以这么记:
__cdecl:参数都走栈,从右到左压栈,调用者清栈__stdcall:参数也走栈,从右到左压栈,被调者清栈__fastcall:前两个参数常用 ecx、edx 传,剩下再走栈thiscall:C++ 成员函数常把 this 放 ecx但你这题里,sub_40159A 不是 fastcall,也不是 ecx 传参,而是普通栈传参。调用点很明显:
asmlea eax, [ebp+Destination] mov [esp], eax call sub_40159A
这就是:
csub_40159A(Destination);
v3[5] 总是 0因为这个 v3[5] 不是一个真正被单独赋值的数组元素,而是反编译器把栈上的别的局部变量错认成了 v3 的一部分。
我把这段真实汇编还原后,关键部分是这样的:
asm40159a: push ebp 40159b: mov ebp, esp 40159d: sub esp, 28h 4015a0: mov byte ptr [ebp-15h], 25h ; '%' 4015a4: mov byte ptr [ebp-14h], 74h ; 't' 4015a8: mov byte ptr [ebp-13h], 70h ; 'p' 4015ac: mov byte ptr [ebp-12h], 26h ; '&' 4015b0: mov byte ptr [ebp-11h], 3Ah ; ':' ... 4015f1: mov [ebp-0Ch], eax ; rand()%100 4015f4: mov [ebp-10h], 0 ; i = 0 4015fb: cmp [ebp-10h], 4 ... 401601: lea edx, [ebp-15h] 401604: mov eax, [ebp-10h] 401607: add eax, edx ; &buf[i] 401609: movzx edx, byte ptr [eax] ; buf[i] 40160c: mov ecx, [ebp-10h] 40160f: mov eax, [ebp+8] 401612: add eax, ecx 401614: movzx eax, byte ptr [eax] ; a1[i]
cchar buf[5]; // [ebp-15] ~ [ebp-11] 存 "%tp&:" int i; // [ebp-10] int v4; // [ebp-0c]
而反编译器给你硬拼成了:
cchar v3[9]; // [ebp-15] ~ [ebp-0d] int v4; // [ebp-0c]
这样一来:
v3[0..4] 对应 "%tp&:"v3[5] 实际上已经踩到 [ebp-10]i所以你看到:
cv3[v3[5]] *(v3[5] + a1)
本质上其实是:
cbuf[i] a1[i]
v3[5] 总是 0因为它其实对应的是:
asmmov [ebp-10h], 0
也就是循环下标 i = 0 的初始化。
刚进比较逻辑时它当然先是 0,后面会在循环里递增。如果你断在刚开始的位置,看到它总是 0 很正常。
你看到的这个:
cstrcpy(v3, "%tp&:"); v3[6] = 0; *&v3[7] = 0;
也不是真实源码逻辑,至少不是可靠还原。真实机器码里没有这串 strcpy,而是直接逐字节写入:
asmmov byte ptr [ebp-15h], 25h mov byte ptr [ebp-14h], 74h mov byte ptr [ebp-13h], 70h mov byte ptr [ebp-12h], 26h mov byte ptr [ebp-11h], 3Ah
所以这里不是“字符串数组 + 未初始化元素”的正常 C 代码,而是 反编译器因为前面函数被异或加密过,局部变量恢复错位了。
cint __cdecl finally(char *a1) { char buf[5]; int r; int i; buf[0] = '%'; buf[1] = 't'; buf[2] = 'p'; buf[3] = '&'; buf[4] = ':'; srand(time(0)); r = rand() % 100; for (i = 0; i <= 4; i++) { if ( ((buf[i] != a1[i]) ? 1 : 0) == r ) puts("Really??? Did you find it?OMG!!!"); else puts("I hide the last part, you will not succeed!!!"); } }
不过它还有逻辑 bug,因为:
(buf[i] != a1[i]) 只能是 0 或 1rand()%100 是 0~99所以除非随机数正好是 0 或 1,否则条件根本不可能按出题人预期工作。这也正好对应你前面说的“错题”。
你这里的 v3[5] 之所以总是 0,不是因为“没赋值却莫名其妙变 0”,而是:
v3[5] 实际对应 [ebp-10][ebp-10] 实际是循环变量 i0所以真正应该读成:
cbuf[i] a1[i]
不是:
cv3[v3[5]] *(v3[5] + a1)
如果你愿意,我可以继续把 sub_401500 也按真实汇编完整还原出来,然后把这题“前 24 位怎么验、后 6 位为什么过不了”一口气给你理顺。
.text:00401601 lea edx, [ebp+var_15]
.text:00401604 mov eax, dword ptr [ebp+var_15+5]
.text:00401607 add eax, edx
.text:00401609 movzx edx, byte ptr [eax]
.text:0040160C mov ecx, dword ptr [ebp+var_15+5]
.text:0040160F mov eax, [ebp+arg_0]
.text:00401612 add eax, ecx
.text:00401614 movzx eax, byte ptr [eax]
.text:00401617 cmp dl, al
.text:00401619 setnz al
.text:0040161C movzx eax, al
.text:0040161F cmp eax, [ebp+var_C]
.text:00401622 jz short loc_401632
帮我逐行分析这段指令,它是怎么被一步步反编译成 if ( (v3[v3[5]] != *(v3[5] + a1)) == v4 )的
可以,这段正好能看出反编译器是怎么“机械地”把汇编拼成那句伪代码的。
先说结论:
这段汇编真正表达的是:
cif ( (buf[i] != a1[i]) == v4 )
而反编译器之所以给你还原成
cif ( (v3[v3[5]] != *(v3[5] + a1)) == v4 )
是因为它把:
buf 识别成了 v3i 这个独立局部变量,错识别成了 v3[5]所以才会出现这种很怪的写法。
你这几条里:
[ebp+arg_0]:函数参数 a1[ebp+var_C]:v4[ebp+var_15]:局部字符数组起始地址[ebp+var_15+5]:反编译器认为是 v3[5]但这个 [ebp+var_15+5] 其实不是“数组第 6 个元素”的本意,而是另一个局部变量 i,只是地址上刚好紧挨着。
因为:
var_15 = -15hvar_15 + 5 = -10h所以:
asm[ebp+var_15+5] = [ebp-10h]
这其实是循环变量 i 所在的位置。
asm.text:00401601 lea edx, [ebp+var_15]
lea 取地址,不取内容。
所以这里:
cedx = &v3[0];
或者更准确点:
cedx = &buf[0];
即:
edx 现在保存局部数组首地址asm.text:00401604 mov eax, dword ptr [ebp+var_15+5]
把 [ebp-10h] 的 4 字节值读到 eax。
反编译器看到:
v3 后面一点v3[5]于是还原成:
ceax = v3[5];
但真实更像:
ceax = i;
asm.text:00401607 add eax, edx
现在:
edx = &buf[0]eax = i所以相加后:
ceax = &buf[0] + i;
也就是:
ceax = &buf[i];
如果沿着反编译器的错误理解走,就是:
ceax = &v3[0] + v3[5];
asm.text:00401609 movzx edx, byte ptr [eax]
从 eax 指向的位置取 1 字节,零扩展到 edx。
也就是:
cedx = (unsigned char)buf[i];
错误视角下被还原成:
cedx = (unsigned char)v3[v3[5]];
所以到这里,左半边来源就是:
cv3[v3[5]]
asm.text:0040160C mov ecx, dword ptr [ebp+var_15+5]
再次取 [ebp-10h]。
真实是:
cecx = i;
反编译器错误理解成:
cecx = v3[5];
asm.text:0040160F mov eax, [ebp+arg_0]
取函数参数 a1。
ceax = a1;
asm.text:00401612 add eax, ecx
把 a1 指针加上偏移。
真实是:
ceax = a1 + i;
错误视角下:
ceax = a1 + v3[5];
asm.text:00401614 movzx eax, byte ptr [eax]
取 a1[i] 这个字节。
真实是:
ceax = (unsigned char)a1[i];
错误视角下就是:
ceax = (unsigned char)*(v3[5] + a1);
这就是伪代码右半边:
c*(v3[5] + a1)
它其实只是另一种写法:
ca1[v3[5]]
也就是本质上的 a1[i]。
asm.text:00401617 cmp dl, al
比较两个字节:
dl = buf[i]al = a1[i]即:
cbuf[i] ?= a1[i]
asm.text:00401619 setnz al
如果不相等,则 al = 1,否则 al = 0。
于是:
cal = (buf[i] != a1[i]);
错误视角下就是:
cal = (v3[v3[5]] != *(v3[5] + a1));
asm.text:0040161C movzx eax, al
把 al 扩展成整型 eax:
ceax = (unsigned int)((buf[i] != a1[i]) ? 1 : 0);
也就是:
ceax = (buf[i] != a1[i]);
asm.text:0040161F cmp eax, [ebp+var_C]
拿布尔结果和 v4 比较:
cif ( (buf[i] != a1[i]) == v4 )
asm.text:00401622 jz short loc_401632
如果相等则跳转。
cmp eax, [ebp+var_C] 后 jz 的意思就是:
cif ( eax == v4 ) goto loc_401632;
也就是:
cif ( (buf[i] != a1[i]) == v4 )
它大概按下面的机械过程生成:
asmlea edx, [ebp+var_15] mov eax, [ebp+var_15+5] add eax, edx movzx edx, byte ptr [eax]
被理解成:
cedx = v3[v3[5]];
因为它认为:
&v3[0] 是数组首地址[ebp+var_15+5] 是 v3[5]v3[v3[5]]asmmov ecx, [ebp+var_15+5] mov eax, [ebp+arg_0] add eax, ecx movzx eax, byte ptr [eax]
被理解成:
ceax = *(v3[5] + a1);
因为它认为:
[ebp+var_15+5] 是 v3[5]a1 + v3[5] 再取字节,就是 *(v3[5] + a1)asmcmp dl, al setnz al movzx eax, al
变成:
ceax = (v3[v3[5]] != *(v3[5] + a1));
asmcmp eax, [ebp+var_C] jz ...
变成:
cif ( (v3[v3[5]] != *(v3[5] + a1)) == v4 )
因为反编译器错把:
cint i;
并进了数组 v3 的地址范围里,导致:
ci
被显示成:
cv3[5]
所以真正人类应当读成:
cif ( (buf[i] != a1[i]) == v4 )
而不是:
cif ( (v3[v3[5]] != *(v3[5] + a1)) == v4 )
cchar *buf = &v3[0]; int i = *(int *)(ebp-10h); left = buf[i]; right = a1[i]; cond = (left != right); if (cond == v4)
cleft = v3[v3[5]]; right = *(v3[5] + a1); cond = (left != right); if (cond == v4)
cif ( (buf[i] != a1[i]) == v4 )
如果你愿意,我下一条可以继续把这整个 sub_40159A 直接整理成“错误反编译版 / 正确C版 / 对应汇编版”三栏对照,你看这种题会更快。
.text:00401617 cmp dl, al这一行,dl和al装载的不是地址值吗,怎么能直接比较地址
.text:00401617 cmp dl, al这一行,dl和al装载的不是地址值吗,怎么能直接比较地址
不是在比较地址,cmp dl, al 比较的是 两个字节的值。
关键在前两条 movzx:
asm.text:00401609 movzx edx, byte ptr [eax] ... .text:00401614 movzx eax, byte ptr [eax] .text:00401617 cmp dl, al
你把流程拆开看就清楚了。
先看左边这段:
asmlea edx, [ebp+var_15] ; edx = 数组首地址 mov eax, [ebp+var_15+5] ; eax = i add eax, edx ; eax = &buf[i] movzx edx, byte ptr [eax] ; edx = buf[i]
注意最后这一句:
asmmovzx edx, byte ptr [eax]
含义不是“把地址放进 edx”,而是:
eax 当成地址eax 指向内存里的 1 个字节edx所以执行完以后:
eax 之前是地址edx 里装的是 那个地址处的字节内容例如如果 buf[i] 是字符 '%',那执行后:
asmedx = 0x25 dl = 0x25
不是地址。
右边同理:
asmmov ecx, [ebp+var_15+5] ; ecx = i mov eax, [ebp+arg_0] ; eax = a1 add eax, ecx ; eax = &a1[i] movzx eax, byte ptr [eax] ; eax = a1[i]
最后这句执行完后:
eax 不再是地址语义eax 里已经变成了 a1[i] 这个字节值al 就是这个字节的低 8 位所以到这句:
asmcmp dl, al
比较的是:
cbuf[i] 和 a1[i]
也就是两个字符/字节值,比如:
c'%' 和 'f'
不是两个地址。
你可以把它理解成下面这个等价 C:
cunsigned char left = buf[i]; unsigned char right = a1[i]; if (left != right)
其中:
dl 对应 leftal 对应 right你之所以会觉得“好像是地址”,是因为前面确实有一段在算地址:
asmadd eax, edx ; eax = 地址
但是紧接着的:
asmmovzx edx, byte ptr [eax]
已经把“地址指向的内容”读出来了。
这是最常见的模式:
一行一行用“类型”标出来就是:
asmlea edx, [ebp+var_15] ; edx = char* (地址) mov eax, [ebp+var_15+5] ; eax = int (下标) add eax, edx ; eax = char* (buf+i 的地址) movzx edx, byte ptr [eax] ; edx = unsigned char 值 mov ecx, [ebp+var_15+5] ; ecx = int mov eax, [ebp+arg_0] ; eax = char* (a1 地址) add eax, ecx ; eax = char* (a1+i 的地址) movzx eax, byte ptr [eax] ; eax = unsigned char 值 cmp dl, al ; 比较两个字节值
所以真实语义就是:
cif (buf[i] != a1[i])
不是比较地址。
你要是愿意,我可以接着把 lea / mov / add / movzx / cmp / setnz 这一串总结成一个“逆向里最常见的数组取值模板”,以后你一眼就能认出来。