【文章标题】: 简略翻译《Write Small Shellcode》NGS
【文章作者】: fqucuo
【作者邮箱】: fqucuo@163.com
【作者QQ号】: 389990968
【下载地址】: 自己搜索下载
--------------------------------------------------------------------------------
【详细过程】
  简略翻译《Write Small Shellcode》NGS
  
  声明:此文是我在看阅failwest Sir's 《深入浅出MS06-040(看雪网络版).pdf》提到的,搜了一下有就下了,不过是英文版的,至于有没有人翻译我不知道,因为自己也是初学者,准备比赛需要很多时间,也无暇去费心去找有没有翻译过的,反正自己先看了再说。强烈建议先看“完全分析failwert Sir's Shellcode”一贴,绝对不是为了顶贴和炫耀,因为文中中间部分与我的原帖(其实是failwest Sir的shellcode)有很多类似之处,文中我会提到,因为不想占用过多篇幅,使之又臭又长(原文挺长的,应该是我英文太烂),所以省去,届时将会出现请参阅XXX贴之类的,所以如果有需要就在看雪找。
  
  由于本人英文水平实在有限,如果英文水平好的就看英文版的,我尽量用通俗的语句解释,再加上自己也是新手,所以会保持原创的所有精华之处,当然,一些见解和心得我也会说出来,也许就是自己写兴奋了连英文都没看就呼啦出来了(这也是我建议读英文版的用意),或许这样对跟我一样的新手吸收会快一些。
  
  
  正文:
  这个bindshell的文档使用了191个非空字节代码, 大概的描述了如何写出短小精悍的shellcode
  
  当我们在“攻击”软件的时候,shellcode的大小是至关重要的,因为我们经常会遇到仅有一小部分可利用的空间,
  
  我现在假设阅读者已经具备了汇编语言的能力
  
  介绍:
  这个任务主要是完成以下三步:
  1. Bind 一个shell到6666端口
  2. 再建立一个链接
  3. 安全的退出
  
  这段代码必须运行在NT4, 2000, XP和2003,并且建立在以下代码上,并且假定eax寄存器指向我们的shellcode首地址(具体情况灵活发挥):
  void main()
  {
    unsigned char sc[256] = "";
    strncpy(sc, "Shellcode here", 256);
    __asm
    {
      lea eax, sc
      push eax
      ret
    }
  }
  
  接下来,我们再说一下细节:
    这段shellcode里面不能有空值(0x00,因为会被strncpy截断)
    这段shellocde必须运行在栈空间里
    Winsock没有被初始化
    我们假设eax指向我们的shellcode的首地址(具体细节部分请参阅failwest Sir's 教程)
  所有的代码将会附加在后面,首先,我先介绍几个关于这个任务的注意事项
  
  如何写出小巧的shellcode
  
  1. 使用短指令
    我们知道,x86指令是变长的,有些时候不同长度的指令却可以完成相同的两件事,现在我介绍几个非常有用的“单字节”的指令
    
    xchg eax, reg     交换两寄存器值
    lodsd/lodsb 从esi所指向的地址中读取dword/byte 到 eax中,并且esi会根据所传数据的字节数自加
    stosd/stosb 与上面相反,是从eax读出dword/byte 到edi所指向的地方
    pushad/popad 这个嘛,不多说了,地球人都知道
    cdq     扩展32位寄存器到64,其高字节部分根据eax高位的符号位扩展,当eax == 0 或者 eax <0x80000000的时候,edx = 0,反之edx = -1(0xFFFFFFFF),这样的话我们可以在我们能控制的清空下实现edx清零的操作
    
  2. 一条指令完成多件任务
    例如上面的xchg 就相当于mov ebx, eax  mov eax, reg, mov reg, ebx 等等此类指令不列举了
    
  3. API法则
    有些时候API需要一个确定的值或大小,不管怎样,通过实验,我们确定是可以解决的。比如,许多API使用了结构成员,其结构成员必须指定大小, 并且需要一块足够大的空间保存参数值,否则运行起来有可能会不正常, 但是,如果我们已经知道某些栈空间已经释放,并且足够的大小,我们就可以利用API的容忍度(这点翻译的挺绕口)去设置结构体参数
    
    许多API使用非空值最为参数,但是他们通常都是在最后入栈的,相比之下,我们传入空寄存器倒不如清空一块栈空间来的容易,之后我们就仅需要传入非空值即可,当我们连续的调用几个需要此类大小空间的作为参数的API的时候,这个时候,这种方案将是十分有意义的!
    
    对于我们来说,就可以在栈空间中为API划出一块足够大的空间作为API的结构体参数,通常,我们可以使用单字节指令“push esp”作为结构体指针参数传入,某些情况,API不做边界检查的。
    
  4. 不要认为自己只是个程序员
    未翻
    
  5.如何有效的使用寄存器
    x86寄存器不全是等价的,有些有用的指令只能使用在指定寄存器上,确定使用某几个寄存器是经常的也是必须的, (比如用来保存API函数地址的ebp, edi, esi 等,lodsb stosd等都可以有效的控制他们的位置)使用这些寄存器来保存信息远比用栈来保存信息有意义的多(对我们来说)
    
  6. 如何压缩我们的shellcode
    这段不翻译了(又臭又长),具体就是说如何寻找一个高效的压缩代码的方法,文中使用的是hash,并且是压缩成一个字节,这些failwest Sir都提到过的,在我逆向failwest Sir的文中他用的是压成4字节的值,这样碰撞几率就很低了,也就有很多方式解决,当然这样这也是最稳最通用的,不过本文题目就是small,那就跟作者玩狠的吧!但是事先一定要确定你将使用的有哪些API字符串,并且通过写程序将所有的函数名都通过一个值hash,直到确定一个有效值就OK了
  
  为了使我们的shellcode能够运行在各个版本中,我们要先完成两件比较重要的事情:
    1. 确定需要哪些函数
    2. 使用这些函数完成哪些任务
    
  这些函数必须能够完成我们下面的任务
  ws2_32.dll
    WSASTartup     因为Winsock没有初始化,所以我们需要他
    WSASocketA  创建套接字
    bind ---
    listen ---- 
    accept ----- 这些就不想解释了,不懂了回去翻书
  kernel32.dll
    LoadLibraryA 我们需要他去获得ws2_32.dll的模块句柄
    CreateProcessA 创建一个子进程用以执行我们创建客户端链接的任务
    ExitProcess 结束已完成使命的子进程
    
  下面这块我也真懒得翻(主要是因为我对hash算法理解不是很深)不过大体意思有必要讲一下:
  当我们要完成以上函数的调用时,必须在shellcode中放置这些函数名,但是实际情况允许你放入这么多的字符串吗?绝对是不行的,那么我们就要运用一套行之有效的方式取解决字符串的问题,文中使用hash也不是随便就找个差值随便就可以用的,首先我们确定要将这些字符串都被hash成为一个字节,(牛吧),其实道理很简单,我上面提到过了,但是一定要遵守几个原则,
    1. 非空
    2. 在茫茫多的函数中查找(查找方式自己写程序实现)
    3. 这个值与寄存器搭配之后指令还要最短的
    4.通过这个值保证能够准确无误的匹配到我们的函数上去
    ....
  下面的这段文字又臭又长(哈哈,自己英文烂还要埋怨别人。。),所以就不翻了,其最终确定的一个算法是这样:
    hash_loop:
      lodsb
      xor al, 0x71
      sub dl, al
      cmp al, 0x71
      jne hash_loop
  可以看到这个是通过异或,其值为0x71(注意:此值是与当前任务中的API异或并且在异或完所有kernel32中的API(甚至包括ws2_32中的)所得到的值,绝不是通用值,选择异或或者是移位一定要根据现实情况决定)
  
  OK,这样我们就得到了我们的函数字符串值(单字节的哦)
    0x59       ;LoadLibraryA   相当于pop ecx
    0x81       ;CreateProcessA 相当于 or ecx, 0x203062d3
    0xc9       ;ExitProcess
    0xd3       ;WSAStartup
    0x62       ;WSASocketA
    0x30       ;bind
    0x20       ;listen
    0x41       ;inc ecx  相当于inc ecx
    
    0x43       ;C 等价于inc ebx
    0x4d       ;M 等价于 dec ebp
    0x64       ;D 等价于 FS:
    
  动态查找API这块我不讲了吧?看过“完全分析failwert Sir's Shellcode”一贴的人绝对是不需要这部分的,好,我们直接跳到下面建立连接部分:
  
  还是要接着原帖讲(天知道还有这篇文章,我也是在发完贴后才发现的,还好能串起来)
  原文最后部分是找到了LoadLibraryA,只不过作为例子failwest Sir只是实现了MessageBoxA,那么下面我们就是玩真的了
  
  完成我们的bindshell:
    这里开始我将会以逆向的方式讲解,原文的精华之处如果被我发现了我绝对不加保留的奉献,当然,漏过的。。。
    现在的寄存器状态:ebp 保存了ws2_32的基地址, edi指向第一个Socket需要的函数
    记得例子push的是user32吧?换成ws2_32就行了(这点有点跳跃,不过结合英文版附录中的代码看不会有任何问题的)
    
  在我们使用这些函数之前,我们首先要初始化Socket(套接字), 需要去call WSAStartup, 而这些函数地址就存在我们edi所指向的地址中,我们先来看下WSAStartup的声明(查MSDN去)
    int WSAStartup(
    WORD wVersionRequested,
    LPWSADATA lpWSAData
  );
    
  我们将使用栈空间去构造WSADATA,因为他是个out值,我们不需要去初始化他,我们只需要确定他不会写到我们重要的代码就ok了,我们已经有了很多栈空间了(哪儿来的?回去看上一个贴去)所以我们可以放心大胆的用就是了
    pop esi ;去WSAStartup
    push esp  ;给他一个大空间地址用来outWSADATA结构
    push 0x02 ; 版本号一定要给的
    lodsd  
    call eax  ;读出来call就是了
    
  如果函数返回成功将会返回0值,也就是eax = 0,那么我们就相当于获得了一个0值寄存器,这个时候我结合其他的几个套接字函数,他们需要大量的0值作为参数传递,那么我们何不妨选择一个大点的结构填充为0,到时候谁用谁拿去就是了呗,那我们选择STARTUPINFO这个结构的大小填充个全0的栈空间(STARTUPINFO够大了吧?),还可以给一个不用的地方放一个4字节的0,留着以后用呗,照着这个思路有下面代码:
    mov byte ptr [esi + 0x13], al    ;留给cmd字符串用的,用作0结尾
    lea ecx, [eax + 0x30]; 取结构大小0x30
    mov edi, esp 
    rep stosd ;填充全0
    
  下面我们来看WSASocket,它需要6个参数
    SOCKET WSASocket(
      int af,
      int type,
      int protocol,
      LPWSAPROTOCOL_INFO lpProtocolInfo,
      GROUP g,
      DWORD dwFlags
    );
    
  其中我们仅仅关心的是af和type这两个参数(看到了吧?往往有用的参数总是最后被压栈的),这两个参数我们需要分别放2和1
  其他的统统给0。WSASocket将返回一个标识用来操作剩下的socket函数,所以我们准备用ebp保存他
    inc eax ; 因为eax我们知道是0,Inc eax eax = 1(SOCK_STREAM)
    push eax
    inc eax  ; eax = 2 (AF_INET)
    push eax
    lodsd
    call eax ; WSASocket
    xchg ebp, eax ; 保存SOCKET标识到ebp    (这段也是很有技巧的)
    
  咦?不是6个参数吗?怎么只传了两个就call了,其实配合上面的就理解了,_stdcall方式调用,内部函数不管你push了没push他总会去取栈顶上的值,并且帮忙恢复堆栈,也就是说我们在push有效值之前esp栈顶指针已经指向了一块全0的大空间,这时候仅仅需要传有效值到栈顶就行了(这点是很有技巧性的,值得学习)
  
  下来就是监听了,在监听之前我们要先完成bind函数的调用
    int bind(
      SOCKET s,                          
      const struct sockaddr FAR *name,   
      int namelen                        
    );
    
  回想一下我们的程序,我们还需完成那几步实现bind
    1. 创建并初始化sockaddr_in结构
    2. push 一个结构长度
    3. push sockaddr结构的地址 改成sockaddr_in
    4. push SOCKET标识 s
    
  
  不管怎样,我们只需要稍微修改一下规则,就可以高效的完成这些任务。首先,sockaddr_in结构大多数可以为0,我们只需要关心的是他的
    short sin_family
    u_short sin_port
  这两个参数,第一个为AF_INET(2),第2个是端口号,因为我们需要6666端口,其16进制位0x1A0A那么在空栈的基础上再压入栈就可以了,这里还要说一下,参数namelen不是很重要,给0即可,所以为了对齐上面的0x1A0A(32位压栈只能4字节4字节的压),又因为sin_family和sin_port都是short,我们只用压一个DWORD就够了,这个值确定为0x0A1AFF02,先看代码:
  
    mov eax, 0x0A1AFF02
    xor ah, ah ; FF清0,还记不记得我们的规定,不能有0值出现在shellcode中的
    push eax ;这一步就相当于完成了赋值给结构体中sin_family和sin_port的过程
    
    push esp ; 压入结构体地址
    push ebp; 压入SOCKET表示
    lodsd
    call eax ;OK调用
    
  心得:这点技巧真的不是盖的,所以要想成为专家,路确实挺长。。。
  
  剩下的任务就是完成建立listen和accept, 呼快完了,别休息!可要知道哥们是在北京时间凌晨3:49分啊,今年第一次熬这么晚的,不说了继续!
    int listen(
      SOCKET s,    
      int backlog  
    );
    SOCKET accept(
      SOCKET s,
      struct sockaddr FAR *addr,
      int FAR *addrlen
    );
    
  到了这部分花样更多了,看看bind listen accept有什么相似之处?对了,就是最后压栈的总是SOCKET标识,首先这点插点花,看下我们的堆栈:
  
  
  bind需要的栈:SOCKET s  ;push ebp
            const struct sockaddr FAR *name ; push esp
            int namelen  
  listen需要的栈:SOCKET s  ;push ebp
              int backlog ; 这个参数作用不是很大,所以只要非0就行
  accept需要的栈:SOCKET s  ;push ebp
        const struct sockaddr FAR *name ; push esp
  
  聪明的朋友一定看到了规律,参数顺序基本相同(虽然用处不同),那我们就可以简化上面bind的代码,用一个loop去完成bind listen 和accept的任务!强啊!!!
    call_loop:
      push ebp
      lodsd
      call eax
      test eax, eax
      jz call_loop
  
  至此我们已经成功的完成了在服务器上开端口设监听的操作,接下来就是我们扫尾工作了,我们首先建立一个客户端连接,去执行cmd.exe
  
  哎~实在是太困了,剩下的这一小段大家可以参照英文版(不到20行了大哥!基本上都是代码),再说了,引用作者的话(in fact, wo could save  a single byte of code by creating our shell without stderr , but let's be generous),人家都无所谓(gentleman)了,我们还赖着干嘛,行了,网络后门之类的事情本人没研究过,所以如何如何的也没啥见解,具体实战中,如何运用完全是基本功和经验技巧的问题,向高手学习总是没错的(可不是向我学习啊,我指的是作者和看雪的老大们!)
  
  原英文版资料不发了,自己可以去网上搜到的
  
  困了,睡去了。。。
    
  
--------------------------------------------------------------------------------
【经验总结】
  
  不好意思,起晚了(谁叫睡的晚呢),一起来就来发帖(敬业吧),文章中用此达句是否恰当还望高人指点(千万别骂我啊。。水平问题啊,)其中一定有理解不深刻,甚至错误之处,还望过路高人万万指出,也给其他需要此贴的人以明示。
  行了,废话不多说了,发帖先,有什么问题回帖讨论
--------------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!

                                                       2007年12月28日 12:20:54