3. OpCode:Prefixes - Part I

本文通过实例详细解析了x86指令集中的前缀指令功能及其使用方法,包括如何改变默认操作数大小和重复执行指令等特性。
 3. Prefixes - Part I

转自老罗

To be, or not to be: that is the question.

-- William Shakespeare, "HAMLET".

Hello, Prefixes!

就像经典的“Hello World!”程序一样,让我们也从最简单的一个实例看起:

OpCode && mnemonic
OpCodemnemonic
40INC EAX
66 40INC AX

假设默认的操作数是32位,我们就可以得到上表的结果。(为什么默认是32位?看到后面就会明白的)

我们可以看到,40表示的是INC EAX66 40表示的是INC AX,两者的分别在于:前者的操作数是32位的(EAX),而后者是16位的(AX)。

从OpCode的角度来看,后者比前者多了一个66,就导致了不同的结果,唔……Intel x86规定:

66是一个Prefix,我们把Prefix翻译为前缀,所谓前缀,就是与code进行组合用以产生出某些变化形式的“东西”。唔……好拗口啊,真不好解释,请看晕了的朋友不要抛砖头,继续往下阅读吧。

认识 Prefixes

回忆一下第一章中介绍的OpCode的6个域:

  1. Prefixes
  2. code
  3. ModR/M
  4. SIB
  5. Displacement
  6. Immediate

记住:

  • 在实际的使用中,并不是这所有的6个域都会被用到的,但是有一项却是一定会有的,那就是第2项:code,有些指令甚至只会用到code这一项。
  • 这6个域的排列顺序绝对不能乱,必须严格按照上面的顺序进行。有些域也许不会出现,但是只要出现了,编号小的域就绝对不允许出现在编号大的域的后面,反之亦然。

Prefixes是所有的域中最容易理解的一个,请先明了它的一些特性:

Prefixes的几个特性

1. 它是唯一的一个可能出现在code之前的域。
2. 所有的Prefixes都只有1个字节。
3. 在一个OpCode中可能会有多个Prefixes

看看刚才提到过的prefix 66,这个prefix的意思是“切换默认的操作数的大小”。例如在有的系统中有2种默认的操作数大小:16位和32位。操作数有可能会被写成16位或者32位,唯一的区分方法是看它有没有prefix 66

唔……是不是讲得不够清楚呢?我们来看看:

OpCode && mnemonic
OpCodemnemonic
66 ADLODSW
ADLODSD

依然假设默认的操作数是32位的,有没有发现什么不寻常的地方?

LODSWLODSDcode域是一样的——都是AD!其实,LODSWLODSD这两个指令是同一个指令,只不过它们的操作数大小不一样——LODSW使用了2个字节(16位)的WORD作为操作数,而LODSD则使用了4个字节(32位)的DWORD作为操作数。

看到这里,读者应该能够明白了:prefix 66的作用是切换默认的操作数大小。请注意我们并没有说“指定”,而是“切换”!反映到这个例子中,就是“切换默认的32位操作数到16位”,而不是“指定操作数的大小为16位”。

这点非常重要!!!绝对不是在玩文字游戏!!

如果默认的操作数大小是WORD16位),那么切换后就是DWORD32位);反之,如果默认的操作数大小是DWORD32位),那么切换后就是WORD16位)。

切记!Prefixes 66就像一个触发器一样,起的作用就是进行切换。

让我们再来看一个特例:

B0 FF    MOV AL, 0FF
8A C1    MOV AL, CL

看清楚了吗?现在的操作数是ALCL,加上prefix 66后会如何?

66 B0 FF    MOV AL, 0FF
66 8A C1    MOV AL, CL

Faint!没有任何变化!

为什么呢?我们可以猜测一下:也许并不是所有情况下的操作数大小都可以随意改变的。假如这个改变是不允许的,那么它就会被忽略。

为了证实这个猜想,让我们来看看下一个更有趣的例子:

prefix F3(rep)的作用是让CPU对随后的指令循环执行ecx(cx)次,指令INC EAX的OpCode是40,好,如果我们想连续执行3次INC EAX的话,应该怎么样呢?

也许你会想当然地认为应该这样写:

xor eax, eax
mov ecx, 3
rep inc eax

实际上!并不是这样!这样的程序的运行结果是:

  1. 没有任何异常(exception)产生。
  2. 最后eax = 1,这意味着prefix F3并没有起作用——它被忽略了。

现在我们可以证实之前的想法:

如果Prefixes不能对随它之后的OpCode起作用,那么它就会被忽略。

再回忆一下之前提到的三个特性:

  1. Prefixes是唯一的一个可能出现在code之前的域。
  2. 所有的Prefixes都只有1个字节。
  3. 在一个OpCode中可能会有多个Prefixes

前面两点应该比较容易理解,让我们来看看第3点是什么意思。

如果想得到下面的指令:

REP LODSW

它的OpCode将会是:

66 F3 AD

解释如下:

66 AD:LODSW
F3: REP

都是前面讲过的内容,不难吧?只是组合起来使用罢了。

不过……细心的读者可能会问:为什么要把66放在第一位,把F3放在第二位呢?把它们的位置调换一下行不行?答案是:行!事实上它也可以写成:

F3 66 AD

效果是一样的!

Prefixes的特性

一个OpCode中可以有多个Prefixes。
如果有多个Prefixes,那么它们的顺序可以打乱,不会有任何问题。

最后,我们还可以得出一个推论:

由于每个Prefixes会多占用1个字节,所以也必定会导致处理器多使用一个指令周期进行解码——无论在时间还是空间上都会造成浪费。因此,我们应该权衡在哪些场合才使用Prefixes,如非必要,应该减少对它的使用。

Is it ALL?

Of course not! 由于章节的篇幅问题,Prefixes的进一步讲解会放在后面的章节中继续进行,我们会看到更多的有关Prefixes的信息。



(注:如果出现链接打不开的情况,请去掉IE浏览器的“工具->Internet选项->高级->总是以UTF-8发送URL”前面的勾。谢谢!)
 
<think>我们注意到程序输出了很多Unknown TFTP Opcode: 0,并且源IP和目的IP是多播地址(224.0.0.251)和端口5353(mDNS端口)。这是因为我们的程序捕获了所有UDP数据包,并试图将它们解析为TFTP数据包。但是,非TFTP的UDP数据包也会被处理,导致解析错误。 问题分析: 1. 当前程序在捕获UDP包后,只通过端口过滤(69端口或双方端口都大于1024)来判断是否为TFTP包。但是,其他协议(如mDNS)也可能使用高端口,因此会被错误地解析。 2. TFTP协议的数据包前两个字节是操作码(opcode),正常为1~5。如果收到的UDP包不是TFTP,那么前两个字节可能为0或其他值,导致解析为未知操作码。 解决方案: 我们需要更精确地识别TFTP数据包。TFTP协议的特点: - 服务端端口为69(初始请求),但后续数据传输使用临时端口(高端口)。 - TFTP数据包的最小长度为4字节(ACK和ERROR包为4字节,其他包更大)。 - 操作码只能是1~5。 因此,我们可以: a) 在解析前增加操作码检查:如果操作码不在1~5范围内,则跳过解析。 b) 加强端口过滤:只处理满足以下条件之一的UDP包: - 目标端口是69(初始请求) - 源端口是69(来自服务器的响应) - 或者,源端口和目标端口都是高端口(>1024)且数据包符合TFTP格式(操作码在1~5) 但是,仅靠操作码检查可能会误判其他协议的数据包(如果前两个字节恰好是1~5)。因此,我们结合端口和操作码进行判断。 修改建议: 步骤1:在parse_tftp_packet函数开头,增加操作码检查,如果操作码不在1~5,则打印一条消息并返回(或者不打印,直接忽略)。 步骤2:在调用parse_tftp_packet之前,除了端口过滤,再增加一个操作码的初步检查(可选,但为了效率可以在解析前检查)。 然而,我们注意到在main函数中已经进行了端口过滤,但过滤条件过于宽泛。我们可以调整端口过滤条件,只保留: - 目标端口是69 或者 源端口是69 或者 (目标端口和源端口都大于1024 且 数据包长度>=4 且 前两个字节表示的操作码在1~5) 但是,在main函数中我们还没有解析TFTP数据包,所以无法检查操作码。因此,我们可以在main函数中先进行一个简单的操作码检查(只需要读取前两个字节并判断是否在1~5)。 具体修改如下: 修改main函数中调用解析器的条件: 原条件: if (!(dst_port == 69 || src_port == 69 || (dst_port > 1024 && src_port > 1024))) continue; 修改为: // 首先,端口条件:必须满足以下之一 // 1. 目标端口是69(客户端发往服务器的请求) // 2. 源端口是69(服务器发往客户端的响应) // 3. 双方都是高端口(可能是TFTP数据传输) if (dst_port == 69 || src_port == 69 || (dst_port > 1024 && src_port > 1024)) { // 然后,检查数据包长度是否至少4字节(操作码2字节+至少2字节内容) if (tftp_len < 4) { // 太短,不是有效的TFTP包 continue; } // 检查操作码是否在1~5 uint16_t opcode = ntohs(*(uint16_t *)tftp_data); if (opcode >= TFTP_RRQ && opcode <= TFTP_ERROR) { parse_tftp_packet(tftp_data, tftp_len, src_ip, dst_ip, src_port, dst_port); } else { // 可以打印一条调试信息,或者忽略 // 注意:如果不想看到其他协议的数据包,就注释掉打印 // print_timestamp(); // printf("%s:%d -> %s:%d | Not a TFTP packet (opcode=%d)\n", src_ip, src_port, dst_ip, dst_port, opcode); } } 这样,只有端口条件满足且操作码在1~5之间的包才会被解析。 但是,注意:在初始请求(端口69)中,数据包一定是TFTP包,所以我们可以不对端口69的包进行操作码检查?但为了统一,我们还是检查,因为非TFTP包不会发到69端口,所以理论上不会出现。但是,如果网络中有错误包,我们可以通过操作码检查过滤掉。 另外,我们还可以在parse_tftp_packet函数中保留操作码检查,这样双重保险。 修改parse_tftp_packet函数,在开头增加: if (len < 4) return; uint16_t opcode = ntohs(*(uint16_t *)data); if (opcode < TFTP_RRQ || opcode > TFTP_ERROR) { // 不是有效的TFTP操作码 return; } 这样,即使main函数中漏掉,这里也不会解析。 但是,我们已经在main函数中进行了操作码检查,所以parse_tftp_packet函数中可以不重复检查,但保留长度检查。 考虑到代码清晰,我们可以在parse_tftp_packet函数中不检查操作码范围(因为调用者已经确保操作码在1~5),但保留长度检查。 因此,我们修改main函数中的过滤逻辑,增加操作码检查,只将操作码在1~5之间的包交给解析函数。 修改后的main函数循环部分: while (1) { // ... [接收数据包,解析以太网、IP、UDP头] ... // 转换IP地址为字符串 inet_ntop(AF_INET, &(ip->saddr), src_ip, INET_ADDRSTRLEN); inet_ntop(AF_INET, &(ip->daddr), dst_ip, INET_ADDRSTRLEN); uint16_t src_port = ntohs(udp->source); uint16_t dst_port = ntohs(udp->dest); // 获取TFTP数据负载 (跳过UDP头) unsigned char *tftp_data = (unsigned char *)udp + sizeof(struct udphdr); int tftp_len = udp_len - sizeof(struct udphdr); // 改进的过滤条件:端口条件满足,且数据包长度至少4字节,且操作码在1~5 int is_candidate = 0; if (dst_port == 69 || src_port == 69) { is_candidate = 1; } else if (dst_port > 1024 && src_port > 1024) { is_candidate = 1; } if (is_candidate) { // 检查长度至少4字节 if (tftp_len < 4) { continue; } uint16_t opcode = ntohs(*(uint16_t*)tftp_data); if (opcode >= TFTP_RRQ && opcode <= TFTP_ERROR) { parse_tftp_packet(tftp_data, tftp_len, src_ip, dst_ip, src_port, dst_port); } // 否则,忽略这个包(不是TFTP) } } 这样修改后,非TFTP协议的数据包(如mDNS)就不会被解析,也不会打印未知操作码。 注意:我们仍然打印了端口69和高端口且操作码在1~5的包。如果网络中确实有非TFTP的数据包但是操作码恰好是1~5,那么会被错误解析,但这种概率极低。 另外,我们也可以考虑在parse_tftp_packet函数中对每个case的default分支进行修改,打印未知操作码(虽然理论上不会发生,因为已经过滤)。保留default分支可以防止程序出错。 修改后的parse_tftp_packet函数中default分支可以这样: default: // 由于我们已经在main中过滤了操作码,所以这里不应该出现其他值 // 但为了健壮性,保留 printf("Unknown TFTP Opcode: %d (this should not happen)", opcode); 但是,如果出现,说明我们的过滤有问题,所以打印出来有助于调试。 最后,我们还要注意网络字节序和主机字节序。在读取操作码时,我们使用了ntohs,这是正确的。 修改后的代码应该可以避免将mDNS等协议的数据包当作TFTP包解析。 让我们重新编译并运行程序,应该不会再出现mDNS的包被解析为TFTP。 如果还有问题,我们可以进一步调整。 注意:如果TFTP数据包分片,我们的程序可能无法正确处理(因为原始套接字只处理了链路层、IP层和UDP层,但分片重组需要由内核处理,而原始套接字不会自动重组)。不过TFTP通常使用小数据包(每个数据块512字节),所以一般不会分片。 现在,我们修改代码,然后测试。 由于代码较长,这里只给出修改的关键部分。 完整修改后的main函数循环部分: while (1) { // 接收数据包 ssize_t data_size = recvfrom(sockfd, buffer, sizeof(buffer), 0, NULL, NULL); if (data_size < 0) { perror("Packet receive error"); continue; } // 解析以太网帧头 struct ethhdr *eth = (struct ethhdr *)buffer; // 只处理IPv4包 (0x0800) if (ntohs(eth->h_proto) != ETH_P_IP) continue; // 解析IP头 struct iphdr *ip = (struct iphdr *)(buffer + sizeof(struct ethhdr)); unsigned short iphdr_len = ip->ihl * 4; // 只处理UDP包 (协议号17) if (ip->protocol != IPPROTO_UDP) continue; // 解析UDP头 struct udphdr *udp = (struct udphdr *)(buffer + sizeof(struct ethhdr) + iphdr_len); unsigned short udp_len = ntohs(udp->len); // 转换IP地址为字符串 inet_ntop(AF_INET, &(ip->saddr), src_ip, INET_ADDRSTRLEN); inet_ntop(AF_INET, &(ip->daddr), dst_ip, INET_ADDRSTRLEN); uint16_t src_port = ntohs(udp->source); uint16_t dst_port = ntohs(udp->dest); // 获取TFTP数据负载 (跳过UDP头) unsigned char *tftp_data = (unsigned char *)udp + sizeof(struct udphdr); int tftp_len = udp_len - sizeof(struct udphdr); // 改进的过滤条件:端口条件满足,且数据包长度至少4字节,且操作码在1~5 int is_candidate = 0; if (dst_port == 69 || src_port == 69) { is_candidate = 1; } else if (dst_port > 1024 && src_port > 1024) { is_candidate = 1; } if (is_candidate) { // 检查长度至少4字节 if (tftp_len < 4) { continue; } uint16_t opcode = ntohs(*(uint16_t*)tftp_data); if (opcode >= TFTP_RRQ && opcode <= TFTP_ERROR) { parse_tftp_packet(tftp_data, tftp_len, src_ip, dst_ip, src_port, dst_port); } // 否则,忽略这个包(不是TFTP) } } 重新编译运行,应该不会再出现Unknown TFTP Opcode: 0的输出了。 如果还有问题,请提供进一步的错误输出。 注意:在测试时,确保使用NetAssist发送TFTP请求,以验证程序仍然能正确捕获TFTP包。 相关问题: §§相关问题§§: 1. 如何在C语言中实现更精确的网络协议过滤(如基于数据包内容)? 2. 为什么在原始套接字处理中需要特别注意字节序转换? 3. 如何处理分片的IP数据包以实现完整协议解析? 4. 如何优化原始套接字的性能以减少CPU占用?</think>### 问题分析:TFTP 解析器误报非TFTP流量 您的程序输出了大量 `Unknown TFTP Opcode: 0` 的警告,这些是 mDNS(多播DNS)流量(端口5353),不是TFTP流量。问题在于过滤逻辑不够精确,导致程序尝试将非TFTP数据包当作TFTP解析。 ### 解决方案:增强过滤逻辑 以下是关键修改点: #### 1. **改进端口过滤逻辑** ```c // 修改main函数中的端口过滤部分 // 原始过滤条件(过于宽泛): // if (!(dst_port == 69 || src_port == 69 || (dst_port > 1024 && src_port > 1024))) // 改进后的精确过滤: if (dst_port == 69 || src_port == 69) { // 标准TFTP端口流量 } else if ((dst_port > 1024 && src_port > 1024) && (dst_port >= 1024 && dst_port <= 65535) && (src_port >= 1024 && src_port <= 65535)) { // 客户端和服务器都在高端口的TFTP数据流量 } else { continue; // 跳过非TFTP流量 } ``` #### 2. **增加操作码预检查** 在调用解析函数前增加操作码验证: ```c // 在调用parse_tftp_packet前添加检查 uint16_t opcode = ntohs(*(uint16_t *)tftp_data); if (opcode < TFTP_RRQ || opcode > TFTP_ERROR) { // 非TFTP操作码,跳过解析 continue; } parse_tftp_packet(tftp_data, tftp_len, src_ip, dst_ip, src_port, dst_port); ``` #### 3. **添加协议识别标志** ```c // 在parse_tftp_packet开头添加验证 void parse_tftp_packet(...) { if (len < 2) return; // 至少需要2字节的操作码 uint16_t opcode = ntohs(*(uint16_t *)data); if (opcode < 1 || opcode > 5) { // TFTP操作码范围是1-5 print_timestamp(); printf("%s:%d -> %s:%d | Non-TFTP UDP packet (Opcode: %d)\n", src_ip, src_port, dst_ip, dst_port, opcode); return; } // ...原有解析逻辑... } ``` #### 4. **完整修复版代码** ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <net/ethernet.h> #include <netinet/ip.h> #include <netinet/udp.h> #include <netinet/in.h> #include <sys/socket.h> #include <sys/ioctl.h> #include <net/if.h> #include <time.h> #include <netpacket/packet.h> #include <linux/if_ether.h> #define TFTP_RRQ 1 #define TFTP_WRQ 2 #define TFTP_DATA 3 #define TFTP_ACK 4 #define TFTP_ERROR 5 const char *tftp_errors[] = { "未定义错误", "文件未找到", "访问违规", "磁盘满或分配超过", "非法操作", "未知传输ID", "文件已存在", "无此用户" }; void print_timestamp() { time_t now = time(NULL); struct tm *t = localtime(&now); printf("[%02d:%02d:%02d] ", t->tm_hour, t->tm_min, t->tm_sec); } void parse_tftp_packet(const unsigned char *data, int len, const char *src_ip, const char *dst_ip, uint16_t src_port, uint16_t dst_port) { if (len < 2) return; uint16_t opcode = ntohs(*(uint16_t *)data); // 验证操作码有效性 if (opcode < TFTP_RRQ || opcode > TFTP_ERROR) { print_timestamp(); printf("%s:%d -> %s:%d | Non-TFTP UDP packet (Opcode: %d)\n", src_ip, src_port, dst_ip, dst_port, opcode); return; } print_timestamp(); printf("%s:%d -> %s:%d | ", src_ip, src_port, dst_ip, dst_port); switch (opcode) { // ... 原有case保持不变 ... } } int main(int argc, char *argv[]) { // ... 初始化代码保持不变 ... while (1) { // ... 数据包接收和解析保持不变 ... // 改进的端口过滤 if (dst_port == 69 || src_port == 69) { // 标准TFTP端口 } else if ((dst_port > 1024 && src_port > 1024) && (dst_port >= 1024 && dst_port <= 65535) && (src_port >= 1024 && src_port <= 65535)) { // 高端口TFTP数据 } else { continue; } // 操作码预检查 if (tftp_len < 2) continue; uint16_t opcode = ntohs(*(uint16_t *)tftp_data); if (opcode < TFTP_RRQ || opcode > TFTP_ERROR) continue; parse_tftp_packet(tftp_data, tftp_len, src_ip, dst_ip, src_port, dst_port); } close(sockfd); return 0; } ``` ### 问题原因详解 1. **端口过滤不足**: - 原代码 `(dst_port > 1024 && src_port > 1024)` 条件过于宽泛 - mDNS使用5353端口(>1024),被误判为TFTP流量 2. **缺少协议验证**: - 未验证数据包是否包含有效的TFTP操作码 - mDNS数据包前两个字节为0,被错误解析为操作码0 3. **未处理非TFTP流量**: - 程序未区分真正的TFTP流量和其他高端口UDP流量 - 所有满足端口条件的UDP包都被尝试解析 ### 测试建议 1. **使用tcpdump验证**: ```bash sudo tcpdump -i ens33 udp port 69 -vv -X ``` 2. **模拟TFTP流量测试**: ```bash # 在另一终端运行 echo "test" > test.txt tftp localhost tftp> put test.txt ``` 3. **过滤特定流量**(可选): ```c // 添加IP过滤(示例) if (strcmp(src_ip, "192.168.130.1") != 0 && strcmp(dst_ip, "192.168.130.1") != 0) { continue; } ``` ### 优化方向 1. **添加白名单功能**: ```c char *allowed_ips[] = {"192.168.130.1", "192.168.130.2"}; int allowed = 0; for (int i = 0; i < sizeof(allowed_ips)/sizeof(char*); i++) { if (strcmp(src_ip, allowed_ips[i]) == 0 || strcmp(dst_ip, allowed_ips[i]) == 0) { allowed = 1; break; } } if (!allowed) continue; ``` 2. **添加协议统计**: ```c static int total_packets = 0; static int tftp_packets = 0; static int non_tftp_packets = 0; total_packets++; if (is_tftp) tftp_packets++; else non_tftp_packets++; if (total_packets % 50 == 0) { printf("[Stats] TFTP: %d, Non-TFTP: %d, Total: %d\n", tftp_packets, non_tftp_packets, total_packets); } ``` 3. **添加详细日志模式**: ```c int verbose = 0; // 通过命令行参数设置 if (verbose && opcode == 0) { printf("Full Hex Dump:\n"); for (int i = 0; i < len; i++) { if (i % 16 == 0) printf("\n%04X: ", i); printf("%02X ", data[i]); } printf("\n"); } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值