-
-
[分享][原创]记一次crack me逆向
-
发表于: 2025-9-11 15:24 521
-
首先运行一下,有两个输入框,猜测第二个输入框是第一个输入框内容的加密结果

查看基础信息,64位,.rsrc段可能被加密

在x64 dbg先f9运行,日志提示一个异常

由图分析得到原因是“把 0 当作函数指针去执行”,要找出是哪条指令把控制流引到了 0 地址(可以从堆栈分析,但不知道如何定位导致异常的代码),通过单步调试发现是一个initterm函数导致的异常,


该函数的作用是:从First地址到Last地址存储着一系列函数地址,该函数会调用所有函数地址不为空的函数,分析该函数的汇编代码,可得到注释一行中的rbx参数就对应反编译代码中的v3,然后再x64dbg中运行到第一次异常,可看到RBX=00000001401D19D0,说明是调用该地址存储的函数sub_140001030(该函数正好是整个函数数组的最后一个)导致的异常,分析一下该函数。
分析可得,第5行到第49行代码均与反调试相关,其中第48行代码就是出现异常的根源,随后我们在x64dbg中对这些代码进行NOP填充,也就是地址
140001066-1400011A0,然后再次步过initterm函数可看到没有异常出现。(这里应该看下反调试和jump 0函数是否有其他引用函数的并分析一下这些函数,这些甘薯大概率是程序主要逻辑或其他反调试手段)
继续f9,同样的是RIP为0的异常,继续步过,发现在winmian函数异常,然后通过调试继续定位到sub_140004B70函数,该函数会反调试并调用同样的sub_140006660()函数来触发异常,这次将该函数的其他逻辑NOP掉并编辑最后一条指令为ret。(在ida中看sub_140006660()函数的交叉引用,可以看到有多个函数调用,也可以逐个对这些函数调用sub_140006660()的代码NOP)(call qword ptr [func] 无论目标是否 noreturn,CPU 都会把 下一条指令地址 压栈)


将sub_140006660()函数补丁后发现程序不会报异常了,但是未运行程序关键逻辑便退出了,说明还是有反调试只不过没有异常报错。然后通过之前动态运行得到的字符串信息“12345678”定位到函数sub_140004B70,而且该函数就是上面第二次异常函数的父函数,该函数极有可能和程序主要逻辑相关
在ida中分析sub_140004B70函数可以看到有CDialog::DoModal((CDialog *)v33);主界面显示函数的调用,但是在x64dbg中对该代码下断点发现程序并没有运行到这里,通过不断的调试并把反调试后的跳转逻辑翻转使程序可以运行到CDialog::DoModal((CDialog *)v33);()(下面是需要修改代码的部分)

但之后发现运行到CDialog::DoModal函数程序还是退出,通过动调找到最后使程序退出的windows API CreateDialogIndirectParamA,然后使用以下prompt让AI分析一下原因:
1 2 3 | 我在分析一个具有反调试手段的样本时出现以下问题:该程序是一个GUI程序,在动态运行时可看到有GUI窗口,但在调试时发现该窗口没有弹出程序便结束了,我在ida中定位到程序的GUI初始化在函数sub_140004B70中,但该函数有很多的反调试手段,我通过打补丁使调试程序可以运行到sub_140004B70函数中的GUI初始化函数CDialog::DoModal((CDialog *)v33);,但是我发现程序步过这个函数便退出了。我通过动态调试发现是DoModal在“ if ( !(unsigned int)CWnd::CreateRunDlgIndirect((CWnd *)v1, v2, v13, v16) && !*((_DWORD *)v1 + 72) )”调用CWnd::CreateRunDlgIndirect函数,然后CreateRunDlgIndirect在 DlgIndirect = CWnd::CreateDlgIndirect(this, a2, a3, a4);中调用CreateDlgIndirect,然后CreateDlgIndirect调用sub_14001DDEC函数,最后 sub_14001DDEC调用DialogIndirectParamA = CreateDialogIndirectParamA(hInstance, lpTemplate, hWndParent, lpDialogFunc, dwInitParam);中的CreateDialogIndirectParamA导致程序退出的请你使用ida_mcp分析sub_140004B70及上面提到的所有子函数弄清问题的原因所在并给我让GUI窗口弹出来的解决方法 |
o3的分析结果
首先使用方案一,就是在x64dbg中的scyllaHide插件中勾选NtQueryInformationProcess函数这个选项,然后f9运行,可以看到程序的GUI窗口弹出,说明上面AI分析的结果正确。(经过后面进一步分析,他所说的hook NtqueryInformationprocess实际上是为了解决程序无法执行到CDialog::DoModal的问题而不是我说的调用DialogIndirectParamA程序退出的问题,但我已经在prompt说了”过打补丁使调试程序可以运行到sub_140004B70函数中的GUI初始化函数CDialog::DoModal((CDialog *)v33)“(也可能是我说的不清楚),这个解决方法实际上是”误打误撞",该问题的正确原因可看下面的拓展学习第三部分)
接着需要定位读取输入框内容并判断注册码是否正确的代码,首先在调试器中让程序运行起来,然后在所有模块搜索字符串“注册”,发现了“注册成功”字符串,则判断注册码是否正确的代码便在引用“注册成功”代码的上面,在IDA中发现是sub_140005FB0,其分析结果如下:

在x64dbg中查看this+48指向的函数不在程序空间内,而是virtualalloc申请的,所以只能在动态运行时分析校验函数,而在ida中分析函数功能更加高效,但有一个问题是该样本前面有一个反调试手段是通过scyllahide过反调试的,而scyllahide没有对IDA专门的版本,因此需要通过以下方式将dbg中的运行的样本转移到IDA:在x64dbg中使程序运行到上面的this+48指针有效然后detach该进程,然后在ida中attach即可。然后就是在ida中反编译校验的代码然后分析。
一开始对IDA中的校验主函数进行反编译,发现没有反应,最后发现是程序没有识别这个函数,需要在函数开头右键->Create funtion后再f5反编译,下面是分析结果。

后来发现IDA没有成功反编译这个函数,后面很长的一段汇编代码没有反编译,因此需要详细分析其汇编代码,于是到x64dbg中调试到对应位置进行分析,下面是分析结果:
阶段一:判断Name的长度是否为8、Serial的长度是否为32

阶段二:在栈帧 rbp-60 开头位置生成一张 62 字节长的字符表alp[62],内容顺序为“0-9” → “a-z” → “A-Z” ,"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

阶段三:由 8 字符 Name 算出 32 个索引并写到rsp+10的栈中,首先需要注意的是%63的汇编代码如下,这在后面的算法中大量出现(最后的ecx结果就是r10d的%63结果)
1 2 3 4 5 6 7 8 9 10 | 000001F634A00107 | B8 83200882 | mov eax,82082083 |000001F634A0010C | 41:F7EA | imul r10d |000001F634A00116 | 41:03D2 | add edx,r10d |000001F634A00119 | 44:894D 48 | mov dword ptr ss:[rbp+48],r9d |000001F634A0011D | C1FA 05 | sar edx,5 |000001F634A00120 | 8BC2 | mov eax,edx |000001F634A00122 | C1E8 1F | shr eax,1F |000001F634A00125 | 03D0 | add edx,eax |000001F634A00127 | 6BC2 3F | imul eax,edx,3F |000001F634A0012A | 2BC8 | sub ecx,eax | |
接下来就是分析32个索引是如何计算的,分析方法:找到mov dword ptr ss:[rsp+xx],ecx(xx>10),这个语句的作用就是将计算的索引结果放入栈中保存,然后网上找第二个imul语句,其操作数便为要%63的数字,然后再向上分析这个操作数是什么即可,下面便是对ind[0]-ind[6]的分析。


到这里便可以发现一个规律,由这个规律继续计算正好可以得到32个索引,后面便没有继续分析(后面验证这个方法是对的)
阶段四:对于上面算法得到的Name的serial索引数组ind[32],比较alp[index[i]]与serial[i]是否相同,如果32个都相同则校验成功。

根据以上对校验算法的分析,编写对应的c++注册机
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include<iostream>#include<string>using namespace std;int main() { cout << "请输入Name(必须8个字符):"; string N; cin >> N; string S = "zs0GBnYJDiVMFdSPH8PSJ3MVLZJYNUG0"; string alp = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; string ind; string res; for (int i = 0; i < 8; i++) { char a1 = 2 * N[i]; char a2 = 5 * N[7 - i]; ind+= (2 * N[i])%63; ind += (5 * N[7 - i])%63; ind += (2 * N[i] +5 * N[7 - i])%63; ind += (5 * N[i] + 2 * N[7 - i])%63; } for (int i = 0; i < 32; i++) { res += alp[ind[i]]; } cout <<"Name对应的serial为:"<< res; return 0;} |
经过测试,该注册机正确。
拓展学习
1、栈回溯
在遇到第一个异常点时,我是通过单步步过的方式一步一步缩小函数的范围最终定位到导致异常的点,但这样有点慢,之前阅读文章的时候了解过可以通过栈回溯来找到导致异常的代码(对应的免杀对抗技术有堆栈欺骗等),所以下面学习一下如何栈回溯定位异常代码
相关文章:cccK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6E0M7q4)9J5k6i4N6W2K9i4S2A6L8W2)9J5k6i4q4I4i4K6u0W2j5$3!0E0i4K6u0r3M7#2)9J5c8W2m8h3f1K6S2W2c8@1t1I4K9f1!0i4P5r3u0h3b7#2S2s2g2%4k6d9j5i4M7`.
d75K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6E0M7q4)9J5k6i4N6W2K9i4S2A6L8W2)9J5k6i4q4I4i4K6u0W2j5$3!0E0i4K6u0r3M7#2)9J5c8U0t1J5x3X3I4d9N6o6q4F1d9Y4k6a6y4s2V1^5f1V1u0T1e0W2W2F1N6f1q4Q4c8f1k6Q4b7V1y4Q4z5o6S2Q4c8e0S2Q4b7V1k6Q4z5e0W2Q4c8e0c8Q4b7U0S2Q4b7f1q4Q4c8e0k6Q4z5f1y4Q4z5o6W2Q4c8e0N6Q4z5o6u0Q4b7U0W2Q4c8e0g2Q4b7e0c8Q4z5p5c8Q4c8e0k6Q4z5f1c8Q4z5o6u0Q4c8f1k6Q4b7V1y4Q4z5p5y4Q4c8e0g2Q4b7V1g2Q4z5o6S2Q4c8e0g2Q4b7e0c8Q4z5f1q4Q4c8e0k6Q4b7U0u0Q4b7e0q4Q4c8e0N6Q4z5f1y4Q4z5p5u0Q4c8e0k6Q4z5o6N6Q4z5o6u0Q4c8f1k6Q4b7V1y4Q4z5o6V1`.
有了相关知识,直接动手操作,下面就是步进sub_1401ADC00的函数堆栈,解释如下:
1、一个标准的线程启动调用链:kernel32.BaseThreadInitThunk->ntdll.RtlUserThreadStart` -> 你的线程函数,因此下面会有这两个函数的堆栈信息
2、返回到 demo x64(1).0000000140006E28 自 demo x64(1).00000001401ADC00 这条注释是指返回地址为140006E28,调用的函数为sub_1401ADC00。
3、 返回到 demo x64(1).0000000140006F29 自 demo x64(1).0000000140007628 这个是start函数调用_security_init_cookie()函数的记录,因为函数调用结束后是不会清除在栈上的数据,所以依然能看到。在调用sub_1401ADC00上面的栈信息是__scrt_common_main_seh()调用其他子函数的遗留栈信息,_security_init_cookie()调用信息为红色只是因为栈中记录的返回地址在rsp范围内。

再次f9便会异常停止、下面便是异常停止时的栈信息,经过分析调用sub_1401Adc00上面的“返回到1401ADC2D”以及上面调用sub_140006660便是导致异常的函数链,其他两个返回地址记录都是无关的信息。

此外,调试的时候还发现一个现象,那就是这个样本的函数调用没有用到rbp,只用到了rsp,,这是现代编译器的优化现象,AI解释如下
还有就是发现在调试到异常后发现rsp变成0了,我还以为是异常会有标准流程使rsp变为0,分析之后发现是作者手动将rsp变为0的,应该是干扰栈回溯的。(下面是跳转到0导致异常的汇编代码)

想法:栈调用信息自动清楚,去除噪声快速定位异常
由于函数调用后不会自动清楚栈中的数据,这会导致在栈回溯时有无用的函数调用信息影响分析,如果使函数调用后自动清楚栈中数据这样栈中便是清晰的当前线程的函数调用链,便于分析(问了AI说没有现成的工具)
2、CRT初始化——**initterm**函数
第一个异常便是由该函数导致的,这个函数在ida中有名字说明是C++自带的代码,问一下AI了解一下相关知识:(解释的还是很清楚的)
initterm 函数是 Microsoft C/C++ 运行时库(CRT)的一部分,它的核心职责是在程序主函数(如 main 或 WinMain)执行*之前*,遍历并调用一个由函数指针组成的数组,从而完成一系列的全局/静态对象的构造和运行时初始化工作。
- 程序的入口点:不是 main()
当你编写一个 C++ 程序时,你从 main() 函数开始写。但实际上,操作系统加载器首先调用的并不是你的 main 函数。在 Windows 上,可执行文件的真正入口点通常是一个名为 mainCRTStartup, WinMainCRTStartup, wmainCRTStartup 或 DllMainCRTStartup 的函数(取决于程序是控制台程序、GUI程序还是DLL)。这个函数由 CRT 提供。
- CRT 初始化的步骤
在这个真正的入口点函数中,会按顺序完成以下工作:
初始化操作系统环境(如获取命令行参数、环境变量)。
设置 CRT 的内部状态(如随机数种子、异常处理机制)。
初始化全局和静态 C++ 对象。这就是
initterm大显身手的地方。调用用户的
main或WinMain函数。当
main函数返回后,执行清理工作(如调用全局对象的析构函数)。最后调用
exit或ExitProcess结束程序。initterm的具体作用
initterm 负责上述第 3 步。它的函数签名通常类似于:
1 | void initterm(_PVFV * pfbegin, _PVFV * pfend); |
其中 _PVFV 是一个预定义的函数指针类型,通常等价于 void (*)()(一个指向不接受参数也不返回值的函数的指针)。
它接受两个参数:
pfbegin: 指向函数指针数组起始位置的指针。pfend: 指向函数指针数组结束位置之后的下一个位置的指针。
它的工作流程非常简单:
- 从
pfbegin开始,遍历到pfend。 - 对于数组中每一个非空且有效的函数指针,执行该函数。
- 这些被调用的函数,就是各个编译单元(.cpp 文件)中全局/静态 C++ 对象的构造函数****。
3、研究为什么hook Ntqueryprocessinformation后程序就可以正常调用CreateDialogIndirectParamA初始化GUI
首先再问一下更聪明的gpt5
1 2 3 4 | 我在分析一个具有反调试手段的样本时出现以下问题:该程序是一个GUI程序,在动态运行时可看到有GUI窗口,但在调试时发现该窗口没有弹出程序便结束了,我在ida中定位到程序的GUI初始化在函数sub_140004B70中,但该函数有很多的反调试手段,我通过打补丁使调试程序可以运行到sub_140004B70函数中的GUI初始化函数CDialog::DoModal((CDialog *)v33);,但是我发现程序步过GUI初始化函数函数便退出了。我通过动态调试发现是CDialog::DoModa在“ if ( !(unsigned int)CWnd::CreateRunDlgIndirect((CWnd *)v1, v2, v13, v16) && !*((_DWORD *)v1 + 72) )”调用CWnd::CreateRunDlgIndirect函数,然后CreateRunDlgIndirect在 DlgIndirect = CWnd::CreateDlgIndirect(this, a2, a3, a4);中调用CreateDlgIndirect,然后CreateDlgIndirect调用sub_14001DDEC函数,最后 sub_14001DDEC调用DialogIndirectParamA = CreateDialogIndirectParamA(hInstance, lpTemplate, hWndParent, lpDialogFunc, dwInitParam);中的CreateDialogIndirectParamA导致程序退出的此外,很奇怪的一个现象是如果我如果在上面的基础上在x64dbg中对Ntqueryinformationprocess进行反反调试则CreateDialogIndirectParamA会正常运行并弹出窗口,但我不对该函数进行反反调试则步过CreateDialogIndirectParamA这个函数程序会立即退出,请你详细分析这个的原因请你使用ida_mcp分析sub_140004B70及上面提到的所有子函数弄清问题的原因所在并给我让GUI窗口弹出来的解决方法 |
gpt 5分析结果



在这个分析中,上面的反调试点三便是“hook Ntqueryprocessinformation后程序就可以正常调用CreateDialogIndirectParamA初始化GUI“的原因,即在调用CreateDialogIndirectParamA后,程序还会调用sub_140005BD0()这个函数初始化GUI,在该函数中有CheckRemoteDebuggerPresent()来进行反调试,而该函数实际上是内部调用NtQueryInformationProcess查询ProcessDebugPort值进行反调试,因此在x64dbg中hook NtQueryInformationProcess过掉CheckRemoteDebuggerPresent反调试。
此外,继续分析AI的分析过程,发现其找到CreateDialogIndirectParamA是通过查找ExitProccess(0)的引用函数找到的,而我在prompt也说了”CreateDialogIndirectParamA导致程序退出的“,AI应该是根据这一句prompt推断的。当然这也是一个调试经验:当调试程序时发现程序正常退出,可在ExitProcess下断点进一步查明其退出原因
然后又问了调用CreateDialogIndirectParamA后程序控制流如何转移到sub_140005BD0()

下面是AfxDlgProc的分析,可以看到CDialog::OnInitDialog被重写了。

4、IDA未成功反编译伪代码原因研究(待完成)
当然还是问一下gpt 5,下面是prompt:
1 2 3 4 5 | 在 ”校验函数汇编代码.txt“是一个校验输入的Name和Serial是否正确的汇编代码,该汇编代码为x64dbg中的反汇编结果,该算法的两个参数分别为Name和Serial字符串,然后我在ida中对该汇编代码进行反编译发现其反编译结果如下:“IDA中的反编译结果,这里不展示”很明显后面大部分的汇编代码都没有反编译,我发现在ida中后面的很多机器码都没有成功翻译为汇编代码,而是大量的db数据“IDA中的反汇编结果,包括正常反汇编的和少量db部分”请你分析上面的信息找到ida没有成功反编译函数的原因并给出解决方法 |
AI分析结果
感觉这个分析是正确的,但是让AI教我如何成功反编译这个函数,按AI分析的结果弄了很长时间没弄成功,索性先放一下这个问题,先去了解学习下IDA反编译的基础知识再弄。
25年腾讯游戏安全初赛的一个write up,里面也有导出算法的汇编代码然后在ida分析的部分,但是很简略:3c8K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4q4X3M7X3!0K6N6q4)9J5k6h3y4G2L8g2)9J5c8Y4m8G2M7%4c8K6i4K6u0r3j5%4c8X3i4K6u0r3i4K6t1#2c8e0S2Q4x3U0f1^5y4g2)9J5y4f1u0q4i4K6t1#2c8e0S2Q4x3U0g2m8c8g2)9J5y4f1q4r3i4K6t1#2c8e0k6Q4x3U0g2n7z5q4)9J5y4f1t1^5i4K6t1#2c8e0k6Q4x3U0f1^5z5q4)9J5y4e0S2r3i4K6t1#2c8e0g2Q4x3U0g2m8c8g2)9J5y4e0R3&6i4K6t1#2c8e0g2Q4x3U0f1^5y4g2)9J5y4f1p5^5i4K6t1#2c8e0N6Q4x3U0g2m8b7W2)9J5y4e0W2q4i4K6t1#2c8e0S2Q4x3U0g2n7y4g2)9J5y4e0W2n7i4K6g2X3i4K6t1#2c8e0g2Q4x3U0g2m8c8g2)9J5y4f1p5J5i4K6t1#2c8e0k6Q4x3U0f1^5z5q4)9J5y4f1t1%4i4K6t1#2c8e0N6Q4x3U0g2m8b7W2)9J5y4f1q4r3i4K6t1#2c8e0g2Q4x3U0g2m8c8g2)9J5y4e0R3&6i4K6t1#2c8e0g2Q4x3U0f1^5y4g2)9J5y4f1p5^5i4K6g2X3x3U0l9J5y4g2)9J5c8R3`.`.