当前位置: 首页 > news >正文

【病毒分析】新兴TOP2勒索软件!存在中国受害者的BianLian勒索软件解密原理剖析

1. 概述

近期,笔者在浏览网络中威胁情报信息的时候,发现美国halcyon.ai公司于2024年3月25日发布了一篇《Ransomware on the Move: LockBit, BianLian, Medusa, Hunters International》报告,此报告对当前勒索软件团伙的实力进行了排名,排名顺序为:LockBit勒索软件、BianLian勒索软件、Medusa勒索软件、Hunters International勒索软件;进一步对报告内容进行研读,发现报告中在对BianLian勒索软件进行描述时,提到「总部位于上海的知名汽车电子企业科博达科技」已成为BianLian勒索软件团伙的受害者,相关报告截图如下:

图片

尝试对BianLian勒索软件团伙进行调研,发现BianLian勒索软件团伙的攻击方式为:利用漏洞在网络中传播,窃取最有价值的数据并对关键机器进行加密。相关报告截图如下:

图片

为了能够深层次的对BianLian勒索软件进行研究,笔者从近期Palo Alto Networks安全公司2024年1月23日发布的《Threat Assessment: BianLian》报告中提取了勒索加密工具的Hash信息,成功下载了多个样本用于研究剖析:

  • BianLian勒索软件功能剖析:梳理其样本功能及勒索加密后的特征。

  • BianLian勒索软件加解密原理剖析:梳理其加密算法及加密逻辑。

  • 构建BianLian勒索软件解密工具:结合加解密原理,模拟构建了一款针对BianLian勒索软件的解密工具,可实现一键化勒索解密。

相关报告Hash信息如下:

图片

2. 勒索软件功能分析

通过分析,发现BianLian勒索软件为Golang语言编写,相关截图如下:

图片

图片

进一步分析,发现此样本存在调试信息,可以较快速的识别关键代码函数,相关代码截图如下:

图片

2.1 遍历驱动器中的文件

通过分析,发现样本运行后,将从A盘到Z盘识别系统驱动器信息,并从系统中提取可用的文件,便于后续加密行为,相关代码截图如下:

图片

2.2 创建带付款说明的文本文件

通过分析,发现此样本运行后,将在加密行为开始前,在多个目录中创建名为“look at this Instruction.txt”的带付款说明的文本文件,相关代码截图如下:

图片

图片

实际加密行为截图如下:

图片

图片

2.3 加密系统文件

通过分析,发现样本运行后,将按照如下逻辑对系统中的文件进行加密:

  • 从待加密原始文件中读取指定大小数据块;

  • 调用AES CBC算法对数据块内容进行加密;

  • 将加密后的数据块内容写入原始文件中;「(此时,原始文件的内容已经被修改)」

  • 使用".bianlian"后缀重命名加密文件;

「备注:由于BianLian勒索软件是直接基于原始文件进行的数据读写操作,因此,系统文件被加密后,我们是无法通过数据恢复的方式恢复数据的。」

相关代码截图如下:

图片

3. 勒索软件加解密原理剖析

在尝试对BianLian勒索软件的加解密原理进行剖析的过程中,笔者也是经历了一波三折,「由于笔者在面对此样本的加解密问题时,总是以常规思维去思考此样本的加解密问题,导致在多个问题上走了弯路」

  • 最开始拿到这个样本时,笔者发现网络中针对此家族样本的报告有多种描述,一时分不清哪种说法正确

    • 描述1:“勒索软件主要使用GoLang加密软件包进行AES和RSA加密”;

    • 描述2:“样本中虽然引用了非对称加密库(RSA和elliptic curves),但勒索软件未调用其执行任何操作。文件数据是使用AES-256 CBC模式加密的”

  • 后续,笔者又发现网络中有报告称avast安全公司发布了免费的BianLian勒索软件解密器,因此,笔者推测BianLian勒索软件可能与描述2的内容吻合

  • 笔者尝试从近期Palo Alto Networks安全公司2024年1月23日发布的《Threat Assessment: BianLian》报告中提取BianLian勒索软件Hash,成功下载了5个样本,尝试对其进行分析,笔者发现此5个样本均调用了AES算法,但未调用RSA算法

  • 然后,笔者就开始琢磨研究BianLian勒索软件加解密原理及模拟编写解密工具

    • 「起初,笔者认为:勒索软件运行后,具备勒索软件后缀的文件均是被其勒索加密后的文件。」

    • 于是,笔者根据BianLian勒索软件的加密效果及加密算法原理,快速的编写了一个BianLian勒索软件解密工具

    • 基于笔者编写的BianLian勒索软件解密工具尝试对勒索软件运行后的加密文件进行解密,同时对加密前与解密后的文件MD5做对比,笔者发现存在大量文件MD5不同的情况......此刻有点慌,应该是解密工具编写的有问题。。。

    • 基于此情况,笔者又反复对比加密前与加密后的文件内容,发现部分文件虽然具备勒索软件后缀,但是文件内容却并未加密。后续,笔者还尝试构建了大量不同大小的文件,用以对比其加密前与加密后的变化情况,测试过程中还一度认为是勒索软件作者写代码时的文件加密逻辑没思考清楚......例如:区分使用按块加密还是整块加密的方法是基于文件大小的,但实际上,0xfff大小的文件是按块加密的,0x1009大小的文件是整块加密的,0x1000-0x1008大小的文件是不调用加密的

  • 最后,笔者决定通过动态调试探索一下加密逻辑,在动态调试后,笔者才比较清晰的对其加密逻辑进行了详细梳理,「发现样本中存在两层校验,用于确定是否对文件进行加密」

    • 在开始加密行为前,样本将定义三个特殊数据

      • 文件大小

      • 块数据大小,由文件大小计算所得

      • 加密偏移位置:「内置数据」,不同样本的加密偏移位置不同,用于指定从文件的哪个数据位置开始数据加密

    • 第一层校验:样本在开始加密行为前,将调用project1_common_GetBlocksAmount函数,并向project1_common_GetBlocksAmount函数传递上述3个特殊数据,函数返回值将决定样本是否调用加密函数加密文件

    • 第二层校验:样本将计算实际读取的文件大小(从加密偏移位置开始读取)是否与块大小相等,计算结果将决定样本是否调用加密函数加密文件

  • 「最终梳理发现,受害系统中虽然有很多勒索软后缀文件,但并非所有文件均被加密,而且不同文件大小所调用的加密算法逻辑也不一样」

3.1 加密算法

通过分析,笔者发现此样本将调用AES CBC算法进行勒索文件加密,相关代码截图如下:

图片

进一步分析,发现其AES CBC算法所需的KEY和IV值均内置于样本中,相关截图如下:

图片

图片

尝试将已掌握的所有样本的KEY和IV值进行提取,梳理密钥及关键信息如下:

/#1fd07b8d1728e416f897bef4f1471126f9b18ef108eb952f4b75050da22e8e43
加密偏移位置:9
KEY:633A56D0388869237C8DA6B7FC09F55DF35408C332614F692D11222583B36B62
IV:FC55A60A25B0472CFC2C6EBC3DD89DAB#3a2f6e614ff030804aa18cb03fcc3bc357f6226786efb4a734cbe2a3a1984b6f
加密偏移位置:1
KEY:26DD1B400AED80B0980B34BAD76D3BE6599123FA562266B421A14AC8E02ECFA3
IV:D518BA928469306D579D625F56D09883#46d340eaf6b78207e24b6011422f1a5b4a566e493d72365c6a1cace11c36b28b
加密偏移位置:0x34
KEY:979412FF08F7D2F0BD5F7E7E2A4919E9BF68CC7AABAB499872EC822DDCDA5307
IV:0FC14323F7A13CCA0569EBCFAE283996#af46356eb70f0fbb0799f8a8d5c0f7513d2f6ade4f16d4869f2690029b511d4f
加密偏移位置:0x41
KEY:4A4105960D2C127D9711AC851BC1F10D17471B5A184CCDADA79003DA82CFDBA2
IV:5560E19D4B425420F6F5EF387D97065A#eaf5e26c5e73f3db82cd07ea45e4d244ccb3ec3397ab5263a1a74add7bbcb6e2
加密偏移位置:0x3d
KEY:27CFAE34A83C9A7E48060E18A68A233914271DB7D414C838FB1EAEAEA89E5CDE
IV:223B67AC534F9938CC7B1F9777A95840

3.2 加密算法逻辑

结合静态分析及动态调试等分析手段,梳理BianLian勒索软件的加密算法逻辑如下:

  • 获取文件大小,并基于文件大小计算数据块大小

    • 若文件大小小于0x1000,则数据块大小为16**(按小数据块分别加密:从文件加密偏移位置开始提取多个数据块,并分别对每个小数据块内容进行加密)**

    • 若文件大小大于0x400000(4MB),则基于运算计算数据块大小,若计算后结果依然大于0x400000,则数据块大小为0x400000**(按大数据块整个加密:从文件加密偏移位置开始提取整个数据块,并直接对整个数据块内容进行加密)**

图片

  • 第一层校验:调用project1_common_GetBlocksAmount函数,并根据返回值判断是否对此文件进行加密

    图片

图片

  • 第二层校验:从***加密偏移位置***处开始读取块大小的文件内容,若实际读取数据大小等于块大小,则加密文件

图片

基于上述加密算法逻辑,以1fd07b8d1728e416f897bef4f1471126f9b18ef108eb952f4b75050da22e8e43样本作为案例样本(加密偏移位置:9),梳理不同文件大小的加密情况如下:

文件大小块大小第一层校验返回值第二层校验返回值是否加密加密方法
0x90x100x0falsefalse
0xa0x100x0falsefalse
0x170x100x1falsefalse
0x180x100x1falsefalse
0x190x100x1true「true」按小数据块分别加密
0x1a0x100x1true「true」按小数据块分别加密
0xfff0x100xfftrue「true」按小数据块分别加密
0x10000x10000x1falsefalse
0x10010x10000x1falsefalse
0x10080x10000x1falsefalse
0x10090x10000x1true「true」按大数据块整个加密
0x100a0x10000x1true「true」按大数据块整个加密
0x10240x10000x1true「true」按大数据块整个加密
0x3fffff0x3ff0000x0truefalse
0x4000000x4000000x0falsefalse
0x4000010x660000x1true「true」按大数据块整个加密

4. 构建BianLian勒索软件解密工具

通过上述分析,发现BianLian勒索软件的不同样本的AES KEY、AES IV、加密偏移位置均不同,因此,为了能够实现一键化解密,笔者准备从如下角度构建勒索软件解密工具:

  • 根据系统中的勒索加密文件情况,自动化匹配提取AES KEY、AES IV及加密偏移位置信息:以系统桌面中的desktop.ini文件作为参考文件,使用多个内置密钥进行解密尝试,若成功解密,则返回对应的AES KEY、AES IV及加密偏移位置信息。

  • 借助everything文件搜索工具,提取系统中的勒索后缀文件列表。

  • 基于上述BianLian勒索软件的加解密原理,模拟构建针对BianLian勒索软件的解密工具,解密还原原始文件,并将勒索后缀文件重命名为“.bak”文件后缀。

4.1 解密效果

勒索加密后,系统文件截图如下:

图片

使用BianLian勒索软件解密工具解密勒索加密文件,系统文件截图如下:

图片

加密前与解密后的文件MD5信息对比:「(共被勒索加密578个文件,使用BianLian勒索软件解密工具成功解密578个文件,仅有3个文件【系统运行过程中文件内容被修改导致】的MD5不同)」

图片

4.2 代码实现

在这里,笔者将使用golang语言模拟构建BianLian勒索软件的一键化解密工具,详细情况如下:

代码结构如下:

图片

  • main.go

package mainimport ("awesomeProject5/common""bytes""encoding/hex""fmt""io/ioutil""os""os/user""path/filepath""strings"
)func main() {//使用everything导出带勒索软件后缀的文件列表files := common.FileToSlice("C:\\Users\\admin\\Desktop\\11.txt")aeskey, aes_iv, offset := getAeskey()if aeskey == nil {fmt.Println("提取AES密钥失败")os.Exit(1)}fmt.Println("AES key:", hex.EncodeToString(aeskey))fmt.Println("AES iv:", hex.EncodeToString(aes_iv))fmt.Println("offset:", offset)for _, onefile := range files {fileSize, err := common.GetFileSize(onefile)if err != nil {fmt.Println("Error:", err)return}fmt.Printf("%s,0x%x\n", onefile, fileSize)file_decodeData := []byte{}fileData, err := ioutil.ReadFile(onefile)if err != nil {fmt.Println("Error reading file:", err)return}len_block := common.Calc_block(fileSize)if common.GetBlocksAmount(fileSize, len_block, int64(offset)) > 0 {if fileSize < 0x1000 {//按小数据块分别加密if (int64(offset) + len_block) > fileSize {file_decodeData = append(file_decodeData, fileData...)} else {file_decodeData = append(file_decodeData, fileData[:offset]...)blocks := (fileSize - int64(offset)) / 16for i := 0; i < int(blocks); i++ {output, _ := common.DecryptAES(fileData[offset+16*i:offset+16*(i+1)], aeskey, aes_iv)file_decodeData = append(file_decodeData, output...)}file_decodeData = append(file_decodeData, fileData[offset+int(blocks)*16:]...)}} else {//按大数据块整个加密file_decodeData = append(file_decodeData, fileData[:offset]...)if (int64(offset) + len_block) > fileSize {file_decodeData = append(file_decodeData, fileData[offset:]...)} else {output, _ := common.DecryptAES(fileData[int64(offset):int64(offset)+len_block], aeskey, aes_iv)file_decodeData = append(file_decodeData, output...)file_decodeData = append(file_decodeData, fileData[int64(offset)+len_block:]...)}}common.Writefile(strings.Split(onefile, ".bianlian")[0], string(file_decodeData))os.Rename(onefile, strings.Split(onefile, ".bianlian")[0]+".bak")} else {common.Writefile(strings.Split(onefile, ".bianlian")[0], string(fileData))os.Rename(onefile, strings.Split(onefile, ".bianlian")[0]+".bak")}}
}func getAeskey() (aeskey []byte, aes_iv []byte, offset int) {currentUser, _ := user.Current()desktopDir := filepath.Join(currentUser.HomeDir, "Desktop")desktopini := filepath.Join(desktopDir, "desktop.ini.bianlian")aeskeys := []string{"633A56D0388869237C8DA6B7FC09F55DF35408C332614F692D11222583B36B62", "26DD1B400AED80B0980B34BAD76D3BE6599123FA562266B421A14AC8E02ECFA3","979412FF08F7D2F0BD5F7E7E2A4919E9BF68CC7AABAB499872EC822DDCDA5307", "4A4105960D2C127D9711AC851BC1F10D17471B5A184CCDADA79003DA82CFDBA2","27CFAE34A83C9A7E48060E18A68A233914271DB7D414C838FB1EAEAEA89E5CDE"}aesivs := []string{"FC55A60A25B0472CFC2C6EBC3DD89DAB", "D518BA928469306D579D625F56D09883", "0FC14323F7A13CCA0569EBCFAE283996","5560E19D4B425420F6F5EF387D97065A", "223B67AC534F9938CC7B1F9777A95840"}offsets := []int{0x9, 0x1, 0x34, 0x41, 0x3d}for ii := 0; ii < 5; ii++ {fileSize, _ := common.GetFileSize(desktopini)len_block := common.Calc_block(fileSize)fileData, _ := ioutil.ReadFile(desktopini)file_decodeData := []byte{}aeskey, _ = hex.DecodeString(aeskeys[ii])aes_iv, _ = hex.DecodeString(aesivs[ii])offset = offsets[ii]if common.GetBlocksAmount(fileSize, len_block, int64(offset)) > 0 {if fileSize < 0x1000 {if (int64(offset) + len_block) > fileSize {file_decodeData = append(file_decodeData, fileData...)} else {file_decodeData = append(file_decodeData, fileData[:offset]...)blocks := (fileSize - int64(offset)) / 16for i := 0; i < int(blocks); i++ {output, _ := common.DecryptAES(fileData[offset+16*i:offset+16*(i+1)], aeskey, aes_iv)file_decodeData = append(file_decodeData, output...)}file_decodeData = append(file_decodeData, fileData[offset+int(blocks)*16:]...)}} else {file_decodeData = append(file_decodeData, fileData[:offset]...)if (int64(offset) + len_block) > fileSize {file_decodeData = append(file_decodeData, fileData[offset:]...)} else {output, _ := common.DecryptAES(fileData[int64(offset):int64(offset)+len_block], aeskey, aes_iv)file_decodeData = append(file_decodeData, output...)file_decodeData = append(file_decodeData, fileData[int64(offset)+len_block:]...)}}//system32if bytes.Contains(file_decodeData, []byte{0x73, 0x00, 0x79, 0x00, 0x73, 0x00, 0x74, 0x00, 0x65, 0x00, 0x6D, 0x00, 0x33, 0x00, 0x32, 0x00}) {return}} else {fmt.Println(ii, "由于desktop.ini文件未加密,因此暂无法提取AES密钥")os.Exit(1)}}return nil, nil, 0
}
  • common.go

package commonimport ("bufio""bytes""crypto/aes""crypto/cipher""encoding/binary""fmt""io""math/big""os"
)func DecryptAES(ciphertext, key, iv []byte) ([]byte, error) {block, err := aes.NewCipher(key)if err != nil {return nil, err}if len(ciphertext) < aes.BlockSize {return nil, fmt.Errorf("ciphertext too short")}if len(ciphertext)%aes.BlockSize != 0 {return nil, fmt.Errorf("ciphertext is not a multiple of the block size")}mode := cipher.NewCBCDecrypter(block, iv)mode.CryptBlocks(ciphertext, ciphertext)return ciphertext, nil
}func GetBlocksAmount(a1 int64, a2 int64, a3 int64) int64 {var v4 int64if a2 == -1 {v4 = -a1} else {v4 = a1 / a2}v5 := a1 - a3v6 := ((v5 >> 63) >> 54) + v5v7 := v5 / 1024if v7 <= 1024 {return v4}v8 := ((v6 >> 63) >> 54) + v7v9 := float64(a2) * 0.0009765625 * 0.0009765625v10 := (0.2 * float64(v8>>10) / v9)var v11 int64if v8>>10 >= 1024 {v11 = (((v8 >> 63) >> 54) + (v8 >> 10)) >> 10var v12 float64if (v11 - 101) >= 0x18F {if v11 <= 500 {v12 = 0.001} else {v12 = 0.00005}} else {v12 = 0.00015}return int64(v12) * v11 / int64(0.0009765625*v9)}return int64(v10)
}func Calc_block(fileSize int64) (v27 int64) {v27 = (fileSize / 0x1000) << 0xcif fileSize > 0x400000 {num1 := big.NewInt(fileSize)hex_2 := "CCCCCCCCCCCCCCCD"num2 := new(big.Int)num2.SetString(hex_2, 16)result := new(big.Int).Mul(num1, num2)bytes := result.Bytes()tmp1, _ := BytesToInt_mode(bytes[:len(bytes)-8])tmp2, _ := BytesToInt_mode(bytes[:len(bytes)-8])tmp1 = tmp1 >> 3tmp2 = tmp2 >> 0x3f >> 0x34tmp3 := tmp1 + tmp2v27 = int64(tmp3 >> 0xc << 0xc)if tmp3 >= 0x400000 {v27 = 0x400000}}if v27 < 16 {v27 = 16}return
}func BytesToInt_mode(b []byte) (int, error) {if len(b) == 3 {b = append([]byte{0}, b...)}bytesBuffer := bytes.NewBuffer(b)switch len(b) {case 1:var tmp int8err := binary.Read(bytesBuffer, binary.BigEndian, &tmp)return int(tmp), errcase 2:var tmp int16err := binary.Read(bytesBuffer, binary.BigEndian, &tmp)return int(tmp), errcase 4:var tmp int32err := binary.Read(bytesBuffer, binary.BigEndian, &tmp)return int(tmp), errdefault:return 0, fmt.Errorf("%s", "BytesToInt bytes lenth is invaild!")}
}func checkPathIsExist(filename string) bool {var exist = trueif _, err := os.Stat(filename); os.IsNotExist(err) {exist = false}return exist
}func Writefile(filename string, buffer string) {var f *os.Filevar err1 errorif checkPathIsExist(filename) { //如果文件存在f, err1 = os.OpenFile(filename, os.O_CREATE, 0666) //打开文件//fmt.Println(filename, "文件存在,更新文件")} else {f, err1 = os.Create(filename) //创建文件//logger.Logger.Info("文件不存在")}//将文件写进去_, err1 = io.WriteString(f, buffer)if err1 != nil {fmt.Println("写文件失败", err1)return}_ = f.Close()
}func FileToSlice(file string) []string {fil, _ := os.Open(file)defer fil.Close()var lines []stringscanner := bufio.NewScanner(fil)for scanner.Scan() {lines = append(lines, scanner.Text())}return lines
}func GetFileSize(filePath string) (int64, error) {// 打开文件file, err := os.Open(filePath)if err != nil {return 0, fmt.Errorf("error opening file: %v", err)}defer file.Close()// 获取文件信息fileInfo, err := file.Stat()if err != nil {return 0, fmt.Errorf("error getting file info: %v", err)}// 获取文件大小fileSize := fileInfo.Size()return fileSize, nil
}

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Python精选200Tips:166-170
  • 如何确定SAP 某些凭证或者单号的号码编码范围的 OBJECT 是什么?
  • Selenium与数据库结合:数据爬取与存储的技术实践
  • UE学习篇ContentExample解读------Blueprint_Communication-上
  • Leecode刷题之路从今天开始
  • 吸尘器制造5G智能工厂物联数字孪生平台,推进制造业数字化转型
  • 经验——CLion通过SSH远程开发__imx6ull的linux开发
  • 【MySQL】数据库基础认识
  • 分区与分桶
  • PlayerPerfs-不同平台的存储位置
  • 最大似然估计,存在即合理
  • Python自动化测试中替代Seleium库的解决方案
  • JSONC:为JSON注入注释的力量
  • 手把手教你CNVD漏洞挖掘 + 资产收集
  • 最新版电子发票样式html+css--普通发票+增值发票
  • 【402天】跃迁之路——程序员高效学习方法论探索系列(实验阶段159-2018.03.14)...
  • AHK 中 = 和 == 等比较运算符的用法
  • If…else
  • js数组之filter
  • MD5加密原理解析及OC版原理实现
  • MySQL主从复制读写分离及奇怪的问题
  • SpiderData 2019年2月13日 DApp数据排行榜
  • Storybook 5.0正式发布:有史以来变化最大的版本\n
  • TiDB 源码阅读系列文章(十)Chunk 和执行框架简介
  • TypeScript实现数据结构(一)栈,队列,链表
  • vue从入门到进阶:计算属性computed与侦听器watch(三)
  • 百度贴吧爬虫node+vue baidu_tieba_crawler
  • 对话 CTO〡听神策数据 CTO 曹犟描绘数据分析行业的无限可能
  • 分布式事物理论与实践
  • 更好理解的面向对象的Javascript 1 —— 动态类型和多态
  • 聊聊redis的数据结构的应用
  • 前端技术周刊 2018-12-10:前端自动化测试
  • 使用SAX解析XML
  • 责任链模式的两种实现
  • 带你开发类似Pokemon Go的AR游戏
  • #Linux(make工具和makefile文件以及makefile语法)
  • #pragma pack(1)
  • #pragma预处理命令
  • #知识分享#笔记#学习方法
  • (02)vite环境变量配置
  • (1)Nginx简介和安装教程
  • (12)Linux 常见的三种进程状态
  • (173)FPGA约束:单周期时序分析或默认时序分析
  • (HAL)STM32F103C6T8——软件模拟I2C驱动0.96寸OLED屏幕
  • (JSP)EL——优化登录界面,获取对象,获取数据
  • (solr系列:一)使用tomcat部署solr服务
  • (vue)el-cascader级联选择器按勾选的顺序传值,摆脱层级约束
  • (zhuan) 一些RL的文献(及笔记)
  • (保姆级教程)Mysql中索引、触发器、存储过程、存储函数的概念、作用,以及如何使用索引、存储过程,代码操作演示
  • (非本人原创)史记·柴静列传(r4笔记第65天)
  • (三)模仿学习-Action数据的模仿
  • (算法)求1到1亿间的质数或素数
  • (学习日记)2024.03.12:UCOSIII第十四节:时基列表
  • (杂交版)植物大战僵尸
  • (转)jdk与jre的区别