前言:这是我近日以来一点小小的工作,由于字数太多一次写不完,先写
的部分分节发表在我的Blog上,这篇文章是根据Blog的内容整理出来的,
特此声明,以免抄袭之嫌。
又及:刚刚想要发这贴的时候,发现已经有一个贴分析了同一个
KeyGenMe:
http://bbs.pediy.com//showthread.php?threadid=25015
真是够郁闷的。这篇文章的内容每一个字都是我自己搞的,如果你觉得是
用其他方法得到的,我也能够理解,谁叫我落后一步呢。
KeyGenMe的下载地址:http://www.crackmes.de/users/haggar/keyme_no.4
使用工具:
反编译及调试工具:OllyDbg, IDA, Resource Hacker
程序开发包:MASM32v9
第一节:去除花指令
破解一般的共享软件时,都需要用PEID之类工具先检查文件类
型,以确定文件是否加了壳。但是这次不需要。这倒不是说CrackMe
就一定不会加壳,而是crackmes.de这个站点不允许提交加过壳的
CrackMe。以下用OllyDbg载入。
分析开头的一段代码:
00401000 > $ 50 push eax
00401001 . E8 03000000 call 00401009
00401006 . EB db EB
00401007 > EB 02 jmp short 0040100B
00401009 $^ EB FC jmp short 00401007
0040100B > 3E:8B0424 mov eax, ds:[esp]
0040100F . 83C0 09 add eax, 9
00401012 > EB 02 jmp short 00401016
00401014 . EB 06 jmp short 0040101C
00401016 > FFE0 jmp eax
00401018 . 58 pop eax
00401019 . 83C0 19 add eax, 19
0040101C >^ EB F4 jmp short 00401012
0040101E - E9 5833C050 jmp 5100437B
00401023 . E8 03000000 call 0040102B
00401028 .^ EB EB jmp short 00401015
0040102A ? 02EB add ch, bl
0040102C ? FC cld
0040102D > 3E:8B0424 mov eax, ds:[esp]
00401031 . 83C0 09 add eax, 9
00401034 > EB 02 jmp short 00401038
00401036 . EB 06 jmp short 0040103E
00401038 > FFE0 jmp eax
0040103A . 58 pop eax
0040103B > 83C0 19 add eax, 19
0040103E >^ EB F4 jmp short 00401034
00401040 - E9 5850E840 jmp 4128609D
401001这个call压进堆栈的地址是401006,而本身则转移到401009
处执行。经过两个jmp语句后,将刚压入的地址401006送回eax(但
不修改堆栈指针,与出栈不同)。在eax上加9以后,eax为40100F,
刚好等于add eax,9这一句的地址。接下来执行401016处的jmp eax
语句,又在eax上加9,于是eax现在是401018,jmp eax以后执行的
指令是pop eax,把call压进的401006弹出到eax。接下来在eax上
加19后变成40101F,再一次执行到jmp eax语句。但是且慢!从反
汇编的结果来看,地址40101F不是一条语句的开始呀?但是出现这
种情况,只能以实际运行的结果为准,姑且把40101E处的字节E9看
成字节数据,就可以看到正确的代码了:
0040101E . E9 db E9
0040101F . 58 pop eax
00401020 . 33C0 xor eax, eax
00401022 . 50 push eax
也就是说,实际执行的指令是pop eax,将最初401000处保存的eax
值恢复,而40101E处的E9是一个垃圾字节,用来干扰反汇编程序的
识别。
这些细小的地方姑且不去管他,程序自运行以来到执行40101F
处的pop eax为止,所涉及到的寄存器只有eax和esp,而现在push
和call两次入栈操作已经被两个pop eax平衡,eax也已经复原,那
么岂不是相当于什么都没做,这段程序究竟有什么用意呢?既然没
有用,那就用nop刷掉。(还可以省下一些CPU时间。)需要说明的
是,与之大同小异的花指令段在整个程序中实在是太多了,整个程
序的全部代码少说有三分之一是这种类型的花指令段。用相同的处
理方法处理。也就是说在文件中查找十六进制字节串:
50E803000000EBEB02EBFC3E8B042483C009EB02EB06FFE05883C019EBF4E958
(倒数第二个字节的E9有时也可是EB)然后全部改成:
9090909090909090909090909090909090909090909090909090909090909090
我想应该可以编个脚本专门做这种事情(我是用逐块替换的,感觉
还是有点多)。
这样改完之后反汇编的代码中会有大块的nop指令,影响代码
的阅读。可以把修改过的文件用IDA打开,然后选择输出为汇编源文
件,这样一来,所有的地址引用都被替换成了对标号的引用,就可
以去掉这些nop指令,而不用担心指令引用错误地址的问题了。
你或许会说,为什么能判断它是一段花指令,万一是正常的程
序功能指令,那不是就陷进去出不来了吗?判断一段程序是不是花
指令,没有一定的准则,只能靠个人感觉。以我个人来说,我是看
到这段程序里的jmp指令太多,有时连用好几个jmp。而正常的功能
指令段通常不会有很多的无条件跳转。还有call引用的地址跟它本
身的地址十分接近,这就可能是变形的jmp。
第二节:反向工程(1)还原界面代码
在继续下去之前,我想阐明一下这里“反向工程”的涵义。一
般地,反向工程是把已经编译成机器指令的程序还原成源代码的形
式。而这个KeyGenMe原本是用汇编写的,那是不是说,把它的机器
指令用汇编的形式表示出来就算是反向了呢?如果这种说法是正确
的话,那随便一个会用电脑软件的人去找个象W32Dasm这样的软件,
把exe文件加载进去,出来的就是汇编代码了,这样的话,人人都
可以是反向工程专家了。
反向工程还原的不仅是代码,更是程序的算法。创造性的反向
工程首先要详尽分析反汇编出来的代码,理解程序功能的算法,然
后自己编写代码实现相同或相仿的功能。即使是照抄反汇编出来的
代码,也要书写成让人能读懂的形式;对于那些与程序功能无关的
或者执行起来拖泥带水的冗余代码,该删的删,该改的改。还原出
来的代码,要在忠实于原程序功能的前提下,维持良好的可读性,
这才是反向工程的精神所在。
废话打住,首先把IDA生成的汇编代码文件去掉那些大块的nop
指令,接着找到程序入口点的代码:
public start
start proc near
xor eax, eax
push eax ; lpModuleName
call GetModuleHandleA
mov hInstance, eax
push 0 ; dwInitParam
push offset DialogFunc ; lpDialogFunc
push 0 ; hWndParent
push 3E9h ; lpTemplateName
push hInstance ; hInstance
call DialogBoxParamA
push 0 ; uExitCode
call ExitProcess
start endp
由此可见,这是一个标准的使用对话框的程序:
GetModuleHandle --> DialogBoxParam --> ExitProcess
lpTemplateName参数中需要提供对话框ID,通常这是在资源文件中
定义的,所以用Resource Hacker把它的资源提取出来辅助分析。
这里的3E9h就是1001,在资源脚本中有这么一句:
1001 DIALOGEX 0, 0, 463, 160
下文中还会出现在资源中定义的ID号,对此只要在源文件头部加上
一些诸如“DLG_MAIN equ 1001”的符号定义,就可以用DLG_MAIN
这些有意义的符号来引用资源了,所以都不再一一说明。
使用对话框还必须有回调过程,上面程序中提供的回调过程是
DialogFunc,接下来看看它的代码:
; Attributes: bp-based frame
; BOOL __stdcall DialogFunc(HWND,UINT uMsg,WPARAM wParam,LPARAM)
DialogFunc proc near ; DATA XREF: start+8Fo
hDlg = dword ptr 8
uMsg = dword ptr 0Ch
wParam = dword ptr 10h
push ebp
mov ebp, esp
cmp [ebp+uMsg], WM_INITDIALOG
jnz short loc_4010DE
push 7D1h ; lpIconName
push hInstance ; hInstance
call LoadIconA
push eax ; lParam
push ICON_BIG ; wParam
push WM_SETICON ; Msg
push [ebp+hDlg] ; hWnd
call SendMessageA
jmp loc_4012BC
loc_4010DE: ; CODE XREF: DialogFunc+Aj
cmp [ebp+uMsg], WM_COMMAND
jnz loc_4012AC
cmp [ebp+wParam], 3F6h
jnz short loc_401107
push 0 ; lParam
push 0 ; wParam
push WM_CLOSE ; Msg
push [ebp+hDlg] ; hWnd
call SendMessageA
jmp loc_4012BC
loc_401107: ; CODE XREF: DialogFunc+45j
cmp [ebp+wParam], 3F5h
jnz loc_4012BC
push 10h ; nMaxCount
push offset dword_4036C4 ; lpString
push 3ECh ; nIDDlgItem
push [ebp+hDlg] ; hDlg
call GetDlgItemTextA
cmp eax, 4
jz short loc_401136
call sub_4012C2
leave
retn 10h
loc_401136: ; CODE XREF: DialogFunc+7Ej
push 10h ; nMaxCount
push offset dword_4036D4 ; lpString
push 3EDh ; nIDDlgItem
push [ebp+hDlg] ; hDlg
call GetDlgItemTextA
cmp eax, 4
jz short loc_401178
call sub_4012C2
leave
retn 10h
loc_401178: ; CODE XREF: DialogFunc+C0j
push 10h ; nMaxCount
push offset dword_4036E4 ; lpString
push 3EEh ; nIDDlgItem
push [ebp+hDlg] ; hDlg
call GetDlgItemTextA
cmp eax, 4
jz short loc_4011BA
call sub_4012C2
leave
retn 10h
loc_4011BA: ; CODE XREF: DialogFunc+102j
push 10h ; nMaxCount
push offset dword_4036F4 ; lpString
push 3EFh ; nIDDlgItem
push [ebp+hDlg] ; hDlg
call GetDlgItemTextA
cmp eax, 4
jz short loc_4011FC
call sub_4012C2
leave
retn 10h
loc_4011FC: ; CODE XREF: DialogFunc+144j
push 10h ; nMaxCount
push offset dword_403704 ; lpString
push 3F0h ; nIDDlgItem
push [ebp+hDlg] ; hDlg
call GetDlgItemTextA
cmp eax, 4
jz short loc_40123E
call sub_4012C2
leave
retn 10h
loc_40123E: ; CODE XREF: DialogFunc+186j
call _VerifyCDKey
cmp eax, 0
jz short loc_401281
push 40h ; uType
push offset Caption ; "Information"
push offset Text ; "The entered key appears to be valid. Yo"...
push 0 ; hWnd
call MessageBoxA
xor eax, eax
leave
retn 10h
loc_401281: ; CODE XREF: DialogFunc+1B9j
call sub_4012C2
leave
retn 10h
loc_4012AC: ; CODE XREF: DialogFunc+38j
cmp [ebp+uMsg], 10h
jnz short loc_4012BC
push 0 ; nResult
push [ebp+hDlg] ; hDlg
call EndDialog
loc_4012BC: ; CODE XREF: DialogFunc+2Cj
; DialogFunc+55j ...
xor eax, eax
leave
retn 10h
DialogFunc endp
由于反汇编代码中的局部变量名是用ebp指针表示的,在此可赋予
它们一些有意义的名称,如uMsg,wParam等。stdcall调用方式是
参数自右向左入栈,所以参数表的第一个参数通常是[ebp+8],而
右边的参数放在高地址中。于是uMsg就是dword ptr [ebp+0C],
余类推。
我们感兴趣的部分是在对WM_COMMAND(原为数值111h)消息的
处理上。程序运行界面有两个按钮:“Next >”和“Cancel”。按
下Cancel按钮,则调用SendMessage向对话框发送WM_CLOSE,界面
就关闭。而按下Next按钮,程序依次到五个文本框中取文本,只要
有其中一个文本框中取得的字符数不是4,就显示CDKey检验失败
(sub_4012C2是显示失败信息的例程)。也就是说,CDKey的每一
小节都应该是4个字符,满足这个条件才开始检验。取完文本后调
用一个过程_VerifyCDKey(这个过程名称是自定义的),如果返回
值是0,也检验失败。反之,则显示检验成功。至此,界面功能已
基本分析清楚,可以写出源代码的框架了。
第三节:反向工程(2)CDKey验证部分算法分析
前面已经弄清了界面的工作原理,接下来该接触程序的核心算
法部分,也就是上面提到的_VerifyCDKey过程。
算法分析的一个重要指导思想就是等效替代。如果一段代码能
够用较简单的指令序列实现同样的功能,那就应该用简单的指令序
列去替换复杂的指令序列。事实上,在去除花指令一节,就已经用
到了这种思想。现在再看一个例子:
xor eax, eax
xor edi, edi
xor esi, esi
mov ecx, 1
mov ebx, 0Dh
loc_401630: ; CODE XREF: _VerifyCDKey+35Fj
movsx eax, byte ptr dword_403714[esi]
add eax, ecx
xor edx, edx
mov ecx, 0FFF1h
div ecx
mov ecx, edx
lea eax, [ecx+edi]
xor edx, edx
mov edi, 0FFF1h
div edi
inc esi
cmp esi, ebx
mov edi, edx
jl short loc_401630
如果预先知道了dword_403714[esi]的内容是ASCII字符,那么符号
扩展就是零扩展,eax最多不超过7Fh。用它去除以0FFF1h,岂非就
相当于xchg eax, edx?循环中每次将eax的值累加送到ecx中,但
是只循环13次,13个字符的ASCII码加起来也远远达不到0FFF1h。
因此这句除法指令完全可以改成xchg eax, edx。其次,对各寄存
器的赋值情况作标记,删去那些只用来保存中间结果的寄存器。譬
如:
add eax, ecx
xor edx, edx
xchg eax, edx
mov ecx, edx
edx是保存中间结果的,下面并没有用到,那么就可以简化为:
add ecx, eax
xor eax, eax
同样还可以做其他一些简化。上面那段代码最终可以化为:
xor eax, eax
xor edi, edi
xor esi, esi
mov ecx, 1
loc_401630:
movsx eax, byte ptr dword_403714[esi]
add ecx, eax
add edi, ecx
inc esi
cmp esi, 0Dh
jl short loc_401630
当然,这段代码并非程序的关键代码,只是用来说明分析算法时使
用的一些简化技巧。设计算法和分析算法,从某种意义上来说是相
反的过程。设计算法时,算法越复杂,可能实现的功能越多。而分
析算法时,算法的表述越简单,就越容易理解。
等效替代的思想,不仅可以应用在修改指令序列这样的微观方
面,同样可以应用在分析模块功能这样的宏观方面。譬如我们现在
要分析_VerifyCDKey过程,它有些什么功能?根据程序上下文,它
的作用是检验输入的CDKey,返回值代表检验结果:非0为成功,0
为失败。既然如此,我们的重点就应该放在CDKey是如何被校验的
方面,最后只要做出一个过程,采用相同方式校验CDKey,并且失
败返回0且成功返回非0(等效替代),就算达到目的了。
基于这一思想,用OllyDbg载入程序,按要求(每4个字符一小
节)随便输入一个CDKey,然后定位到那些与注册码校验相关的代
码位置上。为了方便起见,这里还是使用IDA反汇编出来的代码。
mov dword_403674, 1010101h ;这里是输入一个表
mov dword_403678, 1010101h ;下文中将会用它来
mov dword_40367C, 1010101h ;检验注册码中是否
mov dword_403680, 1010101h ;存在K,P,Q,Y四字
mov dword_403684, 1010101h
mov dword_403688, 1010100h
mov dword_40368C, 1000001h
mov dword_403690, 1010101h
mov dword_403694, 1000101h
mov dword_403698, 1010101h
mov eax, dword_4036C4 ;将输入框中取得
mov dword_403714, eax ;的字符连接到以
mov eax, dword_4036D4 ;403714为起始地
mov dword_403718, eax ;址的内存单元中
mov eax, dword_4036E4
mov dword_40371C, eax
mov eax, dword_4036F4
mov dword_403720, eax
mov eax, dword_403704
mov dword_403724, eax
mov byte_403728, 0
xor ecx, ecx
jmp short loc_4014A3
loc_40144B: ; CODE XREF: _VerifyCDKey+1B0j
mov al, byte ptr dword_403714[ecx]
mov byte_403754, al
cmp byte_403754, 30h ;0
jb short loc_401488
cmp byte_403754, 5Ah ;Z
jbe short loc_40148D
loc_401488: ; CODE XREF: _VerifyCDKey+187j
xor eax, eax
retn
loc_40148D: ; CODE XREF: _VerifyCDKey+190j
cmp byte_403754, 39h ;9
jbe short loc_4014A2
cmp byte_403754, 41h ;A
jnb short loc_4014A2 ;这一段是检验输入码中是否
xor eax, eax ;只有数码及大写字母
retn ;如包含其他字符,则为非法CDKey
xor ecx, ecx
jmp short loc_401516
loc_4014CC: ; CODE XREF: _VerifyCDKey+223j
push ecx
mov al, byte ptr dword_403714[ecx]
xor ecx, ecx
jmp short loc_40150F
loc_4014F7: ; CODE XREF: _VerifyCDKey+21Cj
cmp al, byte_4030FF[ecx]
;查看相应的内存地址可知:
;004030FF 30 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 0123456789ABCDEF
;0040310F 47 48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 GHIJKLMNOPQRSTUV
;0040311F 57 5A 59 58 WZYX
;配合上面输入到地址403674的表,可知这段程序的作用是检验输入码中是否
;有K,P,Q,Y四字符,如果有,则为非法CDKey
jnz short loc_40150E
cmp byte ptr dword_403674[ecx], 1
jz short loc_40150E
add esp, 4
xor eax, eax
retn
loc_40150E: ; CODE XREF: _VerifyCDKey+207j
; _VerifyCDKey+210j
inc ecx
loc_40150F: ; CODE XREF: _VerifyCDKey+1FFj
cmp ecx, 24h
jb short loc_4014F7
pop ecx
inc ecx
loc_401516: ; CODE XREF: _VerifyCDKey+1D4j
cmp ecx, 14h
jb short loc_4014CC
xor ecx, ecx
jmp short loc_40154C
loc_40153F: ; CODE XREF: _VerifyCDKey+259j
mov al, byte ptr dword_403714[ecx]
mov byte_403734[ecx], al
inc ecx ;由于下面要对输入码做变换
;在403734地址处保存一份未变换的输入码
loc_40154C: ; CODE XREF: _VerifyCDKey+247j
cmp ecx, 14h
jb short loc_40153F
lea eax, dword_403714
xor edx, edx
xor ebx, ebx
mov ecx, 2
mov dl, [eax+ecx] ;注册码的第3和第14两个字符交换位置
mov bl, [eax+0Dh]
mov [eax+ecx], bl
mov bl, [eax+0Eh]
mov [eax+0Dh], dl
mov edx, 4
add ecx, eax
lea ecx, [eax+edx]
mov dl, [ecx]
mov [ecx], bl
mov bl, [eax+0Fh]
mov [eax+0Eh], dl ;第5和第15两个字符交换
mov ecx, 5
mov dl, [eax+ecx]
mov [eax+ecx], bl
mov bl, [eax+10h]
mov [eax+0Fh], dl ;第6和第16两个字符交换
mov edx, 7
add ecx, eax
lea ecx, [eax+edx]
mov dl, [ecx]
mov [ecx], bl
mov bl, [eax+11h]
mov [eax+10h], dl ;第8和第17两个字符交换
mov ecx, 0Ch
mov dl, [eax+ecx]
mov [eax+ecx], bl
mov bl, [eax+12h]
add ecx, eax
mov [eax+11h], dl ;第13和第18两个字符交换
mov edx, 1
lea ecx, [eax+edx]
mov dl, [ecx]
mov [ecx], bl
mov bl, [eax+13h]
mov [eax+12h], dl ;第2和第19两个字符交换
mov ecx, 3
mov dl, [eax+ecx]
add ecx, eax
mov [ecx], bl
mov [eax+13h], dl ;第4和第20两个字符交换
mov byte ptr [eax+14h], 0
;注意上面一段的变换的逆变换就是其自身
;以下很长一段代码属于无关代码,兹从略
;执行到下面一句之前,ecx的值为16h
lea eax, byte_403755
mov dl, cl
and dl, 3
shl dl, 3
mov [eax+6], dl
shr ecx, 2
mov dl, cl
and dl, 1Fh
mov [eax+5], dl
mov edx, ecx
shr edx, 5
and dl, 1Fh
mov [eax+4], dl
mov edx, ecx
shr edx, 0Ah
and dl, 1Fh
mov [eax+3], dl
mov edx, ecx
shr edx, 0Fh
and dl, 1Fh
mov [eax+2], dl
mov edx, ecx
shr edx, 14h
shr ecx, 19h
and dl, 1Fh
mov [eax+1], dl
and cl, 1Fh
mov [eax], cl
mov dl, [eax+6]
and cl, 7 ;根据上文中给的ECX值
or dl, cl ;执行完这一句时,403755开始写入的7个字节为
mov [eax+6], dl ;0,0,0,0,0,5,10h
movsx edx, byte ptr [eax]
mov cl, byte_403145[edx]
movsx edx, byte ptr [eax+1]
mov [eax], cl
mov cl, byte_403145[edx]
movsx edx, byte ptr [eax+2]
mov [eax+1], cl
mov cl, byte_403145[edx]
movsx edx, byte ptr [eax+3]
mov [eax+2], cl
mov cl, byte_403145[edx]
movsx edx, byte ptr [eax+4]
mov [eax+3], cl
mov cl, byte_403145[edx]
movsx edx, byte ptr [eax+5]
mov [eax+4], cl
mov cl, byte_403145[edx]
movsx edx, byte ptr [eax+6]
mov [eax+5], cl
mov cl, byte_403145[edx]
mov [eax+6], cl
mov byte ptr [eax+7], 0
;上面用刚写入的7个字节做索引值在一张表中查找相应的项目并写回到原来的字节中
;查看相应的内存单元可知:
;00403145 32 33 34 35 36 37 38 39 41 42 43 44 45 46 47 48 23456789ABCDEFGH
;00403155 4A 4B 4C 4D 4E 51 51 52 53 54 55 56 57 58 59 59 JKLMNQQRSTUVWXYY
;为了验证这张表的内容是否与输入有关,在其上设置内存写入断点,没有发现
;程序往其中写入数据,也就是说这张表不依赖于输入
;于是,用0,0,0,0,0,5,10h做索引,写回的字符应该是'2','2','2','2','2','7','J'
xor ecx, ecx
xor eax, eax
jmp short loc_401946
loc_401939: ; CODE XREF: _VerifyCDKey+653j
mov al, byte_403755[ecx]
mov [ecx+403721h], al ;用上面的7个字节
inc ecx ;改写注册码的后7个字符
loc_401946: ; CODE XREF: _VerifyCDKey+641j
cmp ecx, 7
jb short loc_401939
;执行逆变换,从略
xor eax, eax
xor ebx, ebx
xor ecx, ecx
jmp short loc_401A36
loc_401A22: ; CODE XREF: _VerifyCDKey+743j
mov bl, byte ptr dword_403714[ecx]
mov bh, byte_403734[ecx]
cmp bl, bh ;比较处理后的字符串是否与原输入码相同
jz short loc_401A35 ;不同,则此CDKey非法
xor eax, eax ;反之则合法
retn
loc_401A35: ; CODE XREF: _VerifyCDKey+73Aj
inc ecx
loc_401A36: ; CODE XREF: _VerifyCDKey+72Aj
cmp ecx, 14h
jb short loc_401A22
mov eax, 1
retn
;根据执行逆变换前的结果,注册码的第14,15,16,17,18,19,20个字符分别为'2','2','2',
;'2','2','7','J',这就要求原输入码的第3,5,6,8,13,2,4个字符也为对应的值,方为合
;法CDKey
既然最后是通过一个比较来判定CDKey是否合法,那么我们所需要
关心的就只有与其相关的内存单元,以及在比较之前最后一次写入
的数据是什么。事实上,在上面写入'222227J'之前还写入过另外
一个字符串,这个字符串还是经过十分繁复的运算得出来的。但它
毕竟被覆盖了,那部分算法就犯不着关心。
于是,一个CDKey是合法的当且仅当它有下面几个特征:
●由数码及大写字母构成;
●不含字符'K','P','Q','Y';
●第2,3,4,5,6,8,13个字符依次是'7','2','J','2','2','2',
'2'。
根据这个算法描述重写的_VerifyCDKey过程,就比原来大大简化
了。
第四节:KeyGen
首先做一张表,由全部数字以及除'K','P','Q','Y'的大写
字母组成。然后构造一个20字符的字符串模板,在规定的位置填入
已规定的字符('2'、'7'、'J'等),其他位置的字符就在上述那
张表中随机抽取并填入。但是我不知道产生随机数的API是什么,
所以暂时还写不出KeyGen程序。
上传文件的内容:
rev.asm 还原出来的KeyGenMe源程序
res.rc 资源脚本
Icon_1.ico, Bitmap_1.Bmp 图标及位图资源
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)