160 个 CrackMe 之 099 -- RingZ3r0 的解码脱壳、兼容修复及注册分析
本帖最后由 solly 于 2019-12-2 01:24 编辑160 个 CrackMe 之 099 -- RingZ3r0 在 Windows 10 下不能直接运行,需要修复一个堆栈平衡的问题,并且其代码关键部分和字符串资源是编码了的,在运行时会异或解码,同时,其是一个 KeyFile 方式的 Crackme。
我们先看看文件信息:
显示代码有回写保护,并没有壳。“Scan /t”如下图所示:
显示编译环境是 VC8。节的情况如下:
在 Windows 10 下用 OD 加载,可以正常加载,表示其应该可以在最新的 Windows 下运行,如下图所示:
最前面一段代码首先取得 CrackMe 的命令行数据,然后判断命令行最后一个字符是否“\t”字符,不是则加上一个“\t"字符,接着直接用这个命令行字符串重新创建进程,然后退出了。也算是一种反调试手段吧,如下图所示:
具体代码如下所示:
00401000 > $E8 3A090000 call <jmp.&KERNEL32.GetCommandLineA> ; [GetCommandLineA
00401005 .50 push eax ;eax===> 命令行
00401006 .8BF8 mov edi, eax
00401008 .32C0 xor al, al ;'\0'
0040100A .B9 FFFFFFFF mov ecx, -1
0040100F .F2:AE repne scas byte ptr es: ;edi ===> 命令行
00401011 .F7D9 neg ecx
00401013 .49 dec ecx ;命令行长度 = ecx
00401014 .5E pop esi
00401015 .BF 10204000 mov edi, 00402010 ;ASCII """E:\Downloads\crack\099_RingZ3r0\Krypton.EXE"""
0040101A .F3:A4 rep movs byte ptr es:, byte ptr
0040101C .4F dec edi
0040101D .4F dec edi
0040101E .803F 09 cmp byte ptr , 9 ;最后一个字符是否为"\t"
00401021 .74 31 je short 00401054 ;将 je 改成 jmp 指令,跳过退出
00401023 .47 inc edi
00401024 .C607 09 mov byte ptr , 9 ;添加一个'\t'符
00401027 .47 inc edi
00401028 .C607 00 mov byte ptr , 0 ;添加'\0'
0040102B .68 00204000 push 00402000 ; /pProcessInfo = Krypton.00402000
00401030 .68 14214000 push 00402114 ; |pStartupInfo = Krypton.00402114
00401035 .6A 00 push 0 ; |CurrentDir = NULL
00401037 .6A 00 push 0 ; |pEnvironment = NULL
00401039 .6A 08 push 8 ; |CreationFlags = DETACHED_PROCESS
0040103B .6A 01 push 1 ; |InheritHandles = TRUE
0040103D .6A 00 push 0 ; |pThreadSecurity = NULL
0040103F .6A 00 push 0 ; |pProcessSecurity = NULL
00401041 .68 10204000 push 00402010 ; |CommandLine = """E:\Downloads\crack\099_RingZ3r0\Krypton.EXE"""
00401046 .6A 00 push 0 ; |ModuleFileName = NULL
00401048 .E8 C8080000 call <jmp.&KERNEL32.CreateProcessA> ; \CreateProcessA
0040104D .6A FF push -1 ; /ExitCode = FFFFFFFF
0040104F .E8 C7080000 call <jmp.&KERNEL32.ExitProcess> ; \ExitProcess
00401054 >E9 45070000 jmp 0040179E
我们首先要破除这个限制,如下图所示:
我们需要直接跳转到下面的代码,而不去创建新的进程。修改如下:
直接将 je 指令改成 jmp 指令就可以了。这样就直接来到了下图所示位置:
可以看到,后面一段代码看起来很混乱,那是因为代码加密的了。直接按 F8 跳过这段加密了的代码,来到如下位置:
这里的代码又正常了,继续 F8 执行,来到下面代码处:
这一段代码是 CrackMe 程序在我们的注册表中留一个”到此一游“的标记,内容为("Yado says: Hi gui! ;"),没什么特别的用处。
继续 F8 往下走,来到了代码解码处,如下图所示:
具体解码代码如下:
004017F8 .B8 59104000 mov eax, 00401059
004017FD .BA 9D174000 mov edx, 0040179D
00401802 >4A dec edx
00401803 .3BC2 cmp eax, edx
00401805 .74 05 je short 0040180C
00401807 .8032 11 xor byte ptr , 11 ;代码解码
0040180A .^ EB F6 jmp short 00401802
0040180C >8032 11 xor byte ptr , 11 ;解码最后一个字节
执行完上面的解码程序后,我们前面看到的混乱代码恢复正常,如下图所示,红色代码就是刚才解码后的代码了:
但有可能显示上有问题,没有显示完整代码,需要 OD 重新分析代码,我们按一次 "CTRL+A",就可以重新分析代码,如下图所示,重新分析后就正常了:
这样就显示为正常代码了。这段代码的结束位置如下所示:
我们继续执行 CrackMe,按几次 F8,来到下面代码处,如下图所示:
这里也是一段修改自身代码的程序:是从内存数据段复制8个字节的代码,主要是用来反“SoftICE”的调试的,其实就是一个 Int 68 功能,这个功能与 Windows NT 系列内核不兼容,会导致程序退出。
具体代码如下所示:
0040182F .B8 4D234000 mov eax, 0040234D
00401834 .BA 69234000 mov edx, 00402369
00401839 .8B00 mov eax, dword ptr
0040183B .8B0A mov ecx, dword ptr
0040183D .66:8948 06 mov word ptr , cx ;修改指令码
00401841 .83C2 02 add edx, 2
00401844 .8B0A mov ecx, dword ptr
00401846 .66:8908 mov word ptr , cx ;修改指令码
00401849 .83C2 02 add edx, 2
0040184C .8B0A mov ecx, dword ptr
0040184E .66:8948 02 mov word ptr , cx ;修改指令码
00401852 .83C2 02 add edx, 2
00401855 .8B0A mov ecx, dword ptr
00401857 .66:8948 04 mov word ptr , cx ;修改指令码
按F8 执行这段代码,其修改后的代码如下图所示:
红色部分8个字节被修改了,这个 Int 68 就是检测 SoftICE 有没有在运行的,用于反调试。
其功能是将以下代码:
0040185D .8BF8 mov edi, eax ;Sofice 检测
0040185F .8B00 mov eax, dword ptr ;设置新IP跳过Int68
00401861 ?8BF0 mov esi, eax
00401863 .8B00 mov eax, dword ptr
修改成以下代码了:
0040185D .CD 68 int 68 ;Sofice 检测
0040185F .66:3D 86F3 cmp ax, 0F386 ;设置新IP跳过Int68
00401863 .74 09 je short 0040186E
如上面的图示所示,当执行到 Int 68 指令,重新设置新的 IP 值跳过这条指令即可,如下图所示:
跳过这条指令就不会引起异常退出了。重新 F8 执行程序:
来到 "jmp ebx"这里,代码解码就完成了,在这条跳转指令前,还执行了这样一条指令 “movbyte ptr , 63”,这是很重要的一个数据写入(后面会讲到):
0040185B .B4 43 mov ah, 43
0040185D .CD 68 int 68 ;Sofice 检测
0040185F .66:3D 86F3 cmp ax, 0F386 ;设置新IP跳过Int68
00401863 .74 09 je short 0040186E
00401865 .C605 4B234000 63 mov byte ptr , 63 ;无调试,写入一个 0x63 到 0x0040234B,这个字节是后面注册算法的密钥
0040186C .FFE3 jmp ebx
0040186E >C605 4B234000 17 mov byte ptr , 17 ;有调试
00401875 .FFE3 jmp ebx
这里写入的 0x63 在后面会用到。
再次 F8 执行跳转,来到下图所示位置:
这里是将原来我们跳过混乱代码那一条 JMP 0040179E 改成 nop 指令,原有指令如下所示:
; 跳过未解密代码
00401054 > \E9 45070000 jmp 0040179E
;
来个图片更清楚,修改前状态:
按 F8 执行程序,代码修改后的效果如下(这样改了还有一个好处,本来解码后要脱掉解码代码的话,要把这个jmp去掉,否则还会去解码,没想到 CrackMe 自己去掉了):
再按 F8 执行 JMP 指令,来到如下位置:
这里是我们前面解码后的代码,先静态分析一下这段代码,如上图所示,这里是创建一个基于 Dialog 的程序,其 DlgProc = 0x00401083。
我们来到 0x00401083 看看 DlgProc() 的代码,如上图所示,后面部分即是 DlgProc 代码,处理了三条消息:WM_DESTROY,WM_COMMAND,WM_INITDIALOG。
我们主要关注其 WM_COMMAND 消息的处理代码,如下图所示:
处理了三个 ControlID 分别为 0x65,0x66,0x67 的控件事件。通过三个 je 指令的跟随,可以看出指令功能如下图中的注释所示:
我们在处理第一条指令的位置下一个断点,用来断下 WM_COMMAND 消息的处理。
先看看注册验证代码,如下图所示:
其第一条指令就是取出我前面提醒过的位于0x0040234B一个字节数据:0x63,后接着的是一大段xor解码程序,这个只有进行动态跟踪来确定其完成的功能了。
下面我们先将程序进行脱壳化处理,就是以后不用再自解码代码了,操作如下图所示:
右键菜单”复制到可执行文件“,弹出如下提示:
选择”复制全部“,又会弹出另一个提示:
选择”是“,到保存文件界面,如下图所示,再一次右键菜单:
选择”保存文件“,弹出一个保存文件对话框:
输入一个新的文件名,点”保存“即可。
下面,我们先执行 CrackMe,准备动态跟踪算法:
先下一个断点,便于再次调试。我们直接按 ”F9“ 运行程序。不料再次出现异常,如下图所示:
点”确定“,如下图所示,IP = 0x00000030 了,肯定没有办法执行下去了:
由于刚才执行的是创建对话框的程序代码,看来是 DlgProc() 中的代码有问题。因为基于对话框程序,首先执行的就是 DlgProc()。
重新加载我们刚才保存的已经脱掉解码的程序,如下图所示:
JE 指令已变了 JMP 指令了,但是数据区在0x0040234B处那个重要的数据 0x63 却没有保存出来,所以我们还得先把这个初始化数据恢复,如下图所示:
在上图中OD数据区,将 0x00 手动修改成 0x63 即可。如上图所示,我们跟随那条前面修改的 JMP 指令,来到一段nop 指令处,其中前5条nop指令,是由一条 jmp 指令修改来的。
如下图所示,我们再次来到 DlgProc() 代码处,并下一个断点,动态跟踪看看前面出的是什么错误:
然后,直接按 F9,看看会有什么情况,程序没有出错,来到了上面下的断点处,如下图所示:
我们按 F8 一条一条执行指令,来到下图所示位置:
可以看到,消息码为 0x00000030,这是一条 WM_SETFONT 消息,CrackMe 并不会处理,会退出 DlgProc() 由 Windows 来处理该消息。还是一步一步 F8,来到 DlgProc() 最后一条指令,还是没有出错,如下图所示:
再一次按”F8“,来到了 User32.dll 的代码空间,如下图所示:
如上图所示,DlgProc() 在返回时,只弹出了 0x0C 字节的堆栈数据(返回时执行的是 retn 0x0C),还有一个 0x00000000 留在了堆栈中,导致了堆栈不平衡了。
这个 User32 的过程代码如下所示:
768046A0 55 push ebp
768046A1 8BEC mov ebp, esp
768046A3 56 push esi
768046A4 57 push edi
768046A5 53 push ebx
768046A6 68 CDABBADC push DCBAABCD ; 堆栈平衡标志
768046AB 56 push esi ; 这个重复压入,占坑的
768046AC FF75 18 push dword ptr ; 参数4
768046AF FF75 14 push dword ptr ; 参数3
768046B2 FF75 10 push dword ptr ; 参数2
768046B5 FF75 0C push dword ptr ; 参数1
768046B8 64:800D CA0F0000 01 or byte ptr fs:, 1
768046C0 8B4D 08 mov ecx, dword ptr
768046C3 FF15 C44F8676 call dword ptr ; USER32.76804780
768046C9 FFD1 call ecx ; 调用 DlgProc()
768046CB 64:8025 CA0F0000 FE and byte ptr fs:, 0FE
768046D3 817C24 04 CDABBADC cmp dword ptr , DCBAABCD ; 检查平衡标志
768046DB 74 11 je short 768046EE
768046DD 813C24 CDABBADC cmp dword ptr , DCBAABCD ; 检查平衡标志
768046E4 75 05 jnz short 768046EB
768046E6 83EC 04 sub esp, 4 ; 平衡堆栈(相当于重新 PUSH ESI)
768046E9 EB 03 jmp short 768046EE
768046EB 83C4 10 add esp, 10 ; 平衡堆栈(如果应用的 DlgProc() 没有pop掉4个参数的数据,则由 User32 自己来pop掉4个参数)
768046EE 83C4 08 add esp, 8 ; 平衡堆栈(pop掉ESI和“平衡标志”)
768046F1 5B pop ebx
768046F2 5F pop edi
768046F3 5E pop esi
768046F4 5D pop ebp
768046F5 C2 1400 retn 14
按 F8 执行,来到下图所示位置:
可以看到,由于找不到”平衡标志“,Windows 将再次弹出 0x18 字节的堆栈数据,ESP 将变成 0x0019FA34,然后再执行4个pop指令,ESP 变为 0x0019FA44,可以看到这个位置保存的是 0x00000030,如果这时还执行 ret 0x14 指令,就会出现我们先前看到的错误提示了,IP = 0x00000030 的错误。
如下图所示,我们先看看该过程的前面,将”平衡标志“压入堆栈后,还插入了一个ESI,这个对应于当 == 平衡标志 时,执行 sub esp, 4 时平衡用的。
然后,压入4个参数,这个是传递给 DlgProc() 的,共 0x10 字节,所以当 DlgProc() 只弹出 0x0C 字节时,会导致 User32 找不到平衡标志,从而弹出更多的堆栈数据,导致出错了。
我们先手动修复这个错误,如下图所示:
将 IP 重新指定为 0x768046D3(这个地址每个机器可能不一样),并将 ESP 的值加4,变成 0x0019FA20,再次 F8 单步运行,这次堆栈没有异常了,找到平衡标志了:
如上图所示,堆栈位置正常了。因为函数没有改动 ebx 的值,在执行 pop ebx 前,ebx == ,就表示应该没有问题了。
这样,我们就可以根据前面找到的原因对 CrackMe 进行修的复了,如下图所示,将返回指令 ret 0C 改成 ret 10 即可。
根据 DlgProc() 内的消息处理分支以及 WM_COMMAND 消息内的处理分支,一共有 6 个 ret 0C 需要改成 ret 10。
改好后,我们按前面保存文件的方法,将这些改动保存到 CrackMe 程序中,如下图所示,重新保到一个新的文件中:
保存后,我们先试试能否正常运行,取消前面在 DlgProc() 中下的断点,直接按 F9 运行,这次没有报错了,主界面出现了,如下图所示:
提示需要按”Register“按钮进行注册验证。
下面再看看 WM_COMMAND 消息的处理代码,如下图所示:
跟随跳转到 WM_COMMAND 的处理代码,如下图所示:
其有三个分支,对应界面上的三个按钮的事件处理,如下图所示,加了注释:
下面我们看看 CrackMe 的 Info,点击 "Info" 按钮,如下图所示:
没有反应,没有弹出显示 Info 的消息框。
我们跟随 ControlID = 0x66 的控件消息,可以看到,在 MessageBoxA() 的参数中,第一个参数应该是 hWnd 类型的参数,但 CrackMe 压入的是 hModule 参数,这个在 Windows 9x 下没有问题,但 Windows 10 下无效,导致无法显示消息框。
具体代码如下:
004010D6 > \6A 00 push 0 ; /Style = MB_OK|MB_APPLMODAL
004010D8 .68 A5224000 push 004022A5 ; |Title = "How to ... "
004010DD .68 78214000 push 00402178 ; |Text = "Nothing more simple ...",CR,LF,"Press Register and read ...",CR,LF,"In the box there must be Registered ... ;)",CR,LF,"Disasm arent Allowed (.. there is a little trick ...)",CR,LF,"Debugger like Sice allowed",CR,LF,CR,LF,CR,LF,"Send yo"...
004010E2 .FF35 87244000 push dword ptr ; |hOwner = 00400000
004010E8 .E8 7C080000 call <jmp.&USER32.MessageBoxA> ; \MessageBoxA
我们跟随这个 call MessageBoxA(),来到下图所示位置:
OD的提示区,显示有两个地方调用了 MessageBoxA() 函数,我们在提示区按右键,跳转到调用处,如下图所示:
将 push hModule 改成压入 DlgProc() 函数自己的 hWnd 参数,如上图所示,改成 push dword ptr 即可。
修改后代码如下所示:
004010D6 > \6A 00 push 0
004010D8 .68 A5224000 push 004022A5 ;ASCII "How to ... "
004010DD .68 78214000 push 00402178 ;ASCII "Nothing more simple ...",CR,LF,"Press Register and read ...",CR,LF,"In the box there must be Registered ... ;)",CR,LF,"Disasm arent Allowed (.. there is a little trick ...)",CR,LF,"Debugger like Sice allowed",CR,LF,CR,LF,CR,LF,"Send your"...
004010E2 FF75 08 push dword ptr
004010E5 90 nop
004010E6 90 nop
004010E7 90 nop
004010E8 .E8 7C080000 call <jmp.&USER32.MessageBoxA> ; \MessageBoxA
同样,另一处也进行同样的修改,如下图所示:
然后我们再一次把这些变动保存到文件中,如下图所示:
保存完后,我们先将那个 0x63 没有保存的问题处理一下,用 UltraEditor 等二进制工具打开文件,直接修改即可,如下图所示:
如上图所示的位置,直接将原有的 0x00 修改成我们需要的 0x63 即可,修改后保存文件。
现在我们重新在 OD 中载入我们改好的程序,继续进行测试,返回界面,点”Info“按钮,这次正常弹出消息框了:
下面,我们开始注册算法跟踪了,可以看到数据区中 0x0040234B 处已变成了 0x63,函数返回指令也已是 ret 10 了:
如上图所示,我们在”Register“分支跳转指令上下一个断点,进行动态跟踪。直接在界面点击”Register“,断下后,按"F8"来到下图所示位置:
这里就是处理注册的代码,第1条指令(0x004010F4处的指令)就是取出我们前面讲到的 0x63,按 F8 执行到如下位置:
可以看到,这段代码是将0x63作为异或解密参数生成一个字符串”ya.do“,继续向下看代码:
可以看到,这段代码是打开一个名叫”ya.do“的文件,判断大小,如果大小为21字节则读入其内容后,关闭文件。
我们先不要执行,先用工具生成一个21字节的文件,并保存为 "ya.do",如下图所示,用 ultraEditor 生成并保存:
文件内容先随便输入。按 F8 执行程序,如下图所示:
正常读入文件内容,上图OD数据区显示了文件内容。按 F8 继续执行,如下图所示,又是指令修改:
修改后面的跳转指令,跳转到注册算法代码。如下图所示:
指令已修改好了,将跳转到注册算法代码处。
这里,先把刚才改动的跳转代码又改回去了。
接下来,就是真正的算法代码了,如下图所示:
具体代码如下,这一小段代码处理文件内容的第1个字节 FILE:
00401414 .B8 78234000 mov eax, 00402378
00401419 .BB CE234000 mov ebx, 004023CE ;ASCII 04,"!:'6"
0040141E .B9 4C234000 mov ecx, 0040234C ;ecx-1 == 0x0040234B
00401423 .8A49 FF mov cl, byte ptr ;cl = 0x63, 未调试标志,ecx-1 = 0x0040234B
00401426 .80E9 10 sub cl, 10 ;cl = 0x53
00401429 .8A2B mov ch, byte ptr ;ch = 0x04, 常量数组:xor_Base
0040142B .32E9 xor ch, cl ;ch = 0x53 ^ xor_Base = 0x53 ^ 0x04 = 0x57
0040142D .8A30 mov dh, byte ptr ;dh = FILE,文件内容
0040142F .80C1 0D add cl, 0D ;cl = 0x53 + 0x0D = 0x60
00401432 .32F1 xor dh, cl ;FILE xor 0x60
00401434 .3AEE cmp ch, dh ;FILE ^ 0x60 == ch = 0x57 = 0x53 ^ xor_Base
00401436 .0F85 5B030000 jnz 00401797 ;FILE = 0x60^0x57=0x60^0x53^xor_Base = 0x37
可以看出,这里再一次读取了字节数据 0x63,作为 xor 操作的一个参数(只是在使用前先减去了 0x10 变成了 0x53)。
这里将文件的第1个字节与内存中一个常量字节数组进行 xor 处理,然后对处理结果进行检查。如果 FILE 不等于 0x37 就会退出算法,所以文件第1个字节 FILE = 0x37。
由于注册文件不对,我们直接跳过jnz指令,改变IP来到后一条指令,接下来继续看后面字节的处理:
具体代码如下:
0040143C .8A15 B1224000 mov dl, byte ptr ;dl = 加密的字符
00401442 .32D6 xor dl, dh ;解码
00401444 .8815 E8224000 mov byte ptr , dl ;保存解码字符
0040144A .83C0 01 add eax, 1 ;FILE++
0040144D .83C3 01 add ebx, 1 ;xor_Base++
00401450 .80E9 0D sub cl, 0D ;cl = 0x60 - 0x0D = 0x53,恢复原值
00401453 .8A2B mov ch, byte ptr ;ch = xor_Base
00401455 .32E9 xor ch, cl ;ch = xor_Base ^ 0x53
00401457 .8A30 mov dh, byte ptr ;dh = FILE
00401459 .80C1 0D add cl, 0D ;cl = 0x53 + 0x0D = 0x60,重新赋值 0x60
0040145C .32F1 xor dh, cl ;FILE ^ 0x60
0040145E .3AEE cmp ch, dh ;FILE ^ 0x60 == ch = xor_Base ^ 0x53
00401460 .0F85 31030000 jnz 00401797 ;FILE=0x60^0x53^xor_Base=0x33^0x21=0x12
00401466 .8A15 B2224000 mov dl, byte ptr ;由上面两个数据可以看出:
0040146C .32D6 xor dl, dh ;转换关系为:FILE[ i ]= xor_Base[ i ] ^ 0x33
0040146E .8815 E9224000 mov byte ptr , dl
可以看到,处理方式与 FILE字节一样,得到一个规律:FILE[ i ] = xor_Base[ i ] ^ 0x33。
常量字节数组如下:
004023CE04 21 3A 27 36 0A 32 37 3C 13 3B 3C 27 3E 32 3A
004023DE3F 7D 30 3C 3E 1A 02 4D 07 0C
后面的就不用看了,应该都是这样计算的,直接 F9 退出算法过程,由于我们的注册文件不对,所以会弹出注册错误:
不能注册!!!下面我们重新生成新的注册文件,写一段如下代码,计算文件内容:
#include <iostream>
int genSN();
int main(int argc, char** argv) {
genSN();
return 0;
}
int genSN() {
char xor_Base[] = {0x04, 0x21, 0x3A, 0x27, 0x36, 0x0A, 0x32, 0x37, 0x3C,
0x13, 0x3B, 0x3C, 0x27, 0x3E, 0x32, 0x3A, 0x3F, 0x7D, 0x30, 0x3C, 0x3E};
printf("Key file content for RingZ3r0 crackme:\n");
for(int i=0; i<21; i++) {
printf("%02X ", xor_Base[ i ] ^ 0x33);
}
printf("\n");
}
以上代码在 Dev-C++中调试通过,运行后输出如下:
Key file content for RingZ3r0 crackme:
37 12 09 14 05 39 01 04 0F 20 08 0F 14 0D 01 09 0C 4E 03 0F 0D
--------------------------------
Process exited after 0.02284 seconds with return value 0
请按任意键继续. . .
我们再次用 UltraEditor 对 "ya.do" 文件进行修改,如下图所示:
修改完后,保存文件。
然后再一次到 CrackMe 界面上点”Register“,断下后,直接按”F9"运行,弹出如下消息框:
表示注册成功!!!成功!!!改得好辛苦呀!!!
为了检查改动后的程序,在 Windows 9x 下是否也能运行,在虚拟机下测试,如下图所示:
仍然可以正常运行,表示程序现在可以运行在 Win9x和WinNT两种系列的内核下了,保证了100%的兼容性,虽然这没什么卵用。。。。。。{:1_925:}
分析完毕!!!
回写保护是什么意思? 学习了,谢谢! 感谢分享,学习了 本帖最后由 solly 于 2019-12-1 10:20 编辑
天空藍 发表于 2019-12-1 04:19
回写保护是什么意思?
随便取个名字,大概就是动态重写指令代码来解码恢复到正常指令代码或者改变指令执行流程的意思。。。。。
写贴花了几个小时,大概有4小时才写成并改好。 感谢分享 厉害,感谢分享 厉害,有几个地方没看懂 louties 发表于 2019-12-3 08:46
厉害,有几个地方没看懂
哪几个地方呀?{:1_904:} 感谢 ,吾爱破解论坛因你更精彩!
页:
[1]