0x00 背景——信息内容安全实验

实验要求

  • 使用浏览器登录WebMail,并撰写、发送一封邮件;
  • 编写程序监测登录、发信网络流的交互过程;
  • 详细分析获取用户名、密码、收件人、发件人、发件时间、邮件主题、邮件内容等信息;
  • 撰写报告:图文、代码。

提示 & 注意

  • 基于Libpcap进行网络编程;
  • 使用HTTP通信协议方式的WebMail即可,HTTPS可能涉及对SSL协议的分析,有能力同学可以挑战一下;
  • 邮件正文使用TXT编码即可,有能力的同学可以尝试其他的,也可以挑战一下抓取邮件的附件。


0x01 进行实验

1.0 实验环境准备

  • 找到一个使用HTTP协议的WebMail,我查了好久和听其他同学说的一共有两个可以用——一个是QQ邮箱基础版、一个是TOM邮箱。(不过这两个我都没有用,使用的是舍友用Roundcube Webmail搭建的一个WebMail)。
  • 系统的话我这里使用的是Ubuntu18.04,其他的系统应该也没有关系。
  • 安装还是比较简单的: 先去官网或者GitHub下载安装包,然后解压进入目录打开终端,依次使用./configuremakesudo make install即可安装成功。(这个地方要是出错的话就自行Google叭)
  • 然后可以装个IDE或者编辑器,开始准备写代码。

1.1 学习Libpcap

  • 推荐在官网学习Libpcap各个函数的使用,或者能力强的同学可以直接阅读其源码
  • 接下来我就大概介绍一下几个基本函数的使用和意义吧(我这里就简单介绍一下,具体的还是建议大家去官网看源码):
  1. pcap_lookupdev()

    • 原型:char *pcap_lookupdev(char *errbuf)
    • 作用:函数用于查找网络设备,返回可被 pcap_open_live() 函数调用的网络设备名指针。
  2. pcap_lookupnet()

    • 原型:int pcap_lookupnet(const char *device, bpf_u_int32 *netp, bpf_u_int32 *maskp, char *errbuf)
    • 作用:函数获得指定网络设备的网络号和掩码。
  3. pcap_open_live()

    • 原型:pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms, char *ebuf)
    • 作用:函数用于打开网络设备,并且返回用于捕获网络数据包的数据包捕获描述字。对于此网络设备的操作都要基于此网络设备描述字。
  4. pcap_compile()

    • 原型:int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask)
    • 作用:函数用于将用户制定的过滤策略编译到过滤程序中。
  5. pcap_setfilter()

    • 原型:int pcap_setfilter(pcap_t *p, struct bpf_program *fp)
    • 作用:函数用于设置过滤器。
  6. pcap_loop()

    • 原型:int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)
    • 作用:函数 pcap_dispatch() 函数用于捕获数据包,捕获后还可以进行处理,此外 pcap_next() 和 pcap_next_ex() 两个函数也可以用来捕获数据包。
  7. pcap_close()

    • 原型:void pcap_close(pcap_t *p)
    • 作用:函数用于关闭网络设备,释放资源。
  • 官网的Programming with pcap中的例子还是比较简单可以看明白,用Libpcap进行抓包的思路也十分简单(站在巨人的肩膀上的我们真的是太幸福了),基本步骤如下:

    1. 打开网络设备
    2. 设置过滤规则
    3. 捕获数据 (a.分析数据 b.显示结果)
    4. 关闭网络设备
  • 到现在的,基本上已经对函数和实现思想应该都明白个差不多了,开始撸代码。

1.2 代码 v1.0

  • 代码比较简单,重要的点基本上都写在注释里面了,这里就不再赘述了。
  • 这些代码只实现一个功能,抓发往指定域名的包并输出包的内容。
#include <pcap.h>
#include <stdio.h>

// pcap_loop() 的回调函数,包分析与处理函数
void capture(u_char *user, const struct pcap_pkthdr *packet_header, const u_char *packet_content);

// 数据包格式化输出函数
void print_packet(int packet_id, const struct pcap_pkthdr *packet_header, const u_char *packet_content);

// 主函数 抓包框架的实现函数
int main(int argc, char **argv) {
    pcap_t *handle;                             // 会话句柄
    char *device;                               // 设备
    char errbuf[PCAP_ERRBUF_SIZE] = {0};        // 存储错误信息,其中PCAP_ERRBUF_SIZE 为宏定义的错误缓冲区大小
    struct bpf_program filter;                  // 已经编译好的过滤器
    bpf_u_int32 ip_mask;                        // 所在网络掩码
    bpf_u_int32 ip_addr;                        // 主机的IP地址
    char filter_rules[] = "dst host mail.hitaq.cn"; // 过滤规则 这里的域名是我用的邮箱系统的地址
    int res = 0;                                // 暂存各种函数的返回值,用来判断成功与否

    // 查找网络设备,得到可用的网络设备名指针
    device = pcap_lookupdev(errbuf); // 返回第一个合法的设备 我这里是 wlp3s0
    if (device == NULL) {
        fprintf(stderr, "%s\n", errbuf);
        exit(1);
    }

    // 获取指定网卡的 ip 地址,子网掩码
    res = pcap_lookupnet(device, &ip_addr, &ip_mask, errbuf);
    if (res == -1) {
        printf("Error@pcap_lookupnet(): %s\n", errbuf);
        exit(1);
    }

    // 以混杂模式打开一个用于捕获数据的网络接口
    handle = pcap_open_live(device, BUFF_SIZE, 1, 0, errbuf);
    if (handle == NULL) {
        printf("Error@pcap_open_live(): %s\n", errbuf);
        exit(1);
    }

    // 将用户制定的过滤策略编译到过滤程序中
    if (pcap_compile(handle, &filter, filter_rules, 0, ip_addr) == -1) {
        fprintf(stderr, "Error calling pcap_compile\n");
        exit(1);
    }

    // 设置过滤器
    if (pcap_setfilter(handle, &filter) == -1) {
        fprintf(stderr, "Error setting filter\n");
        exit(1);
    }

    puts("Capturing...\n\n");
    // 捕包 -1代表无限循环
    pcap_loop(handle, -1, capture, NULL);

    // 关闭网络设备,释放资源
    pcap_close(handle);

    return 0;
}

void capture(u_char *user, const struct pcap_pkthdr *packet_header, const u_char *packet_content) {
    static unsigned int count = 0;

    // 小于64byte的数据包为坏包 直接丢弃
    if (packet_header->caplen <= 64) return;

    count++; // 记录接收到的包的顺序
    print_packet(count, packet_header, packet_content);  // 打印整个包的信息
}

void print_packet(int packet_id, const struct pcap_pkthdr *packet_header, const u_char *packet_content) {
    printf("\n----------- Package Content -----------\n");
    printf("Packet ID       : %d\n", packet_id);
    printf("Packet length   : %d\n", packet_header->len);
    printf("Number of bytes : %d\n", packet_header->caplen);
    printf("Received time   : %s\n", ctime((const time_t *) &(packet_header->ts.tv_sec)));

    for (int i = 0; i < packet_header->caplen; i++) {
        // 以十六进制的模式输出
        printf(" %02x", packet_content[i]);
        if ((i + 1) % 32 == 0) printf("\n");

        // 以文本模式输出
        // printf("%c", packet_content[i]);
    }

    for (int i = 0; i < packet_header->caplen; i++) {
        printf("%c", packet_content[i]);
    }
    printf("\n--------------- End ---------------\n");
}

1.3 代码 v2.0

  • 实验基本要求还算简单,v1.0已经可以抓到包了,接下来的工作就是分析数据了,这个地方每个邮件系统应该是都不一样的。
  • 我这里就把我的实现方式贴出来,抛砖引玉吧。
typedef __u_char u_char;
#define L_LEN 1514
#define M_LEN 512
#define S_LEN 256
#define T_LEN 128

// pcap_loop() 的回调函数,包分析与处理函数
void capture(u_char *user, const struct pcap_pkthdr *packet_header, const u_char *packet_content)
{
    char recovery = 0;
    recovery = *((char *)packet_content + packet_header->caplen);
    *((char *)packet_content + packet_header->caplen) = 0;
    char data[L_LEN + 1]; // 每个包最大应该是 1514

    // 获取发件人信息
    memset(data, '\0', sizeof(data));
    find_msg(packet_header->caplen, packet_content, "_task=login", data);
    if (strlen(data) > 0)
    {
        puts("========= Userinfo =========");
        printf("Time: %s", ctime((const time_t *)&(packet_header->ts.tv_sec)));
        get_userinfo(data);
        puts("\n");
    }

    // 解析邮件内容
    memset(data, '\0', sizeof(data));
    find_msg(packet_header->caplen, packet_content, "_task=mail&_action=send", data);
    if (strlen(data) > 0)
    {
        puts("========= Message =========");
        printf("Time: %s", ctime((const time_t *)&(packet_header->ts.tv_sec)));
        get_message(data);
        puts("\n");
    }

    // 有时候可能会出现溢出导致函数返回点被覆盖 这句话用来还原被覆盖的东西
    *((char *)packet_content + packet_header->caplen) = recovery;
}

// 包数据匹配函数,将整个包含有所匹配数据的一部分放到data中
void find_msg(int len, const u_char *packet_content, char *pattern, char *data)
{
    char *res = NULL;
    char *point = (char *)packet_content;
    int len_point = 0;
    int len_left = len;

    do
    {
        res = strstr(point, pattern);
        if (res != NULL)
        {
            if (decode(point, data) < 0)
                strncpy(data, point, strlen(point));
            break;
        }
        len_point = strlen(point);
        len_left -= len_point;
        point = point + 1;
    } while (len_left > 0);
}

// 分析mail.hitaq.cn电子邮件包 获取发件人基本信息
void get_userinfo(char data[])
{
    char user[S_LEN];
    char pass[S_LEN];

    memset(user, '\0', sizeof(user));
    memset(pass, '\0', sizeof(pass));

    char *user_p = strstr(data, "_user") + 6;
    char *pass_p = strstr(data, "_pass") + 6;

    char *res = strchr(user_p, '&');
    strncpy(user, user_p, res - user_p);

    res = strchr(pass_p, '\0');
    strncpy(pass, pass_p, res - pass_p);
    printf("Username: %s\nPassword: %s\n", user, pass);
}

// 分析mail.hitaq.cn电子邮件包 获取发件人发送的邮件的基本信息
void get_message(char data[])
{
    char to[S_LEN] = {0};
    char msg_subject[S_LEN] = {0};
    char msg_content[L_LEN] = {0};
    char *temp = NULL;

    temp = strstr(data, "_to=") + 4;
    strncpy(to, temp, strchr(temp, '&') - temp);
    printf("Send to: %s\n", to);

    temp = strstr(data, "_subject=") + 9;
    strncpy(msg_subject, temp, strchr(temp, '&') - temp);
    printf("Subject: %s\n", msg_subject);

    temp = strstr(data, "_message=") + 9;
    strncpy(msg_content, temp, &data[strlen(data)] - temp);
    printf("Message: %s\n", msg_content);
}

1.4 代码 v3.0

  • 本来我也以为到2.0就可以完成本次实验了,直到我有次测试的时候手贱,发了一个内容比较多的邮件…
  • 我突然发现我之前的代码没有考虑TCP协议的最大负载,邮件数据包过大的时候HTTP协议会控制TCP协议进行分包。
  • 这下子要头疼了…
  • Wireshark抓包并分析后,计算机网络中学的TCP/IP协议开始能够想起来一点了。
  • 我的解决思路比较简单暴力:
    1. 检测特征值,检测到邮件发送请求且该数据包等于TCP数据包的最大负载,就判定为是进行分包了。
    2. 然后利用TCP协议头PSH字段来进行判断分包是不是结束了。
    3. 最后再把这些数据包拼接起来,得到完整的数据包。
  • 时间、能力有限,这样实现出来的代码的鲁棒性比较差,没有考虑丢包情况,希望大佬勿喷。
  • 最后还是恬这脸贴一下代码吧,仅供参考。(其中URLDecode是从Rosetta Code上宕下来的)
#include <pcap.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <math.h>
#include <netinet/tcp.h>
#include <netinet/ip.h>
#include <net/ethernet.h>
#include <netinet/in.h>
#include "urldecode.c"

//typedef __u_char u_char;
#define L_LEN 1514
#define M_LEN 512
#define S_LEN 256
#define T_LEN 128
#define BUFF_SIZE 102400
#define HTTP_PACKS 64
#define TCP_OPTION 12

// 关于这个我也比较迷惑 一般来说这个值应该是固定的才对啊
int MAX_TCP_PAYLOAD = 1460;

// 标志http是不是分包
int keep_live = 0;

// 分包保存情况
struct tcp_pack {
    uint32_t seq;
    char payload[L_LEN];
} http_packs[HTTP_PACKS];

// pcap_loop() 的回调函数,包分析与处理函数
void capture(u_char *user, const struct pcap_pkthdr *packet_header, const u_char *packet_content);

// 数据包格式化输出函数
void print_packet(int packet_id, const struct pcap_pkthdr *packet_header, const u_char *packet_content);

// http_packs 输出函数
void print_http_packs();

// 包数据匹配函数,将整个包含有所匹配数据的一部分放到data中
void find_msg(int len, const u_char *packet_content, char *pattern, char *data);

// 分析mail.hitaq.cn电子邮件包 获取发件人基本信息
void get_userinfo(char data[]);

// 分析mail.hitaq.cn电子邮件包 获取发件人发送的邮件的基本信息
void get_message(char data[]);

// 主函数 抓包框架的实现函数
int main(int argc, char **argv) {
    pcap_t *handle;                             // 会话句柄
    char *device;                               // 设备
    char errbuf[PCAP_ERRBUF_SIZE] = {0};        // 存储错误信息,其中PCAP_ERRBUF_SIZE 为宏定义的错误缓冲区大小
    struct bpf_program filter;                  // 已经编译好的过滤器
    bpf_u_int32 ip_mask;                        // 所在网络掩码
    bpf_u_int32 ip_addr;                        // 主机的IP地址
    char filter_rules[] = "dst host mail.hitaq.cn"; // 过滤规则
    int res = 0;                                // 暂存各种函数的返回值,用来判断成功与否

    // 查找网络设备,得到可用的网络设备名指针
    device = pcap_lookupdev(errbuf); // 返回第一个合法的设备 我这里是 wlp3s0
    if (device == NULL) {
        fprintf(stderr, "%s\n", errbuf);
        exit(1);
    }

    // 获取指定网卡的 ip 地址,子网掩码
    res = pcap_lookupnet(device, &ip_addr, &ip_mask, errbuf);
    if (res == -1) {
        printf("Error@pcap_lookupnet(): %s\n", errbuf);
        exit(1);
    }

    // 以混杂模式打开一个用于捕获数据的网络接口
    handle = pcap_open_live(device, BUFF_SIZE, 1, 0, errbuf);
    if (handle == NULL) {
        printf("Error@pcap_open_live(): %s\n", errbuf);
        exit(1);
    }

    // 将用户制定的过滤策略编译到过滤程序中
    if (pcap_compile(handle, &filter, filter_rules, 0, ip_addr) == -1) {
        fprintf(stderr, "Error calling pcap_compile\n");
        exit(1);
    }

    // 设置过滤器
    if (pcap_setfilter(handle, &filter) == -1) {
        fprintf(stderr, "Error setting filter\n");
        exit(1);
    }

    puts("Capturing...\n\n");
    // 捕包
    pcap_loop(handle, -1, capture, NULL);

    // 关闭网络设备,释放资源
    pcap_close(handle);

    return 0;
}

void capture(u_char *user, const struct pcap_pkthdr *packet_header, const u_char *packet_content) {
    /**
     * struct pcap_pkthdr
     * {
     *     struct timeval ts;  // 抓到包的时间
     *     bpf_u_int32 caplen; // 表示抓到的数据长度,抓取时长度
     *     bpf_u_int32 len;    // 表示数据包的实际长度,本来应有长度
     * }
    */

    // static unsigned int count = 0;
    // count++; // 记录接收到的包的顺序
    // print_packet(count, packet_header, packet_content);  // 打印整个包的信息

    // 小于64byte的数据包为坏包 直接丢弃
    if (packet_header->caplen <= 64) return;

    char recovery = 0;
    recovery = *((char *) packet_content + packet_header->caplen);
    *((char *) packet_content + packet_header->caplen) = 0;
    char data[L_LEN + 1]; // 每个包最大应该是 1514

    // struct iphdr *ip = (struct iphdr*)(packet_content + ETHER_HDR_LEN);
    struct tcphdr *tcp = (struct tcphdr *) (packet_content + ETHER_HDR_LEN + sizeof(struct iphdr));
    u_char *tcp_payload = (u_char *) (packet_content + ETHER_HDR_LEN + sizeof(struct iphdr) + sizeof(struct tcphdr) +
                                      TCP_OPTION);

    uint32_t tcp_seq = htonl(tcp->th_seq);
    uint32_t tcp_len = strlen((char *) tcp_payload);

    // 没有分包发message的情况下
    if (keep_live == 1) {
        int index = (int) ceil((tcp_seq - http_packs[0].seq) / (double) MAX_TCP_PAYLOAD);
        http_packs[index].seq = tcp_seq;
        strncpy(http_packs[index].payload, (const char *) tcp_payload, tcp_len);
        if (tcp->psh == 1) {
            // 分包 End
            keep_live = 0;
            // 输出拼接好的整个http数据包
            print_http_packs();
        }
    } else {
        // 获取发件人信息
        memset(data, '\0', sizeof(data));
        find_msg(packet_header->caplen, tcp_payload, "_task=login", data);
        if (strlen(data) > 0) {
            puts("========= Userinfo =========");
            printf("Time: %s", ctime((const time_t *) &(packet_header->ts.tv_sec)));
            get_userinfo(data);
            puts("\n");
        }

        // 解析邮件内容
        memset(data, '\0', sizeof(data));
        find_msg(packet_header->caplen, tcp_payload, "_task=mail&_action=send", data);
        if (strlen(data) > 0) {
            puts("========= Message =========");
            printf("Time: %s", ctime((const time_t *) &(packet_header->ts.tv_sec)));
            if (tcp_len >= MAX_TCP_PAYLOAD) {
                // 分包 Start
                keep_live = 1;
                http_packs[0].seq = tcp_seq;
                strncpy(http_packs[0].payload, (const char *) tcp_payload, tcp_len);
                MAX_TCP_PAYLOAD = tcp_len;
            } else {
                get_message(data);
                puts("\n\n");
            }
        }
    }

    // 有时候可能会出现溢出导致函数返回点被覆盖 这句话用来还原被覆盖的东西
    *((char *) packet_content + packet_header->caplen) = recovery;
}

void find_msg(int len, const u_char *packet_content, char *pattern, char *data) {
    char *res = NULL;
    char *point = (char *) packet_content;
    int len_point = 0;
    int len_left = len;

    do {
        res = strstr(point, pattern);
        if (res != NULL) {
            if (decode(point, data) < 0)
                strncpy(data, point, strlen(point));
            break;
        }
        len_point = strlen(point);
        len_left -= len_point;
        point = point + 1;
    } while (len_left > 0);
}

void print_packet(int packet_id, const struct pcap_pkthdr *packet_header, const u_char *packet_content) {
    printf("\n-------------- Start --------------\n");
    printf("Packet ID       : %d\n", packet_id);
    printf("Packet length   : %d\n", packet_header->len);
    printf("Number of bytes : %d\n", packet_header->caplen);
    printf("Received time   : %s\n", ctime((const time_t *) &(packet_header->ts.tv_sec)));

    for (int i = 0; i < packet_header->caplen; i++) {
        printf(" %02x", packet_content[i]);
        if ((i + 1) % 32 == 0) printf("\n");
        // printf("%c", packet_content[i]);
    }

    for (int i = 0; i < packet_header->caplen; i++) {
        printf("%c", packet_content[i]);
    }
    printf("\n--------------- End ---------------\n");
}

void print_http_packs() {
    uint32_t len = strlen(http_packs[0].payload);
    uint32_t next_seq = http_packs[0].seq + len;
    char temp[L_LEN] = {0};
    decode(http_packs[0].payload, temp);
    get_message(temp);
    for (int i = 1; i < HTTP_PACKS; i++) {
        memset(temp, '\0', sizeof(temp));
        decode(http_packs[i].payload, temp);
        if (next_seq == http_packs[i].seq) {
            printf("%s", temp);
            len = strlen(http_packs[i].payload);
            next_seq = http_packs[i].seq + len;
        } else if (len < MAX_TCP_PAYLOAD) {
            printf("%s\n\n", temp);
            break;
        } else {
            break;
        }
    }
}

void get_userinfo(char data[]) {
    char user[S_LEN];
    char pass[S_LEN];

    memset(user, '\0', sizeof(user));
    memset(pass, '\0', sizeof(pass));

    char *user_p = strstr(data, "_user") + 6;
    char *pass_p = strstr(data, "_pass") + 6;

    char *res = strchr(user_p, '&');
    strncpy(user, user_p, res - user_p);

    res = strchr(pass_p, '\0');
    strncpy(pass, pass_p, res - pass_p);
    printf("Username: %s\nPassword: %s\n", user, pass);
}

void get_message(char data[]) {
    char to[S_LEN] = {0};
    char msg_subject[S_LEN] = {0};
    char msg_content[L_LEN] = {0};
    char *temp = NULL;

    temp = strstr(data, "_to=") + 4;
    strncpy(to, temp, strchr(temp, '&') - temp);
    printf("Send to: %s\n", to);

    temp = strstr(data, "_subject=") + 9;
    strncpy(msg_subject, temp, strchr(temp, '&') - temp);
    printf("Subject: %s\n", msg_subject);

    temp = strstr(data, "_message=") + 9;
    strncpy(msg_content, temp, &data[strlen(data)] - temp);
    printf("Message: \n%s", msg_content);
}


总结

体会

  • 感觉自己太菜了
  • HTTP协议需要好好学习一番
  • TCP协议需要好好学习一番