OD消息断点的设置方法

一、条件断点:

使用方法(如):
在当前行按[Shift+F2]键->条件断点(这个不太好用,因为程序BUG偶尔失效)。
在当前行按[Shift+F4]键->条件记录断点(只要设置上条件语句和按什么条件生效就可以了)。

条件语句(如):
EAX == 00401000                      ; 当EAX的值为00401000时。
[EAX] == 05201314                    ; 比如EAX的值为00401000,而地址00401000处所指向的值等于5201314时,即EAX的值表示为指针。
[[EAX]] == 05201314                  ; 比如EAX的值为00401000,地址00401000处所指向的值为00402000,而地址00402000处所指向的值等于5201314时,即EAX的值表示为指针的指针。
EAX == 05201314 && EBX == 0x05201314 ; 当EAX的值等于5201314(十进制),并且EBX的值等于5201314(十六进制)时。
[EBP+8] == WM_COMMAND                ; 
[[EBP+8]] == 05201314                ; 
byte ptr[EAX] == 'y'                 ; 
[EAX] == "coderui"                   ; 比如EAX的值为00401000,而地址00401000处所指向的字符串为“coderui”时,即EAX的值表示为指针。
[[EAX+4]+4] == WM_LBUTTONUP          ;

二、消息断点:

原理:就是在消息函数上设置条件断点。

步骤:
1、使用[Ctrl+G]呼出“表达式跟随窗口”,输入“TranslateMessage”,然后回车。
2、在“转到”的位置上使用[Shift+F4]呼出“条件记录断点设置窗口”。
3、在“条件”中输入如下语句“[[ESP+4]]==当前按钮句柄&&[[ESP+4]+4]==WM_LBUTTONUP”。
4、把“暂停程序”设置为“按条件”,其他都为默认,然后确定。
5、点击按钮后,程序会停在“TranslateMessage”函数的系统领空中。
6、查看内存,对代码段下“内存访问断点”,然后经过多次[F9](运行),就会找到关键的处理代码了。


=================================================================================

以下演示如何下消息断点,

OD运行TraceMe.exe,

单击系统菜单View/Windows(查看/窗口)或单击工具栏的W按钮,如无内容,则执行右键菜单"Actualize"刷新命令

现在要对Check按钮下断点,当单击按钮时中断,在Check行上单击鼠标右键,在弹出的右键菜单中,选择"Message breakpoint  on ClassProc"

当单击事件发生时,会发送WM_LBUTTONUP这个消息,所以,选中这个消息,单击OK,设好消息断点:

单击Check按钮,将会中断到windows系统代码中,由于处于系统底层代码里,这时企图使用Alt+F9或Ctrl+F9返回TraceMe程序的领空代码是徒劳的,

所以用Ctrl+M打开内存区,对.text区块下一次性内存访问断点,如图:

按F9运行,立即中断在程序的空间004010D0处,这里正是程序的消息循环处:

注意的是,这段代码是一个消息循环,不停的处理TraceMe主界面的各类消息,因此可能不是直接处理按钮事件,

可以重复这个过程(其他过程会走到系统领域中,这时再下内存访问断点),在几次中断后到达处理按钮的事件代码,很快就能发现check按钮事件的代码:


===============================================================================================

  • 标 题:关于《OllyDBG 入门系列(五)-消息断点及 RUN 跟踪》的补充
  • 作 者:kisser1
  • 时 间:2007-06-18 09:52 
  • 链 接:http://bbs.pediy.com/showthread.php?t=46520

作 者: hklzt
时 间: 2007-06-06,16:50
联 系: QQ:87784858

看完了《OllyDBG 入门系统(五)-消息断点及 RUN 跟踪》之后感觉如何?会否有如下问题:
1、  是否觉得不知道在哪下断?
2、  为什么要这样子下断?
3、  如何确定断下来后的位置就是正确的?

好,就本着这几个问题来分析一遍。

首先,先回顾下Windows的消息机制。要点:所有要处理的消息必然会由程序自己处理,不处理的消息都交由Windows处理。Windows的消息处理函数的格式,如下:
LRESULT CALLBACK WindowProc(
    HWND hwnd,  // handle of window
    UINT uMsg,  // message identifier
    WPARAM wParam,  // first message parameter
    LPARAM lParam   // second message parameter
   );
其中uMsg就是关键,它代表消息的类型,如:WM_COMMAND,WM_GETTEXT等。记好哦。

下面,我们来用实例讲解。
OD消息断点的设置方法_第1张图片
这个CrackMe是用对话框做的(你是怎么知道的?)。
1、  用OllyDbg加载,Crtl + N,找到函数:USER32.DialogBoxParamA,右击->“在每个参考上设置断点”。
2、  F9,运行。看,被拦了下来,如图:
OD消息断点的设置方法_第2张图片

3、  其中DlgProc的内容,就是我们需要找的东东。这个地址是消息处理函数的入口点。现在来解释为什么要在窗口那才有消息断点,看图,
OD消息断点的设置方法_第3张图片
要下消息断点,首先得找到具有Windows消息处理函数格式的函数,然后,再根据栈的数据来判断消息,如果符合下断的消息,那么,OllyDbg就会拦下来(还会觉得不知道在哪下消息断点了吗?知道该如何下消息断点了吧?)。很明显有一点,这个消息断点的功能是有限的,比如,要拦主窗口中的菜单消息呢?所以,消息断点的功能还是有限的。如何扩展?扯远了,下面再讲。

4、  Ctrl + G来到cyle.0041029,我们来到了消息处理函数了,但是,OllyDbg并未识别出这个函数是消息处理函数。所以,在cyle.0041029处,右击->“分析”->“假定参数”,如图:
OD消息断点的设置方法_第4张图片


弹出一个对话框,选择“WinProc(hWnd,msg,wParam,lParam)”。 
OD消息断点的设置方法_第5张图片
点击“应用”后,如图。


5、在cyle.0041029处,右击->“断点”->“在WinProc上的消息断点”(平时是不是没见过这个菜单呀?呵呵)。


6、  在“消息”列表框中,选择你要下断的消息(Alt+B,删除以前的断点,以免影响心情)。

OD消息断点的设置方法_第6张图片
7、  F9,运行,程序运行起来了。这时没什么事情发生,当你在里面点了一个文本框之后(点它,是想让它获得Focus(焦点),以便能够输入数据),事情就发生了。现在没办法在文本框里输入注册码,也没办法点击按钮。这是怎么回事呢?仔细看一下Stack(栈)的那个窗口,噢!原来被文本框的通知EN_SETFOUCE搞坏了!停留在EN_SETFOCUS和EN_KILLFOCUS两个消息之间了。

8、  好,现在来扩展消息断点(消息断点是否是条件断点的特殊例子?),即使用条件断点(卖弄了一下,呵呵)。
OD消息断点的设置方法_第7张图片


看你需求,条件和条件记录,在这里是没什么区别的,因为不需要记录的内容。

9、  按Shitf+F2,输入 MSG ==WM_COMMAND && [ESP+C]==66(这个66是怎么来的?这个就是那个Check按钮的ID,意思就是“当收到WM_COMMAND,且是由ID为0x66发出的时候中断”),确定,F9,运行。

10、现在,输入Name和Serial之后,点”Check”按钮。

11、这次中断,位置上似乎没有变化,但是,明白了消息处理机制,应该知道这次中断的不同吧?(这次会流程会流到注册算法那哦)
00401029 >/.  >enter   0, 0                             ;  解码为 
0040102D  |.  >push    ebx
0040102E  |.  >push    edi
0040102F  |.  >push    esi
00401030  |.  >cmp     [arg.2], 110
00401037  |.  >je      short cycle.0040105E
00401039  |.  >cmp     [arg.2], 111  //111=WM_COMMAND
00401040  |.  >je      short cycle.00401082 //肯定在这里跳(为什么?)
00401042  |.  >cmp     [arg.2], 10
00401046  |.  >je      short cycle.00401057
00401048  |.  >cmp     [arg.2], 2
0040104C  |.  >je      short cycle.00401057
0040104E  |.  >xor     eax, eax

12、来到 cycle.00401082,
00401082  |> \>cmp     [arg.3], 67
00401086  |.  >jnz     short cycle.0040108D  //这里肯定会跳
00401088  |.  >call    cycle.00401151
0040108D  |>  >cmp     [arg.3], 66        
00401091  |.  >jnz     short cycle.00401098   //这里肯定不会跳(又是为什么呢?)
00401093  |.  >call    cycle.0040109C          //关键,
00401098  |>  >xor     eax, eax

13、蓝色部分,已经在CCDebuger那篇文章分析过了,这里就不分析了,我用红色字体来标明重点。
0040109C  /$  >mov     dword ptr ds:[402182], FEDCBA98
004010A6  |.  >push    11                               ; /Count = 11 (17.)
004010A8  |.  >push    cycle.00402171                   ; |Buffer = cycle.00402171
004010AD  |.  >push    3E9                              ; |ControlID = 3E9 (1001.)
004010B2  |.  >push    [arg.1]                          ; |hWnd
004010B5  |.  >call        ; \GetDlgItemTextA(Serial)
004010BA  |.  >or      eax, eax
004010BC  |.  >je      short cycle.0040111F
004010BE  |.  >push    11                               ; /Count = 11 (17.)
004010C0  |.  >push    cycle.00402160                   ; |Buffer = cycle.00402160
004010C5  |.  >push    3E8                              ; |ControlID = 3E8 (1000.)
004010CA  |.  >push    [arg.1]                          ; |hWnd
004010CD  |.  >call        ; \GetDlgItemTextA(name)
004010D2  |.  >or      eax, eax
004010D4  |.  >je      short cycle.0040111F
004010D6  |.  >mov     ecx, 10
004010DB  |.  >sub     ecx, eax
004010DD  |.  >mov     esi, cycle.00402160
004010E2  |.  >mov     edi, esi
004010E4  |.  >add     edi, eax
004010E6  |.  >cld
004010E7  |.  >rep     movs byte ptr es:[edi], byte ptr>
004010E9  |.  >xor     ecx, ecx
004010EB  |.  >mov     esi, cycle.00402171
004010F0  |>  >/inc     ecx
004010F1  |.  >|lods    byte ptr ds:[esi]
004010F2  |.  >|or      al, al
004010F4  |.  >|je      short cycle.00401100
004010F6  |.  >|cmp     al, 7E
004010F8  |.  >|jg      short cycle.00401100
004010FA  |.  >|cmp     al, 30
004010FC  |.  >|jb      short cycle.00401100
004010FE  |.^ >\jmp     short cycle.004010F0
00401100  |>  >cmp     ecx, 11      
00401103  |.  >jnz     short cycle.0040111F   //判断长度是否为16个有效字符,即16个字节,不是则跳
00401105  |.  >call    cycle.004011F1         //算法
0040110A  |.  >mov     ecx, 0FF01
0040110F  |.  >push    ecx
00401110  |.  >call    cycle.00401190       //算法
00401115  |.  >cmp     ecx, 1      
00401118  |.  >je      short cycle.00401120 //需要跳
0040111A  |>  >call    cycle.00401166
0040111F  |>  >retn
00401120  |>  >mov     eax, dword ptr ds:[402168]
00401125  |.  >mov     ebx, dword ptr ds:[40216C]
0040112B  |.  >xor     eax, ebx
0040112D  |.  >xor     eax, dword ptr ds:[402182]
00401133  |.  >or      eax, 40404040
00401138  |.  >and     eax, 77777777
0040113D  |.  >xor     eax, dword ptr ds:[402179]
00401143  |.  >xor     eax, dword ptr ds:[40217D]
00401149  |.^ >jnz     short cycle.0040111A  //不可以跳
0040114B  |.  >call    cycle.0040117B      //提示注册成功!
00401150  \.  >retn

终于写完了,现在来回顾下刚开始的问题:
1、  是否觉得不知道在哪下断?
答:在Windows消息处理函数的入口处下消息断点。
2、  为什么要这样子下断?
答:可能OllyDbg是根据栈的数据和函数原型来匹配,所以,一般来说,匹配条件都会是[Esp + XX] ==XXXXX
3、  如何确定断下来后的位置就是正确的?
答:这里是根据编程的思路以及Windows的消息处理机制来定位的,理论与实战相结合。

最后,总结下,由于windows是消息驱动的,很大一部分都是通过消息来完成的,所以,有很大一部分可以通过对消息下断来达到目的,但是,如何下消息断点?从大体上讲,是这样子的:1、找出消息循环处理的函数 2、在消息循环处理函数的入口处设断

写到这里,废话一下。赞扬CCDebuger的太多了,但是,在赞扬的同时,不知道大家有没仔细消化人家的成果?呵呵,其实,我也没有,因为我看不太懂,所以,还是照着自己的思路走。写这篇文章的目的是为了帮一位朋友,他想下消息断点,但是,不知道如何下,我就把CCDebuger的这那篇消息断点给他,可是,还是没解决,后来,自己也动了一下手,确实,对于WM_COMMAND消息来说,OD肯定会不停的拦下来的,所以,单纯的消息断点就行不通了,所以,再结合Run跟踪来记录下,刚好能解决问题,也就产生了CCDebuger的那篇文章(猜的,呵呵)。
最后,帮忙纠正下错误:
引用:
写到这准备跟踪算法时,才发现这个 crackme 还是挺复杂的,具体算法我就不写了,实在没那么多时间详细跟踪。有兴趣的可以跟一下,注册码是17位,用户名采用复制的方式扩展到 16 位,如我输入“CCDebuger”,扩展后就是“CCDebugerCCDebug”。大致是先取扩展后用户名的前 8 位和注册码的前 8 位,把用户名的前四位和后四位分别与注册码的前四位和后四位进行运算,算完后再把扩展后用户名的后 8 位和注册码的后 8 位分两部分,再与前面用户名和注册码的前 8 位计算后的值进行异或计算,最后结果等于 0 就成功。注册码的第 17 位我尚未发现有何用处。对于新手来说,可能这个 crackme 的难度大了一点。没关系,我们主要是学习 OllyDBG 的使用,方法掌握就可以了。

关于“位”的概念,“位”,是指二进制位,在这里,一个字节等于8位,一个字符等于一个字节。“注册码是17位”,应改成“注册码是17个字节”,如果你跟踪分析过,你可以发现,这样子还是不对的,最后应该是“注册码是16个字节”,
cmp     ecx, 11,这里的11是16进制,即十进制数:17。从代码中可以看出这个十进制数17,还得减1才是字符串的真实长度,所以,应该改成“注册码是16个字节”,后面的“位”,需要改成“字节”。第一次看的时候,没注意看,都给蒙了。


================================================================================================================

OllyDBG消息断点及RUN 跟踪讲解

找了几十个不同语言编写的 crackme,发现只用消息断点的话有很多并不能真正到达我们要找的关键位置,想想还是把消息断点和 RUN 跟踪结合在一起讲,更有效一点。关于消息断点的更多内容大家可以参考 jingulong 兄的那篇《几种典型程序Button处理代码的定位》的文章,堪称经典之作。今天仍然选择 crackmes.cjb.net 镜像打包中的一个名称为 cycle 的 crackme。按照惯例,我们先运行一下这个程序看看:

  \
我们输入用户名 CCDebuger,序列号 78787878,点上面那个“Check”按钮,呵, 没反应!看来是要注册码正确才有动静。现在关掉这个 crackme,用 PEiD 查一下壳,原来是 MASM32 / TASM32 [Overlay]。启动 OllyDBG 载入这个程序,F9让它运行。

这个程序按我们前面讲的采用字串参考或函数参考的方法都很容易断下来。但我们今天主要学习的是消息断点及 RUN 跟踪,就先用消息断点来断这个程序吧。在设消息断点前,有两个内容我们要简单了解一下:首先我们要了解的是消息。

Windows 的中文翻译就是“窗口”,而 Windows 上面的应用程序也都是通过窗口来与用户交互的。现在就有一个问题,应用程序是如何知道用户作了什么样的操作的?这里就要用到消息了。

Windows 是个基于消息的系统,它在应用程序开始执行后,为该程序创建一个“消息队列”,用来存放该程序可能创建的各种不同窗口的信息。比如你创建窗口、点击按钮、移动鼠标等等,都是通过消息来完成的。通俗的说,Windows 就像一个中间人,你要干什么事是先通知它,然后它才通过传递消息的方式通知应用程序作出相应的操作。

说到这,又有个问题了,在 Windows 下有多个程序都在运行,那我点了某个按钮,或把某个窗口最大化,Windows 知道我是点的哪个吗?这里就要说到另一个内容:句柄(handle)了。句柄一般是个 32 位的数,表示一个对象。Windows 通过使用句柄来标识它代表的对象。

比如你点击某个按钮,Windows 就是通过句柄来判断你是点击了那一个按钮,然后发送相应的消息通知程序。说完这些我们再回到我们调试的程序上来,你应该已经用 OllyDBG 把这个 crackme 载入并按 F9 键运行了吧?现在我们输入用户名“CCDebuger”,序列号“78787878”,先不要点那个“Check”按钮,我们来到 OllyDBG 中,点击菜单 查看->窗口(或者点击 工具 栏上那个“W”的图标),我们会看到以下内容:
OD消息断点的设置方法_第8张图片 
我们在选中的条目上点右键,再选择上图所示的菜单项,会来到下面这个窗口:
OD消息断点的设置方法_第9张图片 
现在我们点击图上的那个下拉菜单,呵,原来里面的消息真不少。这么多消息我们选哪个呢?注册是个按钮,我们就在按下按钮再松开时让程序中断。查一下 MSDN,我们知道这个消息应该是 WM_LBUTTON_UP,看字面意思也可以知道是左键松开时的消息:
OD消息断点的设置方法_第10张图片 
从下拉菜单中选中那个 202 WM_LBUTTON_UP,再按确定按钮,我们的消息断点就设好了。现在我们还要做一件事,就是把 RUN 跟踪打开。有人可能要问,这个 RUN 跟踪是干什么的?简单的说,RUN 跟踪就是把被调试程序执行过的指令保存下来,让你可以查看被调试程序运行期间干了哪些事。

RUN 跟踪会把地址、寄存器的内容、消息以及已知的操作数记录到 RUN 跟踪缓冲区中,你可以通过查看 RUN 跟踪的记录来了解程序执行了那些指令。在这还要注意一个缓冲区大小的问题,如果执行的指令太多,缓冲区满了的话,就会自动丢弃前面老的记录。我们可以在调试选项->跟踪中设置:
OD消息断点的设置方法_第11张图片 
现在我们回到 OllyDBG 中,点击菜单调试->打开或清除 RUN 跟踪(第一次点这个菜单是打开 RUN 跟踪,在打开的情况下点击就是清除 RUN 跟踪的记录,对 RUN 跟踪熟悉时还可以设置条件),保证当前在我们调试的程序领空,在反汇编窗口中点击右键,在弹出菜单中选择 RUN 跟踪->添加所有函数过程的入口:
OD消息断点的设置方法_第12张图片 
我们可以看到 OllyDBG 把识别出的函数过程都在前面加了灰色条:
OD消息断点的设置方法_第13张图片 
现在我们回到那个 crackme 中按那个“Check”按钮,被 OllyDBG 断下了:
\ 
这时我们点击菜单查看->内存,或者点击工具栏上那个“M”按钮(也可以按组合键 ALT+M),来到内存映射窗口:
OD消息断点的设置方法_第14张图片 
为什么在这里设访问断点,我也说一下。我们可以看一下常见的 PE 文件,没加过壳的用 PEiD 检测是这样:
OD消息断点的设置方法_第15张图片 
点一下 EP 段后面那个“>”符号,我们可以看到以下内容:
OD消息断点的设置方法_第16张图片 
看完上面的图我们应该了解为什么在 401000 处的代码段下访问断点了,我们这里的意思就是在消息断点断下后,只要按 F9 键运行时执行到程序代码段的指令我们就中断,这样就可以回到程序领空了(当然在 401000 处所在的段不是绝对的,我们主要是要看程序的代码段在什么位置,其实在上面图中 OllyDBG 内存窗口的“包含”栏中我们就可以看得很清楚了)。设好访问断点后我们按 F9 键,被 OllyDBG 断下:
OD消息断点的设置方法_第17张图片
现在我们先不管,按 F9 键(或者按 CTR+F12 组合键跟踪步过)让程序运行,再点击菜单查看->RUN 跟踪,或者点击工具栏上的那个“…”符号,打开 RUN 跟踪的记录窗口看看:
OD消息断点的设置方法_第18张图片 
我们现在再来看看统计的情况:
OD消息断点的设置方法_第19张图片 
在地址 401082 处的那条指令上双击一下,来到以下位置:
OD消息断点的设置方法_第20张图片 
现在我们在地址 4010A6 处的那条指令上按 F2,删除所有其它的断点,点菜单调试->关闭 RUN 跟踪,现在我们就可以开始分析了:

004010E2 |. 8BFE             MOV EDI,ESI                                         ; 用户名送 EDI
004010E4 |. 03F8             ADD EDI,EAX
004010E6 |. FC               CLD
004010E7 |. F3:A4            REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[ESI]
004010E9 |. 33C9             XOR ECX,ECX                                         ; 清零,设循环计数器
004010EB |. BE 71214000      MOV ESI,cycle.00402171                              ; 注册码送ESI
004010F0 |> 41               INC ECX
004010F1 |. AC               LODS BYTE PTR DS:[ESI]                              ; 取注册码的每个字符
004010F2 |. 0AC0             OR AL,AL                                            ; 判断是否为空
004010F4 |. 74 0A            JE SHORT cycle.00401100                             ; 没有则跳走
004010F6 |. 3C 7E            CMP AL,7E                                           ; 判断字符是否为非ASCII字符
004010F8 |. 7F 06            JG SHORT cycle.00401100                             ; 非ASCII字符跳走
004010FA |. 3C 30            CMP AL,30                                           ; 看是否小于30H,主要是判断是不是数字或字母等
004010FC |. 72 02            JB SHORT cycle.00401100                             ; 小于跳走
004010FE |.^ EB F0           JMP SHORT cycle.004010F0
00401100 |> 83F9 11          CMP ECX,11                                          ; 比较注册码位数,必须为十进制17位
00401103 |. 75 1A            JNZ SHORT cycle.0040111F
00401105 |. E8 E7000000      CALL cycle.004011F1                                 ; 关键,F7跟进去
0040110A |. B9 01FF0000      MOV ECX,0FF01
0040110F |. 51               PUSH ECX
00401110 |. E8 7B000000      CALL cycle.00401190                                 ; 关键,跟进去
00401115 |. 83F9 01          CMP ECX,1
00401118 |. 74 06            JE SHORT cycle.00401120
0040111A |> E8 47000000      CALL cycle.00401166                                 ; 注册失败对话框
0040111F |> C3               RETN
00401120 |> A1 68214000      MOV EAX,DWORD PTR DS:[402168]
00401125 |. 8B1D 6C214000    MOV EBX,DWORD PTR DS:[40216C]
0040112B |. 33C3             XOR EAX,EBX
0040112D |. 3305 82214000    XOR EAX,DWORD PTR DS:[402182]
00401133 |. 0D 40404040      OR EAX,40404040
00401138 |. 25 77777777      AND EAX,77777777
0040113D |. 3305 79214000    XOR EAX,DWORD PTR DS:[402179]
00401143 |. 3305 7D214000    XOR EAX,DWORD PTR DS:[40217D]
00401149 |.^ 75 CF           JNZ SHORT cycle.0040111A                             ; 这里跳走就完蛋
0040114B |. E8 2B000000      CALL cycle.0040117B                                  ; 注册成功对话框

写到这准备跟踪算法时,才发现这个 crackme 还是挺复杂的,具体算法我就不写了,实在没那么多时间详细跟踪。有兴趣的可以跟一下,注册码是17位,用户名采用复制的方式扩展到 16 位,如我输入“CCDebuger”,扩展后就是“CCDebugerCCDebug”。

大致是先取扩展后用户名的前 8 位和注册码的前 8 位,把用户名的前四位和后四位分别与注册码的前四位和后四位进行运算,算完后再把扩展后用户名的后 8 位和注册码的后 8 位分两部分,再与前面用户名和注册码的前 8 位计算后的值进行异或计算,最后结果等于 0 就成功。

注册码的第 17 位我尚未发现有何用处。对于新手来说,可能这个 crackme 的难度大了一点。没关系,我们主要是学习 OllyDBG 的使用,方法掌握就可以了。

最后说明一下:

1、这个程序在设置了消息断点后可以省略在代码段上设访问断点那一步,直接打开 RUN 跟踪,消息断点断下后按 CTR+F12 组合键让程序执行,RUN 跟踪记录中就可以找到关键地方。

2、对于这个程序,你可以不设消息断点,在输入用户名和注册码后先不按那个“Check”按钮,直接打开 RUN 跟踪,添加“所有函数过程的入口”后再回到程序中点“Check”按钮,这时在 OllyDBG 中打开 RUN 跟踪记录同样可以找到关键位置。

你可能感兴趣的:(数据逆向)