PKI数字证书在eSIM安全上的应用研究

李宏平 孟玉明 张立星 张傲思 杜琳美
1 联通华盛通信有限公司 北京 100032
2 全国海关信息中心 北京 100005

引言
现有实体SIM卡写卡有以下两种方式:一种是通过写卡器将卡数据写入SIM卡内,此种方式SIM卡与设备是物理接触,不需要进行在线认证就能保证将卡数据写入正确的卡内;另一种是空中写卡方式,运营商通过卡内预置的号码给卡发送数据短信,将卡数据传递到卡内,SIM卡与运营商之间通过预置号码的网络参数进行认证来保证卡数据写入正确的终端内。
eSIM以密码学技术为基础,使运营商签约数据在生产、存储和递送环节与硬件分离,以电子数据的形式存在,支持通过网络远程配置到终端。在这种情况下,eSIM终端出厂前无法确认需要支持的运营商;同时也存在一个终端上可存储多个电子卡数据的情况,这些电子卡数据可能来自不同的运营商,这就会导致终端和管理平台不知道对方各自的情况。
一般来说,网络上的实体间采用数字证书的方式来进行在线的相互认证。GSMA使用PKI(Public Key Infrastructure)公钥证书体系作为eSIM安全机制的基础[1]。eSIM平台RSP(Remote SIM Provisioning,远程SIM卡数据配置)业务数字证书由eSIM CA系统签发,可作为eSIM设备和服务器在RSP业务中的身份标识,实现设备认证、服务器认证以及信息传输的完整性和抗抵赖性。

1 数字证书与eSIM安全机制
1.1 eSIM技术架构
GSMA是eSIM标准的主要研究机构,从2011年至2016年先后发布了4个版本的eUICC标准,主要面向行业物联网市场。为了将eSIM的适用范围拓展至公众消费设备,GSMA又于2016年发布了远程SIM卡数据配置(RSP)标准。RSP采用的是客户端驱动模式,适用于可穿戴、平板电脑等消费类电子产品,针对用户自签约、自配置、自管理的特点进行了优化。
为了实现eSIM的业务需求,RSP技术标准定义了一套包含管理平台、终端、eUICC、CA以及相关配套设施的技术体系[2],如图1所示。

主要实体包括以下6种。
1)Profile:运营商向用户提供服务所需的卡数据和卡应用的集合,需要通过空中下载的方式安装到eUICC上。
2)eUICC:Profile的硬件载体,类似于传统USIM卡的UICC,但软硬件更复杂,可满足动态加载运营商数据的需要。同一张eUICC上可以加载属于不同运营商的多份Pro fi le,但同一时间只有一份能使用。
3)SM-DP+:负责生产、存储、提供Profile的网络服务平台。SM-DP+需具备必要的软硬件能力以确保Profile的安全。
4)终端:需要接入移动网络的实体。eUICC预置在终端中,终端也负责从SM-DP+下载Profile并写入eUICC。
5)DS:发现服务器,协助终端寻址SM-DP+。
6)CA:标准PKI证书权威机构,为体系内的通信各方颁发可信数字证书。

1.2 RSP标准的安全机制
eSIM采用基于公众网络的空中写卡技术,面临Profile数据泄露、Profile被窃取等网络威胁及安全风险。GSMA RSP标准

中提供了一系列安全机制和工具,用于防止Profile在下载、存储等环节的安全威胁[3]。RSP标准要求eSIM在生产、管理、安装、使用等任何环节的安全级别不低于传统可插拔SIM卡,体系中各实体需通过GSMA定义的安全认证。在技术层面,RSP标准也定义了多种安全机制来确保安全性[4],如表1所示。

2 eSIM数字证书管理
2.1 证书分类
eSIM系统中的PKI证书体系分为三个层次:根证书、卡商证书/平台证书、eUICC证书。其中根证书是自签名证书,是信任链的起始点。EUM证书(Embedded UICC Manufacturer)也叫卡商证书,由根证书签发,eUICC证书由EUM证书签发。服务器证书也由根证书签发。RSP业务中所有的证书均为X509格式证书。


2.2 证书链
RSP标准定义的证书链如图2所示。

2.3 证书签发

eSIM管理系统中各个实体的证书管理需要符合PKI架构要求。CI 根证书、卡商证书、平台证书(SM-DP+证书及 SM-DP+ TLS 证书)需要由运营商认可的CI签发,但是允许SM-DP+证书与SM-DP+ TLS 证书可以由不同的CI签发。eUICC证书由卡商使用二级根证书签发,eUICC需要认证eSIM下载服务器的DP+公钥证书。


2.4 证书系统实现
eSIM CA系统由离线CA、在线CA、RA、OCSP组成[5]。离线CA是一个单独的、可信任的根CA,它生成CI根证书,并通过CI根证书签发卡商EUM证书和服务器证书。在线CA即EUM,其核心功能就是签发和管理eUICC证书,EUM具有证书发放、证书更新、证书撤销和证书验证的功能。RA (Register Authority)是面向用户的审核受理机构,负责审核终端实体的相关信息,负责维护用户的信息。通过证书中的OCSP (Online Certificate Status Protocol)服务器地址,使用在线证书状态查询服务。


证书有效性验证包括三个方面内容:
1)验证证书中的签名,确认证书是由CA签发的以及证书的内容没有被篡改;
2)检验证书的有效期,确认该证书在有效期之内;
3)查询证书状态,确认该证书没有被注销。


在进行证书验证时,逐级验证签发者直至根证书。根证书的验证通过自身公钥来验证其签名信息,CA证书验证通过根证书的公钥来验证证书的签名,用户证书验证通过相应CA证书的公钥来验证证书的签名。在验证过程中,需要判断证书是否在有效期内。


2.5 证书生命周期
证书签发下来后状态默认为有效态;当证书处于不安全状态时,可以进行冻结操作,证书状态改为冻结态,冻结态证书可以通过解冻操作恢复为有效态;有效态证书和冻结态证书均可以做吊销操作,使证书改为作废态,吊销后的证书不能改变状态。


证书的整个生命周期如图3所示[6]。

CI签发的各种证书应设置有效期,超过有效期的证书应停止使用。eSIM下载服务器会检查eUICC证书、EUM证书的有效期,超过有效期的证书将被拒绝访问。


3 数字证书在eSIM平台上的应用
eSIM管理平台的主要功能是卡数据的生成、管理以及下发。在卡数据下发之前,管理平台与RSP终端需要进行相互认证之后,才能将卡数据下发到RSP终端中的eUICC卡内。有时,RSP终端内会预置多个不同CA签发的不同根的证书,同样,管理平台也会预置多个不同CA签发的不同根的证书,在用户使用RSP终端选择运营商办理好业务之后,RSP终端可以根据运营商发来的信息访问到管理平台,通过相互认证之后管理平台将卡数据下发到RSP终端上。


管理平台与RSP终端间的相互认证过程如下:
1)当RSP终端需要进行认证时,向管理平台发送认证请求,该认证请求包括RSP终端中eSIM支持的eSIM证书的标识;


2)管理平台接收到认证请求后,在管理平台查找与eSIM证书的标识一致的DP证书,并根据认证请求中的eSIM随机数和生成的DP随机数获得DP签名体,并对DP证书和DP签名体进行签名获得DP的签名;


3)管理平台将DP证书的标识、DP证书、DP签名体和DP签名一并发送给RSP终端;


4)RSP终端接收到上述数据后,用与DP证书标识一致的eSIM证书的公钥对所述DP证书进行验证,若验证失败,则RSP终端向管理平台向发送验证失败消息,如果验证通过,则RSP终端进一步用DP证书的公钥对DP签名进行解签,并将解签后的DP签名与DP签名体进行比对,若比对一致,则RSP终端将DP随机数和eSIM的信息一起打包生成eSIM签名体,并对该eSIM签名体进行签名获得eSIM签名;


5)RSP终端向管理平台发送eSIM证书、eSIM的EUM证书、eSIM签名体和eSIM签名;

6)管理平台根据DP证书对接收到的eSIM证书和EUM证书进行验证,若验证通过,则管理平台用接收到的eSIM证书的公钥对eSIM签名进行解签,并将解签后的eSIM签名与接收到的eSIM签名体进行比对,如果比对一致,则认证成功完成本次认证。


其中,进行签名的步骤可以由管理平台自身进行签名,也可以单独授权给独立的模块进行签名。例如单独设置加密机,管理平台将需要进行签名的数据发给加密机进行签名,并获得加密机返回的签名。如果RSP终端的eSIM支持多个eSIM证书,则针对每个eSIM证书都按照上述流程提供的认证方法进行认证,从而实现RSP终端的多运营商支持功能。


4 多证书管理现状及发展趋势
4.1 多证书
选择合适的证书服务是保障运营商数据安全的关键。eSIM证书服务既要符合GSMA标准,又需满足政府监管要求,目前国际上提供证书服务的主要两家:CyberTrust、Symantec。GSMA特别准许国内运营商自己选择CI,CA证书签发者必须是工业与信息化部公布的具有电子认证服务行政许可的认证机构,目前,三家运营商各自有CA,国内的电信终端产业协会(TAF)也是具备为eSIM平台及EUM颁发证书的CA。


RSP标准基于PKI的信任机制允许eUICC同时访问多个eSIM管理平台,理论上eUICC上只需要安装一张证书就可以,但这只是理想状态。例如美国Verizon和AT&T互不使用对方的证书。为了解决这个问题,RSP标准定义了多证书机制,eUICC中可以同时安装多张CA证书,按需使用。eUICC应能支持多个根证书,eSIM下载服务器也应能支持多个根证书,并能根据eUICC支持根证书的情况选择合适的证书。


4.2 GSMA统一根证书策略分析
为了实现全球互通,GSMA希望根证书统一由经过认证的CA签发,但为了满足各国政府监管需要,GSMA在SGP.28规范定义了两种CI:GSMA Root CI和Independent Root CI。


根据GSMA 规范要求,CI选择需要考虑的因素包括:
1)公司制度完善;
2)有能力对申请证书的企业的资质进行验证;
3)有能力保护整个生态体系;
4)积极参与eSIM发展;
5)能证明产品质量;
6)具备CI领域的知名度;
7) 产品覆盖广度(建议至少被8个运营商支持);
8)产品覆盖的区域(建议至少在2个以上区域使用);
9)具备长期服务的能力;
10)符合技术规范;
11)对各厂商中立无歧视;
12)签发证书的效率。

根据国内运营商发展现状,国内运营商提出针对GSMA的部分条件应当允许适当调整,如:为保证尊重运营商平等选择权利,只要有一家运营商推荐即可申请成为CI;对CI申请评估应该注重技术性因素,综合考虑市场规模因素,而不是运营商个数;覆盖多少国家、有多少运营商支持不应成为对CI的准入要求;签发证书的历史长短不应成为对CI的准入要求等条件。同时,为解决全球互通,减少eUICC需要安装的CA数量,降低DP+平台安装多个CA所需的成本,GSMA需要提供一个完整的CI解决方案,在该方案中应该体现如下原则:


1)多个CI组成CI包(CI Package) ,以实现对多个监管地区的覆盖;
2)对于每个监管地区,都需要有至少1个被该地区法律认可的CI在包中;
3)DP+需支持包中的所有被其所在地区认可的CI(不能选择性支持),eUICC则只需选择其中一个CI(需要支持跨区域时需每个区域选择一个CI);
4)建议GSMA在认证CI时一并解决CI的商务问题,DP+平台可以合理的价格获得并安装该监管地区内所有CI颁发的证书。


4.3 国内CA管理策略研究
由于国内运营商的eSIM平台均采用自建的技术路线,在证书服务选择上,终端厂商及卡商的话语权较弱,国家相关管理机构也未明确政策意见,因此目前仍由运营商自主选择为主。同时,中国三家运营商互不使用对方证书,这导致终端eUICC需预置多家证书,提升了终端复杂度,也不符合GSMA对统一根证书的管理原则。
国内CA管理需要一套互联互通方案,在设计时应考虑符合中国法律和监管要求、保证终端可以与各家运营商的DP/DP+互通、eUICC上只需安装一个CA就可以连接全部运营商的DP/DP+、需要具备商业上的公平性、合理性和可行性。基于此,我们提出如下三种可行方案。表2对三种方案做出了比较。

CI方案优点缺点
公益性单CI监管简单,只需要管理一家CI;
产业链成本低;
对各厂家相对公平
缺少商业驱动,服务质量不能保证;
对新技术新业务不能提供很好的支持
运营商互认多CIeUIIC侧 通过市场公平选择;
运营商背书保证CI服务质量;
对CI的管理与对运营商的管理合二为一
平台需支持多CI
市场化多CI通过市场公平选择;
通过竞争,CI服务质量较有保证
平台需要支持多CI,管理相对复杂;
实力差的企业可能因市场变化无法提供长期的技术支撑

4.3.1 公益性单CI方案
由监管部门指定一个CI作为统一CI,该CI需要是公益事业性质,由政府提供资金支持,不对运营商(平台证书,包括TLS证书)、eUICC厂商(EUM证书)收取费用。


4.3.2 运营商互认多CI方案
1)由监管部门组织统一CI入围招标,入围厂家数量可控制在3家左右。
2)平台证书免费。所有CI无歧视地向个运营商签发平台证书,所有运营商需要无歧视地支持各个CI的证书。
3)eUICC只需要安装一个CI的证书,由eUICC厂家自主选择用哪个CI,eUICC厂家与CI厂家自主进行商务谈判。


4.3.3 市场化多CI方案
1)由监管部门组织统一CI入围招标,入围厂家数量可控制在3~4家左右。
2)平台证书免费。所有CI无歧视地向个运营商签发平台证书,所有运营商需要无歧视地支持各个CI的证书。
3)eUICC只需要安装一个CI的证书(但不限制安装数量),由eUICC厂家自主选择用哪个CI,eUICC厂家与CI厂家自主进行商务谈判。

5 结语
eSIM是对移动通信技术的一次重大改进,使通信服务摆脱了SIM卡线下发卡环节的束缚,可实现全面互联网化的业务流程,让用户获得随需、即时入网的极致体验。终端商则得以摆脱卡槽束缚,优化产品外观并提升性能,如减小体积、增加电池续航、增强防水能力等,为广大消费者带来更多、更好的产品。


随着智能手表等可穿戴设备在国内的普及,eSIM技术在移动通信业务中扮演的角色越来越重要,其安全性问题深受产业链各方的关注。eSIM采用基于公众网络的空中写卡技术,为解决Pofile在传输过程中的安全性,采用了一套基于PKI的同根数字证书作为eSIM设备和服务器的身份标识。目前国内各运营商已确定使用自建的eSIM平台进行业务发展,对eSIM平台上的数字证书应用与管理必将引起更多重视,数字证书颁发机构之间的互相认可也将成为值得研究的新课题。

移远模块adb key生成算法

移远5G模块,开启adb, 需要输入一个key来开启

import crypt

#sn = '18700338'
#sn = '40901409'
sn = '12741851'


def generateUnlockKey(sn):
    """
    @param sn: the serial number to generate an unlock key for
    """
    salt = "$1${0}$".format(sn)
    c = crypt.crypt("SH_adb_quectel", salt)
    print("Salt: {0}\nCrypt: {1}\nCode: {2}\n".format(salt, c, c[12:27]))
    return c[12:27]
    
    
xx = generateUnlockKey(sn)    

print(xx)


def old_key(salt):
    code = crypt.crypt("SH_adb_quectel", "$1$" + salt)
    #code = crypt.crypt("SH_adb_quectel", "$1$" + salt + '$')
    code = code[12:27]
    
    return code
       
       
yy = old_key(sn)    

print('jiandan=', yy)     

全网通EC20使用电信的4G物联网卡

对于电信2020年后发行的普通SIM卡和物联网卡,是没有CDMA所需要的CSIM文件的。早期的EC20为了适配当时的电信UIM卡,是做了特殊处理的,启动后会搜寻SIM卡中的CSIM文件,没有这些文件就不会注册到网络。

解决方法:
(1) 读取nv 配置

发送AT命令给EC20模块

AT+QNVFR="/nv/item_files/modem/mmode/operator_name"

读出来的结果应该是 01
(2) 将该配置改为 00

AT+QNVFW="/nv/item_files/modem/mmode/operator_name",00

(3) 插入 电信4G物联网卡,断电重启EC20

说明:
operator_name = 00 表示 OPERATE_NULL, 选网时采用默认设计,无特定运营商
operator_name = 01 表示 OPERATE_CT, 中国电信,选网时是考虑CDMA兼容,SRLTE(CDMA+LTE双在网)等需求

在一款 2017年出厂的EC20上实测成功, 版本为 EC20CEFAGR06A05M4G

WiFi-Calling

Phone connects to Edge Packet Data Gateway (EPDG)
over WiFi
• Voice calls over WiFi
• Phone connects on low/no signal
• Also connects in Airplane mode + WiFi

Connection to EPDG uses IPsec
• Authenticates using Internet Key Exchange Protocol (IKEv2)

Internet Protocol Security
• Confidentiality, data integrity, access control, and data source
authentication
• Recovery from transmission errors: packet loss, packet replay, and
packet forgery
• Authentication
• Authentication Header (AH) – RFC 4302

• Confidentiality
• Encapsulating Security Payload (ESP) – RFC 4303
• Key management
• Internet Key Exchange v2 (IKEv2) – RFC7296
• Two modes
• Tunnel – used for connection to Gateway (EPDG)
• Transport

Internet Key Exchange (IKEv2)
• Initiates connection in two phases
• IKE_SA_INIT
• Negotiate cryptographic algorithms, exchange nonces, and do
a Diffie-Hellman exchange
• IKE_AUTH
• Authenticate the previous messages, exchange identities (e.g.
IMSI), and certificates, and establish the child Security
Association(s) (SA)
• IKE_AUTH uses EAP-AKA
• IMSI exchange not protected by a certificate
• Open to MitM attacks on identity (IMSI)

IPsec ESP keys are not compromised
• Call content still safe

xiaomi cdn

小米firmware下载的cdn 限速了。
据说是因为一帮搞 pcdn的人,为了让自己的上行流量占比 小一点, 故意刷下行流量。 其中小米firmware的下载地址是很好获取的,所以被狂刷了很多流量。
其实没什么用,现在运营商不是看占比, 而是上行流量超过多少G,就停宽带。
这帮人做一些损人不利己的缺德事:小米付出了不应该的流量费用,普通用户获得了糟糕的下载体验; 他们的宽带还是被运营商停了。

默认的地址是 bigota.d.miui.com, 改成

cdn-ota.azureedge.net
cdnorg.d.miui.com
bn.d.miui.com
bkt-sgp-miui-ota-update-alisgp.oss-ap-southeast-1.aliyuncs.com

有时候可以提高下载速率

https://bn.d.miui.com/V12.5.15.0.RGGEUXM/begonia_eea_global_images_V12.5.15.0.RGGEUXM_20220826.0000.00_11.0_eea_b8c6b15c15.tgz

https://cdn-ota.azureedge.net/V12.5.15.0.RGGEUXM/begonia_eea_global_images_V12.5.15.0.RGGEUXM_20220826.0000.00_11.0_eea_b8c6b15c15.tgz

https://cdn-ota.azureedge.net/V13.0.8.0.SJHCNXM/atom_images_V13.0.8.0.SJHCNXM_20230630.0000.00_12.0_cn_acca549dfc.tgz

wget -c      --referer="https://miui.com/"    https://hugeota.d.miui.com/V12.5.6.0.RGGCNXM/begonia_images_V12.5.6.0.RGGCNXM_20220602.0000.00_11.0_cn_fbed701a77.tgz

MTK之NVRAM实现数据的备份与恢复

2017年的时候有做过这样的一个定制化需求:写入一个文件数据,恢复出厂设置后该文件数据也要恢复。

第一步:nvram lib id定义
vendor/mediatek/proprietary/custom/project/cgen/inc/Custom_NvRam_LID.h

typedef enum
{
    AP_CFG_RDCL_FILE_AUDIO_LID=AP_CFG_CUSTOM_BEGIN_LID, //AP_CFG_CUSTOM_BEGIN_LID: this lid must not be changed, it is reserved for system.
    AP_CFG_RDCL_FILE_AUDIO_MAGI_CONFERENCE_LID,
    AP_CFG_RDCL_FILE_AUDIO_HAC_PARAM_LID,
    AP_CFG_CUSTOM_TEST_CUSTOM1_LID, //zrx add 定义lib id
    AP_CFG_CUSTOM_FILE_MAX_LID,
} CUSTOM_CFG_FILE_LID;

//zrx add 添加LID版本信息
#define AP_CFG_CUSTOM_TEST_CUSTOM1_LID_VERNO        "000"

第二步:nvram lib id的数据结构和版本号声明
vendor/mediatek/proprietary/custom/project/cgen/inc/Custom_NvRam_data_item.h

//zrx add
LID_BIT VER_LID(AP_CFG_CUSTOM_TEST_CUSTOM1_LID)
Test_Custom1_Struct *CFG_TEST_CUSTOM1_REC_TOTAL
{

};

第三步:nvram lib数据结构定义
vendor/mediatek/proprietary/custom/project/cgen/cfgfileinc/CFG_Custom1_File.h

//zrx add start
typedef struct
{
    unsigned char Array[1024];
}Test_Custom1_Struct;
//zrx add end

//zrx add start
#define CFG_TEST_CUSTOM1_REC_SIZE    sizeof(Test_Custom1_Struct)
#define CFG_TEST_CUSTOM1_REC_TOTAL   1
//zrx add end

第四步:nvram lib 默认值定义
vendor/mediatek/proprietary/custom/project/cgen/cfgdefault/CFG_Custom1_Default.h

Test_Custom1_Struct stCustom2Default =
{

    0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0
    
};

第五步:nvram lib内容加入nvram数组中
vendor/mediatek/proprietary/custom/m107/cgen/inc/CFG_file_info_custom.h

#include "../cfgfileinc/CFG_Custom1_File.h"
#include "../cfgdefault/CFG_Custom1_Default.h"

 const TCFG_FILE g_akCFG_File_Custom[]=
    {

    { "/data/nvram/APCFG/APRDCL/Test_Custom1",   VER(AP_CFG_CUSTOM_TEST_CUSTOM1_LID), CFG_TEST_CUSTOM1_REC_SIZE,
            CFG_TEST_CUSTOM1_REC_TOTAL, SIGNLE_DEFUALT_REC  ,    (char *)&stCustom2Default, DataReset , NULL
        },
    };

第六步:nvram lib id需要备份到BinRegion
vendor/mediatek/proprietary/external/nvram/libcustom_nvram/CFG_file_info.c

FileName aBackupToBinRegion[]=
{
  {"CUSTOM_TEST",AP_CFG_CUSTOM_TEST_CUSTOM1_LID},
}

pfConvertFunc aNvRamConvertFuncTable[]=
{
  NULL,//AP_CFG_CUSTOM_TEST_CUSTOM1_LID
}

const TABLE_FOR_SPECIAL_LID g_new_nvram_lid[] =
{
    { AP_CFG_CUSTOM_TEST_CUSTOM1_LID, 1024 * 1024, 1024 * 1024},
};

第七步:上层读写nvram数据接口
NvRAMAgent.java

package com.example.nvram_test;

import android.os.IBinder;

public interface NvRAMAgent extends android.os.IInterface
{
    /** Local-side IPC implementation stub class. */
    public static abstract class Stub extends android.os.Binder implements NvRAMAgent
    {
        private static final java.lang.String DESCRIPTOR = "NvRAMAgent";
        /** Construct the stub at attach it to the interface. */
        public Stub()
        {
            this.attachInterface(this, DESCRIPTOR);
        }
        /**
         * Cast an IBinder object into an NvRAMAgent interface,
         * generating a proxy if needed.
         */
        public static NvRAMAgent asInterface(android.os.IBinder obj)
        {
            if ((obj == null)) {
                return null;
            }

            android.os.IInterface iin = (android.os.IInterface) obj.queryLocalInterface(DESCRIPTOR);

            if (((iin != null) && (iin instanceof NvRAMAgent))) {
                return ((NvRAMAgent) iin);
            }

            return new NvRAMAgent.Stub.Proxy(obj);
        }
        public android.os.IBinder asBinder()
        {
            return this;
        }
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
        {
            switch (code)
            {
            case INTERFACE_TRANSACTION:
            {
                reply.writeString(DESCRIPTOR);
                return true;
            }
            case TRANSACTION_READFILE:
            {
                data.enforceInterface(DESCRIPTOR);
                int _arg0;
                _arg0 = data.readInt();
                byte[] _result = this.readFile(_arg0);
                reply.writeNoException();
                reply.writeByteArray(_result);
                return true;
            }
            case TRANSACTION_WRITEFILE:
            {
                data.enforceInterface(DESCRIPTOR);
                int _arg0;
                _arg0 = data.readInt();
                byte[] _arg1;
                _arg1 = data.createByteArray();
                int _result = this.writeFile(_arg0, _arg1);
                reply.writeNoException();
                reply.writeInt(_result);
                return true;
            }
            default:
            {
                break;
            }
            }

            return super.onTransact(code, data, reply, flags);
        }
        private static class Proxy implements NvRAMAgent
        {
            private android.os.IBinder mRemote;
            Proxy(android.os.IBinder remote)
            {
                mRemote = remote;
            }
            public android.os.IBinder asBinder()
            {
                return mRemote;
            }
            public java.lang.String getInterfaceDescriptor()
            {
                return DESCRIPTOR;
            }
            public byte[] readFile(int file_lid) throws android.os.RemoteException
            {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                byte[] _result;

                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeInt(file_lid);
                    mRemote.transact(Stub.TRANSACTION_READFILE, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.createByteArray();
                }
                finally {
                    _reply.recycle();
                    _data.recycle();
                }

                return _result;
            }
            public int writeFile(int file_lid, byte[] buff) throws android.os.RemoteException
            {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                int _result;

                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeInt(file_lid);
                    _data.writeByteArray(buff);
                    mRemote.transact(Stub.TRANSACTION_WRITEFILE, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readInt();
                }
                finally {
                    _reply.recycle();
                    _data.recycle();
                }

                return _result;
            }

            public byte[] readFileByName(String filename) throws android.os.RemoteException
            {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                byte[] _result;

                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeString(filename);
                    mRemote.transact(Stub.TRANSACTION_READFILEBYNAME, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.createByteArray();
                }
                finally {
                    _reply.recycle();
                    _data.recycle();
                }

                return _result;
            }

            public int writeFileByName(String filename, byte[] buff) throws android.os.RemoteException
            {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                int _result;

                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeString(filename);
                    _data.writeByteArray(buff);
                    mRemote.transact(Stub.TRANSACTION_WRITEFILEBYNAME, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readInt();
                }
                finally {
                    _reply.recycle();
                    _data.recycle();
                }

                return _result;
            }
        }
        static final int TRANSACTION_READFILE = (IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_WRITEFILE = (IBinder.FIRST_CALL_TRANSACTION + 1);
        static final int TRANSACTION_READFILEBYNAME = (IBinder.FIRST_CALL_TRANSACTION + 2);
        static final int TRANSACTION_WRITEFILEBYNAME = (IBinder.FIRST_CALL_TRANSACTION + 3);
    }
    public byte[] readFile(int file_lid) throws android.os.RemoteException;
    public int writeFile(int file_lid, byte[] buff) throws android.os.RemoteException;
    public byte[] readFileByName(String filepath) throws android.os.RemoteException;
    public int writeFileByName(String filepath, byte[] buff) throws android.os.RemoteException;
}

第八步:读写数据帮助类
Utils.java

ackage com.example.nvram_test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.ArrayList;

import android.content.Context;
import android.os.IBinder;
import android.os.ServiceManager;
import android.util.Log;
import android.widget.Toast;

public class Utils {
    final String TAG = "Utis";
    byte[] buff;
    Context mContext;
    public Utils(Context mContext){
        this.mContext=mContext;
    }   

    /**
     * 多个字节数组合并为一个字节数组
     * @param a 字节数组
     * @param b 字节数组
     * @return byte[]
     */
    public byte[] combineBytes(byte[] a, byte[] b) {
        byte[] bytes = new byte[a.length + b.length];
        System.arraycopy(a, 0, bytes, 0, a.length);
        System.arraycopy(b, 0, bytes, a.length, b.length);
        return bytes;
    }   

    /**
     * 写文件到app下
     */
    public void writeOwnFile(String fileName,String message){
        try {
            FileOutputStream fout = mContext.openFileOutput(fileName,
                    mContext.MODE_PRIVATE);
            byte[] bytes = message.getBytes();
            fout.write(bytes);
            fout.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 从app下读文件
     * @param fileName
     * @return
     */
    public String readOwnFile(String fileName){
        try {
            FileInputStream fis=mContext.openFileInput(fileName);
            ByteArrayOutputStream bos=new ByteArrayOutputStream();
            byte[] buffer=new byte[1024];
            int len=0;
            while((len=fis.read(buffer))!=-1){
                bos.write(buff, 0, len);
            }
            byte[] content_byte = bos.toByteArray();  
            String content = new String(content_byte);  
            return content;
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 写文件
     * @param fileName 文件名
     * @param bytes 字节数组
     */
    public void writeFile(String fileName,byte[] bytes){
         try {              
                FileOutputStream fout = new FileOutputStream(fileName);            
                fout.write(bytes);
                fout.close();               
            } catch (Exception e) {
                e.printStackTrace();
            }
    }

    /**
     * 读文件
     * @param fileName 文件名
     * @return byte[]
     */
    public byte[] readFile(String fileName){
        try {
            FileInputStream fin = new FileInputStream(fileName);
            int length = fin.available();
            byte [] buffer = new byte[length];
            fin.read(buffer); 
            fin.close();
            return buffer;
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
        return buff;
    }

    /**
     * 写值到NvRam
     * @param file_name NvRam路径
     * @param bytes 字节数组
     */
    public void writeData(String file_name, byte[] bytes) {
        byte[] buff=new byte[1024];
        if(bytes.length 0) {
                Toast.makeText(mContext, "write Success", Toast.LENGTH_SHORT).show();
            //  Log.d(TAG, "zrx----write Success");
            } else {
                Toast.makeText(mContext, "write Failure", Toast.LENGTH_SHORT).show();
                Log.d(TAG, "zrx---- write Failure");
            }
        }
    }

    /**
     * 从NvRam读值
     * @param file_name NvRam路径
     * @param offset  开始位置
     * @param byteCount 字节数
     * @return
     */
    public byte[] readData(String file_name,int offset,int byteCount) {
        byte[] data=new byte[byteCount];
        IBinder binder = ServiceManager.getService("NvRAMAgent");
        NvRAMAgent agent = NvRAMAgent.Stub.asInterface(binder);
        if (agent != null) {
            try {
                buff = agent.readFileByName(file_name);
            } catch (Exception e) {
                // TODO: handle exception
                e.printStackTrace();
            }
        }
        if (buff != null) {
            try {
                for(int i=0;i

第九步:验证
MainActivity.java

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;

import com.example.nvram_test.R;

public class MainActivity extends Activity{
    String TAG="MainActivity";

    /**
     * 文件名在vendor/mediatek/proprietary/custom/m107/cgen/inc/CFG_file_info_custom.h中有定义
     */
    private static final String TEST_FILENAME = "/data/nvram/APCFG/APRDCL/Test_Custom1";
    Button btn1;
    Button btn2;
    Button btn3;
    Button btn4;
    Utils utils;
    /**
     * 测试备份SecretKey文件的路径
     */
    private static final String secret_filename="/storage/emulated/0/SecretKey";

    /**
     * 测试备份goc_database文件的路径
     */
    private static final String goc_database="/storage/emulated/0/goc_database";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        utils=new Utils(this);
        btn1=(Button)findViewById(R.id.btn1);
        btn2=(Button)findViewById(R.id.btn2);

        btn3=(Button)findViewById(R.id.btn3);
        btn4=(Button)findViewById(R.id.btn4);

        /**
         * 测试把SecretKey文件的字节备份到NvRam下
         */
        btn1.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View arg0) {
                // TODO Auto-generated method stub
                byte[] a=utils.readFile(secret_filename); //读SecretKey文件
                utils.writeData(TEST_FILENAME, a); //把SecretKey文件的字节备份到NvRam下

                //byte[] b=utils.readData(TEST_FILENAME, 0, a.length);
            /*    if(!Arrays.equals(a, b)){
                    utils.writeData(TEST_FILENAME, a);
                }else{
                    Toast.makeText(MainActivity.this, "SecretKey Data has existed, no need to backup!", Toast.LENGTH_SHORT).show();
                }*/
            }
        });

        /**
         * 测试从NvRAM读SecretKey文件的字节
         */
        btn2.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View arg0) {
                // TODO Auto-generated method stub
                byte[] bytes=utils.readData(TEST_FILENAME,0,27);//测试从NvRAM读SecretKey文件的字节
                Toast.makeText(getApplicationContext(), "data = "+new String(bytes), Toast.LENGTH_LONG).show();
            }
        });

        /**
         * 测试写goc_database文件的字节到NvRAM
         */
        btn3.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View arg0) {
                // TODO Auto-generated method stub
                byte[] bytes=utils.readFile(goc_database); //goc_database文件的字节
                byte[] a=utils.readData(TEST_FILENAME,0,27); //读NvRAM下SecretKey文件的字节
                utils.writeData(TEST_FILENAME, utils.combineBytes(a, bytes));           
            }
        });

        /**
         * 测试写到NvRAM下的goc_database文件字节是否与原文件goc_database是否一样。
         */
        btn4.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View arg0) {
                // TODO Auto-generated method stub              
                byte[] bytes=utils.readFile(goc_database); //读goc_database文件
                byte[] b=utils.readData(TEST_FILENAME, 27, bytes.length); //读NvRAM下的goc_database字节
                utils.writeFile("/storage/emulated/0/goc_database2", b);    //把NvRAM下的goc_database字节写到goc_database2文件,  goc_database2文件与原文件goc_database比对,发现是相同的。                   
            }
        });
    }


}

JDRead1

硬件

CPU:  Onyx i.MX 6SoloLite
[ro.hardware]: [freescale]
ro.product.manufacturer]: [Onyx]
[ro.build.fingerprint]: [Onyx/JDRead/JDRead:4.4.2/2018-06-29_16-28_r3_a9ff2db/371:user/dev-keys]

开启USB调试
(0)联网
开启JDRead1,连接WiFi

(1)设置密码为0423
设置-设备名称-密码设置:密码输入0423,手机号输入自己的手机号。
为什么设置密码为 0423, 请看 https://www.senventise.com/posts/jdread1-exploit/

(2)开启开发者权限
设置-设备名称-设备信息:连续点击系统版本,直到显示“已开启开发者权限”。

https://www.cnblogs.com/arbo/p/14619813.html

用kingroot进行root
jdread1小白教程及软件v2.2.zip

提醒: 不要替换 services.jar

Kindle Paperwhite 3

型号: DP75SDI
属于 Kindel的 第七代 产品

2015 年 6 月 17 日,亚马逊上架了全新 Kindle Paperwhite(KPW3)电子书阅读器。像素 300 ppi, RAM增大了一倍,也就是说运行速度也会有较大提升,但Storage依然是 4GB。 这款新机在亚马逊中国上的预订价格为 958 元,2015 年 6 月 30 日正式发货。

Jailbreak
参考资料:
https://www.mobileread.com/forums/showthread.php?t=225030
https://bookfere.com/post/892.html
https://bookfere.com/post/311.html
https://bookfere.com/post/59.html

设备信息
序列号 G090 G1
固件版本 5.8.11
硬件 Hardware : Freescale i.MX 6SoloLite based Wario Board

步骤:
1. 通过序列号 确定型号
(G090G1 Kindle PaperWhite 3 (2015) WiFi PW3 型号)
可以root / jailbreak

2. 充满电

3. 关掉密码 , 关掉家长控制

4. 开启 飞行模式,关闭3G/WiFi


更新到 5.12.3 固件 https://s3.amazonaws.com/firmwaredownloads/update_kindle_all_new_paperwhite_5.12.3.bin

固件文件名:update_kindle_all_new_paperwhite_5.12.3.bin
MD5 校验码:e4d7ff60132c58448b0c1b31d5961b12
SHA1 校验码:ff8cf3a93ce37f6adf7c12b5166f73dc0ba929fe

6. 下载 kindle-jb-kindlebreak-1.0-r18327.tar.xz

下载 jb-kindlebreak.zip 并解压,得到下列四个文件,把它们全部拷贝到 Kindle 根目录(所谓根目录,就是那个目录下有一个叫documents的子目录)

kindlebreak.jxr
kindlebreak.html
jb.sh
jb

接着点击 Kindle 右上角的菜单按钮,进入“体验版网页浏览器(Experimental Browser)”。

请务必确保“浏览器设置”中的图片设置处于启用状态,也就是显示“禁用图片(Disable Images)”字样。如显示的是“启用图片(Enable Images)”字样,你需要点击启用,以确保浏览器能够正常解析图片。

最后输入如下所示的文件地址(注意 file:/// 是一个冒号三个斜杠):

file:///mnt/us/kindlebreak.html

如果一切正常,Kindle 会在数秒至数分钟后自动重启(时长取决于不同设备)。重启完毕越狱便告成功。之前放进去的文件会被自动清理,你会在 Kindle 根目录看到一个日志文件 kindlebreak_log.txt。

7. 安装热修复补丁 JailBreak-1.16.N-FW-5.x-hotfix.zip

Jailbreak Hotfix 的具体安装步骤如下:

(1)用 USB 数据线把 Kindle 连接到电脑,直到出现 Kindle 盘符;
(2)解压缩下载到的 ZIP 压缩包 JailBreak-x.xx.N-FW-5.x-hotfix.zip,得到一个名为 Update_jailbreak_hotfix_x.xx.N_install.bin 的文件;
(3)将此 bin 文件拷贝到 Kindle 磁盘根目录,然后从电脑弹出 Kindle;
(4) 依次在 Kindle 中点击【菜单 → 设置 → 菜单 → 更新您的 Kindle】,等待重启;

8. 安装 MobileRead Package Installer (MRPI)

(1)用 USB 数据线将 Kindle 连接到电脑上,直到出现 Kindle 磁盘;
(2) 解压缩下载到的 kual-mrinstaller-1.7.N-xxx.tar.xz 得到一个文件夹;
(3)把文件夹内的 extensions 和 mrpackages 拷贝到 Kindle 的根目录。
(4)从电脑上断开 Kindle磁盘,重启Kindel

注意,如果根目录已有 extensions 这个文件夹,可以只把解压得到的 extensions 文件夹中的内容拷贝到 Kindle 根目录原有的 extensions 文件夹内,以避免原文件夹内的其它文件被删除。

9. 安装 KUAL ( Kindle Unified Application Launcher)
(1) 用 USB 数据线将 Kindle 连接到电脑上,直到出现 Kindle 磁盘;
(2) 如果没有安装MRPI, 先按照第8步的方法安装 MobileRead Package Installer (MRPI);
(3) 然后解压缩下载到的 KUAL-v2.x.xx-xxxxxxxx-20xxxxxx.tar.xz 得到一个文件夹;
在文件夹中找到 Update_KUALBooklet_v2.x.xx_install.bin 文件,拷贝到 Kindle 根目录下的 mrpackages 文件夹,然后在 Kindle 搜索框中输入 ;log mrpi 点击回车;
这时会调用 mrpi 安装 KUAL,安装完成并等待 Kindle 重启完毕后即可使用 KUAL。

10. 安装 Koreader

首先确保安装了 MRPI 和 KUAL;
用 USB 数据线将 Kindle 连接到电脑上,直到出现 Kindle 磁盘;
解压缩下载到的 Koreader 压缩包,可得到 extensions 和 koreader 两个文件夹;
先把文件夹 extensions 中的内容拷贝到 Kindle 根目录下的 extensions 文件夹中;
然后把文件夹内的 koreader 文件夹拷贝到 kindle 根目录下;
通过 KUAL 菜单中启动 Koreader 并用它的文件浏览器打开并阅读电子书。

11. 安装 USBNetwork

(1)首先确保安装了 MRPI 和 KUAL;
(2)用 USB 数据线将 Kindle 连接到电脑上,直到出现 Kindle 磁盘;
(3) 解压缩下载到的 kindle-usbnet-0.xx.N-rxxxxx.tar.xz 压缩包,得到一个文件夹;
(4) 把文件夹内的 Update_usbnet_0.xx.N_install_pw2_and_up.bin 拷贝到 Kindle 里 mrpackages 文件夹中;
(5)弹出 Kindle 磁盘,进入 Kindle 界面,打开 KUAL,依次点击菜单【Helper → Install MR Packages】(或在搜索栏输入 ;log mrpi 并回车);
耐心等待 USBNetwork 安装,直到安装完成后 Kindle 重启完毕;

配置 USBNetwork
修改配置文件

重启完成后,可以在 Kindle 根目录可以看到 usbnet 文件夹。首先将此文件夹中的文件 DISABLED_auto 重命名为 auto。然后在此文件夹里的 etc 文件夹中找到配置文件 config,并用代码编辑器(如 Sublime Text)将其打开。找到 USE_WIFI 和 USE_WIFI_SSHD_ONLY 两个配置项,将两者的值从默认的 false 修改为 true(注意,两者都要改成 true),保存并关闭。

USE_WIFI="true"
USE_WIFI_SSHD_ONLY="true"

如果不将USE_WIFI_SSHD_ONLY 改为true, 会导致 将kindel通过usb插入电脑时, 不识别 Kindle 磁盘

创建密钥

ssh-keygen -t rsa -f ~/Desktop/KindleKey

会在你的桌面上会出现 KindleKey 和 KindleKey.pub 两个文件,把其中的 KindleKey.pub 重命名为 authorized_keys,并拷贝到 usbnet 插件目录中的 etc 文件夹中。

ssh登陆到kindle

ssh -i KindleKey root@192.168.xxx.xxx

在 Kindle 搜索框中输入 ;711 可以看到 Kindel分配到IP地址