Skip to content

Commit 55f3529

Browse files
committed
feat: 添加 VShell 攻击防御模块,支持 WebSocket 升级请求、版本握手和命令模式检测,更新配置文件示例
1 parent ff9ec1a commit 55f3529

File tree

4 files changed

+622
-50
lines changed

4 files changed

+622
-50
lines changed

config.go

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,26 @@ type Config struct {
1717

1818
// GlobalConfig 全局配置
1919
type GlobalConfig struct {
20-
BufferSize int `toml:"buffer_size"`
21-
LogLevel string `toml:"log_level"`
22-
LogFile string `toml:"log_file"` // 日志文件路径,为空则只输出到控制台
23-
GeoIP GeoIPConfig `toml:"geoip"`
24-
TimeWindow TimeWindowConfig `toml:"time_window"`
20+
BufferSize int `toml:"buffer_size"`
21+
LogLevel string `toml:"log_level"`
22+
LogFile string `toml:"log_file"` // 日志文件路径,为空则只输出到控制台
23+
GeoIP GeoIPConfig `toml:"geoip"`
24+
TimeWindow TimeWindowConfig `toml:"time_window"`
25+
VShellDefense VShellDefenseConfig `toml:"vshell_defense"` // VShell 攻击防御配置
26+
}
27+
28+
// VShellDefenseConfig VShell 防御配置
29+
type VShellDefenseConfig struct {
30+
Enabled bool `toml:"enabled"` // 是否启用 VShell 防御
31+
BlockWebSocketUpgrade bool `toml:"block_websocket_upgrade"` // 拦截可疑的 WebSocket 升级请求
32+
BlockVersionHandshake bool `toml:"block_version_handshake"` // 拦截 VShell 版本握手
33+
BlockCommandPatterns bool `toml:"block_command_patterns"` // 拦截 VShell 命令模式
34+
BlockEncryptedPayloads bool `toml:"block_encrypted_payloads"` // 拦截可疑的加密载荷
35+
BlockVkeyPatterns bool `toml:"block_vkey_patterns"` // 拦截 Vkey 哈希模式
36+
BlockSuspiciousPaths bool `toml:"block_suspicious_paths"` // 拦截可疑路径
37+
CustomBlockPaths []string `toml:"custom_block_paths"` // 自定义拦截路径
38+
BlockedVkeys []string `toml:"blocked_vkeys"` // 已知恶意 Vkey 黑名单
39+
LogAttempts bool `toml:"log_attempts"` // 记录攻击尝试
2540
}
2641

2742
// GeoIPConfig GeoIP 配置
@@ -70,13 +85,13 @@ type TCPProcessorConfig struct {
7085

7186
// Processor 处理器规则
7287
type Processor struct {
73-
Path interface{} `toml:"path"` // string 或 []string
74-
MatchMode string `toml:"match_mode"` // prefix (前缀), exact (精确), regex (正则)
75-
Action string `toml:"action"` // allow, drop, rewrite, file, proxy
76-
Response string `toml:"response"` // 404, 403, 502, close (用于 drop)
77-
RewriteTo string `toml:"rewrite_to"` // 路径重写目标 (用于 rewrite)
78-
File string `toml:"file"` // 文件路径 (用于 file)
79-
ProxyTo string `toml:"proxy_to"` // 代理目标 (用于 proxy)
88+
Path interface{} `toml:"path"` // string 或 []string
89+
MatchMode string `toml:"match_mode"` // prefix (前缀), exact (精确), regex (正则)
90+
Action string `toml:"action"` // allow, drop, rewrite, file, proxy
91+
Response string `toml:"response"` // 404, 403, 502, close (用于 drop)
92+
RewriteTo string `toml:"rewrite_to"` // 路径重写目标 (用于 rewrite)
93+
File string `toml:"file"` // 文件路径 (用于 file)
94+
ProxyTo string `toml:"proxy_to"` // 代理目标 (用于 proxy)
8095
}
8196

8297
// RouteRule 路由规则(兼容旧配置)
@@ -173,11 +188,11 @@ func (c *Config) Validate() error {
173188
return fmt.Errorf("listener[%d]: backend_addr is required", i)
174189
}
175190

176-
// 检查协议类型
177-
validProtocols := map[string]bool{"tcp": true}
178-
if !validProtocols[listener.Protocol] {
179-
return fmt.Errorf("listener[%d]: protocol must be: tcp", i)
180-
} // 检查超时配置
191+
// 检查协议类型
192+
validProtocols := map[string]bool{"tcp": true}
193+
if !validProtocols[listener.Protocol] {
194+
return fmt.Errorf("listener[%d]: protocol must be: tcp", i)
195+
} // 检查超时配置
181196
if listener.Timeout.InitialRead < 0 {
182197
return fmt.Errorf("listener[%d]: timeout.initial_read cannot be negative", i)
183198
}
@@ -226,15 +241,15 @@ func (c *Config) Validate() error {
226241
func validateProcessor(proc Processor, listenerIdx int, processorType string, procIdx int) error {
227242
validActions := map[string]bool{"allow": true, "drop": true, "rewrite": true, "file": true, "proxy": true}
228243
if !validActions[proc.Action] {
229-
return fmt.Errorf("listener[%d].%s.processor[%d]: action must be one of: allow, drop, rewrite, file, proxy",
244+
return fmt.Errorf("listener[%d].%s.processor[%d]: action must be one of: allow, drop, rewrite, file, proxy",
230245
listenerIdx, processorType, procIdx)
231246
}
232247

233248
// 验证匹配模式
234249
if proc.MatchMode != "" {
235250
validModes := map[string]bool{"prefix": true, "exact": true, "regex": true}
236251
if !validModes[proc.MatchMode] {
237-
return fmt.Errorf("listener[%d].%s.processor[%d]: match_mode must be one of: prefix, exact, regex",
252+
return fmt.Errorf("listener[%d].%s.processor[%d]: match_mode must be one of: prefix, exact, regex",
238253
listenerIdx, processorType, procIdx)
239254
}
240255
}
@@ -244,22 +259,22 @@ func validateProcessor(proc Processor, listenerIdx int, processorType string, pr
244259
case "drop":
245260
validResponses := map[string]bool{"404": true, "403": true, "502": true, "close": true}
246261
if proc.Response != "" && !validResponses[proc.Response] {
247-
return fmt.Errorf("listener[%d].%s.processor[%d]: response must be one of: 404, 403, 502, close",
262+
return fmt.Errorf("listener[%d].%s.processor[%d]: response must be one of: 404, 403, 502, close",
248263
listenerIdx, processorType, procIdx)
249264
}
250265
case "rewrite":
251266
if proc.RewriteTo == "" {
252-
return fmt.Errorf("listener[%d].%s.processor[%d]: rewrite_to is required for rewrite action",
267+
return fmt.Errorf("listener[%d].%s.processor[%d]: rewrite_to is required for rewrite action",
253268
listenerIdx, processorType, procIdx)
254269
}
255270
case "file":
256271
if proc.File == "" {
257-
return fmt.Errorf("listener[%d].%s.processor[%d]: file is required for file action",
272+
return fmt.Errorf("listener[%d].%s.processor[%d]: file is required for file action",
258273
listenerIdx, processorType, procIdx)
259274
}
260275
case "proxy":
261276
if proc.ProxyTo == "" {
262-
return fmt.Errorf("listener[%d].%s.processor[%d]: proxy_to is required for proxy action",
277+
return fmt.Errorf("listener[%d].%s.processor[%d]: proxy_to is required for proxy action",
263278
listenerIdx, processorType, procIdx)
264279
}
265280
}
@@ -355,27 +370,27 @@ func validateTimeWindowConfig(tw *TimeWindowConfig) error {
355370
if tw.Timezone == "" {
356371
return fmt.Errorf("timezone is required")
357372
}
358-
373+
359374
// 验证时区
360375
if _, err := time.LoadLocation(tw.Timezone); err != nil {
361376
return fmt.Errorf("invalid timezone '%s': %w", tw.Timezone, err)
362377
}
363-
378+
364379
if tw.StartTime == "" {
365380
return fmt.Errorf("start_time is required")
366381
}
367382
if tw.EndTime == "" {
368383
return fmt.Errorf("end_time is required")
369384
}
370-
385+
371386
// 验证时间格式
372387
if _, err := time.Parse("15:04", tw.StartTime); err != nil {
373388
return fmt.Errorf("invalid start_time format '%s', expected HH:MM", tw.StartTime)
374389
}
375390
if _, err := time.Parse("15:04", tw.EndTime); err != nil {
376391
return fmt.Errorf("invalid end_time format '%s', expected HH:MM", tw.EndTime)
377392
}
378-
393+
379394
return nil
380395
}
381396

@@ -384,26 +399,26 @@ func (tw *TimeWindowConfig) IsInTimeWindow() (bool, error) {
384399
if !tw.Enabled {
385400
return true, nil
386401
}
387-
402+
388403
// 加载时区
389404
loc, err := time.LoadLocation(tw.Timezone)
390405
if err != nil {
391406
return false, fmt.Errorf("failed to load timezone: %w", err)
392407
}
393-
408+
394409
// 获取当前时间(在指定时区)
395410
now := time.Now().In(loc)
396411
currentHour := now.Hour()
397412
currentMinute := now.Minute()
398413
currentTimeInMinutes := currentHour*60 + currentMinute
399-
414+
400415
// 解析开始和结束时间
401416
startTime, _ := time.Parse("15:04", tw.StartTime)
402417
endTime, _ := time.Parse("15:04", tw.EndTime)
403-
418+
404419
startMinutes := startTime.Hour()*60 + startTime.Minute()
405420
endMinutes := endTime.Hour()*60 + endTime.Minute()
406-
421+
407422
// 检查是否在时间窗口内
408423
if startMinutes <= endMinutes {
409424
// 正常情况:start_time < end_time (如 00:00 - 11:00)

config.toml.example

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ start_time = "00:00" # 允许连接的开始时间 (HH:MM 格式)
2323
end_time = "11:00" # 允许连接的结束时间 (HH:MM 格式)
2424
# 窗口外时,现有连接保持,但新的 TCP 连接请求会被丢弃
2525

26+
# VShell 攻击防御配置
27+
# VShell 是一款流行的C2/RAT工具,此模块可检测并拦截其攻击流量
28+
[global.vshell_defense]
29+
enabled = true # 是否启用 VShell 防御
30+
block_websocket_upgrade = true # 拦截可疑的 WebSocket 升级请求 (VShell通过WebSocket通信)
31+
block_version_handshake = true # 拦截 VShell 版本握手 (如 "4.9.3" 等版本号)
32+
block_command_patterns = true # 拦截 VShell 命令模式 (conf, file, sucs 等)
33+
block_encrypted_payloads = true # 拦截可疑的加密载荷 (AES-GCM 加密数据特征)
34+
block_vkey_patterns = true # 拦截 Vkey 哈希模式 (MD5 哈希认证)
35+
block_suspicious_paths = true # 拦截可疑路径 (/ws, /websocket, /beacon 等)
36+
custom_block_paths = ["/c2", "/shell", "/cmd"] # 自定义拦截路径列表
37+
blocked_vkeys = [] # 已知恶意 Vkey 黑名单 (可添加已知的 vkey 值)
38+
log_attempts = true # 记录所有攻击尝试到日志
39+
2640
# 监听端口配置 - 可以配置多个
2741
[[listeners]]
2842
name = "http_proxy" # 监听器名称

main.go

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@ import (
1616
var (
1717
configFile = flag.String("config", "config.toml", "配置文件路径")
1818
version = flag.Bool("version", false, "显示版本信息")
19-
19+
2020
// 版本信息 (通过 -ldflags 注入)
2121
Version = "dev"
2222
BuildTime = "unknown"
2323
GitCommit = "unknown"
24-
24+
2525
// 全局 GeoIP 管理器
2626
geoipManager *GeoIPManager
27+
28+
// 全局 VShell 防御模块
29+
vshellDefense *VShellDefense
2730
)
2831

2932
func main() {
@@ -61,6 +64,12 @@ func main() {
6164
}
6265
defer geoipManager.Close()
6366

67+
// 初始化 VShell 防御模块
68+
vshellDefense = NewVShellDefense(config.Global.VShellDefense)
69+
if config.Global.VShellDefense.Enabled {
70+
log.Println("VShell defense module enabled")
71+
}
72+
6473
// 启动所有监听器
6574
var wg sync.WaitGroup
6675
for _, listenerConfig := range config.Listeners {
@@ -92,7 +101,7 @@ func setupLogging(logFilePath string) (*os.File, error) {
92101
// 创建多重写入器:同时写入文件和控制台
93102
multiWriter := io.MultiWriter(os.Stdout, logFile)
94103
log.SetOutput(multiWriter)
95-
104+
96105
// 设置日志格式:包含日期和时间
97106
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
98107

@@ -182,6 +191,43 @@ func handleConnection(clientConn net.Conn, cfg ListenerConfig, global GlobalConf
182191
clientConn.SetReadDeadline(time.Time{}) // 移除超时,支持长连接
183192
}
184193

194+
// VShell 防御检测
195+
clientIP := getIPFromAddr(clientConn.RemoteAddr().String())
196+
if vshellDefense != nil && vshellDefense.config.Enabled {
197+
// 提取路径用于检测
198+
path := ""
199+
if isHTTPRequest(initialData) {
200+
firstLineEnd := findFirstLine(initialData)
201+
if firstLineEnd > 0 {
202+
requestLine := string(initialData[:firstLineEnd])
203+
path = extractHTTPPath(requestLine)
204+
}
205+
}
206+
207+
// 执行 VShell 检测
208+
result := vshellDefense.CheckRequest(clientIP, initialData, path)
209+
if result.IsBlocked {
210+
if global.VShellDefense.LogAttempts {
211+
LogVShellAttempt(clientIP, result.BlockReason, result.ThreatLevel, result.Details)
212+
}
213+
if global.LogLevel == "debug" || global.LogLevel == "info" {
214+
log.Printf("[%s] VShell attack blocked from %s: %s (threat: %s)",
215+
cfg.Name, clientIP, result.BlockReason, result.ThreatLevel)
216+
}
217+
// 发送适当的响应
218+
if isHTTPRequest(initialData) {
219+
sendErrorResponse(clientConn, "403")
220+
}
221+
return
222+
}
223+
224+
// 检查连接是否可疑
225+
isSuspicious, score := vshellDefense.IsConnectionSuspicious(clientIP)
226+
if isSuspicious && global.LogLevel == "debug" {
227+
log.Printf("[%s] Suspicious connection from %s (score: %d)", cfg.Name, clientIP, score)
228+
}
229+
}
230+
185231
// 检测是否为 HTTP 请求
186232
isHTTP := isHTTPRequest(initialData)
187233

@@ -264,18 +310,18 @@ func handleConnection(clientConn net.Conn, cfg ListenerConfig, global GlobalConf
264310
// forwardConnection 转发连接到后端
265311
func forwardConnection(clientConn net.Conn, reader *bufio.Reader, initialData []byte,
266312
cfg ListenerConfig, global GlobalConfig, protocol string, processor *Processor) {
267-
313+
268314
// 连接后端
269315
var backendConn net.Conn
270316
var err error
271-
317+
272318
if cfg.Timeout.Enabled && cfg.Timeout.ConnectBackend > 0 {
273319
backendConn, err = net.DialTimeout("tcp", cfg.BackendAddr,
274320
time.Duration(cfg.Timeout.ConnectBackend)*time.Second)
275321
} else {
276322
backendConn, err = net.Dial("tcp", cfg.BackendAddr)
277323
}
278-
324+
279325
if err != nil {
280326
log.Printf("[%s] Failed to connect to backend %s: %v",
281327
cfg.Name, cfg.BackendAddr, err)
@@ -314,7 +360,7 @@ func forwardConnection(clientConn net.Conn, reader *bufio.Reader, initialData []
314360
go func() {
315361
defer wg.Done()
316362
buf := make([]byte, global.BufferSize)
317-
363+
318364
// 先读取 reader 缓冲中的剩余数据
319365
for reader.Buffered() > 0 {
320366
n, err := reader.Read(buf)
@@ -327,7 +373,7 @@ func forwardConnection(clientConn net.Conn, reader *bufio.Reader, initialData []
327373
return
328374
}
329375
}
330-
376+
331377
// 然后直接从连接读取
332378
io.CopyBuffer(backendConn, clientConn, buf)
333379
if tcpConn, ok := backendConn.(*net.TCPConn); ok {
@@ -389,42 +435,42 @@ func rewriteHTTPPath(data []byte, fromPath, toPath string) []byte {
389435
break
390436
}
391437
}
392-
438+
393439
if firstLineEnd < 0 {
394440
return data
395441
}
396-
442+
397443
requestLine := string(data[:firstLineEnd])
398444
parts := strings.Split(requestLine, " ")
399-
445+
400446
// 检查是否需要重写路径
401447
if len(parts) >= 3 {
402448
path := parts[1]
403-
449+
404450
// 如果路径匹配 fromPath,则重写为 toPath
405451
if strings.HasPrefix(path, fromPath) {
406452
newPath := toPath + strings.TrimPrefix(path, fromPath)
407453
parts[1] = newPath
408-
454+
409455
// 重新构造请求行
410456
newRequestLine := strings.Join(parts, " ")
411-
457+
412458
// 构造新的请求数据
413459
result := make([]byte, 0, len(data))
414460
result = append(result, []byte(newRequestLine)...)
415461
result = append(result, data[firstLineEnd:]...)
416-
462+
417463
return result
418464
}
419465
}
420-
466+
421467
return data
422468
}
423469

424470
// sendErrorResponse 发送错误响应
425471
func sendErrorResponse(conn net.Conn, responseType string) {
426472
var response string
427-
473+
428474
switch responseType {
429475
case "404":
430476
response = "HTTP/1.1 404 Not Found\r\n" +
@@ -450,7 +496,7 @@ func sendErrorResponse(conn net.Conn, responseType string) {
450496
default:
451497
return
452498
}
453-
499+
454500
conn.Write([]byte(response))
455501
}
456502

0 commit comments

Comments
 (0)