简介:这是一份面向Android开发者与移动端技术研究者的SIM卡管理工具资源包,通过反射调用Android隐藏API,实现SIM卡联系人、短信的增删改查与导出,也能查看SIM卡相关信息。双卡双待设备需将SIM卡放置在主卡槽,整体适合对系统API机制、Android工程结构有进阶需求、且希望动手实践与二次开发的读者。压缩包为zip格式,共738个文件、约7.69MB,主要包含175个xml布局/配置、62个dex、60个class、21个java源码以及jar、gradle、properties等构建与资源文件,既保留了可阅读的源代码,也提供编译后的字节码与类文件,便于对照分析隐藏API的反射调用流程和实际方法签名。已有564人学习下载,对于希望掌握SIM卡管理功能或研究Android隐藏API的开发者,可从源码、XML配置和编译产物入手拆解,理解双卡场景下的卡槽处理逻辑,并将相关能力迁移到自己的工具或功能模块中。资源包内含完整Android工程结构与编译产物,便于离线分析与复用。
1. SIM卡管家到底管什么:从文件系统说起
一张插在手机里的SIM卡,除了用来鉴权,还藏着一套遵循ISO 7816标准的文件系统。联系人存在EF_ADN,短信存在EF_SMS,每一条记录对应一个线性固定长度的数据结构。普通读卡器插到电脑上不会显示任何盘符,因为这张卡根本不走USB-SCSI协议。要想对它做增删改查,要么通过短信猫的AT命令,要么通过PC/SC读卡器直接发APDU指令。这篇博文就把两条路各拆一遍,从最小的可执行命令到Python脚本,再到中文乱码和权限这类必踩的坑。适合正在做SIM卡模块、短信转发器或者后端对接短信猫的工程师。
2. 用AT命令对SIM卡联系人和短信增删改查:先跑通最小命令
GSM模块(也就是常说的短信猫)通过串口或USB虚拟串口暴露AT命令接口。SIM卡插在模块上,模块负责把AT命令转换成SIM卡文件操作。常见的设备有USB转串口的SIM800、SIM900,以及工业级4G模组。先看怎么把链路打通,再逐条执行增删改查。
2.1 环境准备:短信猫 + 串口工具
把SIM卡插入模块,模块的USB接电脑,Linux下一般会枚举成/dev/ttyUSB0。用minicom或picocom打开串口,波特率通常固定为115200或9600。推荐直接用Python的pyserial快速验证:
import serial ser = serial.Serial( port='/dev/ttyUSB0', baudrate=115200, timeout=1, rtscts=False, dsrdtr=False ) ser.write(b'AT\r\n') print(ser.read(64).decode('ascii', errors='replace'))打开串口后先发一个AT,模块返回OK就说明链路通了。需要注意,SIM模块上电后可能处于PIN锁状态,此时需要先发AT+CPIN?确认,返回+CME ERROR: SIM PIN required时用AT+CPIN=1234解锁。这里用\r\n结尾是因为AT命令规范要求每行以回车换行结束。
2.2 联系人的增删改查:AT+CPBR / CPBW / CPBF / CPBD
SIM卡电话簿的AT命令分为读、写、查找、删除四个方向,对应关系如下:
| 操作 | AT命令 | 说明 |
|---|---|---|
| 查询全部联系人 | AT+CPBR=1,250 | 一次读取存储区条目1到250 |
| 新增联系人 | AT+CPBW=<index>,"13512345678",129,"张三" | 129表示国际号码格式 |
| 修改联系人 | AT+CPBW=<index>,"13900000000",129,"李四" | 写入已有索引即覆盖 |
| 删除联系人 | AT+CPBW=<index> | 只给索引,不填号码即删除 |
| 按号码查找 | AT+CPBF="135" | 返回所有含135的号码 |
实际操作时先读一下当前存储列表:
AT+CPBR=1,20返回结果可能是:
+CPBR: 1,"13512345678",129,"张三" +CPBR: 4,"13900000000",129,"李四" +CPBR: 10,"13800001111",129,"王五" OK实现增删改查时,新增和修改共用AT+CPBW,区别在于是否提供索引。不指定索引时模块自动找空位;指定已有索引时覆盖数据;只给索引不给号码时删除该条记录。删除必须跟原始索引,所以先用AT+CPBR拉全量,再定位索引。
2.2.1 用Python封装联系人操作
class SimContactManager: def __init__(self, ser): self.ser = ser self.ser.reset_input_buffer() def _send_at(self, cmd, wait='OK'): self.ser.write(f'{cmd}\r\n'.encode()) buf = b'' while True: chunk = self.ser.read(128) buf += chunk if wait in buf.decode('ascii', errors='ignore'): break return buf.decode('ascii', errors='replace') def read_all(self): resp = self._send_at('AT+CPBR=1,250') contacts = [] lines = resp.splitlines() for line in lines: if line.startswith('+CPBR:'): # +CPBR: <index>,"<number>",<type>,"<name>" parts = line.split(',', 3) idx = int(parts[0].split()[1]) number = parts[1].strip('"') name = parts[3].rsplit('"', 1)[0].strip('"') contacts.append((idx, number, name)) return contacts def add(self, number, name): return self._send_at(f'AT+CPBW="{number}",129,"{name}"') def update(self, index, number, name): return self._send_at(f'AT+CPBW={index},"{number}",129,"{name}"') def delete(self, index): return self._send_at(f'AT+CPBW={index}')代码核心是给模块发送带参数的AT+CPBW命令,解析时需要注意返回行可能被串口缓冲拆分。split(',', 3)把前三个逗号分开,保证姓名即使包含逗号也不会被截断。rsplit('"', 1)[0]是为了从形如+CPBR: 1,"135...",129,"张三"的字符串中干净取出姓名。
2.3 短信的增删改查:AT+CMGR / CMGS / CMGD / CMGW
短信在SIM卡中也有专门存储区,AT命令支持读、写(发未发)、发送、删除。先设置短信格式再操作:
AT+CMGF=1 # 切到text模式 AT+CSMP=17,167,0,0 # 设置tp参数,0表示短信未发送常用命令:
| 操作 | AT命令 | 说明 |
|---|---|---|
| 读短信 | AT+CMGR=<index> | 按索引读取 |
| 发短信 | AT+CMGS="139..." | 随后输入内容并以0x1A结束 |
| 存草稿 | AT+CMGW="139..." | 写入SIM卡不发送,返回索引 |
| 删除短信 | AT+CMGD=<index> | 删除单条 |
| 列出所有短信 | AT+CMGL="ALL" | 按状态过滤 |
短信写完一定要用十六进制0x1A(即Ctrl+Z)提交,不能用\r\n直接提交。这是最常见的失败点。
2.3.1 完整读取一条短信
def read_sms(index): ser.write(f'AT+CMGR={index}\r\n'.encode()) buf = b'' while True: buf += ser.read(128) if b'OK' in buf: break text = buf.decode('utf-8', errors='replace') # 解析 +CMGR: "REC READ","139...","","23-05-12 10:20" # 正文在下一行 lines = text.splitlines() payload = lines[1] if len(lines) > 1 else '' return payloadAT+CMGR返回的正文可能因为中文编码问题变成UCS2字符,后面第4章单独说这个坑。如果是纯英文短信,text模式下直接可读。写入短信时用CMGW比CMGS更稳妥,因为CMGW只存不发,可以反复验证格式正确后再让模块真正发送。
2.4 这三个必调参数和两个典型误用
第一个参数是AT+CSMP,它控制短信的TP-Status、TP-Message-Reference等,默认值在各厂商模块间不一致,不设置可能导致短信存储后状态异常。建议统一设成17,167,0,0。第二个参数是AT+CSCS,设置字符集。想正确存中文姓名,先发AT+CSCS="UCS2",然后号码及姓名内容也要转成UCS2十六进制字符串。第三个参数是AT+CPMS,选择存储介质:
AT+CPMS="SM","SM","SM"分别指读取来源、写入目标、接收目标,这里全部指定为SIM卡。许多模块默认第一参数是"ME"(模块内存),不设这个命令,你存的短信可能根本没写进SIM卡。
常见误用是把AT+CMGD=1当作删除索引1,实际有些固件要求先发AT+CMGR读到索引,再删。另一个误用是认为AT+CPBR=1,250会返回所有条目的索引,但SIM卡存储区中的空位往往被跳过,删除后索引不会自动压缩。
3. 底层方案:用PC/SC APDU直接读写EF_ADN和EF_SMS
如果不用短信猫,直接把SIM卡放进PC/SC读卡器(类似读银行卡的USB读卡器),系统会把它识别为一个ICC设备。这时可以自定义APDU命令,直接操作ISO 7816规定的文件系统,适合需要做更精细控制的场景。
3.1 ISO 7816文件系统与EF文件映射
SIM/USIM卡内部通过MF(主文件)、DF(专有文件)、EF(基础文件)三层管理。联系人和短信分别存放在特定EF文件里:
| 文件ID(十六进制) | 文件名 | 内容 |
|---|---|---|
| 0x6F3A | EF_ADN | 固话/手机号电话簿,每条记录长度通常为28或34字节 |
| 0x6F3C | EF_SMS | 短信记录,每条约176字节 |
| 0x6F3B | EF_SMSR | 短信状态报告 |
| 0x6F40 | EF_SMSP | 短信参数 |
访问这些文件要先选择MF和对应DF,具体路径:3F00(MF)→7F10(DF_TELECOM)→6F3A。通过读卡器发APDU时,选择命令格式为:
00 A4 04 04 02 7F10前两个字节是CLA和INS,A4表示SELECT,04 04表示按文件ID选择,后面跟上根目录到目标DF的路径。选择完后才能执行READ BINARY或UPDATE BINARY。
3.2 选EF:ADN、SMS、SMSR
实际项目中如果拿到的SIM卡里既有联系人又有短信,我建议先用SELECT逐层进入:先选3F00,再选7F10,最后选6F3A。对于短信,则选6F3C。每个EF文件都有EF-STATUS信息,可以用00 B2 XX之类的命令读,但更简单的是直接尝试读取文件头,确认文件存在或返回6A82(文件未找到)。
3.3 Python pyscard 实现联系人读取
pyscard是PC/SC跨平台库的Python绑定。最小脚本如下:
from smartcard.System import readers from smartcard.util import toHexString, toBytes r = readers() connection = r[0].createConnection() connection.connect() # 1. 选择MF conn.transmit(toBytes('00 A4 00 00 02 3F 00')) # 2. 选择DF_TELECOM conn.transmit(toBytes('00 A4 00 00 02 7F 10')) # 3. 选择EF_ADN conn.transmit(toBytes('00 A4 00 00 02 6F 3A')) # 4. 读取EF文件大小,先读文件控制参数FCP resp, sw1, sw2 = conn.transmit(toBytes('00 C0 00 00 00')) # 或者直接用 0xB0 读固定长度,但最好先读 0x200 字节 resp, sw1, sw2 = conn.transmit(toBytes('00 B0 00 00 20')) print(toHexString(resp))读取出来的原始字节需要解析ADN记录。ADN记录格式因SIM类型而异,但大概率满足以下结构:
- 长度首字节表示记录长度(不含首字节)
- 第1字节为关联ID,
FF表示无关联号码 - 第2字节为电话号码长度
- 后续字节为BCD编码的电话号码,两位数字压缩一个字节
- 再后面为姓名的文本字节(GSM 7-bit或UCS2)
用hexdump看原始数据,再逐字段拆。常见的坑是号码长度不固定,手机号超过12位时类型标识符和拨号字符串会进入下一段。
3.3.1 用APDU写一条联系人
写入用UPDATE命令,命令格式为:
00 DC 00 <offset> <len> <data>例如写索引1,需要先摁住EF_ADN内部偏移量。但多数读卡器要求先发00 C0读多条记录,然后按记录的字节排列写入。实操中建议直接发00 DC到对应记录起始地址。写之前要在文件控制数据(FCP)里读取记录长度和文件大小,否则偏移量会错位。
3.4 短信读取与删除的APDU流程
短信存储在EF_SMS中,每一条记录由状态字节(80表示已读,40表示未读)加上TPDU内容组成。读取命令与联系人读取相同:
# 选择EF_SMS connection.transmit(toBytes('00 A4 00 00 02 6F 3C')) # 假设EF_SMS大小为176字节,读第一条记录偏移0 resp, sw1, sw2 = connection.transmit(toBytes('00 B0 00 00 00'))如果sw1, sw2返回6A83,说明记录不存在。删除短信在APDU层面通常不是专门命令,而是把该记录的状态字节改为00或写入空数据。为了不破坏文件结构,常见做法是向该记录位置写入全零数据,再把状态置为00:
zero_payload = [0x00] * 176 connection.transmit([0x00, 0xDC, 0x00, 0x00, 0xB0] + zero_payload)这里0xDC是UPDATE BINARY,0xB0是长度。需要注意,不同USIM格式的记录长度可能不同,不能盲目写176字节,必须先读FCP获取实际大小。
4. 实战排错:为什么读出来的中文短信是乱码,电话簿无法写入?
不管用AT命令还是APDU,SIM卡管家的开发一定会遇到两类高频故障:中文编码不对,以及文件写入被权限拦截。这一章把排错思路和验证方法串起来。
4.1 中文编码:UCS2 vs PDU vs GSM 7bit
AT命令的text模式下,默认字符集可能是GSM 7bit,这种编码根本无法表达中文。所以先发AT+CSCS="UCS2",然后号码和姓名都要转换成十六进制UCS2。举例,姓名“张三”转成十六进制5F205B50,那么新增命令变成:
AT+CPBW=1,"00310033003500310032003300340035",129,"5F205B50"这里的号码也转成了UCS2。如果不转,模块会返回ERROR或写入乱码。对于短信,读出来的UCS2正文是一长串十六进制,需要两个字节一组转成字符:
def hex_ucs2_to_str(s): if len(s) % 4 != 0: return s b = bytes.fromhex(s) return b.decode('utf-16-be', errors='replace')如果返回内容以0891开头,则短信是PDU模式。PDU比text模式复杂,它把SMSC信息、报头、编码方案和正文全部打包。排查时先看模块返回的开头几个字节,0891是SMSC地址长度+地址,后面跟随TPDU。遇到PDU直接切换AT+CMGF=0再处理和解析,不要试图在text模式下强行解PDU。
4.2 存储状态:EF_STATUS位、去重与NV损坏
APDU读写时经常遇到能够选择文件,但读取就返回6B00(错误参数)或6A86(参数错误)。大概率是EF文件内部管理字节中标记了“文件损坏”或“记录数溢出”。SIM卡中电话簿和短信的文件大小是出厂就固定的,比如EF_ADN常见140字节,最多5条记录;EF_SMS最多10~15条。删除后文件大小不会缩小,只是标记对应记录状态为无效。因此写入新联系人时如果SIM卡存储区受限,必须先把同名或同号码的旧记录清掉,否则会报+CME ERROR: memory full。
注意:SIM卡是EEPROM,写入次数有限。频繁增删改查会磨损存储单元。自己开发测试时不要用循环刷同一个索引,否则可能把卡片写废。批量写入脚本可以加一个“仅在内容变化时写入”的判断。
4.3 权限问题:READ/UPDATE条件与PIN
SIM卡文件系统从设计上就分AT直通权限和CHV权限。ADN和SMS文件通常允许PIN解锁后读写,但不同运营商的卡可能把UPDATE条件设为ADM(管理员),普通AT命令无法改写。遇到这种卡,AT+CPBW会返回+CME ERROR: operation not allowed。此时只能通过PC/SC发APDU,先验证PIN:
00 20 00 01 08 31 32 33 34 35 36 37 3800 20是VERIFY命令,00 01指CHV1,后面8字节是BCD编码的PIN。验证通过后再次尝试UPDATE。如果仍被拒绝,多半是文件条件被运营商写死为NEV(永不验证),AT+CPBR能读不能写,这种情况下任何常规手段都改不了,只能把卡换掉。
4.4 用日志和交叉验证锁定问题根源
排查时我一般分三层记录:应用层的参数、AT模块的原始返回、以及APDU层面的SW状态码。单独看AT返回OK只能说明模块收到命令,不代表SIM卡真的写入成功。要验证是否真写进去了,干一件事:写完后立刻读回同一索引,比对字段是否一致。删除也同理,删完读回,必须收到“记录不存在”才算成功。推荐在脚本里内置verify_after_write逻辑:
def write_contact(ser, index, number, name): resp = send_at(f'AT+CPBW={index},"{number}",129,"{name}"') assert 'OK' in resp, f'write failed: {resp}' # 读回验证 read_resp = send_at(f'AT+CPBR={index}') assert number in read_resp, 'verify failed'如果读回比对不一致,优先怀疑字符集设置被模块重启后重置了。很多模块每次上电都会恢复默认字符集,所以每个会话开始都要重新设置AT+CSCS和AT+CMGF,不能只设一次就默认永久有效。
5. 把管家做成应用:REST接口、vCard备份与读卡器热插拔
当增删改查的原子操作稳定后,自然要封装成服务。常用的做法是让本机跑一个Python进程,通过串口或PC/SC连接读卡器,对外暴露REST接口,这样前端、移动端甚至充电桩设备都能直接调。
5.1 把增删改查映射成REST接口
用Flask搭一个极简服务,不引入太重ORM,因为存储介质是SIM卡,不是数据库。这里直接复用第2章的类,接口命名与数据库增删改查习惯保持一致:
from flask import Flask, request, jsonify app = Flask(__name__) contact_mgr = SimContactManager(ser) @app.route('/contacts', methods=['GET']) def list_contacts(): return jsonify(contact_mgr.read_all()) @app.route('/contacts', methods=['POST']) def add_contact(): data = request.json contact_mgr.add(data['number'], data['name']) return '', 201 @app.route('/contacts/<int:index>', methods=['PUT']) def update_contact(index): data = request.json contact_mgr.update(index, data['number'], data['name']) return '', 204 @app.route('/contacts/<int:index>', methods=['DELETE']) def delete_contact(index): contact_mgr.delete(index) return '', 204这里思维上可以类比MySQL的增删改查语句:GET对应SELECT,POST对应INSERT,PUT对应UPDATE,DELETE对应DELETE。但要注意SIM卡的索引不是自增主键,而是物理位置,不能依赖自增。REST接口里把路径上的index当作资源ID,但底层要先用AT+CPBR全表扫描确认当前索引,否则删除会误伤。
5.2 批量备份与恢复:vCard和短信eml格式
管家要有备份能力。联系人的通用交换格式是vCard 3.0,短信可以导出为.eml或自定义JSON。备份时逐条读取,拼接文件内容:
def export_vcard(contacts): with open('contacts.vcf', 'w', encoding='utf-8') as f: for idx, number, name in contacts: f.write('BEGIN:VCARD\n') f.write('VERSION:3.0\n') f.write(f'FN:{name}\n') f.write(f'TEL;TYPE=CELL:{number}\n') f.write('END:VCARD\n')恢复时反向解析vCard,调add接口写入。注意vCard文件里的号码可能包含+86前缀,而SIM卡本地可能不需要这个前缀,恢复前要做一次规范化:去掉空格、括号、将+86转换成0开头,否则会把号码存成国际格式导致拨打时出错。
5.3 一个可靠技巧:用轮询监控读卡器热插拔
很多读卡器和USB串口模块在物理拔插后设备句柄会失效,这时进程不能自杀,要重新初始化。在Linux下用pyserial监听/dev/ttyUSB*的生成/消失事件,但更稳的是每隔数秒主动探测一次。做法如下:
def wait_for_serial(): while True: try: s = serial.Serial('/dev/ttyUSB0', 115200, timeout=1) s.write(b'AT\r\n') if b'OK' in s.read(64): return s except serial.SerialException: time.sleep(3)不要用inotify监听串口目录,因为某些USB转串口驱动会在拔插时保留symlink短暂存在。轮询加AT探测能覆盖大部分模块,而且不会误判。对于PC/SC读卡器,则监听smartcard库的CardConnectionObserver事件,当卡片被移除时,后续transmit会抛异常,捕获后重新等待卡片插入。
最后一个技巧是给管家加“写后必读”和“会话保活”。SIM卡模块空闲时会进入省电模式,长时间不通信后第一条AT命令无效。每次对外接口被调用前,先发一个AT空探测,没回应就重新初始化串口并解锁PIN。这套做法能避免绝大多数因设备休眠导致的“接口超时”,也是把模拟增删改查真正变成可靠服务的最后一道工序。
本文还有配套的精品资源,点击获取