文章首发地址:
https://xz.aliyun.com/t/14979
文章首发作者:
T0daySeeker
概述
在上一篇《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: %vn", err)
return
}
defer file.Close()
peFile, err := pe.NewFile(file)
if err != nil {
fmt.Printf("Error parsing PE file: %vn", err)
return
}
defer peFile.Close()
fmt.Printf("RawSize: 0x%Xn", peFile.Sections[len(peFile.Sections)-1].Size)
fmt.Printf("RawAddress: 0x%Xn", 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("无法读取文件:%vn", 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)publics+statics+strings+PcHwid(.*?)publics+statics+strings+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, "rnrn", "rn")
fmt.Print(strings.Split(output, "StartupRegName")[0])
} else {
fmt.Println("未找到匹配的内容")
}
re1 := regexp.MustCompile(`publics+statics+strings+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账号信息,相关截图如下:
原文始发于微信公众号(T0daySeeker):半自动化批量剖析AgentTesla最新变体的方法探究--最终获取大量SMTP、FTP账号信息
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论