GachiLoader:利用API追踪技术剖析Node.js恶意软件

本文深入分析了名为GachiLoader的新型高度混淆Node.js加载器恶意软件,介绍了Check Point Research开发的开源Node.js追踪工具,并详细剖析了其使用的利用Vectored Exception Handling实现PE注入的新型技术。

GachiLoader:利用API追踪击败Node.js恶意软件

研究团队: Sven Rath (@eversinc33), Jaromír Hořejší (@JaromirHorejsi)

关键要点

  • YouTube幽灵网络 是一个恶意软件分发网络,利用被入侵的账户推广恶意视频并传播窃密软件等恶意软件。
  • 观察到的一个攻击活动中使用了一种新的、经过高度混淆的Node.js编写的加载器恶意软件,我们称之为 GachiLoader
  • 为了更容易分析混淆的Node.js恶意软件,Check Point Research开发了一个开源的Node.js追踪器,它显著减少了分析此类恶意软件和提取配置所需的工作量。
  • GachiLoader 的一个变种部署了第二阶段的恶意软件 Kidkadi,它实现了一种用于可移植可执行文件(PE)注入的新技术。该技术加载一个合法的DLL,并滥用Vectored Exception Handling来动态地将其替换为恶意负载。

引言

在之前的一份报告中,我们研究了YouTube幽灵网络,这是一个利用被入侵账户滥用平台推广恶意软件的协调集合。在当前的研究中,我们分析了该网络的一个特定攻击活动,其突出之处在于部署的恶意软件使用了一种先前未文档化的PE注入方法,该方法滥用Vectored Exception Handling来加载其恶意负载。

攻击活动概述

与我们之前记录的类似,感染链始于被入侵的账户,这些账户托管旨在诱使观众从外部文件托管平台下载恶意软件的视频。此活动的主题是游戏作弊器和各种破解软件:

图 1 — 被入侵账户示例开始分享恶意游戏作弊广告

视频描述随后提供包含恶意软件的存档文件的密码,以及通常包括禁用Windows Defender的说明。

我们识别出属于此活动的超过一百个视频,它们累计获得了大约 220,000次观看。这些视频分布在 39个被入侵账户上,第一个视频于2024年12月22日上传。这意味着此活动已持续运行超过9个月。在我们向YouTube报告这些视频后,大部分已被下线,尽管新视频将继续出现在新被入侵的账户上。

自我们开始监控此特定活动以来,它部署了 Rhadamanthys 窃密软件作为最终负载,该软件通过一个我们称之为 GachiLoader 的自定义加载器分发。

GachiLoader

GachiLoader 是一个高度混淆的Node.js JavaScript恶意软件,用于向受感染的机器部署额外的负载。Node.js是威胁行为者为传播恶意软件不断适应使用非传统编程语言和平台的长线工具之一。

由于混淆的JavaScript需要大量时间和精力进行手动去混淆,我们开发了一个 Node.js脚本追踪器 来动态分析此类恶意软件,击败常见的反分析技巧,并显著减少手动分析工作量。这个工具不仅对 GachiLoader 有用,而且对任何分析高度混淆的Node.js恶意软件的人都有用。因此,我们决定在此与研究人员社区分享:

https://github.com/CheckPointSW/Nodejs-Tracer

一些分析过的 GachiLoader 样本会丢弃一个第二阶段的加载器,我们称之为 Kidkadi。这个加载器特别有趣,因为它实现了一种 新颖的PE注入技术,该技术欺骗Windows加载器从内存加载恶意PE,而不是合法的DLL。我们分析了这种我们称之为 Vectored Overloading 的技术,并在下面分享的概念验证(PoC)中重新实现了它。

技术分析

GachiLoader的JavaScript模块使用 nexe 打包器捆绑成一个独立可执行文件,大小大约在60到90 MB之间。 nexe 是一个开源项目,它将Node.js应用程序编译成单个可执行文件,并捆绑Node.js运行时,以便该文件可以在没有安装Node.js的主机上运行。虽然可执行文件的大小相当大,但受害者期望接收一个软件包,因此并不显得可疑。工具 nexe_unpacker 可用于从PE中提取混淆的JavaScript源代码。

图 2 — 混淆(但格式化了)的JavaScript源代码

反分析功能

为了避免被安全研究人员或自动化沙箱分析,GachiLoader JavaScript模块采用了多种反虚拟机(anti-VM)和反分析检查:

  • 检查总RAM是否至少为4GB。
  • 检查是否至少可用2个CPU核心。
  • 将用户名与一个列表进行比较,该列表包含可能与各种沙箱或分析系统相关的用户名(完整列表见附录A)。
  • 将主机名与类似的主机名列表进行比较(完整列表见附录B)。
  • 探测正在运行的程序并与一个列表进行比较,该列表包含分析工具、沙箱指示器或虚拟机上运行的常见程序(完整列表见附录C)。

然后,恶意软件继续运行多个PowerShell命令,通过 WMI 枚举系统资源和功能:

  • 检查是否存在至少一个端口连接器对象:(Get-WmiObject Win32_PortConnector).Count
  • 获取驱动器制造商并与黑名单比较:Get-WmiObject Win32_DiskDrive | Select-Object -ExpandProperty Model(所有驱动器制造商列表见附录D)。
  • 通过 Get-WmiObject Win32_VideoController | Select-Object -ExpandProperty Name 解析视频控制器,并根据与VM环境相关的黑名单检查名称(所有视频控制器名称列表见附录E)。

如果任何这些检查表明存在虚拟机、沙箱或分析环境,恶意软件将进入一个循环,向良性网站(如 linkedin.comgrok.comwhatsapp.comtwitter.com)发送 HTTP GET 请求:

图 3 — 当检测到实验室环境时无限循环发送GET请求

最后,为了避免在短时间内多次运行,程序在首次运行时会在 %TEMP% 目录中创建一个随机生成的、带 .lock 扩展名的互斥文件。如果该文件已存在或在过去5分钟内被修改过,程序将终止。

我们能够使用Node.js Tracer轻松绕过所有这些反分析检查:该工具钩住相应的方法,并将结果欺骗给调用者(即恶意软件),允许脚本运行并暴露其恶意行为:

图 4 — 使用Node.js Tracer绕过反分析检查

通过UAC提示进行权限提升

如果恶意软件判断环境不是沙箱,它会通过运行 net session 命令检查是否在提升的上下文中运行,如果由非管理员用户运行,该命令预计会失败。如果命令失败,恶意软件会尝试使用以下PowerShell命令以提升的上下文重新启动自身:

1
powershell -WindowStyle Hidden -Command "Start-Process cmd.exe -Verb RunAs -WindowStyle Hidden -ArgumentList '/c \"<path_to_program_itself>\"'"

虽然这会触发UAC提示,但受害者很可能接受该提示,因为他们期望运行某种软件的安装程序,这通常需要管理员权限。

防御规避

为了避免检测后续负载,恶意软件尝试通过运行 taskkill /F /IM SecHealthUI.exe 来终止Windows Defender的 SecHealthUI.exe 进程,并通过 Add-MpPreference -ExclusionPath 为以下路径添加Defender排除项:

  • C:\Users\
  • C:\ProgramData\
  • C:\Windows\
  • 对于所有其他现有驱动器,在根目录下(例如 D:\)。

此外,通过 Add-MpPreference -ExclusionExtension '.sys'*.sys 文件添加排除项,尽管我们未观察到分析过的样本丢弃任何 *.sys 文件。

负载投递与执行

为了获取下一阶段的负载,恶意软件有两个变种:

  • 一个变种从远程URL获取负载。
  • 另一个变种丢弃另一个加载器 kidkadi.node,该加载器使用Vectored Overloading方法加载最终负载。该负载嵌入在加载器的JavaScript源代码中。

第一个变种 - 远程负载

图 5 — 第一个GachiLoader变种加载远程负载

GachiLoader 首先获取其运行主机的信息,例如防病毒产品和操作系统版本,并通过 POST 请求发送到其C2(命令与控制)地址的 /log 端点。所有样本都嵌入了多个C2地址以实现冗余,并依次尝试每个地址,正如我们通过追踪器追踪调用时看到的那样:

图 6 — 通过Node.js Tracer追踪到的C2通信

接下来,向 /richfamily/<key> 端点(其中 <key> 是每个样本的唯一值)发送带有 X-Secret: gachifamily 请求头的 GET 请求,获取要下载的最终负载的URL,该URL以Base64编码。只有再次使用正确的 X-Secret 请求头(这次使用嵌入在二进制文件中的唯一密钥,例如 X-Secret: 5FZQY1gYj0UKw4ZC99d1oNYR8LvTPtrfN357Eh5gmRvsMaPYgXtMxRXpMb2bTFOb2h2HqMnvUKT9CUpj9864gckmPUzf9uLIIU9)时才能获取最终负载。否则,Web服务器将返回 Forbidden 错误。

然后将最终负载下载到 %TEMP% 目录,并使用随机名称保存,模仿合法软件,例如 KeePass.exeGoogleDrive.exeUnrealEngine.exe 等,其中包含使用VMProtect或Themida打包和保护的Rhadamanthys窃密软件。

第二个变种 - Kidkadi

我们在野外观察到的第二个变种并未联系C2服务器获取第二负载,而是嵌入了负载,该负载通过另一个加载器执行,该加载器被丢弃到磁盘的 %TEMP% 下,命名为 kidkadi.node

图 7 — GachiLoader的第二个变种丢弃Kidkadi

.node 文件是Node.js的原生插件,本质上是可以通过 dlopen 从Node.js代码调用的DLL。因此,当Node-API没有暴露足够的功能时,开发人员可以使用它们。

恶意软件向Node.js公开了一个要调用的函数,该函数的名称在不同样本中有所不同。在某些情况下,名称以及某些样本中的错误消息具有俄语起源:

图 8 — 向JavaScript代码公开一个函数

加载器通过这个暴露的函数将负载PE作为二进制缓冲区传递给 Kidkadi,然后通过反射式PE加载运行该负载。我们发现该加载器采用了一种新颖的 模块重载 手法,滥用Vectored Exception Handlers(VEHs)来欺骗Windows操作系统在调用 LoadLibrary 加载任意DLL时运行最终负载。这种技术尚未有文档记录,表明作者对Windows内部有相当的理解。我们将这种方法命名为 Vectored Overloading

通过Vectored Overloading进行PE加载

恶意软件首先从合法的 wmp.dll(Windows Media Player使用的DLL)创建一个带有 SEC_IMAGE 属性的新节。然后,它用负载(要加载的PE)的内容覆盖这个节,并通过 NtMapViewOfSection 将该节的视图映射到进程中。然后,PE的各个节被逐个复制到内存中,并应用重定位以及正确的保护:

图 9 — PE映射器

这导致恶意的PE视图被映射到进程中,该视图由合法的DLL wmp.dll 支持。这个节视图正是Windows加载器(即 ntdll!Ldr*)稍后将被欺骗加载的对象。

由于通过 LoadLibrary 调用的Windows加载器并不加载任意的PE,只加载那些具有DLL特性的文件,因此如果负载不是DLL,则会将 FileHeaderCharacteristics 设置为 IMAGE_FILE_DLL。此外,入口点被清零,可能是为了避免加载器调用非DLL的入口点。如果负载是DLL,则头不会被更改。

图 10 — 检查并更新FileHeader的Characteristics

之后,恶意软件注册一个Vectored Exception Handler(VEH)。 VEH是用户模式回调,当发生异常时由操作系统调用。滥用VEH的一种常见恶意软件技术是在特定指令上注册硬件断点(HWBP),当到达该指令时会触发异常。该异常随后由VEH处理,它可以拦截调用并,例如,更改参数。这实质上允许在不修补内存的情况下挂钩函数,就像使用经典的蹦床挂钩时那样。

在这种情况下,硬件断点设置在 NtOpenSection 上:

图 11 — 在NtOpenSection上设置硬件断点

然后恶意软件通过 LoadLibrary 加载 amsi.dll,从而启动注入:

图 12 — 加载目标库并移除异常处理程序

LoadLibrary 的调用内部最终会由Windows加载器创建要加载的目标DLL的节对象,该对象通过调用 NtOpenSection 打开。这会触发硬件断点,随后触发先前注册的VEH。这是主要注入逻辑实现的地方。

为了使加载器映射恶意的PE而不是实际的 amsi.dll 节,指向 amsi.dll 的节对象被交换为之前创建的恶意负载节。VEH只是将之前创建的节句柄放置在栈上对应于 NtOpenSection[out] PHANDLE SectionHandle 参数的位置。然后,VEH将指令指针 eip 前进到 ret 指令并恢复执行。这跳过了对内核的实际调用,同时仍然返回一个有效的句柄,实质上模拟了 NtOpenSection

图 13 — 跳过对NtOpenSection的调用,用指向恶意负载的SectionHandle替换期望的输出参数

在退出VEH之前,硬件断点被重新设置为 NtMapViewOfSection

图 14 — 在NtMapViewOfSection上设置硬件断点

NtMapViewOfSection 随后被Windows加载器用来将节映射到进程中,这再次触发硬件断点。为了确保恶意负载被映射,系统调用再次通过推进指令指针并用相关值(如节基址或节大小)替换 [out] 参数来模拟。这是可能的,因为当恶意负载被写入 wmp.dll 的视图时,节视图已被恶意软件早期映射:

图 15 — 跳过对NtMapViewOfSection的调用,用指向恶意负载的指针替换期望的输出参数

然后设置最后一个硬件断点到 NtClose,恶意软件在此处简单地验证正确的节句柄被关闭。

图 16 — 在NtClose上设置硬件断点

回到程序的正常流程中,在VEH之外,如果负载是常规PE,入口点将被调用。如果它是DLL,加载器期望它是另一个 .node 模块,并解析正确的导出进行调用:

图 17 — EXE和DLL调用

与此活动完全无关,我们发现一个原始文件名为 HookPE.exe 的文件,它是该技术的64位PoC版本,带有调试打印信息,使用该技术将 calc.exe 加载到内存中。此二进制文件中的错误字符串表明加载器使用来自 libpeconv 的代码进行PE操作。

图 18 — HookPE PoC项目,使用相同的技术

与“经典”的RunPE风格反射式加载相比,这种注入技术具有多重优势:

  • 就像使用模块重载技术时一样,注入的DLL将显示为由合法映像(如 wmp.dll)支持,因为该节最初是为该DLL创建的。但是,由于内存中的代码将与磁盘上的代码不同,因此像 Moneta 这样的工具能够检测到它。 图 19 — 虽然Moneta检测到不匹配的模块,但大多数分析工具显示原始DLL名称
  • 一些加载器工作被卸载到Windows加载器。这显著降低了恶意软件作者的复杂性,因为他们不必实现例如解析导入或TLS回调,这反过来提高了负载兼容性。例如,许多公开可用的PE加载器不能正确处理TLS回调。
  • 通过模拟系统调用,相应的内核端回调(如ETWti)不会被调用,因为对内核的调用被完全跳过。这可能会欺骗仅依赖这些节的ETWti事件的安全解决方案。当然,注入之前的早期调用(当映射映像时)仍然会触发这些事件,但不是通常预期的顺序。

我们发布了一个重新实现的64位变种注入方法作为工具,供安全研究人员分析该技术和测试检测规则:

https://github.com/CheckPointSW/VectoredOverloading

大规模动态配置提取

由于JavaScript源代码的去混淆是一项繁琐且部分手动的工作,我们决定通过Node.js Tracer运行所有可用的 GachiLoader 样本,以绕过反分析检查并接收最终负载。通过钩住文件系统相关的Node API,下载的文件在恶意软件试图删除其痕迹之前被保存下来供分析人员使用。

图 20 — 追踪器显示GachiLoader将Kidkadi丢弃到磁盘

GachiLoader 两个变种的最终负载都使用Themida或VMProtect进行打包和保护。在自动沙箱中运行它们时,从内存中转储未受保护的配置,然后允许我们提取最终负载的C2服务器。

图 21 — 最终负载的Detect-it-Easy输出

作为此活动一部分的所有分析样本都丢弃了Rhadamanthys作为最终恶意软件。提取的C2服务器可以在下面的IoC部分找到。

结论

为Node.js平台编写的恶意软件已变得越来越普遍,并且大多以混淆形式出现,静态去混淆和分析起来很繁琐。通过使分析人员能够使用我们的开源Node.js Tracer动态追踪和钩住Node-API执行,在分类和分析上必须花费的时间显著减少,并且可以轻松击败常见的反分析检查。

GachiLoader 背后的威胁行为者展示了精通Windows内部结构的能力,创造了一种已知技术的新变体。这突显了安全研究人员需要及时了解PE注入等恶意软件技术,并积极寻找恶意软件作者试图逃避检测的新方法。

YouTube幽灵网络背后的威胁行为者利用对YouTube平台的信任来诱骗受害者下载恶意软件。用户应特别警惕提供破解软件、破解补丁、修改器或作弊器的提议,因为这些文件经常被植入旨在窃取数据和/或危害设备的恶意软件。虽然安全社区和YouTube都积极努力识别并删除此类内容,但这些攻击仍然持续存在。

保护措施

Check Point Threat Emulation 和 Harmony Endpoint 提供全面的攻击战术、文件类型和操作系统覆盖,并保护免受本报告中描述的攻击和威胁。

威胁指标(IoC)

描述
.zip 存档文件 062d342f59136c3bbc729e0c412d2c2589b6f9058912583eeb9b61d7916db00e34e1cd959c0c586fcd495225803061e6e2a19e7818c47a46a47822ba6726500d434fc84cc190bb0c8af86d3566d6517672fed9c171eb0df5c7541f0dce679c8b606eca698d0d4a67b21428b0812a261daab36598fded60b189106b0b27992225775b05b8cc8d03751828986727cd1929caf6868e1df9cd21e9366c48ce161c5e872fde8128f3a0f074975b6ca0d83fa56a8289b2063351f298bbf0c9025948d399f4755fd9b25aadae4e154d661ccceecbbb3d4343dc6c81e04aa81516be81d0a4e2c0ffb93103db23777c12b48a31816b83b0799c9bc71e92bb576e884d76d4b48f3e7e6c67bfb3c73c85a33a377f9bb840e1b7b09871ab29a19cdb7965d5d1c4266da90d6c655388ae8d64aebf5f9178adbbe486b2249e6bb7d18451f28a3bcc95609cc375263129b8f425800a9bb462055b11dbf0d8aef2b3312aa2e90daff0de35ff0b889c7e93a89e918488a33aa21e4b6e7743ae87f1993ea77b237ecf
变种1 – GachiLoader 00bcfecad4b679f72c50cbdcd883caf55b6a1f641258a636317871c7b894015600db4aa911e95ecfafa6f10ebfeb9f0a8051ee63de51ea1d9515ece5be2a294b01a3da42f74578c0b7c1146f30eceb2a2bc26c2d814a48fcf29ae527a1048aff028711c1b435c773ba600a863f4d4a2d1218860de799a1275d15d4ea93f0cbef02c0de5116d9b05d930e4858cd9768cc2ba70e91be62690439537fdf0f52de53032a297bfbdc94226f0d88c77ab27148c54ebde6bfa2750fed09b1d8667ddcd603d55245ef2766943813c0d1eaa3859d3918ee6fed2705bb5eeb38f4f87a5643079a180eeed0f4fc84c2412ba0398a79c5262efa1d9e8fd53290cd001b5abf9f094240cd298de1121da36adb96b3cdd632f866837f27e3951b6a0a544e5437f60a6d41411ef3c65540a525dc5c3ab0964cd595aa73c3a477a8a96ec9862776600bd44592e75854a1c763384bf9dcea6dfe1174f6f45df342ebd9dfaa3a27dc850c03845b9e2ff5ddac56f6e75b8e9dadf1a7bd1681d074e732478596b31739220f81656ce724b65c230c4d63259c3a0edff20cc664de964f16451417eda6000514bfaf75b5c7ffac451f41352f8e94b6cc060efe7d645189795fa921f4e602bc16b2f7d9d4ace9e3004bd47f97c252a7fea21662656ec6b906d30a6b21900fc418649874ab887ab613a3ccdd7cddc683e2b21f7cbe0762d2ce8201fc7e57540c1d28c23b271eb2156bf2780cb0dd042573f38f4758ef61877a7347bbbc756c8b1ebeca5dc62d759904c47597ebeb67865017a99892081c94d7647206b78a6cd21f35a5ee4ead5c286f3e0d3ddecaf8789f12da7b8b7422b0511af619353284b72038f38ccd42cd1df84abfb5915e3a6eb9c976b8d822768068343716f46a09f1210d821109ec1dff3b92ad3cfdde59912581327f4017b754864ba1e263c3c3662601d2c2b4515d3f1414d4543cfe2091490e2502457eab6c437a310f7e5e2a1a266216b097561e57448b940c3087b82c4cea7581b67e5dcc52c8c4dfcbbf8333278c5a0acd6603947e59e1961642279e29cc4b9be299c8edb7b719d6568eb8da29fac0ce48b9114990a4dd942d6de1da55bf9c49938929123fb1f221be385eff2a87f4d47ad95f4eb46c08a4d33fd4732c10a1408db1b758871dfe6b1059c6bb2e5389a32a6c21fec476fdc6e80fcb577de31c43adb7c090c3a11f3b048787ed2fee47e12ca72863ee132d63dcac3b39aeace1a4d71b0aa14a30b56ecabf29c930bcfa6bbb5e9d9bc64c65a27e1565a9ad21af3d5e1f202933a340cc400abdb93124fe59b26dc77c1e4b4d615112928ca1830c890c8c77e853ac6948069ba4633151700d8f13cf55ad46148cd46ccb0b3409c0adf253433f16ef6612e9280eb231dac5bc21b0dabfac51cb99c821e62421c39949971a44898a1ec15efe33e8c432855dba1ec6b3c9ba422cf9203d8130e59dcb5235764b8f56b6d02970a5e5b533dce93dfdb43f47ef1d36e2dd16725ed365300c371dc45491b52afe13b6e4123630538febdedf693ca9d996c3f1998d50c97052ab99e653d95b381ddb3546ef38a7feb5ab611e6a487ce8b048732f7721484ceebb316fd34c9cc611dbc4e3cc38ac7917ce895448203e6d14f121850ecc4ff89f530e792c794d771f881c7b073c16548ab32996a58298978b20db1d4133827298e166f93b7c943dc3ffc517823d8c1469de3bb01ef72992e07d1feea9380183983327576978851b8c78ea7fc93ed63941e7411e93f644a064094b5a6c7e2a9547840a5198dd7f6b4d45ef9eea401e7b72f4b7ed4119b625ab34c2c7d37c0dabc08bbdf943fd291445e2fe753c40f899294ea02f7a9823ada63c869ede18a8afc6238aedb62d2b30a2744cb8464210e9e1df0bc41e497285483782609c0b4777ef6682fb40b0d25c8149c9f3d2428f86204b69f31dfc3f3479d18d23b15cad63d72998a8418e8da22941c7495643b1a11962f83db6bb59bb7467d5456e852d0421ca5eaeae3a249a34839e67b443d9130c8f077a5885842bda24bee19e4dd231c49f88629442e5b9f02ff5f33a45fd42669157357f1e16c0b542eab5836061f5b2e2160a5104a4bde38cac85bf47ab9b9deaa14202b94320df16f52c8d98adee49c9bff8909ab5deefcbdfe40148a269d2c083868e2b5347012afb85bb3c233c9f042985bcab764f77883166604b71d8cb7ce8de8d557283df3543aa2aed89dd5446c7acf855c0ea2e5e7e89dd4be48937c603c910c29de2af3b0d3e3bc05b809b19190e90ade2489a347d8b034ed90af2fb3fe13eb8ab69fe2fcd82a0775426da33da4ba043d7e7e2fd4a18f74f8c55cb3f99741f4fcccdbcff07c7d0b8ab7fa23dcbf8847d7a37a35f6d3f5e4f95af5b4a1569eb54f6995e547584f429a49895d0d81c71d74970275b170a085173c6b57642dd89dba2f039a1ad630d6d73d3557248dd09ca2a51a329e6119e53ac3b1601f2fa43121cfd43ac9b49f6751a8b84b4ffcc5a1241f71eb1e8d7b85538d6f24e1330c934cdfc95188aede5c9668154376e507c41fe1c752cdd7a5b561436df09f87be34317eeb25a2b7bf5c67201fa501262f72a9a63b9977ae21759c93f81063e8b77b20292d1d03598f74d997690aac41f5fb7a248ac8ad866c25c88a6efd0a713460dcf8b828575285be3a43d6481e245662bafb3472d344dd15d1bf72af319901f22d78625d60c877d7a8d6c54bbbcbcfc643376558e1762115dbdd6d45021383a3c76b2e0c7258a7b0fcfd70904602eb2fb1afe3b33efc80e60de97fff85ba6f0b114fe565f53ffc1ef43a19de95c31299884e034f05dc037616b74a6b17b70bae357c43cf03fe1946abc36eea1d0e7d911ca29bd067f63ae61e215ccf73cba014ecd72abd38ff78d5a23c2727577c5b3e2c8f52b90dc2a4a62bab101900a92db76e2c368c4a83f7340f42c460b16d11dea94c8db002d5bc962bcb939df4a8b7bdc896cd229cf34f55d93555c14e5816ac2aa6285d1cf41126463e7f48f01f482fa846bc106de245c833ad7c3ea7fae4caa7ead54b2901cb964d6d018e3b7a1d718b96d9950b3579af2a784ab004ad575e13cb41b2c27aa2566e684ab10b1daf2a46df1031c6ddc331ab80af4e21144a68997d4a1859e9fd76985717a754fe121e99c337cc33b0e9a25852fa33c580dc9caaffefaf090823369c0084b78bf963997033759fa45933b61de425aea7612a06289ec6c784927456a8dd64af57926514131efdc388c9883db2c23aebfd8b97c44e808d637f0fc236b80c4fa88fbb35af2b254c63586fd6e0455d0e917b842afc79b821ac87a2b9d6c428016506c2ae076d049deeba60514cb8c0afa6fd00fc349722cdbc6e1b3056d5af67f05c9db6763cd494f64c5f62faeb8f1b67ba26a7ab278e27d4c9b8f226f1b97838bc5702954ef5f536de86a8477e0008f25bcfba72b7fda4c1f37b9d26fcd071c6ab51e71407e8bf242fac8552a10aedff113c9efb92ccc53cc49fbed7029d2c60ee04772d9dc4d8d34f5effd3e3be17769584bbf912954e92628013171415238f740c7528f3314f94dc07ffd9b802a34c3997b09ad02da1bcd3c8137717f05b96a344b1fdd159b4c45e3089a26d1f64e63cd4ac2ed3bf2db33074c3a765041cdf97bf0b55734cd5619d7d4568a641ae3fc35540344a488184839674b78908b01a8d959b80f7fa1f42c734c4c64a8cec58394f94cf362b8efd38c7b9c78b6c96910d8f1e3889bad17f97cd26aed5f6c7a15432cc11c2224a8c9adf6917a155a20c1e5df83b566fdad3bf59ca49ac6559e0561233a95c7cd70a5caa6dc7db2025192f4f2497bdc356c1920dfc4740bb868de8a6b5786f01865dfa9e564825bd0b103d647c296bd2b9eee251b04b7f5dc72f27898fcf0fc25ca245871258295cddaf1c23d554b90e4d1ee1ca064f68124df63003a046f58241c3513cd1e8383a421e9a4f55af53cf1911680042659b28722cff8a30cd202bc728a8fea238443994a687269f2d7d19678e571ce1a1658df7da69c25b4bd902f87f849c98d857f68127546f829861e796b11b80304e2c53e70e54191fec8087f64d7c8146d86082a735440124bae953c0a68e5eff6a7fe6792f90ea1e71cc0c83a724bc27387c1c62369657904418affebca3f706a4e968dd1a672729274ba287dfae43be488938ad37225074c923ac4baa0b4a171076c273cb064a4905c66a25ca3acfee08d473631c12231079a241d63ddc9e4b537d2531135e9aa4d795abf22f2aefd398db4cf8f666b7c4ec5051139570b5d3b88569c9e62de31249a70b6cdc716aecd9211d6fb5db70a51ba5795e0a7126aa1efd0f4b78262031dfb72e98c319ce37e95760397b9
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计