首页
社区
课程
招聘
[原创]Genocide Crackme #15 分析补完 —— 算法分析详解
发表于: 2008-10-31 21:25 8193

[原创]Genocide Crackme #15 分析补完 —— 算法分析详解

2008-10-31 21:25
8193

【文章标题】: Genocide Crackme #15 分析补完 —— 算法分析详解
【文章作者】: 书呆彭
【软件名称】: Genocide Crackme #15
【编写语言】: Delphi
【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教!
--------------------------------------------------------------------------------
【详细过程】
  上次我写了个定位关键代码的东西,能帮上大家,真是很荣幸。

  http://bbs.pediy.com/showthread.php?t=75638
  
  我原以为这个算法分析的部分比较容易,所以就略去了。
  
  后来看到有朋友回帖要让把算法分析的过程也说说。
  
  想一想,也是,自己当时刚接触这个的时候,对着大段的汇编代码,也曾经十分头疼。
  
  而一些高手写的文章,要么不屑于多说,因为对高手来说,阅读汇编代码跟阅读C代码一样简单;要么就是粘贴大段代码,把分析思路写在注释里,但一般只写分析如果,讲分析技巧的不是很多。
  
  这给刚入门的新人造成不小的困难,也是新人特别难过的一道槛。
  
  在下不才,虽不是什么高手,但毕竟阅读汇编代码也有不少心得了。就应大家的要求,正好以这个简单的CrackMe为例,详细地说说如何“读懂”汇编代码中的算法的技巧问题。
  
  此文着重于讲解技巧,不会采用粘贴完整的代码的方式,请大家自己用调试器找到完整的代码来看。
  
  我就按我的分析过程讲了,以文字为主,必要时粘贴代码片段。对代码的分析不放在注释当中。
  
  我也不知道这种写法大家看起来有没有困难。如果有什么意见,尽管请提就是了。
  
  
  
  上一篇文章已经找到了关键的代码位置,就接着上一次直接讲了。
  
  程序的关键函数是Form1.OnKeyPress()函数。我们在这里下断点后,运行程序,随便按键(我按下的是‘a’键),被OD中断下来。
  
  像上次讲的,我们看一下寄存器,发现ECX是我们感兴趣的。
  
  于是我们先看看函数中前几行代码对ECX的处理。
  
  先看到这一行
  
  00429964    >MOV     EBX, ECX
  
  好,那我们现在需要关注的是ECX,EBX两个寄存器了。再往下看,又看到
  
  0042997D    >MOV     DL, BYTE PTR DS:[EBX]
  0042997F    >CALL    gncrk.004036D4
  
  根据猜测,此时EBX指向一个包含我们输入的字符的字符串
  
  而这两条指令的意思很明显,那就是取我们所按键的 字符,然后用它作参数调用函数004036D4。
  
  根据经验,Delphi的编译器一般是用寄存器来传递参数的。前三个参数分别通过EAX,EDX,ECX来传递的。
  
  这个函数还有其它参数吗?我们再往上看几行代码,发现还给给寄存器EAX进行了赋值,相关代码完整如下:
  
  0042997A    >LEA     EAX, DWORD PTR SS:[EBP-68]
  0042997D    >MOV     DL, BYTE PTR DS:[EBX]
  0042997F    >CALL    gncrk.004036D4
  
  那么这个函数的第一个参数EAX,是一个地址 SS:[EBP-68]
  第二个参数DL,是一个char型的,正是我们输入的字符。
  
  那么关键的问题来了,这个函数是功能是什么呢?
  
  我们用鼠标点一下这一行,按ENTER,又是一个函数,再ENTER,又是几个函数,,,,
  
  如果一直进去看,一会就记不住我们进去几层了。不到不得已,我一般不采取这种“笨办法”,那怎么办???
  
  一个字,“猜”。
  
  我们来想一下,第一个参数是一个指针,指向[EBP-68],是一个局部变量
  
  我们写程序时用指针一般是用做输出的,所以我们“大胆”猜测,[EBP-68]这个变量用来接收函数的返回值。
  
  我们就从返回值来猜测,很多函数都用这种“猜测”的方法就能猜个八九不离十。
  
  我们把光标移动到这行:
  
  0042997A    >LEA     EAX, DWORD PTR SS:[EBP-68]
  
  然后按ENTER,我们看到数据窗口来到了 0012FD3C,也就是变量[EBP-68]的位置:
  
  0012FD3C       00 00 00 00 B0 FD 12 00 B4 FE 12 00 14 2B A0 00  ....褒.逮.+?
  
  初始值是0,然后我们用F8走过call那条指令,果然这个变量被赋值了,现在变成了:
  
  0012FD3C       20 10 A0 00 B0 FD 12 00 B4 FE 12 00 14 2B A0 00   ?褒.逮.+?
  
  它的值现在是00A01020,这个值是什么意思???怎么猜啊???
  
  这里又有一个经验的问题。我一看这个值它就像个堆空间的指针。我们在数据窗口CTRL+G,输入00A01020,来到了:
  
  00A01020       61 00 20 53 24 10 A0 00 24 10 A0 00 28 00 00 00  a. S$?$?(...
  
  这像是一个长度为1的字符串,内容刚好是我们输入的字符。是不是呢???再确定一下吧。再F9一次,这次按个,比如‘1’,然后在OD里重复上面的步骤,用F8走过那个CALL。
  
  这回[EBP-68]的值是00A01030,来到这里一看,果然是我们期望的字符串“1”:
  
  00A01030       31 00 00 00 34 10 A0 00 34 10 A0 00 18 00 00 00  1...4?4?...
  
  那好,我们来到004036D4,按分号(:),给它加个标签,比如我叫它 CharToStr,现在原来的代码成了:
  
  0042997A    >LEA     EAX, DWORD PTR SS:[EBP-68]
  0042997D    >MOV     DL, BYTE PTR DS:[EBX]
  0042997F    >CALL    <gncrk.CharToString>
  
  现在我们记住了一个变量,即[EBP-68],所以往下看代码时就需要注意这个值。
  
  我们发现它马上就被使用了。
  
  00429984    >MOV     EDX, DWORD PTR SS:[EBP-68]
  00429987    >MOV     EAX, gncrk.0042B714
  0042998C    >CALL    gncrk.004037B4
  
  这里又遇到两个地址,一个是变量0042B714,一个是函数004037B4
  
  我们先看看变量0042B714是个什么东西:
  
  0042B714       20 10 A0 00 FF FF FF FF 00 00 00 00 00 00 00 00   ?........
  
  而00A01020指向一个字符串,内容是我们刚才输入的“a”。
  
  那这个是我们上一次按键输入的内容呢,还是我们前面所有输入的内容呢?我们还需要验证一下。再次F9,再按一个键,这次我按下了‘B’,然后在OD中查看,0042B714处的值还是00A01020:
  
  00A01020       61 31 00 53 24 10 A0 00 24 10 A0 00 28 00 00 00  a1.S$?$?(...
  
  看到了吧,这就是我们刚才输入的“a1”,同时我很有理由相信函数004037B4就是我们熟悉的strcat()。
  
  我们再次F8过去,再看数据窗口,已经变成了:
  
  00A01020       61 31 42 00 12 00 00 00 01 00 00 00 01 00 00 00  a1B..........
  
  正是我们期待的“a1B”。
  
  我们每推断出一个名字,就立即加上标签。数据和函数都加。加完标签后,现在这段代码是不是很容易懂了??
  
  00429984    >MOV     EDX, DWORD PTR SS:[EBP-68]
  00429987    >MOV     EAX, <gncrk.pInputBuffer>
  0042998C    >CALL    <gncrk.strcat>
  
  再往下看,下面是一个条件分支:
  
  00429991    >CMP     DWORD PTR DS:[42B710], 0C
  00429998    >JNZ     gncrk.00429B02
  
  现在有一个地址,它是个变量,0042B710。而当这个变量值不等于12时,会向下跳转,到跳转的目标地址看一下,发现已经接近retn了。
  
  我们现在知道了,我们每按一个键,程序就把它追加到pInputBuffer之中。
  
  由此们很自然地想到,变量42B710记录的是我们输入了多少个字符,而且正确的字符应该是12个,否则就不进行处理。
  
  我们先暂时把它命名为inputCount,然后我们发现,上面的我们跳过的代码中有一行发生了变化,它的地址用标签名代替了,这一行是:
  
  00429974    >INC     DWORD PTR DS:[<inputCount>]
  
  这条指令使我有百分之九十的把握相信,这个变量确实是inputCount。
  
  
  这里也看出使用标签不仅使代码更容易理解,而且还有其它的好处。
  
  如果不使用标签,那么 INC 和 CMP 这两条指令中的地址,我们很难注意到它们是同一个地址,因为汇编代码中的地址太多了,根本无法去记住。
  
  而当我们给这个地址加上标签后,所有引用这个变量的地方,OD全部会用标签名来显示,我们马上就知道了在一个地方赋值的变量都在哪里使用了。
  
  
  再回到程序。既然它要求输入长度为12,那我们就多输入几次。
  
  当然,每输入一次,被OD断下后再按一次F9,然后再输入下一次,再断,再F9。这是一种方法。
  
  我采用的方法是OD的条件断点功能。在上一篇文章中已经说过了。这里假设我们已经输入了12个字符。
  
  然后F8到
  
  00429998   />JNZ     gncrk.00429B02
  
  OD显示跳转的箭头是灰色,不跳。我们再往下分析。下面几行代码是一个循环:
  
  0042999E    >MOV     EBX, 0C
  004299A3    >MOV     EAX, DWORD PTR DS:[<inputCount>]
  004299A8    >XOR     EDX, EDX
  004299AA    >MOV     DWORD PTR SS:[EBP+EAX*4-68], EDX
  004299AE    >DEC     EBX
  004299AF  ^ >JNZ     SHORT gncrk.004299A3
  
  这几行代码的功能,我想大家都能很容易看出来吧,对了,它是作者故意扰乱我们的,是一段完全可以忽略的代码。
  
  过了这几行,再往下看:
  
  004299B1    >MOV     EAX, DWORD PTR DS:[<pInputBuffer>]
  004299B6    >MOV     AL, BYTE PTR DS:[EAX]
  004299B8    >XOR     AL, 47
  004299BA    >AND     EAX, 0FF
  004299BF    >MOV     DWORD PTR SS:[EBP-34], EAX
  004299C2    >MOV     EAX, DWORD PTR DS:[<pInputBuffer>]
  004299C7    >MOV     AL, BYTE PTR DS:[EAX+1]
  004299CA    >XOR     AL, 45
  004299CC    >AND     EAX, 0FF
  004299D1    >MOV     DWORD PTR SS:[EBP-30], EAX
  004299D4    >MOV     EAX, DWORD PTR DS:[<pInputBuffer>]
  004299D9    >MOV     AL, BYTE PTR DS:[EAX+2]
  004299DC    >XOR     AL, 4E
  004299DE    >AND     EAX, 0FF
  004299E3    >MOV     DWORD PTR SS:[EBP-2C], EAX
  
  。。。
  
  
  这可不是垃圾代码了。像这样的代码,共有12段,它们的结构相同,只是每段中异或的常数不同(注:上一篇文章中我说用笔抄几个常数,就是指这12个常数)
  
  所有代码实现的功能就是把pInputBuffer中的字节进行异或加密,然后扩展成为32位整数值,再复制到栈上的局部空间。
  
  输入的12个字节分别被复制到栈上的 [EBP-34],[EBP-30]……[EBP-8];
  
  我们记这12个整数为一个数组,记为 int array1[12];
  
  
  再往下看:
  
  00429A88    >MOV     EAX, DWORD PTR SS:[EBP-34]                   ; array1[0]
  00429A8B    >XOR     EAX, DWORD PTR SS:[EBP-30]                   ; ^= array1[1]
  00429A8E    >MOV     DWORD PTR SS:[EBP-64], EAX                   ; [EBP-64]=
  
  这三条指令的伪代码描述是:  local_64 = array1[0] ^ array1[1];
  
  其中local_64表示一个局部变量,由于我们目前不知道它的意义,暂且这么表示。在IDA中,一般表示为Var_64.
  
  接下来的循环有点意思,为了清晰,我在中间空了一行,实际中这是一整段代码:
  
  00429A91    >MOV     EBX, 0A
  00429A96    >LEA     EDX, DWORD PTR SS:[EBP-2C]
  00429A99    >LEA     EAX, DWORD PTR SS:[EBP-64]
  
  00429A9C    /MOV     ECX, DWORD PTR DS:[EDX]
  00429A9E    |XOR     ECX, DWORD PTR DS:[EAX]
  00429AA0    |MOV     DWORD PTR DS:[EAX+4], ECX
  00429AA3    |ADD     EAX, 4
  00429AA6    |ADD     EDX, 4
  00429AA9    |DEC     EBX
  00429AAA  ^ \JNZ     SHORT gncrk.00429A9C
  
  
  先看这一个循环是做什么的。
  
  分析循环过程,我的方法是先不管循环的初始条件和结束条件,以及循环控制变量的自增,只看循环体内的代码。
  
  循环体内的代码非常简单,把[EDX]中的32位值拿出来,跟[EAX]中的值进行异或,然后存入[EAX+4]中。
  
  而每次循环,EDX和EAX都增加4,即指向下数组的下一个位置。
  
  而循环次数呢,是10(0x0A)。
  
  
  之后我们再看循环的初始条件
  
  00429A96    >LEA     EDX, DWORD PTR SS:[EBP-2C]
  00429A99    >LEA     EAX, DWORD PTR SS:[EBP-64]
  
  EDX最开始指向[EBP-2C],我们上面已经分析了,就是 EBX = &array1[2];
  EAX最开始指向[EBP-64],我们上面把它暂命名为local_64,现在看来它也是个数组,那么就叫array2吧,注意这个数组元素是11。
  
  我们把这个循环也写成伪C代码吧,同时把循环之前的那几句代码加上,得到这整段代码的功能描述:
  
  array2[0] = array1[0] ^ array1[1];
  
  int  i,* pSrc,*pDst;
  
  
  for ( i = 0x0A, pSrc = & array1[2], pDst= &array2[0] ; i > 0; --i )
  {
         pDst[1] = pDst[0] ^ pSrc[0];
  }
  
  
  再下面是最后一次处理了:
  
  00429AAC    >MOV     EBX, 0B                                      ; for (int i = 11; i > 0; --i)
  00429AB1    >LEA     ESI, DWORD PTR SS:[EBP-64]
  00429AB4    /LEA     EDX, DWORD PTR SS:[EBP-68]
  00429AB7    |MOV     EAX, DWORD PTR DS:[ESI]
  00429AB9    |CALL    gncrk.00406528
  00429ABE    |MOV     EDX, DWORD PTR SS:[EBP-68]
  00429AC1    |LEA     EAX, DWORD PTR SS:[EBP-4]
  00429AC4    |CALL    <gncrk.strcat>
  00429AC9    |ADD     ESI, 4
  00429ACC    |DEC     EBX
  00429ACD  ^ \JNZ     SHORT gncrk.00429AB4
  
  这段又是做什么呢???还是分析循环,我们看到在循环体中有两次函数调用。
  
  第二次调用的函数我们已经标出来了(记得吗,在前面),即 strcat()函数
  
  现在我们要再次施展我们的“猜测”大法,来得到第一个函数的内容。
  
  我们先进这个函数里看一下:
  
  00406528    >ADD     ESP, -8
  0040652B    >PUSH    0
  0040652D    >MOV     DWORD PTR SS:[ESP+4], EAX
  00406531    >MOV     BYTE PTR SS:[ESP+8], 0
  00406536    >LEA     ECX, DWORD PTR SS:[ESP+4]
  0040653A    >MOV     EAX, EDX
  0040653C    >MOV     EDX, gncrk.00406554                          ; ASCII "%d"
  00406541    >CALL    gncrk.00406DD0
  00406546    >POP     ECX
  00406547    >POP     EDX
  00406548    >RETN
  
  眼睛一亮,对不对???
  
  我们看到了一个非常熟悉的字符串——"%d",不用说,CALL    gncrk.00406DD0,一定是sprintf了。
  
  那这个函数我们怎么命名呢?? 其实它就是itoa()。我们同样给它加上标签,再回到刚才的代码,是不是豁然开朗???
  
  00429AAC    >MOV     EBX, 0B                                      ; for (int i = 11; i > 0; --i)
  00429AB1    >LEA     ESI, DWORD PTR SS:[EBP-64]
  00429AB4    >LEA     EDX, DWORD PTR SS:[EBP-68]
  00429AB7    >MOV     EAX, DWORD PTR DS:[ESI]
  00429AB9    >CALL    <gncrk.itoa>
  00429ABE    >MOV     EDX, DWORD PTR SS:[EBP-68]
  00429AC1    >LEA     EAX, DWORD PTR SS:[EBP-4]
  00429AC4    >CALL    <gncrk.strcat>
  00429AC9    >ADD     ESI, 4
  00429ACC    >DEC     EBX
  00429ACD  ^ >JNZ     SHORT gncrk.00429AB4
  
  这里用到的变量是[EBP-68],我们记为tmpstr;
  以及[EBP-4],我们记为string1。
  
  
  再往下,我们便看到胜利的曙光了:
  
  00429ACF    >MOV     EAX, DWORD PTR SS:[EBP-4]
  00429AD2    >MOV     EDX, gncrk.00429B48                          ; ASCII "2517170143162210624"
  00429AD7    >CALL    gncrk.004038BC
  00429ADC    /JNZ     SHORT gncrk.00429AF1
  00429ADE    |PUSH    0
  00429AE0    |PUSH    gncrk.00429B5C                               ; ASCII "Congratulations!!!"
  00429AE5    |PUSH    gncrk.00429B70                               ; ASCII "Good work!"
  00429AEA    |PUSH    0
  00429AEC    |CALL    <JMP.&USER32.MessageBoxA>
  00429AF1    \MOV     EAX, <gncrk.pInputBuffer>
  00429AF6    >CALL    <gncrk.free>                                 ; 这个是释放内存的,猜测
  
  不用多说,CALL    gncrk.004038BC一定是strcmp()函数(我没有用实验验证,而是后面算出注册码后间接地证明了)。
  
  虽然已无必要,我们还是给它加个标签吧。
  
  到这里,我们的算法已经大白于天下了。
  
  先把输入的12个字符,分别用异或的方式处理后扩展为整数,得到数组array1。
  
  array1的开头两个元素异或的结果得到另一个数组array2的第一个元素,而array2后续的每一个元素都是前一元素与array1中顺序往下排的元素的异或值。
  
  最后,把array2的元素依次转化成字符串,并拼接,得到一个字符串string1。
  
  如果string1恰好是“2517170143162210624”这个值,则输入的字符序列被接受。
  
  
  
  好了,我的分析就讲到这里了。至于从“2517170143162210624”这个串反推注册码的算法,就留给读者了,因为它并不复杂。
  
  
  小提示:需要将这个串切割成为11个子串。原则上可以任意划分,但是如果逆推出的字符序列包含不可打印字符,或者说无法从键盘直接输入的字符,是不可以。
  
  具体每个子串应该满足什么条件就呢?所以实际上可行的划分只有几种而已。
  
  就提示到这里。
  
  顺便给出一组可以满足条件的注册码吧,括号中间的,注意半角符号,字母大写(又一个提示):
  
  【_DFORGU\SNII】
  
  
  
  
--------------------------------------------------------------------------------
【经验总结】
  本文详细地分析介绍了这个CrackMe的分析过程,详细到我的每一步具体思路都包括进来了。
  
  所以虽然短短几行代码,却写了很长的东西。如果采取直接粘代码,然后在注释中解释的写法,相信会短得多,不过正如文
  章开头所说,这有违我写此文的目的。
  
  如果你的水平在我之上,分析这个小程序完全不在话下,就略过不看就是了。
  
  另外,我是第一次写这样的文章,不知道效果如何,欢迎大家提意见。
  
  
  需要说明的是,文章中提到的strcat和sprintf,我在上一篇文章中说是operator+(string,string)和string::format()
  
  实际上Delphi中的字符串不像C中那样,是“以空字符'\0'结束的字节数组”,而更像是C++中的std::string
  
  本文为了使只学过C的读者便于理解,就用C的方式来说了。
  
  实际上大家在内存中定位到字符串的缓冲区,往前翻一下,就会看到,在字符串之前有一个整数,用来存放字符串的长度的
  。所以实际上,上面提到的函数虽然是Delphi的函数,但相比于C,它与C++更接近。
  
--------------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!

                                                       2008年10月31日 21:20:16


[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 7
支持
分享
最新回复 (11)
雪    币: 201
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
沙发是我的,好好学习一下
2008-10-31 22:22
0
雪    币: 201
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
写的很好。。。发现新手读完这个真的需要毅力的。。。唉。。。停电了。。明天早上起来继续看,然后实践。。。谢谢这么详细的新手能看懂的文章。。
2008-10-31 22:58
0
雪    币: 201
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
这个程序的计算过程我已经明白了,它的比较方式我也大概理解了,就是先输入12个字符(相当于注册名),然后这12个字符中的每一个都与一个不同的常数异或,每次异或后的值以字符的形式相连接,形成一个字符型数组sn1[11],sn1[0] xor sn1[1]=sn2[0],然后就是一个循环:这里我认为先要声明一个数组,sn4[10],sn4[i]=sn1[i+1],sn2[0+i] xor sn4[i],i为循环变量,为10,然后把计算后的结果累加成一个数组,sn3[11],把这个数组的每个元素转换成字符就OK了,读到这边,会发现只算到了11位,没算到12位,就是sn2[11],我也不知道sn[11]怎么算的,麻烦楼主解释释一下:)

还有就是一个字符的Ascii都是由一个两位数构成的,可是这里相比较的数据却达不到12位,请看下面:
25 17 17 01 43 16 22 10 62 4→总共是10位,而且最后一位还是一位数字,这怎么比较呢?
2008-10-31 23:08
0
雪    币: 2110
活跃值: (21)
能力值: (RANK:260 )
在线值:
发帖
回帖
粉丝
5
[QUOTE=那年夏天;528821]这个程序的计算过程我已经明白了,它的比较方式我也大概理解了,就是先输入12个字符(相当于注册名),然后这12个字符中的每一个都与一个不同的常数异或,每次异或后的值以字符的形式相连接,形成一个字符型数组sn1[11],sn1[0] xor sn1[1]=sn2[0],然后就是一个循环:这里我认为先要声...[/QUOTE]

第一个问题,请看我文中给出的伪C描述:

array2[0] = array1[0] ^ array1[1];
  
  int  i,* pSrc,*pDst;
  
  
  for ( i = 0x0A, pSrc = & array1[2], pDst= &array2[0] ; i > 0; --i )
  {
         pDst[1] = pDst[0] ^ pSrc[0];
  }

循环展开后,就是:

array2[0] = array1[0] ^ array1[1];
array2[1] = array2[0] ^ array1[2];
array2[2] = array2[1] ^ array1[3];
...
array2[10]= array2[9] ^ array1[11]

即从一个12个元素的数组array1映射到一个11个元素的数组array2

至于你说到的第3,4个数组,汇编指令中很多变量都是编译器生成的临时变量。

比如,下面一个C的表达式,

a ^= ( a % 0x20 ) + a * a;

在看反汇编时,会多出来许多中间过程的变量,而我们在还原算法时,要能识别这些临时变量,并将其舍去,这样才能还原出来容易理解的C的高级表达式的形式。

在分析中,我们可能会把每一步都记下来,但最后我们要学会化简,将中间的临时变量都略去。

拿上例来说,我们可能得到这样一些中间步骤:

eax = [ebp - 8];

ecx = eax;

edx = eax % 0x20

[ebp - 0x0C] = edx

edx:eax = eax * ecx;

edx = [ebp-0x0C]

eax = eax + edx

eax = eax ^ ecx

[ebp-8]=eax

但最终,我们是能还原出原表达式的。

关于最后那个串的问题,这个需要自己好好想清楚。我直接说出来就没意思了。

可以再提示你一下,对array2的处理那个循环,itoa() 函数是转化成十进制的字串。

而且,不一定是两位数哦!
2008-11-1 00:29
0
雪    币: 200
活跃值: (46)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
这样的才看得懂,谢谢书呆彭!
2008-11-3 23:42
0
雪    币: 200
活跃值: (14)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
谢谢LZ的详细发现哈。
2008-12-18 16:56
0
雪    币: 33
能力值: (RANK:10 )
在线值:
发帖
回帖
粉丝
8
我们先暂时把它命名为inputCount,然后我们发现,上面的我们跳过的代码中有一行发生了变化,它的地址用标签名代替了,这一行是:
  
  00429974    >INC     DWORD PTR DS:[<inputCount>]
  
上面这段话里面怎么给地址使用标签?请知道的人说一下,谢谢!
2008-12-29 15:18
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
太高深了,打印,躺着看
2008-12-31 09:30
0
雪    币: 2242
活跃值: (169)
能力值: ( LV11,RANK:180 )
在线值:
发帖
回帖
粉丝
10
这是一片我看得最懂得文章了,太感谢你了
2008-12-31 22:02
0
雪    币: 239
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
  很好的文
2009-3-24 01:38
0
雪    币: 7
活跃值: (135)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
[QUOTE=ikea;557705]我们先暂时把它命名为inputCount,然后我们发现,上面的我们跳过的代码中有一行发生了变化,它的地址用标签名代替了,这一行是:
  
  00429974    >INC     DWORD PTR DS:[<inputCount>]
  
上面这段话里面怎么给地址使用...[/QUOTE]

CTRL + G到这个地址处, 按";",输入标签名就OK
2009-3-26 22:24
0
游客
登录 | 注册 方可回帖
返回
//