这道逆向题的flag是flag{d07abccf8a410cb37a},后面6位也是异或加密只是没有...

تم الإنشاء في: ٢٣ مارس ٢٠٢٦

تمت الإجابة باستخدام GPT-5.4 Thinking بواسطة Chat01

سؤال

jocker.exe

这道逆向题的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是传入参数啊,怎么回事

الفكرة

我先按调用约定把这段汇编拆一下,再结合可执行文件看 sub_40159A 的实际函数签名,重点确认它是不是 thiscall/优化后的伪多参数显示问题。

Thought for 6s

الفكرة

Explaining decompiler behavior and calling conventions

The decompiler shows many arguments because it's working through encrypted self-modifying code, leading it to incorrectly interpret stack accesses. In reality, the function only has one argument because the first is passed via the stack (not using ECX). With calling conventions like cdecl, ECX is just a scratch register and not used for argument passing. The mangling __Z7finallyPc indicates a plain function with one argument.

Explaining function behavior and flaws

The ecx register in the main prologue at 401726 is not relevant; it just preserves the original stack for alignment. In the finally(char* s) function, ecx is used temporarily to store rand()'s return. The function essentially compares random values to characters in s and key, and if the random value is 0 or 1, mismatches are checked accordingly. If the random value is always different from 0 or 1, it prints “wrong.”

Thought for 1m 9s

الإجابة

不是 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 前只有:

asm
lea eax, [ebp+Destination] mov [esp], eax call sub_40159A

这说明调用者把参数放到栈顶 esp,也就是:

c
sub_40159A(Destination);

如果真是 thiscall,常见形式应该是类似:

asm
lea ecx, [obj] call xxx

但这里根本没有。


再看解密后的 sub_40159A

这段解密后其实是:

asm
40159a: 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 位栈传参函数。


为什么反编译会显示“很多参数”?

因为这段函数在静态状态下前半截是加密字节。反编译器会发生几种典型误判:

  1. 错误切分函数边界
    把中间某个位置误认成函数入口。

  2. 把垃圾指令里的栈访问误识别成参数
    比如 [ebp+0Ch] [ebp+10h] [ebp+14h] 这种本来只是错误反汇编产物,却被伪代码当成“第 2、第 3、第 4 个参数”。

  3. 错误推断调用约定
    有时会被推成 stdcall / fastcall / 甚至奇怪的自定义签名。

你这个文件还带了 C++ 符号名,40159A 对应的是:

c
__Z7finallyPc

解出来就是:

c
finally(char *)

也正好说明它就一个 char * 参数。


你问“我看汇编却没有看到 ecx,应该是 ecx 是传入参数啊,怎么回事”

结论就是:

不是。这里的 ecx 不是传参寄存器,只是普通临时寄存器。

sub_40159A 里,ecx 后面这几句:

asm
4015cd: mov ecx, eax ... 4015ed: sub ecx, eax

只是拿来算 rand() % 100 的中间值,和参数无关。


这函数本质在干嘛

解密后逻辑大概是:

c
void 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]) 的结果只有 01
  • r = rand()%1000~99

所以除非 r 恰好等于 01,否则永远过不了。
这也印证了你说的“错题”。


一句话总结

你这里不是 “ecx 隐式传参没显示出来”,而是:

  • sub_40159A 实际只有 一个 char * 参数
  • 参数通过 [esp] / [ebp+8] 栈传递
  • 反编译显示很多参数,是因为这段函数在静态状态下被 XOR 加密,反编译器误分析了

如果你愿意,我可以直接把 sub_401500sub_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,为什么,没有给他赋值啊

Python

python
import 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)

الفكرة

已经定位到原因了:v3[5] 不是“真有这个数组元素”,而是反编译器把 [ebp-0x10] 这个独立局部变量错并进了 v3。所以你动态里总看到它是 0,是因为那其实是循环下标/计数变量,被明确初始化成 0 了。

Thought for 55s

الإجابة

对,32 位里 __cdecl__fastcall 传参方式确实不一样。

最常见的区别可以这么记:

  • __cdecl:参数都走栈,从右到左压栈,调用者清栈
  • __stdcall:参数也走栈,从右到左压栈,被调者清栈
  • __fastcall:前两个参数常用 ecxedx 传,剩下再走栈
  • thiscall:C++ 成员函数常把 thisecx

但你这题里,sub_40159A 不是 fastcall,也不是 ecx 传参,而是普通栈传参。调用点很明显:

asm
lea eax, [ebp+Destination] mov [esp], eax call sub_40159A

这就是:

c
sub_40159A(Destination);

你问的核心:为什么 v3[5] 总是 0

因为这个 v3[5] 不是一个真正被单独赋值的数组元素,而是反编译器把栈上的别的局部变量错认成了 v3 的一部分。

我把这段真实汇编还原后,关键部分是这样的:

asm
40159a: 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]

正确的栈布局其实更像这样

c
char buf[5]; // [ebp-15] ~ [ebp-11] 存 "%tp&:" int i; // [ebp-10] int v4; // [ebp-0c]

而反编译器给你硬拼成了:

c
char v3[9]; // [ebp-15] ~ [ebp-0d] int v4; // [ebp-0c]

这样一来:

  • v3[0..4] 对应 "%tp&:"
  • v3[5] 实际上已经踩到 [ebp-10]
  • 也就是 循环变量 i

所以你看到:

c
v3[v3[5]] *(v3[5] + a1)

本质上其实是:

c
buf[i] a1[i]

为什么你动态调试时 v3[5] 总是 0

因为它其实对应的是:

asm
mov [ebp-10h], 0

也就是循环下标 i = 0 的初始化。

刚进比较逻辑时它当然先是 0,后面会在循环里递增。如果你断在刚开始的位置,看到它总是 0 很正常。


这段伪代码里还有一个更大的坑

你看到的这个:

c
strcpy(v3, "%tp&:"); v3[6] = 0; *&v3[7] = 0;

也不是真实源码逻辑,至少不是可靠还原。真实机器码里没有这串 strcpy,而是直接逐字节写入:

asm
mov 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 代码,而是 反编译器因为前面函数被异或加密过,局部变量恢复错位了


这函数更接近下面这样

c
int __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]) 只能是 01
  • rand()%1000~99

所以除非随机数正好是 01,否则条件根本不可能按出题人预期工作。这也正好对应你前面说的“错题”。


结论

你这里的 v3[5] 之所以总是 0,不是因为“没赋值却莫名其妙变 0”,而是:

  • 反编译器把局部变量边界识别错了
  • v3[5] 实际对应 [ebp-10]
  • [ebp-10] 实际是循环变量 i
  • 它在函数里被明确初始化为 0

所以真正应该读成:

c
buf[i] a1[i]

不是:

c
v3[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 )的

الفكرة

Thought for a few seconds

الإجابة

可以,这段正好能看出反编译器是怎么“机械地”把汇编拼成那句伪代码的。

先说结论:
这段汇编真正表达的是:

c
if ( (buf[i] != a1[i]) == v4 )

而反编译器之所以给你还原成

c
if ( (v3[v3[5]] != *(v3[5] + a1)) == v4 )

是因为它把:

  • buf 识别成了 v3
  • i 这个独立局部变量,错识别成了 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 = -15h
  • var_15 + 5 = -10h

所以:

asm
[ebp+var_15+5] = [ebp-10h]

这其实是循环变量 i 所在的位置。


逐行分析


1.

asm
.text:00401601 lea edx, [ebp+var_15]

lea 取地址,不取内容。

所以这里:

c
edx = &v3[0];

或者更准确点:

c
edx = &buf[0];

即:

  • edx 现在保存局部数组首地址

2.

asm
.text:00401604 mov eax, dword ptr [ebp+var_15+5]

[ebp-10h] 的 4 字节值读到 eax

反编译器看到:

  • 这地址位于 v3 后面一点
  • 就误以为它是 v3[5]

于是还原成:

c
eax = v3[5];

但真实更像:

c
eax = i;

3.

asm
.text:00401607 add eax, edx

现在:

  • edx = &buf[0]
  • eax = i

所以相加后:

c
eax = &buf[0] + i;

也就是:

c
eax = &buf[i];

如果沿着反编译器的错误理解走,就是:

c
eax = &v3[0] + v3[5];

4.

asm
.text:00401609 movzx edx, byte ptr [eax]

eax 指向的位置取 1 字节,零扩展到 edx

也就是:

c
edx = (unsigned char)buf[i];

错误视角下被还原成:

c
edx = (unsigned char)v3[v3[5]];

所以到这里,左半边来源就是:

c
v3[v3[5]]

5.

asm
.text:0040160C mov ecx, dword ptr [ebp+var_15+5]

再次取 [ebp-10h]

真实是:

c
ecx = i;

反编译器错误理解成:

c
ecx = v3[5];

6.

asm
.text:0040160F mov eax, [ebp+arg_0]

取函数参数 a1

c
eax = a1;

7.

asm
.text:00401612 add eax, ecx

a1 指针加上偏移。

真实是:

c
eax = a1 + i;

错误视角下:

c
eax = a1 + v3[5];

8.

asm
.text:00401614 movzx eax, byte ptr [eax]

a1[i] 这个字节。

真实是:

c
eax = (unsigned char)a1[i];

错误视角下就是:

c
eax = (unsigned char)*(v3[5] + a1);

这就是伪代码右半边:

c
*(v3[5] + a1)

它其实只是另一种写法:

c
a1[v3[5]]

也就是本质上的 a1[i]


9.

asm
.text:00401617 cmp dl, al

比较两个字节:

  • dl = buf[i]
  • al = a1[i]

即:

c
buf[i] ?= a1[i]

10.

asm
.text:00401619 setnz al

如果不相等,则 al = 1,否则 al = 0

于是:

c
al = (buf[i] != a1[i]);

错误视角下就是:

c
al = (v3[v3[5]] != *(v3[5] + a1));

11.

asm
.text:0040161C movzx eax, al

al 扩展成整型 eax

c
eax = (unsigned int)((buf[i] != a1[i]) ? 1 : 0);

也就是:

c
eax = (buf[i] != a1[i]);

12.

asm
.text:0040161F cmp eax, [ebp+var_C]

拿布尔结果和 v4 比较:

c
if ( (buf[i] != a1[i]) == v4 )

13.

asm
.text:00401622 jz short loc_401632

如果相等则跳转。

cmp eax, [ebp+var_C]jz 的意思就是:

c
if ( eax == v4 ) goto loc_401632;

也就是:

c
if ( (buf[i] != a1[i]) == v4 )

所以反编译器是怎么一步步拼成那句的

它大概按下面的机械过程生成:


左边取字节

asm
lea edx, [ebp+var_15] mov eax, [ebp+var_15+5] add eax, edx movzx edx, byte ptr [eax]

被理解成:

c
edx = v3[v3[5]];

因为它认为:

  • &v3[0] 是数组首地址
  • [ebp+var_15+5]v3[5]
  • 首地址 + 下标,再解引用,就是 v3[v3[5]]

右边取字节

asm
mov ecx, [ebp+var_15+5] mov eax, [ebp+arg_0] add eax, ecx movzx eax, byte ptr [eax]

被理解成:

c
eax = *(v3[5] + a1);

因为它认为:

  • [ebp+var_15+5]v3[5]
  • a1 + v3[5] 再取字节,就是 *(v3[5] + a1)

比较并转成布尔值

asm
cmp dl, al setnz al movzx eax, al

变成:

c
eax = (v3[v3[5]] != *(v3[5] + a1));

再和 v4 比较

asm
cmp eax, [ebp+var_C] jz ...

变成:

c
if ( (v3[v3[5]] != *(v3[5] + a1)) == v4 )

为什么这个伪代码看着怪

因为反编译器错把:

c
int i;

并进了数组 v3 的地址范围里,导致:

c
i

被显示成:

c
v3[5]

所以真正人类应当读成:

c
if ( (buf[i] != a1[i]) == v4 )

而不是:

c
if ( (v3[v3[5]] != *(v3[5] + a1)) == v4 )

最后给你一个“对照版”

汇编真实语义

c
char *buf = &v3[0]; int i = *(int *)(ebp-10h); left = buf[i]; right = a1[i]; cond = (left != right); if (cond == v4)

反编译器误还原

c
left = v3[v3[5]]; right = *(v3[5] + a1); cond = (left != right); if (cond == v4)

人类修正版

c
if ( (buf[i] != a1[i]) == v4 )

如果你愿意,我下一条可以继续把这整个 sub_40159A 直接整理成“错误反编译版 / 正确C版 / 对应汇编版”三栏对照,你看这种题会更快。

سؤال

.text:00401617 cmp dl, al这一行,dl和al装载的不是地址值吗,怎么能直接比较地址

سؤال

.text:00401617 cmp dl, al这一行,dl和al装载的不是地址值吗,怎么能直接比较地址

الفكرة

Thought for 4s

الإجابة

不是在比较地址,cmp dl, al 比较的是 两个字节的值

关键在前两条 movzx

asm
.text:00401609 movzx edx, byte ptr [eax] ... .text:00401614 movzx eax, byte ptr [eax] .text:00401617 cmp dl, al

你把流程拆开看就清楚了。

先看左边这段:

asm
lea 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]

注意最后这一句:

asm
movzx edx, byte ptr [eax]

含义不是“把地址放进 edx”,而是:

  • 先把 eax 当成地址
  • 去读 eax 指向内存里的 1 个字节
  • 再零扩展放进 edx

所以执行完以后:

  • eax 之前是地址
  • edx 里装的是 那个地址处的字节内容

例如如果 buf[i] 是字符 '%',那执行后:

asm
edx = 0x25 dl = 0x25

不是地址。


右边同理:

asm
mov 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 位

所以到这句:

asm
cmp dl, al

比较的是:

c
buf[i] 和 a1[i]

也就是两个字符/字节值,比如:

c
'%''f'

不是两个地址。


你可以把它理解成下面这个等价 C:

c
unsigned char left = buf[i]; unsigned char right = a1[i]; if (left != right)

其中:

  • dl 对应 left
  • al 对应 right

你之所以会觉得“好像是地址”,是因为前面确实有一段在算地址:

asm
add eax, edx ; eax = 地址

但是紧接着的:

asm
movzx edx, byte ptr [eax]

已经把“地址指向的内容”读出来了。

这是最常见的模式:

  1. 先算地址
  2. 再解引用
  3. 比较解引用后的值

一行一行用“类型”标出来就是:

asm
lea 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 ; 比较两个字节值

所以真实语义就是:

c
if (buf[i] != a1[i])

不是比较地址。

你要是愿意,我可以接着把 lea / mov / add / movzx / cmp / setnz 这一串总结成一个“逆向里最常见的数组取值模板”,以后你一眼就能认出来。

مشاركة هذا السؤال والجواب