//测试环境:Windows XP + SP2 Intel T2050 Centrino Duo //编译环境:Visual C++ 8.0 (无CLR) #include "stdafx.h" void func(){ printf("Never called implicitly./n"); }
void inject(){ //用来测试溢出的函数
int sth=10; int* p=&sth; p[3]=(int)func; }
int _tmain(int argc, _TCHAR* argv[]) { inject(); printf("Normal END./n"); } |
结果却出现问题,归结为两个:
1, 为什么p[3]才是保存的EIP?! 2, 即使修改了EIP值,但是为什么程序会出现非法操作? |
呵呵,经过一番波折and努力,终于都顺利解决了……
(偶毕竟是这方面新手,高手嘛……别笑话亚…… |-_-)
先说第一个了……
既然,p[3]才是EIP,根据首次的出错信息,p[2]应该是EBP。那么……
p[0]应该是sth,呃……p[1]是什么?
测试下……应该就知道了。
在inject中添加如下代码:
for(int i=0;i<=3;i++) printf("p[%d] value: %8x/n",i,p[i]); //均以8字符宽的16进制输出 |
输出如下(不同机器肯定有所不同):
p[0] value: a p[1] value: cccccccc p[2] value: 12ff68 p[3] value: 411483 |
p[0],a,即十进制的10,是变量sth的值,这个没问题。
p[1]……暂时不明白。
p[2],p[3],显得无规则,这倒正常,因为都是内存地址嘛……
呃……
Visual Studio,让很多程序员开心的一点……就是它提供了很强大的调试功能(如反汇编)。
呵呵,这里貌似只有试试它了,兴许可以解决问题。
直接在调试时选择“反汇编”,进入inject函数的汇编码。
如下:
void inject() { 004135A 0 push ebp 004135A 1 mov ebp,esp 004135A 3 sub esp,0E4h 004135A 9 push ebx 004135AA push esi 004135AB push edi 004135AC lea edi,[ebp-0E4h] 004135B2 mov ecx,39h 004135B7 mov eax,0CCCCCCCCh 004135BC rep stos dword ptr es:[edi] int sth=10; 004135BE mov dword ptr [sth],0Ah int* p=&sth; 004135C 5 lea eax,[sth] 004135C 8 mov dword ptr [p],eax p[2]=(int)func; 00413626 mov eax,dword ptr [p] 00413629 mov dword ptr [eax+8],offset func (41108Ch) } 00413630 push edx 00413631 mov ecx,ebp 00413633 push eax 00413634 lea edx,[ (413658h)] 0041363A call @ILT+145(@_RTC_CheckStackVars@8) (411096h) 0041363F pop eax 00413640 pop edx 00413641 pop edi 00413642 pop esi 00413643 pop ebx 00413644 add esp,0E4h 0041364A cmp ebp,esp 0041364C call @ILT+315(__RTC_CheckEsp) (411140h) 00413651 mov esp,ebp 00413653 pop ebp 00413654 ret |
你会发现……编译器为我们生成了太多……貌似没什么用的代码。
……
先看看,自己为inject,写的汇编码:
; 汇编代码设计:小石头 ; 适应于 IA32 体系 ; 格式仿上述代码,红色为C++指令 ; 经测试,如下代码成功转跳到 func 函数体内(仍然是p[2]) void inject() { push ebp mov ebp,esp sub esp,008h int sth=10; mov dword ptr [ebp-4],0Ah int* p=&sth; lea eax,[ebp-4] mov dword ptr [ebp-8],eax p[2]=(int)func; mov eax,dword ptr [ebp-8] mov dword ptr [eax+8],offset func } add esp,008h mov esp,ebp pop ebp ret |
呃……与之对比,编译器生成的代码显得十分庞大。
原因,是程序编译版本为Debug版。
如果改为Release版,就不会有这么庞大的代码了。
说明:事后,我也做了测试。 当选择编译为Release版的时候,p[2]确实是保存的EIP,这个没错了! |
现在,我们简单解读下Debug的汇编代码吧。
sub esp,0E4h
先为栈帧预留这么大空间(57×4=228字节),然后又push了一组寄存器。
lea edi,[ebp-0E4h]
mov ecx,39h ;39h是十进制57
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
其实是写入cccccccc到范围[EDI,EDI+ECX],计算下…正好到达EBP的位置。
而编译器把第一个临时变量,也就是sth,安排到了EBP-4的位置。
(这个……我不是太明白,编译器为什么这么做)
至于编译器为什么要这么做……大概是因为0cccccccch其实是int 3中断指令,它可以把程序中断,将控制权交给调试器。
现在说第二个问题。
怎么对付这个出错的窗口。
我们先想下出错的原因……
很明显,程序的流程被打乱了!
这样导致进入函数,却没有使用call指令,因此EIP也没有被压入堆栈。
当离开这个函数(func),弹出EIP就会出问题。
……
总之,是溢出,导致了堆栈的不平衡。
这里,我们是让func取代了应该返回的main中的位置。那么,它就应该表现的像main一样……
呵呵,因此,我们需要拒绝编译器为我们生成的那些乱七八糟的代码。
C++中,可以很容易实现这一点。
就是使用关键字__declspec(naked),并且使用__asm关键字来内嵌汇编代码。
我们修改下func函数,使之如下:
void __declspec(naked) func(){ printf("Never called implicitly./n"); __asm{ leave ret } } |
再次运行下吧……
呵呵,问题解决了,程序输出Never called implicitly。
安然无恙!
需要说明下的是,leave指令在这里可以看成一串pop指令和一句很关键的mov esp,ebp,这样通过自己书写的汇编代码,平衡了堆栈结构。
下篇将会给出自己针对strcpy,这个常用函数的溢出实践……
因为对于正常的程序,肯定不可能写出这么有“破坏性”的代码。
需要做的是……嘿嘿,自己找出一个注入点,然后通过传递一定的参数使之溢出。
等有空再发表,谢谢支持喔。