`
soboer
  • 浏览: 1320013 次
文章分类
社区版块
存档分类
最新评论

Shell Code 原理深入剖析

 
阅读更多

这两天都一直在解析3D模型数据。今天闲暇之余写了段测试代码。分析下黑客们用的缓冲区溢出攻击原理及Shell code原理。好,直接进入正题。有什么说得不对的地方还望大家纠正。嘿嘿!
首先来这么一段小小的测试代码:
void test( void )
{
cout << "Success!" << endl;
}
int main( void )
{
int a[ 1 ];
a[ 3 ] = ( int )test;
return 0;
}
上面这段代码,可以简单的解释缓冲区溢出的原理,首先定义了一个整形数组a,红色部分代码已经写入越界。导致的结果就是会输出:Success!
这里就有一个疑问了,为什么在程序里没有调用test函数,也执行了test函数里面的代码呢?
这里就是缓冲区溢出导致的结果。这里要从函数的调用原理来解释这种现象,在函数被调用时,会保存函数的栈帧。会将ebp, eip进行压栈保存。顺序就是:
高地址 低地址
[ eip ] [ ebp ] [ a[ 0 ] ]
在汇编层面,调用函数会call [ 地址 ],call又两步骤,一是push eip到堆栈进行保存作为函数的ret返回地址,eip的值就是当前call指令的下一条指令的地址。当函数结束时,执行ret指令就会跳转到eip所存的地址,也就是主调函数的栈帧里面,完成调用。
说了这么多,这里的a[ 3 ]将test的地址当好覆盖到存放eip的地址上了。main函数的ret指令便跳转到test的代码空间里了。所以便输出了Success! 当然这样程序会崩溃,因为执行到test的ret指令时,此时的eip的值已经未知了,堆栈已经不平衡。因此跳转将到未知地方,便崩溃了。

从上面的例子中不难看出,我们可以通过Buffer Overflow来改变在堆栈中存放的函数返回地址,从而改变整个程序的流程,使它转向任何我们想要它去的地方。这就为黑客们提供了可乘之机。
最常见的方法是:在长字符串中嵌入一段代码(就是通过溢出越界写入,覆盖掉函数的返回地址),并将函数的返回地址覆盖为这段代码的地址, 这样当函数返回时,程序就转而开始执行这段我们自编的代码了。 一般来说,这段代码都是执行一个Shell程序(如/bin/sh),因为这样的话,当我们入侵一个带有Buffer Overflow缺陷且具有suid-root属性的程序时。 我们会获得一个具有root权限的shell,在这个shell中我们可以干任何事。因此,这段代码一般被称为Shell Code。
下面我们来举个例子说明下Shell Code的原理:
int Code( int a, int b )
{
return a + b;
}

void TestShell( void )
{
int result = 0;
BYTE FuncByte[ 512 ];
BYTE* JmpAddr = ( BYTE* ) Code;
DWORD ofsFuncAddr = *( ( DWORD* )( JmpAddr + 1 ) ) + 5;
BYTE* FuncAddr = ( BYTE* )( ( ( DWORD )JmpAddr ) + ofsFuncAddr );
BYTE* pFuncBuff = FuncAddr;
BYTE* pInput = FuncByte;

while ( true )
{
if ( (*pInput++ = *pFuncBuff++ ) == 0xC3 )
break;
}


__asm
{
lea eax, FuncByte
push 100
push 200

mov ecx, 1
call __label


__label:
cmp ecx, 0
je __ret
sub ecx, 1
jmp eax

__ret:
mov result, eax
add esp, 8
}

cout << result << endl;
system( "pause" );
}


int main( void )
{
TestShell ();
return 0;
}

上面的FuncByte用于保存Code函数的字节码JmpAddr指向jmp到Code函数的jmp指令地址。call [目标函数] 会先跳转到jmp [ 函数地址 ]指令的地址上,然后才会jmp到目标函数的首地址上。ofsFuncAddr用于保存当前jmp指令5个字节中后4个字节保存的函数地址偏移量(这里暂不管跳转的远近,这里是无条件转移,就粗略认为是4个字节存放的是偏移)。
指令地址 字节码 指令 目标函数地址
0041954B E9 10 1D 00 00 jmp TestShell (41B260h)
从上面的jmp指令可以看出,E9就是jmp指令的字节码,后面蓝色的4个字节就是:目标函数的地址 - jmp指令地址 - jmp指令的5个字节。也就是:0x41B260 - 0x41954B - 5 = 0x001D10。FuncAddr保存的是目标函数的首地址。之后的while就是将Code函数的字节码拷贝到FuncByte里。因为函数结束会执行ret指令,ret指令的字节码就是0xC3。所以我们以它来终止循环停止拷贝。
之后的汇编代码是为了执行我们拷贝的字节码,并维护堆栈平衡,让跳转地址正确跳转到__ret后面的 mov result, eax 语句。但是要怎么样才能让执行了我们拷贝的字节码后正确跳转到我们想要的位置呢?这里我们使用call指令来完成这项工作,红色的call __label会将下一条汇编语句的地址压入堆栈,作为函数的返回地址。由于FuncByte里面存放的是Code函数的字节码,因此执行FuncByte里面的字节码与Code函数的效果是一样的。这里执行FuncByte直接用jmp eax来进行跳转。执行到0xC3(ret)字节码后,就会跳转到cmp ecx, 0这条语句上。这里我做了个限制使用ecx计数让ret回来后因为ecx为零(sub ecx, 1 ),执行je __ret。红色的代码段也可以用 push __ret 一条指令来替换,相当于把返回地址push到堆栈,当拷贝的字节码执行完后返回到__ret:。这里只是为了说明CALL指令的原理。 然后将返回值赋给result。之后pop掉两个参数100, 200。维持堆栈平衡。之后就是打印result的值:300。实现了ShellCode的原型。
好了,基本上是说完了!这里的Code函数里面只是简单的一条语句,如果有复杂的操作还需要进一步处理FuncByte里面的字节码。比如,Code函数里面有函数调用,将会有jmp跳转。而jmp跳转使用的是距当前语句的指令地址的偏移量。FuncByte是一临时的字节数组,执行的字节码的指令地址也将在临时的地址空间里。字节码不变的情况下,jmp指令的地址变了,自然jmp同样的偏移是不会跳转到正确的目标函数地址的。我的初步想法是在拷贝字节码的同时对使用偏移的指令进行特殊计算处理。让在临时地址空间中也能正确跳转。暂时留个思绪,抛砖引玉!各位多多指教! - -

如果Code函数里面有函数调用:
int Code( int a, int b )
{
cout << a + b << endl;
return a + b;
}

下面是我们拷贝的字节码和Code函数的字节码对比:


FuncByte的拷贝字节码:
0013FBF8 55 push ebp
0013FBF9 8B EC mov ebp,esp
0013FBFB 81 EC C0 00 00 00 sub esp,0C0h
0013FC01 53 push ebx
0013FC02 56 push esi
0013FC03 57 push edi
0013FC04 8D BD 40 FF FF FF lea edi,[ebp-0C0h]
0013FC0A B9 30 00 00 00 mov ecx,30h
0013FC0F B8 CC CC CC CCmov eax,0CCCCCCCCh
0013FC14 F3 ABrep stos dword ptr [edi]
0013FC16 68 D8 94 41 00 push 4194D8h
0013FC1B 8B 45 08mov eax,dword ptr [ebp+8]
0013FC1E 03 45 0C add eax,dword ptr [ebp+0Ch]
0013FC21 50 push eax
0013FC22 B9 88 86 45 00 mov ecx,458688h
0013FC27 E8 2B E2 FF FF call 0013DE57
0013FC2C 8B C8 mov ecx,eax
0013FC2E E8 10 E7 FF FFcall 0013E343
0013FC33 8B 45 08 mov eax,dword ptr [ebp+8]
0013FC36 03 45 0Cadd eax,dword ptr [ebp+0Ch]
0013FC39 5F pop edi
0013FC3A 5E pop esi
0013FC3B 5B pop ebx
0013FC3C 81 C4 C0 00 00 00 add esp,0C0h
0013FC42 3B EC cmp ebp,esp
0013FC44 E8 D0 E8 FF FF call 0013E519
0013FC49 8B E5mov esp,ebp
0013FC4B 5D pop ebp
0013FC4C C3 ret

Code函数本身字节码:
0041B770 55 push ebp
0041B771 8B ECmov ebp,esp
0041B773 81 EC C0 00 00 00 sub esp,0C0h
0041B779 53 push ebx
0041B77A 56 push esi
0041B77B 57 push edi
0041B77C 8D BD 40 FF FF FFlea edi,[ebp-0C0h]
0041B782 B9 30 00 00 00 mov ecx,30h
0041B787 B8 CC CC CC CCmov eax,0CCCCCCCCh
0041B78C F3 AB rep stos dword ptr [edi]
0041B78E 68 D8 94 41 00 push offset std::endl (4194D8h)
0041B793 8B 45 08 mov eax,dword ptr [a]
0041B796 03 45 0C add eax,dword ptr [b]
0041B799 50 push eax
0041B79A B9 88 86 45 00 mov ecx,offset std::cout (458688h)
0041B79F E8 56 DE FF FF call operator<< (4195FAh)
0041B7A4 8B C8 mov ecx,eax
0041B7A6 E8 40 E3 FF FF call operator<< (419AEBh)
0041B7AB 8B 45 08 mov eax,dword ptr [a]
0041B7AE 03 45 0C add eax,dword ptr [b]
0041B7B1 5F pop edi
0041B7B2 5Epop esi
0041B7B3 5B pop ebx
0041B7B4 81 C4 C0 00 00 00 add esp,0C0h
0041B7BA 3B EC cmp ebp,esp
0041B7BC E8 00 E5 FF FF call (__RTC_CheckEsp) (419CC1h)
0041B7C1 8B E5 mov esp,ebp
0041B7C3 5D pop ebp
0041B7C4 C3ret
从红色的3个call可以看出,我们的字节码没有变,也就是同样的偏移值。计算出来的call地址是不一样的。拷贝的在0x0013....空间内。而正确的应该是0x0041....空间内。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics