基于proc文件系统的简易主机端口扫描器

博士归来,提出,捕捉到app全部端口就可以追踪app的数据发送了,果然好想法;这个比劫持系统调用实现应该简单许多了。
于是,想到netsat ,zys提到lsof。先看netstat,顺便看到这一文章,遂记下来,多谢原作者,他还题动了net-tool源码,呵呵

          

基于proc文件系统的简易主机端口扫描器


一 引言
     proc文件是内核的一个快照, 它存储了系统运行时的状态信息,同时又可以作为输入接口- 用户可以修改/proc目录下一些文件中的内容来改变内核运行时的参数设置. 本文属于对第一种功能的应用,即通过读取proc文件来实现netstat命令的一些基本功能, 包括TCP和UDP端口的扫描(支持IPV6格式的地址).
二  /proc/net/文件介绍
打开Linux源代码中的文档proc.txt
(linux-2.6.16.20/Documentation/filesystems),我们可以在目录表中看到一栏
1.4                         Networking info in /proc/net
猜测一下,/proc/net目录下的文件大概存储的就是与网络有关的信息吧,定位到1.4看看具体的解释:
表1-6: /proc/net下的ipv6信息.
.................................................................
文件                    内容
udp6                    udp套接字(ipv6)
tcp6                    tcp套接字(ipv6)
raw6                    原始设备统计信息(ipv6)
.............
正如目录所介绍的, /proc/net下的文件存储了系统运行时与网络相关的信息,其中tcp文件中存储了TCP套接字的信息, udp文件中存储了UDP套接字的信息,本文的程序中要实现的端口扫描器的直接数据就是来源于此(还有tcp6文件和udp6文件).
三 从数据源入手
首先我们应该明确, 协议端口的概念只有在IP层以上的传输层才是有意义的, 因为IP层只能将IP数据包传送到主机,而无法分发给具体的进程, 为了实现传输层进程到进程的通讯功能, 除了使用主机IP地址外, 在TCP/IP协议中又加入了进程的区分标志,就是端口号的概念. 本文要实现的端口扫描程序就是要获取此类协议端口的信息.而这些协议端口又是与TCP,UDP套接字相关联的,所以只要获取到所有的套接字信息,也就取到 了端口的信息,而proc文件系统正好在/proc/net的tcp(6)和udp(6)文件中为我们提供了所有的tcp与udp套接字的信息,所以我们 可以很容易的从这些文件中解析出所有的端口信息.
接下来,看一下具体的文件内容:
less /pro/net/tcp
sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
0: 00000000:8000 00000000:0000 0A 00000000:00000000 00:00000000 00000000    29        0 4804 1 f73bd700 3000 0 0 2 -1                             
   1: 3A00AA0A:00C7 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 25926 1 f73bc080 3000 0 0 2 -1                           
   2: 00000000:008B 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 5819 1 f6d37700 3000 0 0 2 -1                             
   3: 00000000:006F 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 4759 1 f73bdb80 3000 0 0 2 -1  
根据标题提示, 并和netstat -tn命令的输出进行对比, 可以分析出这个文件中每列数据的意义: 第一列是打开套接字的编号,1,2,3..表示打开了几个此种类型的套接字, 第二列是本地地址,格式为"十六进制(网络字节序)的IP地址:端口号", 第三列是远程地址,第四列是连接状态(st = status), 后面的信息与我们的程序没什么关系,不做介绍;而且文件中的每行都代表了一个完整的套接字的信息.同样可知, udp文件中的信息也是类似的组织起来的,只是在远程地址和状态上有些区别(udp套接字中对于远程地址不给出任何信息,而且UDP套接字不存在连接状态 的概念).
      需要注意的另外一个问题就是, 如果我们的程序所在的机子支持IPV6协议的话,那么这个端口扫描程序应该也能表示出此类的IP地址,暂不谈IPV4和IPV6的具体区别有那些,单单从 tcp和tcp6(如果你所使用的内核版本支持IPV6的话,/proc/net下面应该会有这个文件的)文件中的内容来对比, 可以得出一个初步的结论,那就是IPV4的IP地址采用8位的16进制数来表示,而IPV6的地址采用32位的16进制数来表示,所以你可以将一个 IPV4的地址表示为XX.XX.XX.XX的形式, 而将一个IPV6的地址表示为
XXXX.XXXX.XXXX.XXXX. XXXX.XXXX.XXXX.XXXX的形式,事实上IPV6地址就是这样表示的(IPV4地址长度为128位,采用8位16进制数表示,而新的 IPV6地址为128位,采用32位16进制数来表示),关于两者的细微区别,可以查阅其它资料,因为我并不知道太多..
      在netstat的源代码中,系统将每个套接字的所有信息存储在一个sockaddr_in结构体中,所以对于不同版本的IP协议, 就要使用不同类型的struct sockaddr_in,这一点似乎有些麻烦.仔细分析我们要实现的功能和proc目录下的文件, 可以发现, proc文件中存储的是纯粹的数据, 而其他的实现都是可选的,所以我们应该可以不使用任何与平台相关的数据结构就能完成这个功能,在本文中,我就是这么做的: 为了将128位的IPV6地址和32的IPV4地址采用统一的方式进行存储, 我们使用一个32字节的字符数组来存储IP地址, 每一位存储一个字符格式的十六进制数字,而没有采用网络编程的sockaddr结构体. 因为我的目的是使这个程序尽可能的简单,并尽量减少与平台的关联,因为它的原理很简单,就是读文件,所以,理论上说,在任何平台上,这个程序的代码都应该 能编译通过,即所谓的平台无关性.事实上,本文的代码正式这样的(如果windows系统也提供了相同的proc文件的话,这个程序不需要任何修改马上就 能应用于新的平台.)
四 程序实现过程中需要注意的问题
分析/proc/net/tcp文件的格式可以发现:
   第一,文件中的每一行字符串对应了一个套接字的信息,因此我们可以通过解析这一行字符串来生成一个套接字的信息(对于UDP来说是一个套接字,对于 tcp来说就是一对套接字),不妨将之称为一个协议端口信息,如果你平时对函数设计很重视的话,你可能马上会想到构造一个函数,它的输入是一个字符串,返 回一个有效或者无效的表示端口信息的结构体,对了,其实我也是这么想的.
   第二,/proc/net/udp文件和/proc/net/tcp文件中的数据格式几乎是一摸一样,所以对于udp和tcp端口,完全没有必要使用不 同的函数来读取并解析,而只要通过一个通用的函数加上若干个标志位就可以了, 而要区分是udp还是tcp,是ipv6还是ipv4,文件名已经足够了.
    第三,注意到/proc文件中的数据是网络字节序存储的,所以我们要手动将其转换成主机上的本地字节序,这样就不会得到错误的数据了.
    第四,为了将IPV4与IPV6的地址统一存储,并且不使用数据结构sockaddr_in,我们将IP地址存储在一个32个字节的数组中,并且每次扫描 时都尝试去读取一个v6版本的文件(tcp6和udp6), 如果存在这个文件的话,说明系统支持IPV6,反之,如果系统不支持IPV6,打开文件会失败,当然我们将认为这是一个正常的情况(因为该系统根本不支持 IPV6,但是如果打开tcp或者udp文件出错,那你就得注意了).
     第五,一般的端口都是与服务名或者程序名绑定的,在Linux系统上,你可以通过查看/etc/service文件来获取这些信息, 另外, linux系统提供了若干函数来获取服务的端口以及服务名等信息,在netdb.h文件中(/usr/include/netdb.h)中有定义

struct servent
{
  char *s_name;//正式的服务名称
  char ** s_aliases;//服务别名列表
  int s_port;//服务端口
  char* s_proto;//使用的协议
}

如果你已经知道了一个服务的名字,就可以通过函数

struct servent * getservbyname(char*serv_name, char* proto)

来获取这个服务的其它信息,从而得到端口的数值,反之,如果你已经拿到了一个端口号,就可以通过函数

struct servent * getservbyport(int proto, const char)

来获取这个服务的其它信息,从而得到服务的名字,但有一个前提,那就是函数的返回值必须是一个有效的指向struct servent类型的指针,因为并不是每次调用这些函数都能返回一个有效的结构体指针,举个例子,当你调用

getservebyname("ssh", "tcp")

时,肯定会返回一个有效的结构提指针,因为这个服务在大多数Linux系统中是存在的,所以代码

Struct servent * serv = getservbyname("ssh", "tcp"),
printf("%d",  ntohs(serv->s_port))

将会输出22,因为一般的ssh服务使用的都是tcp协议,并且占用22号端口,但是,

getservbyname("ning", "ning")

肯 定会返回一个无效的结构体指针,暂不说"ning"这个服务名存不存在,一个名为"ning"的协议应该不存在吧.当然,对于getservbyport 也需要注意返回指针是否有效的问题,所以在这些函数返回之后,一定要先检验指针的有效性,然后再去引用其中的值,如果你直接去取其中的成员;如下

Struct servent* serv = getservbyport(ntohs(9999), "ning");
Printf("%s/n", serv->s_name);

可以肯定程序会在此处出现段错误, 因为你访问了无效的指针,所以应该加上一句判断

if(serv)
{
  Printf("%s/n", serv->s_name);
}

之所以在这里对这个问题讨论这么多,我们的最终目的就是在获取到每个端口的时候,再去检测看它是不是一个知名端口,如果是的话,就在数字格式端口的后面加上此端口对应的服务名,以便于理解,并与netstat的-n和-N参数相统一.
     还需要注意的就是,在proc文件系统中, IPV6地址是用32个字节存储起来的字符串,IPV4地址是用8个字节存储的字符串,所以我们可以通过这个字段的长度来区分是IPV4还是IPV6格式 的地址, 在netstat的源码中和我的代码中都是这么做的,因为我还没有什么更好的办法来区分这二者.
     最后一个问题(程序员讨论的问题大都是针对策略而很少涉及机制,因为他们都知道要去做什么,但是却会为采取那种方法去实现要求的功能(怎么做)而产 生争议.经常是每个人都有一套自己的实现方法,而且他能就自己的方法讲出一定的优缺点来.但是最终还是得采取一个最好的途径,在本文中也是如此)是: 我们已经知道,文件中的每一行存储了一个端口信息,所以,有人马上会想到一个主意,那就是每读一行,就对这行进行解析,然后得到一个端口信息,接着读下一 行.这是一种可行的方法.但是对于有经验的程序员来说,他可能会不太满意这种做法,因为,作为程序来说,我们要求某一模块(或者某一段代码)的功能尽可能 单一,不要跟其他段之间有太大的关联.而且在文件的读写中间去解析读到的内容,并构造一个结构体,(我个人认为)这本身就是是一个不太好的编程习惯,你必 须清楚,你是在读写文件,每打开一个文件,就对应产生了一个文件描述符,而每个文件被打开的次数都是有上限的,因此你的程序必须用尽可能短的时间来处理文 件的读写过程,从而避免可能出现的错误或者系统瓶颈.这个道理在系统的中断处理中体现的特别明显(读文件和解析内容应该正好对应了中断上半部处理和下半部 处理).所以,在本文的程序中我们总是先将文件的所有有用信息读取到一个缓存中,关闭文件.然后再逐行进行解析,得到端口的信息,事实上,Linux下有 些版本的netstat的源代码也是这么处理的.
       作为实现不止一个功能的程序来说,用户并不期望在每次运行该程序时都去启动它所有的功能,而是希望有选择的执行这些功能,这也符合常规的思想,所以自然而 然的就是,我们的程序必须能够接受用户传递的不同参数并进行不同的处理(这是任何一个Linux命令最起码的功能).在一开始的时候,我使用了Linux 的getopt函数进行处理,但是为了使代码的编译尽可能不依赖于平台(因为它的功能只与系统提供的功能有关),我最终放弃了这个函数,而且由于本人比较 懒惰, 所以也没有去仔细的阅读别人给我的适用于多平台的getopt函数,而是做了一个比较笨拙的处理: 程序只接受一个输入参数,这个参数是一个字符串,而且它的值只能为tcp,udp,或者all这三者之一.对于tcp参数,我们打印tcp套接字的所有端 口信息,对于udp参数,我们打印udp套接字的所有端口信息,当然all就是打印所有的信息了,对于不同的参数,我们统一调用do_command函数 来进行响应.这种处理确实有些逊色.
        在文章将要结束的时候,忽然意识到这个端口扫描器其实就是一个简单的读文件的C程序,只要知道proc文件的功能,谁都做的出来,而且语言的选择也 决不局限于C语言,你也可以用perl,shell,python,ruby等来处理,相信使用shell的awk命令应该不用花费太大气力也能作出这个 功能来.
五 源代码以及Makefile

#ifndef _PORT_SCANNER_
#define _PORT_SCANNER_
/**********PortScanner.h**************
*
*author :duanjigang @2006-12 <duanjigang1983@126.com>
*
*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
/*
include this file when the program is complied
on a unix os, macro "__LINUX__" was defined in
the Makefile with flag -D  
*/
#ifdef __LINUX__
#include <netdb.h>
#endif

#define PROC_TCP "/proc/net/tcp" /*tcp sockets*/
#define PROC_UDP "/proc/net/udp" /*udp sockets*/
#define PROC_TCP6 "/proc/net/tcp6" /*tcp sockets for ipv6*/
#define PROC_UDP6 "/proc/net/udp6" /*udp sockets for ipv6*/

#define ADDR_SIZE 32 /*size  of a char array to
store a 32 BYTE long address*/
#define ERROR_STATUS 0
#define INVALID_PORT -1
/*
define protocol type for our program, just tcp and udp
*/
typedef enum
{
ERROR_PROTO,
UDP_PROTO,
TCP_PROTO
} PROTO_TYPE;
/*
check an valid protocol type
*/
static int VALID_PROTO(PROTO_TYPE type)
{
int nRet = 0;
switch(type)
{
case UDP_PROTO:
case TCP_PROTO:
         nRet =1;
         break;
default : break;
}
return nRet;
}
/*
get the name of a specific protocol
*/
static char * STR_PROTO(PROTO_TYPE type)
{
static char szRet[20];
switch(type)
{
case UDP_PROTO: sprintf(szRet, "udp");break;
case TCP_PROTO: sprintf(szRet, "tcp"); break;
default: sprintf(szRet,  "UNKNOW PROTO");break;
}
return szRet;
}
/*
ipversion type
*/
typedef enum
{
  IP_VERSION_ERROR = 0,
  IP_VERSION4 = 4,
  IP_VERSION6 = 6,
} IP_VERSION_TYPE;
/*
define tcp status for a tcp connection
*/
#ifndef _TCP_STATUS_
#define _TCP_STATUS_
#define TCP_STATUS_COUNT 12 /*size of this array*/
static const char * TCP_STATUS [] =
{
  "ERROR_STATUS",
  "TCP_ESTABLISHED",
  "TCP_SYN_SENT",
  "TCP_SYN_RECV",
  "TCP_FIN_WAIT1",
  "TCP_FIN_WAIT2",
  "TCP_TIME_WAIT",
  "TCP_CLOSE",
  "TCP_CLOSE_WAIT",
  "TCP_LAST_ACK",
  "TCP_LISTEN",
  "TCP_CLOSING",
};
#endif
/*
define a struct for a (pair of) port(s)
noticed that with a udp sockets, nStatus is of no meaning  
*/
struct port
{
char szLocalAddr[ADDR_SIZE]; //local ip addres
char szRemotAddr[ADDR_SIZE]; //remete ip address
IP_VERSION_TYPE nVersion; //ip version
int nLocalPort; //local port
int nRemotPort; //remote port if exists
int nStatus;//status for tcp sockets but not udp sockets
PROTO_TYPE proto; //tcp or udp
};

/*
just cleanup the memory of a  port stuct
return 0 when the pointer is an invalid one
*/
extern int init(struct port * port);
/*
decode contents of data and store all information into
struct port, return 0 when port or data is an invalid pointer
*/
extern int fill_data (struct port * port, const char * data);
/*
get the information in struct port with a user-readable format
if flag equals to 1, the formation is print to the screen too
when it is return in a buffer, we must use type to
decide the format of this string that will be returned and printed
*/
extern char* print (const struct port* port, int flag, PROTO_TYPE type);
/*
array, an array used to store all the port information of a
protocol decided by protocol type
size: the max number of ports can stored in this array
*/
extern int get_port_list(struct port * array, int size, PROTO_TYPE type);
static unsigned int SHOW_TCP = 0X01;
static unsigned int SHOW_UDP = 0X02;
static unsigned int SHOW_ALL = 0X0F;
/*
deal with input parameters
*/
extern int do_command(unsigned int command);
#endif
 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
操作系统实验中的proc文件系统是一种特殊的文件系统,主要用于提供操作系统内核信息的访问和管理。在实现proc文件系统时,需要完成以下几个主要步骤。 首先,需要定义一个proc文件结构体,用于描述proc文件的属性和操作。这个结构体通常包含文件名、文件大小、文件访问权限等信息,以便操作系统能够对其进行正确的处理。 接下来,需要实现创建proc文件的函数。这个函数负责在内核中创建一个proc文件,并将文件结构体与之关联。在创建过程中,需要为proc文件分配一个唯一的文件名,并指定相应的访问权限。 然后,需要实现读取proc文件内容的函数。这个函数会在用户空间打开proc文件时被调用,并将proc文件的内容返回给用户。根据不同的需求,可以通过读取内核变量或调用相关系统调用来获取文件内容。 此外,还需要实现写入proc文件内容的函数。这个函数会在用户空间对proc文件进行写入时被调用,并将用户输入的内容写入到相应的内核变量或系统调用中。 最后,还需要实现删除proc文件的函数。这个函数会在用户空间关闭proc文件或操作系统关闭时被调用,负责释放proc文件的相关资源。 总之,实现proc文件系统需要定义文件结构体、创建、读取、写入和删除文件的函数。通过这些功能,用户可以在用户空间访问和管理内核的信息,提供了一个方便的方式来查看和调试操作系统内部状态。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值