简介:这份资源围绕ET199智能电子锁的客户号与ATR值修改及模拟操作展开,面向门禁系统开发者、智能卡调试人员及具备一定嵌入式基础的技术爱好者,用于解决更换锁所有者、调整权限配置或适配新智能卡类型时的参数改写需求。压缩包共37个文件,约10.79MB,以C/C++源码、头文件、工程配置与编译产物为主,包含h、cpp、c等源文件,sln、vcxproj、vcproj等工程文件,以及lib、dll、exe、hex、bin等库与固件文件,另附manifest、htm等辅助说明,便于直接编译调试。目前已有775人学习下载。资源提供了客户号与ATR的定位修改思路、硬件接入与软件工具配合方式、备份验证流程,以及模拟ET199响应行为的实现参考,适合在合法授权下研究锁具参数改写与智能卡识别机制的技术人员对照使用。
1. ET199 改客户号与 ATR 模拟:一张锁卡背后的可复现路径
手里拿到一张 ET199 加密锁,客户号被写死、ATR 应答固定、换台机器就报错,这是很多做工业软件授权迁移的人都会撞上的场景。ET199 是早期常见的 USB 加密锁方案,配套的 ATR(Answer To Reset,复位应答)决定了主机识别这把锁的身份特征,而客户号则绑定了授权归属。所谓「改客户号」「模拟 ET199」,本质是两件事:一是把锁内可写的客户标识改成目标值,二是让上位机在握手阶段收到与目标锁一致的 ATR 序列。热词里反复出现的「锁」「改号」「ATR」正好对应这条链路的三段:识别、写入、验证。这篇笔记面向需要复现 ET199 改号与 ATR 模拟的工程师,从协议层拆到命令层,给出可抄作业的步骤、参数和踩坑记录,不涉及任何绕过授权的用途,只讨论自有设备与合法授权范围内的调试。
2. ET199 的识别链路:ATR、客户号与锁状态到底谁管谁
2.1 ATR 在复位阶段决定了什么
ET199 插入主机后,上位机先发复位信号,锁返回一串字节,这就是 ATR。它通常包含协议类型、历史字节和校验信息。上位机驱动会拿这串字节和内置模板比对,匹配不上就直接判定「非本品牌锁」。所以改客户号之前,必须先确认 ATR 是否可改、改哪几个字节。常见做法是先用读卡器抓一次原始 ATR,存成十六进制串,再逐字节对照目标锁的 ATR。注意 ATR 里的历史字节往往带厂商编码,这部分如果被固件写死,就只能靠模拟层在传输时替换,而不是改锁内存储。
2.2 客户号存在哪:可写区与只读区的边界
客户号一般落在锁内的一块配置区,可能是 EEPROM 也可能是带写保护的 Flash 段。判断方法很直接:用厂商工具读一次配置区,记下偏移;尝试写入一个测试值,再读回。如果读回变了,说明可写;如果写命令返回错误码或读回不变,说明该段被锁。血泪经验是,很多 ET199 变种的客户号区和授权计数区相邻,写客户号时如果长度算错,会顺手把计数区覆盖,锁直接报废。所以写之前一定先做全量备份,把整块配置区 dump 出来。
2.3 锁状态:为什么有的锁改完就「锁死」
热词里「锁」出现频率极高,这里说的锁状态指锁内的一次性写保护位或授权绑定标志。ET199 有些批次在出厂时把配置区设为写保护,一旦触发就不可逆。判断锁状态可以看写命令的返回码,也可以看配置区某个标志位。如果标志位显示已保护,改号这条路就走不通,只能转向模拟方案:不改锁,改上位机看到的应答。这也是「模拟 ET199」这个方向存在的现实原因。
2.4 最小识别流程的复现步骤
下面这段 Python 用 pyserial 和基础串口读写演示如何抓取一次 ATR 并解析关键字节。实际硬件可能是读卡器或带 USB 转串口的锁座,端口号按系统实际改。
import serial import time # 打开锁对应的串口,波特率按读卡器手册,常见 9600 或 115200 ser = serial.Serial('COM3', 9600, timeout=1) def reset_and_read_atr(): # 发送复位指令,不同读卡器指令不同,这里用常见 0x00 0xFF 触发 ser.write(bytes([0x00, 0xFF])) time.sleep(0.05) # 读取返回,ATR 长度不定,先读 32 字节 raw = ser.read(32) return raw atr = reset_and_read_atr() print('ATR hex:', atr.hex().upper()) # 解析:前两字节通常是 TS 和 T0,历史字节从 T0 低四位决定 ts = atr[0] t0 = atr[1] hist_len = t0 & 0x0F print('TS:', hex(ts), 'T0:', hex(t0), '历史字节长度:', hist_len) print('历史字节:', atr[2:2+hist_len].hex().upper())逻辑说明:复位后读到的字节流里,TS 表示初始字符,T0 的高四位指示后续接口字节,低四位指示历史字节长度。参数上,timeout 设 1 秒是防止读卡器没响应时卡死;sleep 0.05 秒是给锁复位留时间,太短会读到空。失败时先看端口是否被占用,再看波特率是否匹配,最后确认读卡器是否需要额外握手命令。这一步拿到的 ATR 是后续改号和模拟的基准,务必存文件。
3. 改客户号:从备份到写入的完整命令链
3.1 先备份整块配置区
改号前不备份,等于没有后悔药。ET199 的配置区通常可以通过读命令按页读出,常见页大小 16 或 32 字节。下面脚本按页 dump 并保存为二进制文件,方便后续比对和回滚。
import serial ser = serial.Serial('COM3', 9600, timeout=1) def read_page(page_addr, page_size=16): # 构造读页命令,具体命令码按锁手册,这里用 0x30 示意 cmd = bytes([0x30, (page_addr >> 8) & 0xFF, page_addr & 0xFF, page_size]) ser.write(cmd) return ser.read(page_size) with open('et199_backup.bin', 'wb') as f: for addr in range(0, 0x400, 16): # 假设配置区 1KB data = read_page(addr) if len(data) != 16: print('读取异常,地址:', hex(addr), '长度:', len(data)) break f.write(data) print('备份完成')逻辑说明:page_addr 是页起始地址,page_size 要和锁手册一致,写错会读到错位数据。命令码 0x30 只是示意,实际以你手上锁的协议文档为准。备份文件要保留原始十六进制视图,后面改号时只改目标偏移,其余字节原样写回。如果读取过程中长度不足,说明通信不稳或地址越界,先解决通信再继续。
3.2 定位客户号偏移的三种方法
第一种是差异法:拿一把已知客户号的锁和一把目标锁,各 dump 一份,逐字节比对,差异处大概率就是客户号区。第二种是字符串法:客户号如果是 ASCII,直接在 dump 里搜可见字符串。第三种是写测试法:在疑似区域写一个独特值,读回确认。三种方法结合用最稳。注意有些客户号是加密存储或带校验,直接改明文会导致校验失败,这时要连校验字节一起算。
3.3 写入客户号并校验
确认偏移后,构造写命令。下面示例把偏移 0x20 处的 8 字节客户号改成目标值,写完后立即读回校验。
import serial ser = serial.Serial('COM3', 9600, timeout=1) def write_page(page_addr, data): # 写页命令,命令码 0x40 示意,数据长度需与页大小一致 cmd = bytes([0x40, (page_addr >> 8) & 0xFF, page_addr & 0xFF]) + data ser.write(cmd) return ser.read(2) # 返回状态码 def read_page(page_addr, page_size=16): ser.write(bytes([0x30, (page_addr >> 8) & 0xFF, page_addr & 0xFF, page_size])) return ser.read(page_size) # 读取原页,只改客户号部分 page = bytearray(read_page(0x20)) new_customer = b'CUST2024' # 8 字节目标客户号 page[0:8] = new_customer status = write_page(0x20, bytes(page)) print('写状态:', status.hex()) # 读回校验 verify = read_page(0x20) print('校验:', verify[0:8])逻辑说明:写命令通常返回状态码,0x00 或 0x90 表示成功,其他值要查手册。参数上,客户号长度必须和原字段一致,多写少写都会破坏相邻数据。校验步骤不能省,读回不一致说明写保护未解除或命令码不对。如果写状态返回错误,先检查锁状态标志,再确认是否需要对配置区先发解锁命令。
3.4 写保护与解锁命令的常见形态
部分 ET199 在写配置区前需要先验证口令或发解锁序列。常见形态是先写一个口令到指定地址,再在限定时间内发写命令。口令可能是固定值,也可能由客户号派生。如果手册没写,可以用逻辑分析仪抓一次厂商工具的通信,看它在写之前发了什么。注意抓包只用于自有设备调试,不要用于未授权场景。
4. ATR 模拟:不改锁也能让上位机认账
4.1 模拟层的两种落点
ATR 模拟有两种做法:一是在读卡器固件层拦截并替换返回字节;二是在上位机驱动层挂钩,把读到的 ATR 改成目标值。前者对硬件有要求,后者更通用但需要处理驱动签名和兼容性。常见做法是用一个中间层串口转发程序,主机以为在跟锁通信,实际中间层按规则改写 ATR 和后续应答。这种方式不改锁,适合锁已写保护的情况。
4.2 串口转发模拟的最小实现
下面是一个串口转发骨架,把真实锁的 ATR 替换成目标 ATR,其余数据透传。实际部署时中间层要接两个串口或一个虚拟串口对。
import serial # 真实锁端口和上位机端口,虚拟串口对可用 com0com 等工具创建 real_lock = serial.Serial('COM4', 9600, timeout=0.1) upstream = serial.Serial('COM5', 9600, timeout=0.1) TARGET_ATR = bytes.fromhex('3B 12 34 56 78 90 00') # 目标 ATR,按实际填 def forward(): # 上位机到锁方向,透传 if upstream.in_waiting: real_lock.write(upstream.read(upstream.in_waiting)) # 锁到上位机方向,拦截复位应答 if real_lock.in_waiting: data = real_lock.read(real_lock.in_waiting) if data.startswith(b'\x3B'): # 检测到 ATR 起始 upstream.write(TARGET_ATR) else: upstream.write(data) while True: forward()逻辑说明:TARGET_ATR 要和目标锁完全一致,包括校验字节。转发循环里用 in_waiting 做非阻塞读,避免卡死。参数上,timeout 设 0.1 秒是平衡实时性和 CPU 占用。失败时先确认虚拟串口对是否配对,再看 ATR 检测条件是否命中,有些锁的 ATR 起始字节不是 0x3B,要按实际改。
4.3 模拟方案的边界与风险
模拟层能解决识别问题,但解决不了锁内授权计算。如果上位机后续还要用锁内密钥做运算,单纯改 ATR 会在下一步失败。所以模拟前要确认上位机的校验深度:只比 ATR,还是还要读客户号、做挑战应答。只比 ATR 的场景模拟成本最低,深度校验的场景要么改锁,要么在中间层完整实现协议。这里没有银弹,先摸清校验链路再选方案。
5. 避坑与排查:改号模拟中最容易翻车的五件事
5.1 写客户号后锁完全不响应
现象:写命令返回成功,但重新插拔后锁无应答。原因:写入长度覆盖了相邻的配置或校验区,导致固件启动校验失败。解决:用备份文件整块回写,恢复原状;下次写之前严格按字段长度操作,写完立即读回全页比对。
5.2 ATR 替换后上位机仍报错
现象:中间层已返回目标 ATR,上位机还是提示设备不匹配。原因:上位机不只比 ATR,还比了客户号或后续握手字节。解决:用抓包工具记录完整通信,看它在 ATR 之后还发了什么、期待什么,把模拟层扩展到后续应答。
5.3 写保护标志误判
现象:读到的标志位显示未保护,但写命令一直失败。原因:标志位可能有多个,或者解锁命令没发。解决:对照手册确认所有保护位,抓一次厂商工具的写流程,看解锁序列是否遗漏。
5.4 串口转发丢字节
现象:模拟层运行一段时间后上位机随机报错。原因:转发循环里读和写没有做缓冲,高波特率下丢数据。解决:加大读取缓冲,用线程分别处理两个方向,或者降低波特率测试。参数上,timeout 和 read 大小要匹配实际流量。
5.5 备份文件不可用
现象:想回滚时发现备份文件读不回去。原因:备份时页地址或页大小和实际不符,文件内容错位。解决:备份后立即做一次回写测试,确认能恢复;备份文件保留多份,记录当时的端口参数和命令码。
6. 进阶:用校验和反推客户号编码规则
改号做到后面,会遇到客户号不是明文而是带校验或加密的情况。这时可以固定其他字节,只变客户号,观察校验字节的变化,用几组样本反推规则。常见是简单累加和或 CRC8。下面这段代码演示用多组样本暴力匹配校验算法。
import itertools samples = [ (b'CUST0001', 0x3A), (b'CUST0002', 0x3B), (b'CUST0003', 0x3C), ] def checksum_sum(data): return sum(data) & 0xFF def checksum_xor(data): r = 0 for b in data: r ^= b return r for name, func in [('sum', checksum_sum), ('xor', checksum_xor)]: ok = all(func(s[0]) == s[1] for s in samples) print(name, '匹配' if ok else '不匹配')逻辑说明:samples 里是客户号和对应校验字节,先用常见算法试。如果都不匹配,可能是 CRC 或带密钥的算法,需要更多样本或抓取写入过程。参数上,样本要覆盖不同字符,避免巧合。反推出规则后,改号时连校验一起算,才能通过上位机验证。
我自己的习惯是,每次动锁之前先做三件事:备份、抓包、记录参数。这三样齐了,翻车也能救回来。ET199 这类老锁的资料散、批次杂,别信单一来源的手册,以实测为准。希望帮到你。
本文还有配套的精品资源,点击获取