简介:Python实现的Android锁屏手势密码破解工具包,面向移动安全爱好者、Android逆向初学者以及系统取证人员,旨在帮助理解Android手势密码的存储与校验机制,并演示从加密文件还原出手势路径的完整过程。在Android系统中,九宫格手势轨迹会被按顺序编号,并以十六进制字符串形式进行SHA1哈希,最终保存于/data/system/gesture.key中;设备获取root权限后,将这一密钥文件取出,运行配套的Python 2.x脚本即可自动遍历九宫格组合,还原出原始解锁序列。压缩包内共12个文件,核心为main.py破解脚本,同时提供示例gesture.key、手势截图gesture_jpg.png、README.md使用说明,以及6个XML工程配置、1个iml和1个name等辅助文件,整体仅61KB,结构清晰。描述还给出了实际解密案例:从哈希63f6171ce48733d6fca74f0c76c78a9ef2e3c430还原出00-01-02-04-06-03-05-08-07,耗时约4.78秒,便于读者对照验证。目前已有1223人学习下载,适合希望快速上手Android安全测试、移动取证的开发者进行本地实验与二次开发。
1. 从 gesture.key 到明文手势:一场百万次哈希的逆向推演
Android 7.0 之前,锁屏手势密码的安全性约等于一串点序列的 SHA1 哈希:无盐、无慢哈希、文件固定 20 字节。在一次旧测试机的取证验证中,我在 /data/system/gesture.key 里直接看到了这串摘要。只要把 9 个点的所有合法画法枚举出来,逐个算哈希去比对,普通笔记本花几秒就能还原出手势,完全不需要特殊硬件。
这篇文章要解决三件事:gesture.key 的存储格式与点编码规则、9 点手势的全部可能性与剪枝逻辑、一份可直接运行的 Python 爆破脚本,并附上多进程加速、常见问题和新版系统的差异。适合做授权取证实验、排查锁屏逻辑漏洞,或者给忘了锁屏密码的老设备留一条出路。下面从哈希来源开始。
2. 存储机制摸底:gesture.key 怎么把 9 个点变成 20 字节
2.1 哈希从哪来:三种合法提取途径
破解的第一步是拿到 gesture.key 这个文件本身。它位于 /data/system/gesture.key,属于系统目录,普通应用权限读不到。实际操作中有三条路径:设备已 root 或者刷过工程固件,直接 adb root 后 pull;设备刷了第三方 Recovery,在恢复模式下挂载 /data 分区再拷贝;或者用商业取证工具做物理镜像,从镜像里解析文件。测试机我一般用第一条,一条命令搞定。
adb root adb pull /data/system/gesture.key ./gesture.keypull 下来后的第一件事永远是检查文件长度。标准 gesture.key 就是 20 字节,多一个字节都不对。我遇到过 pull 下来 36 字节的情况,差点误判成加密格式,最后发现是某个 ROM 在文件头拼了厂商标识。先用ls -l看长度,再xxd gesture.key | head观察十六进制结构,能提前排除这类干扰。
2.2 九宫格点编码:从 0x00 到 0x08
九宫格在系统内部被编码成字节值,顺序是从左到右、从上到下:左上角是 0x00,往右是 0x01、0x02;第二行是 0x03、0x04、0x05;第三行是 0x06、0x07、0x08。手势序列保存时就是这串字节依次排开,中间没有分隔符,也没有拼接盐值。
举个例子,手势 "左上 → 中上 → 右上 → 正中" 对应序列 (0, 1, 2, 4)。系统保存时把它转成字节数组 [0x00, 0x01, 0x02, 0x04],然后计算 SHA1,最后写入 gesture.key。这里要特别留意:点编号是从 0 开始,不是从 1 开始。如果这个基线搞错了,后面比哈希怎么都比不上。
2.3 无盐 SHA1 的致命点
哈希破解能不能跑起来,核心看两件事:算法计算成本和输入空间大小。SHA1 在 Python 里每秒能算几百万次,手势密码的合法输入空间又只有几十万条。两者一叠加,离线爆破就是几秒钟的事。
真正让破解变难的,不是把 SHA1 换成 SHA256,而是在哈希前加一个随机盐。加了盐之后,候选序列的哈希都依赖盐值,没法提前做统一的预计算,攻击者只能针对目标哈希逐个试,配合锁定策略能彻底卡死离线爆破。老版本 Android 的问题恰恰是把原始哈希裸露在文件里,等于把所有材料摆上了桌。后来 gatekeeper 的引入,核心改动不是换算法,而是在存储格式上加了盐和验证次数限制。
拿到文件后我一般会做一个 30 秒的自检:挑一个自己知道的手势序列,按上面的编码算出 SHA1,和文件内容对比。一致就说明设备走的是标准无盐逻辑;不一致,再判断是厂商魔改还是提取路径有问题。这一步能省下后面跑完整个枚举才发现方向不对的时间。
3. 枚举思路:9 点手势的全部可能性和剪枝规则
3.1 理论总量:从 4 个点到 9 个点的全排列
手势密码的基本约定是:最短连接 4 个点,最长连接 9 个点,同一个点不能重复经过。如果先不管画线时的几何规则,只把 "从 9 个点里有序取出 4 到 9 个不同点" 当成候选,总候选数就是各级排列数之和:
| 长度 | 候选数量 | 累计 |
|---|---|---|
| 4 | P(9,4)=3024 | 3024 |
| 5 | P(9,5)=15120 | 18144 |
| 6 | P(9,6)=60480 | 78624 |
| 7 | P(9,7)=181440 | 260064 |
| 8 | P(9,8)=362880 | 622944 |
| 9 | P(9,9)=362880 | 985824 |
总共有 985,824 条候选,接近一百万。这个量级对 SHA1 计算压力不大,但直接当爆破字典会浪费大量算力,因为相当一部分序列在真实设备上根本画不出来。问题出在九宫格的连线规则上。
3.2 跳跃规则剪枝:系统画线时发生了什么
九宫格里存在八组 "跨越" 关系:0 连到 2 中间隔着 1,0 连到 6 中间隔着 3,0 连到 8 中间隔着 4;完整列表是 0-2 隔 1、0-6 隔 3、0-8 隔 4、1-7 隔 4、2-6 隔 4、2-8 隔 5、3-5 隔 4、6-8 隔 7,反之亦然。
系统处理画线时有个很关键的行为:如果一条新线段中间夹着还没被选中的点,系统不会允许跳过它,而是强制把中间点补进序列。反过来,如果中间点已经出现在之前的路径里,再画这条线就合法。比如从 0 往 2 画,而 1 还没被选过,系统实际记录的是 0-1-2,不是 0-2;如果 1 已经在路径里,系统才允许 0-2 直接连线。
这个规则直接影响爆破准确性。判断一条候选序列是否可能被系统接受,就要检查每一对相邻点之间有没有夹着的中间点,有且没被用过,就剪掉。用代码表达:
CROSS = {} for a, b, mid in [ (0, 2, 1), (0, 6, 3), (0, 8, 4), (1, 7, 4), (2, 6, 4), (2, 8, 5), (3, 5, 4), (6, 8, 7), ]: CROSS[(a, b)] = CROSS[(b, a)] = mid def is_valid(seq): used = {seq[0]} for a, b in zip(seq, seq[1:]): mid = CROSS.get((a, b)) if mid is not None and mid not in used: return False used.add(b) return True这里的CROSS表是剪枝依据,is_valid从序列第二个点开始逐个判断。used.add(b)放在判断之后很重要,因为下一轮判断要用的是 "到达 b 之后" 的集合状态。如果提前添加,上一对相邻点还没检查完,中间点是否在路径里的判断就不准了。
3.3 剪枝之后:真实的爆破规模
用严格规则跑一遍,合法序列数量大概在 38 万到 39 万之间,不是一个固定的整数。原因在于不同 Android 版本对共线跳点的处理有细微差别,个别厂商 ROM 甚至允许跳过未选点,我在不同模拟器镜像上跑出来的数量都会差一点。
实际爆破时不需要纠结精确数量,原则是候选集合必须覆盖系统所有可能接受的序列。我的做法是分两轮:第一轮用严格规则生成约 39 万条;如果目标设备解不出,第二轮放开 "允许跨过未选点" 的限制,把候选集合补充到 60 万附近再跑。两轮加起来也就一百万次 SHA1,Python 扛得住,不用上 GPU。
4. Python 爆破脚本:从序列生成到 SHA1 哈希比对
4.1 生成器实现:不要一次性装下所有序列
候选序列接近一百万,如果全部落成列表再遍历,内存会被白白吃掉。虽然不到撑爆的程度,但没有必要。生成器逐条产出,边算哈希边释放,内存只有几 KB。把第 3 章的剪枝规则和生成器组合起来:
import itertools def all_valid_patterns(): for length in range(4, 10): for seq in itertools.permutations(range(9), length): if is_valid(seq): yield seqitertools.permutations(range(9), length)天然保证序列内部不重复使用点,因为排列不会取同一个元素两次。range(9)生成 0-8 的整数,正好对应九宫格点编码。length 从 4 开始,和系统要求的最短 4 个点对齐。
这个函数是整套脚本的骨架。输出是一个元组,比如 (0, 1, 4, 7),后面直接转 bytes 算哈希。注意不要用列表推导式把生成器包起来,否则又回到一次性物化的老路。
4.2 哈希比对主逻辑:长度校验别省
import hashlib def crack_gesture(gesture_key_path): with open(gesture_key_path, "rb") as f: target = f.read() if len(target) != 20: raise ValueError( "目标文件长度不是 20 字节,收到 %d 字节,先检查提取方式" % len(target) ) for seq in all_valid_patterns(): if hashlib.sha1(bytes(seq)).digest() == target: return seq return None参数说明:gesture_key_path指向从设备 pull 下来的文件;返回值是元组,可以直接映射回九宫格画图。bytes(seq)把 (0, 1, 2, 4) 这样的元组转成字节序列,正是系统保存手势时用来做摘要的原始数据,不需要额外调整。
长度校验不是可有可无。遇到过 36 字节的文件,如果跳过校验,脚本会把厂商附加的字节一起当成哈希比对,怎么跑都没结果。校验放在循环前面,可以在无效输入上秒退,避免白算几十秒。
这段代码单核跑完整枚举,一台普通笔记本耗时 2 到 4 秒,取决于 Python 版本和 SHA1 是否走硬件指令。这个速度做验证足够,优化的真正需求来自批量场景。
4.3 多进程与批量:真正的提速点
多进程的正确打开方式是分块,不是每条序列发一个任务。每条都 submit 一个 future 会产生大量任务对象,调度开销比计算还大。我的做法是把候选序列按 8 到 16 份切块,每块一个 worker 内部循环比对,最后汇总命中结果:
from concurrent.futures import ProcessPoolExecutor import itertools def worker_chunk(chunk): seqs, target = chunk for seq in seqs: if hashlib.sha1(bytes(seq)).digest() == target: return seq return None def crack_chunked(path, n_chunks=8): target = open(path, "rb").read() all_patterns = list(all_valid_patterns()) chunk_size = (len(all_patterns) + n_chunks - 1) // n_chunks chunks = [] for i in range(n_chunks): seg = all_patterns[i * chunk_size:(i + 1) * chunk_size] chunks.append((seg, target)) with ProcessPoolExecutor(max_workers=n_chunks) as pool: for result in pool.map(worker_chunk, chunks): if result is not None: return result return None参数说明:n_chunks一般取 CPU 物理核心数,8 是常见选择;pool.map按 chunk 顺序返回结果,某个 chunk 命中就立即返回,不等待剩余部分跑完。这里我把all_valid_patterns()物化成列表,因为切块需要随机访问,39 万条内存可控;不想占这份内存,可以按长度分段,每个长度单独一批,同样能实现分块。
实测在 8 核机器上,单机并行可以把整轮枚举压到 0.5 秒左右。再加核数收益递减,瓶颈已经从 SHA1 计算变成了对象创建和 IPC 通信。批量跑几百个文件时,我还会把跑过的哈希做成缓存,同型号设备哈希相同直接秒出,这一招在大批量时比多进程更省时间。
5. 实战复现与避坑指南:提取、还原与新版系统差异
5.1 一条龙流程:从 pull 到解锁
复现流程并不长,四个步骤走完:
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 提取 | adb pull /data/system/gesture.key | 本地得到 20 字节文件 |
| 2 自检 | 用自己设置的手势算哈希对比 | 确认无盐 SHA1 逻辑 |
| 3 爆破 | python crack_gesture.py gesture.key | 输出类似 (0,1,2,5,8) 的元组 |
| 4 还原 | 按编号映射回九宫格重新画线 | 解锁成功 |
第 2 步自检最值得保留。在模拟器上设置一个印象深刻的手势,比如 0-1-2-3,算一遍哈希和 gesture.key 比对。一旦一致,就同时验证了文件提取、点编码、SHA1 算法三条链路,后续爆破出结果就非常可信。第 4 步画线时要留意编号从左上角 0 开始,不是 1,很多人习惯按 1-9 换算,画出来错一半。
5.2 常见问题排查:四条高频翻车记录
问题一:跑了几分钟都出不来结果
现象:脚本既不报错也不返回,CPU 占满但一直空转。
原因:最常见是厂商在哈希前多拼了一段固定字符串,或者文件长度不对导致比对基准错误。另一种是候选集合没覆盖目标设备接受的画法。
解决:先用 5.1 的自检确认脚本本身没问题,再用openssl dgst -sha1验证文件确实是无盐 SHA1。验证对不上就是厂商魔改,离线枚举这条路要换思路。
问题二:gesture.key 文件长度不是 20 字节
现象:pull 下来是 36、40 甚至更大。
原因:部分 ROM 在标准哈希前后附加了厂商标识段或版本号;也有厂商把自定义格式直接存成同名文件。
解决:先xxd gesture.key | head看十六进制结构。如果前 20 字节能独立匹配已知手势的 SHA1,截断前 20 字节处理;完全看不出结构,就确认是自定义格式。
问题三:Android 7.0 之后的设备找不到 gesture.key
现象:/data/system 下只有 gatekeeper.pattern.key,没有 gesture.key。
原因:新版系统换用了 gatekeeper 组件,验证带盐、带次数限制,攻击面完全不同。
解决:这类设备离线枚举基本失效,不要在旧脚本上死磕。老版本模拟器仍然能完整走通这套流程,适合做实验验证。
问题四:脚本输出了序列,但设备解锁不通过
现象:返回的序列看着是对的,照着画就轻微振动被拒绝。
原因:大多是编号映射理解错了,把 0-8 当成 1-9 去画;或者是序列里包含中间点,为了省事画对角线,实际路径产生了新的中间点。
解决:严格按脚本输出的序列逐点画,不要跳线。编号基线的确认回到 2.2 的方法,先拿自己知道的手势做回归,确认 0 和 8 的位置。
5.3 新版系统与厂商差异:先看文件再动手
拿到设备别急着跑脚本。先看 /data/system 下有什么:有 gesture.key 就按标准流程走;只有 gatekeeper.* 就换方向。不过很多中低端设备在 Android 8/9 上仍保留 gesture.key 兼容逻辑,或者直接把老格式原样保留,"新版一定用 gatekeeper"是个误区,一切以实际文件为准。
厂商魔改也是常见的坑。同一批设备,A 厂的文件 20 字节标准无盐,B 厂的文件 24 字节加了自己的校验,爆破前必须逐台看文件头。这个检查成本很低,但能避免在错误格式上白跑几小时。
6. 验证与反制:三条来自破解视角的防线
6.1 先让脚本破解一个自己设的密码
写代码之前,先做一轮已知答案回归。选一个记得住的手势序列,比如 (0, 1, 2, 3),用脚本把它的 SHA1 写进一个测试文件:
import hashlib seq = bytes([0, 1, 2, 3]) open("test.key", "wb").write(hashlib.sha1(seq).digest())然后让爆破脚本去解 test.key。如果返回 (0, 1, 2, 3),说明枚举、剪枝、比对整条链路是对的;解不出,优先查CROSS表有没有漏填中间点,或者点编码基线错位。这一步只要 30 秒,能挡掉后面至少一半的低级坑。
6.2 破解者视角的三条防线
从破解成功的角度反推,系统的加固要点其实很清楚。
第一,手势存储必须带随机盐。不带盐的 SHA1/SHA256 都挡不住离线枚举,这是最核心的一条。第二,验证逻辑放进可信执行环境,配合连续失败锁定和指数退避,让暴力只能走在线路径,而在线路径的速率会被卡死。单纯把哈希藏进私有目录不够,总会有渠道拿到文件。第三,开启全盘加密。手势密码的破解价值在于能读应用数据,如果磁盘本身是密文,破解锁屏之后拿到的还是一堆密文块。
从那以后,我每次写哈希破解脚本,都会先做一轮已知答案回归,三秒钟成本换来的是不用熬夜排查枚举边界错误。拿到设备的第一件事也从 "开始跑脚本" 改成了 "先确认存储格式版本"。希望帮到你。
本文还有配套的精品资源,点击获取