基础知识
栈
栈是操作系统在运行时自动初始化的一块区域,是数据暂时存储的的动态内存区域。它的大小在Windows操作系统下由PE文件结构中PE文件头中IMAGE_OPTIONAL_HEADER结构中SizeOfStackReserve字段所定义。在OD中,要想顺利分析软件的功能,在单步需要关注的便是寄存器和栈空间。下面,我们来看一下软件是怎么利用栈的。
函数的调用过程
我们用一个充满函数的小软件来研究函数的调用,源代码如下:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<windows.h>
using namespace std;
int function(int a,int b);//这是第一个函数,揭示控制台应用程序函数调用过程
int WINAPI winfunction(int a,int b);//winapi调用方式
int _cdecl cfunction(int a,int b);//c函数方式
int _stdcall cppfunction(int a,int b);//c++函数方式
int _fastcall ffunction(int a,int b);//寄存器函数方式
int _stdcall stdfunction(int a,int b);//其实就是WINAPI调用方式
int myfunction(int a,int b);//递归调用演示
int main(){
int a;int b;
printf("定位汇编代码");
scanf("%d",&a);
scanf("%d",&b);//scanf方便找汇编代码
winfunction(a,b);
cfunction(a,b);
cppfunction(a,b);
ffunction(a,b);
stdfunction(a,b);
myfunction(a,b);
return 0;
}
int function(int a,int b){
int c = a + b;//加法运算最简单
return c;
}
int WINAPI winfunction(int a,int b){
int c = a + b;//加法运算最简单
return c;
}
int _cdecl cfunction(int a,int b){
int c = a + b;//加法运算最简单
return c;
}
int _stdcall cppfunction(int a,int b){
int c = a + b;//加法运算最简单
return c;
}
int _fastcall ffunction(int a,int b){
int c = a + b;//加法运算最简单
return c;
}
int _stdcall stdfunction(int a,int b){
int c = a + b;//加法运算最简单
return c;
}
int myfunction(int a,int b){
a++;
b++;
myfunction(a,b);
}
将编译出的软件用OD载入,通过API定位至调用语段:
简单分析一下,易得:
0040159A 8B55 F0 mov edx,dword ptr ss:[ebp-0x10] ; 将scanf的结果放入EDX(b)
0040159D 8B45 F4 mov eax,dword ptr ss:[ebp-0xC] ; 将scanf的结果放入EAX(a)
004015A0 895424 04 mov dword ptr ss:[esp+0x4],edx ; 将EDX内容压入栈,就是我输入的b(=2)
004015A4 890424 mov dword ptr ss:[esp],eax ; 将EAX内容压入栈,就是我输入的a(=1)
004015A7 E8 81000000 call 未命名1.0040162D ; WINAPI调用方式
004015AC 83EC 08 sub esp,0x8 ; 恢复堆栈
004015AF 8B55 F0 mov edx,dword ptr ss:[ebp-0x10] ; 将scanf的结果放入EDX(b)
004015B2 8B45 F4 mov eax,dword ptr ss:[ebp-0xC] ; 将scanf的结果放入EAX(a)
004015B5 895424 04 mov dword ptr ss:[esp+0x4],edx ; 将EDX内容压入栈,就是我输入的b(=2)
004015B9 890424 mov dword ptr ss:[esp],eax ; 将EAX内容压入栈,就是我输入的a(=1)
004015BC E8 84000000 call 未命名1.00401645 ; c函数方式
004015C1 8B55 F0 mov edx,dword ptr ss:[ebp-0x10] ; 将scanf的结果放入EDX(b)
004015C4 8B45 F4 mov eax,dword ptr ss:[ebp-0xC] ; 将scanf的结果放入EAX(a)
004015C7 895424 04 mov dword ptr ss:[esp+0x4],edx ; 将EDX内容压入栈,就是我输入的b(=2)
004015CB 890424 mov dword ptr ss:[esp],eax ; 将EAX内容压入栈,就是我输入的a(=1)
004015CE E8 88000000 call 未命名1.0040165B ; c++函数方式
004015D3 83EC 08 sub esp,0x8 ; 恢复堆栈
004015D6 8B55 F0 mov edx,dword ptr ss:[ebp-0x10] ; 将scanf的结果放入EDX(b)
004015D9 8B45 F4 mov eax,dword ptr ss:[ebp-0xC] ; 将scanf的结果放入EAX(a)
004015DC 89C1 mov ecx,eax ; 将EAX寄存器的值放入ECX
004015DE E8 90000000 call 未命名1.00401673 ; 寄存器方式
004015E3 8B55 F0 mov edx,dword ptr ss:[ebp-0x10] ; 将scanf的结果放入EDX(b)
004015E6 8B45 F4 mov eax,dword ptr ss:[ebp-0xC] ; 将scanf的结果放入EAX(a)
004015E9 895424 04 mov dword ptr ss:[esp+0x4],edx ; 将EDX内容压入栈,就是我输入的b(=2)
004015ED 890424 mov dword ptr ss:[esp],eax ; 将EAX内容压入栈,就是我输入的a(=1)
004015F0 E8 9A000000 call 未命名1.0040168F ; stdcall调用方式
004015F5 83EC 08 sub esp,0x8 ; 恢复堆栈
004015F8 8B55 F0 mov edx,dword ptr ss:[ebp-0x10] ; 将scanf的结果放入EDX(b)
004015FB 8B45 F4 mov eax,dword ptr ss:[ebp-0xC] ; 将scanf的结果放入EAX(a)
004015FE 895424 04 mov dword ptr ss:[esp+0x4],edx ; 将EDX内容压入栈,就是我输入的b(=2)
00401602 890424 mov dword ptr ss:[esp],eax ; 将EAX内容压入栈,就是我输入的a(=1)
00401605 E8 9D000000 call 未命名1.004016A7 ; 递归调用
然后我们随意跟进一个函数,如图(我选择的是C函数方式)
简单分析,易得:
00401645 55 push ebp ; ebp入栈保护现场
00401646 89E5 mov ebp,esp ; 保存栈指针
00401648 83EC 10 sub esp,0x10 ; 设置栈指针
0040164B 8B55 08 mov edx,dword ptr ss:[ebp+0x8] ; 从栈中读入edx(a)
0040164E 8B45 0C mov eax,dword ptr ss:[ebp+0xC] ; 从栈中读入eax(b)
00401651 01D0 add eax,edx ; 将eax和ebx相加,读入eax
00401653 8945 FC mov dword ptr ss:[ebp-0x4],eax ; 保存eax的值入栈准备引用(c)
00401656 8B45 FC mov eax,dword ptr ss:[ebp-0x4] ; 将c作为返回值放入eax
00401659 C9 leave ; 恢复栈
0040165A C3 retn ; 函数返回
综上所述,我们可以得到以下几点:
1.大多数调用协定都将栈作为参数传递的途径
2.在进入一个函数时执行call指令就是做了两步:
(1)将下一行指令地址压入栈
(2)跳转到call后的地址
3.将eax作为返回值
4.return指令实际做的事:
(1)跳转到栈顶的那个地址
(2)栈顶中的地址出栈
所以,函数为了返回必定要用栈,分析栈也就可以知道函数是从哪调用的。
正式开始
调用栈
栈不止可以用于函数调用,临时数据的存储都是用栈,但是,栈中为了函数调用的部分称为调用栈,用调用栈可以分析函数是从哪里调用的。
OD对调用栈的支持
快捷键(Alt+K)
如图便是OD的调用栈窗口
第一行是Main函数里的函数
第二行是Main函数
调用栈找地址法原理
通过调用栈,我们可以知道函数是从哪里调用的,便可以知道是哪里触发了函数,便可轻易找到是哪里判断了注册码是正确还是错误的。
实战调用栈法
我又写了一个小程序,源码如下(验证部分和我上次发帖的一样):
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<string>
#include<windows.h>
using namespace std;
int check(string username,string sn);//检测注册码正确性
int registe();//注册界面
int help();//帮助界面
int main(){
cout<<"1.注册"<<endl<<"2.帮助"<<endl;
int choose;
cin>>choose;
if(choose==1){
registe();
}
else if(choose==2){
help();
}
else{
cout<<"没有这个选项!"<<endl;
system("pause");
}
return 0;
}
int help(){
cout<<"这只是个帮助"<<endl;
system("pause");
return 0;
}
int registe(){
string username,sn;
cout<<"请输入用户名:"<<endl;
cin>>username;
cout<<"请输入序列号:"<<endl;
cin>>sn;
check(username,sn);
system("pause");
return 0;
}
int check(string user,string sn){
for(int i=0;i<user.length();i++){
for(int j=0;j<sn.length();j++){
if(user[i]-1!=sn[i]){
MessageBox(NULL,"序列号错误","失败",MB_OK);
return 0;
}
}
}
MessageBox(NULL,"序列号正确","成功",MB_OK);
return 1;
}
OD载入,运行
在弹出错误框后,暂停,打开调用栈
可以看出,从我的exe调用的而非系统dll调用的最上层的调用来自:
未命名1.004018B2,其中未命名1为我的程序名。
进入004018B2
看到上面将序列号错误压入了栈,所以我们判断这个call显示了这个弹窗,所以我们想要跳过这个call,而它上面就有一个je跳转,我们改为jmp看一下。
至此,软件爆破完成!感兴趣的同学可以尝试分析一下剩余的语句的功能,对自己的能力提升是有帮助的。
结语
调用栈破解法很常用,学会了它,你就不用总是搜索字符串了。现在,自己找个CrackMe或编译我提供的源代码试一试吧,祝你好运!