本文献给所有像我一样刚从事安全行业的萌新,希望能对您有一定帮助。同时感谢所有在我迷茫时伸出援手的大佬们。
REvil又称Sodinokibi,是一种以私有软件即服务(RaaS)体系运营牟利的黑客组织。他们通过招募会员为其分发勒索软件攻击,当受害者支付赎金后该组织会将赎金与会员进行分成,因此很难定位其组织真实所在地。
md5:FBF8E910F9480D64E8FF6ECF4B10EF4B
sha1:E6B32975ACB2CC5230DD4F6CE6F243293FD984FA
sha256:CEC23C13C52A39C8715EE2ED7878F8AA9452E609DF1D469EF7F0DEC55736645B
VT:4d1K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2$3K9i4u0#2M7%4c8G2N6r3q4D9i4K6u0W2j5$3!0E0i4K6u0r3k6%4g2A6i4K6u0r3k6X3W2D9k6g2)9J5c8X3y4W2j5K6t1K6j5K6p5K6j5K6f1J5j5e0x3&6j5K6R3%4x3e0g2W2k6e0u0W2k6o6M7^5y4K6S2X3z5r3q4S2z5e0b7#2x3X3f1$3x3o6W2V1k6U0q4V1y4o6j5&6k6h3j5%4k6U0m8V1k6h3x3#2y4e0M7K6y4U0j5@1y4h3u0Q4x3V1k6V1k6i4c8W2j5%4c8A6L8$3^5`.
VT沙箱以将其标识为Sodinokibi。
查看节区信息,可以看到存在一个自定义节.11hix,且各个节区熵值都较高证明程序存在大量加密数据。
查看导入dll,识别出导入了kernel32.dll和user32.dll两个模块。证明样本对程序的导入表做了处理。
查看识别出的API,没有什么重要内容,恶意行为被隐藏起来。需要病毒样本动态运行进行解密修复IAT。
查看字符串资源,大部分为未识别的字符串,进一步验证了数据被加密处理的猜想。
第一次静态加载时应该尽量勾选右下角选项,这样IDA会额外加载资源数据动态导入的DLL数据以及默认不加载的静态节数据,以便我们透彻分析样本。
加载完成后,我们可以看到入口点OEP只有两个函数调用
点击进入第一个函数调用,并没有发现有趣的内容,继续点击跟进第一个函数查看内容。
进入函数后发现了有趣的内容,病毒样本貌似在填充一个由ESI寄存器控制的数组。首先传入数组的4字节数据给sub_406817函数,然后将运算结果(eax)的值重新回填给数组,一共循环了160次,计算了640个字节。这与我们熟知的动态解析IAT方法很相似,加上我们之前分析病毒抹去了IAT可以合理猜测该处在做回填IAT表的操作。
使用F5插件查看如下
点击查看该数组内容,发现是.data节的一个全局数组。根据之前的分析运算一共占用640字节。每4字节为一个hash值(经过hash处理的数据通常会将4字节全部占满,所以合理猜测为hash数据)
设置4字节的hash数组,数组元素为160,每个元素占用4字节。
处理后的数据如下
最后我们还需验证下我们的猜想。程序调用动态导入函数的时候,汇编会以间接调用的方式呈现出来,如下形式。我们只需要在后续找到该形式的函数调用,且数据在该数组内即可验证成功。
继续向下查找,可以看到“call 数组基址+数组偏移”的形式的函数调用(数组最大偏移为0x280),该调用在重复调用同一个函数。验证了该静态数组为hash处理过的IAT表。
同时也可以确定第一个函数整体逻辑在修复IAT表,修改变量名。
进入mv_dy_get_api函数查看代码内容,可以看到IDA为我们解析的函数原型如下,虽然数长度相同但是看着很别扭(IDA最强大的就是注释功能,不要小看每条注释。小的注释积累起来可以引发质变)
按Y快捷键修改函数原型如下
继续向下查看代码,将输入的hash进行异或运算得到转换后的hash,又将转换后的hash值高11位提取赋值给临时变量v2。可以看到
继续阅读代码,看到疑似switch/case的判断比较高11位数据。根据不同数据进行执行不同流程中的函数地址赋值,然后跳转到LABEL_29。根据病毒动态加载的经验,第一件事一般是动态获取目标模块的基地址,然后根据基地址解析对应导出函数的位置。所以我们合理假设该switch/case语句是在做获取dll基址的操作。而转换后的高11位hash值代表不同的hash值。
整理后如下
继续向下阅读代码,看到调用了赋值的函数指针pfn_ld_dll,符合获取指定dll的基址的猜想,将临时变量改名为imagebase可以看到v18变量在获取导出符号的地址偏移。
0x3c偏移为_IMAGE_DOS_HEADER的e_lfanew字段,通过该字段值获取imagebase到_IMAGE_NT_HEADERS的偏移。
可以看到_IMAGE_NT_HEADERS一共占用0x18个字节,也就是说0x78中有0x18字节偏移是为了跳过_IMAGE_NT_HEADERS占用的空间,为了获取_IMAGE_OPTIONAL_HEADER结构体偏移0x60 == 0x78 - 0x18的字段值。
查看_IMAGE_OPTIONAL_HEADER结构体。偏移0x60处字段正好为数据目录表的首地址,而数据目录表第0项正好记录着导出符号表的偏移和大小。
查看数据目录表表项结构体如下
可以推出该语句为获取VirtualAddress偏移值
查看导出表结构体
整理出以下内容
按下shift+F1快捷键,切换到本地类型窗口。按快捷键ins,插入一个结构体。
双击导入该结构体
将变量va_offset转换成结构体指针
导入结构体指针类型。
整理得到如下结果。
查看后续代码,因为之前的注释修改,我们可以很清晰的看到函数逻辑。
进入calc_fn_name_hash函数可以看到是对导出函数字符串进行逐字节hash运算。
PE解析API地址整体逻辑如下
接下来继续向上回溯分析加载dll的方法。点开加载dll的函数,可以发现绝大多数函数模板类似,都传入了一个固定hash值进行函数递归。获取api后将v2变量作为参数传递给该动态获取的API并调用执行。根据函数形式结合之前的分析,合理推测0xCB0F8A29为LoadLibraryA函数的hash值。而v2是要加载的dll名称。
继续回溯,发现除了上述模板的调用方式,还有直接传入了静态hash给一个函数,这种方式值得我们额外关注。
继续跟进来到以下函数中,看到有循环猜测正在做某种运算。看到循环中有个判断条件在与一个常数做差,转换成16进制为0x41,正好对应Ascii码表中的‘A’字符。因此可以猜测在比较字符串大小。
我们首先修改v5为chr,接下来就会发现非常神奇的事,找寻其他跟chr关联的变量修改名称让整个逻辑结构一下变得清晰。
继续向上查阅代码可以看到函数sub_404B12,跟进函数内部可以看到熟悉的fs:30h。通过fs寄存器获取PEB
获取PEB基址偏移0xC处的字段值,即LDR地址。
再通过LDR偏移0x14获取InMemoryOrderModuleList地址值。
判断内存模块加载顺序链表是否为空。
InMemoryOrderModuleList是一个双向链表指针,它链接的是LDR_DATA_TABLE_ENTRY结构体中的InMemoryOrderModuleList链表节点。
获取dll的名称(F5插件在此处解析错误,所以使用汇编查看)
如果匹配hash值则返回模块基址
list_entry+0x10为DllBase模块基址
总体逻辑如下
接下来继续分析其他模块加载函数中对字符串的解密函数。首先可以看到解密函数传入了5个参数,第一个参数为一个静态全局数据,第2、3、4个参数全部是确定的常量值。
传入的静态数据
将静态数据处理成数组
进入解密函数查看内容,发现第二个函数将传入的参数重新组合传入。类似计算偏移值。
继续跟入函数,将返回变量名修改为解密可以确定最后一个参数为解密后的字符串数据。
继续跟入第一个函数,可以看到两个明显循环次数256。
汇编形式表现如下。(如果逆向时突然出现两次循环且每次循环次数都为256,后续存在异或操作基本可以猜测为rc4解密算法,这种特性为rc4算法特征)
接下来我们需要进入后续函数寻找异或算法,可以发现字节异或算法。基本可以确定为rc4算法。
首先根据Wiki百科获取rc4算法的伪代码,rc4算法分为初始阶段和加密阶段。初始化阶段首先会创建一个256字节从0递增不重复的数组。第二次循环会循环会根据密钥数组、长度、第一次构建的s盒(用来随机交换加密的数组,这里用S标识)来重新打乱S盒中,用于随机加密数据。由此可以看出,初始化阶段需要一个s盒,一个密钥以及密钥长度作为基本元素。
回到IDA可以清晰看到一个长度为256字节的数组,且无论初始化和加密阶段都使用了该数组。确定该数组为S盒。
由于初始化阶段还需要密钥盒密钥长度,我们先将参数按顺序重命名。
进入初始化函数,可以看到与我们的伪代码逻辑一致,参数传入顺序正确。
对算法进行调整如下
接下来我们查看加密数据部分的伪代码。下面i,j是两个指针。每收到一个字节,就进行while循环。通过一定的算法((a),(b))定位S盒中的一个元素,并与输入字节异或,得到k。循环中还改变了S盒((c))。从该伪代码中需要获取的参数有:打乱顺序的S盒、要加密/解密的数据、数据长度,最后返回的数据是加密/解密后的数组字符串。
与初始化部分类似,首先按照分析和假设修改变量。可以看到整体加密算法逻辑变得清晰
跟进加密函数,根据伪代码调整函数变量内容。
利用交叉引用继续向上回溯来到一下函数中,可以看到调用解密函数的父函数初始的时候就传入了:密钥长度、加密数据长度、加密的数据。而第一个参数密钥是利用基址加偏移的方式计算的,第三个在第一个参数基础上还增加了密钥长度的偏移。但是我们不确定哪个参数是密钥基址哪个是密钥偏移。
继续向上回溯,来到了整体调用的地方,可以看到第一个参数为静态未知数据(第一个参数是固定的),而第二个参数正好类似一个偏移差值。
查看第一个静态数组
调整为数组类型
返回继续调整变量可以看到清晰的函数调用逻辑:
1、传入固定的加密数据首地址+密钥偏移获取密钥地址
2、密钥偏移+密钥长度获取解密数据的地址
在分析完了病毒构建导入表和字符串加密的手法后,我们便可以开始解析IDB数据。核心手法与脱壳类似,就是对IDA未解析的IAT数组进行修复。
所谓动态修复IAT类似动态脱壳,原理是利用病毒自身携带的解密程序,在病毒自动修复完成后中断。将内存状态保存到IDB中进行解析IAT。首先我们要在IDA中设置断点,让IDA调试器运行起来断住。
可以看到iat表已经全部修复为函数地址。
按O快捷键进行符号解析。
使用脱壳插件进行动态修复IAT表
修复IAT后的效果如下。
IDAPython修复脚本是通过对算法的逆向,利用ida提供的python接口编写脚本,从静态数据中解密出我们想要的内容。以下是我编写的IDAPython脚本,为了方便没有做优化有亿点卡。(最好重新加载程序到IDA,并勾选Load Resouce选项)
程序中利用了pefile.py模块,如果当前python环境变量中没有该模块可以通过pip安装。IDAPython调试方法可以参考我的文章https://bbs.pediy.com/thread-267362.htm
IDAPython脚本
除了解析API外,该脚本解密完成后会将解密字符串以注释形式添加到rc4解密函数后,方便我们静态查看病毒后续行为。
因为python的发展,IDAPython也逐渐从python2.x过度到python3.x。IDA作者也在博客中表示过度到新的IDA7.2版本以上的python默认使用3.x并不在支持IDAPython2.x的部分接口。我们在编写脚本的过程中很可能因为使用了老版本的接口导致无法实现我们的功能。
编写时可参考IDAPython官方手册:8b8K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2Z5k6i4S2Q4x3X3c8J5j5i4W2K6i4K6u0W2j5$3!0E0i4K6u0r3M7s2u0G2k6s2g2U0N6s2y4Q4x3V1k6A6k6r3q4Q4x3V1k6K6N6i4m8H3L8%4u0@1i4K6u0r3K9h3c8S2M7s2W2@1K9r3!0F1i4K6g2X3k6r3!0U0M7#2)9J5c8R3`.`.
利用浏览器的搜索功能搜索想要的功能API(不知道有哪些接口就靠猜,比如我想要一个修改函数名的接口我尝试了很多简单词组组合最后找到了idc.set_name),如果某一模块中搜索不到,点击手册左上角的所有模块再次搜索。
还要善用IDAPython API过度表:e3dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2Z5k6i4S2Q4x3X3c8J5j5i4W2K6i4K6u0W2j5$3!0E0i4K6u0r3M7s2u0G2k6s2g2U0N6s2y4Q4x3V1k6A6k6r3q4Q4x3V1k6K6N6i4m8H3L8%4u0@1i4K6u0r3K9h3c8S2y4K6c8Q4y4h3k6A6k6r3q4H3P5i4c8Z5L8$3&6Q4y4h3k6F1L8#2)9#2k6X3u0U0y4U0V1#2i4K6g2X3M7r3!0J5N6r3W2F1k6#2)9#2k6X3N6#2K9h3c8W2i4K6u0W2M7$3S2@1L8h3H3`.
使用方法仍然是浏览器搜索匹配不同版本的接口
可以看到完整的修复IAT后的逻辑,首先动态获取API后再加载ole32模块获取创建COM组件所需接口。
退回到函数主逻辑,可以看到病毒首先修复了IAT表保证程序正常运行
设置错误模式获取错误信息并创建名为Global\01EB1FCA-9835-27F4-DB93-6F722EB23FB4的全局互斥体保证进程唯一性。
如果返回系统错误码显示已存在互斥体则返回标志1。
使用M快捷键找到宏定义对常量进行替换
替换后的样式如下
再次来到主函数,可以看到病毒有两个逻辑函数,暂且分别命名为mv_logic_x。
来到主逻辑1中可以看到以下几个解析的函数与字符串信息。
进入第一个未解析函数,用于F5插件解析错误通过汇编查看内容。可以看到该函数获取PEB地址后分别读取了偏移0xA4和0xA8处地址的低一个字节数据。
在PEB中,0xA4和0xA8分别是当前操作系统的主版本号和次版本号
调整如下,可以看出该函数用于获取当前系统版本号。
返回逻辑可以看出病毒在Vista版本以上的操作系统上运行时会执行
进入sub_404842函数可以看到该函数主要获取当前进程令牌信息。
返回的令牌类型是一个enum类型
根据微软描述一共可以返回如下三个类型。
所以以下代码为判断是否有管理员权限,如果没有则执行以下提权代码。
在判断当前用户进程处于受限权限用户组下时,病毒打开令牌获取用户SID和属性的组合值,总逻辑如下。
首先通过GetTokenInformation函数获取_SID_AND_ATTRIBUTES结构体。该结构体包含了Sid和Sid的属性。
可以看到微软官方形容SID代表一个用户组,而SID的属性用来表示当前用户组启动、禁用以及强制托管的权限属性。SID同时也标志如何使用这些属性。
病毒接下来获取SID中的偏移值,获取SID中Subauthority的第一个四字节值判断权限。
首先我们来观察下SID结构体,该结构体前8字节是用于记录SID的一些信息,后20字节分为5部分,每部分占用4字节表示域账户或组使用的唯一ID也称为SIDs。SIDs中前4个四字节元素用来表示域ID(我理解为网络域名之类的)。最后一个字节表示RID,也就是表示用户组或用户。
根据Identifier Authority字段与后续的域ID组合可以代表出不同的含义。微软将特定的组合称为"Well-know SIDs"。以下是对该类型的介绍
返回函数调用位置使用IDA插件查看内容。可以看到他在比较SIDs的第一个4字节内容。
我们通过计算器换算成10进制,并查询"Well-know SIDs"表,发现为高强制级别的含义,所以我们了解了该if判断仍然是在判断权限。我们可以看到该类型在Vista和Win server 2008中添加,这也印证了病毒为什么要判断系统版本。
接下来病毒会通过动态申请堆内存用于存储当前病毒路径字符串。
以下为病毒申请堆函数的简单分析
可以看到sub_404DC9函数将某个值赋给了变量v4,这里我们可以不用着急分析该函数,因为v4与后续的函数调用有直接关系,可以考虑先分析明显行为反向猜测推导内容。继续向下分析可以看到病毒调用了ShellExecuteExW,并且将v7作为了一个参数。
MSDN官方定义结构体为SHELLEXECUTEINFOW
将v7变量类型修改为SHELLEXECUTEINFOW,可以看到病毒实际上在通过ShellExecuteExW循环调用runas命令提权当前进程。
整理得到函数获取参数字符串逻辑如下。
分析完成前述所有代码后整理提权函数逻辑如下:首先通过令牌分别进行系统信息、令牌首先信息以及令牌权级三个方面进行判断,如果进程处于高度受限的环境下则通过ShellExecuteExW调用runas进程循环提升当前进程权限,否则就陷入提权界面循环中。(如果受害者因为厌烦而点击提权确认则病毒开始加密逻辑)
Runas是一个命令行工具,它会帮助进程提升为管理员权限运行。
接下来我们进入加密逻辑进行静态分析
进入加密逻辑函数首先看到病毒进程设置了线程状态为ES_CONTINUOUS + ES_SYSTEM_REQUIRED
第一个属性代表设置一个状态值,该值在当前病毒逻辑函数中一直有效,直到下一次调用SetThreadExecutionState(ES_CONTINUOUS )函数时该值会被清除。第二个属性通过设置系统计时器强制系统处于工作状态。
接下来病毒会调用函数,使病毒进程模拟管理员账户登录执行病毒逻辑。
首先进入第一个未解析的函数,进入函数可以看到函数内部又一次调用了一个函数,参数为1个常量,要获取的目标进程字符串名和一个函数指针。
继续跟进,病毒创建了一个进程快照通过循环对当前内存中进程信息块进行遍历,并将获取到的环境进程块作为参函数和进程名一起传入函数指针,调用函数指针指向的地址。
再看函数指针指向的函数地址,它将传入的进程字符串名和目标进程字符串进行比较,如果与目标进程字符串命相等则获取进程信息块的进程ID,并将它传入字符串指针向后偏移4字节的地址存储,所以我们可以判断出函数传入的参数实际上为病毒作者设计的一个结构体,拥有目标进程字符串指针和进程ID两个元素。
定义结构体
修改后参数如下
使用Vistual Studio查看宏对应的值计算出0xF01FF代表所有令牌权限
[培训]科锐逆向工程师培训第53期2025年7月8日开班!
最后于 2021-5-17 12:15
被独钓者OW编辑
,原因: