【nmap源码】Nmap OS扫描核心技术深度解析:从循环迭代器到随机数生成

Nmap OS扫描核心技术深度解析:从循环迭代器到随机数生成

目录

  1. 循环迭代器:高效遍历未完成主机列表
  2. 执行与状态分离:工业级设计模式
  3. 全局静态变量:性能统计的核心
  4. 拥塞控制结构体:扫描速率的智能调节器
  5. cwnd翻倍机制:慢启动阶段的核心特征
  6. 初始化方法:动态配置拥塞控制参数
  7. TCP拥塞控制四大算法:网络传输的基石
  8. MSS概念:TCP报文的最大数据长度
  9. 时间计算函数:扫描耗时的精确计量
  10. 抓包器初始化:构建高性能抓包环境
  11. 字符串安全拼接:Snprintf详解
  12. 跨平台接口名转换:Windows平台的特殊处理
  13. 随机数生成:加密级安全随机源

1. 循环迭代器:高效遍历未完成主机列表

1.1 核心概念

在Nmap的OS扫描代码中,std::list<HostOsScanInfo *>::iterator nextI 这个"循环迭代器"并非C++标准库的原生概念,而是基于标准库迭代器实现的业务逻辑层面的功能

本质特征:

  • nextIstd::list 的普通迭代器,用于遍历 incompleteHosts 链表容器
  • "循环"是指:当迭代器遍历到链表末尾(end())时,会自动回到链表开头(begin()),实现循环遍历未完成扫描的主机列表

1.2 核心方法实现

cpp 复制代码
// 重置迭代器到链表开头
void OsScanInfo::resetHostIterator() {
    nextI = incompleteHosts.begin();
}

// 获取下一个未完成主机(核心:循环遍历)
HostOsScanInfo *OsScanInfo::nextIncompleteHost() {
    // 如果链表为空,直接返回空
    if (incompleteHosts.empty()) {
        return nullptr;
    }

    // 保存当前迭代器指向的主机
    HostOsScanInfo *host = *nextI;

    // 迭代器后移
    ++nextI;

    // 关键:如果迭代器到末尾,回到开头(实现"循环")
    if (nextI == incompleteHosts.end()) {
        nextI = incompleteHosts.begin();
    }

    return host;
}

1.3 使用场景与价值

在OS扫描类中,循环迭代器的核心价值体现在:

  1. 轮询未完成主机:扫描任务通常需要轮询未完成的主机(比如多线程扫描时,每个线程从列表中取一个主机扫描)
  2. 避免重复遍历:避免每次遍历都从头开始,而是"接力式"遍历,提升扫描效率
  3. 动态适应 :即使链表中的主机被移除(removeCompletedHosts()),迭代器仍能通过重置/重新遍历保持逻辑正确

1.4 设计优势总结

特性 优势
私有化nextI 避免外部直接修改迭代器,保证遍历逻辑的安全性和一致性
循环机制 实现对未完成主机的循环、无遗漏遍历
重置能力 通过resetHostIterator()保证遍历的起点可控

2. 执行与状态分离:工业级设计模式

2.1 设计理念对比

不好的设计(状态耦合) 好的设计(执行与状态分离)
类 = 扫描逻辑 + 扫描状态 类 = 纯扫描逻辑;状态单独存在 HostOsScanStats
一个 HostOsScan 实例只能处理一台主机的扫描 一个 HostOsScan 实例可以复用处理多台主机的扫描
状态存在类成员里,多线程/多主机场景易冲突 状态通过参数传递,每台主机的状态独立隔离

2.2 核心优势详解

2.2.1 无状态化设计,支持实例复用

不好的设计:

cpp 复制代码
class HostOsScan {
private:
    std::vector<Probe> probes; // 每个实例只能存一台主机的探测包
    std::string targetIp;      // 只绑一个IP
public:
    HostOsScan(std::string ip) { targetIp = ip; }
    void sendProbe() {
        send_tcp_probe(probes[0], targetIp);
    }
};

// 扫描100台主机,要创建100个实例,浪费!
HostOsScan scan1("192.168.1.1");
scan1.sendProbe();
HostOsScan scan2("192.168.1.2");
scan2.sendProbe();

好的设计:

cpp 复制代码
// 专门存状态的类:只装数据,不干活
class HostOsScanStats {
public:
    std::vector<Probe> probes; // 某台主机的探测包
    std::string targetIp;      // 某台主机的IP
};

// 通用引擎类:只干活,不存具体主机的状态
class HostOsScan {
public:
    void sendProbe(HostOsScanStats *hss) {
        send_tcp_probe(hss->probes[0], hss->targetIp);
    }
};

// 只创建1个引擎,扫100台主机只需100个状态对象
HostOsScan scanEngine; // 通用引擎,只创建一次

HostOsScanStats host1;
host1.targetIp = "192.168.1.1";
host1.probes = {probe1, probe2};
scanEngine.sendProbe(&host1); // 扫主机1

HostOsScanStats host2;
host2.targetIp = "192.168.1.2";
host2.probes = {probe3, probe4};
scanEngine.sendProbe(&host2); // 扫主机2
2.2.2 状态隔离,避免并发问题

OS扫描通常是多线程/多进程的(同时扫描多台主机),状态耦合的设计会导致严重问题:

  • 不好的设计 :如果多个线程共用一个 HostOsScan 实例,会同时修改类内的 probes 成员,导致数据错乱
  • 好的设计 :每台主机的状态(HostOsScanStats)是独立的,即使多线程调用同一个 HostOsScan 实例的 sendProbe(),只要传入不同的 hss 指针,状态就不会互相干扰
2.2.3 职责单一,代码易维护/扩展

软件工程的"单一职责原则"要求:一个类只做一件事。

  • 不好的设计HostOsScan 既负责"怎么扫描(构建包、发送、处理响应)",又负责"存扫描数据(probes、响应结果)"
  • 好的设计
    • HostOsScan 只专注于"扫描逻辑的执行"
    • HostOsScanStats 只专注于"存储单主机的扫描状态"
2.2.4 状态可灵活管理(持久化/恢复)

扫描过程中可能需要暂停、恢复、保存扫描进度:

  • 不好的设计 :状态和执行逻辑绑死在 HostOsScan 实例里,要保存进度需要从类的各个成员中提取数据
  • 好的设计 :所有状态都集中在 HostOsScanStats 中,只需序列化/反序列化这个对象,就能轻松实现扫描进度的保存和恢复

2.3 生活化类比

HostOsScan(扫描执行引擎)和"状态"的关系,类比成 "外卖骑手"和"订单信息"

  • HostOsScan = 外卖骑手(核心能力是"会送外卖")
  • 扫描状态(probes/响应/速率等)= 订单信息(收货地址、菜品、配送时间、备注等)
  • 多台待扫描主机 = 多个外卖订单

不好的设计 :给每个订单配一个专属骑手,且这个骑手脑子里只记这一个订单的信息
好的设计:一个骑手可以送多个订单,每个订单的信息写在"订单小票"(HostOsScanStats)上,骑手送哪个订单就拿对应的小票


3. 全局静态变量:性能统计的核心

3.1 代码解析

cpp 复制代码
static struct scan_performance_vars perf;
关键字/标识符 含义
static 静态变量,限制变量的作用域 (只在当前文件可见),且生命周期贯穿整个程序运行期
struct scan_performance_vars 自定义的结构体类型,专门用来封装"扫描性能相关的所有数据"
perf 变量名(performance 的缩写),是这个结构体的实例

简单说:这行代码定义了一个只在当前文件可见、全程存在的"扫描性能统计账本"

3.2 结构体定义

cpp 复制代码
struct scan_performance_vars {
    // 速率相关
    int total_probes_sent;       // 累计发送的探测包总数
    int total_probes_recv;       // 累计接收的响应包总数
    float send_rate;             // 当前发送速率(包/秒)
    float recv_rate;             // 当前接收速率(包/秒)
    
    // 丢包/重试相关
    int lost_probes;             // 丢失的探测包数(发了没回应)
    int retry_count;             // 重试次数
    float loss_rate;             // 丢包率(lost/total_sent)
    
    // 耗时相关
    float avg_round_trip_time;   // 平均往返时延(RTT)
    int max_concurrent_probes;   // 最大并发探测数
    
    // 状态标记
    bool is_rate_limited;        // 是否触发了速率限制
    time_t last_update_time;     // 最后一次更新统计的时间
};

3.3 核心作用

3.3.1 统一收集"全量扫描"的性能数据(全局视角)
  • HostOsScanStats单主机的状态(比如"给192.168.1.1发了5个包")
  • perf整个扫描任务的全局统计(比如"总共扫了100台主机,发了1000个包,丢了20个,平均速率50包/秒")

生活化类比:

  • HostOsScanStats = 每个外卖订单的配送记录("订单A送了30分钟,距离2公里")
  • perf = 骑手一天的整体业绩("今天送了50单,总里程80公里,平均配送时间25分钟,超时2单")
3.3.2 static关键字的关键作用
  • 不加static:这个变量是"全局可见"的,其他文件如果也定义了同名变量会冲突,且容易被误修改
  • 加static
    • ✅ 作用域只在当前 .cpp 文件,其他文件看不到,避免命名冲突
    • ✅ 生命周期是"程序启动到退出",统计数据不会中途丢失

3.4 实际使用示例

cpp 复制代码
// 发送探测包时,更新统计
void send_tcp_probe(...) {
    // 发送逻辑...
    
    // 更新全局性能统计
    perf.total_probes_sent++;          // 发送数+1
    perf.send_rate = calc_send_rate(); // 重新计算发送速率
    perf.last_update_time = time(NULL);// 更新时间戳
}

// 接收响应时,更新统计
void processResp(...) {
    // 响应处理逻辑...
    
    perf.total_probes_recv++;          // 接收数+1
    perf.recv_rate = calc_recv_rate(); // 重新计算接收速率
    perf.loss_rate = (float)perf.lost_probes / perf.total_probes_sent; // 计算丢包率
}

// 提供读取统计的接口(给外部用,比如打印扫描报告)
struct scan_performance_vars get_scan_perf_stats() {
    return perf; // 返回当前的性能统计结果
}

4. 拥塞控制结构体:扫描速率的智能调节器

4.1 核心定位

scan_performance_vars 结构体不是单纯的"统计账本" ,而是 OS 扫描中拥塞控制(Congestion Control)的核心配置/状态结构体 ------ 本质是"扫描速率的智能调节器",用来控制探测包发送的快慢、并发量,避免网络拥塞或被目标主机拉黑。

4.2 拥塞窗口(cwnd)核心概念

拥塞窗口(cwnd):简单说就是"当前允许同时发出去但还没收到响应的探测包最大数量" ------ cwnd 越大,并发越高、扫描越快;cwnd 越小,并发越低、越保守。

4.3 逐字段解释

4.3.1 拥塞窗口(cwnd)的核心阈值(基础配置)
字段名 中文含义 通俗解释 + 扫描场景作用
low_cwnd 最小拥塞窗口 哪怕网络再差,也至少允许同时发这么多探测包(比如设为 2),保证扫描不会完全停摆
host_initial_cwnd 单主机初始拥塞窗口 对每台新主机开始扫描时,初始允许并发的探测包数(比如设为 5),相当于"给单主机的初始油门"
group_initial_cwnd 全局初始拥塞窗口 对所有扫描主机的总并发上限(比如设为 100),避免整体发太多包压垮网络
max_cwnd 最大拥塞窗口 无论网络多好,单主机/全局的 cwnd 都不能超过这个数(比如设为 20),防止过度并发
4.3.2 cwnd 的增长规则("踩油门"的节奏)
字段名 中文含义 通俗解释 + 扫描场景作用
slow_incr 慢启动模式增量 「慢启动」是扫描初期的模式,每收到一个响应,cwnd 就加这么多(比如设为 1)------ 初期慢慢提速,避免一开始就发太多
ca_incr 拥塞避免模式增量 当 cwnd 超过阈值后进入「拥塞避免」模式,每经过一个 RTT(往返时延),cwnd 加这么多(比如设为 0.5)------ 平稳提速,防止拥塞
cc_scale_max cwnd 增量的最大缩放因子 限制 cwnd 增长的"上限倍数"(比如设为 4),避免某些极端情况下 cwnd 疯长
initial_ssthresh 初始慢启动阈值 cwnd 超过这个值,就从「慢启动」切换到「拥塞避免」模式(比如设为 10)------ 相当于"提速到一定程度就换平稳模式"
4.3.3 cwnd 的下降规则("踩刹车"的节奏)
字段名 中文含义 通俗解释 + 扫描场景作用
group_drop_cwnd_divisor 全局丢包时 cwnd 除数 只要有任何一个包丢了,全局 cwnd 就除以这个数(比如设为 2)------ 全局降速,比如 cwnd=100 → 丢包后变成 50
group_drop_ssthresh_divisor 全局丢包时慢启动阈值除数 同时降低全局的慢启动阈值(比如设为 2),让后续更难进入"快提速"的慢启动模式,更保守
host_drop_ssthresh_divisor 单主机丢包时慢启动阈值除数 某台主机丢包时,单独降低这台主机的慢启动阈值(比如设为 2)------ 只针对有问题的主机降速,不影响其他主机

4.4 实际扫描示例

假设配置:host_initial_cwnd=5slow_incr=1initial_ssthresh=10ca_incr=0.5group_drop_cwnd_divisor=2

扫描流程:

  1. 开始扫描某台主机 → 初始 cwnd=5(host_initial_cwnd),进入「慢启动」模式
  2. 每收到1个响应 → cwnd +=1(slow_incr)→ 5→6→7→...→10
  3. cwnd 到 10(initial_ssthresh)→ 切换到「拥塞避免」模式
  4. 每经过1个 RTT → cwnd +=0.5(ca_incr)→ 10→10.5→11→...→max_cwnd(20)就不再涨
  5. 如果出现丢包 → 全局 cwnd = 全局 cwnd / 2(group_drop_cwnd_divisor)→ 比如从 100 降到 50,整体降速
  6. 丢包后再恢复 → 从 low_cwnd(2)重新慢慢涨,避免再次拥塞

5. cwnd翻倍机制:慢启动阶段的核心特征

5.1 核心结论

cwnd 只在「慢启动阶段」才会快速翻倍,进入「拥塞避免阶段」后就不再翻倍,而是缓慢增长。

5.2 慢启动阶段定义

慢启动是 TCP 拥塞控制(也是扫描工具借鉴的核心逻辑)的第一个阶段,核心特点是:

网络状态未知时,cwnd 以"指数级"增长(比如 1→2→4→8→16...),快速试探网络的承载能力,直到触发"慢启动阈值(ssthresh)"。

5.3 cwnd 翻倍的具体条件

5.3.1 标准 TCP 慢启动:每收到一个 ACK,cwnd 加 1 → 一轮 RTT 后翻倍

TCP 场景:

  • 初始 cwnd=1,发送 1 个报文段 → 收到 1 个 ACK(响应)→ cwnd 变成 2
  • 接着发送 2 个报文段 → 收到 2 个 ACK → cwnd 变成 4
  • 一轮 RTT(往返时延)内,发出去的包都收到响应,cwnd 就从 1→2→4→8... 指数级翻倍

扫描场景(适配探测包):

  • 初始 cwnd = host_initial_cwnd(比如设为 2)
  • 每收到 1 个探测包的响应 → cwnd += slow_incr(通常设为 1)
  • 一轮 RTT 内,所有发出去的探测包都收到响应 → cwnd 翻倍(比如 2→4→8...)
5.3.2 扫描工具的优化:按"响应数/时间"翻倍(更灵活)
触发条件 例子(结合结构体)
累计收到 N 个响应 N = 当前 cwnd 值 → 比如 cwnd=5,收到 5 个响应 → cwnd 翻倍到 10(slow_incr=1 时刚好触发)
一轮 RTT 无丢包 只要一轮 RTT 内发出去的探测包都收到响应,不管数量多少,直接翻倍(简化逻辑)
未触发速率限制 只要 perf.is_rate_limited 为 false(无拥塞),每秒钟检查一次,cwnd 翻倍(扫描特有的时间驱动逻辑)

5.4 代码实现逻辑

cpp 复制代码
// 单主机的拥塞窗口状态(存在 HostOsScanStats 里)
struct HostOsScanStats {
    int current_cwnd;       // 当前cwnd
    int ssthresh;           // 该主机的慢启动阈值(初始=perf.initial_ssthresh)
    int resp_received;      // 本轮已收到的响应数
};

// 收到一个探测包响应时,更新cwnd
void on_probe_response(HostOsScanStats *hss) {
    // 1. 只在慢启动阶段(current_cwnd < ssthresh)才会触发翻倍逻辑
    if (hss->current_cwnd < hss->ssthresh) {
        // 2. 每收到1个响应,cwnd += slow_incr(=1)
        hss->current_cwnd += perf.slow_incr;
        hss->resp_received++;
        
        // 3. 本轮响应数 == 当前cwnd → 触发翻倍(简化逻辑)
        if (hss->resp_received == hss->current_cwnd) {
            hss->current_cwnd *= 2; // cwnd翻倍
            hss->resp_received = 0; // 重置响应计数
            printf("cwnd 翻倍:%d → %d\n", hss->current_cwnd/2, hss->current_cwnd);
        }
        
        // 4. 不能超过max_cwnd(上限)
        if (hss->current_cwnd > perf.max_cwnd) {
            hss->current_cwnd = perf.max_cwnd;
        }
    } else {
        // 拥塞避免阶段:不再翻倍,每次只加 ca_incr(比如0.5)
        hss->current_cwnd += perf.ca_incr;
    }
}

5.5 cwnd 停止翻倍的条件

翻倍不是无限的,满足以下任一条件就会停止:

  1. 达到慢启动阈值current_cwnd >= ssthreshinitial_ssthresh)→ 切换到拥塞避免阶段,不再翻倍
  2. 达到最大cwndcurrent_cwnd >= max_cwnd → 无论如何都不涨了
  3. 出现丢包/拥塞 :只要有探测包丢包 → 触发 group_drop_cwnd_divisor/host_drop_ssthresh_divisor,cwnd 直接减半(比如8→4),重新从慢启动开始
  4. 触发速率限制is_rate_limited = true → 强制停止增长,甚至降低cwnd

6. 初始化方法:动态配置拥塞控制参数

6.1 核心前置知识

在看代码前,先搞懂两个关键变量:

  1. o:全局的 NmapOps 结构体实例,存储用户输入的扫描配置(比如 -T4 时序等级、--min-parallelism 最小并发、--max-parallelism 最大并发)
  2. o.timing_level:Nmap 的时序等级(-T0~-T5),数字越大扫描越激进(越快),是这段代码中最核心的控制变量

6.2 逐行拆解代码

cpp 复制代码
void scan_performance_vars::init() {
  /* TODO: 后续要优化这些值,至少要受 -T 时序参数影响 */
  // 1. 初始化最小拥塞窗口 low_cwnd
  low_cwnd = o.min_parallelism ? o.min_parallelism : 1;
  /* 逻辑:
     - 如果用户通过 --min-parallelism 设置了最小并发数 → low_cwnd 用这个值;
     - 否则默认 1(哪怕网络再差,至少允许1个并发探测包);
     - 作用:保证扫描不会完全停摆,是速率的"底线"。
   */

  // 2. 初始化最大拥塞窗口 max_cwnd
  max_cwnd = MAX(low_cwnd, o.max_parallelism ? o.max_parallelism : 300);
  /* 逻辑:
     - MAX() 是取最大值的宏;
     - 优先用用户设置的 --max-parallelism,否则默认 300;
     - 但必须 ≥ low_cwnd(比如用户设 min=5、max=3 → 最终 max_cwnd=5,避免矛盾);
     - 作用:限制并发的"上限",防止过度发包导致网络拥塞。
   */

  // 3. 初始化全局初始拥塞窗口 group_initial_cwnd
  group_initial_cwnd = box(low_cwnd, max_cwnd, 10);
  /* 逻辑:
     - box(a, b, c) 是 Nmap 自定义宏 → 把 c 限制在 [a, b] 区间内(即 max(a, min(b, c)));
     - 这里是把初始全局 cwnd 设为 10,但不能小于 low_cwnd、不能大于 max_cwnd;
     - 例子:如果 low_cwnd=5、max_cwnd=20 → group_initial_cwnd=10;
            如果 low_cwnd=15、max_cwnd=20 → group_initial_cwnd=15(不能小于low);
     - 作用:给所有主机的总并发设置一个"初始值",平衡保守和激进。
   */

  // 4. 初始化单主机初始拥塞窗口 host_initial_cwnd
  host_initial_cwnd = group_initial_cwnd;
  /* 逻辑:
     - 单主机的初始 cwnd 和全局初始值一致(简化设计);
     - 后续可根据单主机的丢包/响应情况单独调整,不影响全局;
     - 作用:单主机扫描的"初始油门",和全局保持一致避免局部过载。
   */

  // 5. 初始化慢启动增量 slow_incr
  slow_incr = 1;
  /* 逻辑:
     - 慢启动阶段,每收到1个响应,cwnd 加1(固定值);
     - 这是标准 TCP 慢启动的经典配置,保证指数级增长(翻倍)的基础;
     - 作用:控制慢启动的"提速步长"。
   */

  // 6. 初始化拥塞避免增量 ca_incr(受时序等级影响)
  /* 拥塞窗口在激进时序下增长更快 */
  if (o.timing_level < 4)
    ca_incr = 1;
  else
    ca_incr = 2;
  /* 逻辑:
     - o.timing_level(-T0~-T5):<4 是保守/普通模式(-T0~T3),≥4 是激进模式(-T4/T5);
     - 保守模式:拥塞避免阶段 cwnd 每次加1(平稳增长);
     - 激进模式:每次加2(更快增长,适配快速扫描需求);
     - 作用:时序越激进,拥塞避免阶段的提速越快。
   */

  // 7. 初始化 cwnd 增量最大缩放因子 cc_scale_max
  cc_scale_max = 50;
  /* 逻辑:
     - 限制 cwnd 增量的"最大倍数"(比如本来该加1,缩放后最多加50);
     - 防止极端情况下 cwnd 疯长(比如网络突然变通畅);
     - 作用:给增量加"天花板",避免失控。
   */

  // 8. 初始化初始慢启动阈值 initial_ssthresh
  initial_ssthresh = 75;
  /* 逻辑:
     - cwnd 超过75就从慢启动(翻倍增长)切换到拥塞避免(缓慢增长);
     - 固定值,后续可根据丢包动态调整;
     - 作用:慢启动和拥塞避免的"切换临界点"。
   */

  // 9. 初始化全局丢包时 cwnd 除数 group_drop_cwnd_divisor
  group_drop_cwnd_divisor = 2.0;
  /* 逻辑:
     - 只要有丢包,全局 cwnd 直接除以2(减半);
     - 标准 TCP 拥塞控制的"快速恢复"策略,快速降速避免进一步拥塞;
     - 作用:丢包时的"刹车力度"(全局)。
   */

  // 10. 初始化 ssthresh 除数(受时序等级影响)
  /* 根据时序等级调整 ssthresh 的下降幅度 */
  double ssthresh_divisor;
  if (o.timing_level <= 3)
    ssthresh_divisor = (3.0 / 2.0); // 1.5
  else if (o.timing_level <= 4)
    ssthresh_divisor = (4.0 / 3.0); // ~1.333
  else
    ssthresh_divisor = (5.0 / 4.0); // 1.25
  group_drop_ssthresh_divisor = ssthresh_divisor;
  host_drop_ssthresh_divisor = ssthresh_divisor;
  /* 逻辑:
     - 时序越保守(≤3),ssthresh 下降越多(除以1.5)→ 更谨慎,后续难进入慢启动;
     - 时序越激进(≥5),ssthresh 下降越少(除以1.25)→ 更大胆,快速恢复提速;
     - 例子:ssthresh=75,保守模式丢包后 → 75/1.5=50;激进模式 → 75/1.25=60;
     - 作用:丢包后调整"慢启动阈值",时序越激进,恢复越快。
   */
}

6.3 实际场景示例

假设用户执行扫描命令:nmap -T4 --min-parallelism=2 --max-parallelism=200 192.168.1.0/24

  • o.timing_level=4o.min_parallelism=2o.max_parallelism=200
  • 初始化后关键参数值:
参数名 计算过程 最终值
low_cwnd o.min_parallelism=2 2
max_cwnd MAX(2, 200) 200
group_initial_cwnd box(2, 200, 10) 10
host_initial_cwnd = group_initial_cwnd 10
slow_incr 固定1 1
ca_incr o.timing_level=4 → 2 2
initial_ssthresh 固定75 75
group_drop_cwnd_divisor 固定2.0 2.0
ssthresh_divisor o.timing_level=4 → 4/3 ~1.333

扫描时的表现:

  1. 初始全局 cwnd=10,单主机 cwnd=10
  2. 慢启动阶段:每收到1个响应,cwnd+1 → 快速翻倍到75
  3. 拥塞避免阶段:cwnd 每次+2(比保守模式快)
  4. 丢包时:全局 cwnd 减半(10→5),ssthresh 从75→56.25(75/1.333),恢复更快

7. TCP拥塞控制四大算法:网络传输的基石

7.1 慢启动 Slow Start

  • 刚建连时cwnd = 1 MSS
  • 规则每收到一个 ACK,cwnd += 1
  • 效果一个 RTT 后 cwnd 翻倍 ,呈指数增长
  • 直到cwnd >= ssthresh,进入拥塞避免

目的:慢慢试探网络,不一开始就打满。

7.2 拥塞避免 Congestion Avoidance

  • 规则每个 RTT,cwnd 只 +1
  • 效果线性缓慢增长
  • 一直增长到出现丢包/超时

加法增大 AI(Additive Increase)

7.3 快速重传 Fast Retransmit

  • 收到 3 个重复 ACK → 判定丢包
  • 不等超时,直接重传丢失的包
  • 比超时重传更快、更平滑

7.4 快速恢复 Fast Recovery(配合快速重传)

收到 3 个重复 ACK 时:

  1. ssthresh = cwnd / 2
  2. cwnd = cwnd / 2
  3. 直接进入拥塞避免,不再回到 cwnd=1

乘法减小 MD(Multiplicative Decrease)

7.5 超时重传(最严厉)

一旦定时器超时

  1. ssthresh = cwnd / 2
  2. cwnd = 1
  3. 重新走慢启动

网络拥塞严重,直接"跪到底"。

7.6 一句话串起整个流程

  1. 刚开始:慢启动(指数涨)
  2. 过阈值:拥塞避免(线性涨)
  3. 丢包但收到 3 个重复 ACK:快速重传 + 快速恢复
  4. 直接超时:窗口砍半 + 重置为1,重走慢启动

7.7 最经典口诀

慢启动翻倍,拥塞避免加一
三次ACK快重传,快速恢复不回一
一旦超时全重来,窗口直接降到一


8. MSS概念:TCP报文的最大数据长度

8.1 一句话定义

MSS = Maximum Segment Size

= 一个 TCP 段里,纯数据部分的最大字节数。

公式:

复制代码
MSS ≈ MTU - IP头(20) - TCP头(20)

日常以太网里:

  • MTU = 1500
  • MSS ≈ 1460 字节

8.2 用最通俗的比喻

  • 一辆货车:整个 IP 包
  • 车头:IP 头 + TCP 头
  • 车厢里的货:TCP 数据

MSS = 车厢最多能装多少货

8.3 和拥塞控制的关系

在 TCP 拥塞控制里:

  • cwnd(拥塞窗口)的单位经常就是 多少个 MSS
  • 比如:
    • cwnd = 1 → 一次最多发 1 个 MSS 的数据
    • cwnd = 8 → 一次最多发 8 个 MSS 的数据

所以文章里说:

慢启动 cwnd 从 1 开始指数增长

这里的 1,就是 1 个 MSS

8.4 简单记忆

MSS = TCP 一次能发的"最大纯数据长度"
cwnd = 一次能发几个 MSS


9. 时间计算函数:扫描耗时的精确计量

9.1 核心作用

这个函数的核心功能是:计算从 Nmap 扫描启动到"当前时刻"的耗时,返回以"秒"为单位的浮点值(比如 10.5 秒、0.23 秒),是扫描过程中"计时、速率控制、超时判断"的基础工具函数。

9.2 代码实现

cpp 复制代码
// ===================== 计算扫描启动后的耗时(秒) =====================
// now: 可选参数,传入当前时间(避免重复调用gettimeofday)
float NmapOps::TimeSinceStart(const struct timeval *now) {
    struct timeval tv;
    if (!now)  // 未传入当前时间,则获取系统当前时间
        gettimeofday(&tv, NULL);
    else tv = *now;

    // 计算当前时间与启动时间的差值(浮点秒数)
    return TIMEVAL_FSEC_SUBTRACT(tv, start_time);
}

9.3 关键数据类型

标识符/类型 含义
struct timeval Linux/Unix 系统的"高精度时间结构体",存储秒+微秒,精度达 1 微秒(1e-6 秒)
start_time NmapOps 类的成员变量,记录扫描启动时的时间
now 可选入参:如果外部已经获取过当前时间,直接传进来复用,避免重复系统调用
gettimeofday(&tv, NULL) 系统调用:获取当前系统的高精度时间

9.4 核心宏 TIMEVAL_FSEC_SUBTRACT

cpp 复制代码
// 自定义宏:计算两个 timeval 的差值(t1 - t0),返回浮点秒数
#define TIMEVAL_FSEC_SUBTRACT(t1, t0) \
    ((t1.tv_sec - t0.tv_sec) + (t1.tv_usec - t0.tv_usec) / 1000000.0)

拆解计算逻辑:

  1. t1.tv_sec - t0.tv_sec:计算"秒数差值"
  2. (t1.tv_usec - t0.tv_usec) / 1000000.0:计算"微秒差值"并转换成秒
  3. 两者相加:得到高精度浮点秒数

9.5 实际使用场景

场景1:打印扫描耗时
cpp 复制代码
float elapsed = o.TimeSinceStart(NULL);
printf("扫描耗时:%.2f 秒\n", elapsed); // 输出:扫描耗时:10.50 秒
场景2:速率控制
cpp 复制代码
float elapsed = o.TimeSinceStart(&current_tv);
int sent_count = 150;
float send_rate = sent_count / elapsed;
if (send_rate > 100) {
    usleep(10000); // 速率超限,休眠10毫秒
}
场景3:超时判断
cpp 复制代码
float host_elapsed = o.TimeSinceStart(NULL);
if (host_elapsed > 30.0) {
    printf("主机扫描超时(耗时%.2f秒),放弃\n", host_elapsed);
    return;
}

9.6 关键设计细节

  1. 可选参数 now 的价值 :避免重复调用 gettimeofday(系统调用比普通函数慢)
  2. 浮点秒数的优势:保留了微秒精度,适合高精度的速率控制、超时判断
  3. 鲁棒性 :即使 nowNULL,也能自动兜底获取当前时间

10. 抓包器初始化:构建高性能抓包环境

10.1 核心目标

这个函数的唯一目的:

HostOsScan(单主机 OS 扫描引擎)初始化高性能的抓包器 ------ 打开指定网卡的 pcap 抓包句柄,并设置精准的 BPF 过滤器,只抓取"和当前 OS 扫描相关的数据包"。

10.2 关键前置知识

  1. pcap:Linux/Unix 下的抓包库(WinPcap/Npcap 是Windows版本)
  2. BPF 过滤器:Berkeley Packet Filter,是内核态的数据包过滤规则,只把符合规则的包交给用户程序,能大幅减少用户态处理的数据包数量

10.3 完整执行流程

阶段1:初始化变量 + 决定过滤策略
cpp 复制代码
char pcap_filter[2048]; // 存完整BPF过滤规则
char dst_hosts[1200];   // 存"源地址列表"子串
int filterlen = 0, len;
unsigned int targetno;
// 核心判断:目标数≤20时,用"精确地址过滤";否则用"模糊过滤"
bool doIndividual = Targets.size() <= 20;
pcap_filter[0] = '\0';
阶段2:构建源地址过滤子串(仅目标数≤20时)
cpp 复制代码
if (doIndividual) {
  for (targetno = 0; targetno < Targets.size(); targetno++) {
    len = Snprintf(dst_hosts + filterlen, sizeof(dst_hosts) - filterlen,
                   "%ssrc host %s", (targetno==0)?"":" or ", 
                   Targets[targetno]->targetipstr());
    if (len < 0 || len + filterlen >= (int) sizeof(dst_hosts))
      fatal("ran out of space in dst_hosts");
    filterlen += len;
  }
  len = Snprintf(dst_hosts + filterlen, sizeof(dst_hosts) - filterlen, ")))");
  if (len < 0 || len + filterlen >= (int) sizeof(dst_hosts))
    fatal("ran out of space in dst_hosts");
}

例子 :若目标是 192.168.1.1、192.168.1.2,dst_hosts 最终会是:
src host 192.168.1.1 or src host 192.168.1.2)))

阶段3:打开pcap抓包句柄
cpp 复制代码
HOS->pd = my_pcap_open_live(
  Targets[0]->deviceName(), // 网卡名(如eth0)
  8192,                    // snaplen:只抓8KB包头
  o.spoofsource ? 1 : 0,   // 混杂模式
  pcap_selectable_fd_valid() ? 200 : 2 // 超时时间
);
if (HOS->pd == NULL)
  fatal("%s", PCAP_OPEN_ERRMSG);
阶段4:构建完整BPF过滤器 + 生效
cpp 复制代码
struct sockaddr_storage ss = Targets[0]->source();
if (ss.ss_family == AF_INET) {
  if (doIndividual) { // 目标少:精确过滤
    len = Snprintf(pcap_filter, sizeof(pcap_filter),
                   "dst host %s and (icmp or (tcp and (%s",
                   inet_ntoa(((struct sockaddr_in *)&ss)->sin_addr), dst_hosts);
  } else { // 目标多:模糊过滤
    len = Snprintf(pcap_filter, sizeof(pcap_filter),
                   "dst host %s and (icmp or tcp)",
                   inet_ntoa(((struct sockaddr_in *)&ss)->sin_addr));
  }
  if (len < 0 || len >= (int) sizeof(pcap_filter))
    fatal("ran out of space in pcap filter");

  if (o.debugging)
    log_write(LOG_PLAIN, "Packet capture filter (device %s): %s\n",
              Targets[0]->deviceFullName(), pcap_filter);
  set_pcap_filter(Targets[0]->deviceFullName(), HOS->pd, pcap_filter);
}

完整过滤器例子 (目标少+本机IP=192.168.1.100):
dst host 192.168.1.100 and (icmp or (tcp and (src host 192.168.1.1 or src host 192.168.1.2))))

过滤器含义:只抓取"目的地址是本机、协议是ICMP,或协议是TCP且源地址是扫描目标"的包

10.4 核心设计亮点

  1. 性能优先

    • 内核态BPF过滤:减少用户态处理的数据包数量
    • 分策略过滤:目标少则精确过滤,目标多则模糊过滤
    • 只抓包头(snaplen=8192):节省内存和CPU
  2. 安全可靠

    • 所有字符串拼接用 Snprintf + 越界检查
    • 抓包句柄打开失败直接终止
    • 括号配对严格检查
  3. 兼容性/鲁棒性

    • 仅处理IPv4,IPv6分支留空
    • 调试模式打印过滤器
    • 依赖封装的 my_pcap_open_live

11. 字符串安全拼接:Snprintf详解

11.1 核心作用

这行代码的唯一目的是:把当前遍历到的目标IP,以"src host IP"的格式(首个IP不加前缀,后续IP加" or "前缀),安全地追加到 dst_hosts 缓冲区的指定位置

11.2 完整代码

cpp 复制代码
len = Snprintf(
  dst_hosts + filterlen, // 参数1:写入的起始地址
  sizeof(dst_hosts) - filterlen, // 参数2:最大可写入的字符数
  "%ssrc host %s", // 参数3:格式化模板
  (targetno == 0)? "" : " or ", // 参数4:第一个占位符的取值(前缀)
  Targets[targetno]->targetipstr() // 参数5:第二个占位符的取值(目标IP)
);

11.3 逐个参数拆解

参数1:dst_hosts + filterlen ------ 决定"从缓冲区的哪个位置开始写"
  • 本质:这是一个指针偏移操作
  • 第一次循环(targetno=0):filterlen=0dst_hosts + 0 = 缓冲区起始地址
  • 第二次循环(targetno=1):假设第一次写入了16个字符,filterlen 已更新为16 → dst_hosts + 16 = 缓冲区第17个字符的位置
  • 核心意义:实现"追加写入",而不是每次都从头覆盖
参数2:sizeof(dst_hosts) - filterlen ------ 决定"最多能写多少字符"
  • 本质:计算缓冲区的剩余可用空间
  • Snprintf 的关键特性:会严格遵守这个数值,最多只写「该数值-1」个字符(留1个位置给 \0
  • 核心意义:从根源避免"缓冲区溢出"
参数3:"%ssrc host %s" ------ 决定"写入的内容格式"
  • 本质:这是格式化模板字符串 ,里面的 %s 是"占位符"
  • 模板拆解:
    • 第一个 %s:替换成"前缀"(要么是空字符串,要么是" or ")
    • src host :固定字符串(BPF过滤器的语法)
    • 第二个 %s:替换成目标IP字符串
  • 核心意义:统一所有目标IP的格式,符合BPF过滤器的语法要求
参数4:(targetno == 0)? "" : " or " ------ 决定"是否加'or'前缀"
  • 本质:这是三元运算符
  • 第一次循环(targetno=0):前缀是空字符串 → 直接写"src host 192.168.1.1"
  • 第二次循环(targetno=1):前缀是" or " → 写" or src host 192.168.1.2"
  • 核心意义:保证拼接后的字符串语法正确
参数5:Targets[targetno]->targetipstr() ------ 提供"要拼接的目标IP字符串"
  • 本质:调用 Target 类的成员函数 targetipstr(),返回当前遍历到的目标主机的IP地址字符串
  • 核心意义:动态获取每个目标的IP,不用硬编码

11.4 核心设计亮点

  1. 安全优先 :用 Snprintf + 剩余空间计算,彻底杜绝缓冲区溢出
  2. 动态追加:通过指针偏移和已用长度记录,实现多个IP的无缝拼接
  3. 语法合规:三元运算符控制"or"前缀,保证BPF过滤器能识别
  4. 灵活性:适配任意数量的目标(≤20)

12. 跨平台接口名转换:Windows平台的特殊处理

12.1 函数整体说明

c 复制代码
int DnetName2PcapName(const char *dnetdev, char *pcapdev, int pcapdevlen)

功能:

  • 输入:dnetdev(dnet 库使用的网卡名)
  • 输出:pcapdev(对应的 pcap 格式设备名)
  • 返回值:
    • 1:成功找到并写入 pcapdev
    • 0:找不到对应的 pcap 设备名

此转换 仅在 Windows 上需要,因为 Windows 下 dnet 和 pcap 使用的接口命名格式不同。

12.2 静态缓存结构

c 复制代码
static struct NameCorrelationCache {
  char dnetd[64];
  char pcapd[128];
} *NCC = NULL;
static int NCCsz = 0;
static int NCCcapacity = 0;
  • NCC:已成功转换的映射缓存(dnet → pcap)
  • NCCsz:当前缓存数量
  • NCCcapacity:当前缓存容量
c 复制代码
static struct NameNotFoundCache {
  char dnetd[64];
} *NNFC = NULL;
static int NNFCsz = 0;
static int NNFCcapacity = 0;
  • NNFC:记录 查过但找不到对应 pcap 名称 的 dnet 设备名,避免重复查询

12.3 初始化缓存

c 复制代码
if (!NCC) {
  NCCcapacity = 5;
  NCC = (struct NameCorrelationCache *) safe_zalloc(NCCcapacity * sizeof(*NCC));
  NCCsz = 0;
}
if (!NNFC) {
  NNFCcapacity = 5;
  NNFC = (struct NameNotFoundCache *) safe_zalloc(NNFCcapacity * sizeof(*NNFC));
  NNFCsz = 0;
}
  • 第一次调用时分配初始缓存空间(5 条)
  • 使用 safe_zalloc,表示分配并清零,防止脏数据

12.4 查询缓存

第一步:查成功缓存
c 复制代码
for (i = 0; i < NCCsz; i++) {
  if (strcmp(NCC[i].dnetd, dnetdev) == 0) {
    Strncpy(pcapdev, NCC[i].pcapd, pcapdevlen);
    return 1;
  }
}

如果该 dnetdev 已经转换过,直接从缓存取值,避免再次调用系统接口。

第二步:查失败缓存
c 复制代码
for (i = 0; i < NNFCsz; i++) {
  if (strcmp(NNFC[i].dnetd, dnetdev) == 0) {
    return 0;
  }
}

如果之前已经确认这个设备名 找不到对应的 pcap 名称,直接返回失败。

12.5 调用系统接口转换

c 复制代码
if (intf_get_pcap_devname(dnetdev, tmpdev, sizeof(tmpdev)) != 0) {
    // 转换失败:将该dnet名称加入"未找到"缓存
    if (NNFCsz >= NNFCcapacity) {
      NNFCcapacity <<= 2;  // 容量乘 4
      NNFC = (struct NameNotFoundCache *) safe_realloc(NNFC,
                                                       NNFCcapacity * sizeof(*NNFC));
    }
    Strncpy(NNFC[NNFCsz].dnetd, dnetdev, sizeof(NNFC[0].dnetd));
    NNFCsz++;
    return 0;
}

12.6 转换成功处理

c 复制代码
if (NCCsz >= NCCcapacity) {
  NCCcapacity <<= 2;
  NCC = (struct NameCorrelationCache *) safe_realloc(NCC,
                                                     NCCcapacity * sizeof(*NCC));
}
Strncpy(NCC[NCCsz].dnetd, dnetdev, sizeof(NCC[0].dnetd));
Strncpy(NCC[NCCsz].pcapd, tmpdev, sizeof(NCC[0].pcapd));
NCCsz++;
Strncpy(pcapdev, tmpdev, pcapdevlen);
return 1;

12.7 总结

这段代码实现了:

在 Windows 下,将 dnet 接口名转换为 pcap 接口名,并通过双缓存(成功缓存 + 失败缓存)机制提高性能,避免重复查询系统接口。


13. 随机数生成:加密级安全随机源

13.1 get_random_u32() 函数

cpp 复制代码
u32 get_random_u32() {
  u32 i;
  get_random_bytes(&i, sizeof(i));
  return i;
}

核心作用:

  • 通过系统安全随机源生成高质量的32位无符号随机数
  • 依赖 get_random_bytes() 接口,保证了随机数的不可预测性
  • 函数返回的随机数直接用于设置 TCP 序列号、ACK 值等核心扫描参数

13.2 nrand_init() 函数

13.2.1 数据结构
cpp 复制代码
struct nrand_handle {
  u8    i, j;          // RC4 算法的两个索引变量(核心状态)
  u8    s[256];        // RC4 的 S 盒(256字节置换表,算法核心)
  u8    *tmp;          // 临时缓存指针
  int   tmplen;        // 临时缓存长度
};
13.2.2 完整实现
cpp 复制代码
void nrand_init(nrand_h *r) {
  u8 seed[256];
  int i;

  /* 第一步:跨平台收集高熵种子 */
#ifdef WIN32
  // Windows 平台:使用系统加密API获取安全随机种子
  HCRYPTPROV hcrypt = 0;
  CryptAcquireContext(&hcrypt, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT);
  CryptGenRandom(hcrypt, sizeof(seed), seed);
  CryptReleaseContext(hcrypt, 0);
#else
  // Linux/Unix 平台:混合多源熵值 + 读取系统随机设备
  struct timeval *tv = (struct timeval *)seed;
  int *pid = (int *)(seed + sizeof(*tv));
  int fd;

  gettimeofday(tv, NULL);
  *pid = getpid();

  if ((fd = open("/dev/urandom", O_RDONLY)) != -1 ||
      (fd = open("/dev/arandom", O_RDONLY)) != -1) {
    ssize_t n;
    do {
      errno = 0;
      n = read(fd, seed + sizeof(*tv) + sizeof(*pid),
               sizeof(seed) - sizeof(*tv) - sizeof(*pid));
    } while (n < 0 && errno == EINTR);
    close(fd);
  }
#endif

  /* 第二步:初始化 RC4 的 S 盒 */
  for (i = 0; i < 256; i++) { r->s[i] = i; };
  r->i = r->j = 0;

  /* 第三步:用种子打乱 S 盒(密钥调度算法 KSA) */
  nrand_addrandom(r, seed, 128);
  nrand_addrandom(r, seed + 128, 128);

  /* 第四步:初始化临时缓存 */
  r->tmp = NULL;
  r->tmplen = 0;

  /* 第五步:丢弃前 1KB 随机数据(消除 RC4 初始偏置) */
  nrand_get(r, seed, 256); nrand_get(r, seed, 256);
  nrand_get(r, seed, 256); nrand_get(r, seed, 256);
}

13.3 HCRYPTPROV 类型

HCRYPTPROV 本质上是 Windows CryptoAPI 中的一个句柄类型,你可以把它理解成:

  • 一个"加密服务上下文的身份证",用来唯一标识一个打开的、与加密服务提供程序(CSP) 的连接
  • 类比生活中的例子:你去银行办理业务,银行会给你一个排号单(句柄),凭这个排号单才能调用银行的服务

从代码层面,它的底层定义在 wincrypt.h 中:

cpp 复制代码
typedef unsigned long HCRYPTPROV;
#define NULL_PROV_HANDLE ((HCRYPTPROV)0)

13.4 CryptAcquireContext 函数

cpp 复制代码
CryptAcquireContext(&hcrypt, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT);

逐参数拆解:

参数 含义 作用
&hcrypt 输出:加密上下文句柄 函数执行成功后,会把有效的加密上下文句柄写入 hcrypt 变量中
NULL 密钥容器名称 指定"密钥容器"的名称,传NULL表示使用默认容器
NULL CSP名称 指定要使用的加密服务提供程序(CSP)的名称,传NULL表示使用系统默认的CSP
PROV_RSA_FULL CSP类型 指定要使用的CSP类型,支持RSA加密/解密、数字签名/验证、哈希计算,同时也支持生成安全随机数
CRYPT_VERIFYCONTEXT 操作标志 无需用户权限、无需访问密钥容器、无需交互式登录(比如服务/后台程序也能调用)

13.5 跨平台熵源设计思路

平台 熵源组成 安全性
Windows 系统加密API(CryptGenRandom) 最高(底层混合硬件/系统事件熵值)
Linux 时间戳 + 进程ID + /dev/urandom 高(/dev/urandom 是系统熵池的非阻塞接口)

13.6 为什么要丢弃前 1KB 数据?

RC4 算法存在初始输出偏置问题:前几百字节的随机数分布不够均匀,容易被破解。

通过调用 4 次 nrand_get 生成 1024 字节并丢弃,让 RC4 算法运行到稳定状态,后续生成的随机数才符合加密安全要求。

13.7 与上层函数的完整链路

复制代码
HostOsScan::reInitScanSystem()
  ↓
get_random_u32() → get_random_bytes()
  ↓
nrand_get()(基于 RC4 PRGA 生成随机字节)
  ↓
nrand_init()(初始化 RC4 状态,保证 nrand_get 有安全的随机源)

总结

本文档深入解析了Nmap OS扫描的核心技术,涵盖了从循环迭代器设计到随机数生成的完整技术栈。主要知识点包括:

  1. 循环迭代器:实现高效遍历未完成主机列表的机制
  2. 执行与状态分离:工业级设计模式,支持实例复用和并发安全
  3. 全局静态变量:性能统计的核心,统一收集全量扫描数据
  4. 拥塞控制结构体:扫描速率的智能调节器,动态调节探测包并发量
  5. cwnd翻倍机制:慢启动阶段的核心特征,实现指数级增长
  6. 初始化方法:动态配置拥塞控制参数,适配用户需求
  7. TCP拥塞控制四大算法:网络传输的基石,保证网络稳定性
  8. MSS概念:TCP报文的最大数据长度,拥塞控制的基础单位
  9. 时间计算函数:扫描耗时的精确计量,支持速率控制和超时判断
  10. 抓包器初始化:构建高性能抓包环境,通过BPF过滤器精准过滤
  11. 字符串安全拼接:Snprintf详解,避免缓冲区溢出
  12. 跨平台接口名转换:Windows平台的特殊处理,通过缓存机制提升性能
  13. 随机数生成:加密级安全随机源,基于RC4算法实现

这些技术共同构成了Nmap OS扫描的核心能力,使其成为工业级网络扫描工具的典范。通过深入理解这些技术,开发者可以更好地掌握网络扫描的核心原理,并将其应用到实际项目中。

相关推荐
Byron Loong13 小时前
【调试】Dump 文件分析的完整流程
windows
Geoking.15 小时前
VSCode 安装 Claude Code 插件 + ccswitch 配置 DeepSeek API 完整教程(Windows 新手向)
ide·windows·vscode
潘达斯奈基~16 小时前
Windows 下 Claude Code使用 Agent Teams 配置教程
windows
happymaker062618 小时前
Spring框架学习日记——DAY02(依赖注入的方式)
windows
honder试试19 小时前
Elasticsearch(es)在Windows系统上的安装与部署(含Kibana)
windows·elasticsearch·jenkins
IT里的交易员20 小时前
【系统】Windows 安装 uv
windows·uv
我不是立达刘宁宇21 小时前
windows密码操作
windows
Royzst1 天前
一、集合概述(前置基础)
开发语言·windows·python
时光追逐者1 天前
一款基于 C# 开发的 Windows 10/11 系统增强工具,精简、优化、定制一站完成!
开发语言·windows·c#·.net
liuhuizuikeai1 天前
菜品抽奖活动MFC+服务端
c++·windows·mfc