一、基础知识
协议简介
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;
}