简介:冰蝎V4.1 Behinder 是一套面向网络安全测试与渗透测试人员的 Webshell 管理与利用工具,适合已具备一定 Web 安全基础、需要以攻击者视角验证系统防护能力的中高级学习者使用。压缩包共 39 个文件,约 130.19MB,以 jar(主程序本体)、json 与 js(插件配置与前端脚本)、php/jsp/aspx(服务端 shell 样本)、png 与 htm(界面截图与说明页)、db 配置文件及 txt 更新日志等类型为主,兼顾程序、插件与文档,便于按模块检索查阅。目录中 Plugins 下的 JNDInjector、SolrExploit、ServiceScan 以及 HTTP 助手、SSH 助手等扩展,配合 server 端 payload 与 offline 离线资源,可支撑漏洞探测、命令执行、连接管理等常见测试环节;基于 Java 的实现也便于跨平台部署。已有 1592 人学习下载,适合作为搭建授权测试环境、熟悉工具结构与插件机制的参考。
1. 一个两千字节的 POST,为什么所有设备都放它过去
一台只开放 80 和 443 端口的站点,访问日志里清一色 200,POST 请求体全是密文,字节数稳定在两三千,UA 是半年前就不用的旧版本。WAF 没响,流量设备没响,最后是运维发现某个静态目录里多了一个修改时间很新的脚本文件,才回头对上号。事后把抓包翻出来,几乎每次都能落到同一个东西上:冰蝎V4.1(Behinder)。
它属于加密型 webshell 管理工具,V4.1 把指令通道整体换成了 AES,可读的特征串基本消失,签名库自然没法识别。这篇写给三类人:要出检测规则的、要做应急回溯的、要给站点做加固的。重点不在怎么用它,而在协议里哪些东西改不掉、特征怎么从抓包里提取、规则怎么写才不误报、哪几个点最容易出错。
2. 拆开 Behinder V4.1 的加密通道:从密钥协商到 AES 载荷
要把检测做稳,得先把“密文”拆成能判定的结构。这一章只做一件事:把冰蝎V4.1 的一次完整往返,拆成几个可以写进规则的字段,并说明每个字段为什么稳定、为什么可能失真。拆的时候要带着一个问题——哪些东西是协议被迫留下的,哪些只是某一版实现的习惯。前者能进规则,后者只能进临时黑名单。
2.1 首包里的明文密钥:为什么“全加密”是个错觉
一个 webshell 落地之后,服务端往往只有一个脚本文件,没有独立的密钥分发通道,也没有可信第三方。客户端要让服务端解出指令,密钥只有两条路:随请求送过去,或者由双方共享的口令各自派生。冰蝎V4.1 常见实现是两者结合——连接口令先派生出固定长度的密钥材料,首次通信时随请求体前段一起送到服务端,服务端把它记进当前会话,后续指令全部用这把密钥做 AES 加解密。
所以“全流量加密”这个说法只在指令层面成立。握手层面一定是可解析的,因为不解析就没法建立会话。公开分析里比较一致的结构大致如下,具体偏移和长度会随版本、魔改程度变化,动手前以你手上那份样本为准。
| 位置 | 典型长度 | 内容 | 检测价值 |
|---|---|---|---|
| body 前段 | 16 字节或 32 位十六进制字符串 | 会话密钥材料 | 高,跨主机可关联 |
| body 中段 | 16 字节 | IV | 中,可判随机性 |
| body 剩余 | 16 的整数倍 | AES 密文载荷 | 高,长度和对齐可统计 |
| 响应体 | 16 的整数倍 | 同密钥加密的返回 | 中,与请求长度无相关性 |
从检测角度看,这段密钥材料不但是障碍的反面,还是最强的线索。它长度固定、位置靠前、跟连接口令强相关:同一个人用同一个口令打十台机器,这十几字节在十台机器上完全一致。做横向关联时,这个字段比源 IP 好用得多——IP 会换,口令往往懒得换。
提示:不要把偏移量写死在解析代码里。魔改版会把密钥挪到 header、挪到 cookie、或者先做一次 base64 再放。解析器要先判长度再切片,切不出来就整条样本落盘,别让异常样本把整条管道带崩。
还有一个细节值得注意:密钥材料如果是十六进制字符串形式,它就只含 0-9a-f,字符集极小;如果是原始二进制,则高字节比例明显偏高。这两种形态都能用一个很便宜的字符集判断先筛一遍,再进后面的重活。
2.2 载荷结构:16 字节 IV、填充与长度对齐
AES-CBC 的分组长度是 16 字节,PKCS#7 填充会把明文补到整块,密文长度必然是 16 的整数倍;IV 通常 16 字节,多数实现把它放在密文前面一起传。两条叠加的结果很直白:请求体总长是 16 的倍数,减掉固定头部之后仍然是 16 的倍数。
单看“长度是 16 的倍数”毫无意义。multipart 表单、某些 JSON 序列化结果、图片上传都能凑上这个条件。真正有价值的是组合特征:同一个 URI 上,请求长度只有寥寥几种取值,且每种都 16 对齐,同时字节熵接近 8 bit/byte。业务请求不长这样——业务请求的长度是连续分布的,熵通常在 4 到 5 之间,因为里面全是可读的字段名和值。
熵的计算很简单,按字节统计频率再算负熵和即可,一段纯密文的经验值在 7.2 以上,压缩过的数据也能到 7.5 以上,所以熵只能当“与”条件里的一项,不能单独成规则。我一般把它和“长度种类数”“16 对齐率”绑在一起用,三个条件同时满足才往外抛。
注意:站点开了 gzip 或 br 压缩时,响应侧的长度对齐会被彻底打乱,请求侧如果也走压缩,长度特征会一起失效。这种环境要么在解压后的 body 上算特征,要么干脆放弃长度维度,改用熵加 UA 基线。
响应侧还有一个容易被忽略的点:正常网页的响应长度和请求长度基本无关,而加密通道的响应长度是跟着指令内容走的,往往也在几十到几千字节之间、并且同样 16 对齐。请求和响应同时对齐的比例,比只看请求侧要稳。
2.3 与 V3.x 的差异:为什么靠特征串的规则全部失效
V3 时代的请求体是异或之后再 base64,日志里是一眼能认出来的长串字母数字,配上固定的参数名和几个经典函数名,签名规则几行就能写完。到了 V4.1,body 变成二进制密文,参数名可以自定义,UA 能随便填,落地文件还能做各种变形。凡是“匹配内容”的规则,基本全部失效。
| 维度 | V3.x 常见形态 | V4.1 常见形态 | 检测策略变化 |
|---|---|---|---|
| 载荷编码 | 异或 + base64 | AES-CBC 二进制 | 内容匹配失效,转向结构统计 |
| 可读特征串 | 有,且稳定 | 基本没有 | 签名库需要重写 |
| 请求长度 | 随内容浮动 | 16 字节对齐 | 长度对齐成为主特征 |
| 参数名 | 相对固定 | 可自定义 | 不能作为主判据 |
| 会话密钥 | 弱,靠口令 | 首包携带密钥材料 | 新增跨主机关联抓手 |
有一类东西改不掉:协议必须自洽。服务端要能解,就必须拿到密钥;用 AES,长度就必须对齐;指令要往返,同一条链路就会反复出现相似长度的包。这些是数学和工程约束,不是风格问题,所以它们比特征串稳定得多。把规则建在这一层,版本升级对你的影响会小很多;把规则建在 UA 和路径上,改一行配置就全废。
3. 抓包现场:把密文还原成可检字段的分析流程
拿到一份可疑流量,最忌讳上来就写规则。先做聚合,把几万个请求压成几十行会话摘要,特征自己会浮出来。这一章给一套从原始会话表到可进规则的字段的完整流程,命令行和脚本都能直接改改就用。
3.1 采样与预处理:先定会话再谈特征
第一步是缩小范围。可疑 URI 的典型画像是:只接受 POST、状态码常年 200、响应长度在一个很窄的区间里波动、同源 IP 的访问间隔极有规律。先把这些条件套上去,剩下的量通常不到原始的百分之一。
第二步导出会话表,字段至少要有:时间、源 IP、方法、URI、UA、Content-Type、请求体长度、响应体长度、状态码。注意请求体长度必须是 body 的字节数,不是 URL 长度,很多默认日志格式根本不记这个,得从抓包工具或反向代理侧补。
# sess.csv 由抓包工具或代理导出,表头:ts,src_ip,method,uri,ua,ctype,req_len,resp_len,status # 1) 先筛出 POST 且状态 200 的会话 awk -F',' 'NR>1 && $3=="POST" && $9==200 {print}' sess.csv > post200.csv # 2) 按 URI 聚合:请求数、长度种类数、16 对齐率、平均长度 awk -F',' 'NR>1{ uri=$4; n[uri]++; if ($7 % 16 == 0) aligned[uri]++; key=uri"|"$7; len[key]=1; sum[uri]+=$7; }END{ for (u in n) { c=0; for (k in len) { split(k,a,"|"); if (a[1]==u) c++ } printf "%s\t请求数=%d\t长度种类=%d\t对齐率=%.2f\t平均长度=%.0f\n", u, n[u], c, aligned[u]/n[u], sum[u]/n[u]; } }' post200.csv | sort -t= -k4 -nr | head -20这段脚本的逻辑是“用聚合代替逐条判断”。请求数说明这条 URI 被打了多少次;长度种类数是最关键的一列,正常业务接口的长度种类会随着参数变化不断增长,而加密通道因为指令集有限,长度种类往往稳定在个位数;对齐率把 16 字节分组约束量化;平均长度用来区分轻量探测和真正的命令执行。
参数上有几处要按环境调:$7 % 16里的字段序号要对上你自己的导出顺序;head -20只是看头部,真正排查时建议全量导出再按对齐率排序;如果站点走 HTTPS 且你只有日志没有解密能力,长度字段就只能从服务端或代理侧取,别指望交换机镜像能给出明文字节数。
3.2 用 Python 提取结构性字段
命令行聚合只能看趋势,要落到单条样本还得靠脚本。下面这段只做特征提取,不解密、不还原指令,输出的是一行可以直接入库的结构化字段。
import math from collections import Counter HEX_CHARS = set(b"0123456789abcdefABCDEF") def shannon_entropy(data: bytes) -> float: """按字节统计频率后计算负熵和,纯密文经验值 > 7.2""" if not data: return 0.0 n = len(data) return -sum((v / n) * math.log2(v / n) for v in Counter(data).values()) def extract(body: bytes, ua: str, uri: str, ctype: str) -> dict: head, rest = body[:16], body[16:] return { "body_len": len(body), "mod16": len(body) % 16, # 0 表示整体对齐 "rest_aligned": len(rest) % 16 == 0, # 去掉头部后仍对齐 "entropy": round(shannon_entropy(body), 2), "head_hex_ratio": round(sum(c in HEX_CHARS for c in head) / 16, 2), "head_printable": round(sum(32 <= c < 127 for c in head) / 16, 2), "ua_is_empty": ua.strip() == "", "uri_depth": uri.count("/"), "ctype_mismatch": ctype.startswith("application/x-www-form-urlencoded") and len(body) % 16 == 0, }逐项说明。body_len和mod16是最便宜的一层筛,先在数据库里建索引,能把量压掉九成。rest_aligned比整体对齐更严格,因为它假设前 16 字节就是密钥材料——魔改版可能不成立,所以它只作加分项,不作必要条件。
entropy是主力特征,但阈值别拍脑袋,先在你自己站点的正常流量上跑一遍,取 99 分位再上浮一点。head_hex_ratio用来识别十六进制形态的密钥材料,全十六进制就是 1.0,二进制形态会掉到 0.4 以下,两种都正常,中间值反而可疑。
ctype_mismatch抓的是“声明是表单,实际长度却是 16 对齐的二进制”这种自相矛盾,单独看误报不低,跟熵一起用就很干净。ua_is_empty和uri_depth属于弱特征,只在组合里加权,不建议单独触发。
3.3 特征汇总:六个能进规则的字段
把上面两步的产出收敛成一张表,这张表就是后面写规则的全部依据。每个字段都标了误报来源,写规则时按“至少命中三项、其中必须包含一项强特征”的与逻辑组合,比堆一堆弱特征靠谱。
| 字段 | 判据 | 强度 | 主要误报来源 |
|---|---|---|---|
| 请求长度 16 对齐率 | 同 URI 内 ≥ 0.9 | 强 | 文件上传、二进制协议接口 |
| 长度种类数 | 同 URI 内 ≤ 8 | 强 | 参数枚举少的查询接口 |
| body 熵 | ≥ 7.2 | 强 | 图片、压缩包、加密业务载荷 |
| Content-Type 与长度矛盾 | 表单声明 + 二进制长度 | 中 | 序列化框架的非常规用法 |
| UA 偏离站点基线 | 不在近 30 天常见集合内 | 中 | 新版本浏览器、爬虫 |
| 单 URI 的 POST 占比 | ≥ 0.8 且总量 ≥ 5 | 弱 | 提交类接口 |
要强调的是,这张表不含任何“字符串”维度。路径、参数名、UA 具体值都可以被使用方随意更改,把它们写进规则等于给自己埋雷。结构维度改起来成本高得多,因为一改就要动服务端脚本,而服务端脚本往往是最不容易同步更新的那一环。
4. 检测规则落地:网络层、主机层、日志层三层布防
特征有了,接下来是把它变成能上生产的东西。三层各有各的盲区:网络层看不到加密流量内部,主机层看不到横向移动,日志层看不到没进日志的请求。三层叠起来才勉强够用,单靠任何一层都会被绕过。
4.1 网络层:写一条能上生产的规则
网络层的规则不要写成“匹配某个固定值”,要写成“多个结构条件同时成立”。下面这份是自建检测器的规则配置,字段名对应上一章的输出,逻辑是五分钟窗口内同源同 URI 至少五次命中才告警。
- id: enc-post-001 desc: 结构性特征组合命中的加密 POST 通道 match: method: POST status: 200 body_len_mod16: 0 body_entropy_gte: 7.2 ua_not_in_baseline: true uri_post_share_gte: 0.8 window: 300s threshold: 5 severity: high whitelist_uri: - /api/upload - /api/binary/sync tags: [encrypted-channel, structural]逐项解释。body_len_mod16: 0是必要条件但不是充分条件,它只负责把候选集缩小。body_entropy_gte: 7.2是主力,这个值要在自己环境里校准,站点如果本身就有大量加密业务(比如客户端加密的登录包),阈值要往上抬到 7.5 甚至 7.6。
ua_not_in_baseline依赖一张基线表:近 30 天该站点出现过的 UA 集合,出现次数低于某个阈值的算“不在基线”。这条比“UA 等于某个值”稳得多,因为没有攻击者会为了绕规则去用一个从来没出现过的 UA——那本身就更可疑。
uri_post_share_gte: 0.8抓的是“这条路径几乎只被 POST”,正常页面路径一般 GET 占大头。window和threshold是一对,五分钟五次是为了压掉单次误报,窗口太短会被限速打散,太长会把不同事件粘在一起。whitelist_uri必须维护,否则上线上传接口第一天就会产生大量误报。
规则上线前一定要做灰度:先只记录不告警,跑满一周,把命中的 URI 排序看一遍,把业务接口全部加进白名单,再开告警。这一步省不得,跳过它换来的就是第二天全组人一起处理数百条误报。
4.2 主机层:文件落地的时序特征
网络层看不见加密流量里面,主机层却能看见文件本身。冰蝎V4.1 的落地文件有个很稳定的形态:为了把加密载荷塞进去,代码往往被压成一两行超长语句,正常业务代码很少这么写。配合“最近被修改”这个时序条件,抓起来很便宜。
#!/bin/bash # 找出最近 10 分钟内被修改、且存在超长单行的脚本文件 ROOT=/var/www/html find "$ROOT" -type f \( -name '*.php' -o -name '*.jsp' -o -name '*.aspx' \) \ -mmin -10 -print0 2>/dev/null | while IFS= read -r -d '' f; do maxline=$(awk '{ if (length($0) > m) m = length($0) } END { print m+0 }' "$f") if [ "$maxline" -gt 4096 ]; then printf '%s\tmaxline=%s\tmtime=%s\n' "$f" "$maxline" "$(date -r "$f" '+%F %T')" fi done-mmin -10控制时间窗,按你的发布节奏调,有固定发布窗口的站点可以缩到 5 分钟并且只在窗口外触发。-name那几个后缀要按实际技术栈改,漏了就是漏检。
maxline -gt 4096这个阈值来自经验:正常业务代码单行超过 4KB 的极少,而混淆过的载荷、压缩过的加密代码经常压成一行几万字符。误报主要来自前端构建产物,所以后缀限定要写死,别图省事用-type f全量扫。
这段脚本的价值不在它本身,而在它代表的思路:把“文件内容形态”和“写入时间”两个维度乘起来。单看形态,压缩过的第三方库会产生大量误报;单看时间,正常发布会产生大量误报;乘起来之后噪音会小一个数量级。
提示:脚本要放进定时任务,但结果不要只写本地日志。落到集中日志平台并且带上主机名和路径,应急的时候能一条查询拉出全网同一时间段的新增脚本,比一台台登上去快得多。
4.3 日志层:用 SQL 做低频高熵统计
日志层的优势是覆盖面广、留存时间长,劣势是维度少。适合做的是统计型检测:同一个源 IP 在一条 URI 上反复提交长度高度一致的请求,这种行为在访问日志里非常显眼。
-- 找“低频 URI + 请求长度种类少 + 全 POST + 高 16 对齐率”的组合 SELECT client_ip, uri, COUNT(*) AS req_cnt, COUNT(DISTINCT req_len) AS len_kinds, AVG(req_len) AS avg_len, SUM(CASE WHEN req_len % 16 = 0 THEN 1 ELSE 0 END) * 1.0 / COUNT(*) AS align_rate FROM http_access WHERE ts >= NOW() - INTERVAL 1 HOUR AND method = 'POST' AND status = 200 GROUP BY client_ip, uri HAVING COUNT(*) >= 5 AND COUNT(DISTINCT req_len) <= 8 AND SUM(CASE WHEN req_len % 16 = 0 THEN 1 ELSE 0 END) * 1.0 / COUNT(*) >= 0.9 ORDER BY align_rate DESC, req_cnt DESC LIMIT 100;len_kinds <= 8是这条查询的核心。正常业务接口的请求长度会随着参数长度、字段数量、用户输入不断变化,种类数很快就上去了;加密通道的指令集有限,长度种类长期稳定在个位数。
align_rate >= 0.9留了 10% 的余量,因为有些变体会在载荷前后加填充或者分隔符,不是每次都严格对齐。req_cnt >= 5是下限,防止把偶发请求算进来。INTERVAL 1 HOUR按站点流量调,大站缩到 15 分钟,小站放到 6 小时。
这条查询有个前提:req_len必须是请求体字节数。很多默认日志格式只记 URL 长度甚至不记,直接拿 URL 长度当req_len会让整条规则失效——这是我在实际排查中遇到过不止一次的问题,排查半天才发现问题出在日志字段上,不是规则本身。
5. 检测落地时最容易出错的五个点
规则写完不等于能用。下面这五条是我自己遇到过、也在别人环境里见过的问题,每条都按“现象、根因、处置”写,照着对一遍能省不少来回。
5.1 五条记录:现象、根因与处置
第一条:规则一上线,文件上传接口大量告警。现象是告警源 IP 集中,URI 全是上传类接口,人工看包全是正常业务文件。根因很直接:上传的二进制内容熵本来就高,文件大小凑巧 16 对齐的概率也不低,两条强特征同时命中。处置是双管齐下——把上传类 URI 加进白名单,同时给规则补一条约束:Content-Type必须是表单或文本类型。补完之后误报下降了绝大部分,且没有丢掉真实告警。
第二条:站点上线压缩之后,长度对齐率直接掉到接近零。现象是原本每天都有几条的告警,某天开始一条都没有,还以为是攻击停了。根因是反向代理开了响应压缩,同时某些客户端也开始压缩请求体,压缩后的长度不再遵守 16 字节分组。处置是两条路:要么在解压后的 body 上算特征,要么放弃长度维度,改用熵、UA 基线、URI 的 POST 占比三项组合。我一般选后者,改造成本低,代价是灵敏度下降一点。
第三条:某份样本把解析脚本打崩了,整条管道停了两小时。现象是采集端进程退出,日志里是切片越界。根因是把“典型结构”当成了“必然结构”,代码里直接取body[:16]和body[16:],遇到长度不足 16 字节的畸形请求就崩。处置是所有切片操作前先判长度,解析函数整体包一层异常捕获,异常样本单独落盘不参与统计。这个改动十分钟就能做完,但没做之前它就是一颗随时会炸的雷。
第四条:日志层的规则跑了一周,一条结果都没有。现象是查询返回空,但网络层同一时间段是有告警的。根因是访问日志里根本没有请求体长度这个字段,req_len取的是 URL 长度,永远是几十字节,永远不满足 16 对齐。处置是改日志格式或者从反向代理侧单独导一份带 body 长度的日志,两边按请求 ID 关联。这件事提醒我:写规则之前先确认字段是真的存在,不是想当然。
第五条:告警全落在几个固定的 IP 上,定位不到真实来源。现象是同一批 IP 反复告警,查下去全是内容分发节点的回源地址。根因是源站看到的对端地址是回源地址,不是终端用户地址。处置是取转发链里的第一个非内网地址,或者直接在边缘节点侧做检测。同时要注意把节点的健康检查流量加进白名单,否则它们会稳定产生误报。
5.2 先量基线再定阈值:把误报压到可接受
阈值不要凭直觉定。正确顺序是:先取 7 到 30 天的正常流量,按 URI 分组算每个特征的分布,取 99 分位往上一点作为初始阈值,再灰度一周看实际命中。
按 URI 分组这一步经常被省掉,但它很关键。全站混在一起算,上传接口会把熵的分布整个拉高,导致阈值定得过高,真正的问题反而漏掉。分组之后每个接口有自己的基线,判断精度会明显提升。
| 特征 | 正常基线参考 | 建议初始阈值 | 备注 |
|---|---|---|---|
| body 熵 | 4.0 到 5.2 | 7.2,加密业务站抬到 7.6 | 必须按 URI 分组 |
| 长度种类数 | 20 以上 | 8 以下 | 参数少的接口单独处理 |
| 16 对齐率 | 0.1 以下 | 0.9 以上 | 压缩开启时失效 |
| 单 URI POST 占比 | 0.3 左右 | 0.8 以上 | 提交类接口除外 |
误报预算也要提前定:按站点规模,每天每条规则不超过 5 条告警是能长期维护的上限,超了就要么加白名单要么调阈值,不能放着不管。告警长期没人看,等于没有这条规则。
6. 把规则跑成回归:样本回放与一个最小验证脚本
规则是会退化的。站点改版、加了压缩、换了反向代理,任何一个变化都能让一条昨天还灵的规则今天彻底失效,而你不会收到任何通知——它只是不再告警了。我现在的习惯是给每条规则配一个小样本集:三十条左右的历史可疑流量,加上三百条左右的正常业务流量,每次改规则先跑一遍回放,看召回和误报有没有变差。
样本集不用大,但要覆盖变化:不同版本的载荷、开了压缩的、走转发链的、上传接口的正常流量,各留几条。样本按 JSON 行存,每行带label字段,1 表示可疑、0 表示正常,其余字段就是前面提取出来的结构特征。回放脚本二十行就够。
import json def replay(rules, samples): """rules 是判定函数列表,任一命中即视为告警""" tp = fp = fn = tn = 0 for s in samples: hit = any(r(s) for r in rules) if s["label"] == 1: tp, fn = (tp + 1, fn) if hit else (tp, fn + 1) else: fp, tn = (fp + 1, tn) if hit else (fp, tn + 1) return { "tp": tp, "fp": fp, "fn": fn, "tn": tn, "precision": round(tp / (tp + fp), 3) if tp + fp else 0.0, "recall": round(tp / (tp + fn), 3) if tp + fn else 0.0, } if __name__ == "__main__": samples = [json.loads(l) for l in open("samples.jsonl", encoding="utf-8")] rules = [lambda s: s["mod16"] == 0 and s["entropy"] >= 7.2 and s["ua_not_in_baseline"]] print(replay(rules, samples))precision掉到 0.9 以下说明误报开始难维护,recall掉到 0.8 以下说明有变体漏了。两个数一起看,只盯一个容易自我安慰:把阈值拉满,召回自然崩;把阈值放开,精确率就没了。我一般让精确率优先,因为误报消耗的是人的耐心,而漏报至少还有别的层兜着。
还有个小技巧:把每次线上真实告警里人工确认过的样本,定期回灌进样本集。这样样本集会跟着你的环境一起演化,半年之后它比任何公开的特征库都好用。
说个自己的教训。早些年我特别迷信“找到那个唯一特征”,总想用一条规则解决问题,结果每次版本一更新就得从头再来。后来改成攒结构特征、按组合判定、配样本回放,工作量反而小了,因为协议里那些被迫留下的约束——密钥必须传、分组必须对齐、指令必须往返——比任何具体实现都活得久。检测这件事,赌的是约束,不是细节。希望帮到你。
本文还有配套的精品资源,点击获取