SIM卡管家实战:AT命令与APDU操作联系人和短信全攻略

发布时间:2026/9/14 2:15:11
SIM卡管家实战:AT命令与APDU操作联系人和短信全攻略
简介这是一份面向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, baudrate115200, timeout1, rtsctsFalse, dsrdtrFalse ) ser.write(bAT\r\n) print(ser.read(64).decode(ascii, errorsreplace))打开串口后先发一个AT模块返回OK就说明链路通了。需要注意SIM模块上电后可能处于PIN锁状态此时需要先发ATCPIN?确认返回CME ERROR: SIM PIN required时用ATCPIN1234解锁。这里用\r\n结尾是因为AT命令规范要求每行以回车换行结束。2.2 联系人的增删改查ATCPBR / CPBW / CPBF / CPBDSIM卡电话簿的AT命令分为读、写、查找、删除四个方向对应关系如下操作AT命令说明查询全部联系人ATCPBR1,250一次读取存储区条目1到250新增联系人ATCPBWindex,13512345678,129,张三129表示国际号码格式修改联系人ATCPBWindex,13900000000,129,李四写入已有索引即覆盖删除联系人ATCPBWindex只给索引不填号码即删除按号码查找ATCPBF135返回所有含135的号码实际操作时先读一下当前存储列表ATCPBR1,20返回结果可能是CPBR: 1,13512345678,129,张三 CPBR: 4,13900000000,129,李四 CPBR: 10,13800001111,129,王五 OK实现增删改查时新增和修改共用ATCPBW区别在于是否提供索引。不指定索引时模块自动找空位指定已有索引时覆盖数据只给索引不给号码时删除该条记录。删除必须跟原始索引所以先用ATCPBR拉全量再定位索引。2.2.1 用Python封装联系人操作class SimContactManager: def __init__(self, ser): self.ser ser self.ser.reset_input_buffer() def _send_at(self, cmd, waitOK): 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, errorsignore): break return buf.decode(ascii, errorsreplace) def read_all(self): resp self._send_at(ATCPBR1,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(fATCPBW{number},129,{name}) def update(self, index, number, name): return self._send_at(fATCPBW{index},{number},129,{name}) def delete(self, index): return self._send_at(fATCPBW{index})代码核心是给模块发送带参数的ATCPBW命令解析时需要注意返回行可能被串口缓冲拆分。split(,, 3)把前三个逗号分开保证姓名即使包含逗号也不会被截断。rsplit(, 1)[0]是为了从形如CPBR: 1,135...,129,张三的字符串中干净取出姓名。2.3 短信的增删改查ATCMGR / CMGS / CMGD / CMGW短信在SIM卡中也有专门存储区AT命令支持读、写发未发、发送、删除。先设置短信格式再操作ATCMGF1 # 切到text模式 ATCSMP17,167,0,0 # 设置tp参数0表示短信未发送常用命令操作AT命令说明读短信ATCMGRindex按索引读取发短信ATCMGS139...随后输入内容并以0x1A结束存草稿ATCMGW139...写入SIM卡不发送返回索引删除短信ATCMGDindex删除单条列出所有短信ATCMGLALL按状态过滤短信写完一定要用十六进制0x1A即CtrlZ提交不能用\r\n直接提交。这是最常见的失败点。2.3.1 完整读取一条短信def read_sms(index): ser.write(fATCMGR{index}\r\n.encode()) buf b while True: buf ser.read(128) if bOK in buf: break text buf.decode(utf-8, errorsreplace) # 解析 CMGR: REC READ,139...,,23-05-12 10:20 # 正文在下一行 lines text.splitlines() payload lines[1] if len(lines) 1 else return payloadATCMGR返回的正文可能因为中文编码问题变成UCS2字符后面第4章单独说这个坑。如果是纯英文短信text模式下直接可读。写入短信时用CMGW比CMGS更稳妥因为CMGW只存不发可以反复验证格式正确后再让模块真正发送。2.4 这三个必调参数和两个典型误用第一个参数是ATCSMP它控制短信的TP-Status、TP-Message-Reference等默认值在各厂商模块间不一致不设置可能导致短信存储后状态异常。建议统一设成17,167,0,0。第二个参数是ATCSCS设置字符集。想正确存中文姓名先发ATCSCSUCS2然后号码及姓名内容也要转成UCS2十六进制字符串。第三个参数是ATCPMS选择存储介质ATCPMSSM,SM,SM分别指读取来源、写入目标、接收目标这里全部指定为SIM卡。许多模块默认第一参数是ME模块内存不设这个命令你存的短信可能根本没写进SIM卡。常见误用是把ATCMGD1当作删除索引1实际有些固件要求先发ATCMGR读到索引再删。另一个误用是认为ATCPBR1,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十六进制文件名内容0x6F3AEF_ADN固话/手机号电话簿每条记录长度通常为28或34字节0x6F3CEF_SMS短信记录每条约176字节0x6F3BEF_SMSR短信状态报告0x6F40EF_SMSP短信参数访问这些文件要先选择MF和对应DF具体路径3F00MF→7F10DF_TELECOM→6F3A。通过读卡器发APDU时选择命令格式为00 A4 04 04 02 7F10前两个字节是CLA和INSA4表示SELECT04 04表示按文件ID选择后面跟上根目录到目标DF的路径。选择完后才能执行READ BINARY或UPDATE BINARY。3.2 选EFADN、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字节为关联IDFF表示无关联号码第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或写入空数据。为了不破坏文件结构常见做法是向该记录位置写入全零数据再把状态置为00zero_payload [0x00] * 176 connection.transmit([0x00, 0xDC, 0x00, 0x00, 0xB0] zero_payload)这里0xDC是UPDATE BINARY0xB0是长度。需要注意不同USIM格式的记录长度可能不同不能盲目写176字节必须先读FCP获取实际大小。4. 实战排错为什么读出来的中文短信是乱码电话簿无法写入不管用AT命令还是APDUSIM卡管家的开发一定会遇到两类高频故障中文编码不对以及文件写入被权限拦截。这一章把排错思路和验证方法串起来。4.1 中文编码UCS2 vs PDU vs GSM 7bitAT命令的text模式下默认字符集可能是GSM 7bit这种编码根本无法表达中文。所以先发ATCSCSUCS2然后号码和姓名都要转换成十六进制UCS2。举例姓名“张三”转成十六进制5F205B50那么新增命令变成ATCPBW1,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, errorsreplace)如果返回内容以0891开头则短信是PDU模式。PDU比text模式复杂它把SMSC信息、报头、编码方案和正文全部打包。排查时先看模块返回的开头几个字节0891是SMSC地址长度地址后面跟随TPDU。遇到PDU直接切换ATCMGF0再处理和解析不要试图在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条件与PINSIM卡文件系统从设计上就分AT直通权限和CHV权限。ADN和SMS文件通常允许PIN解锁后读写但不同运营商的卡可能把UPDATE条件设为ADM管理员普通AT命令无法改写。遇到这种卡ATCPBW会返回CME ERROR: operation not allowed。此时只能通过PC/SC发APDU先验证PIN00 20 00 01 08 31 32 33 34 35 36 37 3800 20是VERIFY命令00 01指CHV1后面8字节是BCD编码的PIN。验证通过后再次尝试UPDATE。如果仍被拒绝多半是文件条件被运营商写死为NEV永不验证ATCPBR能读不能写这种情况下任何常规手段都改不了只能把卡换掉。4.4 用日志和交叉验证锁定问题根源排查时我一般分三层记录应用层的参数、AT模块的原始返回、以及APDU层面的SW状态码。单独看AT返回OK只能说明模块收到命令不代表SIM卡真的写入成功。要验证是否真写进去了干一件事写完后立刻读回同一索引比对字段是否一致。删除也同理删完读回必须收到“记录不存在”才算成功。推荐在脚本里内置verify_after_write逻辑def write_contact(ser, index, number, name): resp send_at(fATCPBW{index},{number},129,{name}) assert OK in resp, fwrite failed: {resp} # 读回验证 read_resp send_at(fATCPBR{index}) assert number in read_resp, verify failed如果读回比对不一致优先怀疑字符集设置被模块重启后重置了。很多模块每次上电都会恢复默认字符集所以每个会话开始都要重新设置ATCSCS和ATCMGF不能只设一次就默认永久有效。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对应SELECTPOST对应INSERTPUT对应UPDATEDELETE对应DELETE。但要注意SIM卡的索引不是自增主键而是物理位置不能依赖自增。REST接口里把路径上的index当作资源ID但底层要先用ATCPBR全表扫描确认当前索引否则删除会误伤。5.2 批量备份与恢复vCard和短信eml格式管家要有备份能力。联系人的通用交换格式是vCard 3.0短信可以导出为.eml或自定义JSON。备份时逐条读取拼接文件内容def export_vcard(contacts): with open(contacts.vcf, w, encodingutf-8) as f: for idx, number, name in contacts: f.write(BEGIN:VCARD\n) f.write(VERSION:3.0\n) f.write(fFN:{name}\n) f.write(fTEL;TYPECELL:{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, timeout1) s.write(bAT\r\n) if bOK 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。这套做法能避免绝大多数因设备休眠导致的“接口超时”也是把模拟增删改查真正变成可靠服务的最后一道工序。本文还有配套的精品资源点击获取