SMTP(简单邮件传输协议)协议学习(Wireshark分析&C语言解析)

一、基础知识

协议简介

SMTP (Simple Mail Transfer Protocol)是一种用于电子邮件传输的协议,它定义了如何在网络上传输和路由邮件。SMTP协议通常与POP3或IMAP协议一起使用,以实现电子邮件的发送和接收.它的底层原理主要是通过TCP建立可靠的连接,使用命令和响应模式来传输邮件。SMTP服务器通常监听TCP端口25,SMTP客户端使用这个端口与服务器建立连接,然后进行邮件传输。

命令和错误码

SMTP协议定义了一系列的命令和错误码,用于在电子邮件传输过程中进行通信和错误处理,状态码在200-300之间都是正确的,只是代表的涵义不一样下面是一些常见的SMTP命令和状态码:

SMTP命令

命令

作用

HELO/EHLO

发送方向服务器发送问候命令,以建立SMTP连接

MAIL FROM

指定发件人地址

RCPT TO

指定收件人地址

DATA

指示邮件正文的开始

QUIT

关闭与服务器的SMTP连接

状态码

状态码

涵义

211

系统状态或系统帮助响应

214

帮助信息

220

服务就绪

221

服务关闭传输通道

250

请求的操作完成

251

用户非本地,将转发到<forward-path>

354

开始邮件输入

421

服务未就绪,关闭传输信道。

450

请求的操作未完成,可能会再次尝试

451

请求的操作中止:错误处理中发生局部故障

452

请求的操作未执行,系统存储空间不足

500

语法错误,命令无法识别

501

参数语法错误

502

命令不可执行

503

错误的命令序列

504

命令参数不可识别

550

请求的操作未完成,信箱不可用(例如,不存在,无法访问)

551

用户非本地,请尝试<forward-path>

552

请求操作超出存储分配

553

请求的操作未完成,邮箱名不可用(例如,格式错误)

通信流程

1.建立连接:客户端使用TCP连接到SMTP服务器的25号端口。在连接成功后,客户端向服务器发送HELO命令,以表示自己的身份和意图。

2.发送发件人信息:客户端使用MAIL FROM命令指定发件人的电子邮件地址。如果发件人地址无效,则服务器会返回错误码并中止传输。

3.发送收件人信息:客户端使用RCPT TO命令指定一个或多个收件人的电子邮件地址。如果收件人地址无效,则服务器会返回错误码并中止传输。

4.发送邮件内容:客户端使用DATA命令指定邮件文本内容,以及包含的附件等信息。数据通常以点号(.)结束。

5.退出连接:当所有邮件内容都已发送后,客户端使用QUIT命令结束SMTP会话并断开连接。

协议优点

1.可靠性高:使用TCP建立可靠的连接,保证数据传输的可靠性。

2.灵活性强:支持扩展命令,可以根据需要定制邮件传输的方式。

3.通用性强:被广泛使用于电子邮件系统中。

二、Wireshark分析

下载安装Foxmail工具,这里我是登录的qq邮箱

选择账号管理->账号->取消SSL勾选(这里一定要取消勾选,否则抓取不到SMTP的数据包)

数据包分析

Wireshark查看SMTP数据包捕获情况

追踪SMTP数据包TCP流,逐个分析

服务器返回了一系列信息,包括服务器名称、PIPELINING(表示服务器支持命令的串行执行)、邮件大小限制、STARTTLS(表示服务器支持加密连接)、认证方式等。在发送邮件之前,这个特征协商过程会发生在每次SMTP传输的开始部分。

邮件传输从SMTP数据包的第4个数据包开始,这个抓包文件的剩余的大部分均为邮件传输过程的数据包。

数据包4发送了一条AUTH XOAUTH2命令,表示要使用XOAUTH2方式进行认证。这是一种基于OAuth 2.0的安全认证方式,用于授权第三方应用访问用户的邮件账户。

接着服务器返回了"235 2.7.0 Accepted",表示认证成功。

接下来客户端发送了一条MAIL FROM命令,表示发件人地址,并包含邮件大小信息,服务器返回"250 OK",表示发件人地址验证通过。

客户端接着发送了一条RCPT TO命令,表示收件人地址,服务器同样返回"250 OK",表示收件人地址验证通过。

紧接着客户端发送了DATA命令,表示即将发送邮件内容,服务器返回"354 End data with <CR><LF>.<CR><LF>.",表示可以开始发送邮件内容,并告知结束标志为<CR><LF>.<CR><LF>。

该部分,主要是包含日期、发件人、收件人、主题、邮件优先级、消息ID等信息的邮件内容,以及以multipart/alternative格式包含的纯文本和HTML格式的邮件正文内容。

服务器返回"250 OK: queued as.",表示该封邮件已成功入队等待发送,表明整个邮件发送过程顺利完成

最后,客户端发送了QUIT命令,表示断开连接。服务器返回"221 Bye.",表示成功断开连接。

分析验证

打开我们最开始发送的邮件,查看相关信息

解码邮件正文内容

可以看到这些信息与我们刚才所分析出来的信息全部一致。

三、C语言实现

注:这里代码解析的邮件与上面Wireshark解析的邮件是不一样的,所以邮件内容不是一样的!!!

运行结果

环境配置

配置文件

一共有五个文件需要导入,这五个文件内容可以直接在网络协议分析(二)(C语言实现---ethernet、arp、ip、icmp、udp、tcp 完整代码)_c语言实现arp-CSDN博客文章里面复制。

主文件

注:里面涉及到离线文件路径的地方需要修改为自己的

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h> // 添加头文件
#include <arpa/inet.h>
#include <nids.h>
#include <iniparser.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/buffer.h>

void extract_email_info(const char *content);

int ip_printed = 0;
int content_printed = 0; // 标记是否已经输出过Content

void smtp_protocol_callback(struct tcp_stream *smtp_connection, void **arg) {
    int i;
    char ascii_string[10000];

    switch(smtp_connection->nids_state) {
        case NIDS_JUST_EST:
            if (smtp_connection->addr.dest == 25) {
                smtp_connection->client.collect++;
                smtp_connection->server.collect++;
                smtp_connection->server.collect_urg++;
                smtp_connection->client.collect_urg++;
            }
            return;
        case NIDS_CLOSE:
            return;
        case NIDS_RESET:
            return;
        case NIDS_DATA: {
            struct half_stream *hlf;
            if (smtp_connection->client.count_new) {
                hlf = &smtp_connection->client;
            } else if (smtp_connection->server.count_new) {
                hlf = &smtp_connection->server;
            } else {
                return;
            }

            memcpy(ascii_string, hlf->data, hlf->count_new);
            ascii_string[hlf->count_new] = '\0';
            extract_email_info(ascii_string);

            if (!ip_printed) {
                char src_ip[16], dest_ip[16];
                inet_ntop(AF_INET, &(smtp_connection->addr.saddr), src_ip, 16);
                inet_ntop(AF_INET, &(smtp_connection->addr.daddr), dest_ip, 16);
                printf("Source IP: %s\n", src_ip);
                printf("Dest IP: %s\n", dest_ip);
                printf("Port: %d\n", smtp_connection->addr.dest);
                ip_printed = 1;
            }
        }
        default:
            break;
    }
}

void extract_email_info(const char *content) {
    const char *tmp;
    char from[1000] = {0};
    char to[1000] = {0};

    // 提取"From:"字段

    const char *tmp1;
    tmp1 = strstr(content, "From: ");
    if (tmp1 != NULL) {
        const char *start = strchr(tmp1, '<'); // 查找 "<"
        const char *end = strchr(tmp1, '>');   // 查找 ">"
        if (start != NULL && end != NULL) {
            printf("From: %.*s\n", end - (start + 1), start + 1);
        }
    }

    // 提取"To:"字段
    tmp = strstr(content, "To: ");
    if (tmp != NULL) {
        const char *start = strchr(tmp, '<'); // 查找 "<"
        const char *end = strchr(tmp, '>');   // 查找 ">"
        if (start != NULL && end != NULL) {
            size_t length = end - (start + 1);
            char address[length + 1]; // 新建一个足够容纳邮件地址的字符数组
            strncpy(address, start + 1, length); // 拷贝邮件地址到新的字符数组中
            address[length] = '\0'; // 添加结尾的空字符
            printf("To: %s\n", address);
        }
    }

    tmp = strstr(content, "Subject:");
    if (tmp != NULL) {
        char subject[1000] = {0};
        sscanf(tmp + 8, "%[^\r\n]", subject);
        printf("Subject: %s\n", subject);
    }


    int is_content = 0;
    int content_printed = 0;
    const char *content_type = strstr(content, "Content-Type: ");
    if (content_type != NULL) {
        if (strstr(content_type, "text/plain") || strstr(content_type, "text/html")) {
            is_content = 1;
        }
    }

    tmp = strstr(content, "\r\n\r\n");
    while (tmp != NULL) {
        if (!content_printed && is_content) {
            printf("Content: ");
            content_printed = 1;
        }

        if (is_content) {
            // Base64解密
            BIO *bio, *b64;
            BUF_MEM *bufferPtr;
            b64 = BIO_new(BIO_f_base64());
            bio = BIO_new_mem_buf((void *)tmp, -1);
            bio = BIO_push(b64, bio);

            // 解密并输出
            char decoded_content[10000];
            int length = BIO_read(bio, decoded_content, sizeof(decoded_content));
            if (length > 0) {
                decoded_content[length] = '\0'; // 添加结尾的空字符
                printf("%s\n", decoded_content);
                break;  // 解密一次后退出循环
            }

            BIO_free_all(bio);
        }

        tmp = strstr(tmp + 4, "\r\n\r\n"); // 继续搜索下一个附件或正文内容的起始位置
    }

    // 提取附件标题和内容
    tmp = strstr(content, "\r\n\r\n");
    if (tmp != NULL) {
        tmp = strstr(tmp + 4, "Content-Disposition: attachment");
        if (tmp != NULL) {
            const char *filename_start = strstr(tmp, "filename=\"");
            const char *filename_end = strstr(filename_start + 10, "\"");
            if (filename_start != NULL && filename_end != NULL) {
                //printf("Attachment Title: %.*s\n", (int)(filename_end - (filename_start + 10)), filename_start + 10);
                const char *attachment_content_start = strstr(tmp, "\r\n\r\n") + 4;
                const char *attachment_content_end = strstr(attachment_content_start, "\r\n--");
                if (attachment_content_start != NULL && attachment_content_end != NULL) {
                    //printf("Attachment Content: ");

                    // Base64解密
                    BIO *bio2, *b64_2;
                    BUF_MEM *bufferPtr2;
                    b64_2 = BIO_new(BIO_f_base64());
                    bio2 = BIO_new_mem_buf((void *)attachment_content_start, -1);
                    bio2 = BIO_push(b64_2, bio2);

                    // 解密并输出
                    char decoded_attachment[20000]; // 增加缓冲区大小
                    memset(decoded_attachment, 0, sizeof(decoded_attachment)); // 初始化为全0
                    int length2 = BIO_read(bio2, decoded_attachment, sizeof(decoded_attachment) - 1);
                    if (length2 > 0) {
                        decoded_attachment[length2] = '\0'; // 在解密后的内容末尾添加终止符
                        //printf("%s\n", decoded_attachment); // 直接输出解密后的内容

                        // 将解密后的附件内容写入文件
                        const char *attachment_title = filename_start + 10;
                        size_t attachment_length = filename_end - attachment_title;
                        char attachment_filename[attachment_length + 1];
                        strncpy(attachment_filename, attachment_title, attachment_length);
                        attachment_filename[attachment_length] = '\0';

                        char filepath[100];
                        sprintf(filepath, "/home/gxy/work/project-smtp/Debug/%s", attachment_filename);

                        FILE *fp = fopen(filepath, "w");
                        if (fp != NULL) {
                            fwrite(decoded_attachment, sizeof(char), strlen(decoded_attachment), fp);
                            fclose(fp);
                        }
                    }

                    BIO_free_all(bio2);
                }
            }
        }
    }
}

int main() {
    dictionary* ini = iniparser_load("/home/gxy/work/project-smtp/conf.ini");
    int method = iniparser_getint(ini, "Method:Method", 0);
    const char* net_interface = iniparser_getstring(ini, "Interface:Interface", "NULL");
    const char* fname = iniparser_getstring(ini, "OpenPath:Fname", "/home/gxy/work/project-smtp/Debug/smtp.pcap");

    struct nids_chksum_ctl op;
    op.mask = 0;
    op.netaddr = 0;
    op.action = NIDS_DONT_CHKSUM;
    nids_register_chksum_ctl(&op, 1);

    if (method == 1) {
        nids_params.device = net_interface;
        nids_params.filename = 0;
    } else {
        nids_params.device = 0;
        nids_params.filename = fname;
    }

    if (0 == nids_init()) {
        printf("params is error: %s\n", nids_errbuf);
        return 0;
    }

    nids_register_tcp(smtp_protocol_callback);
    nids_run();

    return 0;
}
  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
Wireshark是一款常用的网络协议分析工具,可以帮助用户捕获和解析网络数据包。而BACnet(Building Automation and Control Networks)是一种用于建筑自动化和控制系统的通信协议。 当用户使用Wireshark捕获BACnet通信流量时,Wireshark可以帮助用户在捕获数据包的同时解析BACnet协议Wireshark通过在捕获的数据包中识别BACnet协议头部,可以将BACnet通信单元(APDU)的内容可视化。用户可以通过在Wireshark中查看解析后的数据包来获得关于BACnet协议的详细信息。 解析BACnet协议时,Wireshark可以显示以下信息: 1. BACnet对象:Wireshark可以将BACnet通信单元中的对象标识符(Object ID)解析为具体的BACnet对象,例如设备对象(Device Object)、模拟值对象(Analog Value Object)等。这有助于用户了解BACnet通信中涉及的具体对象类型。 2. BACnet服务:Wireshark可以解析BACnet通信单元中的服务选择标识符(Service Choice)并将其转换为具体的BACnet服务,例如读取属性(Read Property)、写入属性(Write Property)等。这有助于用户了解BACnet通信中所执行的具体服务操作。 3. BACnet属性:Wireshark可以解析BACnet通信单元中的属性标识符(Property ID)并将其转换为具体的BACnet属性,例如设备名称(Device Name)、模拟值(Analog Value)等。这有助于用户了解BACnet通信中所涉及的具体属性。 总之,Wireshark解析BACnet协议上提供了有用的功能,使用户能够深入了解和分析BACnet通信流量,从而更好地理解和调试BACnet网络。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值