android逆向奇技淫巧二十七:AOSP改源码制作沙箱实现“无痕”hook和避开server端风控
搞逆向,hook是必备的基本技能之一,常见的hook工具有xpose、frida等。这些hook框架出来的时间都有好多年了,历史悠久,用的人也多,影响范围非常大,导致app开发商也会重点检测和防御。现在规模稍微大一点的开发商都会针对xpose、frida做各种防护,导致部分场景下使用xpose、frida可能失效,无法hook,这种情况下该怎么进一步hook和trace了?
还记得上一篇文章的分析么:既然基于android系统做开发,肯定要调用android提供的各种API,不可能啥都开发商自己实现,原因很简单:
- 自己实现耗费时间,推迟app上线周期,可能导致被竞争的app甩开日活、月活、收入等运营数据
- 自己实现耗费人力,只有个别大厂才能承担这些高额的成本
上一篇是用frida hook了操作系统底层的库和API,既然android是开源的,是不是能直接把hook的代码写死在操作系统层面了?这样hook就不再需要用到frida、xpose,上层的app也无从检测,岂不完美?
1、网络抓包
(1)基于charles/fidder/burpsuit等抓包软件。APP为了防止被抓包,使用的功能之一是只信任系统内置的证书(可以在“设置->安全和隐私->受信任的凭据”查看系统和用户证书),不信任用户安装的证书,然而逆向人员手动安装的抓包软件证书只能位于用户区,无法进入系统区,自然是无法骗过APP检测的,这可咋整了?
证书不是从天上掉下来的,也是操作系统厂家在出厂时内置的;既然操作系统厂家都可以内置,那么逆向人员拿到源码后不也可以内置证书么?其实很简单,把证书放在AOSP源码目录下的“system/ca-certificates/files”目录下即可!看下图第一个证书,就是我放置的charlse证书(我用的是kali,貌似没能识别出来charlse的类型......)!
在正式抓包时,charlse会冒充server给client发证书,一般情况下是能够骗过client的,但有些client比较鸡贼,做了双向认证,即client也要给server发送证书来让server验明正身!这种情况下如果还想用软件抓包,必须找到client的证书导入charlse。现在面临的第一个问题就是找到client的证书!既然client既然要发给server,第一件事肯定是从磁盘把证书读到内存,肯定需要用到File函数,所以hook File类大概率是能找到client证书的路径。这都不算啥,更鸡贼的是部分app还给client的证书设置了密码,光找到证书还不够,没有密码也没法导入charles安装,密码在哪找了?既然安装证书要密码,肯定有相应的API来验证,这个API就是aosp810r1/libcore/ojluni/src/main/java/java/security/KeyStore.java类中的load方法,第二个参数就是password,整个函数和hook代码如下:
public final void load(InputStream stream, char[] password) throws IOException, NoSuchAlgorithmException, CertificateException { if (password != null) { String inputPASSWORD = new String(password); Class logClass = null; try { logClass = this.getClass().getClassLoader().loadClass("android.util.Log"); } catch (ClassNotFoundException e) { e.printStackTrace(); } Method loge = null; try { loge = logClass.getMethod("e", String.class, String.class); } catch (NoSuchMethodException e) { e.printStackTrace(); } try { loge.invoke(null, "theseventhson KeyStoreLoad", "KeyStore load PASSWORD is => " + inputPASSWORD); Exception e = new Exception("theseventhson KeyStoreLoad"); e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } Date now = new Date(); String currentTime = String.valueOf(now.getTime()); FileOutputStream fos = new FileOutputStream("/sdcard/Download/" + inputPASSWORD + currentTime); byte[] b = new byte[1024]; int length; while ((length = stream.read(b)) > 0) { fos.write(b, 0, length); } fos.flush(); fos.close(); } keyStoreSpi.engineLoad(stream, password); initialized = true; }
一旦client的证书加密,通过这个方法就可以把证书的安装密码自吐了!证书安装密码有了,证书怎么dump得到了?先站在正向开发角度看看证书是怎么导出的(文章末尾参考链接8),核心代码如下:
// 合成p12证书 public static void storeP12(PrivateKey pri, String p7, String p12Path, String p12Password) throws Exception { CertificateFactory factory = CertificateFactory.getInstance("X509"); //初始化证书链 X509Certificate p7X509 = (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(p7.getBytes())); Certificate[] chain = new Certificate[]{p7X509}; // 生成一个空的p12证书 KeyStore ks = KeyStore.getInstance("PKCS12", "BC"); ks.load(null, null); // 将服务器返回的证书导入到p12中去 ks.setKeyEntry("client", pri, p12Password.toCharArray(), chain); // 加密保存p12证书 FileOutputStream fOut = new FileOutputStream(p12Path); ks.store(fOut, p12Password.toCharArray()); }
这份java代码既然能dump证书保存到磁盘,把这段代码加入AOSP源码岂不是也能达到同样的效果?所以同样在KeyStore.java类中可以在如下两个方法内加上dump证书的代码,如下:
public PrivateKey getPrivateKey(){ Date now = new Date(); String currentTime = String.valueOf(now.getTime()); String certPassword = "theseventhson"; String certPath = "/sdcard/Download/getPrivateKey"+currentTime+".p12"; X509Certificate p7X509 = (X509Certificate)chain[0]; Certificate[] myChain = new Certificate[]{p7X509}; KeyStore myKS = null; try { myKS = KeyStore.getInstance("PKCS12","BC"); } catch (KeyStoreException e) { e.printStackTrace(); } catch (NoSuchProviderException e) { e.printStackTrace(); } try { myKS.load(null,null); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (CertificateException e) { e.printStackTrace(); } try { myKS.setKeyEntry("client",privKey,certPassword.toCharArray(),myChain); } catch (KeyStoreException e) { e.printStackTrace(); } FileOutputStream output = null; try { output = new FileOutputStream(certPath); } catch (FileNotFoundException e) { e.printStackTrace(); } try { myKS.store(output,certPassword.toCharArray()); } catch (KeyStoreException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (CertificateException e) { e.printStackTrace(); } return privKey; } public Certificate getCertificate() { Date now = new Date(); String currentTime = String.valueOf(now.getTime()); String certPassword = "theseventhson"; String certPath = "/sdcard/Download/getCertificate"+currentTime+".p12"; X509Certificate p7X509 = (X509Certificate)chain[0]; Certificate[] myChain = new Certificate[]{p7X509}; KeyStore myKS = null; try { myKS = KeyStore.getInstance("PKCS12","BC"); } catch (KeyStoreException e) { e.printStackTrace(); } catch (NoSuchProviderException e) { e.printStackTrace(); } try { myKS.load(null,null); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (CertificateException e) { e.printStackTrace(); } try { myKS.setKeyEntry("client",privKey,certPassword.toCharArray(),myChain); } catch (KeyStoreException e) { e.printStackTrace(); } FileOutputStream output = null; try { output = new FileOutputStream(certPath); } catch (FileNotFoundException e) { e.printStackTrace(); } try { myKS.store(output,certPassword.toCharArray()); } catch (KeyStoreException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (CertificateException e) { e.printStackTrace(); } return chain[0]; }
(2)上层的app为了发送数据,肯定要调用android提供的接口,不太可能自己单独写(发送数据需要通过硬件网卡,涉及到系统调用了,自己写接口还要适配网卡驱动和考虑3环和内核的上下文切换,非常麻烦),上文hook了libc.so的send和sendto接口,其实还有很多java层的接口也可以试试(这些java层接口的native层调用的也是send/sendto,但java层更接近应用)。先看看最常见的SocketOutPutStream.java文件,在/aosp810r1/libcore/ojluni/src/main/java/java/net/下面,发送数据用的就是socketWrite函数,在这个函数hook不就能看到app发送的数据了么?hook的原理也很简单,直接把函数第一个参数打印出来即可!平时在应用层app打印日志,要么用system.out.println,要么log.i等函数,但是在这里打印就不同了:现在还在底层,system.out.println是用不了的,log类也没法直接用,需要拐个弯:用反射!通过反射的方式获取log类,然后调用log.i或其他方法在console下打印日志信息!参考的代码如下(可参考文章末尾4、5链接):
/** * Writes to the socket with appropriate locking of the * FileDescriptor. * @param b the data to be written * @param off the start offset in the data * @param len the number of bytes that are written * @exception IOException If an I/O error has occurred. */ private void socketWrite(byte b[], int off, int len) throws IOException { if (len <= 0 || off < 0 || len > b.length - off) { if (len == 0) { return; } throw new ArrayIndexOutOfBoundsException("len == " + len + " off == " + off + " buffer length == " + b.length); } FileDescriptor fd = impl.acquireFD(); try { BlockGuard.getThreadPolicy().onNetwork(); socketWrite0(fd, b, off, len); if(len>0){ byte[] input = new byte[len]; System.arraycopy(b,off,input,0,len); String inputString = new String(input); Class logClass = null; try { logClass = this.getClass().getClassLoader().loadClass("android.util.Log"); } catch (ClassNotFoundException e) { e.printStackTrace(); } Method loge = null; try { loge = logClass.getMethod("e",String.class,String.class); } catch (NoSuchMethodException e) { e.printStackTrace(); } try { loge.invoke(null,"theseventhson SOCKETrequest","Socket is => "+this.socket.toString()); loge.invoke(null,"theseventhson SOCKETrequest","buffer is => "+inputString); Exception e = new Exception("theseventhson SOCKETrequest"); e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } } catch (SocketException se) { if (se instanceof sun.net.ConnectionResetException) { impl.setConnectionResetPending(); se = new SocketException("Connection reset"); } if (impl.isClosedOrPending()) { throw new SocketException("Socket closed"); } else { throw se; } } finally { impl.releaseFD(); } }
同理:在SocketInputStream的socketRead方法能收到数据包,hook代码如下:
/** * Reads into an array of bytes at the specified offset using * the received socket primitive. * @param fd the FileDescriptor * @param b the buffer into which the data is read * @param off the start offset of the data * @param len the maximum number of bytes read * @param timeout the read timeout in ms * @return the actual number of bytes read, -1 is * returned when the end of the stream is reached. * @exception IOException If an I/O error has occurred. */ private int socketRead(FileDescriptor fd, byte b[], int off, int len, int timeout) throws IOException { int result = socketRead0(fd, b, off, len, timeout); if(result>0){ byte[] input = new byte[result]; System.arraycopy(b,off,input,0,result); String inputString = new String(input); Class logClass = null; try { logClass = this.getClass().getClassLoader().loadClass("android.util.Log"); } catch (ClassNotFoundException e) { e.printStackTrace(); } Method loge = null; try { loge = logClass.getMethod("e",String.class,String.class); } catch (NoSuchMethodException e) { e.printStackTrace(); } try { loge.invoke(null,"theseventhson SOCKETresponse","Socket is => "+this.socket.toString()); loge.invoke(null,"theseventhson SOCKETresponse","buffer is => "+inputString); Exception e = new Exception("theseventhson SOCKETresponse"); e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } return result; }
上述只是普通的数据收发接口,其实java层也有ssl的,核心功能都是在aosp810r1/external/conscrypt/common/src/main/java/org/conscrypt/SslWrapper.java中实现的,其中收发数据接口的hook代码如下:
// TODO(nathanmittler): Remove once after we switch to the engine socket. int read(FileDescriptor fd, byte[] buf, int offset, int len, int timeoutMillis) throws IOException { int result = NativeCrypto.SSL_read(ssl, fd, handshakeCallbacks, buf, offset, len, timeoutMillis) ; if(result>0){ byte[] input = new byte[result]; System.arraycopy(buf,offset,input,0,result); String inputString = new String(input); Class logClass = null; try { logClass = this.getClass().getClassLoader().loadClass("android.util.Log"); } catch (ClassNotFoundException e) { e.printStackTrace(); } Method loge = null; try { loge = logClass.getMethod("e",String.class,String.class); } catch (NoSuchMethodException e) { e.printStackTrace(); } try { loge.invoke(null,"theseventhson SOCKETresponse","SSL is =>"+this.handshakeCallbacks.toString()); loge.invoke(null,"theseventhson SOCKETresponse","buffer is => "+inputString); Exception e = new Exception("theseventhson SOCKETresponse"); e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } return result; } // TODO(nathanmittler): Remove once after we switch to the engine socket. void write(FileDescriptor fd, byte[] buf, int offset, int len, int timeoutMillis) throws IOException { if(len>0){ byte[] input = new byte[len]; System.arraycopy(buf,offset,input,0,len); String inputString = new String(input); Class logClass = null; try { logClass = this.getClass().getClassLoader().loadClass("android.util.Log"); } catch (ClassNotFoundException e) { e.printStackTrace(); } Method loge = null; try { loge = logClass.getMethod("e",String.class,String.class); } catch (NoSuchMethodException e) { e.printStackTrace(); } try { loge.invoke(null,"theseventhson SSLrequest","SSL is => "+this.handshakeCallbacks.toString()); loge.invoke(null,"theseventhson SSLrequest","buffer is => "+inputString); Exception e = new Exception("theseventhson SSLrequest"); e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } NativeCrypto.SSL_write(ssl, fd, handshakeCallbacks, buf, offset, len, timeoutMillis); }
大家有没有发现write函数有啥特殊之处了?最末尾还是调用了ssl_write函数,也就是我之前hook过的函数!
2、加密/sign方法自吐:x音对关键字段求sign值是在native层干的,但其实在java层有很多现成的API可以用来加密/sign数据!常见的类如下:
javax.crypto.Cipher;
javax.crypto.KeyGenerator;
javax.crypto.Mac;
javax.crypto.SecretKey;
javax.crypto.spec.SecretKeySpec;
java.security.MessageDigest;
java.security.SecureRandom;
x音这种大厂研发实例雄厚甚至过剩,不直接调用现成的接口,而是在开源算法基础上更改是可行的;但还有很多中小厂家,自己肯定是没有实力研发算法的,只能直接简单粗暴调用现成的接口,这就给了逆向人员可乘之机!老规矩,可以用objection批量hook这些类的方法,也可以在AOSP源码层面增加打印的代码,最常见的MD5算法,在aosp810r1/libcore/ojluni/src/main/java/java/security/MessageDigest.java类中,关键的几个函数:updata、digest等用同样的方式打印参数、调用栈和返回值!
3、app厂家打击黑灰产的重心已经转移到了后端的风控,不再是客户端的防护,原因很简单:客户端一旦发布出去,被技术大佬破解只是时间和成本问题。就算客户端的ollvm或vmp做的很好,大不了在客户端通过rpc生成关键的加密/sign字段,更加快速、简单和粗暴,纯靠客户端是防止不了黑灰产的,所以近些年很多大厂都把防护重心放到了后台:通过规则引擎、机器学习/深度学习等方法判断服务器收到的数据包是client正常发送的,还是黑灰产机器人的恶意流量!站在逆向角度,怎么才能最大程度地骗过后台的风控防护措施、让他们以为刷单、爬虫等都是正常的流量了?
如果client每次只发送和业务相关的数据,黑灰产也可以模拟client每次也发送相同的数据,后台是没法判断这个数据是来自正常app,还是黑灰产机器人的!但是如果app每次发送数据时都加上设备指纹了?如果后台短时间内收到大量来自同一设备的流量,那么发送这些数据的账号难道都在同一台设备上登陆的?这不是很明显有问题么?所以部分app在向后台server发送数据时会带上设备的指纹信息,后台可以判断当前设备有多少账号同时登陆和请求数据。比如x音这种短视频app,正常情况下一台终端设备只可能登陆一个账号操作向后台server请求数据。如果同一台设备同时登陆了多个账号向server请求数据,这些账号大概率都是黑灰产养的,肯定是要封号的!同样是站在逆向角度,怎么绕过后台这种检测了?
既然每次app发送数据都要带上设备指纹,那改设备指纹不就行了么? 最常用的一些设备信息收集的类在这:aosp810r1/frameworks/base/core/java/android/os/Build.java里面,有个方法直接得到设备指纹:
private static String deriveFingerprint() { String finger = SystemProperties.get("ro.build.fingerprint"); if (TextUtils.isEmpty(finger)) { finger = getString("ro.product.brand") + '/' + getString("ro.product.name") + '/' + getString("ro.product.device") + ':' + getString("ro.build.version.release") + '/' + getString("ro.build.id") + '/' + getString("ro.build.version.incremental") + ':' + getString("ro.build.type") + '/' + getString("ro.build.tags"); } return finger; }
设备指纹就是设备的品牌、名称、设备号、操作系统版本、类型等等信息简单平凑而成的,所以直接改这个函数的返回值finger就能达到更改设备指纹的目的;如果再精细一点,更改getString函数了,就能进一步更改各个关键的字段了(部分app可能不会直接调用deviceFingerprint函数,而是单独读取各个字段后用自己的算法生成设备指纹),所以改各个字段是最保险和稳当的了,比如下面这种:
private static String getString(String property) { String result = SystemProperties.get(property, UNKNOWN); if(property.equals("ro.product.brand")){ result = new String("theseventhsonBRAND"); }else if(property.equals("ro.product.manufacturer")){ result = new String("theseventhsonMANUFACTURER"); }else if(property.equals("ro.product.board")){ result = new String("theseventhsonBOARD"); }else if(property.equals("no.such.thing")){ result = new String("theseventhsonSERIAL"); } /*who invoked the fingerprint function*/ Exception e= new Exception("theseventhsonFINGERPRINT"); e.printStackTrace(); return result; }
除了Biuld.java,另一个类也很重要:aosp810r1/frameworks/base/telephony/java/android/telephony/TelephonyManager.java;从名字就能看出是“管理”手机的类!里面有3个重要的函数同样可以更改,如下:
public String getSubscriberId(int subId) { try { IPhoneSubInfo info = getSubscriberInfo(); if (info == null) return null; String result = info.getSubscriberIdForSubscriber(subId, mContext.getOpPackageName()); return "theseventhsonSubscriberIMESI"; } catch (RemoteException ex) { return null; } catch (NullPointerException ex) { // This could happen before phone restarts due to crashing return null; } } public String getDeviceId() { try { ITelephony telephony = getITelephony(); if (telephony == null) return null; String result = telephony.getDeviceId(mContext.getOpPackageName()); return "theseventhsonIMEI"; } catch (RemoteException ex) { return null; } catch (NullPointerException ex) { return null; } } public String getSimSerialNumber(int subId) { try { IPhoneSubInfo info = getSubscriberInfo(); if (info == null) return null; String result = info.getIccSerialNumberForSubscriber(subId, mContext.getOpPackageName()); return "theseventhsonSERIALNO"; } catch (RemoteException ex) { return null; } catch (NullPointerException ex) { // This could happen before phone restarts due to crashing return null; } }
上述函数都是java层的,众所周知java层编译后的smail代码都是虚拟机执行的,虚拟机最终还是调用了native层的C接口函数获取设备信息,其实最终的功能接口都来自libc/bionic/system_properties.c文件的函数,定义如下:
int __system_property_get(const char *name, char *value) { const prop_info *pi = __system_property_find(name); if(pi != 0) { return __system_property_read(pi, 0, value); } else { value[0] = 0; return 0; } }
本质就是个“KV结构”的数据,调用接口函数时输入设备属性名称,返回值存放在value!里面最关键的两个函数:一个是根据name找到prop_info结构体,另一个是根据该结构体返回属性的value值,实现的方法如下:
const prop_info *__system_property_find(const char *name) { prop_area *pa = __system_property_area__; unsigned count = pa->count; unsigned *toc = pa->toc; unsigned len = strlen(name); prop_info *pi; while(count--) { unsigned entry = *toc++; if(TOC_NAME_LEN(entry) != len) continue; pi = TOC_TO_INFO(pa, entry); if(memcmp(name, pi->name, len)) continue; return pi; } return 0; } int __system_property_read(const prop_info *pi, char *name, char *value) { unsigned serial, len; for(;;) { serial = pi->serial; while(SERIAL_DIRTY(serial)) { __futex_wait((volatile void *)&pi->serial, serial, 0); serial = pi->serial; } len = SERIAL_VALUE_LEN(serial); memcpy(value, pi->value, len + 1); if(serial == pi->serial) { if(name != 0) { strcpy(name, pi->name); } return len; } } }
总结:
(1)、其实改源码刷机和frida/xpose hook在目的和功能上是一样的,不同的是刷机可以防止被app检测,比frida 这种改变app源码的hook方式安全多了,可以认为是无痕hook!但既然是底层源码改变,所有app的数据都会被hook和打印,所以逆向时一般刷好的机都是针对单个app做测试,避免多个app同时打印日志干扰逆向人员分析!
(2)、第一次编译源码很费事,我花了3个多小时(下图)。后续每次编译应该是只编译更改的代码,耗时少了很多,一般10来分钟就能完成;
编译过程相当烧CPU,我给虚拟机分配了8 core全都100%用上了,内存也吃紧,建议至少16G,如下:
(3)关于证书绑定,网上有很多"高大上"的解释,基本都是照搬各种概念,一点都不接地气,我这里永通俗易懂的方式解释一下证书的核心功能:网络通信核心的诉求之一身份鉴别,比如和我通信的是是真正的李逵,而不是冒充的李鬼,怎么实现身份鉴别了?最传统的方式就是账号和密码,比如去银行取钱,要求输入卡的密码才能进入账号做各种操作,本质上是通过密码鉴别身份。远程网银操作时,为了账号安全,银行还会提供配套的U盾,这个U盾就是client的证书,和银行server通信时client会把证书发给server来证明自己就是李逵而不是李鬼,这个client证书的本质和用户设置的密码是一样的:就是通信双方提前商量好一段口令,后续通信时先验证口令,如果能对的上,说明是对的人!再举个更直白的例子:龙门飞甲都看过么?两伙人为了确认双方是不是一伙的、对方有没有被冒牌,事先想了个口令:“龙门飞甲,便知真假”,如果能对上,说明就是一伙的!这个思路在其他很多地方也用上了:比如防止CSRF攻击的token!server收到client的请求后会检查token,如果和自己发给client的不一致,或者甚至没token,直接拒绝提供数据!
回到app和server通信,为了防止被中间人抓包,app可以事先内置server公钥的MD5。通信时如果收到server公钥的MD5和事先内置的MD5对不上,说明这个公钥肯定有问题(如果是抓包,那么这个公钥大概率是抓包软件的,在这里冒充server)!java层绑定证书的核心代码如下:
String hostname = "example.com"; CertificatePinner certificatePinner = new CertificatePinner.Builder() .add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") .build(); OkHttpClient client = new OkHttpClient(); client.setCertificatePinner(certificatePinner); Request request = new Request.Builder() .url("https://" + hostname) .build(); client.newCall(request).execute();
通过CertificatePinner,只接受指定域名的证书,并且证书的MD5值也不能有任何偏差,否则拒绝连接的请求!
参考:
1、https://github.com/WalterInSH/risk-management-note 甲方常见风控技术汇总
2、https://www.cnblogs.com/tjp40922/p/15612710.html 利用frida修改android设备唯一标识符
3、https://www.anquanke.com/post/id/199898 android 源码编译指南
4、https://onejane.github.io/2021/05/06/frida%E6%B2%99%E7%AE%B1%E8%87%AA%E5%90%90%E5%AE%9E%E7%8E%B0/#%E6%B2%99%E7%AE%B1 沙箱
5、https://github.com/r0ysue/AndroidSecurityStudy
6、https://android.googlesource.com/platform/bionic/+/0d787c1fa18c6a1f29ef9840e28a68cf077be1de/libc/bionic/system_properties.c _system_property_get 函数源码
7、https://www.androidos.net.cn/doc/androidos/15134.html SEAndroid安全机制对Android属性访问的保护分析
8、https://codeantenna.com/a/cAEaT7dpgJ client证书导出代码