半自动化批量剖析AgentTesla最新变体的方法探究--最终获取大量SMTP、FTP账号信息

由于AgentTesla远控是目前比较流行的远控木马之一,因此,基于简单的扩线方法,即可很快扩线一大批AgentTesla远控木马。

起初,笔者拿到扩线的AgentTesla远控木马时,还是基于上一篇文章的分析思路进行的一步一步分析,几个样本还好,但是当样本量突然上去后,笔者发现,这个工作不好干啊。。。重复工作。。。无意义工作。。。

因此,基于上述分析样本过程中的矛盾点,笔者就在琢磨,如何能够快速的对这一批样本进行剖析,并且提取我所需要的内容呢?

为了实现这个目标,笔者就开始对分析过程中的各个环节进行琢磨:

  • 分析难点:
    • 如何提取解密后的PE文件?能否在进程替换前提取最终木马载荷
  • 关键功能信息:
    • 配置信息
    • download功能的下载地址?
    • 窃取数据的方法?Web Panel、SMTP、FTP

基于上述思路,笔者尝试编写了多个脚本用以辅助半自动化的对AgentTesla最新变体样本开展分析工作,通过努力,最终实现对扩线样本进行了批量剖析,获得大量SMTP、FTP账号信息。

内存提取PE文件

为了能够完全提取解密后的PE文件,笔者也是尝试了多个思路:

  • 思路一:基于上一篇文章中提到的解密算法对相关加密载荷进行解密

    • 实际遇到的问题1:在SimpleLogin.dll解密Tyrone.dll样本的过程中,无法很好的动态调试其解密算法,而且其解密过程涉及了多个样本文件中的代码,因此很容易触发异常。
    • 实际遇到的问题2:解密算法均需要解密密钥,若解密密钥不同,就算成功复现了解密算法,还是需要再次通过动态调试才能提取解密密钥,因此也没有减少工作量。
  • 思路二:在调试过程中,提取进程内存中的所有PE文件

    • 实际遇到的问题1:每个木马进程内存中均能提取很多PE文件,而且大部分PE文件还是正常的dll模块。
    • 实际遇到的问题2:从木马进程内存中提取的PE文件,存在PE文件重复的情况。
    • 实际遇到的问题3:从木马进程内存中提取的PE文件,存在PE文件头是正常PE文件载荷,PE文件头后的内容并非为实际PE文件载荷。
  • 思路三:由于木马进程会调用VirtualAllocEx、WriteProcessMemory等函数以实现进程替换,因此,我们可通过OD对WriteProcessMemory函数下断点,成功断下来后,即可根据API查看待写入的PE文件载荷内容。

通过对比,笔者最终采用了思路三对AgentTesla最新变体运行过程中的最终载荷进行提取。

提取内存片段

由于AgentTesla最新变体在运行过程中,会通过进程替换技术将解密后的最终载荷写入新进程的内存空间中,因此,我们可通过如下方式尝试提取内存片段:

  • 使用OD调试AgentTesla最新变体程序,尝试下WriteProcessMemory函数断点;
  • 运行,即可在WriteProcessMemory函数断点处中断;
  • 查看WriteProcessMemory函数的lpBuffer参数(指向要写入数据的缓冲区的指针),发现其数据为PE文件载荷;
  • 由于lpBuffer参数对应内存地址为内存块中的一部分,因此,若尝试直接提取内存数据,然后再根据PE文件结构提取PE文件数据还是比较麻烦,而且此内存块中还存在其他PE文件;
  • 尝试直接提取lpBuffer参数对应内存地址的内存块;

相关截图如下:

在这里插入图片描述

在这里插入图片描述

从内存片段中还原PE文件

成功提取携带AgentTesla最终载荷的内存块后,我们即可尝试从内存片段中还原PE文件,为有效提取还原PE文件,笔者尝试以如下逻辑简单编写了一个脚本程序:

  • 在内存块中搜索PE文件结构中的"This program cannot be run in DOS mode"字符串;
  • 根据字符串偏移,查找PE文件头;
  • 根据PE文件头,计算PE文件大小;
  • 根据载荷偏移,计算PE文件载荷的Hash值;(简单去重,避免还原出来的PE文件为相同PE文件)
  • 根据载荷偏移,判断PE文件载荷末尾是否是以00结尾;(简单区分,避免PE文件头以后的数据为非实际PE文件数据)
  • 若Hash值为首次计算所得,则从内存块中提取PE文件;

代码效果如下:

在这里插入图片描述

解密后的载荷属性信息截图如下:(最终载荷的属性名称均是形如bfb5da9f-48ba-40d7-85d4-7ec204e8e6d3.exe结构)

在这里插入图片描述

代码实现如下:

package main

import (
    "crypto/sha256"
    "debug/pe"
    "encoding/hex"
    "fmt"
    "io"
    "io/ioutil"
    "os"
    "path/filepath"
    "regexp"
    "strconv"
    "strings"
)

func main() {
    hashs := \[\]string{}

    files, err := WalkDir("C:\\\\Users\\\\admin\\\\Desktop\\\\新建文件夹", "")
    if err != nil {
        fmt.Println("Error:", err.Error())
    }
    for \_, onefile := range files {
        SearchPE(onefile, &hashs)
    }
}

func SearchPE(file\_in string, hashs \*\[\]string) {
    output\_dir := "./output/"
    \_, fileName := filepath.Split(file\_in)
    // 读取文件的所有内容
    data, err := ioutil.ReadFile(file\_in)
    if err != nil {
        fmt.Println("Error reading file:", err)
        return
    }
    reg := regexp.MustCompile("This program cannot be run in DOS mode")
    offsets := reg.FindAllIndex(data, \-1)
    //fmt.Println(offsets)
    for \_, offset := range offsets {
        buffer := \[\]byte{}
        buffer \= append(buffer, data\[offset\[0\]\-0x4e:\]...)
        Writefile("tmp", string(buffer))
        size := getfilesize("tmp")
        buffer1 := \[\]byte{}
        buffer1 \= append(buffer1, buffer\[:size\]...)
        fmt.Println("offset PE:0x" + strconv.FormatInt(int64(offset\[0\]\-0x4e), 16))

        hash := HashData\_sha256(buffer1)
        //部分提取的文件其实只有PE头是正常的,因此,通过判断末尾数据来筛选
        if !ContainsAny(hash, \*hashs) && strings.HasSuffix(hex.EncodeToString(buffer1), "00000000000000000000000000000000") {
            \*hashs \= append(\*hashs, hash)
            Writefile(output\_dir+fileName+"offset\_0x"+strconv.FormatInt(int64(offset\[0\]\-0x4e), 16), string(buffer1))
        }
        os.Remove("tmp")
    }
}

func getfilesize(filePath string) (size int) {
    file, err := os.Open(filePath)
    if err != nil {
        fmt.Printf("Error opening file: %v\\n", err)
        return
    }
    defer file.Close()

    peFile, err := pe.NewFile(file)
    if err != nil {
        fmt.Printf("Error parsing PE file: %v\\n", err)
        return
    }
    defer peFile.Close()

    fmt.Printf("RawSize: 0x%X\\n", peFile.Sections\[len(peFile.Sections)\-1\].Size)
    fmt.Printf("RawAddress: 0x%X\\n", peFile.Sections\[len(peFile.Sections)\-1\].Offset)

    aa := peFile.Sections\[len(peFile.Sections)\-1\].Size + peFile.Sections\[len(peFile.Sections)\-1\].Offset
    size \= int(aa)
    return
}

func WalkDir(dirPth, suffix string) (files \[\]string, err error) {
    files \= make(\[\]string, 0, 30)
    suffix \= strings.ToUpper(suffix) //忽略后缀匹配的大小写

    err \= filepath.Walk(dirPth, func(filename string, fi os.FileInfo, err error) error { //遍历目录
        if fi.IsDir() { // 忽略目录
            return nil
        }

        if strings.HasSuffix(strings.ToUpper(fi.Name()), suffix) {
            files \= append(files, filename)
        }

        return nil
    })

    return files, err
}

func CheckPathIsExist(filename string) bool {
    var exist \= true
    if \_, err := os.Stat(filename); os.IsNotExist(err) {
        exist \= false
    }
    return exist
}

func Writefile(filename string, buffer string) {
    var f \*os.File
    var err1 error

    if CheckPathIsExist(filename) {
        f, err1 \= os.OpenFile(filename, os.O\_CREATE, 0666)
    } else {
        f, err1 \= os.Create(filename)
    }
    \_, err1 \= io.WriteString(f, buffer)
    if err1 != nil {
        fmt.Println("写文件失败", err1)
        return
    }
    \_ \= f.Close()
}

func HashData\_sha256(data \[\]byte) string {
    // 创建 SHA256 哈希函数
    hash := sha256.New()

    // 将字符串转换为字节数组,并计算哈希值
    hash.Write(data)
    hashValue := hash.Sum(nil)

    // 将哈希值转换为十六进制字符串
    hashString := hex.EncodeToString(hashValue)
    return hashString
}

func ContainsAny(str string, elements \[\]string) bool {
    for element := range elements {
        e := elements\[element\]
        if strings.Contains(e, str) {
            return true
        }
    }
    return false
}

自动化提取配置信息

为了能够完全提取配置信息,笔者也是尝试了多个思路:

  • 思路一:尝试基于配置信息的16进制特征对AgentTesla最终载荷样本的配置信息进行提取
    • 实际遇到的问题:基于16进制只能提取配置信息的变量值,具体变量名无法很好的对应
  • 思路二:尝试对反编译后的AgentTesla最终载荷样本的代码进行分析,提取反编译后的配置信息
    • 实际遇到的问题:最开始没有找到很合适的命令行反编译工具

通过对比,笔者最终采用了思路二对AgentTesla最新变体运行过程中的最终载荷的配置信息进行提取。

思路一的部分截图如下:

在这里插入图片描述

批量反编译NET样本

为了实现批量反编译NET样本,笔者尝试对多款NET反编译工具进行了研究,梳理发现:

  • dnspy:不支持命令行运行;
  • ILSpy存在一个命令行版本ilspycmd,支持命令行运行;

安装ilspycmd工具的流程如下:

  • 安装 .NET SDK 6.0:笔者安装的版本为dotnet-sdk-6.0.423-win-x64.exe;
  • 安装ilspycmd工具:dotnet tool install --global ilspycmd
  • 验证ilspycmd安装:ilspycmd --help(如果安装非.NET SDK 6.0版本,则会报错)
  • 使用ilspycmd反编译程序集:ilspycmd -o “./output” “assembly.dll”

相关操作截图如下:(反编译后,会生成.decompiled.cs后缀文件)

在这里插入图片描述

配置信息结构对比

尝试对AgentTesla最新变体运行过程中的最终载荷的配置信息进行对比,发现其配置信息项的顺序以及名称均基本相同。

相关截图如下:

在这里插入图片描述

在这里插入图片描述

自动化提取配置信息

因此,基于上述配置对比信息,笔者尝试基于如下思路构建自动化提取配置信息的脚本程序:

  • 遍历AgentTesla最新变体运行过程中的最终载荷;
  • 使用ilspycmd工具对样本进行批量反编译;
  • 使用正则匹配反编译.decompiled.cs文件中的URL信息;
  • 使用正则匹配反编译.decompiled.cs文件中的配置信息;

代码效果如下:

在这里插入图片描述

代码实现如下:

package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "os/exec"
    "path/filepath"
    "regexp"
    "strings"
)

func main() {
    fmt.Println("需安装ilspycmd工具")
    fmt.Println()
    files, err := WalkDir("C:\\\\Users\\\\admin\\\\Desktop\\\\test", "")
    if err != nil {
        fmt.Println("Error:", err.Error())
    }
    for \_, onefile := range files {
        fileName := filepath.Base(onefile)
        fileExt := filepath.Ext(onefile)
        decompiledfile := strings.Split(fileName, fileExt)\[0\] + ".decompiled.cs"
        if !CheckPathIsExist("./output/" + decompiledfile) {
            CmdRun(\`C:\\\\Users\\\\admin\\\\.dotnet\\\\tools\\\\ilspycmd.exe -o ./output/ \` + onefile)
        }
        if CheckPathIsExist("./output/" + decompiledfile) {
            fmt.Println("\*\*\*\*\*\*\*\*\*\*" + fileName + " URL\*\*\*\*\*\*\*\*\*\*")
            SearchURLs("./output/" + decompiledfile)
            fmt.Println("\*\*\*\*\*\*\*\*\*\*" + fileName + " Config\*\*\*\*\*\*\*\*\*\*")
            SearchConfig("./output/" + decompiledfile)
            fmt.Println()
        }
    }
}

func SearchURLs(onefile string) {
    // 读取文本文件内容
    content, err := ioutil.ReadFile(onefile) // 请替换为实际的文件路径
    if err != nil {
        fmt.Printf("无法读取文件:%v\\n", err)
        return
    }

    // 定义正则表达式
    re := regexp.MustCompile(\`(http://|https://)\[^\\s\]+\`)

    // 在文本中查找匹配的字符串
    matches := re.FindAllString(string(content), \-1)

    // 输出匹配到的字符串
    for \_, match := range matches {
        if strings.Contains(match, \`"\`) {
            fmt.Println(strings.Split(match, \`"\`)\[0\])
        } else {
            fmt.Println(match)
        }
    }
}

func SearchConfig(onefile string) {
    // 读取文本文件内容
    content, err := ioutil.ReadFile(onefile) // 请替换为实际的文件路径
    if err != nil {
        fmt.Println(err.Error())
    }

    // 定义正则表达式,用于匹配两个字符串之间的内容
    re := regexp.MustCompile(\`(?s)public\\s+static\\s+string\\s+PcHwid(.\*?)public\\s+static\\s+string\\s+StartupRegName\`)

    // 查找匹配的字符串
    matches := re.FindStringSubmatch(string(content))

    if len(matches) \> 1 {
        // 输出匹配到的字符串(第一个子匹配项)
        //fmt.Println("匹配到的内容:", matches\[0\])
        output := matches\[0\]
        output \= strings.ReplaceAll(output, "\\t", "")
        output \= strings.ReplaceAll(output, "public static string ", "")
        output \= strings.ReplaceAll(output, "public static bool ", "")
        output \= strings.ReplaceAll(output, "public static int ", "")
        output \= strings.ReplaceAll(output, "Convert.ToBoolean(", "")
        output \= strings.ReplaceAll(output, "Convert.ToInt32(", "")
        output \= strings.ReplaceAll(output, ");", "")
        output \= strings.ReplaceAll(output, ";", "")
        output \= strings.ReplaceAll(output, "\\r\\n\\r\\n", "\\r\\n")
        fmt.Print(strings.Split(output, "StartupRegName")\[0\])
    } else {
        fmt.Println("未找到匹配的内容")
    }

    re1 := regexp.MustCompile(\`public\\s+static\\s+string\\s+StartupRegName.\*\`)

    // 查找匹配的字符串
    matches1 := re1.FindStringSubmatch(string(content))

    if len(matches1) \> 0 {
        output := matches1\[0\]
        output \= strings.ReplaceAll(output, "public static string ", "")
        output \= strings.ReplaceAll(output, ";\\r", "")
        fmt.Println(output)
    } else {
        fmt.Println("未找到匹配的内容")
    }

}

func WalkDir(dirPth, suffix string) (files \[\]string, err error) {
    files \= make(\[\]string, 0, 30)
    suffix \= strings.ToUpper(suffix) 

    err \= filepath.Walk(dirPth, func(filename string, fi os.FileInfo, err error) error {
        if fi.IsDir() { // 忽略目录
            return nil
        }
        if strings.HasSuffix(strings.ToUpper(fi.Name()), suffix) {
            files \= append(files, filename)
        }
        return nil
    })

    return files, err
}
func CheckPathIsExist(filename string) bool {
    var exist \= true
    if \_, err := os.Stat(filename); os.IsNotExist(err) {
        exist \= false
    }
    return exist
}

func CmdRun(command string) {
    parts := strings.Fields(command)
    head := parts\[0\]
    parts \= parts\[1:\]
    cmd := exec.Command(head, parts...)
    output, err := cmd.CombinedOutput()
    if err != nil {
        fmt.Println(err.Error())
        fmt.Println(string(output))
    } else {
        fmt.Println(string(output))
    }
}

多阶段部分解密算法复现

虽然笔者最终未通过复现多阶段解密算法的方式去解密各阶段中的PE文件,但笔者也尝试编写了部分脚本用以做解密尝试,因此,笔者也将其解密脚本放置于文章中。

解密SimpleLogin.dll

代码效果如下:

在这里插入图片描述

代码实现如下:

package main

import (
    "fmt"
    "io"
    "io/ioutil"
    "os"
)

func main() {
    file\_in := "C:\\\\Users\\\\admin\\\\Desktop\\\\新建文件夹\\\\off"

    array, err := ioutil.ReadFile(file\_in)
    if err != nil {
        fmt.Println("Error reading file:", err)
        return
    }
    num := len(array)
    first := "J9EZ6H5428445"
    second := "C755C8RZH"
    first\_second := first + second
    for i, data := range array {
        num2 := i % 22
        b := first\_second\[num2\]
        num3 := (i + 1) % num
        num4 := int(data ^ b)
        num5 := num4 \- int(array\[num3\]) + 256
        array\[i\] \= byte(num5 & 255)
    }
    Writefile(file\_in+"\_dec\_SimpleLogin.dll", string(array))
}

func CheckPathIsExist(filename string) bool {
    var exist \= true
    if \_, err := os.Stat(filename); os.IsNotExist(err) {
        exist \= false
    }
    return exist
}

func Writefile(filename string, buffer string) {
    var f \*os.File
    var err1 error

    if CheckPathIsExist(filename) {
        f, err1 \= os.OpenFile(filename, os.O\_CREATE, 0666)
    } else {
        f, err1 \= os.Create(filename)
    }
    \_, err1 \= io.WriteString(f, buffer)
    if err1 != nil {
        fmt.Println("写文件失败", err1)
        return
    }
    \_ \= f.Close()
}

解密Gamma.dll

直接使用压缩软件即可实现解密Gamma.dll文件。

相关截图如下:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

半自动化批量剖析的最终实现效果–获取大量SMTP、FTP账号信息

在文章开头,笔者就说笔者通过扩线获取了大量的AgentTesla最新变体样本,因此,基于上述半自动化分析手法,笔者可很快速对上述样本的功能行为进行梳理提取。

通过梳理,笔者发现了大量的SMTP、FTP账号信息,相关截图如下:

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

  • 21
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

网络安全技术库

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值