---
title: vidar-scan开发日志-2
date: 2026-04-01 19:58:26
tags: [golang,开发]
---

突然想起还没写后续,回忆着写一下

继上次路径爆破功能介绍后,我记忆中更新的内容。。。

路径爆破部分

在原来只是通过响应码识别的基础上,加入了 404page 的识别模型,也就是我们常说的 ai 赋能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func Predict404(text string) (string, error) {
relativePath := "src/model/model_404.bin"

modelPath, err := filepath.Abs(relativePath)
if err != nil {
return "", fmt.Errorf("failed to load model: %v", err)
}

model := fasttext.Open(modelPath)
defer model.Close()

preds, err := model.Predict(text)
if err != nil {
return "", err
}

if preds == nil || len(preds) == 0 {
return "", errors.New("no prediction")
}

pred1, pred2 := preds[0], preds[1]

if pred1.Probability > pred2.Probability {
return pred1.Label, nil
} else {
return pred2.Label, nil
}
}

很可惜,这部分是朋友写的,我只负责用,不是很懂,但是好歹也是解决了一些大网站存在的软 404 页面

端口扫描

之后重点应该转到端口扫描上来了,一开始我想着,端口扫描不就是给一个个端口发包么,和路径爆破应该差别不大,之后经过一番了解和学长的指导,了解到不需要建立完整的 tcp 握手,只需要发送 syn 包探活即可

很显然现在不能直接用 client.Do 发个 http 包了,大致流程如下

  1. 路由查询:调用 routing.New() 读取内核路由表,然后 router.Route(dst) 确定访问目标 IP 时使用的网卡接口(iface)、网关(gw)和源 IP(srcIP
  2. 获取网卡对象net.InterfaceByName(iface.Name) 拿到完整网卡信息,包括 MAC 地址
  3. 打开 pcap 句柄pcap.OpenLive(iface.Name, 65535, false, pcap.BlockForever) 打开原始抓包/发包句柄
  4. ARP 解析目标 MAC:调用 resolveMAC(handle, ifaceObj, srcIP, nextHop)——构造 ARP Request 广播包,发出后等待 ARP Reply,从中提取下一跳的 MAC 地址(如果有网关就解析网关 MAC,否则解析目标主机 MAC)
  5. 设置 BPF 过滤器tcp and src host <dst> and dst host <srcIP> and dst port <srcPort>,只捕获从目标返回的、目的端口为 56789 的 TCP 包
  6. 启动监听协程go s.listenReply() 在后台持续读包

设置 worker,每个 worker 负责一个端口,通过令牌桶控制发包速率 basework.Limiter.Wait(context.Background()),且每个 worker 完成一个任务后 “休息” 1000ms

逐层构造原始 SYN 包

  1. 以太网层layers.Ethernet{SrcMAC, DstMAC, EthernetTypeIPv4}
  2. IP 层layers.IPv4{Version:4, IHL:5, TTL:64, Protocol:TCP, SrcIP, DstIP}
  3. TCP 层layers.TCP{SrcPort:56789, DstPort:目标端口, SYN:true, Window:14600}
  4. 计算校验和tcp.SetNetworkLayerForChecksum(ip4) 让 gopacket 根据 IP 伪首部自动计算 TCP 校验和
  5. 序列化gopacket.SerializeLayers(buf, opts, eth, ip4, tcp),开启 FixLengthsComputeChecksums
  6. 发送s.handle.WritePacketData(buf.Bytes()) 通过 pcap 句柄直接写入网卡

之后就是发包后等待回包,判断标志位:SYN+ACK"open"RST"close" 这里,把超时也视为 close 处理(原来一直在和超时较劲,之后学长告知相关内容,仿制在扫描器中超时和 close 几乎是一回事)所以,设置一个最大等待时间,不会一直等待

在一开始,我使用的是并发控制,之后改为了并发控制和速率控制一起,所以写了个自适应速率控制器,因为要考虑到目标服务器的性能,本机的性能,不能瞬发巨量包

每个 worker 完成任务后上传信息

1
2
3
4
5
6
7
8
9
10
func RecordResult(err error, latency time.Duration) {
StatLock.Lock()
defer StatLock.Unlock()

Stats.Total++ // 总请求数 +1
Stats.LateSum += latency // 累加延迟
if err != nil {
Stats.Err++ // 错误数 +1
}
}

此处,只有出现在 open close timeout 外的状态才会算作异常 err

此处,限制速率的具体实现是一个令牌桶,每个 worker 的任务中会有一个 Wait,只有令牌桶中有令牌时,才可以拿到令牌去运行

自适应速率的实现比较粗糙,还有待改进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
if ErrRate > _ErrRateHigh _|| AvgLatency > _RttHigh _{
Ssthresh = CurRps / 2.0
if Ssthresh < MinRps {
Ssthresh = MinRps
}

NewRps = CurRps * 0.5
if NewRps < MinRps {
NewRps = MinRps
}

SlowStart = true

fmt.Printf("[auto] CONGESTION: errRate=%.4f avgLatency=%v -> cut RPS to %.1f, ssthresh=%.1f\n",
ErrRate, AvgLatency, NewRps, Ssthresh)
} else {
if SlowStart && CurRps < Ssthresh {
NewRps = CurRps * 2

if NewRps > Ssthresh {
NewRps = Ssthresh
}
fmt.Printf("[auto] slowStart: Rps to %.1f\n", NewRps)

if NewRps >= Ssthresh {
SlowStart = false
}

} else {
step := CurRps * 0.1
if step < 5 {
step = 5
}

NewRps = CurRps + step
fmt.Printf("[auto] congestion-avoid: RPS to %.1f (+%.1f)\n", NewRps, step)

}
}

以及什么 retry 机制就不多说了

漏扫模块

本模块比较简单,依旧是传统那套依据 yaml 文件发包,多的部分可能是可以通过编辑 yaml 中加入某个字段实现获取想要的信息,其他没什么太多变化,说一下有这么个东西,但还不是很成熟