Nmap 源码深度解析:从设计原则到核心实现
本文深入剖析 Nmap 网络扫描工具的源码实现,涵盖设计模式、内存管理、网络编程、路由查询等核心技术点,帮助开发者理解高性能网络工具的设计精髓。
目录
- [设计原则:单一职责原则在 Target 类中的应用](#设计原则:单一职责原则在 Target 类中的应用)
- [C++ 内存管理:从裸指针到智能指针](#C++ 内存管理:从裸指针到智能指针)
- [网络诊断:Traceroute 原理与实现](#网络诊断:Traceroute 原理与实现)
- [跨平台编程:C/C++ 混合编程技巧](#跨平台编程:C/C++ 混合编程技巧)
- 算法实现:端口列表处理与优化
- 系统编程:终端与网络初始化
- 路由查询:跨平台路由信息获取
- [地址解析:域名到 IP 的转换机制](#地址解析:域名到 IP 的转换机制)
- 错误处理:健壮的程序设计
- [网络接口管理:Windows 平台实现](#网络接口管理:Windows 平台实现)
- 扫描优化:端口随机化与优先级
- 配置管理:排除列表加载
设计原则:单一职责原则在 Target 类中的应用
核心概念
单一职责原则(Single Responsibility Principle, SRP) :一个类或模块应该只有一个引起它变化的原因。在 Nmap 的 Target 类中,这一原则得到了完美的体现。
Target 类的职责边界
✅ Target 类负责:管理目标主机信息
Target 类专注于数据管理,其核心职责包括:
- 存储主机信息:IP 地址、MAC 地址、主机名、端口列表、OS 指纹等
- 提供访问接口:getter/setter 方法用于读写这些信息
- 维护主机状态:在线/离线状态、超时时间、扫描开始/结束时间
- 管理静态信息:地址、路由、MAC、接口等配置信息
比喻:Target 类就像一个"档案管理员",只负责建立档案、存储档案、提供档案查询服务。
❌ Target 类不负责:扫描逻辑
Target 类严格避免涉及扫描相关的业务逻辑:
- 不构造网络包:SYN 包、ACK 包的构造由网络模块负责
- 不执行发包收包:底层网络操作由网络模块处理
- 不实现扫描算法:端口扫描、OS 探测、服务识别的算法逻辑由专门的扫描引擎实现
- 不管理并发控制:多线程调度、超时控制由扫描引擎负责
设计优势
1. 代码清晰度提升
查看主机信息 → 只看 Target 类
查看扫描逻辑 → 去看扫描相关代码
职责分离使得代码结构清晰,不同模块互不干扰。
2. 维护性增强
- 修改数据结构:只需修改 Target 类
- 优化扫描算法:只需修改扫描逻辑模块
- 避免牵一发而动全身:降低修改风险
3. 复用性提高
- Target 类可被多种扫描方式共用(TCP、UDP、SYN、版本探测)
- 扫描逻辑变更不影响 Target 类的设计
- 模块化设计便于功能扩展
4. 测试便利性
- 测试 Target:只需测试数据存取逻辑
- 测试扫描:只需测试发包收包逻辑
- 解耦后单元测试更加简洁高效
反面教材:违反单一职责原则
cpp
// ❌ 错误示例:职责混乱
class Target {
// 数据管理
IP ip;
PortList ports;
// 扫描逻辑(不应该在这里)
void scanPort() {
// 发包、收包、判断逻辑...
}
void doOSFingerprint() {
// 复杂算法...
}
};
这种设计会导致:
- 数据结构修改影响扫描逻辑
- 扫描算法变更影响数据管理
- 代码臃肿、难读、难测、难维护
架构对比
遵守单一职责的架构
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Target 类 │ ←→ │ 扫描引擎 │ ←→ │ 网络发包模块 │
│ (目标主机信息) │ 数据 │ (扫描调度逻辑) │ 发包 │ (Socket/抓包) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
职责划分:
- Target 类:存储、管理主机信息
- 扫描引擎:控制扫描流程、遍历 Target、决定扫描方式
- 网络模块:底层发包、收包、解析响应
违反单一职责的架构
┌─────────────────────────────────────┐
│ Target │
│ ┌───────────┐ ┌─────────────┐ │
│ │ 主机信息 │ │ 扫描+发包逻辑 │ │
│ └───────────┘ └─────────────┘ │
└─────────────────────────────────────┘
问题:
- 改信息结构 → 影响扫描逻辑
- 改扫描算法 → 影响数据类
- 无法复用、无法单元测试、代码臃肿
实际应用示例
cpp
// ✅ 正确示例:职责分离
class Target {
private:
IP address;
std::vector<Port> ports;
HostState state;
public:
// 只提供数据访问接口
IP getAddress() const { return address; }
void setAddress(const IP& addr) { address = addr; }
std::vector<Port> getPorts() const { return ports; }
void addPort(const Port& port) { ports.push_back(port); }
HostState getState() const { return state; }
void setState(HostState s) { state = s; }
};
// 扫描逻辑由专门的类处理
class ScanEngine {
public:
void scanTarget(Target& target) {
// 扫描逻辑实现
// 构造数据包、发送、接收、解析
}
};
总结
Target 类只管"存主机信息、管主机信息",不管"怎么去扫描、怎么发包、怎么判断端口是否开放"。一个类只专心干一件事,这就是单一职责原则。
这一原则在 Nmap 源码中的体现,展示了大型项目如何通过良好的设计原则保证代码的可维护性和可扩展性。
C++ 内存管理:从裸指针到智能指针
核心概念
在 C++ 编程中,内存管理是一个核心且容易出错的领域。Nmap 源码中展示了从传统裸指针到现代智能指针的演进过程。
裸指针的使用场景
定义解析
cpp
std::vector<EarlySvcResponse *> earlySvcResponses;
逐部分解析:
| 部分 | 含义 |
|---|---|
std::vector |
C++ 标准库的动态数组容器,可自动扩容 |
<EarlySvcResponse *> |
容器中每个元素的类型:指向 EarlySvcResponse 的指针 |
earlySvcResponses |
变量名,语义为"早期服务响应"的集合 |
使用示例
cpp
// 1. 定义
std::vector<EarlySvcResponse *> earlySvcResponses;
// 2. 创建实例并添加到 vector
EarlySvcResponse *resp1 = new EarlySvcResponse();
resp1->port = 80;
resp1->service = "http";
resp1->response = "HTTP/1.1 200 OK";
earlySvcResponses.push_back(resp1);
// 3. 遍历访问
for (auto *resp : earlySvcResponses) {
std::cout << "端口 " << resp->port
<< " 的服务响应:" << resp->response << std::endl;
}
// 4. 手动释放内存(必须!)
for (auto *resp : earlySvcResponses) {
delete resp;
}
earlySvcResponses.clear();
裸指针的内存管理陷阱
问题 1:内存泄漏
cpp
// ❌ 错误示例:忘记释放
void processResponses() {
std::vector<EarlySvcResponse *> responses;
responses.push_back(new EarlySvcResponse());
// 函数结束,vector 销毁,但指针指向的内存未释放!
}
问题 2:重复释放
cpp
// ❌ 错误示例:重复释放
EarlySvcResponse *resp = new EarlySvcResponse();
std::vector<EarlySvcResponse *> responses;
responses.push_back(resp);
delete resp; // 第一次释放
// ... 后续代码
delete resp; // 第二次释放 → 崩溃!
问题 3:野指针
cpp
// ❌ 错误示例:野指针访问
EarlySvcResponse *resp = new EarlySvcResponse();
delete resp;
// ... 后续代码
resp->port = 80; // 访问已释放的内存 → 未定义行为!
智能指针的解决方案
unique_ptr:独占所有权
定义解析:
cpp
std::vector<std::unique_ptr<EarlySvcResponse>> resp_list;
核心特性:
- 独占所有权 :一个对象只能被一个
unique_ptr拥有 - 自动释放 :超出作用域时自动调用
delete - 禁止拷贝 :只能通过
std::move转移所有权
使用示例
cpp
#include <vector>
#include <memory>
#include <iostream>
struct EarlySvcResponse {
int port;
const char* service;
bool is_open;
EarlySvcResponse(int p, const char* s, bool o)
: port(p), service(s), is_open(o) {}
};
int main() {
// 定义:vector 存储 EarlySvcResponse 的独占指针
std::vector<std::unique_ptr<EarlySvcResponse>> resp_list;
// 方式 1:C++14 及以上推荐(make_unique 更安全)
resp_list.push_back(std::make_unique<EarlySvcResponse>(80, "http", true));
resp_list.push_back(std::make_unique<EarlySvcResponse>(443, "https", true));
// 方式 2:C++11 兼容
resp_list.emplace_back(new EarlySvcResponse(22, "ssh", false));
// 遍历访问(和裸指针用法几乎一样)
for (const auto& ptr : resp_list) {
std::cout << "端口 " << ptr->port
<< ":服务=" << ptr->service
<< ",状态=" << (ptr->is_open ? "开放" : "关闭")
<< std::endl;
}
// ✅ 无需手动释放!vector 销毁时自动释放所有内存
return 0;
}
输出结果:
端口 80:服务=http,状态=开放
端口 443:服务=https,状态=开放
端口 22:服务=ssh,状态=关闭
独占特性的体现
cpp
// ❌ 错误示例:禁止拷贝
std::unique_ptr<EarlySvcResponse> ptr1 =
std::make_unique<EarlySvcResponse>(80, "http", true);
// std::unique_ptr<EarlySvcResponse> ptr2 = ptr1; // 编译报错!
// ✅ 正确示例:移动所有权
std::unique_ptr<EarlySvcResponse> ptr2 = std::move(ptr1);
// 此时 ptr1 变为空,ptr2 拥有对象所有权
裸指针 vs 智能指针对比
| 特性 | 裸指针 (vector<T*>) |
独占指针 (vector<unique_ptr<T>>) |
|---|---|---|
| 内存释放 | 需手动 delete |
自动释放 |
| 内存泄漏风险 | 高 | 无 |
| 重复释放风险 | 高 | 无 |
| 拷贝语义 | 允许(易出错) | 禁止拷贝,只能移动 |
| 性能开销 | 无 | 极小(可忽略) |
| 代码复杂度 | 需要手动管理 | 自动管理 |
内存管理决策树
是否需要手动释放?
│
├─ 是 → 指针指向堆内存(new 分配)
│ │
│ ├─ 使用裸指针 + 统一释放
│ │ └─ 在析构函数中批量释放
│ │
│ └─ 使用智能指针(推荐)
│ └─ unique_ptr / shared_ptr
│
└─ 否 → 指针指向栈/全局内存
└─ 无需释放
Nmap 源码中的实践
场景 1:裸指针 + 统一释放
cpp
class Target {
private:
std::vector<EarlySvcResponse *> earlySvcResponses;
public:
~Target() {
// 析构函数中统一释放
for (auto *resp : earlySvcResponses) {
delete resp;
}
earlySvcResponses.clear();
}
};
场景 2:避免存储指针
cpp
// 直接存储对象,避免指针管理
std::vector<EarlySvcResponse> responses;
场景 3:智能指针(现代 C++)
cpp
// 使用 unique_ptr 自动管理内存
std::vector<std::unique_ptr<EarlySvcResponse>> responses;
最佳实践建议
- 优先使用智能指针 :C++11 及以上版本推荐使用
unique_ptr或shared_ptr - 避免裸指针的 vector :除非有特殊需求,否则优先使用
vector<T>或vector<unique_ptr<T>> - 统一释放策略:如果必须使用裸指针,在析构函数中统一释放
- 明确所有权:清楚每个指针的所有权归属,避免重复释放或内存泄漏
总结
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 新项目(C++11+) | unique_ptr |
自动管理,安全高效 |
| 遗留代码维护 | 裸指针 + 统一释放 | 保持一致性 |
| 性能关键场景 | 直接存储对象 | 避免指针开销 |
| 需要共享所有权 | shared_ptr |
引用计数管理 |
智能指针是现代 C++ 内存管理的标准实践,能够有效避免内存泄漏、重复释放等常见问题,提升代码的安全性和可维护性。
网络诊断:Traceroute 原理与实现
核心概念
Traceroute (Linux/macOS)/ Tracert(Windows)是网络诊断的核心工具,用于追踪数据包从源主机到目标主机所经过的每一跳路由器,并显示每一跳的延迟和 IP 地址。
工作原理
通俗比喻
想象你要给远方的朋友寄快递:
- 数据包 = 快递包裹
- 路由器 = 中转站
- Traceroute = 查询快递路径的工具
Traceroute 帮你回答:
- 快递经过了哪些中转站?
- 每个中转站花了多久?
- 最终有没有送到?
技术实现
Traceroute 利用 IP 协议的 TTL(Time To Live) 字段实现路径追踪:
- TTL 机制:数据包每经过一个路由器,TTL 减 1
- 超时机制:当 TTL 减为 0 时,路由器丢弃数据包并返回 ICMP Time Exceeded 消息
- 逐步探测:从 TTL=1 开始,逐步增加 TTL,收集每一跳的路由器信息
Nmap 中的 Traceroute 实现
探测规格结构体
c
struct probespec {
int probe_type; // 探测类型(ICMP/UDP/TCP)
int port; // 探测目标端口
int ttl; // 生存时间(Traceroute 核心)
int timeout; // 探测超时时间(毫秒)
int retries; // 重试次数
const char* payload; // 探测包载荷(可选)
};
struct probespec traceroute_probespec;
使用示例
c
// 初始化 Traceroute 探测参数
traceroute_probespec.probe_type = PROBE_UDP;
traceroute_probespec.port = 33434; // 默认 Traceroute UDP 端口
traceroute_probespec.timeout = 1000; // 超时 1 秒
traceroute_probespec.retries = 3; // 重试 3 次
// 调用 Traceroute 核心函数
traceroute(target_ip, &traceroute_probespec);
Nmap Traceroute 的增强特性
1. 多种探测类型支持
| 探测类型 | 适用场景 | 优势 |
|---|---|---|
| ICMP | 通用场景 | 兼容性好 |
| UDP | 绕过防火墙 | 不易被拦截 |
| TCP | 穿透 NAT | 更可靠 |
| SCTP | 特定协议 | 专业场景 |
2. 自定义参数
bash
# 指定探测类型
nmap --traceroute-probe=udp target.com
# 指定端口
nmap --traceroute-probe=tcp:80 target.com
# 指定超时
nmap --traceroute-timeout=2000 target.com
3. 批量扫描
bash
# 对多个目标同时进行 Traceroute
nmap --traceroute 192.168.1.0/24
实际使用示例
Linux 终端使用
bash
# 追踪到百度的路径
traceroute www.baidu.com
输出示例:
1 192.168.1.1 (192.168.1.1) 1.234 ms 0.987 ms 1.012 ms
2 10.0.0.1 (10.0.0.1) 5.678 ms 4.567 ms 5.123 ms
3 223.112.0.1 (223.112.0.1) 10.123 ms 9.876 ms 10.001 ms
...
10 110.242.68.3 (110.242.68.3) 25.345 ms 24.987 ms 25.111 ms
输出解析:
- 第 1 列:跳数(TTL 值)
- 第 2 列:路由器 IP 地址
- 第 3-5 列:三次探测的延迟时间(毫秒)
应用场景
1. 网络故障定位
bash
# 访问网站卡顿,定位问题节点
traceroute slow-website.com
如果某一跳延迟异常高,说明该路由器可能是瓶颈。
2. 路由路径验证
bash
# 确认数据包走的是预期路径
traceroute target-server.com
3. 网络拓扑发现
bash
# 发现网络中的路由器
traceroute 192.168.1.0/24
设计原则体现
在 traceroute_probespec 的设计中,体现了单一职责原则:
- 参数存储 :
probespec只存储探测参数 - 核心逻辑:Traceroute 的追踪逻辑在其他函数中
- 分离设计:修改探测参数不影响核心追踪逻辑
总结
| 特性 | 系统自带 Traceroute | Nmap Traceroute |
|---|---|---|
| 探测类型 | 通常仅 ICMP | ICMP/UDP/TCP/SCTP |
| 自定义参数 | 有限 | 丰富 |
| 批量扫描 | 不支持 | 支持 |
| 集成度 | 独立工具 | 集成到扫描流程 |
Traceroute 是网络诊断的利器,Nmap 通过增强的 Traceroute 功能,为网络扫描提供了更强大的路由信息获取能力。
跨平台编程:C/C++ 混合编程技巧
核心问题
在 C++ 中写 struct sockaddr_storage 而非直接写 sockaddr_storage,本质是兼容 C 语言的写法习惯 + Nmap 源码的历史风格。
C 和 C++ 的差异
| 语言 | 结构体使用方式 | 示例 |
|---|---|---|
| C | 必须加 struct |
struct sockaddr_storage addr; ✅ sockaddr_storage addr; ❌ |
| C++ | 可以不加 struct |
两种写法都正确 |
系统结构体的 C 风格定义
sockaddr_storage 是 POSIX 标准的网络结构体,其原始定义是纯 C 风格:
c
// 系统头文件中的 C 风格定义
struct sockaddr_storage {
sa_family_t ss_family; // 地址族(IPv4/IPv6)
char __ss_pad1[XXX]; // 填充字段
uint64_t __ss_align; // 内存对齐
char __ss_pad2[XXX];
};
没有使用 typedef 重命名,所以在纯 C 代码中必须写 struct sockaddr_storage。
Nmap 使用 struct 的三个原因
原因 1:兼容 C 编译器/代码模块
Nmap 是「C + 少量 C++」混合编写的项目:
cpp
// ❌ 只写 sockaddr_storage
list<sockaddr_storage> unscanned_addrs;
// 在 C++ 中没问题,但传到 C 模块编译会报错
// ✅ 加 struct
list<struct sockaddr_storage> unscanned_addrs;
// C 和 C++ 编译器都能识别
原因 2:遵循系统网络编程惯例
在 Linux/Unix 网络编程中,大家都习惯给系统结构体加 struct:
cpp
struct sockaddr_in // IPv4 地址
struct sockaddr_in6 // IPv6 地址
struct sockaddr_storage // 通用地址
这种写法能明确告诉阅读者:「这是系统定义的结构体,不是自定义的类」。
原因 3:避免作用域冲突
防御性编程,避免同名变量/宏的冲突:
cpp
// 假设有人定义了宏
#define sockaddr_storage xxx
// ❌ 不加 struct 可能被宏替换
sockaddr_storage addr; // 可能变成 xxx addr;
// ✅ 加 struct 强制识别为结构体类型
struct sockaddr_storage addr; // 正确识别
验证:两种写法的等价性
cpp
#include <list>
#include <sys/socket.h>
using namespace std;
int main() {
// 写法 1:加 struct(Nmap 风格,推荐)
list<struct sockaddr_storage> unscanned_addrs1;
// 写法 2:不加 struct(纯 C++ 合法)
list<sockaddr_storage> unscanned_addrs2;
return 0;
}
在纯 C++ 环境下,两种写法功能完全一样。
什么时候可以省略 struct?
只有满足以下条件时,C++ 中可以省略 struct:
- ✅ 代码完全是 C++ 编写
- ✅ 不会被 C 模块调用
- ✅ 没有同名的变量/宏冲突
- ✅ 团队代码规范允许
但在 Nmap 这类「跨语言、跨平台、多人维护」的大型项目中,加 struct 是更稳妥、更易读的选择。
总结
| 写法 | 适用场景 | 优势 |
|---|---|---|
struct sockaddr_storage |
跨平台项目、混合编程 | 兼容性好、可读性强 |
sockaddr_storage |
纯 C++ 项目 | 简洁 |
Nmap 选择前者的核心原因是兼容 C 代码模块 + 遵循网络编程惯例 + 避免作用域冲突,这是大型系统级项目的通用写法。
算法实现:端口列表处理与优化
端口列表合并与去重
函数功能
merge_port_lists 函数的核心作用是:合并两个端口列表,自动去除重复的端口号,并优化内存占用。
函数签名
c
/**
* @brief 合并两个端口列表并去重
* @param port_list1 第一个端口列表
* @param count1 第一个列表的元素个数
* @param port_list2 第二个端口列表
* @param count2 第二个列表的元素个数
* @param merged_port_count 输出参数,返回合并后列表的实际长度
* @return 动态分配的合并列表(需手动 free)
* @note 调用者必须手动释放返回的内存,避免内存泄漏
*/
static unsigned short* merge_port_lists(
unsigned short* port_list1, int count1,
unsigned short* port_list2, int count2,
int* merged_port_count
);
实现逻辑
c
static unsigned short* merge_port_lists(
unsigned short* port_list1, int count1,
unsigned short* port_list2, int count2,
int* merged_port_count)
{
int i;
unsigned short* merged_port_list = NULL;
*merged_port_count = 0;
// 1. 初始内存分配(最坏情况:无重复)
merged_port_list = (unsigned short*)safe_zalloc(
(count1 + count2) * sizeof(unsigned short)
);
// 2. 插入第一个列表的端口(去重)
for (i = 0; i < count1; i++) {
insert_port_into_merge_list(
merged_port_list,
merged_port_count,
port_list1[i]
);
}
// 3. 插入第二个列表的端口(去重)
for (i = 0; i < count2; i++) {
insert_port_into_merge_list(
merged_port_list,
merged_port_count,
port_list2[i]
);
}
// 4. 内存优化(释放冗余空间)
if (*merged_port_count < (count1 + count2)) {
merged_port_list = (unsigned short*)safe_realloc(
merged_port_list,
(*merged_port_count) * sizeof(unsigned short)
);
}
return merged_port_list;
}
使用示例
c
// 示例:合并两个端口列表
unsigned short list1[] = {80, 443, 8080};
unsigned short list2[] = {443, 8080, 3306};
int count1 = 3, count2 = 3;
int merged_count;
// 调用函数
unsigned short* merged_list = merge_port_lists(
list1, count1, list2, count2, &merged_count
);
// 输出结果:merged_count=4,merged_list=[80,443,8080,3306]
for (int i = 0; i < merged_count; i++) {
printf("%d ", merged_list[i]);
}
// 必须手动释放内存
free(merged_list);
merged_list = NULL;
端口映射初始化
函数功能
initializePortMap 为指定的网络协议初始化端口映射表,建立"端口号 ↔ 索引值"的双向映射关系。
函数签名
cpp
/**
* @brief 为指定协议初始化端口映射表
* @param protocol 网络协议类型(IPPROTO_TCP/IPPROTO_UDP)
* @param ports 端口列表(必须已排序)
* @param portcount 端口数量
* @note 必须在任何 PortList 对象创建前调用
*/
void PortList::initializePortMap(
int protocol,
u16 *ports,
int portcount
);
实现逻辑
cpp
void PortList::initializePortMap(
int protocol,
u16 *ports,
int portcount)
{
int i;
int ports_max = (protocol == IPPROTO_IP) ?
MAX_IPPROTONUM + 1 : 65536;
int proto = INPROTO2PORTLISTPROTO(protocol);
// 检查重复初始化
if (port_map[proto] != NULL || port_map_rev[proto] != NULL)
fatal("%s: portmap for protocol %i already initialized",
__func__, protocol);
// 断言校验
assert(port_list_count[proto] == 0);
// 内存分配(永不释放)
port_map[proto] = (u16 *)safe_zalloc(sizeof(u16) * ports_max);
port_map_rev[proto] = (u16 *)safe_zalloc(sizeof(u16) * portcount);
// 记录端口数量
port_list_count[proto] = portcount;
// 构建双向映射
for (i = 0; i < portcount; i++) {
port_map[proto][ports[i]] = i; // 正向:端口 → 索引
port_map_rev[proto][i] = ports[i]; // 反向:索引 → 端口
}
}
映射结构示例
假设传入的端口是 [2, 4, 6]:
port_map[proto]:
[0, 0, 1, 0, 2, 0, 3, ...]
↑ ↑ ↑
端口2 端口4 端口6
port_map_rev[proto]:
[2, 4, 6]
↑ ↑ ↑
索引0 索引1 索引2
性能优势
| 操作 | 无映射表(线性查找) | 有映射表(数组访问) |
|---|---|---|
| 时间复杂度 | O(n) | O(1) |
| 查找 10000 次 | ~10000 次比较 | ~10000 次数组访问 |
这是典型的"空间换时间"优化策略。
端口随机化
Fisher-Yates 洗牌算法
c
/**
* @brief 随机打乱端口列表(Fisher-Yates 算法)
* @param arr 端口数组
* @param num_elem 元素个数
*/
void shortfry(unsigned short *arr, int num_elem) {
int num;
unsigned short tmp;
int i;
if (num_elem < 2)
return;
// 从后向前遍历
for (i = num_elem - 1; i > 0; i--) {
// 生成 [0, i] 范围内的随机下标
num = get_random_ushort() % (i + 1);
// 跳过自身交换
if (i == num)
continue;
// 交换元素
tmp = arr[i];
arr[i] = arr[num];
arr[num] = tmp;
}
}
算法特点
- 公平性:每个元素出现在任意位置的概率均等
- 高效性:时间复杂度 O(n),空间复杂度 O(1)
- 原地操作:不需要额外内存
执行示例
输入:[1, 2, 3, 4]
第1轮(i=3):随机数范围[0,3],假设num=1
交换 arr[3] 和 arr[1] → [1, 4, 3, 2]
第2轮(i=2):随机数范围[0,2],假设num=0
交换 arr[2] 和 arr[0] → [3, 4, 1, 2]
第3轮(i=1):随机数范围[0,1],假设num=1
跳过交换
输出:[3, 4, 1, 2]
端口优先级优化
函数功能
random_port_cheat 将热门 TCP 端口优先移到端口列表前端,优化扫描效率。
c
/**
* @brief 将热门端口移到列表前端(需先随机化)
* @param ports 端口列表
* @param portcount 端口数量
*/
void random_port_cheat(u16 *ports, int portcount) {
int allportidx = 0;
int popportidx = 0;
int earlyreplidx = 0;
// 热门端口列表(基于 2008 年统计数据)
u16 pop_ports[] = {
80, 23, 443, 21, 22, 25, 3389, 110, 445, 139,
143, 53, 135, 3306, 8080, 1723, 111, 995, 993, 5900,
1025, 587, 8888, 199, 1720,
113, 554, 256
};
int num_pop_ports = sizeof(pop_ports) / sizeof(u16);
// 遍历端口列表
for (allportidx = 0; allportidx < portcount; allportidx++) {
// 检查当前端口是否是热门端口
for (popportidx = 0; popportidx < num_pop_ports; popportidx++) {
if (ports[allportidx] == pop_ports[popportidx]) {
// 找到热门端口,移到前端
if (allportidx != earlyreplidx) {
ports[allportidx] = ports[earlyreplidx];
ports[earlyreplidx] = pop_ports[popportidx];
}
earlyreplidx++;
break;
}
}
}
}
执行示例
输入(已随机化):[1000, 80, 2000, 22, 3000, 443, 4000]
处理过程:
- 找到 80,移到位置 0 → [80, 1000, 2000, 22, 3000, 443, 4000]
- 找到 22,移到位置 1 → [80, 22, 2000, 1000, 3000, 443, 4000]
- 找到 443,移到位置 2 → [80, 22, 443, 1000, 3000, 2000, 4000]
输出:[80, 22, 443, 1000, 3000, 2000, 4000]
设计目的
- 扫描效率优化:先扫描更可能开放的端口
- 兼顾随机性:依赖前置随机化,避免顺序固定
- 无额外内存:原地修改数组
总结
| 算法 | 时间复杂度 | 空间复杂度 | 应用场景 |
|---|---|---|---|
| 端口合并去重 | O(n+m) | O(n+m) | 合并多个端口列表 |
| 端口映射初始化 | O(n) | O(65536) | 高频端口查询 |
| Fisher-Yates 洗牌 | O(n) | O(1) | 端口随机化 |
| 端口优先级优化 | O(n*m) | O(1) | 扫描效率优化 |
这些算法展示了 Nmap 在端口处理上的精心设计,通过合理的算法选择和优化,实现了高效的端口扫描能力。
系统编程:终端与网络初始化
终端初始化
函数功能
tty_init() 将终端初始化为"无缓冲、非阻塞"的输入模式,适合实时读取键盘按键。
c
/**
* @brief 初始化终端为无缓冲、非阻塞输入模式
* @note 调用 keyWasPressed() 前必须先调用此函数
*/
void tty_init() {
struct termios ti;
// 非交互模式直接返回
if (o.noninteractive)
return;
// 安装信号处理器
install_all_handlers();
// 避免重复初始化
if (tty_fd)
return;
// 打开终端设备
if ((tty_fd = open("/dev/tty", O_RDONLY | O_NONBLOCK)) < 0) {
o.noninteractive = true;
return;
}
// 平台适配:排除 CYGWIN32
#ifndef __CYGWIN32__
if (tcgetpgrp(tty_fd) != getpgrp()) {
close(tty_fd);
return;
}
#endif
// 获取当前终端属性
tcgetattr(tty_fd, &ti);
saved_ti = ti; // 保存原有属性
// 修改本地模式:关闭规范模式、关闭回显
ti.c_lflag &= ~(ICANON | ECHO);
// 设置最小输入字节数和超时时间
ti.c_cc[VMIN] = 1; // 至少需要 1 个字节
ti.c_cc[VTIME] = 0; // 无超时
// 应用新的终端属性
tcsetattr(tty_fd, TCSANOW, &ti);
// 注册退出清理函数
atexit(tty_done);
}
核心概念
| 模式 | 说明 | 应用场景 |
|---|---|---|
| 规范模式(ICANON) | 缓冲输入直到换行 | 普通命令行输入 |
| 原始模式 | 输入立即读取 | 实时按键响应 |
| 回显模式(ECHO) | 输入字符显示 | 普通输入 |
| 无回显模式 | 输入字符不显示 | 密码输入 |
终端属性修改
c
// 关闭规范模式(输入立即读取)
ti.c_lflag &= ~ICANON;
// 关闭回显模式(按键不显示)
ti.c_lflag &= ~ECHO;
// 设置最小读取字节数
ti.c_cc[VMIN] = 1;
// 设置读取超时(十分之一秒为单位)
ti.c_cc[VTIME] = 0;
清理函数
c
void tty_done() {
if (tty_fd) {
// 恢复原有终端属性
tcsetattr(tty_fd, TCSANOW, &saved_ti);
close(tty_fd);
tty_fd = 0;
}
}
Windows 网络初始化
函数功能
win_pre_init() 初始化 Windows 平台的 Winsock 2.2 网络库。
c
/**
* @brief 初始化 Windows Winsock 2.2 网络库
*/
void win_pre_init() {
WORD werd;
WSADATA data;
// 指定 Winsock 版本 2.2
werd = MAKEWORD(2, 2);
// 初始化 Winsock 库
if ((WSAStartup(werd, &data)) != 0)
fatal("failed to start winsock.\n");
// 注册退出清理函数
atexit(win_cleanup);
}
清理函数
c
void win_cleanup() {
WSACleanup(); // 释放 Winsock 资源
}
完整示例
c
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
void fatal(const char* msg) {
printf("错误:%s,错误码:%d\n", msg, WSAGetLastError());
exit(1);
}
void win_cleanup() {
WSACleanup();
printf("Winsock 已清理\n");
}
void win_pre_init() {
WORD werd;
WSADATA data;
werd = MAKEWORD(2, 2);
if ((WSAStartup(werd, &data)) != 0)
fatal("failed to start winsock.\n");
atexit(win_cleanup);
printf("Winsock 2.2 初始化成功\n");
}
int main() {
win_pre_init();
// 网络操作代码...
return 0; // 程序退出时自动调用 win_cleanup
}
平台差异对比
| 特性 | Linux/Unix | Windows |
|---|---|---|
| 网络初始化 | 无需显式初始化 | 必须调用 WSAStartup() |
| 终端控制 | termios 结构体 | Console API |
| 信号处理 | signal/sigaction | SetConsoleCtrlHandler |
| 清理机制 | 自动清理 | 必须调用 WSACleanup() |
总结
- 终端初始化 :通过修改
termios结构体实现无缓冲、非阻塞输入 - Windows 网络初始化 :必须先调用
WSAStartup(),最后调用WSACleanup() - 资源管理 :使用
atexit()注册清理函数,确保资源正确释放 - 平台适配:通过条件编译实现跨平台兼容
这些系统编程技巧展示了 Nmap 如何在不同平台上正确初始化和管理系统资源。
路由查询:跨平台路由信息获取
核心功能
route_dst_generic 是跨平台的通用路由查询实现,给定目标 IP,返回:
- 出口网卡
- 源地址
- 是否直连
- 下一跳网关
函数签名
c
/**
* @brief 跨平台路由查询
* @param dst 目标地址(IPv4/IPv6)
* @param rnfo 输出参数,存储路由结果
* @param device 指定出口网卡(NULL 则自动选择)
* @param spoofss 自定义源地址(源地址欺骗)
* @return 1=成功,0=失败
*/
static int route_dst_generic(
const struct sockaddr_storage *dst,
struct route_nfo *rnfo,
const char *device,
const struct sockaddr_storage *spoofss
);
查询流程
┌─────────────────────────────────────────┐
│ 路由查询三级匹配策略 │
└─────────────────────────────────────────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
第1步 第2步 第3步
本机地址 系统路由表 网卡直连网段
│ │ │
▼ ▼ ▼
回环网卡 精确路由 局域网直连
实现逻辑
1. 参数校验与初始化
c
struct interface_info *ifaces;
struct interface_info *iface;
int numifaces = 0;
struct sys_route *routes;
int numroutes = 0;
int i;
char namebuf[32];
char errstr[256];
errstr[0] = '\0';
// 致命校验:目标地址不能为空
if (!dst)
netutil_fatal("%s passed a NULL dst address", __func__);
2. 处理源地址欺骗
c
if (spoofss != NULL) {
// 使用自定义源地址
memcpy(&rnfo->srcaddr, spoofss, sizeof(rnfo->srcaddr));
// 断言:必须指定出口网卡
assert(device != NULL && device[0] != '\0');
}
3. 处理 IPv6 作用域 ID
c
if (device == NULL || device[0] == '\0') {
if (dst->ss_family == AF_INET6) {
const struct sockaddr_in6 *sin6 =
(struct sockaddr_in6 *) dst;
if (sin6->sin6_scope_id > 0) {
// 作用域 ID → 网卡名称
device = lookup_ifindex(
sin6->sin6_scope_id,
sin6->sin6_family,
namebuf,
sizeof(namebuf)
);
if (device == NULL) {
netutil_error(
"Could not find interface with index %u",
(unsigned int) sin6->sin6_scope_id
);
return 0;
}
}
}
}
4. 解析指定出口网卡
c
if (device != NULL && device[0] != '\0') {
iface = getInterfaceByName(device, dst->ss_family);
if (!iface)
netutil_fatal("Could not find interface %s", device);
} else {
iface = NULL;
}
5. 读取系统路由表和网卡列表
c
// 读取系统路由表
if ((routes = getsysroutes(&numroutes, errstr, sizeof(errstr))) == NULL)
netutil_fatal("%s: Failed to obtain system routes: %s",
__func__, errstr);
// 读取系统网卡列表
if ((ifaces = getinterfaces(&numifaces, errstr, sizeof(errstr))) == NULL)
netutil_fatal("%s: Failed to obtain system interfaces: %s",
__func__, errstr);
6. 第一步匹配:目标是否是本机地址
c
for (i = 0; i < numifaces; i++) {
// 目标地址 ≠ 当前网卡 IP,跳过
if (!sockaddr_equal(dst, &ifaces[i].addr))
continue;
// 找到回环网卡
struct interface_info *loopback;
if (ifaces[i].device_type == devt_loopback)
loopback = &ifaces[i];
else
loopback = find_loopback_iface(ifaces, numifaces);
if (loopback == NULL)
break;
// 检查网卡约束
if (iface != NULL &&
strcmp(loopback->devname, iface->devname) != 0)
continue;
if (iface == NULL && !loopback->device_up)
continue;
// 填充路由结果
rnfo->ii = *loopback;
rnfo->direct_connect = 1;
if (!spoofss) {
if (get_srcaddr(dst, &rnfo->srcaddr) == -1)
rnfo->srcaddr = rnfo->ii.addr;
}
return 1;
}
7. 第二步匹配:匹配系统路由表
c
for (i = 0; i < numroutes; i++) {
// 目标地址不匹配当前路由网段
if (!sockaddr_equal_netmask(dst, &routes[i].dest,
routes[i].netmask_bits))
continue;
// 检查网卡约束
if (iface != NULL &&
strcmp(routes[i].device->devname, iface->devname) != 0)
continue;
if (iface == NULL && !routes[i].device->device_up)
continue;
// 填充路由结果
rnfo->ii = *routes[i].device;
// 判断是否直连
rnfo->direct_connect = (
sockaddr_equal_zero(&routes[i].gw) ||
sockaddr_equal(&routes[i].gw, &routes[i].device->addr) ||
sockaddr_equal(&routes[i].gw, dst)
);
if (!spoofss) {
if (get_srcaddr(dst, &rnfo->srcaddr) == -1)
rnfo->srcaddr = rnfo->ii.addr;
}
rnfo->nexthop = routes[i].gw;
return 1;
}
8. 第三步匹配:匹配网卡直连网段
c
for (i = 0; i < numifaces; i++) {
// 目标地址不在当前网卡网段
if (!sockaddr_equal_netmask(dst, &ifaces[i].addr,
ifaces[i].netmask_bits))
continue;
// 检查网卡约束
if (iface != NULL &&
strcmp(ifaces[i].devname, iface->devname) != 0)
continue;
if (iface == NULL && !ifaces[i].device_up)
continue;
// 填充路由结果
rnfo->ii = ifaces[i];
rnfo->direct_connect = 1;
if (!spoofss) {
if (get_srcaddr(dst, &rnfo->srcaddr) == -1)
rnfo->srcaddr = rnfo->ii.addr;
}
return 1;
}
9. 匹配失败
c
return 0;
直连判断逻辑
c
// 满足以下任一条件则为直连
rnfo->direct_connect = (
sockaddr_equal_zero(&routes[i].gw) || // 网关是 0.0.0.0/::
sockaddr_equal(&routes[i].gw, &routes[i].device->addr) || // 网关 = 网卡 IP
sockaddr_equal(&routes[i].gw, dst) // 网关 = 目标 IP
);
实际应用示例
示例 1:本机地址
目标 IP:127.0.0.1
查询过程:
1. 第1步匹配:找到回环网卡 lo
2. 返回结果:
- 出口网卡:lo
- 源地址:127.0.0.1
- 直连:是
- 网关:无
示例 2:局域网地址
目标 IP:192.168.1.50
网卡 eth0:192.168.1.100/24
查询过程:
1. 第1步匹配:不是本机地址
2. 第2步匹配:路由表有 192.168.1.0/24 dev eth0 gw 0.0.0.0
3. 返回结果:
- 出口网卡:eth0
- 源地址:192.168.1.100
- 直连:是
- 网关:0.0.0.0
示例 3:外网地址
目标 IP:8.8.8.8
默认路由:default via 192.168.1.1 dev eth0
查询过程:
1. 第1步匹配:不是本机地址
2. 第2步匹配:匹配默认路由
3. 返回结果:
- 出口网卡:eth0
- 源地址:192.168.1.100
- 直连:否
- 网关:192.168.1.1
跨平台实现
| 平台 | 路由表来源 | 网卡信息来源 |
|---|---|---|
| Linux | /proc/net/route | /sys/class/net |
| Windows | 注册表/WMI | GetAdaptersAddresses() |
| macOS | route -n 输出 | ifconfig 输出 |
总结
- 三级匹配策略:本机地址 → 系统路由表 → 网卡直连网段
- 跨平台兼容:通过封装系统差异实现统一接口
- 精确路由优先:路由表按精确度排序,保证最优路由
- 灵活配置:支持指定网卡、源地址欺骗等定制需求
这个路由查询函数展示了 Nmap 如何在不同平台上准确获取路由信息,为网络扫描提供基础支持。
地址解析:域名到 IP 的转换机制
核心功能
resolve 和 resolve_internal 函数封装了标准库的 getaddrinfo,实现域名/IP 字符串到结构化地址的转换。
函数签名
c
/**
* @brief 地址解析(对外接口)
* @param hostname 域名/IP 字符串
* @param port 端口号(0 表示不指定)
* @param ss 输出参数,存储解析后的地址
* @param sslen 输出参数,返回地址长度
* @param af 地址族(AF_INET/AF_INET6/AF_UNSPEC)
* @return 0=成功,非0=失败
*/
int resolve(
const char *hostname,
unsigned short port,
struct sockaddr_storage *ss,
size_t *sslen,
int af
);
/**
* @brief 地址解析(内部实现)
* @param addl_flags 额外解析标志(如 AI_NUMERICHOST)
*/
static int resolve_internal(
const char *hostname,
unsigned short port,
struct sockaddr_storage *ss,
size_t *sslen,
int af,
int addl_flags
);
实现逻辑
对外接口
c
int resolve(const char *hostname, unsigned short port,
struct sockaddr_storage *ss, size_t *sslen, int af) {
return resolve_internal(hostname, port, ss, sslen, af, 0);
}
内部实现
c
static int resolve_internal(
const char *hostname,
unsigned short port,
struct sockaddr_storage *ss,
size_t *sslen,
int af,
int addl_flags)
{
struct addrinfo hints;
struct addrinfo *result;
char portbuf[16];
char *servname = NULL;
int rc;
// 断言校验
assert(hostname);
assert(ss);
assert(sslen);
// 初始化解析配置
memset(&hints, 0, sizeof(hints));
hints.ai_family = af;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_flags |= addl_flags;
// 端口号转为字符串
if (port != 0) {
rc = Snprintf(portbuf, sizeof(portbuf), "%hu", port);
assert(rc >= 0 && (size_t) rc < sizeof(portbuf));
servname = portbuf;
}
// 调用 getaddrinfo 解析
rc = getaddrinfo(hostname, servname, &hints, &result);
if (rc != 0)
return rc;
if (result == NULL)
return EAI_NONAME;
// 校验结果长度并拷贝地址
assert(result->ai_addrlen > 0 &&
result->ai_addrlen <= (int) sizeof(struct sockaddr_storage));
*sslen = result->ai_addrlen;
memcpy(ss, result->ai_addr, *sslen);
// 释放内存
freeaddrinfo(result);
return 0;
}
使用示例
示例 1:解析域名
c
struct sockaddr_storage ss;
size_t sslen;
// 解析 www.baidu.com,端口 80,IPv4
int rc = resolve("www.baidu.com", 80, &ss, &sslen, AF_INET);
if (rc == 0) {
printf("解析成功,地址长度:%zu\n", sslen);
} else {
printf("解析失败:%s\n", gai_strerror(rc));
}
示例 2:强制解析数字 IP
c
// 添加 AI_NUMERICHOST 标志,仅解析数字 IP
int rc = resolve_internal("192.168.1.1", 0, &ss, &sslen,
AF_INET, AI_NUMERICHOST);
// 若传入 "www.baidu.com",会返回 EAI_NONAME
错误处理
c
// 解析失败时输出错误信息
if (rc != 0) {
fatal("Can't resolve %s: %s.", dst, gai_strerror(rc));
}
gai_strerror() 将错误码转为可读字符串:
EAI_NONAME:名称不存在EAI_AGAIN:DNS 解析超时EAI_FAIL:不可恢复的失败
关键数据结构
addrinfo 结构体
c
struct addrinfo {
int ai_flags; // 解析标志
int ai_family; // 地址族(AF_INET/AF_INET6)
int ai_socktype; // 套接字类型(SOCK_STREAM/SOCK_DGRAM)
int ai_protocol; // 协议类型
size_t ai_addrlen; // 地址长度
struct sockaddr *ai_addr; // 地址指针
char *ai_canonname; // 规范名称
struct addrinfo *ai_next; // 链表下一项
};
sockaddr_storage 结构体
c
struct sockaddr_storage {
sa_family_t ss_family; // 地址族
char __ss_pad1[XXX]; // 填充字段
uint64_t __ss_align; // 内存对齐
char __ss_pad2[XXX];
};
通用地址结构体,兼容 IPv4 和 IPv6。
总结
- 分层设计 :
resolve(对外简洁接口)+resolve_internal(内部灵活实现) - 标准库封装 :基于
getaddrinfo,支持 IPv4/IPv6 - 错误处理:返回标准错误码,便于上层处理
- 内存管理 :正确释放
getaddrinfo分配的内存
地址解析是网络编程的基础,Nmap 通过封装标准库函数,提供了简洁易用的接口。
错误处理:健壮的程序设计
致命错误处理函数
函数功能
netutil_fatal 输出格式化的致命错误信息到 stderr,并终止程序。
c
/**
* @brief 输出致命错误信息并退出程序
* @warning 此函数不返回(调用 exit())
*/
void netutil_fatal(const char *str, ...) {
va_list list;
char errstr[NBASE_MAX_ERR_STR_LEN];
memset(errstr, 0, NBASE_MAX_ERR_STR_LEN);
// 初始化可变参数列表
va_start(list, str);
// 刷新标准输出
fflush(stdout);
// 输出错误信息到 stderr
vfprintf(stderr, str, list);
fprintf(stderr, "\n");
// 清理可变参数列表
va_end(list);
// 终止程序
exit(EXIT_FAILURE);
}
使用示例
c
// 目标地址为空
if (!dst)
netutil_fatal("%s passed a NULL dst address", __func__);
// 读取路由表失败
if ((routes = getsysroutes(&numroutes, errstr, sizeof(errstr))) == NULL)
netutil_fatal("%s: Failed to obtain system routes: %s",
__func__, errstr);
可变参数处理
核心宏
c
#include <stdarg.h>
va_list list; // 声明可变参数列表
va_start(list, str); // 初始化列表
vfprintf(stderr, str, list); // 使用列表
va_end(list); // 清理列表
工作原理
va_start(list, str)
│
├─ 获取最后一个固定参数的地址
│
├─ 计算可变参数的起始位置
│
└─ 初始化 va_list 指针
va_arg(list, type)
│
├─ 读取当前参数
│
├─ 移动到下一个参数
│
└─ 返回参数值
va_end(list)
│
└─ 清理 va_list 资源
输出流管理
stdout vs stderr
| 特性 | stdout | stderr |
|---|---|---|
| 用途 | 正常输出 | 错误输出 |
| 缓冲 | 行缓冲 | 无缓冲 |
| 重定向 | 可重定向 | 通常不重定向 |
| 用例 | printf() | fprintf(stderr, ...) |
刷新缓冲区
c
fflush(stdout); // 强制刷新 stdout 缓冲区
确保在输出错误信息前,正常输出已经显示。
退出状态
c
exit(EXIT_FAILURE); // 失败退出(通常为 1)
exit(EXIT_SUCCESS); // 成功退出(通常为 0)
总结
- 可变参数 :使用
va_list系列宏处理格式化输出 - 输出流分离:正常输出用 stdout,错误输出用 stderr
- 缓冲区刷新:输出错误前刷新 stdout,避免乱序
- 退出状态 :使用标准宏
EXIT_FAILURE/EXIT_SUCCESS
健壮的错误处理是程序稳定性的基础,Nmap 通过统一的错误处理机制,确保程序在异常情况下能够优雅退出。
网络接口管理:Windows 平台实现
接口查询函数
函数功能
lookup_ifindex 根据接口索引查询对应的接口名称。
c
/**
* @brief 根据接口索引查询接口名称
* @param index 接口索引
* @param af 地址族(AF_INET/AF_INET6)
* @param namebuf 输出缓冲区,存储接口名称
* @param len 缓冲区长度
* @return 成功返回 namebuf,失败返回 NULL
*/
static char *lookup_ifindex(
unsigned int index,
int af,
char *namebuf,
size_t len
) {
intf_t *it;
struct intf_entry entry;
int rc;
// 打开接口查询上下文
it = intf_open();
assert(it != NULL);
// 初始化结构体
entry.intf_len = sizeof(entry);
// 查询接口信息
rc = intf_get_index(it, &entry, af, index);
intf_close(it);
if (rc == -1)
return NULL;
// 拷贝接口名称
Strncpy(namebuf, entry.intf_name, len);
return namebuf;
}
接口句柄结构体
intf_handle 定义
c
struct intf_handle {
struct ifcombo ifcombo[MIB_IF_TYPE_MAX];
IP_ADAPTER_ADDRESSES *iftable;
};
字段说明
1. ifcombo 数组
c
struct ifcombo ifcombo[MIB_IF_TYPE_MAX];
- 类型 :
ifcombo结构体数组 - 长度 :
MIB_IF_TYPE_MAX(MIB 接口类型最大值) - 作用:按接口类型分类存储接口信息
MIB 接口类型:
IF_TYPE_ETHERNET_CSMACD(以太网,值为 6)IF_TYPE_LOOPBACK(回环,值为 24)IF_TYPE_IEEE80211(WiFi,值为 71)IF_TYPE_PPP(PPP,值为 23)
2. iftable 指针
c
IP_ADAPTER_ADDRESSES *iftable;
- 类型 :指向
IP_ADAPTER_ADDRESSES的指针 - 平台:Windows 专属
- 作用:指向系统网络适配器信息链表
IP_ADAPTER_ADDRESSES 结构体:
c
typedef struct _IP_ADAPTER_ADDRESSES {
union {
ULONGLONG Alignment;
struct {
ULONG Length;
DWORD IfIndex;
};
};
struct _IP_ADAPTER_ADDRESSES *Next; // 链表下一项
PCHAR AdapterName; // 适配器名称
// ... 更多字段
} IP_ADAPTER_ADDRESSES, *PIP_ADAPTER_ADDRESSES;
设计逻辑
双层缓存架构
┌─────────────────────────────────────┐
│ intf_handle │
├─────────────────────────────────────┤
│ ifcombo[] │
│ ├─ [6] → 以太网接口列表 │
│ ├─ [24] → 回环接口列表 │
│ └─ [71] → WiFi 接口列表 │
├─────────────────────────────────────┤
│ iftable │
│ └─ → IP_ADAPTER_ADDRESSES 链表 │
└─────────────────────────────────────┘
- iftable:存储原始、完整的系统接口信息
- ifcombo:按类型分类的缓存,提升查询效率
资源管理
打开接口上下文
c
intf_t *it = intf_open();
内部调用 GetAdaptersAddresses() 获取系统接口信息。
关闭接口上下文
c
intf_close(it);
释放 iftable 指向的内存(调用 LocalFree())。
平台差异
| 特性 | Linux | Windows |
|---|---|---|
| 接口信息获取 | ioctl(SIOCGIFCONF) | GetAdaptersAddresses() |
| 接口结构体 | struct ifreq | IP_ADAPTER_ADDRESSES |
| 接口索引 | ifr_ifindex | IfIndex |
| 接口名称 | ifr_name | AdapterName |
总结
- 双层缓存 :
iftable存储原始数据,ifcombo提供分类缓存 - 平台专属 :
IP_ADAPTER_ADDRESSES是 Windows 专属结构体 - 资源管理:正确打开和关闭接口上下文,避免内存泄漏
- 类型分类:按 MIB 接口类型分类,提升查询效率
Windows 平台的网络接口管理展示了 Nmap 如何适配不同操作系统的差异。
扫描优化:端口随机化与优先级
端口随机化
Fisher-Yates 算法
c
void shortfry(unsigned short *arr, int num_elem) {
int num;
unsigned short tmp;
int i;
if (num_elem < 2)
return;
// 从后向前遍历
for (i = num_elem - 1; i > 0; i--) {
// 生成 [0, i] 范围内的随机下标
num = get_random_ushort() % (i + 1);
// 跳过自身交换
if (i == num)
continue;
// 交换元素
tmp = arr[i];
arr[i] = arr[num];
arr[num] = tmp;
}
}
算法特点
- 公平性:每个元素出现在任意位置的概率均等
- 高效性:时间复杂度 O(n),空间复杂度 O(1)
- 原地操作:不需要额外内存
端口优先级优化
热门端口列表
c
u16 pop_ports[] = {
80, 23, 443, 21, 22, 25, 3389, 110, 445, 139,
143, 53, 135, 3306, 8080, 1723, 111, 995, 993, 5900,
1025, 587, 8888, 199, 1720,
113, 554, 256
};
基于 2008 年的 nmap-services-all 数据,包含最常开放的 28 个 TCP 端口。
优化函数
c
void random_port_cheat(u16 *ports, int portcount) {
int allportidx = 0;
int popportidx = 0;
int earlyreplidx = 0;
int num_pop_ports = sizeof(pop_ports) / sizeof(u16);
// 遍历端口列表
for (allportidx = 0; allportidx < portcount; allportidx++) {
// 检查当前端口是否是热门端口
for (popportidx = 0; popportidx < num_pop_ports; popportidx++) {
if (ports[allportidx] == pop_ports[popportidx]) {
// 找到热门端口,移到前端
if (allportidx != earlyreplidx) {
ports[allportidx] = ports[earlyreplidx];
ports[earlyreplidx] = pop_ports[popportidx];
}
earlyreplidx++;
break;
}
}
}
}
执行流程
输入(已随机化):[1000, 80, 2000, 22, 3000, 443, 4000]
处理过程:
├─ 找到 80,移到位置 0 → [80, 1000, 2000, 22, 3000, 443, 4000]
├─ 找到 22,移到位置 1 → [80, 22, 2000, 1000, 3000, 443, 4000]
└─ 找到 443,移到位置 2 → [80, 22, 443, 1000, 3000, 2000, 4000]
输出:[80, 22, 443, 1000, 3000, 2000, 4000]
设计目的
1. 扫描效率优化
- 优先扫描热门端口:这些端口更可能开放
- 提前获取结果:更快得到有效扫描结果
2. 兼顾随机性
- 依赖前置随机化:避免端口顺序完全固定
- 防止模式识别:避免被目标主机识别扫描模式
3. 无额外内存
- 原地修改数组:不占用额外内存空间
- 时间复杂度可接受:O(n*m),但 m 很小(28)
使用示例
c
// 1. 随机化端口列表
shortfry(ports, portcount);
// 2. 优化端口顺序(热门端口优先)
random_port_cheat(ports, portcount);
// 3. 开始扫描
for (int i = 0; i < portcount; i++) {
scan_port(target, ports[i]);
}
总结
| 优化策略 | 目的 | 实现方式 |
|---|---|---|
| 端口随机化 | 避免固定扫描模式 | Fisher-Yates 算法 |
| 热门端口优先 | 提升扫描效率 | 将热门端口移到前端 |
| 双重优化 | 兼顾效率和隐蔽性 | 先随机化,再优化 |