当前位置:Linux教程 - Linux - raw_socket server设计文档(1)

raw_socket server设计文档(1)

socket( PF_INET, SOCK_RAW, IPPROTO_RAW );
socket( PF_INET, SOCK_RAW, IPPROTO_TCP );

在RedHat 6.1下这两种socket都可以正常建立,内核支持了的。但是对于
Solaris 2.6,如果以root身份truss跟踪这两个函数,发现第二个socket建立的时候
内核不支持这种情况下指定IPPROTO_TCP,库函数本身做了处理:

so_socket(2, 4, 6, """", 1) Err#98 EPROTOTYPE
stat(""/dev/rawip"", 0xEFFFFAC4) = 0
so_socket(2, 4, 6, ""/dev/rawip"", 1) = 4
setsockopt(4, 65535, 4105, 0xEFFFFBB4, 4) = 0

从执行效果看,这样的处理和Linux下的意义不同了。

如果考虑广泛兼容性,应该扔弃第二种socket,全部以IPPROTO_RAW方式出现。这样
的话,理论上可以考虑不用TCP/UDP协议,但是涉及client/server模式,显然应该继
续使用TCP/UDP。从突破防火墙角度看,还是以鬼子的ACK方式为好。UDP通信被很多
防火墙屏蔽,TCP也好不到哪里去。而且按照目前的设想,等于仅仅使用TCP的头部概
念,并没有使用TCP协议的超时、重传等机制,更没有有限状态机介入,为什么不使
用UDP呢?还是应该从防火墙角度考虑这个设计选择,具体问题具体分析吧。现在的
难点是完全使用IPPROTO_RAW,写没多大问题,读有了麻烦,又需要重翻UNP;此外,
丢包是毫无疑问的,因此必须尽量设计成无状态方式(NFS Server就是一个例子),这
个也仅仅是说说,技术问题尚未可知。

关于内核传递IP报文到一个raw_socket,有几点需要注意,我们分别探讨之:

1) TCP/UDP报文(IP报文负载为TCP/UDP)""永远""不会传递给raw_socket。Stevens介绍
的时候以BSD家族为例。

对于Linux显然已经不适用这个结论,socket( PF_INET, SOCK_RAW, IPPROTO_TCP )
就可以接收到TCP报文,Linux内核是给了这个机会的,此时正常的TCP协议层也会
收到TCP报文(后面我们会写测试代码验证它)。于是造成潜在的安全隐患,在无需
数据链路层和网卡混杂模式介入的情况下,利用raw_socket监视发往本机的TCP报
文。尽管只有root才可以创建raw_socket,但获得创建raw_socket的机会和获得
完整root权限相比要大得多。对于Solaris系统,内核应该是没有支持
socket( PF_INET, SOCK_RAW, IPPROTO_TCP )方式,尽管以root身份执行库函数
并没有报错(此时库函数自己做了其他处理)。

对于Windows 2K,从backend拖回来的程序执行效果以及袁哥分析代码的结论看,
2K可能支持socket( PF_INET, SOCK_RAW, IPPROTO_TCP )这种方式。抓包分析
backdoor的client/server通信,发现除了预料中的ACK,还夹带有RST,只能说明
2K内核传递IP报文到raw_socket的同时传递给了正常的TCP协议层,RST是由正常
TCP协议层发出的。NT/9x估计没戏。

考虑我们要达到的目的,如果内核不给这个机会(传递TCP报文到raw_socket),意
味着ACK方式破产。UDP自然也不用想了。虽然Linux可以,但我们希望得到一个更
广泛兼容的backdoor。可以从数据链路层考虑这个问题,牵扯的问题更多,没有
太大必要。

2) 对于伯克利实现而言,内核一般处理了几种常见ICMP报文(3种,回应请求、时间
戳请求、地址掩码请求),其余未处理ICMP报文交给raw_socket。注意内核并没有
处理上面三种请求报文的应答报文,想想ping.c的实现,如果内核处理
icmp echo reply,即使指定IPPROTO_ICMP,处于应用层的ping也没有机会得到应
答报文。这里所说内核处理,都是指处理入IP报文,对于发送IP报文,基本上任
由应用程序处理的,所以ping可以发送自己的icmp echo request。

Linux/Solaris的实现有差别,提供给应用层更多机会。内核处理了icmp echo
request,同时会交给socket( PF_INET, SOCK_RAW, IPPROTO_ICMP ),不同于BSD
实现。内核未处理的icmp报文依旧交给raw_socket。这给我们一个机会,编写自
己的icmp daemon,利用被内核传递到raw_socket的icmp报文进行交互式通信。从
突破防火墙角度考虑,比较现实,一般管理员会允许icmp echo request进入。管
理员要是在防火墙上过滤了icmp echo request,估计我们也没有机会在这种敌人
内部安装icmp daemon,走先。

3) 所有的IGMP报文交给raw_socket。

同上,可以利用。现在的操作系统好象已经开始在内核里处理igmp,那样的话,
机会不大。而且防火墙对IGMP报文比较敏感。

socket( AF_INET, SOCK_RAW, IPPROTO_IGMP ),Linux上可以接收到IGMP报文,
Solaris上不行。

4) 如果内核无法理解IP报文头中高层协议类型,传递该报文给raw_socket。

内核无法理解的,对于防火墙也是无法理解的,除非不考虑突破防火墙的网络拓
扑,否则暂时别想。此外从前面的测试中看到,Linux/Solaris下必须精确指定第
三个参数可以接收匹配IP报文,如果要利用内核无法理解之协议类型,必须确保
该类型可以指定在第三个参数中。

5) IP分片一定是在内核中重组完成了才会传递给raw_socket。

换句话说,raw_socket无法分析IP分片,数据链路层可以。这里隐含着一个意思,
IP分片重组永远在内核完成,一旦这部分的处理代码出了问题,就是内核的麻烦,
所以死得快。

6) 如果内核决定传递一个IP报文到raw_socket,则系统中所有进程创建的所有
raw_socket都会收到这个IP报文,这是一个潜在的安全问题。

我们在测试程序中创建socket( PF_INET, SOCK_RAW, IPPROTO_ICMP ),启动了两
个实例,然后从其他主机ping本机,两个实例都收到了icmp echo request。

7) 创建socket( PF_INET, SOCK_RAW, 0 ),并且不调用bind、connect,这样的
raw_socket接收所有内核传递上来的IP报文。第三个参数是指定匹配的,如果非
零,不匹配的IP报文不会被传递给该raw_socket。对于这种系统,企图监视本机
所有入IP报文,不需要数据链路层介入,也不要求网卡混杂模式,简单创建一个
raw_socket,指定第三个参数为0即可。

遗憾的是,我们在Linux下测试,根本就不支持第三个参数指定为0,指定成
255(IPPROTO_RAW)也无法达到Stevens描述的效果,255主要用于发送,Stevens介
绍的可能仅仅是BSD实现吧。

关于这个,觉得看看Linux关于raw_socket的实现部分比较好,瞎猜也不是办法。

8) 有些代码使用了raw_socket,并未指定IP_HDRINCL选项。1988年为了解决
traceroute问题引入了一个patch,创建SOCK_RAW时,指定第三个参数为
IPPROTO_RAW(值255),效果和指定IP_HDRINCL选项一样,还更方便些。

--------------------------------------------------------------------------
/*
* For Solaris
* gcc -O3 -o raw raw.c -lsocket -lnsl
*
* For Linux
* gcc -O3 -o raw raw.c
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#define SUCCESS 0
#define FAILURE -1

int recvSocket;
u_char packet[ 1500 ];

void Close ( int fd )
{
if ( close( fd ) == -1 )
{
perror( ""close"" );
exit( FAILURE );
}
return;
} /* end of Close */

void outputBinary ( const unsigned char * byteArray, const size_t byteArrayLen )
{
u_long offset;
int i, j, k;
fprintf( stderr, ""byteArray [ %lu bytes ] ----> "", byteArrayLen );
if ( byteArrayLen <= 0 )
{
return;
}
i = 0;
offset = 0;
for ( k = byteArrayLen / 16; k > 0; k--, offset += 16 )
{
fprintf( stderr, ""%08X "", offset );
for ( j = 0; j < 16; j++, i++ )
{
if ( j == 8 )
{
fprintf( stderr, ""-%02X"", byteArray[i] );
}
else
{
fprintf( stderr, "" %02X"", byteArray[i] );
}
}
fprintf( stderr, "" "" );
i -= 16;
for ( j = 0; j < 16; j++, i++ )
{
/* if ( isprint( (int)byteArray[i] ) ) */
if ( ( byteArray[i] >= '' '' ) && ( byteArray[i] <= 255 ) )
{
fprintf( stderr, ""%c"", byteArray[i] );
}
else
{
fprintf( stderr, ""."" );
}
}
fprintf( stderr, "" "" );
} /* end of for */
k = byteArrayLen - i;
if ( k <= 0 )
{
return;
}
fprintf( stderr, ""%08X "", offset );
for ( j = 0 ; j < k; j++, i++ )
{
if ( j == 8 )
{
fprintf( stderr, ""-%02X"", byteArray[i] );
}
else
{
fprintf( stderr, "" %02X"", byteArray[i] );
}
}
i -= k;
for ( j = 16 - k; j > 0; j-- )
{
fprintf( stderr, "" "" );
}
fprintf( stderr, "" "" );
for ( j = 0; j < k; j++, i++ )
{
if ( ( byteArray[i] >= '' '' ) && ( byteArray[i] <= 255 ) )
{
fprintf( stderr, ""%c"", byteArray[i] );
}
else
{
fprintf( stderr, ""."" );
}
}
fprintf( stderr, "" "" );
return;
} /* end of outputBinary */

int Socket ( int family, int type, int protocol )
{
int n;
if ( ( n = socket( family, type, protocol ) ) < 0 )
{
perror( ""socket"" );
exit( FAILURE );
}
return( n );
} /* end of Socket */

int main ( int argc, char * argv[] )
{
recvSocket = Socket( AF_INET, SOCK_RAW, IPPROTO_ICMP );
while ( 1 )
{
memset( packet, 0, 1500 );
outputBinary( packet, recvfrom( recvSocket, packet, 1500, 0, NULL, NULL ) );
}
Close( recvSocket );
exit( SUCCESS );
} /* end of main */
--------------------------------------------------------------------------

可以参考阅读Phrack49-6、Phrack51-6两篇文章。

上面的raw.c可以用于测试,现在简单讨论一下要实现的功能。一个rs和rc,rs接收
icmp echo request报文,从icmp数据区提取有效数据。icmp数据区可能需要自定义
格式(暂时不考虑加密传输的问题)。比如rs最后得到的指示是要做ls -la操作,那么
rs应该fork/exec执行/bin/sh -c ""ls -la""这个命令。可能涉及到无名管道的技术。
rs将最终shell命令执行输出利用icmp echo reply负载发送到rc。rc负责接收用户输
入并利用icmp echo request负载发送到rs,还接收来自rs的icmp echo reply并提取
有效数据输出到标准输出。简单地、概括地讲,icmp pad backdoor就是这样的。具
体实现中可能有变化,核心部分不变,那就是利用icmp pad交换数据。

另外有个想法,就是利用icmp pad建立一个通道,server host上运行一个daemon,
client host上也运行一个daemon,假设server host位于firewall一侧,client
host位于firewall另一侧。client创建一个tcp socket,监听7704端口,
telnet client 7704,一切输入都被放入icmp pad,client向server发送icmp报文。
server创建一个tcp socket,连接到本机23端口。server接收icmp报文,提取icmp
pad数据,发送到本机23端口。一切来自本机telnetd的数据都被放入icmp pad,
server向client发送icmp报文。client接收icmp报文,提取icmp pad,从本机7704端
口上建立的连接发送出去,实际就是返回给最终用户。

|
user telnet 7704 | telnetd(23)
| | |
| | |
client host ---------- firewall ---------- server host
listen 7704 允许icmp进出 connect 23
| | |
| | |
------------------- icmp tunnel ----------------
|
|

防火墙设置了很多过滤规则,比如只允许http、icmp通过,此时上述icmp tunnel很
好用,可以根据不同需要修改server host上connect的目标端口。可以根据不同需要
安置client/server。client host总是代表最终用户可以直接访问的那一侧。

如果既要突破防火墙过滤规则,又要保证server host上的隐蔽性,至少不能被
netstat看出异常,上述icmp tunnel实现就需要修改。

client --> server的时候使用icmp echo reply效果比较好。有些Linux可能做了设
置,不处理icmp echo request,但是icmp echo reply可以在没有出现过icmp echo
request的情况下被发送被接收,而且也更容易突破防火墙。也可以不使用icmp echo,
但效果可能都不如icmp echo reply。server --> client的时候使用icmp echo
request效果比较好,还是从突破防火墙角度考虑。暂时先这样考虑,以后实在不行
就提供参数设置。

在SPARC/Solaris 2.6下man p2open看看,Linux下没有这个函数。编译的时候需要指
定-lgen开关。如果要在Linux实现同样功能,可能需要自己pipe、fork、dup2然后
exec?

根据p2open的man手册,应该没有重定向标准错误输出,既然如此,还是采用pipe方
式好些,可能将来移植方便吧。

socket( AF_INET, SOCK_RAW, IPPROTO_ICMP )这种套接字,接收的时候对应整个IP
报文,可以读取也必须读取整个IP报文,包括IP头;发送的时候如果没有额外设置
IP_HDRINCL选项,就不能干涉IP头部数据,sendto()参数中的指针指向ICMP数据区,
而不是IP头,内核会构造IP头。

ICMP报文头部的校验和包括ICMP数据区,也就是说对应整个ICMP报文。

因为使用到无名管道,应该处理SIGPIPE信号。很多问题靠man手册和例子代码是无法
理解的。一定要从实际编程、调试中才能找到问题、解决问题。这次的
raw_socket_server.c里处理SIGPIPE信号的技术比较典型,虽然不是最好的解决办法,
但对于我们企图达到的效果足够了。首先设置忽略该信号,然后在write的返回值处
进行errno判断,如果EPIPE,就认为远程shell已经终止,此时我们关闭以前建立的
所有管道,重新创建新的管道,重新fork/exec出远程shell,这个远程shell的标准
输入/输出/错误输出都重定向过了。目前要尽量保持client/server均处在""无状态""
中,避免操作的前后依赖性,否则很多地方处理复杂化。

7.12注:上述这个设计文档的实战例子已经可用,所有提到的猜测和技术问题都得到
实践验证,所以,如果你要写自己的icmp tunnel,可以开工了。上面没有
提到的就是需要加密传输、口令验证。

<待续>