第七章更新...
第七章
call和ret
我故意把call和ret指令留到这个部分来讲,因为你们必须得弄明白前面的基础知识,有了前面的基础,我们现在来介绍call和ret。
尽管这两条指令看起来很简单,但是很多初学者并不能完全领会这两条指令的本质。所以我把它们作为一个单独的章节来介绍,我认为这样做是可取的。
让我们继续用OD载入CrueHead'а的CrackMe。
在任意一行点击鼠标右键选择-“Go to”-“Expression”。
在弹出的对话框中输入401245。
直接跳转到一个反汇编代码的地址处,这里有一个Call指令,我们拿它来练习。
我们在练习的这个Call指令上面单击鼠标右键选择-“New origin here”。现在,EIP的值就变成了401245,这就意味着下一条要执行的指令就是我们的这个Call指令。
我们将看到EIP被修改了。
我们回到我们Call指令这里。
Call指令是将转移到指定的子程序处,它的操作数就是给定的地址。例如:
Call 401362表示将转移到地址401362处,将调用401362处的子程序,一旦子程序调用完毕就返回到Call指令的下一条语句处。
在这种情况下,完成401362的子程序调用以后,则会返回到40124A地址处。
针对于call指令,OD提供了一些有用的跟踪机制。如果想继续跟进子程序内部,你可以按F7键跟进。如果只是想先看看子程序里面的内容再决定要不要跟进的话,可以单击鼠标右键-“Follow”。最后,如果我们不想继续跟踪该子程序了,我们可以按下F8键,继续执行call指令的下一条语句。
OD允许我们看我们感兴趣的子程序里面的内容,但是不执行,只需要在call指令上面单击鼠标右键选择“Follow”。
正如你所看到的,EIP的值仍然是401245。“Follow”按钮只是简单的将指定地址处代码反汇编,但是并不会执行代码,并且等待我们进一步的操作。
我们这个子程序起始地址是401362,那么哪里是子程序的结束呢?这里我们下面第一个出现的ret指令就是子程序的结束。OD中还可以写成retn。执行完ret指令以后,程序就会返回到call指令的下一条指令40124A处。
现在我们知道了怎么查看子程序里面代码了,如果我们想回到之前的那一行的话,我们可以按数字键减号。这个键可以让我们回到按“Follow”的那一行。
在这里,我们可以看到原先的call指令:
现在我们将按F7键跟进这个子程序,首先我们来看看堆栈的情况。这一步很重要,为了让子程序执行完ret指令后,知道要返回到哪里,返回地址将要存到堆栈当中。
上一张图片显示是我机器上的栈中的内容。或许跟你机器上的不一样,这无关紧要。
按F7键:
现在跟进子程序里面了,可以看到不像上面的“Follow”操作,现在EIP的值已经改变了-现在它等于401362,这就说明我们真正开始执行代码了。
让我们继续把注意力转移到堆栈中。
这张图片高亮显示的部分是我们跟进子程序之前的情况,现在我们有了一个新的元素,即call指令的下一条指令的地址被压入了堆栈中作为返回地址。
40124A这个地址就是call指令的下一条指令的地址,如果你不记得了,我们来call指令处看看。
OD还为我们提供了一些额外的信息。
红色的提示信息告诉我们,40124A就是以401262为起始地址的子程序的返回地址。
但是OD仍然不知道ret指令在哪里,但是知道401362是子程序的起始地址,当执行完ret指令后就会返回到40124A地址处。
我们继续按F7键,执行push 0指令,看看这个0是不是压入堆栈中并且存放到返回地址的上面了。
这个程序可能包含了成千上万个堆栈操作(push,pop等),在堆栈中添加或者删除了各种各样的值,但是当我们执行到ret指令的时候,栈顶存放的一般是子程序的返回地址。我们一直按F8不跟进call指令里面,直到遇到了ret指令停止。
现在我们到了子程序末尾的ret指令了,这个时候栈顶存放的是子程序的返回地址。
因此,我们可以知道ret指令是子程序的结束,也就是说,如果我们call跟进的话,那么ret就能返回到call指令的下一条语句处。
按F7键:
现在返回到了40124A处了,堆栈也恢复到了调用call指令之前的状态。
我这里补充一点,ret指令可不仅仅用于子程序的返回,例如:
PUSH 401256
RET
这里将401256压入堆栈。下面的ret指令会将401256当做子程序的返回地址,其实它并不是返回地址,但是执行ret指令后我们依然可以转移到401256地址处。这段代码和JMP 401256指令的功能是一样的。
接下来,我们来看看CALL/RET的另外一个例子。重新载入 CrueHead'a的CrackMe。单击鼠标右键选择“Go to”-“Expression”来到401364地址处。
我这里补充一点,ret指令可不仅仅用于子程序的返回,例如:
PUSH 401256
RET
这里将401256压入堆栈。下面的ret指令会将401256当做子程序的返回地址,其实它并不是返回地址,但是执行ret指令后我们依然可以转移到401256地址处。这段代码和JMP 401256指令的功能是一样的。
接下来,我们来看看CALL/RET的另外一个例子。重新载入 CrueHead'a的CrackMe。单击鼠标右键选择“Go to”-“Expression”来到401364地址处。
该地址处红色突出显示了,这说明这里条语句设置了断点。按F9,让程序运行起来。
出现CrackMe的窗口。如果你没有看到它,通过Alt+Tab键切换窗口试试。
该程序还没有执行到我们设置了断点的代码处。我们找到CrackMe的菜单项,选择“Help”-“Register”。
出现了下面的窗口,你可以输入名字和序列号。
随便填:
单击OK。
弹出一个消息框,说我们不走运,提示名字和序列号不正确,当你关闭这个消息框,程序就会中断下来。
我们处于程序的代码中间,但是我们可以从堆栈当中得到一些基本的信息。
这里我们看到几个RETURN TO指令。显然,这些是我们在程序中保存的返回地址。我们现在在40124A这行按下回车键。
你会发现,我们来到EIP改变之前分析的子程序处。但是这次我们程序已经执行起来了。
想之前一样一直单击F8键,直到遇到了ret指令。但是,在执行到ret指令之前,会弹出一个消息框,我们关闭消息框继续单步。
关闭它。
现在我们执行到了ret指令,和我们想的一样,栈顶现在保存的是返回地址。
这次我们直接F9运行起来。
总结一下,我想说明的是,当程序中断下来的时候,我们可以从堆栈中得到很多有用的信息。我们只要粗略的看一眼堆栈中内容,就基本上可以知道哪些call指令被调用了,以及将要返回地址是多少。很明显,当一个子程序调用了另一个子程序的时候,通过的这个子程序也可以去调用别的子程序,这就叫做嵌套调用。
现在来看看另外一个例子。重新载入CrackMe,按下空格键,输入一条指令:Call 401245。
完了以后,我们单击鼠标右键选择“Follow”查看这个子程序。
这个子程序起始于401245,到401288地址处结束(其中retn 10指令跟我们熟悉ret指令略有不同,稍后详细说明)。注意嵌套调用(这个子程序的第一条指令就是调用另一个子程序)。
按减号键返回到上一层。然后按F7键,跟进到子程序里面。
这里我们就到了子程序里面,EIP的值为401245。
栈顶保存了call指令的返回地址。
这确实是一个返回地址,但是OD没有以红色显示RETURN TO 401005。要弄明白为什么这一次OD没有以红色显示返回信息,我们回车键跟随到call里面,然后让OD重新分析代码。
因为我们已经让OD混淆了。
为了纠正这种问题,我们可以在反汇编窗口中单击鼠标右键选择-“Analysis”-“Analyse code”。
我们来看看重新分析后的代码。
现在返回地址被表示成了:程序入口点+5 = 401000 + 5 = 401005。
一般情况下,在改变程序代码后不要忘了重新分析代码。有的时候,代码分析可能是错的,这个时候应该选择“Analysis”-“Remove analysis from module”。
继续回到我们的例子。
F7键跟进这个子程序。
现在的返回地址变成了另一个。一般情况下,返回地址之间的内容是子程序嵌套调用中PUSH指令或者其他操作压入堆栈的值。
如果你对程序没有什么头绪,不妨设置一个断点,然后将程序中断下来观察一下堆栈里面的情况以获取一些有用的信息。
继续看代码。
查看一下堆栈:
最上面的一个返回地址是当前子程序的返回地址。这里是40124A。
此外下面一个返回地址可能表示该子程序被另个子程序调用。
另外还告诉我们,程序最终会返回到401005处,我们直接在这一行按回车键,就能看到是由哪里调用的。
我们要习惯子程序的嵌套调用,因为有的情况下,不是2层的子程序嵌套调用,有的时候可能是30层子程序的嵌套调用让我们来跟踪,所以我们要有足够的耐性。
如果你要什么不明白的地方就提出来。只有很好的理解了本章的内容才会更好的学习下一章。
WORD版:
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)