【文章标题】: Themida的另类破解
【下载地址】: 自己搜索下载
【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教!
--------------------------------------------------------------------------------
【详细过程】
大家都知道Themida的强大保护功能,几乎无人能完整的破解它的VM中的handler(最新进展如何?)。我也试图跟踪过它的几个handler,它为一个简单的入栈操作竟有长达数千行的垃圾代码和无穷的转跳,如果要把它168(?)个handler都清理出来,他一定会疯掉!或累死掉?我曾经下决心不碰Themida了,不知为什么鬼使神差的我又跟踪了几个Themida加密软件,竟然还有新的发现!
附件提供的一个程序是用汇编写的PE信息小工具,用Themida1855加密,加密选择RISC-64 processor,全选加密可选项,并对部分API函数使用VM加密,入口虚拟系数选择15。应该说对该程序用了较强的加密措施。现在介绍用我的脚本,一步步对它破解(绝不暗中使用对原程序已知的信息),居然还原了所有的代码,请看下面详细过程。
一、介绍一个万能脚本
1.下面的脚本适用于任何用Themida 1.8.5.5版加密的程序,操作十分简单。
--------------------------------------------------------------
//本脚本适用于Thmida 1.8.5.5版本加密的任何程序
bphwc
bc
jmp Second //第一次运行后,将本行注释掉后保存一次(本)脚本
data:
var mem
var mem1
var temIAT
var temESI
var temAPI
var APIstr
var Addr_0
var Addr_1
var Addr_2
var Addr_3
var Addr_4
var Addr_5
var Rva
Init:
mov Rva,65b970 //用对话框中的值修改该值,并将第一行的jmp Second注释掉
mov Addr_0,Rva-997
mov Addr_1,Rva-809
mov Addr_2,Rva-ea
mov Addr_3,Rva-9d
mov Addr_4,Rva
mov Addr_5,Rva+74c
start:
esto
esto
bphws Addr_0,"x"
esto
bphwc Addr_0
mov [Addr_0],0062e990 //废除监视断点的代码
bp Addr_1
bp Addr_2
bp Addr_3
bp Addr_4
bp Addr_5
First:
run
cmp eip,Addr_1
jz A1
cmp eip,Addr_2
jz A2
cmp eip,Addr_3
jz A3
cmp eip,Addr_4
jz A4
cmp eip,Addr_5
jz A5
jmp First
A1:
mov temAPI,eax //eax 是API函数地址
gn temAPI //显示函数名
log $RESULT
add APIstr," "
add APIstr,$RESULT_2 //恢复API函数名(INT表)
jmp First
A2:
mov temIAT,eax
cmp edx,10000
ja next
xor edx,80000000
mov [eax],edx //修复IAT表(写入序列号)
jmp First
next:
mov [eax],temAPI
jmp First
A3:
mov mem1,eax //eax是内存中呼叫API地址
mov temESI,[esi] //获取转跳标记
jmp First
A4:
cmp temESI,AAAAAAAA
jnz step3
mov [mem1],#FF25# //修复代码中转跳地址
mov [mem1+2],temIAT
jmp First
step3:
mov [mem1],#FF15# //修复代码中呼叫地址
mov [mem1+2],temIAT
jmp First
A5:
bc
pause
Second: //查找写IAT地址的一条指令
var memory
var tmp
var temp
var tmpbp
var Str
gmi eip,CODEBASE
mov memory,$RESULT
find memory,#0000000000000000000000000000000000000000000000#
mov tmp,$RESULT
bphws tmp,"w"
esto
esto
esto
bphwc tmp
sto
find memory,#909090909090909090909090909090909090#
mov temp,$RESULT+2
bphws temp,"w"
mov tmpbp,eip
run
cmp eip,tmpbp+2
jnz next2
run
next2:
itoa eip
mov Str,"请用下列数值:"
add Str,$RESULT
add Str," 修改脚本中的Rva."
bphwc
msg Str
pause //(脚本完)
本脚本的使用方法是:用OD打开程序后,如果第一次加载本脚本,则会弹出一个对话框,用对话框中的数据修改脚本中的Rva值(第20行,且只有一处),并一定将第3行的jmp Second注释掉,一定将脚本保存一次!这时脚本就是该程序的专用脚本了。重新用OD加载程序后,可以无须任何更改,直接调用本脚本了!
第2次运行脚本,屏幕快速闪动,一会就出现了神奇的效果了。如果想应用于新的程序,则只须将注释掉的jmp Second恢复即可。
----------------------------------------------------------------------------
2.挖掉Themida的一只监视眼,蒙上它的另一只眼
本脚本的运行原理,请参考我原先的一篇文章《对Themida加密VC++程序的完美脱壳》,当时我初次接触Themida,那个脚本有很多不完善的地方,如不容易进行到底,移植困难等。Themida有很多监视断点的SEH,当时没有采用相应措施,所以被Themida频频发现。它的“监视”方法很多,用得最多的大约有(1)int 1中断,(2)内存访问异常中断,(3)代码扫描等。对于(1)(2)没有任何办法摘除,但脚本的一个循环可与它周旋;对于(3)在我跟踪的代码段中,用脚本将它摘除了。所以脚本运行得非常顺利(使用在其它被Themida加密的程序中也一样)。
(1)脚本中的 mov [Addr_0],0062e990 就是将该地址的“jb”改成了“jmp”,跳过了代码扫描监视。---相当于挖掉了它的一只眼睛---
(2)Thmida的另一(多)只眼睛实在难于发现,不管你是什么类型的断点,很快被发现并单步异常后弹出对话框,然后退出运行。请注意,Themida设置的是单步中断,如果让它自行处理这个中断,就是前面的结果。如果将它拦截下来,自已处理就可以绕过了它的SEH。“拦截”方法就是打开OD“选项--调试设置--异常页”只选“非法访问内存”一项,其余全部不能选取。这样除非法访问内存异常让它自行处理外,其余全部交由OD处理。这样当Themida发现断点后产生的单步异常就被OD拦截了。自行处理的方式就是脚本中“First:”标签部分。(用脚本处理OD拦截下来的中断是非常容易的)
脚本中的First部分(断点循环)就是判断中断地址是不是自己设置的,是则进入相应操作;若不是,则一定是Thmida发现断点后的SEH中断地址(单步中断),处理方式就是用“run“。OD的这个run,就是让程序在eip中断地址上继续运行,绕过了Themida的SEH处理程序(陷阱)。---相当于蒙上了它的另一只眼睛----
脚本中的其余部分无须多说,请参考我的前一篇文章。
3.运行脚本后的神奇效果
用OD打开程序后,加载这个脚本,程序运行得很快。当它暂停后Themida外壳已经被除掉。转到代码段401000,让OD分析代码一次。如果Themida在code段中没有虚拟API函数,则显示在你面前的是一个非常清晰的可读的反汇编程序,各个API函数调用和名称显露无遗。如果你仅仅是想了解原程序的基本结构和思路,这就够了。如果Themida虚拟了部分API,请往下看。如果你一定要dump出来,并去掉垃圾代码,也请往下看。
二、恢复程序的入口代码
如果想进入它的VM(虚拟机)部分,清理它的handler,还原出原代码,我是绝对无能为力了,下决心不碰它了。
那么,我是怎样恢复入口代码的呢?跟踪Themida发现,它对解压出来的代码段并不重视,居然没有设置断点监视?你可以任意设断和修改,我巧妙地利用了它这一弱点。
1.设断技巧(以提供的PeInfo_them.exe为例)
用脚本脱壳后,转到401000代码区,让OD对代码分析一次,来到程序的尾部,API函数都集中在这里(称它为API表)(VC++程序的函数表分散在代码中,相对集中)。
004015FC $- FF25 5820400>jmp dword ptr [402058] ; USER32.wsprintfA
00401602 .- FF25 5420400>jmp dword ptr [402054] ; USER32.DialogBoxParamA <---F2设断
00401608 $- FF25 5020400>jmp dword ptr [402050] ; USER32.EndDialog
0040160E $- FF25 4C20400>jmp dword ptr [40204C] ; USER32.GetDlgItem
00401614 .- FF25 4820400>jmp dword ptr [402048] ; USER32.GetWindowTextLengthA <---F2设断
0040161A $- FF25 4420400>jmp dword ptr [402044] ; USER32.LoadIconA
00401620 .- FF25 4020400>jmp dword ptr [402040] ; USER32.MessageBoxA <---F2设断
00401626 .- FF25 3C20400>jmp dword ptr [40203C] ; USER32.SendMessageA <---F2设断
0040162C $- FF25 3820400>jmp dword ptr [402038] ; USER32.SetWindowTextA
00401632 $- FF25 2420400>jmp dword ptr [402024] ; kernel32.CloseHandle
00401638 $- FF25 2820400>jmp dword ptr [402028] ; kernel32.CreateFileA
0040163E $- FF25 2C20400>jmp dword ptr [40202C] ; kernel32.CreateFileMappingA
00401644 .- FF25 3020400>jmp dword ptr [402030] ; kernel32.ExitProcess <---F2设断
0040164A .- FF25 2020400>jmp dword ptr [402020] ; kernel32.FreeLibrary <---F2设断
00401650 $- FF25 1C20400>jmp dword ptr [40201C] ; kernel32.GetFileSize
00401656 .- FF25 1820400>jmp dword ptr [402018] ; kernel32.GetModuleHandleA <---F2设断
0040165C .- FF25 1420400>jmp dword ptr [402014] ; kernel32.LoadLibraryA <---F2设断
00401662 $- FF25 1020400>jmp dword ptr [402010] ; kernel32.MapViewOfFile
00401668 $- FF25 0C20400>jmp dword ptr [40200C] ; kernel32.UnmapViewOfFile
0040166E $- FF25 0820400>jmp dword ptr [402008] ; kernel32.lstrcpyA
00401674 $- FF25 0020400>jmp dword ptr [402000] ; comdlg32.GetOpenFileNameA
(1)注意到API表中有“$”的地方(如果全都没有,则再分析一次),$表示程序中有call在呼叫,若没有则表示没有关联(VC++编写的程序有很多无关联的垃圾函数)。在这里没有关联的函数肯定是被Themida虚拟了call代码。在没有“$”的地址上设F2中断。
明眼一看,本例是一个对话框窗口,它只有DialogBoxParamA,没有CreateWindowEx、消息循环等函数。若是Window窗口,则不能在消息循环函数上设断,否则OD将进入假死状态。
(2)在解压后代码段中浏览,发现了一段连续的乱码,用OD分析也无用,它一般就是入口代码段了。本例是4015B6--4015FB这一段(被虚拟了)。
(3)从401000开始寻找跳进006xxxxx段的jmp,这些jmp都是跳到被VM虚拟后的API代码中(代码E9前面都有一个“-”号),本例中跳进VM中的8个jmp地址如下:
0040100D .- E9 E0532300 jmp 006363F2
00401029 .- E9 408F2300 jmp 00639F6E
0040103E .- E9 95CC2300 jmp 0063DCD8
00401371 .- E9 4A092400 jmp 00641CC0
00401385 .- E9 F0422400 jmp 0064567A
004013C9 .- E9 B2802400 jmp 00649480
004013DD .- E9 61B52400 jmp 0064C943
0040151B .- E9 39F32400 jmp 00650859
在以上的每个地址都用F2设断。(放心设断,Themida是不来过问的。)
2.操作技巧(以后的操作基本上都是F9或alt-F9,无须shift-F9)
(1)当设置完成后,按F9运行一次,程序中断在
0040165C .- FF25 1420400>jmp dword ptr [402014] ; kernel32.LoadLibraryA
且在堆栈中出现
0013FF90 00654697 /CALL 到 LoadLibraryA
0013FF94 00402060 \FileName = "RichEd20.dll"
0013FF98 004001C0 ASCII " " <--这是应用程序开始的栈顶,dump堆栈平衡时弹出至此
0013FF9C 0013FFE0 指向下一个 SEH 记录的指针
0013FFA0 005E0DB2 SE处理程序
0013FFA4 7C930738 ntdll.7C930738
堆栈中0013FF98是进入应用程序后的栈顶,13FF94的(00402060)是压入的参数,0013FF90的(00654697)是VM中的呼叫地址。
记录下堆栈中的数据,并将4015B6开始的乱码改写为:(它就是解密出来的入口第一组代码)
push 00402060
call 0040165C <---虚拟机中怎样操作不知道,而还原后的代码就是call LoadLibraryA,即call 0040165C。
(2)继续F9一次,中断在
00401656 .- FF25 1820400>jmp dword ptr [402018] ; kernel32.GetModuleHandleA
堆栈中出现
0013FF90 006546A3 /CALL 到 GetModuleHandleA
0013FF94 00000000 \pModule = NULL
这里记下堆栈,接着前面如下修改乱码;
push 0
call 00401656 <---入口的第二组数据
(3)继续一次F9,(记录每一次中断后堆栈中的数据)中断在
00401602 .- FF25 5420400>jmp dword ptr [402054] ; USER32.DialogBoxParamA
堆栈是
0013FF80 006546BB /CALL 到 DialogBoxParamA
0013FF84 00400000 |hInst = 00400000 <--GetModuleHandleA的返回值
0013FF88 000003E8 |pTemplate = 3E8 <--资源ID
0013FF8C 00000000 |hOwner = NULL
0013FF90 00401544 |DlgProc = PeInfo_t.00401544 <--窗口程序地址
0013FF94 00000000 \lParam = NULL
因为程序进入了DialogBoxParamA后,不会退出,但初始化窗口时还会调用其它函数,只有关闭窗口后才退出。
这里将前面3次中断后堆栈中的值,加上必须保存的句柄,将入口处的乱码改写如下:
push 402060
call 0040165C ;LoadLibraryA
mov [xxxxx],eax <---返回RichEdit20A句柄
push 0
call 00401656 ;GetModuleHandleA
mov [xxxxx],eax <---返回GetModuleHandleA句柄
push 0
push 401544
push 0
push 3E8
push [xxxxx] <---它一定是GetModuleHandleA返回的句柄
call 00401602 ;DialogBoxParamA
打开403000全局变量区,每中断一次就会写入一些数据。你会发现LoadLibraryA句柄存入了[403004],GetModuleHandleA句柄存入了[403000],这样前面的三个[xxxxx]都解决了(需要一些API函数知识):即
push 402060 <---查402060是字串RichEd20.dll,原来是装载RichEd20库
call 0040165C ;call LoadLibraryA
mov [403004],eax <---返回RichEdit20A句柄
push 0
call 00401656 ;call GetModuleHandleA
mov [403000],eax <---返回GetModuleHandleA句柄
push 0
push 401544 <--DialogBoxParamA函数的Proc地址
push 0
push 3E8 <--窗口资源ID
push [403000] <---它是GetModuleHandleA返回的句柄
call 00401602 ;call DialogBoxParamA
这就是入口的开局部分。
提问:如果程序中有如:mov eax,12345678; mov ecx,eax;mov [403040],ecx;add eax,ecx等代码,怎么办?
应该说,每种语言编写的程序入口都有一种相对固定的格式,在窗口未建立之前一般不会有mov ecx,eax;add eax,ecx等代码,熟习了各种语言的程序入口代码对破解大有帮助。如果在脚本中设置了bpmw [40x000],ff([40x000]是数据区),则对找回如:mov [xxxxx],eax等代码有一定的帮助,但断点在VM中,要防止Themida的监视。退一步说,即使漏掉了一些这样的代码,dump后的程序不能运行,但对程序的结构和思路已经完全掌握了,目的已经达到了。
(4)继续一次F9,中断在地址401371,(窗口初始化调用的函数)
00401371 .- E9 4A092400 jmp 00641CC0
这是code中被虚拟了的API函数,将该地址设置的中断F2关闭(因为程序可能反复调用它,以后每当在前面的8个jmp上中断一次,就关闭该断点),再F9一次,中断在API表中的
00401626 .- FF25 3C20400>jmp dword ptr [40203C] ; USER32.SendMessageA
堆栈中是
0013FC14 00641CD7 /CALL 到 SendMessageA
0013FC18 004600B2 |hWnd = 4600B2
0013FC1C 00000080 |Message = WM_SETICON
0013FC20 00000001 |wParam = 1
0013FC24 0C1A027D \lParam = C1A027D
原来401371加密前应该是call 401625(即call SendMessageA),查看地址401371,参数非常清晰,你不必费力去研究它的参数来源,它是装载图标。
注意,这个call SendMessageA不是接在开局的call DialogBoxParamA后面,因为程序还没有退出DialogBoxParamA。
记下00401371地址,将要修改为call 401625(参数修复前不要修改,可能程序会反复调用它)。
……………………………………
这样反复F9,反复记下中断地址和中断函数,(每中断一个jmp的地址,就关闭它的断点(API表中的F2不能关闭)。这样凡是与初始化有关的被虚拟的API都现身了!若发现再F9,老在API表中的SendMessageA(或其它函数)上中断,这是初始化中的调用,运行时总要进入系统领空,可按alt-F9让其返回(在VM中),再F9回到用户领空。窗口未出现前可以先关闭SendMessageA的F2断点,但窗口出现后一定要恢复其断点。
(5)如果F9后,OD没有反应,但状态栏提示读取[FFFFFFFF]异常,不管它,这时,窗口已经出现了(看桌面状态条),程序进入了DialogBoxParamA中的消息循环。
到此为止,代码段中还有5个断点没有被访问,它们是窗口运行后才要访问的地址。
(6)按普通程序一样运行窗口,让它随便打开一个.exe文件,它立即中断在某jmp虚拟函数上,按前面同样的方法操作,在API函数表中就发现了它调用的API是谁了,这样就一个个地把Themida虚拟的函数清理完了。当代码段中的F2中断地址关闭完后。被VM虚拟的全部函数都现身了。(如何恢复参数,后面再说)
(7)反复运行窗口发现在代码中设置的断点0040151B9(jmp 00650859)始终没有被调用,而API表中的MessageBoxA也没有被调用,可以肯定0040151B9(jmp 00650859)是虚拟了call MessageBoxA。原来它是一个出错对话框,当你打开一个非exe文件时,对话框就出现了。在堆栈中也可以找到它全部参数。
(8)入口的后半部
当清理完代码段中的所有jmp断点后(被虚拟的API),可以关闭窗口了,按下关闭钮,立即中断在
0040164A .- FF25 2020400>jmp dword ptr [402020] ; kernel32.FreeLibrary
堆栈是
0013FF90 006546CA /CALL 到 FreeLibrary
0013FF94 74D90000 \hLibModule = 74D90000 <--LoadLibraryA的句柄
即:接着call DialogBoxParamA后面,按下面代码修改
push [403004]
call 40164A (call FreeLibrary)
再F9,中断在
0013FF90 004015FC (CALL 到 ExitProcess)
再F9,就退出了。那么,完整的入口代码是:
push 402060
call 0040165C
mov [403004],eax
push 0
call 00401656
mov [403000],eax
push 0
push 00401544
push 0
push 3E8
push [403000]
call 00401602
push [403004]
call 0040164A
push 0
call 004015FC
三、参数的恢复
API参数的设置,需要一些API知识,以本例为例简要说明,回到前面,401000这里有3个被虚拟的函数。前面的操作知道,先中断在一个jmp上,接着中断在API表中对应的API函数上。这样都清楚了它们是call xxxxxx。
00401000 $ 55 push ebp
00401001 . 8BEC mov ebp, esp
00401003 . 83C4 F8 add esp, -8
00401006 . 60 pushad
00401007 . FF35 0C304000 push dword ptr [40300C]
->0040100D .- E9 E0532300 jmp 006363F2 <--应该是call GetWindowTextLengthA
00401012 . D3F4 sal esp, cl
00401014 . 0D B38ACE8D or eax, 8DCE8AB3
00401019 . 45 inc ebp
0040101A . F8 clc
0040101B . 50 push eax
0040101C . 6A 00 push 0
0040101E . 68 37040000 push 437
00401023 . FF35 0C304000 push dword ptr [40300C]
->00401029 .- E9 408F2300 jmp 00639F6E <--应该是call SendMessageA
0040102E . 0B37 or esi, dword ptr [edi]
00401030 . B0 F7 mov al, 0F7
00401032 . F5 cmc
00401033 . 68 C2000000 push 0C2
00401038 . FF35 0C304000 push dword ptr [40300C]
->0040103E .- E9 95CC2300 jmp 0063DCD8 <--应该是call SendMessageA
00401043 . 46 inc esi
00401044 . 6F outs dx, dword ptr es:[edi]
00401045 . C2 0400 retn 4
(1)第一个call GetWindowTextLengthA,参数只有一个push [40300C],前面已知这是RichEdit20A的句柄,显然它是获取编辑软件RichEdit20A中的字符数,结果应该暂存,eax怎样操作?
(2)第2个是call SendMessageA,消息发往RichEdit20A,堆栈中的表示是:
0013F798 00639F81 /CALL 到 SendMessageA
0013F79C 00450292 |hWnd = 450292
0013F7A0 00000437 |Message = MSG(437)
0013F7A4 00000000 |wParam = 0
0013F7A8 0013F7CC \lParam = 13F7CC <--移动光标或选择范围的操作是该参数指向一个CHARRANGE结构(8字节)
这下难了,必须弄清RichEdit20A是干什么的?原来它是一个大型文本编辑软件,操作十分复杂,这里是大才小用,只用它来显示数据,并不对数据编辑。对RichEdit20A的操作主要是靠SendMessageA发送消息。它操作前,一定要确定光标位,确定光标位消息中的lParam参数指向一个CHARRANGE结构(2个dword),这两个值相等,其值是光标位置;若不等则表示选择一个区间。
清楚了,GetWindowTextLengthA获取屏幕上字符长度,第1个SendMessageA应该将光标定于尾部,好让第2个SendMessageA向它添加字符。那么第1个SendMessageA的参数13F7CC就一定是指向CHARRANGE结构,从ebp的值知13F7CC是指向[ebp-8],[ebp-4]即一个CHARRANGE结构,两个dword值应该相等且就是当前的字串长度。
这样,显然应该有:mov [ebp-8],eax;mov [ebp-4],eax。所以,代码应该如下:
call GetWindowTextLengthA
mov [ebp-8],eax
mov [ebp-4],eax
lea eax,[ebp-8]
push eax <---取CHARRANGE结构地址
push 0
push 437
push [40300C]
call SendMessageA
第2个SendMessageA一定是将buffer中的字串添加上去。它的参数lParam应该指向一个buffer。堆栈中参数是
0013F798 0063DCE9 /CALL 到 SendMessageA
0013F79C 00E20202 |hWnd = 450292
0013F7A0 000000C2 |Message = EM_REPLACESEL <-- EM_REPLACESEL=C2
0013F7A4 00000000 |wParam = 0
0013F7A8 0040222F \Text = "--------------------------------------
MSG消息EM_REPLACESEL也表示向RichEdit20A添加字符,参数lParam指向40222F,是直接push 40222F还是由调用程序带来?查调用401000的呼叫,知道它有多个地方调用,它的参数一定得由调用程序带来。
这样,接着的代码是
push [ebp+8] <--唯一的带来参数
push 0
push 0C2 <-- 0C2=EM_REPLACESEL
push [40300C]
call SendMessageA
(4)SendMessageA没有返回值,最后还有乱码46 6f的原码是什么?观察程序,前面有pushad,后面一定要有popad,又汇编程序是堆栈自动平衡方式,最后一般都有leave,最后这个C2 0400 (retn 4)是否是原码呢?应该说是,Themida一般不去惹call ebx、jmp eax、retn等代码,因为它的转跳是不定的,themida操作起来很复杂。
至此,这段代码被还原了。有的地方说法可能很勉强(因为有原码参考,所以我说得很肯定),用在其它地方可能就不正确了。不管怎么说,分析起来虽然很困难,但肯定比从VM虚拟代码中还原要容易得多了。
本方法有个致命的弱点是,一些与被虚拟的函数无关的代码,如add eax,6,mov ecx,edi等是永远找不回来的。好在是已经编译过的.exe文件再用Themida加密,如果不提供原编译文件,则只能对API虚拟了,包含它附近的几个代码。有关API的参数代码相对简单一些,很少有前面说到的那些代码形式,退后一步说,即使有,且被漏掉了,但所有的API都被还原了,关系清楚了,虽然dump后的程序不能运行,但破解的欲望达到了!
四、可能出现的问题
1.如果你用OD打开文件后加载脚本,OD象没有反应一样直接就运行到了窗口出现。
结论:你可能是第1次将脚本用到其它程序上而又忘记了将脚本中的jmp Second恢复过来,或者你忘记了存盘。
2.用脚本第1次弹出对话框没问题,第2次按要求修改也存了盘,但就是不出现任何结果。
结论:Themida版本可能不是1.8.5.5版。或者“调试设置--异常页”的选项选得太多,特别是选择了“忽略特权指令或无效指令”。
3.脚本不能运行到底,Themida弹出警告框后退出。
结论:“调试设置--异常页”的选项选得太多,特别是选择了“忽略单步异常”。
我说话很啰索,谢谢你耐心读完。
--------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!
2008年09月05日 10:41:32
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
上传的附件: