端口检测目的意义:
在网络入侵中,端口扫描几乎是所有网络入侵过程中的必然前奏,入侵者首先通过端口扫描来发现目标主机的操作系统类型,提供的服务和系统所使用的软件版本及其他各种相关信息,然后采取针对性的攻击手段。
因此对端口扫描进行检测,发现可能出现的攻击行为,可有效的配合入侵检测。
实现背景:
端口关联检测中最麻烦的部分应该是根据端口找所关联的进程,在网上找了很长时间,相关资料很少很少,倒是有个博主在讨论服务器netstat -anp 无法获取部分所对应的进程pid和程序名是因为socket文件的inode太大,超过INT_MAX,对netstat源码 的实现提到了端口关联进程。故根据 安卓源码和netstat 源码 简单实现了对端口关联的检测。
相关知识简介:
Ubuntu下网络连接文件简介
由于本实验检测的是在ipv4与ipv6下的tcp和udp本连接中端口的关联情况,所以用到以下4种文件
1、/proc/net/tcp文件,这里记录的是ipv4下所有tcp连接的情况。
2、proc/net/tcp6文件,这里记录的是ipv6下所有tcp连接的情况。
3、/proc/net/udp文件,这里记录的是ipv4下所有udp连接的情况。
4、proc/net/udp6文件,这里记录的是ipv6下所有udp连接的情况。
这4种文件都用到以下数据:
local_address 0101007F:0035本地IP(网络字节序):本地端口(网络字节序)
rem_address 00000000:0000远端IP(网络字节序):远端端口(网络字节序)
st 0A套接字状态,不同套接字对应不同的值
enum {
TCP_ESTABLISHED = 1,
TCP_SYN_SENT, // 2
TCP_SYN_RECV, // 3
TCP_FIN_WAIT1, // 4
TCP_FIN_WAIT2, // 5
TCP_TIME_WAIT, // 6
TCP_CLOSE, // 7
TCP_CLOSE_WAIT, // 8
TCP_LAST_ACK, // 9
TCP_LISTEN, // 0A
TCP_CLOSING, // 1 /* Now a valid state */
TCP_MAX_STATES /* leave at the end! */
};
tx_queue:rx_queue 00000000:00000000发送队列中的数据长度:状态是ESTABLISHED,表示的时接受队列中的数据长度,状态是LISTEN,表示已完成队列的长度。
tr tm->when 00:00000000定时器类型,0表示没有启动定时器。1表示重传定时器,4表示持续定时器,2表示连接定时器、FIN_WAIT_2定时器或TCP保活定时器,3表 示TIME_WAIT定时器。
retrnsmt 00000000超时重传次数。
uid 0用户id。
timeout 0持续定时器或者保活定时器周期性发送出去但未被确认的TCP段数目,收到ACK后清零。
inode 11864 1 0000000000000000 100 0 0 10 0
11864套接字对应的inode
1 sock结构的引用数
0000000000000000 sock结构的实例地址
100 RTO,单位是clock_t
0用来计算延时确认的估值
0快速确认数和是否启用的标志位的或运算结果
10当前拥塞窗口大小
0 如果满启动阀值大于0x7ffffff显示-1,否则表示慢启动阀值
Socket文件中inode分析
socket文件的inode存在于Linux的VFS虚拟文件系统中。VFS是一个异构文件系统之上的软件粘合层,可以让open()、read()、write()等系统调用不用关心底层的存储介质和文件系统类型就可以工作。通过VFS,一个抽象的通用访问接口屏蔽了底层文件系统和物理介质的差异性,每一种类型的文件系统代码都隐藏了实现的细节。对于VFS层和内核的其它部分而言,每一种类型的文件系统看起来都是一样的。
VFS inode和磁盘文件系统中的inode不同。比如ext2文件系统,它的inode位于磁盘上划分的块组中,每个inode 128字节。在分割扇区时,系统会先做出一堆inode供以后使用,inode的数量关系着系统中可以建立的文件及目录总数。磁盘上的每个文件都有且仅有一个inode,即使文件中没有数据,inode也存在。
VFS inode只存在于内存中,可以通过inode缓存访问。每个文件在VFS中都有相应的inode结点,包括普通文件、目录以及特殊文件,如socket、pipe等。在需要某个文件的时候系统会在内存中为其建立相应的inode数据结构,建立的inode结构将形成一个链表,可以通过遍历这个链表去得到我们需要的文件结点。但是,VFS对于不同文件的inode号分配方式是不同的。
具体如下:
对于ext2等磁盘文件系统中的普通文件,每个文件在磁盘上都有对应的inode,该inode号也是已知的。在访问这些文件时,VFS会在内存中为这个文件分配VFS inode,将在磁盘文件系统中确定的inode号赋给inode结构。可见,一般普通文件的inode号都不会太大。
对于socket等特殊文件来说,并没有像磁盘文件一样的inode号的来源,内核在实现中维护了一个unsigned long型的静态变量来保存目前分配的inode号的最大值,新分配的inode号在此基础上加1来实现。这个静态变量的值会一直增大而不会减小,直至机器重启。
具体实现步骤:
1.通过网络文件查找获取端口数据
查找/proc/net/tcp,/proc/net/udp,/proc/net/tcp6,/proc/net/udp6,从文件数据中提取所需要的本地ip,本地port,远端ip,远端port,套接字状态,发送队列中的数据长度,接收队列中的数据长度,以及套接字对应的inode。
2.获取端口所关联的进程
- 首先遍历所有的/proc/PID/fd目录,如果某个进程非本账号所有,则无权访问/proc/PID目录,跳过该PID,访问下一个。
- 当某个/proc/PID/fd目录中有一个或多个socket句柄时,则读该/proc/PID目录下的cmdline文件,获取该进程的所执行程序的名称;同时,记录/proc/PID/fd目录下所有socket句柄的inode号。
- 当所有进程遍历完成后,根据在网络文件proc/net/{tcp,udp,tcp6,udp6}获取的inode节点号,匹配你记录的所有socket句柄的inode后,若相等,则拥有该socket句柄的进程即为该端口关联的进程。
1.socket文件inode信息结构:
static struct prg_node {
struct prg_node *next;
int inode;
char name[PROGNAME_WIDTH];
}*prg_hash[PRG_HASH_SIZE];
name数组里存储进程pid以及进程名
2.ipv4 和ipv6中ip地址 网络字节序到主机字节序的转化。
static void addr2str(int , const void * , char * );
3.对/proc/net/tcp和/proc/net/udp中连接数据的获取和输出。
static void ipv4(const char * , const char *);
4.对/proc/net/tcp6和/proc/net/udp6中连接数据的获取和输出。
static void ipv6(const char *, const char *);
5.将prg_node结构的信息存储于链表中。
static void prg_cache_add(int , char * );
6.遍历链表查看是否存在相等的inode节点,若有,将节点关联进程信息返回。
static const char *prg_cache_get(int );
7.清空链表。
static void prg_cache_clear( );
8.筛选类型为socket和0000的句柄,并使用形-参返回对应的inode节点号。
static void extract_type_1_socket_inode(const char a[], long * );
static void extract_type_2_socket_inode(const char a[], long * );
9.实现遍历/proc/pid/fd和查看/proc/pid/cmdline获取信息。
static void prg_cache_load( );
代码如下:
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <ctype.h>
#include <fcntl.h>
#include <netdb.h>
#include <paths.h>
#include <pwd.h>
#include <getopt.h>
#include <sys/param.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <dirent.h>
#define PROGNAME_WIDTH 20
#define PROGNAME_WIDTHs PROGNAME_WIDTH1(PROGNAME_WIDTH)
#define PROGNAME_WIDTH1(s) PROGNAME_WIDTH2(s)
#define PROGNAME_WIDTH2(s) #s
#define PRG_HASH_SIZE 211
#define PRG_HASHIT(x) ((x) % PRG_HASH_SIZE)
#ifndef LINE_MAX
#define LINE_MAX 4096
#endif
#define PATH_PROC "/proc"
#define PATH_FD_SUFF "fd"
#define PATH_FD_SUFFl strlen(PATH_FD_SUFF)
#define PATH_PROC_X_FD PATH_PROC "/%s/" PATH_FD_SUFF
#define PATH_CMDLINE "cmdline"
#define PATH_CMDLINEl strlen(PATH_CMDLINE)
#define PRG_LOCAL_ADDRESS "local_address"
#define PRG_INODE "inode"
#define PRG_SOCKET_PFX "socket:["
#define PRG_SOCKET_PFXl (strlen(PRG_SOCKET_PFX))
#define PRG_SOCKET_PFX2 "[0000]:"
#define PRG_SOCKET_PFX2l (strlen(PRG_SOCKET_PFX2))
static char prg_cache_loaded = 0;
typedef union iaddr iaddr;
typedef union iaddr6 iaddr6;
static struct prg_node {
struct prg_node *next;
int inode;
char name[PROGNAME_WIDTH];
} *prg_hash[PRG_HASH_SIZE];
union iaddr {
unsigned u;
unsigned char b[4];
};
union iaddr6 {
struct {
unsigned a;//unsigned int a; 无符号整型
unsigned b;
unsigned c;
unsigned d;
} u;
unsigned char b[16];
};
static const char *state2str(unsigned state)
{
switch(state){
case 0x1: return "ESTABLISHED";
case 0x2: return "SYN_SENT";
case 0x3: return "SYN_RECV";
case 0x4: return "FIN_WAIT1";
case 0x5: return "FIN_WAIT2";
case 0x6: return "TIME_WAIT";
case 0x7: return "CLOSE";
case 0x8: return "CLOSE_WAIT";
case 0x9: return "LAST_ACK";
case 0xA: return "LISTEN";
case 0xB: return "CLOSING";
default: return "UNKNOWN";
}
}
/* addr + : + port + \0 */
#define ADDR_LEN INET6_ADDRSTRLEN + 1 + 5 + 1
static void addr2str(int af, const void *addr, char *buf)
{
if (inet_ntop(af, addr, buf, ADDR_LEN) == NULL)
*buf = '\0';
return;
}
static void prg_cache_add(int inode, char *name)
{
unsigned hi = PRG_HASHIT(inode);
struct prg_node **pnp,*pn;
prg_cache_loaded=2;
for (pnp=prg_hash+hi;(pn=*pnp);pnp=&pn->next) {
if (pn->inode==inode) {
/* Some warning should be appropriate here
as we got multiple processes for one i-node */
return;
}
}
if (!(*pnp=malloc(sizeof(**pnp))))
return;
pn=*pnp;
pn->next=NULL;
pn->inode=inode;
if (strlen(name)>sizeof(pn->name)-1)
name[sizeof(pn->name)-1]='\0';
strcpy(pn->name,name);
}
static const char *prg_cache_get(int inode)
{
unsigned hi=PRG_HASHIT(inode);
struct prg_node *pn;
for (pn=prg_hash[hi];pn;pn=pn->next)
if (pn->inode==inode) return(pn->name);
return("-");
}
static void prg_cache_clear(void)
{
struct prg_node **pnp,*pn;
if (prg_cache_loaded == 2)
for (pnp=prg_hash;pnp<prg_hash+PRG_HASH_SIZE;pnp++)
while ((pn=*pnp)) {
*pnp=pn->next;
free(pn);
}
prg_cache_loaded=0;
}
static void extract_type_1_socket_inode(const char lname[], long * inode_p) {
/* If lname is of the form "socket:[12345]", extract the "12345"
as *inode_p. Otherwise, return -1 as *inode_p.
*/
if (strlen(lname) < PRG_SOCKET_PFXl+3) *inode_p = -1;
else if (memcmp(lname, PRG_SOCKET_PFX, PRG_SOCKET_PFXl)) *inode_p = -1;
else if (lname[strlen(lname)-1] != ']') *inode_p = -1;
else {
char inode_str[strlen(lname + 1)]; /* e.g. "12345" */
const int inode_str_len = strlen(lname) - PRG_SOCKET_PFXl - 1;
char *serr;
strncpy(inode_str, lname+PRG_SOCKET_PFXl, inode_str_len);
inode_str[inode_str_len] = '\0';
*inode_p = strtol(inode_str,&serr,0);
if (!serr || *serr || *inode_p < 0 || *inode_p >= INT_MAX)
*inode_p = -1;
}
}
static void extract_type_2_socket_inode(const char lname[], long * inode_p) {
/* If lname is of the form "[0000]:12345", extract the "12345"
as *inode_p. Otherwise, return -1 as *inode_p.
*/
if (strlen(lname) < PRG_SOCKET_PFX2l+1) *inode_p = -1;
else if (memcmp(lname, PRG_SOCKET_PFX2, PRG_SOCKET_PFX2l)) *inode_p = -1;
else {
char *serr;
*inode_p=strtol(lname + PRG_SOCKET_PFX2l,&serr,0);
if (!serr || *serr || *inode_p < 0 || *inode_p >= INT_MAX)
*inode_p = -1;
}
}
static void prg_cache_load(void)
{
char line[LINE_MAX],eacces=0;
int procfdlen,fd,cmdllen,lnamelen;
char lname[30],cmdlbuf[512],finbuf[PROGNAME_WIDTH];
long inode;
const char *cs,*cmdlp;
DIR *dirproc=NULL,*dirfd=NULL;
struct dirent *direproc,*direfd;
if (prg_cache_loaded ) return;
prg_cache_loaded=1;
cmdlbuf[sizeof(cmdlbuf)-1]='\0';
if (!(dirproc=opendir(PATH_PROC))) goto fail;
while (errno=0,direproc=readdir(dirproc)) {
#ifdef DIRENT_HAVE_D_TYPE_WORKS
if (direproc->d_type!=DT_DIR) continue;
#endif
for (cs=direproc->d_name;*cs;cs++)
if (!isdigit(*cs))
break;
if (*cs)
continue;
procfdlen=snprintf(line,sizeof(line),PATH_PROC_X_FD,direproc->d_name);
if (procfdlen<=0 || procfdlen>=sizeof(line)-5)
continue;
errno=0;
dirfd=opendir(line);
if (! dirfd) {
if (errno==EACCES)
eacces=1;
continue;
}
line[procfdlen] = '/';
cmdlp = NULL;
while ((direfd = readdir(dirfd))) {
#ifdef DIRENT_HAVE_D_TYPE_WORKS
if (direfd->d_type!=DT_LNK)
continue;
#endif
if (procfdlen+1+strlen(direfd->d_name)+1>sizeof(line))
continue;
memcpy(line + procfdlen - PATH_FD_SUFFl, PATH_FD_SUFF "/",
PATH_FD_SUFFl+1);
strcpy(line + procfdlen + 1, direfd->d_name);
lnamelen=readlink(line,lname,sizeof(lname)-1);
lname[lnamelen] = '\0'; /*make it a null-terminated string*/
extract_type_1_socket_inode(lname, &inode);
if (inode < 0) extract_type_2_socket_inode(lname, &inode);
if (inode < 0) continue;
if (!cmdlp) {
if (procfdlen - PATH_FD_SUFFl + PATH_CMDLINEl >=
sizeof(line) - 5)
continue;
strcpy(line + procfdlen-PATH_FD_SUFFl, PATH_CMDLINE);
fd = open(line, O_RDONLY);
if (fd < 0)
continue;
cmdllen = read(fd, cmdlbuf, sizeof(cmdlbuf) - 1);
if (close(fd))
continue;
if (cmdllen == -1)
continue;
if (cmdllen < sizeof(cmdlbuf) - 1)
cmdlbuf[cmdllen]='\0';
if ((cmdlp = strrchr(cmdlbuf, '/')))
cmdlp++;
else
cmdlp = cmdlbuf;
}
snprintf(finbuf, sizeof(finbuf), "%s/%s", direproc->d_name, cmdlp);
prg_cache_add(inode, finbuf);
}
closedir(dirfd);
dirfd = NULL;
}
if (dirproc)
closedir(dirproc);
if (dirfd)
closedir(dirfd);
if (!eacces)
return;
if (prg_cache_loaded == 1) {
fail:
fprintf(stderr,("(No info could be read for \"-p\": geteuid()=%d but you should be root.)\n"),
geteuid());
}
else
fprintf(stderr, ("(Not all processes could be identified, non-owned process info\n"
" will not be shown, you would have to be root to see it all.)\n"));
}
static void ipv4(const char *filename, const char *label) {
FILE *fp = fopen(filename, "r");
if (fp == NULL) {
return;
}
char buf[BUFSIZ];
fgets(buf, BUFSIZ, fp);
while (fgets(buf, BUFSIZ, fp)){
char lip[ADDR_LEN];
char rip[ADDR_LEN];
iaddr laddr, raddr;
unsigned lport, rport, state, txq, rxq, num;
unsigned tr,tmWhen,ret;
unsigned uid,timeout,inode;
int n = sscanf(buf, " %d: %x:%x %x:%x %x %x:%x %x:%x %x %x %x %d",
&num, &laddr.u, &lport, &raddr.u, &rport,
&state, &txq, &rxq,&tr,&tmWhen,&ret,&uid,&timeout,&inode);
if (n == 14) {
addr2str(AF_INET, &laddr, lip);
addr2str(AF_INET, &raddr, rip);
printf("%10d %-15s %5d %-15s %6d %6d %s %12s %5d %15s\n",lport,lip,rport,rip,txq,rxq,label,state2str(state),inode,prg_cache_get(inode));
}
}
fclose(fp);
}
static void ipv6(const char *filename, const char *label) {
FILE *fp = fopen(filename, "r");
if (fp == NULL) {
return;
}
char buf[BUFSIZ];
fgets(buf, BUFSIZ, fp);
while (fgets(buf, BUFSIZ, fp)){
char lip[ADDR_LEN];
char rip[ADDR_LEN];
iaddr6 laddr6, raddr6;
unsigned lport, rport, state, txq, rxq, num;
unsigned tr,tmWhen,ret;
unsigned uid,timeout,inode;
int n = sscanf(buf, " %d: %8x%8x%8x%8x:%x %8x%8x%8x%8x:%x %x %x:%x %x:%x %x %x %x %d",
&num, &laddr6.u.a, &laddr6.u.b, &laddr6.u.c, &laddr6.u.d, &lport,
&raddr6.u.a, &raddr6.u.b, &raddr6.u.c, &raddr6.u.d, &rport,
&state, &txq, &rxq,&tr,&tmWhen,&ret,&uid,&timeout,&inode);
if (n == 20) {
addr2str(AF_INET6, &laddr6, lip);
addr2str(AF_INET6, &raddr6, rip);
printf("%10d %-15s %5d %-15s %6d %6d %s %12s %5d %15s\n",lport,lip,rport,rip,txq,rxq,label,state2str(state),inode,prg_cache_get(inode));
}
}
fclose(fp);
}
int main(int argc, char *argv[])
{
prg_cache_load();
printf("Local Port Local Address Fport Foreign Address Recv-Q Send-Q proto state inode Pid/Program name\n");
ipv4("/proc/net/tcp", "tcp");
ipv4("/proc/net/udp", "udp");
ipv6("/proc/net/tcp6", "tcp6");
ipv6("/proc/net/udp6", "udp6");
prg_cache_clear();
return 0;
}
运行结果: