本文内容仅供安全研究与学习,请严格遵守相关法律法规。
本文参考了 《新手如何快速做到免杀fscan》 一文。
环境准备
需要一台 Windows 10 和一台 Linux 机器(或者仅用 Windows 10 也可,注意命令行转换)。
Linux 环境准备
首先,在 Linux 上安装和配置 Go 语言环境。
- 下载 Go 1.21.13:
wget https://go.dev/dl/go1.21.13.linux-amd64.tar.gz
- 解压到
/usr/local:
sudo tar -C /usr/local -xzf go1.21.13.linux-amd64.tar.gz
- 编辑 Shell 配置文件(如
~/.zshrc 或 ~/.bashrc),添加以下环境变量:
export GO111MODULE=on
export GOPATH=$HOME/go
export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
export GOPROXY=https://goproxy.cn,direct
export GOSUMDB=off
- 使配置生效:
source ~/.zshrc
- 安装代码混淆工具
garble:
go install mvdan.cc/garble@v0.12.1
- 将
garble 工具复制到你的工作目录(例如 /root/Downloads):
cp /root/go/bin/* /root/Downloads
如果无法使用 wget,可手动下载 Go 安装包。通过 go env | grep GOPATH 确认 garble 的安装位置,以便后续将其移动到 FSCAN 项目目录中。

Windows 环境准备
在 Windows 上获取 FSCAN 源码并进行初步整理。
- 克隆 FSCAN 仓库:
git clone https://github.com/shadow1ng/fscan.git
- 使用 VS Code 打开项目。为了更好地组织代码,新建一个名为
scan 的文件夹,将原有的四个功能文件夹(如 Common, Core, Plugins, WebScan)移动进去。

修改 go.mod 文件
接下来,需要修改项目的模块名称和引用路径,这是进行代码混淆和自定义编译的前提。
- 打开
go.mod 文件,将模块名从 github.com/shadow1ng/fscan 修改为你自定义的名称,例如 OpsTool。


- 在 VS Code 中,选中
scan 文件夹,使用全局搜索(Ctrl+Shift+F)功能,搜索 github.com/shadow1ng/fscan。

- 将其全部替换为新的模块路径,例如
OpsTool/scan。
- 继续查找项目中其他 Go 文件是否还包含
fscan 字符串,并一并修改。

修复 Cassandra.go 文件
garble 工具在混淆代码时,可能会遇到因结构体类型不匹配而导致的编译错误。为了解决这个问题,需要重写 scan/Plugins/Cassandra.go 文件。以下是完整的替换代码:
package Plugins
import (
"context"
"fmt"
"github.com/gocql/gocql"
"OpsTool/scan/Common"
"strconv"
"strings"
"sync"
"time"
)
// =======================
// 类型定义(新增,garble-safe)
// =======================
// cassandraSessionResult 用于会话创建结果
type cassandraSessionResult struct {
session *gocql.Session
err error
}
// cassandraQueryResult 用于查询测试结果
type cassandraQueryResult struct {
success bool
err error
}
// =======================
// 原有结构体
// =======================
// CassandraCredential 表示一个Cassandra凭据
type CassandraCredential struct {
Username string
Password string
}
// CassandraScanResult 表示扫描结果
type CassandraScanResult struct {
Success bool
IsAnonymous bool
Error error
Credential CassandraCredential
}
// =======================
// 扫描主逻辑
// =======================
func CassandraScan(info *Common.HostInfo) (tmperr error) {
if Common.DisableBrute {
return
}
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
ctx, cancel := context.WithTimeout(
context.Background(),
time.Duration(Common.GlobalTimeout)*time.Second,
)
defer cancel()
// 先尝试无认证
Common.LogDebug("尝试无认证访问...")
anonymousCredential := CassandraCredential{}
anonymousResult := tryCassandraCredential(
ctx, info, anonymousCredential, Common.Timeout, Common.MaxRetries,
)
if anonymousResult.Success {
saveCassandraSuccess(info, target, anonymousResult.Credential, true)
return nil
}
credentials := generateCassandraCredentials(
Common.Userdict["cassandra"],
Common.Passwords,
)
Common.LogDebug(fmt.Sprintf(
"开始尝试用户名密码组合 (用户:%d 密码:%d 组合:%d)",
len(Common.Userdict["cassandra"]),
len(Common.Passwords),
len(credentials),
))
result := concurrentCassandraScan(
ctx, info, credentials, Common.Timeout, Common.MaxRetries,
)
if result != nil {
saveCassandraSuccess(info, target, result.Credential, false)
}
return nil
}
// =======================
// 工具函数
// =======================
func generateCassandraCredentials(users, passwords []string) []CassandraCredential {
var credentials []CassandraCredential
for _, user := range users {
for _, pass := range passwords {
actualPass := strings.Replace(pass, "{user}", user, -1)
credentials = append(credentials, CassandraCredential{
Username: user,
Password: actualPass,
})
}
}
return credentials
}
func concurrentCassandraScan(
ctx context.Context,
info *Common.HostInfo,
credentials []CassandraCredential,
timeoutSeconds int64,
maxRetries int,
) *CassandraScanResult {
maxConcurrent := Common.ModuleThreadNum
if maxConcurrent <= 0 {
maxConcurrent = 10
}
if maxConcurrent > len(credentials) {
maxConcurrent = len(credentials)
}
var wg sync.WaitGroup
resultChan := make(chan *CassandraScanResult, 1)
workChan := make(chan CassandraCredential, maxConcurrent)
scanCtx, scanCancel := context.WithCancel(ctx)
defer scanCancel()
for i := 0; i < maxConcurrent; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for cred := range workChan {
select {
case <-scanCtx.Done():
return
default:
result := tryCassandraCredential(
scanCtx, info, cred, timeoutSeconds, maxRetries,
)
if result.Success {
select {
case resultChan <- result:
scanCancel()
default:
}
return
}
}
}
}()
}
go func() {
for i, cred := range credentials {
select {
case <-scanCtx.Done():
break
default:
Common.LogDebug(fmt.Sprintf(
"[%d/%d] 尝试: %s:%s",
i+1, len(credentials), cred.Username, cred.Password,
))
workChan <- cred
}
}
close(workChan)
}()
go func() {
wg.Wait()
close(resultChan)
}()
select {
case result := <-resultChan:
return result
case <-ctx.Done():
return nil
}
}
// =======================
// 核心:连接与验证(已修复 garble 问题)
// =======================
func tryCassandraCredential(
ctx context.Context,
info *Common.HostInfo,
credential CassandraCredential,
timeoutSeconds int64,
maxRetries int,
) *CassandraScanResult {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
select {
case <-ctx.Done():
return &CassandraScanResult{
Success: false,
Error: ctx.Err(),
Credential: credential,
}
default:
connCtx, cancel := context.WithTimeout(
ctx, time.Duration(timeoutSeconds)*time.Second,
)
success, err := CassandraConn(
connCtx, info, credential.Username, credential.Password,
)
cancel()
if success {
return &CassandraScanResult{
Success: true,
IsAnonymous: credential.Username == "" && credential.Password == "",
Credential: credential,
}
}
lastErr = err
}
}
return &CassandraScanResult{
Success: false,
Error: lastErr,
Credential: credential,
}
}
func CassandraConn(
ctx context.Context,
info *Common.HostInfo,
user, pass string,
) (bool, error) {
cluster := gocql.NewCluster(info.Host)
cluster.Port, _ = strconv.Atoi(info.Ports)
cluster.Timeout = time.Duration(Common.Timeout) * time.Second
cluster.ConnectTimeout = cluster.Timeout
cluster.ProtoVersion = 4
cluster.Consistency = gocql.One
if user != "" || pass != "" {
cluster.Authenticator = gocql.PasswordAuthenticator{
Username: user,
Password: pass,
}
}
sessionChan := make(chan cassandraSessionResult, 1)
go func() {
session, err := cluster.CreateSession()
select {
case <-ctx.Done():
if session != nil {
session.Close()
}
case sessionChan <- cassandraSessionResult{session, err}:
}
}()
var session *gocql.Session
select {
case result := <-sessionChan:
if result.err != nil {
return false, result.err
}
session = result.session
case <-ctx.Done():
return false, ctx.Err()
}
defer session.Close()
queryChan := make(chan cassandraQueryResult, 1)
go func() {
var tmp string
err := session.Query(
"SELECT peer FROM system.peers",
).WithContext(ctx).Scan(&tmp)
if err != nil {
err = session.Query(
"SELECT now() FROM system.local",
).WithContext(ctx).Scan(&tmp)
}
select {
case <-ctx.Done():
case queryChan <- cassandraQueryResult{err == nil, err}:
}
}()
select {
case result := <-queryChan:
return result.success, result.err
case <-ctx.Done():
return false, ctx.Err()
}
}
// =======================
// 保存结果
// =======================
func saveCassandraSuccess(
info *Common.HostInfo,
target string,
credential CassandraCredential,
isAnonymous bool,
) {
Common.LogSuccess(fmt.Sprintf(
"Cassandra %s 成功 (%s)",
target,
map[bool]string{true: "匿名", false: "弱口令"}[isAnonymous],
))
Common.SaveResult(&Common.ScanResult{
Time: time.Now(),
Type: Common.VULN,
Target: info.Host,
Status: "vulnerable",
})
}
首次尝试编译
将修改好的整个 FSCAN 项目文件夹上传到 Linux 机器,并将之前准备好的 garble 工具也复制到该项目根目录下。

在项目目录下执行首次混淆编译命令,测试修改是否影响基础编译功能:
./garble -tiny -literals -seed=random build -ldflags="-w -s" main.go

如果编译没有报错,并且生成的 main 二进制文件可以正常运行,则证明我们之前的代码修改没有破坏核心功能。接下来,我们将进行更深度的修改,目标是生成一个能够规避常见杀毒软件检测的 Windows 可执行文件。
去除明显的特征与提示信息
为了避免杀软通过帮助信息、版本信息等静态字符串进行特征匹配,需要删除或修改这些内容。
- 定位相关文件:在
scan/Common 目录下,找到 Flag.go 和 Parse.go 文件。

- 删除标准库flag引用和Usage调用:在
Parse.go 文件中,找到并删除 import 中的 "flag",以及函数中调用的 flag.Usage()。


- 修改国际化文件(i18n.go):定位到
scan/Common/i18n.go 文件。目标是删除所有语言常量定义和多语言映射表,但保留语言设置和文本获取的函数框架。
- 将常量定义
LangZH, LangEN, LangJA, LangRU 的行全部删除或注释掉。
- 将庞大的
var i18nMap = map[string]map{...} 映射表清空或删除。
- 注意:需要保留
currentLang 变量和 SetLanguage(), GetText() 函数的框架,但函数内部逻辑可能需要简化(例如,直接返回键名或空字符串)。


- 检查并删除其他帮助文本:在代码中搜索其他明显的帮助文本、格式说明(例如支持的IP格式说明)并删除。

再次测试编译
完成特征去除后,再次在 Linux 下使用 garble 进行编译测试,确保修改未引入编译错误。

编译 Windows EXE 版本
确认无误后,开始编译针对 Windows 平台的 64 位可执行文件。使用 GOOS 和 GOARCH 环境变量指定交叉编译目标。
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 \
./garble -tiny -literals -seed=random \
build -ldflags="-w -s" -o abc.exe main.go

编译完成后,将 abc.exe 文件传回 Windows 系统,使用杀毒软件(如文中示例的火绒)进行初步扫描。此时,部分安软可能已无法识别。

进一步加固(加壳)
为了达到更好的免杀效果,可以对生成的 abc.exe 进行加壳处理,进一步混淆其二进制特征。
- UPX加壳:使用 UPX 工具压缩并加壳,这是较为简单的方法。
- VMProtect(VMP)等商业加壳工具:使用更强的保护壳进行加密和虚拟化保护,对抗逆向分析和特征检测。

提示:也可以尝试调整 garble 的混淆参数(如 -literals、-tiny 的组合),不同的混淆强度可能会影响程序的稳定性和免杀效果,需要进行测试。
最终,经过源码混淆、特征去除、加壳等一系列操作后,可以提交到在线多引擎扫描平台(如 VirusTotal)进行检测。理想情况下,检出率会显著降低。

这个过程并非一蹴而就,可能需要结合多种混淆技术、调整参数反复尝试,才能在保证工具可用性的前提下,实现相对理想的 免杀 效果。社区里常有关于不同技术和工具组合的讨论,例如在云栈社区的安全板块,就能找到许多相关的实战经验和思路分享。