半自动化批量剖析AgentTesla最新变体的方法探究--最终获取大量SMTP、FTP账号信息
T0daySeeker 发表于 四川 历史精选 2863浏览 · 2024-07-03 00:55

在上一篇《AgentTesla最新变体剖析-通过Web Panel方式窃取终端隐私数据》文章中,笔者对AgentTesla的最新变体样本进行了详细的剖析,由于当时笔者捕获的样本均是基于Web Panel方式窃取终端隐私数据的,而且基于代码以及网络中的介绍,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账号信息,相关截图如下:

0 条评论
某人
表情
可输入 255