news 2026/10/20 20:44:28

键盘相邻性:从asdxzxz看人肉随机数的安全陷阱与检测方法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
键盘相邻性:从asdxzxz看人肉随机数的安全陷阱与检测方法

我最近调一个内部联调环境,需要给本地网关配一个临时访问密钥。不想翻密钥管理系统,也不想用一个太正经的值——毕竟只是沙箱联调,丢了就丢了对吧。于是我随手在键盘上敲了一串字符当占位符:

asdxzxz

敲完自己还觉得挺满意:八个纯字母,没有单词形状,没有数字,看上去比test123之类的默认值“随机”多了。结果过了两天,我在写一个键盘路径分析脚本时,顺手把这串字符丢了进去,画出来的轨迹让我愣了一下。

A→S→D→X→Z→X→Z。这根本不是一串“随机”的字符,而是一段有肌肉记忆参与的手部运动。

这篇文章就从这串字符展开。我会把它当成一个引子,聊清楚三件事:怎么量化键盘输入的“相邻性”,人肉随机和普通随机函数在生成安全值的时候有哪些坑,以及我在做代码审查时是怎样过滤掉这类“看似随机、实则手癖”的字符串的。

1. 先别急着聊安全:asdxzxz 是怎么被“人肉生成”出来的

1.1 从临时密钥到顺手占位符

这件事的背景很简单。某次联调我需要一个临时 token,用途是让两个本地服务在调试时互认身份。这个 token 的生命周期很短,按计划当天结束就该删掉,所以我完全没走正规签发流程,直接在键盘上“随便敲了一段”。

问题是,人脑没有办法在完全没有规律的约束下输出随机字符。你以为自己是在乱打,实际上手指会优先落在最舒服的位置上。asdxzxz就是很典型的一个结果:一只手放在键盘左半区,手指从 A 开始往右推,到 D 之后顺着往下到 X,再从 X 往左到 Z,然后又弹回 X,又蹭回 Z。

这个动作用文字描述就是“画了个折线”。如果记录手指在键盘上的坐标,会发现它根本不是随机跳跃,而是沿着物理键位做了一次局部移动。

1.2 轨迹还原

我把 QWERTY 键盘左半部分的键位抽象成一个简单的坐标矩阵,大概长这样:

行位置键位
上排Q W E R T
中排A S D F G
下排Z X C V B

如果只拉出asdxzxz涉及的键:

  • A 在左中排的起点
  • S 在 A 右边一格
  • D 在 S 右边一格
  • X 在 D 的斜下方,差不多是下排第二个位置
  • Z 在 X 左边一格
  • X 又回到原位
  • Z 又跑到左边

也就是说,绝大多数移动的距离只有 1 格,最多的一步也就是 D→X 的斜向移动。整条路径完全落在左手食指和中指的活动半径里。

这种现象在手机端其实更常见——大家应该见过很多人划屏解锁的九宫格密码,走的路线永远是 L 形、Z 形或者来回折返,因为人的手指天然倾向于在同一个小区域内来回运动。桌面键盘虽然键位多,但人类的运动习惯不会变,只是把触摸屏上的折线搬到了物理键位上而已。

所以从这个角度看,asdxzxz既是一个随机的占位符,也是一个非常典型的“键盘肌肉记忆指纹”。它记录了当时输入者的手部活动习惯,这个指纹在安全分析里是有意义的。

2. 量化“键盘相邻性”:看起来随机,不代表熵够高

2.1 用坐标和距离说话

我上面说的“键盘轨迹”不能只停留在感觉层面。既然要把它当审计点,就得有个可量化的指标。我在脚本里做了三件事:

先定义 QWERTY 键位坐标,然后计算相邻字符之间的曼哈顿距离,最后统计整段字符串的平均步长。

import string qwerty_rows = [ "qwertyuiop", "asdfghjkl", "zxcvbnm" ] key_pos = {} for row_idx, row in enumerate(qwerty_rows): for col_idx, ch in enumerate(row): key_pos[ch] = (row_idx, col_idx) def manhattan(a: str, b: str) -> int: r1, c1 = key_pos[a] r2, c2 = key_pos[b] return abs(r1 - r2) + abs(c1 - c2) def analyze_pattern(s: str): s = s.lower() distances = [] for i in range(len(s) - 1): d = manhattan(s[i], s[i + 1]) distances.append(d) avg = sum(distances) / len(distances) if distances else 0 print(f"字符串: {s}") print(f"相邻距离列表: {distances}") print(f"平均步长: {avg:.2f}") print(f"步长<=1的占比: {sum(1 for d in distances if d <= 1) / len(distances):.0%}") analyze_pattern("asdxzxz")

跑出来的结果很有意思:

  • 每一步的曼哈顿距离依次是 1、1、2、1、1、1
  • 平均步长约 1.17
  • 步长小于等于 1 的占比接近 85%

2.2 字符串熵的再计算

如果按均匀分布来算,一个 8 位小写字母字符串的理论熵大约是 38 bit,对于简单的临时值来说不算太差。但这个计算成立的前提是:每一位字符都是独立、等概率地从 26 个字母里选出来的。

asdxzxz显然不满足这个前提。它根本不是 26 个字母里的独立抽样,而是“按键位置”上的连续移动。真正起作用的约束不是字符种类,而是方向和距离。连续输入的字符之间高度相关,每一步只允许从当前键位的物理邻域里做选择。顺着这个思路继续推导,会发现这个看起来像是 26^8 量级的字符串,实际的有效组合会被压缩到很小的范围。手部运动路径的规律性越强,可用的随机性就越弱。

这也是为什么很多规则引擎在计算口令强度时,会把“顺序键盘序列”直接判为弱模式。因为攻击者的字典里早就收录了qwerty、asdf、zxcv这类片段。我这里的asdxzxz只是换了个组合方式,本质和“键盘序列”是同一类模式。

2.3 为什么步长短是弱熵信号

步长越短,说明输入时的物理位移越小。如果一个随机字符串由独立字符构成,字符之间应该均匀分布在整个键盘区域,那么相邻距离的平均值不会这么低,也不会出现“一步之内必然落在旁边键”的情况。

反过来,当字符串里每一步都落在相邻键上,它对应的就是一个物理上容易执行、视觉上容易被记忆、行为上容易被预测的模式。任何人只要看到你手指落点的大致区域,就很容易通过肩膀视角重建整个序列。即便不是你亲自敲,别人用录像设备记录键盘热区,也能还原出一个概率很高的候选集合。

所以在审计一个字符串是否适合做密钥、token 或口令时,我习惯把“键盘相邻性”作为第一道前置检查,而不是等到暴力破解环节再去操心。

3. 人肉随机之外,普通随机函数的坑同样不小

3.1 从 asdxzxz 引出的“随机性来源”问题

asdxzxz这个问题让我重新审视了一遍项目中生成随机值的代码。结果发现,人肉随机只是问题之一,代码里的随机性来源如果不注意,问题更隐蔽。

很多刚起步的项目会用random模块生成临时密钥:

import random import string def make_token(length=16): chars = string.ascii_lowercase + string.digits return ''.join(random.choice(chars) for _ in range(length))

这在功能上能跑,但random模块生成的是伪随机数,随机性来自一个确定性的算法。如果攻击者能拿到一定数量的连续输出,或者能推测出种子状态,后续输出是可以被算出来的。尤其当应用启动后没有主动加入足够的系统熵,某些环境里生成的序列会表现出明显的周期性和可预测性。

后来我在代码审查里给团队定的要求很简单:凡是和密钥、令牌、口令沾边的值,一律不要用通用随机函数,改用操作系统提供的随机源。在 Python 里就是secrets模块:

import secrets # 生成一个二进制安全的临时令牌 token = secrets.token_hex(16) # 如果想直接得到一个 URL 安全的字符串 url_token = secrets.token_urlsafe(32)

3.2 固定占位符的“惯性传染”

再说回asdxzxz。虽然它最初只是一个本地沙箱的临时值,但坏习惯是会传染的。

开发过程中,很多人会在代码里先写一个固定的字符串占位。比如:

  • 本机联调用的SECRET_KEY = "asdxzxz"
  • 请求头的 mock token 固定为asdxzxz
  • 数据库连接串里先用一个固定值占位

如果这个值只是写在某个人的本地配置里,问题不大。但只要它被提交到代码仓库,或者被复制到测试环境的配置文件里,就会跟着代码一起流转。最典型的情况是:

  1. 配置里写的是占位符,但因为能跑通,大家就默认它是真实密钥
  2. 测试环境沿用后,日志、监控、数据库记录里到处都会出现这个字符串
  3. 等到想统一替换成正式密钥时,根本搜不全所有出现过的位置

我用asdxzxz就吃过这个亏。它后来出现在某内部工具的三个模块里,其中一处还是明文写死在连接逻辑里的。真正解决时花的时间比一开始直接走正规签发流程要长得多。

3.3 占位符和真实密钥的边界模糊

另一种风险是把占位符做得“过于像真的”。asdxzxz这类字符串没有语义,混在真实的 token 里很难一眼分辨。日志告警时,如果看到一个类似asdxzxz的值,你不会立刻意识到它只是个占位符,于是排查方向会跑偏。

后来我在项目规范里加了一条:占位符必须带有明确的标识前缀,比如PLACEHOLDER_、MOCKTOKEN_,并且在静态检查流程里禁止让这些前缀出现在除测试目录以外的代码路径里。

这一条在后续几次安全审查看起来极其有效,至少我再也没有在正式环境的代码里看到裸奔的临时字符串了。

4. 滤除“键盘手癖”:让生成和检查都变得可执行

4.1 给生成器装上模式检测器

既然找到了问题,就得有对应的检测手段。我在一个内部小工具里加了这样一段逻辑:每次生成随机字符串以后,先做一次键盘相邻性检测,如果检测通过就保留,检测失败就丢弃重来。

import secrets import string keyboard_adjacent_limit = 1 # 相邻距离小于等于这个值就判定为可疑 def keyboard_distance(s: str) -> list[int]: s = s.lower() return [manhattan(s[i], s[i + 1]) for i in range(len(s) - 1)] def is_keyboard_pattern(s: str) -> bool: distances = keyboard_distance(s) if not distances: return False short_steps = sum(1 for d in distances if d <= keyboard_adjacent_limit) return short_steps / len(distances) > 0.6 def safe_random_token(length: int = 24) -> str: alphabet = string.ascii_letters + string.digits for _ in range(20): # 最多重试20次 candidate = ''.join(secrets.choice(alphabet) for _ in range(length)) if not is_keyboard_pattern(candidate): return candidate raise RuntimeError("连续生成的候选值都命中键盘相邻模式,请检查随机源")

注意这里不能只检查一次就完事,因为在极短时间内暴力重取,secrets每次返回的概率分布虽然独立,但连续多次命中相邻模式的概率也不是零。给一个重试上限会让工具更健壮。

4.2 代码审查里该查什么

我后来把这类检查写进了自己的开发检查单。每次涉及密钥、token、IV、salt 这类变量,我会多看一眼三件事:

第一,来源。有没有用不安全的随机函数?如果是,直接替换成secrets或操作系统提供的等效接口。

第二,形式。字符串是否存在连续的键盘相邻模式?是否存在明显的递进序列(abc、qwe、asd)?是否存在重复的“往返”运动,比如这里的xzxz?

第三,生命周期。是否真的需要写死?如果不需要写死,就该走配置下发;如果一定阶段内需要占位,那占位符必须满足前面说的显式前缀规则,并登记在一个过期的 TODO 清单上。

4.3 项目命名和其他场景的启示

asdxzxz给我的启发并不只停在密钥生成上。项目名、仓库名、模块名如果也采用“随手乱敲”的方式,同样会埋下隐患。

有的团队新建 Git 仓库时随手叫test123、asdf,结果半年后仓库里全是同名项目。有的团队给内部服务起名字时靠手癖敲出难以拼读的组合,导致后续在监控面板、日志追踪、命令行工具里频繁拼错。相比之下,我现在的做法是:

  • 项目内部代号用有语义但不会泄露业务信息的名字
  • 临时分支用自动生成的编号而不是手敲字符
  • 所有临时密钥统一走签发接口,不留手工敲键盘的空间

这个转变很小,但效果立竿见影。至少最近半年我再也没遇到过“搜一个字符串搜出十几个不同的使用点”的情况。

5. 我后来是怎么改进这套流程的

5.1 给占位符建立生命周期

写代码这些年,我没有少踩“临时值后期转正”的坑。asdxzxz让我意识到,问题不出在临时值本身,而出在临时值没有明确的过期机制。

我现在的做法是给所有占位符加一个遗忘检测点。比如:

  • 本地配置里的 mock token 在每次启动时打印一个 WARNING,提醒这是临时值
  • CI 流程里扫描代码中出现的明显占位模式,命中后直接失败
  • 隔一段时间做一次密钥轮换,确认所有占位符都被清理干净

这套机制跑起来不需要特别复杂,静态扫描的规则也不过几十行,但每次都能在问题发酵前拦住一两个漏网之鱼。

5.2 把键盘路径检测推广到权限口令

我还把前面那段曼哈顿距离代码整合到了内部口令强度快检工具里。业务方提交一个新口令时,工具会同时做三份检查:

  • 长度和字符多样性
  • 是否命中常见弱口令库
  • 是否命中键盘相邻模式

第三项是后来加的。因为很多团队提出来的口令虽然不常见,但构造逻辑还是“从键盘某个位置开始连续滑行”,这类口令在暴力字典里看不出问题,却依然经不起实际攻击者的推演。

5.3 想清楚“谁在用自己的手生成安全值”

最后分享一个我总结出的判断标准:只要某个安全值最终会经过人手,无论是生成、复制还是输入,就必须假定它带有物理规律。手心手背都是肉,手指永远会优先跑到最省力的位置。

所以,我在真正需要高随机性的场景里,会尽量减少人为干预。不手敲、不复制粘贴、不凭记忆修改,全部走自动生成和自动下发。只有当工具不可及时才手工处理,而且手工生成的值一定不会直接投入使用,而是再挂上一层哈希或编码转换,把键盘手癖的痕迹尽量抹掉。

用asdxzxz当占位符这件事本身不是什么大事故,但它让我把一个很朴素的道理重新想明白了:你以为你在随机,其实你的手指一直在画同一幅画。与其事后排查,不如从一开始就别让手参与生成安全值。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 20:44:03

PJSIP跨平台编译实战:Linux与Windows双平台避坑指南

1. 为什么值得折腾 pjsip 的跨平台编译如果你在做VoIP软电话、呼叫中心中间件、对讲系统或者任何需要处理SIP信令的终端产品&#xff0c;pjsip这个库大概率绕不开。它把SIP、SDP、RTP、媒体编解码、NAT穿透这些脏活累活全打包好了&#xff0c;还附带了完整的呼叫控制API。但问题…

作者头像 李华
网站建设 2026/10/20 20:43:00

YOLO26目标检测训练到RK3588部署:ONNX与RKNN适配实践

YOLO26 目标检测的训练到瑞芯微平台适配&#xff0c;这条链路我最近刚完整跑通一遍。先说结论&#xff1a;真正卡人的地方不在训练本身&#xff0c;而在从 PyTorch 模型走向 RKNN 运行时的那几步——ONNX 导出、算子兼容、INT8 量化&#xff0c;每一个都埋着不少雷。我是在给一…

作者头像 李华
网站建设 2026/10/20 20:42:55

钢铁表面缺陷检测数据集实战:1800张带标签图像从结构解析到YOLO训练

简介&#xff1a;这份钢铁表面缺陷检测图像数据集面向从事机器视觉、工业质检与深度学习缺陷识别的研究人员和工程师&#xff0c;尤其适合需要监督学习样本训练分类或检测模型的场景。数据集共1800张钢铁表面图像&#xff0c;每张均配有对应标签&#xff0c;覆盖裂纹、锈蚀、凹…

作者头像 李华
网站建设 2026/10/20 20:42:09

Win11访问Win7共享报错0x80070035?SMB协议与网络配置修复指南

1. 0x80070035 到底是什么&#xff1a;先搞清楚报错在哪里先把结论说在前面&#xff1a;这个错误码的意思是“找不到网络路径”。很多朋友升到 Win11 之后&#xff0c;双击网络里那台老 Win7 电脑的共享文件夹&#xff0c;直接弹一个红叉&#xff0c;错误码 0x80070035&#xf…

作者头像 李华
网站建设 2026/10/20 20:40:58

Linux 下 SSH 连接 GitHub 全流程配置与常见坑排查

聊到 Linux 上连 GitHub&#xff0c;很多人第一反应是 HTTPS 地址凑合着用。但如果你有往远程仓库推代码的习惯&#xff0c;早晚会碰到密码输入得想骂人的时候。HTTPS 一次两次还能忍&#xff0c;高频推送还要配合个人访问令牌&#xff0c;真不如直接把 SSH 链路一次搞定。这篇…

作者头像 李华
网站建设 2026/10/20 20:39:22

把AI当牛马使唤:MiniMax职场效率工具实测与调教指南

最近常听到身边同事开玩笑&#xff0c;说"MiniMax 已经彻底沦为了打工人的牛马"。一开始我还以为牛马是在骂人&#xff0c;结果看大家聊下去才发现&#xff0c;这已经是当下打工人对 AI 工具的最高评价了——意思是这玩意儿太好使唤了&#xff0c;写周报、理会议纪要…

作者头像 李华