找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2997

积分

0

好友

411

主题
发表于 昨天 09:30 | 查看: 1| 回复: 0

本文内容仅供安全研究与学习,请严格遵守相关法律法规。

本文参考了 《新手如何快速做到免杀fscan》 一文。

环境准备

需要一台 Windows 10 和一台 Linux 机器(或者仅用 Windows 10 也可,注意命令行转换)。

Linux 环境准备

首先,在 Linux 上安装和配置 Go 语言环境。

  1. 下载 Go 1.21.13:
    wget https://go.dev/dl/go1.21.13.linux-amd64.tar.gz
  2. 解压到 /usr/local
    sudo tar -C /usr/local -xzf go1.21.13.linux-amd64.tar.gz
  3. 编辑 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
  4. 使配置生效:
    source ~/.zshrc
  5. 安装代码混淆工具 garble
    go install mvdan.cc/garble@v0.12.1
  6. garble 工具复制到你的工作目录(例如 /root/Downloads):
    cp /root/go/bin/* /root/Downloads

    如果无法使用 wget,可手动下载 Go 安装包。通过 go env | grep GOPATH 确认 garble 的安装位置,以便后续将其移动到 FSCAN 项目目录中。

Kali Linux终端中查看Go版本及GOPATH

Windows 环境准备

在 Windows 上获取 FSCAN 源码并进行初步整理。

  1. 克隆 FSCAN 仓库:
    git clone https://github.com/shadow1ng/fscan.git
  2. 使用 VS Code 打开项目。为了更好地组织代码,新建一个名为 scan 的文件夹,将原有的四个功能文件夹(如 Common, Core, Plugins, WebScan)移动进去。

VSCode中FSCAN项目结构示意

修改 go.mod 文件

接下来,需要修改项目的模块名称和引用路径,这是进行代码混淆和自定义编译的前提。

  1. 打开 go.mod 文件,将模块名从 github.com/shadow1ng/fscan 修改为你自定义的名称,例如 OpsTool

修改go.mod文件中的模块名
模块名修改为OpsTool

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

在VSCode中全局搜索旧模块路径

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

搜索并替换其他包含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工具复制到FSCAN项目目录

在项目目录下执行首次混淆编译命令,测试修改是否影响基础编译功能:

./garble -tiny -literals -seed=random build -ldflags="-w -s" main.go

使用garble首次编译成功并运行程序

如果编译没有报错,并且生成的 main 二进制文件可以正常运行,则证明我们之前的代码修改没有破坏核心功能。接下来,我们将进行更深度的修改,目标是生成一个能够规避常见杀毒软件检测的 Windows 可执行文件。

去除明显的特征与提示信息

为了避免杀软通过帮助信息、版本信息等静态字符串进行特征匹配,需要删除或修改这些内容。

  1. 定位相关文件:在 scan/Common 目录下,找到 Flag.goParse.go 文件。

搜索flag相关文件

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

Parse.go文件中删除flag引用及Usage调用
删除import中的flag包

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

删除i18n.go中的语言常量定义
清空i18nMap多语言映射表

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

删除代码中其他明显的帮助文本

再次测试编译

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

再次使用garble编译测试通过

编译 Windows EXE 版本

确认无误后,开始编译针对 Windows 平台的 64 位可执行文件。使用 GOOSGOARCH 环境变量指定交叉编译目标。

GOOS=windows GOARCH=amd64 CGO_ENABLED=0 \
./garble -tiny -literals -seed=random \
build -ldflags="-w -s" -o abc.exe main.go

交叉编译生成Windows exe文件

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

初步免杀后安全软件扫描结果

进一步加固(加壳)

为了达到更好的免杀效果,可以对生成的 abc.exe 进行加壳处理,进一步混淆其二进制特征。

  • UPX加壳:使用 UPX 工具压缩并加壳,这是较为简单的方法。
  • VMProtect(VMP)等商业加壳工具:使用更强的保护壳进行加密和虚拟化保护,对抗逆向分析和特征检测。

使用VMP等工具加壳后的文件

提示:也可以尝试调整 garble 的混淆参数(如 -literals-tiny 的组合),不同的混淆强度可能会影响程序的稳定性和免杀效果,需要进行测试。

最终,经过源码混淆、特征去除、加壳等一系列操作后,可以提交到在线多引擎扫描平台(如 VirusTotal)进行检测。理想情况下,检出率会显著降低。

最终在多个杀毒引擎下的检测结果

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




上一篇:Docker容器时区永久生效实战:三种方案从应急到规范,重启不失效
下一篇:掌握C# LINQ集合关系操作:Distinct/Union/Intersect/Except详解与.NET 6+实战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-9 01:00 , Processed in 0.310105 second(s), 38 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表