吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 51|回复: 2
收起左侧

[原创] 逆向实战:从 argc 伪装到静态注册机——一个无源码 CrackMe 的完整算法还原

[复制链接]
getjob 发表于 2026-10-1 11:52
本帖最后由 getjob 于 2026-10-1 12:10 编辑

逆向实战:从 argc 伪装到静态注册机——一个无源码 CrackMe 的完整算法还原一、环境与样本信息环境:Windows 10 x64、x64dbg(TitanEngine)、IDA Free、VS Community 2026样本:crack.exe,控制台程序,命令行参数形式 crack.exe [text] [key]来源:crackmes.one 难度 1.0 样本特征:无壳、无 PDB、无符号,使用 argc 伪装的“固定随机数”样本 SHA256:4b6ee3f658260683ec2ae4cc129f7adcceafe630f36860c1908425430e4a5386二、如何定位 main 函数(标准化流程)方法一:回显字符串搜索(你实际使用的方法)这是最直观、最不容易迷路的方法:1. 在命令行运行程序,观察输出。程序会打印 Usage: %s [text] [key for the text]、Good job、bad kitty。file:///C:/Users/11/AppData/Local/Temp/ksohtml22852/wps1.jpg 2. 在 x64dbg 里按 Ctrl+F 或右键 → 搜索 → 当前模块 → 字符串。file:///C:/Users/11/AppData/Local/Temp/ksohtml22852/wps2.jpg 3. 找到 Usage 或 Good job,双击跳转到引用位置。4. 引用位置所在的函数,就是 main。5. 这个方法的优势:不需要跟 CRT 启动代码绕圈,直接落到关键逻辑。方法二:从入口点单步跟到 main(x64dbg 盲跟,熟悉的可能很快)1. x64dbg 加载程序后,按 Alt+F9 运行到用户代码,停在 PE 入口点。2. 在入口点按 F2 下断,F9 断下。3. 按 F8 单步,遇到 exe 内部 call 按 F7 进入。4. 跟到 __scrt_common_main → __scrt_common_main_seh → invoke_main。5. 看到 RCX/RDX/R8 被设置为 argc/argv/envp 后 call 的地址,就是 main。方法三:IDA 辅助定位(推荐)1. IDA 打开 exe,等待自动分析完成。2. IDA 有 FLIRT 签名库,能自动识别 mainCRTStartup 并标出 main。3. 在 IDA 里记下 main 的地址。file:///C:/Users/11/AppData/Local/Temp/ksohtml22852/wps3.jpg 4. x64dbg 里按 Ctrl+G,输入这个地址(注意基址重定位),直接跳过去下断。file:///C:/Users/11/AppData/Local/Temp/ksohtml22852/wps4.jpg 复制基址,双击没用不知道为什么file:///C:/Users/11/AppData/Local/Temp/ksohtml22852/wps5.jpg Ctrl去做跳转file:///C:/Users/11/AppData/Local/Temp/ksohtml22852/wps6.jpg file:///C:/Users/11/AppData/Local/Temp/ksohtml22852/wps7.jpg  三种方法对比
方法
优点
缺点
字符串搜索
直接、快速、不易迷路
需要程序有可识别的输出字符串
入口点盲跟
不依赖任何字符串
慢,容易在 CRT 里跟丢
IDA 辅助
最省时间
需要两个工具配合
IDA伪代码(快捷键F5)file:///C:/Users/11/AppData/Local/Temp/ksohtml22852/wps8.jpg 三、如何判断一个栈位置是 argv不是靠猜,是有固定特征x64 Windows 调用约定:main(int argc, char** argv, char** envp)RCX = argc 一共有多少个参数(计数) argv[0]:程序名字/路径RDX = argv 参数字符串数组R8  = envp 系统环境变量字符串数组,以NULL结尾函数开头一定会有保存参数的指令:mov qword ptr ss:[rsp+10], rdx    ; 保存 argvmov dword ptr ss:[rsp+8], ecx     ; 保存 argc之后在函数体里:argc 通常存在 [rsp+70] 或 [rsp+20] 这种小偏移位置。argv 通常存在 [rsp+78] 或 [rsp+40] 这种偏移位置。 识别 argv 索引的固定模式mov rcx, qword ptr ss:[rsp+78]    ; rcx = argv 基地址mov rcx, qword ptr ds:[rcx+rax]   ; rcx = argv[n],rax 是 8 的倍数因为 argv 是 char**,每个元素是指针(8 字节)。所以:[argv + 0] = argv[0](程序路径)[argv + 8] = argv[1](第一个参数)[argv + 16] = argv[2](第二个参数)识别字符串字符访问mov rcx, qword ptr ds:[rdi+rcx]   ; rcx = argv[n] 的字符串地址movsx ecx, byte ptr ds:[rcx+rdx]  ; ecx = argv[n][m]一句话总结看它是不是从 [rsp+78] 或类似位置取出一个指针,再用 8 的倍数偏移去取下一个指针,最后用 movsx/movzx 取字符。 满足这个链条,就是 argv。file:///C:/Users/11/AppData/Local/Temp/ksohtml22852/wps9.jpg file:///C:/Users/11/AppData/Local/Temp/ksohtml22852/wps10.jpg  四、test eax, eax 与 cmp eax, eax 的区别你之前的误解:我以为 test eax, eax 是计算 eax - eax 是否等于 0。纠正:test eax, eax //执行的是 eax & eax,即按位与自身。结果等于 eax 本身,但结果不保存,只根据结果设置标志位。如果 eax = 0,//则 ZF = 1。如果 eax != 0,//则 ZF = 0。test eax, eax 的语义就是:判断 eax 是否等于 0。对比:cmp eax, eax //执行的是 eax - eax,结果恒为 0,ZF 恒为 1。test eax, eax //执行的是 eax & eax,结果取决于 eax 本身。所以:test eax, eaxje  xxx      ; //如果 eax == 0,跳转jne xxx      ; //如果 eax != 0,跳转五、汇编注释(AI纠正)
; ===== main 函数开始 =====00007FF632F710A0 | 48:895424 10 | mov qword ptr ss:[rsp+10], rdx; 你的理解:保存 argv; 正确:保存第二个参数 argv(char**)到栈上; |*| x64 调用约定:RCX=argc, RDX=argv, R8=envp 00007FF632F710A5 | 894C24 08 | mov dword ptr ss:[rsp+8], ecx; 你的理解:保存 argc; 正确:保存第一个参数 argc(int)到栈上 00007FF632F710A9 | 57 | push rdi; 你的理解:(无); 正确:保存 RDI(非易失寄存器); |*| RDI、RBX、RBP、R12-R15 属于非易失寄存器,函数返回前必须恢复原值。RAX、RCX、RDX、R8-R11 属于易失寄存器,可以随意修改。 00007FF632F710AA | 48:83EC 60 | sub rsp, 60; 你的理解:存一下 rsp; 正确:开辟 0x60 字节栈空间; |*| 汇编里的数字默认都是十六进制,0x60 = 十进制 96file:///C:/Users/11/AppData/Local/Temp/ksohtml22852/wps11.jpg ; ----- 栈保护 cookie(可跳过) -----00007FF632F710AE | 48:8B05 CB0F0200 | mov rax, qword ptr ds:[7FF632F92080]; 你的理解:把全局变量放到 rax; 正确:读取 __security_cookie; |*| 编译器安全机制,如果看到 mov rax, [全局地址] 然后 xor rax, rsp,直接判断为栈保护,不用深入分析。 00007FF632F710B5 | 48:33C4 | xor rax, rsp; 你的理解:异或 rsp 干嘛; 正确:生成栈保护值,与 rsp 异或 00007FF632F710B8 | 48:894424 50 | mov qword ptr ss:[rsp+50], rax; 你的理解:(无); 正确:保存栈保护值到栈上 ; ----- 初始化硬编码字节数组(14 字节)-----00007FF632F710BD | C64424 30 18 | mov byte ptr ss:[rsp+30], 18; 数组[0] = 0x1800007FF632F710C2 | C64424 31 1A | mov byte ptr ss:[rsp+31], 1A; 数组[1] = 0x1A00007FF632F710C7 | C64424 32 0B | mov byte ptr ss:[rsp+32], 0B; 数组[2] = 0x0B00007FF632F710CC | C64424 33 5F | mov byte ptr ss:[rsp+33], 5F; 数组[3] = 0x5F00007FF632F710D1 | C64424 34 1D | mov byte ptr ss:[rsp+34], 1D; 数组[4] = 0x1D ← 后面会用到这个00007FF632F710D6 | C64424 35 1E | mov byte ptr ss:[rsp+35], 1E; 数组[5] = 0x1E00007FF632F710DB | C64424 36 16 | mov byte ptr ss:[rsp+36], 16; 数组[6] = 0x1600007FF632F710E0 | C64424 37 0B | mov byte ptr ss:[rsp+37], 0B; 数组[7] = 0x0B00007FF632F710E5 | C64424 38 1A | mov byte ptr ss:[rsp+38], 1A; 数组[8] = 0x1A00007FF632F710EA | C64424 39 1B | mov byte ptr ss:[rsp+39], 1B; 数组[9] = 0x1B00007FF632F710EF | C64424 3A 5F | mov byte ptr ss:[rsp+3A], 5F; 数组[10] = 0x5F00007FF632F710F4 | C64424 3B 13 | mov byte ptr ss:[rsp+3B], 13; 数组[11] = 0x1300007FF632F710F9 | C64424 3C 10 | mov byte ptr ss:[rsp+3C], 10; 数组[12] = 0x1000007FF632F710FE | C64424 3D 13 | mov byte ptr ss:[rsp+3D], 13; 数组[13] = 0x13; 补充:这些是硬编码的加密字节,只有 [4] 参与了密钥计算。; |*| 看到连续的 mov byte ptr [rsp+xx], imm 就是在栈上初始化字节数组。file:///C:/Users/11/AppData/Local/Temp/ksohtml22852/wps12.jpg ; ----- 清零局部缓冲区 -----00007FF632F71103 | 48:8D4424 40 | lea rax, qword ptr ss:[rsp+40]; 你的理解:一个参数,先认为 rsp+40=argv[1] 参数1; 正确:取局部缓冲区地址(用于清零),不是 argv[1]; 补充:argv[1] 在 [rsp+78] 附近,后面会从那里取。; |*| 判断是否是指针参数:看它是从 [rsp+78] 取出 argv 基址,再加 8/16 的倍数偏移。不要看到 rsp+xx 就以为是 argv。 00007FF632F71108 | 48:8BF8 | mov rdi, rax; 你的理解:rdi:"fJ檶,~"; 正确:RDI = 缓冲区地址 00007FF632F7110B | 33C0 | xor eax, eax; eax = 0 00007FF632F7110D | B9 10000000 | mov ecx, 10; ecx = 16 00007FF632F71112 | F3:AA | rep stosb; 你的理解:不知道是什么,起到关键作用了么; 正确:把从 [rsp+40] 开始的 16 字节清零; |*| rep stosb = 重复执行 stosb,用 AL 填充 [RDI],RCX 是次数。遇到清空缓冲区不需要细看。file:///C:/Users/11/AppData/Local/Temp/ksohtml22852/wps13.jpg ; ----- 计算 v5 = argc*42 + argc/3 -----00007FF632F71114 | 6B4424 70 2A | imul eax, dword ptr ss:[rsp+70], 2A; 你的理解:eax = 参数*2a; 正确:eax = argc * 0x2A = 3 * 42 = 126 = 0x7E; 补充:此处 [rsp+70] 是 argc,值为 3。; |*| 三操作数 imul 指令格式:imul 目标, 源, 立即数。0x2A = 42。 00007FF632F71119 | 894424 20 | mov dword ptr ss:[rsp+20], eax; 你的理解:把 eax 的值 7e 放给另外一个参数 argv[2]; 正确:暂存 0x7E 到 [rsp+20]; |*| [rsp+20] 是临时栈变量,不是 argv[2]。argv[2] 在 [rsp+78]+16 的位置。 00007FF632F7111D | 8B4424 70 | mov eax, dword ptr ss:[rsp+70]; 你的理解:eax=rsp+70; 正确:eax = argc = 3 00007FF632F71121 | 99 | cdq; 你的理解:不知道是什么,rax变成了3,cdq是简写么; 正确:cdq 把 EAX 的符号位扩展到 EDX,为有符号除法做准备; |*| cdq = Convert Doubleword to Quadword(EDX:EAX 合并为被除数)。 00007FF632F71122 | B9 03000000 | mov ecx, 3; ecx = 3 00007FF632F71127 | F7F9 | idiv ecx; 你的理解:应该是 rax=ecx/eax=1,因为步过变1; 正确:eax = eax / ecx = 3 / 3 = 1,余数在 edx; |*| idiv 是有符号除法,被除数 EDX:EAX,除数 ECX。 00007FF632F71129 | 8B4C24 20 | mov ecx, dword ptr ss:[rsp+20]; 你的理解:ecx=rsp+20; 正确:ecx = 暂存的 0x7E 00007FF632F7112D | 03C8 | add ecx, eax; 你的理解:ecx=ecx+eax=7E+1=7F; 正确:ecx = 0x7E + 1 = 0x7F 00007FF632F7112F | 8BC1 | mov eax, ecx; eax = 0x7F 00007FF632F71131 | 894424 24 | mov dword ptr ss:[rsp+24], eax; 你的理解:rsp+24=7F; 正确:v5 = 0x7F,存入 [rsp+24]; |*| 核心变量 v5 = argc * 42 + argc / 3。当 argc=3 时,v5 固定为 0x7F。file:///C:/Users/11/AppData/Local/Temp/ksohtml22852/wps14.jpg ; ----- 检查 argc 是否为 3 -----00007FF632F71135 | 837C24 70 03 | cmp dword ptr ss:[rsp+70], 3; 你的理解:rsp+70 看了一下=3,这个就是 3-3; 正确:比较 argc 和 3; |*| cmp 不修改操作数,只影响标志位。ZF=1 表示相等。 00007FF632F7113A | 74 28 | je crack.7FF632F71164; 你的理解:算的值=0,zf 变 1,跳转 164; 正确:如果 argc == 3,则跳转到 1164 正常执行 ; ----- 如果 argc != 3,打印 Usage 并退出 -----00007FF632F7113C | B8 08000000 | mov eax, 8; eax = 800007FF632F71141 | 48:6BC0 00 | imul rax, rax, 0; rax = 000007FF632F71145 | 48:8B4C24 78 | mov rcx, qword ptr ss:[rsp+78]; rcx = argv 基地址00007FF632F7114A | 48:8B1401 | mov rdx, qword ptr ds:[rcx+rax]; rdx = argv[0](程序路径)00007FF632F7114E | 48:8D0D AB0E0200 | lea rcx, qword ptr ds:[7FF632F92000]; rcx = "Usage: %s [text] [key for the text]"00007FF632F71155 | E8 46010000 | call crack.7FF632F712A0; 调用 printf00007FF632F7115A | B8 01000000 | mov eax, 1; 返回值 = 100007FF632F7115F | E9 BA000000 | jmp crack.7FF632F7121E; 跳到函数结尾file:///C:/Users/11/AppData/Local/Temp/ksohtml22852/wps15.jpg ; ----- argc == 3,正常执行 -----00007FF632F71164 | 8B4424 70 | mov eax, dword ptr ss:[rsp+70]; eax = argc = 300007FF632F71168 | 8D04C5 48000000 | lea eax, qword ptr ds:[rax*8+48]; 你的理解:eax=rax*8+48=72, 算错了,16进制 3*8=24+48=60; 正确:eax = 3*8 + 0x48 = 0x18 + 0x48 = 0x60; |*| 十六进制计算的陷阱:3*8=0x18,加上0x48等于0x60,十进制是96。不要算成十进制然后直接加。00007FF632F7116F | 8BC8 | mov ecx, eax; ecx = 0x6000007FF632F71171 | E8 8AFEFFFF | call crack.7FF632F71000; 你的理解:跟进去看一下吧,进去没看懂,出来了,但是好像不影响 test eax,eax; 正确:调用检查函数 sub_7FF632F71000,该函数不修改 EAX,直接返回; 补充:这个函数内部比较传入值和 0x52D1,不相等就返回,不会改变 EAX。 00007FF632F71176 | 85C0 | test eax, eax; 你的理解:进去没看懂,出来了,但是好像不影响 teat eax,eax 值是固定的 0,zf=1,下面必跳转; 正确:EAX 仍然是 0x60(非零),所以 ZF=0,不会跳转; |*| test eax, eax 是判断 EAX 是否为 0,等价于 eax & eax。结果非零则 ZF=0。不要理解成减法。 00007FF632F71178 | 0F84 9E000000 | je crack.7FF632F7121C; 你的理解:实际是 zf=0,不跳转,又记错了?; 正确:ZF=0,所以不跳转,继续执行file:///C:/Users/11/AppData/Local/Temp/ksohtml22852/wps16.jpg ; ----- 调用 strtol(argv[2], NULL, 10) -----00007FF632F7117E | B8 08000000 | mov eax, 8; eax = 800007FF632F71183 | 48:6BC0 02 | imul rax, rax, 2; 你的理解:eax=8*2=10 和 3*8=18 一个道理,算错的自己罚20蹲起; 正确:rax = 8 * 2 = 16 = 0x10; |*| 这里 8 和 2 都是十六进制,8*2=0x10,不是十进制 10。写多了会形成肌肉记忆。 00007FF632F71187 | 41:B8 0A000000 | mov r8d, 10; r8d = 10(十进制)00007FF632F7118D | 33D2 | xor edx, edx; edx = 000007FF632F7118F | 48:8B4C24 78 | mov rcx, qword ptr ss:[rsp+78]; rcx = argv 基地址00007FF632F71194 | 48:8B0C01 | mov rcx, qword ptr ds:[rcx+rax]; 你的理解:rcx看不懂,是 j? 但是步过之后发现 rcx 存的是第二个参数值; 正确:rcx = argv[2](第二个参数,即你输入的密钥字符串); 补充:rax = 16,所以 rcx+rax = argv[2]; |*| 看 argv[x]:第一个参数偏移8,第二个偏移16,第三个偏移24。因为 argv 是指针数组,每个指针 8 字节。 00007FF632F71198 | E8 235E0000 | call crack.7FF632F76FC0; 调用 strtol(argv[2], NULL, 10)00007FF632F7119D | 894424 28 | mov dword ptr ss:[rsp+28], eax; 你的理解:rsp+28 存入 eax; 正确:把 strtol 的返回值(你输入的密钥数字)存入 [rsp+28]; |*| strtol 特征:3个参数,RCX=字符串, RDX=0, R8=10。返回整数在 EAX。是字符串转整数的库函数。 ; ----- 调用 sum(argv[1]) -----00007FF632F711A1 | B9 08000000 | mov ecx, 8; ecx = 800007FF632F711A6 | 48:6BC9 01 | imul rcx, rcx, 1; 你的理解:ecx=8; 正确:rcx = 8 * 1 = 8(argv[1] 的偏移量)00007FF632F711AA | 48:8B5424 78 | mov rdx, qword ptr ss:[rsp+78]; rdx = argv 基地址00007FF632F711AF | 48:8B0C0A | mov rcx, qword ptr ds:[rdx+rcx]; 你的理解:rcx:"1111111111111"; 正确:rcx = argv[1](第一个参数,文本)00007FF632F711B3 | E8 68FEFFFF | call crack.7FF632F71020; 你的理解:计算 sum(argv[1]); 正确:调用 sum 函数,返回字符 ASCII 之和00007FF632F711B8 | 83F0 03 | xor eax, 3; 你的理解:eax 是不是就是每次 call 出来的值,这次输入是 1111111111111,sum 后是 27D,和 3 做异或; 正确:eax = sum ^ 3file:///C:/Users/11/AppData/Local/Temp/ksohtml22852/wps17.jpg ; ----- 计算核心公式 -----00007FF632F711BB | B9 08000000 | mov ecx, 8; ecx = 800007FF632F711C0 | 48:6BC9 01 | imul rcx, rcx, 1; rcx = 800007FF632F711C4 | BA 01000000 | mov edx, 1; edx = 100007FF632F711C9 | 48:6BD2 00 | imul rdx, rdx, 0; 你的理解:edx=0; 正确:rdx = 1 * 0 = 000007FF632F711CD | 48:8B7C24 78 | mov rdi, qword ptr ss:[rsp+78]; rdi = argv 基地址00007FF632F711D2 | 48:8B0C0F | mov rcx, qword ptr ds:[rdi+rcx]; rcx = argv[1]00007FF632F711D6 | 0FBE0C11 | movsx ecx, byte ptr ds:[rcx+rdx]; 你的理解:rcx+rdx*1:"1111111111111"; 步过之后其实就是 rcx 放置参数1,这个是 ecx=参数1; 正确:ecx = argv[1][0](第一个字符),因为 rdx=0; |*| movsx 是符号扩展,把 1 字节扩展到 32 位。常用于取有符号字符。00007FF632F711DA | 0FAFC1 | imul eax, ecx; 你的理解:eax=sum(参数1)^3*参数1第一个字符的ascii值,就是 eax=27d^3*31=27e*31=7a1e; 正确:eax = (sum ^ 3) * argv[1][0]; |*| 双操作数 imul 指令:eax = eax * ecx。00007FF632F711DD | B9 01000000 | mov ecx, 1; ecx = 100007FF632F711E2 | 48:6BC9 04 | imul rcx, rcx, 4; rcx = 400007FF632F711E6 | 0FB64C0C 30 | movzx ecx, byte ptr ss:[rsp+rcx+30]; 你的理解:ecx=1d; 正确:ecx = 硬编码数组[4] = 0x1D; |*| movzx 是零扩展,把 1 字节扩展到 32 位。常用于取无符号字节。00007FF632F711EB | 0FB6C9 | movzx ecx, cl; ecx = 0x1D00007FF632F711EE | 8B5424 24 | mov edx, dword ptr ss:[rsp+24]; 你的理解:edx=7f, 这个是之前算的吧,00007FF632F71131 这一步算的。; 正确:edx = v5 = 0x7F00007FF632F711F2 | 33CA | xor ecx, edx; 你的理解:xor 1d,7f; 正确:ecx = 0x1D ^ 0x7F = 0x62; |*| 0x1D ^ 0x7F = 0x62(十进制 98)。这是固定的异或运算,直接算出结果即可。00007FF632F711F4 | 8D4408 9E | lea eax, qword ptr ds:[rax+rcx-62]; 你的理解:eax 又把 7a1e 取出来了,就是前面说的那个; 正确:eax = eax + ecx - 0x62 = eax + 0x62 - 0x62 = eax; |*| 核心算法简化:因为 0x1D ^ 0x7F = 0x62,所以这一长串加减可以相互抵消。最终 eax = (sum^3) * argv[1][0]。这是还原算法的关键突破口。 ; ----- 最终比较 -----00007FF632F711F8 | 8B4C24 28 | mov ecx, dword ptr ss:[rsp+28]; 你的理解:ecx 刚刚是 b 的值,现在变成 7FFFFfF 前面好像也出现过; 正确:ecx = 你输入的密钥数字(strtol 的结果)00007FF632F711FC | 3BC8 | cmp ecx, eax; 你的理解:这就是最后的比较了,后面就是打印 good 和 bad,我就不比了,应该是让 ecx=7a1e 就行。那么说 7FFFFFFF 就是和之前输入的第二个值有关系,就是 Key.; 正确:比较你输入的密钥和程序算出的正确密钥; |*| 最终判定:你需要输入的密钥 = EAX 的值。也就是 (sum ^ 3) * argv[1][0]。00007FF632F711FE | 75 0F | jne crack.7FF632F7120F; 不相等则跳去打印 "bad kitty"00007FF632F71200 | 48:8D0D 210E0200 | lea rcx, qword ptr ds:[7FF632F92028]; rcx = "Good job"00007FF632F71207 | E8 94000000 | call crack.7FF632F712A0; printf("Good job")00007FF632F7120C | 90 | nop00007FF632F7120D | EB 0D | jmp crack.7FF632F7121C; 跳到结尾00007FF632F7120F | 48:8D0D 220E0200 | lea rcx, qword ptr ds:[7FF632F92038]; rcx = "bad kitty"00007FF632F71216 | E8 85000000 | call crack.7FF632F712A0; printf("bad kitty")file:///C:/Users/11/AppData/Local/Temp/ksohtml22852/wps18.jpg //下面不重要00007FF632F7121B | 90 | nop00007FF632F7121C | 33C0 | xor eax, eax; eax = 0(返回 0)00007FF632F7121E | 48:8B4C24 50 | mov rcx, qword ptr ss:[rsp+50]; 读取栈保护值00007FF632F71223 | 48:33CC | xor rcx, rsp; 校验00007FF632F71226 | E8 65010000 | call crack.7FF632F71390; 检查栈是否被破坏00007FF632F7122B | 48:83C4 60 | add rsp, 60; 恢复栈00007FF632F7122F | 5F | pop rdi; 恢复 RDI00007FF632F71230 | C3 | ret; 返回
sum代码:file:///C:/Users/11/AppData/Local/Temp/ksohtml22852/wps19.jpg
六、算法还原(C 等价代码)
#include <stdio.h>
#include <stdlib.h>#include <string.h> int sum(const char* s) {    int total = 0;    for (int i = 0; i != strlen(s) + 1; ++i) {        total += (unsigned char)s;    }    return total;} int main(int argc, char** argv) {    unsigned char arr[14] = {        0x18, 0x1A, 0x0B, 0x5F, 0x1D, 0x1E, 0x16,        0x0B, 0x1A, 0x1B, 0x5F, 0x13, 0x10, 0x13    };     int v5 = argc * 42 + argc / 3;     if (argc != 3) {        printf("Usage: %s [text] [key for the text]", argv[0]);        return 1;    }     if (argc * 8 + 0x48 == 0x52D1) {        return 0xFFFFFFCE;    }     int user_key = (int)strtol(argv[2], NULL, 10);    int s = sum(argv[1]);    int correct_key = argv[1][0] * (s ^ 3) + (arr[4] ^ v5) - 0x62;     if (user_key == correct_key) {        printf("Good job");    } else {        printf("bad kitty");    }    return 0;}
简化后的公式:correct_key = argv[1][0] * (sum(argv[1]) ^ 3)因为 arr[4] ^ v5 = 0x1D ^ 0x7F = 0x62,与 -0x62 恰好抵消。七、验证 hello 的密钥对 hello:sum = 104 + 101 + 108 + 108 + 111 = 532sum ^ 3 = 535argv[1][0] = 'h' = 104正确密钥 = 104 * 535 = 55640 = 0xD958这个值与你之前在 x64dbg 里看到的 EAX = 0xD958 完全吻合。
八、注册机代码(C++)
#include <iostream>#include <string>int main() {    std::string name;    std::cout << "Enter name: ";    std::cin >> name;    int sum = 0;    for (unsigned char c : name) {        sum += c;    }    int correct_key = name[0] * (sum ^ 3);    std::cout << "Correct key: " << correct_key << std::endl;    return 0;}file:///C:/Users/11/AppData/Local/Temp/ksohtml22852/wps20.jpg
九、总结定位 main:优先用字符串搜索或 IDA 辅助,入口点盲跟只作为备选。识别 argv:看 [rsp+78] 是否被用作基地址,再用 8 的倍数偏移取值。test eax, eax:判断 EAX 是否为 0,不是减法。(重点看头)十六进制计算:x64dbg 里立即数默认十六进制,计算时带 0x 前缀。算法还原:从 cmp ecx, eax 往上追,逐步还原公式。v5 是固定值:由 argc 算出,不是随机数。
0de370af-0879-4928-a765-f7f3b7c6308b.png
02cb93b3-b936-414e-86ef-25676ae189da.png
4c462344-5356-4341-ac1e-0a7b2b4e111f.png
63f033d9-d2d1-4192-9232-900a2112c91f.png
68a77b12-0ced-4ec8-9a91-cbc5fc88a217.png
76cdeaf5-f3a1-4df2-911a-560e426fd335.png
98e52435-ba76-4614-a2e5-4bca0e236894.png
804e2220-b887-42c9-96d9-4c0b88e47e4a.png
881e99de-e339-4b8f-acbf-dacac8aca829.png
35765b9d-aa1f-4bb6-be28-a4f5f55bbe05.png
a52ffd1c-8e37-438d-ae3f-5ea1678aa105.png
aa385237-32e0-4f24-9085-a93d9c12302e.png
b2649a79-5620-4861-8ed1-2937c3949b48.png
ba15f20a-eccf-42c2-aed7-d21ace8e34f6.png
cc542bd9-ac75-47af-b7b2-446d21187c51.png
d3a8e166-413c-471b-b5f8-a34898dc1485.png
d8c56343-e21d-44ed-a3be-5b28598186c6.png
da6ee49a-d1c4-4024-a7a0-bb08258884e6.png
da8c6491-24ab-41ed-87fc-b14980f90cd4.png
fcc7c8f6-64ff-426e-98f1-17d95e9a6f3e.png

4b6ee3f658260683ec2ae4cc129f7adcceafe630f36860c1908425430e4a5386.zip

75.24 KB, 下载次数: 0, 下载积分: 吾爱币 -1 CB

真1.0?

发帖前要善用【论坛搜索】功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| getjob 发表于 2026-10-1 12:17
不太会用帖子,草稿里面排列是没问题的,一发表就全都乱套了。这玩意直接保存电脑的真实路径啊
 楼主| getjob 发表于 2026-10-1 12:21
假期愉快,回来再说吧,话说这帖子怎么删有大佬知道么,我感觉删了重发一个还快一点。相同的文章,可以先看这个
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-10-5 18:58

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表