资讯动态

Python还原安卓手势密码:从哈希提取到路径穷举

发布时间:2026/10/26 14:03:58 来源:尧图企业网站定制
简介这份资源面向对Android安全机制感兴趣的学习者与Python开发者聚焦于通过Python脚本还原Android锁屏手势密码这一具体问题。其核心思路是读取系统存储的gesture.key文件利用SHA1加密特征进行逆向比对从而推导出原始手势轨迹适合具备一定Python基础、希望理解移动端认证机制与哈希碰撞原理的读者参考实践。压缩包共12个文件约61KB以xml工程配置、key样本文件、png示意图、md说明文档与py主脚本为主结构紧凑便于快速定位核心代码与验证数据。目前已有1223人学习下载说明该主题在安全入门群体中具有一定关注度。读者可从中获得完整的脚本实现、示例密钥与解密结果对照以及从加密存储到手势还原的完整思路为后续研究移动端安全与哈希算法应用提供可复用的参考范例。1. 从一次取证说起这个 Python 项目到底在解什么去年帮朋友处理一台旧安卓机屏幕锁着里面有一批孩子照片要导出来机主本人忘了手势。试了十几次手机开始倒计时锁定越试越慌。后来翻到一个思路安卓的锁屏手势密码在系统里并不是存明文而是存一串经过 SHA-1 加盐哈希后的字节只要拿到那串哈希就能在本地用 Python 穷举出对应的手势路径。这个项目干的就是这件事——把安卓手势密码的哈希提取出来用 Python 还原成一条能画出来的手势轨迹。它面向的不是普通用户而是做移动取证、安全教学、设备恢复的开发者你需要懂一点 adb、懂一点哈希剩下的交给脚本。关键词 Python 在这里不是装饰整个还原逻辑、路径枚举、哈希比对全靠 Python 标准库完成没有第三方依赖跑起来门槛很低。2. 原理拆解手势密码在系统里到底存成了什么2.1 手势密码的存储位置与格式安卓把锁屏凭据统一放在/data/system/目录下手势密码对应的是gesture.keyPIN 码是password.key数字密码是password.key的另一套编码。gesture.key的内容非常短通常只有 20 字节因为它就是一个 SHA-1 摘要。系统在用户画完手势后把 3x3 九宫格上的节点按顺序编号成 0 到 8把这条路径拼成一个字节序列再对这个序列做一次 SHA-1把 20 字节结果写进文件。校验时系统重新算一遍当前手势的 SHA-1和文件里的字节逐位比较相等就解锁。所以还原的本质不是“破解加密”而是“已知哈希反推原文”——手势空间很小穷举完全可行。这里有个容易翻车的点不同安卓版本对盐的处理不一样。早期版本直接对路径字节做 SHA-1较新版本会混入一个 saltsalt 存在另一个文件里。项目正文没展开版本差异但实际动手时你得先确认目标机的 Android 版本否则算出来的哈希永远对不上白跑一晚上。2.2 九宫格编号规则与路径约束九宫格的编号是固定的从左到右、从上到下0 1 2 3 4 5 6 7 8一条合法手势有几个硬约束至少连接 4 个点最多 9 个点同一个点不能重复经过。还有一个很多人忽略的规则——如果两个点之间隔着中间点而中间点还没被用过那这条边是非法的。比如从 0 直接到 2中间隔着 1如果 1 还没被连过系统不允许这一步。这条规则直接决定了穷举空间的大小也决定了你的枚举代码必须做合法性剪枝否则会生成大量系统根本不认的路径白白增加比对次数。2.3 为什么穷举是可行的合法手势的总数大约在 38 万条量级这个数字对现代 CPU 来说就是几秒钟的事。SHA-1 单次计算在 Python 里虽然比 C 慢但 38 万次也就是一两秒到十几秒的区间完全在可接受范围。这就是这个项目成立的根本前提空间小、哈希快、无需网络。对比一下 PIN 码4 位 PIN 只有 1 万种组合但系统有错误次数限制手势密码反而因为空间稍大而没有硬件级限速离线穷举成了最务实的路径。3. 环境与数据准备把哈希拿到本地3.1 依赖与运行环境项目本身是纯 Python标准库hashlib、itertools足够Python 3.6 以上都能跑。真正的前置条件是 adb 和一台能开 USB 调试的设备。我一般会先确认三件事adb 版本、设备是否授权、目标文件是否可读。# 确认 adb 可用且设备已连接 adb version adb devices # 拉取手势哈希文件到本地 adb pull /data/system/gesture.key ./gesture.key # 查看文件大小正常应为 20 字节 ls -l ./gesture.keyadb devices输出里设备状态必须是device而不是unauthorized后者说明手机上还没点“允许调试”。adb pull对/data/system/这种系统目录通常需要 root普通权限会报Permission denied。如果拉不下来常见做法是先adb root再重试或者在有 root 的 shell 里cat出来重定向。文件大小是第一个校验点20 字节说明是裸 SHA-1如果明显更大可能混了额外头部或 salt得先拆。3.2 把哈希读成字节序列拿到文件后不要急着比对先把内容读成bytes并打印十六进制确认没有换行符或 BOM 污染。我见过有人用文本模式读结果末尾多了\n哈希永远差一位。import hashlib def load_hash(path): # 二进制模式读取避免换行符污染 with open(path, rb) as f: raw f.read() # 去掉可能的空白字节保留前 20 字节 digest raw.strip()[:20] print(hash hex:, digest.hex()) return digest target load_hash(./gesture.key)strip()处理掉尾部可能的空白[:20]兜底截断。打印十六进制是为了肉眼核对长度40 个十六进制字符对应 20 字节多一个少一个都说明读取方式有问题。这一步看着琐碎但它是后面所有比对的地基读错一个字节穷举再快也是零。4. 核心实现路径枚举与哈希比对4.1 九宫格邻接关系与合法性判断枚举之前先把“哪些点之间可以直接连”编码成数据结构。我习惯用一个集合存非法跳跃的中间点映射判断时查表即可。# 九宫格坐标索引即编号 POS [(i % 3, i // 3) for i in range(9)] # 中间点映射key 为 (起点, 终点)value 为被跨过的中间点 SKIP { (0, 2): 1, (2, 0): 1, (0, 6): 3, (6, 0): 3, (0, 8): 4, (8, 0): 4, (1, 7): 4, (7, 1): 4, (2, 6): 4, (6, 2): 4, (2, 8): 5, (8, 2): 5, (3, 5): 4, (5, 3): 4, (6, 8): 7, (8, 6): 7, } def can_move(path, nxt): if nxt in path: return False if not path: return True last path[-1] mid SKIP.get((last, nxt)) # 有中间点且中间点未被访问过则非法 if mid is not None and mid not in path: return False return TruePOS只是把编号还原成坐标方便调试时画图核对。SKIP覆盖了所有“隔点”情况注意它是双向的所以每个方向都写了一条。can_move先查重再查中间点是否已访问——这是整个枚举正确性的核心漏掉这条规则会多出大量非法路径虽然不影响最终命中但会拖慢速度并让日志变得难以核对。4.2 深度优先枚举所有合法路径用递归做 DFS路径长度从 4 到 9每到一个长度就和目标哈希比一次。这里有个优化点不要等枚举完再统一比对边走边比命中即返回。import hashlib def dfs(target, path, results): if len(path) 4: # 把路径转成字节序列再哈希 seq bytes(path) if hashlib.sha1(seq).digest() target: results.append(list(path)) if len(path) 9: return for nxt in range(9): if can_move(path, nxt): path.append(nxt) dfs(target, path, results) path.pop() def crack(target): results [] dfs(target, [], results) return resultsbytes(path)是关键手势路径在系统里就是按节点编号顺序拼成的字节串不是字符串也不是带分隔符的文本。hashlib.sha1(...).digest()返回原始 20 字节和gesture.key的内容直接可比不要用hexdigest()去和十六进制字符串比那样多一层转换还容易出错。results用列表是因为极少数情况下可能存在哈希碰撞虽然概率极低但保留多条结果便于人工判断。递归深度最大 9不用担心栈溢出。4.3 结果还原成可读手势命中后拿到的是一串 0 到 8 的数字直接给人看没意义得转成九宫格上的坐标序列最好再画个简图。def render(path): grid [[. for _ in range(3)] for _ in range(3)] for step, node in enumerate(path, 1): x, y POS[node] grid[y][x] str(step) for row in grid: print( .join(row)) print(sequence:, -.join(map(str, path))) for p in crack(target): render(p)render把每一步的序号填进对应格子这样一眼能看出轨迹走向sequence那行给出原始编号顺序方便在手机上照着画。如果有多条结果逐条打印人工比对哪条更像机主习惯的路径——实际取证里经常靠这个做二次确认。5. 避坑与排查那些让我重跑整晚的细节5.1 拉下来的文件不是 20 字节现象ls -l显示gesture.key是 0 字节或者几百字节。原因通常是权限不足导致adb pull静默失败或者目标机根本不是手势锁而是 PIN 锁文件名拿错了。解决先adb shell ls -l /data/system/*.key看真实文件列表和大小确认是gesture.key且为 20 字节再拉权限问题先adb root仍不行就在 shell 里cat后重定向到/sdcard/再 pull。5.2 哈希对不上穷举跑完零结果现象脚本正常结束results为空。原因多半是版本差异——新系统混了 salt或者路径字节的编码方式不是简单的bytes(path)。解决先确认 Android 版本查该版本gesture.key的生成逻辑如果存在 salt 文件把它读进来拼在路径字节前后再哈希顺序错了同样对不上。我一般会先用一个已知手势在测试机上生成哈希反向验证自己的拼接顺序确认无误再上真机。5.3 枚举结果里出现系统不认的路径现象脚本报了一条“命中”但照着画解不开。原因通常是SKIP表漏了某个方向或者can_move里中间点判断写反了。解决拿一条已知合法路径和一条已知非法路径做单元测试比如[0,1,2]合法、[0,2]在 1 未访问时非法跑通再扩大枚举。别小看这条血泪经验是表里少写一个(8,0):4结果多跑出几万条废路径。5.4 脚本跑得比预期慢很多现象几分钟还没出结果。原因一般是每次比对都重新构造bytes并调用hashlib.sha1函数调用开销在 38 万次量级下被放大或者用了hexdigest()做字符串比较。解决把hashlib.sha1绑定到局部变量用digest()直接比字节路径用列表原地append/pop而不是每层切片复制。这些改动不大但能把耗时压下来一截。提示所有操作只在自己拥有或明确授权的设备上进行取证场景记得保留操作日志别把测试机和生产机搞混。6. 二次开发把它改成顺手的取证小工具这个项目的骨架很小正因为小改造空间反而大。我一般会从三个方向动刀。第一是加版本适配层把“路径字节如何拼、是否加 salt、salt 从哪读”抽成一个策略函数按 Android 版本分支主流程不变。第二是加结果打分多条命中时按路径长度、转折次数、是否经过中心点给一个“人类习惯分”把最可能的排前面减少人工比对。第三是加批量模式一次读多个gesture.key输出 CSV字段包括文件名、命中路径、耗时方便取证报告直接引用。验证改造是否正确的办法很土但有效在测试机上手动设一个手势跑脚本看能否还原再换一个带 salt 的版本重复一遍。两次都过基本就稳了。下面这个策略函数的骨架可以直接抄def build_payload(path, saltNone, modeplain): seq bytes(path) if mode plain: return seq if mode salted_prefix: return (salt or b) seq if mode salted_suffix: return seq (salt or b) raise ValueError(unknown mode)mode决定拼接方式salt从对应文件读入主流程只调build_payload再哈希版本差异被隔离在一个函数里。改完之后记得把crack里的哈希调用也换成走这个函数否则新旧逻辑会打架。还有一个我踩过的坑别急着上多进程。38 万次 SHA-1 在单进程里也就十几秒多进程的进程创建和结果合并开销可能比省下的时间还多除非你要批量处理几十台设备。真要提速先把 DFS 里的重复计算去掉再考虑并行。从那以后我每次改这类枚举脚本都强制先用一条已知路径做端到端回归——设手势、拉哈希、跑还原、比对结果四步缺一不可。工具再小验证链路不完整就是给自己埋雷。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑