用Python和APDU命令探索USIM卡文件系统的实战指南
当你把手机卡插入设备时,它不仅仅是一个身份标识——实际上,这是一套完整的微型操作系统。本文将带你用Python脚本和APDU命令,像安全研究员一样亲手探索USIM卡内的文件系统结构。
1. 准备工作:搭建USIM卡探索环境
要开始探索USIM卡,你需要准备以下硬件和软件:
- 支持APDU命令的读卡器:推荐使用ACR122U或类似型号,价格约200-500元
- 一张可读写的USIM卡:运营商提供的普通USIM卡即可
- Python开发环境:建议3.8+版本
- 必要的Python库:
pip install pyscard python-apdu
注意:操作USIM卡有一定风险,建议在测试卡或备份重要数据后进行实验
连接读卡器后,用以下代码检测是否识别到卡片:
from smartcard.System import readers # 获取可用读卡器列表 reader_list = readers() if not reader_list: raise Exception("未检测到读卡器") # 连接第一个读卡器 connection = reader_list[0].createConnection() connection.connect() print(f"已连接: {reader_list[0]}") print(f"ATR: {connection.getATR()}")2. APDU命令基础:与USIM卡对话的语言
APDU(Application Protocol Data Unit)是与智能卡通信的基本单位。一个完整的APDU命令包含以下部分:
| 字段 | 长度 | 描述 |
|---|---|---|
| CLA | 1字节 | 指令类别 |
| INS | 1字节 | 指令代码 |
| P1 | 1字节 | 参数1 |
| P2 | 1字节 | 参数2 |
| Lc | 0-3字节 | 数据域长度 |
| Data | 变长 | 命令数据 |
| Le | 0-3字节 | 期望响应长度 |
用Python发送APDU命令的通用函数:
def send_apdu(connection, cla, ins, p1, p2, data=None, le=0): apdu = [cla, ins, p1, p2] if data: apdu.append(len(data)) apdu.extend(data) if le > 0: apdu.append(le) response, sw1, sw2 = connection.transmit(apdu) return response, (sw1 << 8) | sw2 # 示例:选择MF(主文件) response, status = send_apdu(connection, 0x00, 0xA4, 0x00, 0x00, [0x3F, 0x00]) print(f"响应: {response}, 状态: {hex(status)}")3. 探索文件系统:从MF到EF的完整遍历
USIM卡的文件系统采用树状结构,主要包含三种文件类型:
- MF (Master File):根目录,固定ID为3F00
- DF (Dedicated File):专用目录,相当于文件夹
- EF (Elementary File):基础文件,存储实际数据
3.1 选择并解析MF
# 选择MF response, status = send_apdu(connection, 0x00, 0xA4, 0x00, 0x00, [0x3F, 0x00]) if status != 0x9000: raise Exception("选择MF失败") # 获取MF的FCP(文件控制参数) response, status = send_apdu(connection, 0x00, 0xA4, 0x00, 0x00, [0x3F, 0x00], 0x10) print(f"MF FCP: {bytes(response).hex()}")3.2 遍历DF和EF
以下函数可以递归遍历USIM卡的文件系统:
def explore_filesystem(connection, current_path, depth=0): indent = " " * depth print(f"{indent}探索路径: {'/'.join(f'{b:02X}' for b in current_path)}") # 选择当前路径 if current_path: response, status = send_apdu(connection, 0x00, 0xA4, 0x08, 0x00, current_path) if status != 0x9000: print(f"{indent}选择失败: {hex(status)}") return # 获取当前目录下的文件列表 (假设有EFDIR) response, status = send_apdu(connection, 0x00, 0xA4, 0x00, 0x00, [0x2F, 0x00]) # 尝试选择EFDIR if status == 0x9000: # 读取EFDIR内容 response, status = send_apdu(connection, 0x00, 0xB0, 0x00, 0x00, le=0xFF) print(f"{indent}EFDIR内容: {bytes(response).hex()}") # 尝试选择常见DF common_dfs = [ ([0x7F, 0x10], "DFTELECOM"), ([0x7F, 0x20], "DFGSM"), ([0x7F, 0x21], "DFDCS1800"), ([0x7F, 0xFF], "当前ADF") ] for df_id, df_name in common_dfs: new_path = current_path + df_id response, status = send_apdu(connection, 0x00, 0xA4, 0x00, 0x00, df_id) if status == 0x9000: print(f"{indent}发现DF: {df_name} ({bytes(df_id).hex()})") explore_filesystem(connection, new_path, depth+1) # 从MF开始探索 explore_filesystem(connection, [])4. 解析关键文件:IMSI、ICCID等
USIM卡中存储着许多重要信息,以下是常见EF文件及其ID:
| 文件名称 | FID | 描述 |
|---|---|---|
| EF_ICCID | 2FE2 | 卡唯一标识 |
| EF_IMSI | 6F07 | 用户标识 |
| EF_LP | 6F05 | 语言偏好 |
| EF_AD | 6FAD | 管理数据 |
4.1 读取IMSI示例
def read_imsi(connection): # 选择DFTELECOM response, status = send_apdu(connection, 0x00, 0xA4, 0x00, 0x00, [0x7F, 0x10]) if status != 0x9000: raise Exception("选择DFTELECOM失败") # 选择EF_IMSI response, status = send_apdu(connection, 0x00, 0xA4, 0x00, 0x00, [0x6F, 0x07]) if status != 0x9000: raise Exception("选择EF_IMSI失败") # 读取IMSI (前9字节) response, status = send_apdu(connection, 0x00, 0xB0, 0x00, 0x00, le=9) if status != 0x9000: raise Exception("读取IMSI失败") # IMSI格式解析 imsi_bytes = bytes(response) imsi_digits = [] for b in imsi_bytes: imsi_digits.append(f"{(b >> 4) & 0x0F}") imsi_digits.append(f"{b & 0x0F}") # 第一个字节的高4位是长度 imsi_length = int(imsi_digits[0]) imsi = "".join(imsi_digits[1:imsi_length+1]) return imsi print(f"IMSI: {read_imsi(connection)}")4.2 读取ICCID示例
def read_iccid(connection): # 选择MF response, status = send_apdu(connection, 0x00, 0xA4, 0x00, 0x00, [0x3F, 0x00]) if status != 0x9000: raise Exception("选择MF失败") # 选择EF_ICCID response, status = send_apdu(connection, 0x00, 0xA4, 0x00, 0x00, [0x2F, 0xE2]) if status != 0x9000: raise Exception("选择EF_ICCID失败") # 读取ICCID (10字节) response, status = send_apdu(connection, 0x00, 0xB0, 0x00, 0x00, le=10) if status != 0x9000: raise Exception("读取ICCID失败") # ICCID格式解析 iccid_bytes = bytes(response) iccid_digits = [] for b in iccid_bytes: iccid_digits.append(f"{(b >> 4) & 0x0F}") iccid_digits.append(f"{b & 0x0F}") # 去除可能的填充F iccid = "".join(iccid_digits).rstrip('F') return iccid print(f"ICCID: {read_iccid(connection)}")5. 高级技巧:处理TLV格式数据
USIM卡中许多数据采用TLV(Tag-Length-Value)格式存储。以下是一个TLV解析工具函数:
def parse_tlv(data): result = {} index = 0 while index < len(data): tag = data[index] index += 1 # 处理多字节tag (简化版) if (tag & 0x1F) == 0x1F: while (data[index] & 0x80) == 0x80: tag = (tag << 8) | data[index] index += 1 tag = (tag << 8) | data[index] index += 1 # 获取长度 length = data[index] index += 1 if length == 0x81: length = data[index] index += 1 elif length == 0x82: length = (data[index] << 8) | data[index+1] index += 2 # 获取值 value = data[index:index+length] index += length result[hex(tag)] = bytes(value).hex() return result # 示例:解析SELECT响应 response, status = send_apdu(connection, 0x00, 0xA4, 0x00, 0x00, [0x6F, 0x07]) # 选择EF_IMSI if status == 0x9000: tlv_data = parse_tlv(response) print("TLV解析结果:") for tag, value in tlv_data.items(): print(f" {tag}: {value}")6. 安全注意事项与最佳实践
在探索USIM卡文件系统时,需要注意以下安全事项:
- 避免频繁写入:USIM卡有有限的擦写次数(通常约10万次)
- 备份重要数据:操作前备份EF_IMSI等关键文件
- 谨慎处理鉴权相关文件:如EF_Kc、EF_OPc等
- 遵守法律法规:仅对自己的USIM卡进行实验
以下是一些有用的调试技巧:
# 启用APDU日志 def logged_send_apdu(connection, cla, ins, p1, p2, data=None, le=0): cmd = f"CLA: {cla:02X}, INS: {ins:02X}, P1: {p1:02X}, P2: {p2:02X}" if data: cmd += f", Data: {bytes(data).hex()}" if le: cmd += f", Le: {le:02X}" print(f"发送: {cmd}") response, status = send_apdu(connection, cla, ins, p1, p2, data, le) print(f"响应: {bytes(response).hex() if response else ''}, 状态: {status:04X}") return response, status # 示例使用 logged_send_apdu(connection, 0x00, 0xA4, 0x00, 0x00, [0x3F, 0x00])在实际项目中,我发现最常遇到的问题是无法正确解析TLV数据。一个实用的技巧是先用SELECT命令获取文件的FCP(文件控制参数),其中包含了文件的结构信息。