--- name: re-crypto-decrypt description: > 加密数据还原:定位解密函数、写解密脚本。 触发词:解密、decrypt、还原数据、解密流量 capabilities: [crypto-decryption] --- # 加密数据还原 ## 何时使用 / 何时不用 - 用:需要把密文(数据 blob / 流量 / 配置段)还原成明文 - 用:算法与密钥已知(或已由 [[re-crypto-id]] / [[re-crypto-keys]] 得出),需要批量解密 - 用:从样本里还原解密逻辑并重写为独立可复用的脚本 - 不用:算法/密钥都未知(先 [[re-crypto-id]] → [[re-crypto-keys]]) - 不用:静态可读的明文([[re-triage]] 熵低直接读) - 不用:想跑原样本看输出(那是 [[re-behavior]] / [[re-sandbox]] 的活——解密脚本是为了脱离样本复现) ## 工具准备 所有工具先验证再使用。本技能处理的是转储/反编译产物与密文数据,运行样本环节在 [[re-sandbox]] 内([[re-analyze/platform-tips]] 最高原则)。 ### python3 + pycryptodome —— 解密脚本主力 - Linux: `apt install python3 python3-pip` / `dnf install python3 python3-pip` / `pacman -S python python-pip` - macOS: `brew install python` - Windows: python.org 安装包(勾选 Add to PATH);WSL 内 Linux 版 - 加密库: `pip install pycryptodome`(AES/DES/RSA/ChaCha 等标准算法) - 验证: `python3 -c "from Crypto.Cipher import AES; print('ok')"`;`python3 --version` ### 目标程序转储/反编译产物 —— 还原算法的依据 - 转储: [[re-memdump]] 默认转储(gcore)——定位密文输入点与解密调用现场 - 反编译: [[re-ghidra]] / [[re-ida]] / [[re-radare2]] 的产物(函数反编译视图) - 验证: `file out` 是 ELF core;反编译器里能找到目标函数(`ghidra` / `rizin` 可启动) ### angr(可选)—— 符号执行补足难还原的逻辑 - 全平台: `pip install angr`——**Python 版本兼容性以 [[re-angr]] 的「工具准备」为准**(版本矩阵只在该处维护一份,避免多处漂移);依赖多,建议 venv: `python3 -m venv venv && venv/bin/pip install angr` - 验证: `venv/bin/python -c "import angr; print(angr.__version__)"` - 用途: 反编译分支爆炸/混淆严重时,用符号执行求解密函数输出(加载目标二进制 → 设密文输入为符号 → 约束求解) ## 操作步骤 按顺序执行,每步记下结果。前提:算法([[re-crypto-id]])与密钥([[re-crypto-keys]])已确认或至少有一方候选;脚本与验证结果(明文样本 + sha256)存档供报告引用。 0. **证伪"密文":先定位真实密文边界**(跳过此步是加密分析最常见的失败方式): - 高熵段可能是"伪密文"——容器/压缩数据流被误当加密层(如 zip 数据本体、附加在图片后的压缩流)。压缩数据熵同样是 8.0,与加密不可区分 - 先做整体结构观察:文件头/尾魔数、格式 marker 分布(JPEG 逐段统计大小)、尾部已知结构当锚点(zip EOCD 的 `cd_offset`/`cd_size` 反推数据起点,EOCD 签名是已知明文) - 定格式边界**不要用 first/last 裸 marker 搜索**:标记字节对可以合法出现在载荷里——PNG 的 `IEND` 字样可落在 tEXt chunk 数据内、JPEG 的 `FF D9` 可落在 APP1 等 length-delimited 段内(拿首个命中当结尾会截掉合法数据)。正确做法是按格式规范解析:PNG 从 8 字节签名起按 `Length/Type/Data/CRC` 逐 chunk 遍历,直到结构合法且长度为 0 的 `IEND`(必要时逐块验 CRC);JPEG 从 `SOI` 起按 marker/segment 解析,进入 `SOS` 后按 `FF00` stuffing 与 `RSTn` 规则处理,**语法位置成立的 `EOI`** 才是结尾。裸字节搜索只能用来产生候选位置,候选要经结构校验后才能定边界 - 同尺寸文件先比尾部 16 字节/整体哈希——"换头副本"(同一 payload 的第二种封装,仅头部不同)直接省掉一整条支线 - 压缩率是信号:解压后明显变大的层说明内容有结构(可压=编码/明文),接近 1:1 说明内层已压缩或加密 - 确认是孤立高熵 blob 后再进入步骤 1 的密文定位 1. **定位解密函数(交叉引用密文输入点)**: - 从密文偏移出发:[[re-crypto-id]] 步骤 2 的高熵区偏移 → 反编译器里找读取该偏移/该全局变量的函数 → 沿调用链看谁写入了它(写入方常是解密函数) - 从 API 出发:[[re-crypto-keys]] 步骤 4 找到的 `Crypt*`/`EVP_*` 调用点就是候选;观察入参的密文指针是否指向步骤 2 的偏移 - 动态辅助: [[re-gdb]] / [[re-x64dbg]] 在候选函数下断点(沙箱内),打印入参/返回值,确认它输出可读明文 - 找不到明确函数 → 密文可能由内联展开的算法处理(无调用边界),回 [[re-crypto-id]] 用数据特征定位(常量表引用处) 2. **反编译还原算法**: - 把反编译视图逐段抄译成伪代码,明确:算法(AES-CBC/自定义 XOR…)、密钥与 IV 来源(固定值/派生/上下文)、模式与填充(CBC 的 IV 在哪、PKCS7 还是零填充) - 自定义算法: 逐条翻译位运算(XOR/移位/查表),注意字节序([[re-proto-rev]] 坑 1 同理——长度/密钥字段先试大小端) - 还原标准算法时留意细节:AES 用 CBC 还是 ECB、key 长度 16/24/32、IV 是否复用密钥(常见错误实现,见坑 1) - 反编译看不清的循环/查表逻辑 → angr 符号执行兜底(构造求解脚本,把函数当黑盒求输出) 3. **重写为独立脚本(python)**: ```python # decrypt.py —— 按反编译还原的算法重写 from Crypto.Cipher import AES import sys key = bytes.fromhex("...") # 来自 [[re-crypto-keys]] 步骤 1/2/5 iv = key[:16] # 样本实现: IV = key 前 16 字节 data = open(sys.argv[1], 'rb').read() pt = AES.new(key, AES.MODE_CBC, iv).decrypt(data) print(pt) # 或写文件 + 后续校验 ``` - 脚本参数化(密钥/IV/输入文件走参数或配置),一次写对、反复复用——批量解流量用 - 关键:脚本逻辑必须与样本一致(填充处理、尾部截断),不一致时回查边界条件(见坑 1) 4. **用已知明文验证**: - 已知明文来源:协议头 magic(如 `\xAA\x55`)、文件头(`PK` zip / `\x89PNG`)、报文字段([[re-proto-rev]] 步骤 2 的固定头)、或 [[re-behavior]] 行为里观察到的明文串 - 验证方式:解密输出里能找到已知明文片段 → 成功;找不到 → 依次检查:密钥/IV 是否对([[re-crypto-keys]] 候选逐个试)、字节序、填充处理、是否还有外层加密(见坑 3) - 无已知明文时用"可读性"验证:输出可打印率 >70% 或通过 `file -` 识别出格式(PDF/zip/文本)→ 视为成功候选 5. **流量场景:解出明文流量流**: - 已按 [[re-crypto-id]] / [[re-crypto-keys]] 确认流量加密算法与密钥后,从 pcap 提取密文载荷([[re-netcap]] 步骤 3 tshark 导出): ```sh tshark -r c2.pcap -Y 'tcp.payload' -T fields -e data.data | sed 's/://g' | xxd -r -p > payloads.bin ``` - 写批量脚本:按流切分(每 TCP 流一段)、逐段调用解密逻辑(同步骤 3 的脚本),输出明文流文件 - 验证: 明文流里能看到协议结构(会话序号/命令字),再转 [[re-proto-rev]] 做状态机重建 - 注意会话密钥变化(每次握手重新派生)→ 脚本里为每个会话取对应密钥([[re-crypto-keys]] 步骤 5 的派生还原) ## 跨域联合 - [[re-protocol]]:本网关工作流第 4 步(解密)——加密通信链路的落地点(crypto-id → crypto-keys → crypto-decrypt → proto-rev) - [[re-malware]]:C2 流量解密——re-malware 第 4 步;解出的明文(指令/配置)进行为判断与 IOC([[re-ioc]]) - [[re-firmware]]:固件加密层/加密通信解密——配合 [[re-fw-extract]] 解包失败时的加密层处理 - [[re-crypto-id]] / [[re-crypto-keys]]:上游——算法与密钥的输入来源 - [[re-memdump]]:密文输入点定位与解密调用现场(转储产物) - [[re-anti-analysis]]:解密在壳内时先脱壳(见坑 2);[[re-gdb]] / [[re-x64dbg]] 动态辅助确认函数行为 - 解出的明文转 [[re-proto-rev]] 重建状态机,或按 [[re-firmware]] / [[re-malware]] 流程继续 ## 常见坑与陷阱 - **还原脚本与样本行为不一致 → 回查边界条件(长度/填充)**:现象——脚本解出的明文与样本自身输出不一样(多/少字节、尾部乱码);原因——边界条件没对齐:填充方式(PKCS7/zero)、长度字段是否含填充、IV 是否每包变、密文尾部是否截断;对策——反编译里逐条核对填充与长度处理代码,脚本里显式实现,再回步骤 4 用已知明文验证 - **解密在壳内 → 先脱壳**:现象——在加壳样本里找不到解密函数,或找到的函数只是壳的解压;原因——密文数据/解密逻辑被壳包着,静态看是壳的初始状态([[re-memdump]] 坑 2 同理);对策——先 [[re-anti-analysis]] 脱壳(按明文/代码 materialization 定转储时机,不把 OEP 当通用判据),脱壳产物重新做定位 - **多轮解密链**:现象——解出第一层后仍是乱码/高熵(熵 7.0+);原因——多层加密(先 XOR 再 AES,或嵌套压缩+加密,见 [[re-crypto-id]] 坑 3);对策——每层单独验证(解一层测一次熵与可读性),分层还原;可用"熵下降即前进一层"作为停止条件 - **密钥/IV 顺序用错解出乱码**:现象——已知明文验证失败,但算法确定是 AES;原因——key/iv 参数顺序(样本是 key||iv 拼接还是分开传)、密钥字节序、或 IV 复用/固定 IV;对策——把 [[re-crypto-keys]] 的候选(含大小端变体、IV=key 前缀等常见错误实现变体)做成参数组合循环试解,命中即可读性/已知明文判定 - **受限符号集 = 编码不是加密(伪加密)**:现象——熵 7.9+ 但字节值域明显受限(如只有 66 个符号、分布均匀、无周期),xortool/IC 对所有密钥长度全平;原因——这是"编码 + 单字节 XOR":**单字节 XOR 不改变频率分布与符号集**(只平移值),多字节 XOR 才会把符号打散成 256 值;对策——收集全部符号值集合,与已知字符集做 XOR 匹配(对 256 个 key 做集合比较,毫秒级,别用穷举):Base64 64 字符 + `\n` + `=` = 66 符号、Ascii85 85、hex 16 等;命中后 XOR 还原 → 解码;交叉验证:填充符(如 `=` 加密后仅出现 1 次在文件尾)、换行符频率、解码后魔数 - **xortool/IC 结果全平 = 换思路的信号**:现象——密钥长度候选全部 ~10-20% 且无突出峰值;结论——统计上不存在周期结构,多字节重复密钥 XOR 基本排除,别换参数继续跑;对策——退回观察数据本身(值域/符号集/熵),通常是编码层或一次性密钥流(流密码) - **伪密文已确认但层数不止一层 → 分层剥离纪律**:每层剥完用魔数/可读性验证产物完整性再进下一层;中间产物全部保留命名(可回滚);熵不降 / 解出仍高熵 = 还有外层(如 XOR 外再 Base64、再压缩) - **像素级 XOR(视觉密码学/图像隐写)**:现象——两张"随机噪点"图,题目提示叠加;原因——视觉密码学把信息分成两张 share,XOR(或 ADD)像素后可见;对策——`ImageChops.logical_xor` 或 numpy `a^b` 逐像素 XOR 两图(RGB 三通道);结果常是"99.5% 纯白背景 + 极浅灰文字"(文字 254 vs 背景 255)——直接看/反色都不可见,**用阈值增强**:`==255 → 黑、其余 → 白`,文字立现;位图小字再按字符网格(5x7)切分逐字读,重复字符用两两位图相似度矩阵验证(如 `d562333d` 的重复模式) - **ADD 饱和加法是 XOR 的近亲**:现象——XOR 结果无内容时;原因——share 可能由加法式秘密共享生成(XOR 得到噪声,ADD/SUB 才出图);对策——同样试 `clip(a+b)` 饱和加法与 `a-b` 减法,多运算组合对比 - **新版混淆重 → 老版本回溯定位**:现象——最新版核心函数混淆后体积异常庞大,IDA 分析卡顿,硬啃不现实;原因——同一 SDK 早期版本未上重混淆,核心逻辑更清晰;对策——按行为特征(如"同意隐私协议即触发采集")装历史版本包(豌豆荚等渠道)→ 点同意 → 抓包确认最早出现该参数的版本;选行为规范的版本做分析起点(调试方便:spawn/attach 都能复现);老版本还原出的逻辑反过来指导确认新版采集了哪些参数 - **分段/分层加密上报(每段算法不同)**:现象——抓到的上报请求字段单看都像密文,但统一按一种算法解全是乱码;原因——客户端加密流程分多段:**单字段加密 → 分段加密 → 整体加密**,每段的算法/密钥不同(如第一层随机数做 key 逐字节加密、第二层固定 key、第三层拼接组合字段后再加密);且字段本身可能是设备采集的风险特征(svc 指令采集、`r_1_0` 类字段);对策——按"每层单独验证"纪律分层剥(熵降/可读即前进一层,见多轮解密链坑);注意同源 SDK 的"整体加密"可能再套 VM 化算法(约 70 个 handle 模拟基本指令、handle 未混淆时可逐个识别指令语义);key 来源分随机(抓包不可复现)与固定(可静态找)两类,先固定后随机 - **双重 AES + 加密字符串(key/iv 也加密存)**:现象——确认 AES 后直接解仍失败,或 key/iv 每次会话都变;原因——实现常见套路:第一层 key/iv 固定(**以加密字符串形式硬编码在 so,运行时经解密函数解出**),第二层 key/iv 每次变化;两次加密间拼接固定前缀(如 `03000001`)再进同一函数;且加解密可能是同一函数(标志位控制方向);对策——hook 解密函数返回点批量导出全部解密字符串(JNI_OnLoad 前 memcpy 的加密串 → 全局变量 → 解密函数),key/iv 直接在其中搜索;找加密点用 findcrypt 扫特征常量(AES S-box 等)→ xref 定位校验 key/iv 长度(16)的函数;动态 hook 打印两次调用前后数据对照验证