Android應用的驗簽過程分析

0x01 回顧

分析驗簽過程之前,有必要先回顧一下Android應用的簽名過程:

  1. 對APK包中的每個文件做一次運算(Hash+Base64編碼)闪檬,將結(jié)果保存到META-INF/MANIFEST.MF文件中谤牡;
  2. 對MANIFEST.MF整個文件做一次運算(Hash+Base64編碼)壶谒,將結(jié)果保存到META-INF/CERT.SF文件的頭屬性中橱脸,再對MANIFEST.MF文件中的各個屬性塊做同樣的運算(Hash+Base64編碼)葫男,存放到CERT.SF的屬性塊中抱冷。
  3. 開發(fā)者用自己的私鑰對CERT.SF進行簽名,并將簽名信息和包含公鑰信息的數(shù)字證書一同保存到META-INF/CERT.RSA文件中梢褐。

因此旺遮,應用的驗簽過程其實也是圍繞這三步來進行的。

0x02 相關(guān)源碼的位置(AOSP 5.0.1_r1)

  • frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
  • frameworks/base/core/java/android/content/pm/PackageParser.java
  • libcore/luni/src/main/java/java/util/jar/StrictJarFile.java
  • libcore/luni/src/main/java/java/util/jar/JarVerifier.java
  • libcore/luni/src/main/java/java/util/jar/JarFile.java
  • libcore/luni/src/main/java/org/apache/harmony/security/utils/JarUtils.java

0x03 源碼分析

APK的安裝過程主要是由PackageManagerService這個核心服務類來完成的盈咳,所以我們可以從這個類入手耿眉,其中開始執(zhí)行簽名校驗的在installPackageLI方法里,代碼如下:

private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {
        ......
        PackageParser pp = new PackageParser();
        ......
       try {
            pp.collectCertificates(pkg, parseFlags);
            pp.collectManifestDigest(pkg);
        } catch (PackageParserException e) {
            res.setError("Failed collect during installPackageLI", e);
            return;
        }
 }

在這個方法中可以看到一個用來解析傳入的APK包的類PackageParser鱼响,并且這里調(diào)用了PackageParser.collectCertificates方法來進行簽名的校驗鸣剪。于是進入該方法:

public void collectCertificates(Package pkg, int flags) throws PackageParserException {
        ......
        collectCertificates(pkg, new File(pkg.baseCodePath), flags);
       ......
    }

如上,該方法由調(diào)用了一個函數(shù)重載丈积,代碼如下:

private static void collectCertificates(Package pkg, File apkFile, int flags)
            throws PackageParserException {
        final String apkPath = apkFile.getAbsolutePath();

        StrictJarFile jarFile = null;
        try {
            jarFile = new StrictJarFile(apkPath);
            ......
    }

由于該方法的代碼較長西傀,我們先分段看,先看上面的代碼桶癣,很明顯拥褂,是通過傳入的apk文件來構(gòu)造一個StrictJarFile對象,下面來看一下它的構(gòu)造方法都做了些什么事情:

public StrictJarFile(String fileName) throws IOException {
        ......
        try {
            ......
            HashMap<String, byte[]> metaEntries = getMetaEntries();
            this.manifest = new Manifest(metaEntries.get(JarFile.MANIFEST_NAME), true);
            this.verifier = new JarVerifier(fileName, manifest, metaEntries);

            isSigned = verifier.readCertificates() && verifier.isSignedJar();
      ......
    }

如上牙寞,首先調(diào)用getMetaEntries()方法將META-INF目錄下每一個文件的文件名及其數(shù)據(jù)流存放到metaEntries這個HashMap對象中饺鹃;然后通過MANIFEST.MF文件的數(shù)據(jù)流構(gòu)造一個Manifest對象;接著利用得到的metaEntries和manifest來構(gòu)造一個JarVerifier對象间雀,最后調(diào)用JarVerifier的readCertificates()方法和isSignedJar()方法悔详。下面先看JarVerifier.readCertificates方法:

synchronized boolean readCertificates() {
        if (metaEntries.isEmpty()) {
            return false;
        }

        Iterator<String> it = metaEntries.keySet().iterator();
        while (it.hasNext()) {
            String key = it.next();
            if (key.endsWith(".DSA") || key.endsWith(".RSA") || key.endsWith(".EC")) {
                verifyCertificate(key);
                it.remove();
            }
        }
        return true;
    }

如上,該方法首先判斷META-INF目錄是否為空惹挟,如果為空表示根本沒簽名茄螃,直接返回false。不為空的話就對metaEntries對象進行遍歷连锯,如果是證書文件归苍,則將其傳入verifyCertificate()方法進行校驗,JarVerifier.verifyCertificate()方法的代碼如下:

private void verifyCertificate(String certFile) {
        // Found Digital Sig, .SF should already have been read
        String signatureFile = certFile.substring(0, certFile.lastIndexOf('.')) + ".SF";
        byte[] sfBytes = metaEntries.get(signatureFile);
        ......
        byte[] manifestBytes = metaEntries.get(JarFile.MANIFEST_NAME);
        ......
        byte[] sBlockBytes = metaEntries.get(certFile);
        try {
            Certificate[] signerCertChain = JarUtils.verifySignature(
                    new ByteArrayInputStream(sfBytes),
                    new ByteArrayInputStream(sBlockBytes));
            if (signerCertChain != null) {
                certificates.put(signatureFile, signerCertChain);
            }
        } catch (IOException e) {
            return;
        } catch (GeneralSecurityException e) {
            throw failedVerification(jarName, signatureFile);
        }
        ......
    }

該方法首先通過傳入的證書文件<CERT>.RSA的路徑來獲取<CERT>.SF的路徑运怖,然后通過前面得到的metaEntries來分別取得MANIFEST.MF拼弃、CERT.SF、CERT.RSA這三個文件的字節(jié)流:manifestBytes摇展、sfBytes吻氧、sBlockBytes,然后將sfBytes和sBlockBytes傳入JarUtils.verfySignature()方法中,進行數(shù)字簽名的校驗盯孙,校驗的過程這里就不貼代碼了鲁森,簡單說就是用CERT.RSA這個文件中的包含的公鑰對數(shù)字簽名進行解密,將解密后的結(jié)果與CERT.SF文件hash運算后的結(jié)果進行比對振惰,一致的話就返回證書鏈信息歌溉,并將證書鏈保存在certificates對象中,同時說明CERT.SF文件沒有被篡改报账,另外研底,Jarverifier.isSignedJar()方法就是判斷certificates是否為空,不為空返回true透罢,空則返回false榜晦。否則就拋出GeneralSecurityException異常。接著上面繼續(xù)看JarVerifier.verifyCertificate()方法:

        // Verify manifest hash in .sf file
        Attributes attributes = new Attributes();
        HashMap<String, Attributes> entries = new HashMap<String, Attributes>();
        try {
            ManifestReader im = new ManifestReader(sfBytes, attributes);
            im.readEntries(entries, null);
        } catch (IOException e) {
            return;
        }

        // Do we actually have any signatures to look at?
        if (attributes.get(Attributes.Name.SIGNATURE_VERSION) == null) {
            return;
        }

        boolean createdBySigntool = false;
        String createdBy = attributes.getValue("Created-By");

        if (mainAttributesEnd > 0 && !createdBySigntool) {
            String digestAttribute = "-Digest-Manifest-Main-Attributes";
            if (!verify(attributes, digestAttribute, manifestBytes, 0, mainAttributesEnd, false, true)) {
                throw failedVerification(jarName, signatureFile);
            }
        }

        // Use .SF to verify the whole manifest.
        String digestAttribute = createdBySigntool ? "-Digest" : "-Digest-Manifest";
        if (!verify(attributes, digestAttribute, manifestBytes, 0, manifestBytes.length, false, false)) {
            Iterator<Map.Entry<String, Attributes>> it = entries.entrySet().iterator();
            while (it.hasNext()) {
                Map.Entry<String, Attributes> entry = it.next();
                Manifest.Chunk chunk = manifest.getChunk(entry.getKey());
                if (chunk == null) {
                    return;
                }
                if (!verify(entry.getValue(), "-Digest", manifestBytes,
                        chunk.start, chunk.end, createdBySigntool, false)) {
                    throw invalidDigest(signatureFile, entry.getKey(), jarName);
                }
            }
        }
        metaEntries.put(signatureFile, null);
        signatures.put(signatureFile, entries);

根據(jù)代碼中的注釋也能夠很清晰的了解到羽圃,這段代碼主要就是通過讀取CERT.SF乾胶,然后來驗證MANIFEST.MF文件是否被篡改。來先看一下CERT文件的部分內(nèi)容:



結(jié)合上圖朽寞,再回到JarVerifier.verifyCertificate()方法的代碼中來分析一下具體的流程吧:

首先讀取CERT.SF文件识窿,并創(chuàng)建與之相關(guān)的兩個對象attributes和entries;
接著就通過attributes對象判斷CERT.SF文件中是否存在"Signature-Version"屬性脑融,沒有的話直接返回喻频;
再判斷CERT.SF文件中的"Created-By"屬性的值是否包含"signtool"子串,有的話表示該apk是用其他簽名工具簽的名肘迎;如上圖甥温,這里用的是JDK自帶jarsigner簽的名,所以不含signtool字符串妓布,這樣的話之后就會調(diào)用JarVerifier.verify()方法來判斷是否有"SHA1-Digest-Manifest-Main-Attributes"屬性姻蚓,有的話就校驗它的值,看它是否為MANIFEST.MF的頭屬性塊運算(Hash+Base64編碼)后的值匣沼≌玻可以看到,JarVerifier.verify()方法的第三個參數(shù)傳的就是MANIFEST.MF的字節(jié)流释涛。
JarVerifier.verify()方法的代碼如下:

private boolean verify(Attributes attributes, String entry, byte[] data,
            int start, int end, boolean ignoreSecondEndline, boolean ignorable) {
        for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) {
            String algorithm = DIGEST_ALGORITHMS[i];
            String hash = attributes.getValue(algorithm + entry);
            if (hash == null) {
                continue;
            }

            MessageDigest md;
            try {
                md = MessageDigest.getInstance(algorithm);
            } catch (NoSuchAlgorithmException e) {
                continue;
            }
            if (ignoreSecondEndline && data[end - 1] == '\n' && data[end - 2] == '\n') {
                md.update(data, start, end - 1 - start);
            } else {
                md.update(data, start, end - start);
            }
            byte[] b = md.digest();
            byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1);
            return MessageDigest.isEqual(b, Base64.decode(hashBytes));
        }
        return ignorable;
    }

在這個方法中加叁,由于不知道用的什么Hash算法,所以會遍歷DIGEST_ALGORITHMS數(shù)組枢贿,該數(shù)組的內(nèi)容如下:

private static final String[] DIGEST_ALGORITHMS = new String[] {
    "SHA-512",
    "SHA-384",
    "SHA-256",
    "SHA1",
};

將遍歷到的算法名與字符串"-Digest-Manifest-Main-Attributes"組合殉农,然后判斷該屬性是否存在,不存在則略過(continue;)局荚。然后就是hash值的比對了。

再次回到JarVerifier.verifyCertificate()方法的代碼:
接下來就是再次調(diào)用JarVerifier.verifier()方法,不過這次是對MANIFEST.MF整個文件的Hash與CERT.SF的"SHA1--Digest-Manifest"屬性的值進行比對耀态,如果一致轮傍,則說明MANIFEST.MF沒有被篡改,并將CERT.SF文件的信息添加到metaEntries和signatures的屬性中首装。如果不一致创夜,則遍歷所有的屬性塊,看是哪一個屬性塊的值不正確仙逻。

到這里驰吓,StrictJarFile的構(gòu)造方法就完成了。從上面的分析可以看到系奉,驗簽的三個步驟中檬贰,有兩步是再StrictJarFile的構(gòu)造方法中完成的,分別是:CERT.SF是否被篡改缺亮,MANIFEST.MF是否被篡改翁涤。

接下來,讓我們再回到PackageParser.collectCertificates()方法中萌踱,繼續(xù)完成后續(xù)的校驗分析葵礼,代碼如下:

private static void collectCertificates(Package pkg, File apkFile, int flags)
            throws PackageParserException {
        final String apkPath = apkFile.getAbsolutePath();

        StrictJarFile jarFile = null;
        try {
            jarFile = new StrictJarFile(apkPath);

            // Always verify manifest, regardless of source
            final ZipEntry manifestEntry = jarFile.findEntry(ANDROID_MANIFEST_FILENAME);
            if (manifestEntry == null) {
                throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,
                        "Package " + apkPath + " has no manifest");
            }

            final List<ZipEntry> toVerify = new ArrayList<>();
            toVerify.add(manifestEntry);

            // If we're parsing an untrusted package, verify all contents
            if ((flags & PARSE_IS_SYSTEM) == 0) {
                final Iterator<ZipEntry> i = jarFile.iterator();
                while (i.hasNext()) {
                    final ZipEntry entry = i.next();

                    if (entry.isDirectory()) continue;
                    if (entry.getName().startsWith("META-INF/")) continue;
                    if (entry.getName().equals(ANDROID_MANIFEST_FILENAME)) continue;

                    toVerify.add(entry);
                }
            }

            for (ZipEntry entry : toVerify) {
                final Certificate[][] entryCerts = loadCertificates(jarFile, entry);
                ......
            }
        } catch (GeneralSecurityException e) {
            throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
                    "Failed to collect certificates from " + apkPath, e);
        } catch (IOException | RuntimeException e) {
            throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                    "Failed to collect certificates from " + apkPath, e);
        } finally {
            closeQuietly(jarFile);
        }
    }

如上,在創(chuàng)建了StrictJarFile對象后并鸵,就對該對象進行遍歷鸳粉,將除了目錄和META-INF目錄下的文件外的所有文件的ZipEntry對象添加到toVerify這個列表中。然后遍歷該列表园担,將每一個文件代表的ZipEntry對象傳入PackageParser.loadCertificates()方法中届谈,代碼如下:

private static Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry entry)
            throws PackageParserException {
        InputStream is = null;
        try {
            // We must read the stream for the JarEntry to retrieve
            // its certificates.
            is = jarFile.getInputStream(entry);
            readFullyIgnoringContents(is);
            return jarFile.getCertificateChains(entry);
        } catch (IOException | RuntimeException e) {
            throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
                    "Failed reading " + entry.getName() + " in " + jarFile, e);
        } finally {
            IoUtils.closeQuietly(is);
        }
    }

這里調(diào)用了StrictJarFile.getInputStream()方法來獲取InputStream對象,看下該方法:

public InputStream getInputStream(ZipEntry ze) {
        final InputStream is = getZipInputStream(ze);

        if (isSigned) {
            JarVerifier.VerifierEntry entry = verifier.initEntry(ze.getName());
            if (entry == null) {
                return is;
            }

            return new JarFile.JarFileInputStream(is, ze.getSize(), entry);
        }

        return is;
    }

這里主要是獲取JarVerifier.VerifierEntry對象粉铐,最后返回一個JarFile.JarFileInputStream對象疼约。看一下JarVerifier.initEntry()方法:

VerifierEntry initEntry(String name) {
        // If no manifest is present by the time an entry is found,
        // verification cannot occur. If no signature files have
        // been found, do not verify.
        if (manifest == null || signatures.isEmpty()) {
            return null;
        }

        Attributes attributes = manifest.getAttributes(name);
        // entry has no digest
        if (attributes == null) {
            return null;
        }

        ArrayList<Certificate[]> certChains = new ArrayList<Certificate[]>();
        Iterator<Map.Entry<String, HashMap<String, Attributes>>> it = signatures.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, HashMap<String, Attributes>> entry = it.next();
            HashMap<String, Attributes> hm = entry.getValue();
            if (hm.get(name) != null) {
                // Found an entry for entry name in .SF file
                String signatureFile = entry.getKey();
                Certificate[] certChain = certificates.get(signatureFile);
                if (certChain != null) {
                    certChains.add(certChain);
                }
            }
        }

        // entry is not signed
        if (certChains.isEmpty()) {
            return null;
        }
        Certificate[][] certChainsArray = certChains.toArray(new Certificate[certChains.size()][]);

        for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) {
            final String algorithm = DIGEST_ALGORITHMS[i];
            final String hash = attributes.getValue(algorithm + "-Digest");
            if (hash == null) {
                continue;
            }
            byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1);

            try {
                return new VerifierEntry(name, MessageDigest.getInstance(algorithm), hashBytes,
                        certChainsArray, verifiedEntries);
            } catch (NoSuchAlgorithmException ignored) {
            }
        }
        return null;
    }

該方法就是創(chuàng)建一個JarVerifier.VerifierEntry對象:
第一個參數(shù)name是文件名蝙泼;
第二個參數(shù)是用來產(chǎn)生摘要的對象MessageDigest程剥,且摘要算法algorithm也是同前面的方法一樣,從DIGEST_ALGORITHMS數(shù)組中遍歷汤踏,再根據(jù)MANIFEST.MF文件的屬性名來得到织鲸;
第三個參數(shù)是MANIFEST.MF中所保存的對應文件名的Hash值;
MANIFEST.MF的部分內(nèi)容如下:


第四個參數(shù)是對該APK進行簽名的所有證書鏈信息溪胶。它為什么是二維數(shù)組搂擦?是因為Android允許用多個證書對apk進行簽名,且它們的證書文件名必須不同哗脖。
最后一個參數(shù)是已經(jīng)驗證過的文件列表瀑踢,VerifierEntry在完成了對指定文件的摘要驗證之后會將該文件的信息加到其中扳还。

接著,來看一下JarFile.JarFileInputStream的構(gòu)造方法:

JarFileInputStream(InputStream is, long size, JarVerifier.VerifierEntry e) {
            super(is);
            entry = e;

            count = size;
        }

只是幾個賦值操作橱夭,其中將前面得到的JarVerifier.VerifierEntry對象傳入并賦值給這里的entry氨距。

將視線在回到PackageParser.loadCertificates()方法中,經(jīng)過上面的分析棘劣,StrictJarFile.getInputStream()所返回的是JarFile.JarFileInputStream對象俏让。接著將該對象傳入PackageParser.readFullyIgnoringContents()方法中,來看下該方法做了什么:

public static long readFullyIgnoringContents(InputStream in) throws IOException {
        byte[] buffer = sBuffer.getAndSet(null);
        if (buffer == null) {
            buffer = new byte[4096];
        }

        int n = 0;
        int count = 0;
        while ((n = in.read(buffer, 0, buffer.length)) != -1) {
            count += n;
        }

        sBuffer.set(buffer);
        return count;
    }

看起來只是對傳入的字節(jié)輸入流對象進行讀取茬暇,直到讀完首昔,然后返回讀到的字節(jié)數(shù)。但由于傳入的是InpuStream對象的子類對象JarFile.JarFileInputStream糙俗,而且它重寫了read()方法勒奇,來看一下這個子類的read()方法做了什么事:

@override
public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
            if (done) {
                return -1;
            }
            if (count > 0) {
                int r = super.read(buffer, byteOffset, byteCount);
                if (r != -1) {
                    int size = r;
                    if (count < size) {
                        size = (int) count;
                    }
                    entry.write(buffer, byteOffset, size);
                    count -= size;
                } else {
                    count = 0;
                }
                if (count == 0) {
                    done = true;
                    entry.verify();
                }
                return r;
            } else {
                done = true;
                entry.verify();
                return -1;
            }
        }

如上,它會調(diào)用父類的read()方法進行讀取臼节,然后將讀取到的數(shù)據(jù)傳入entry.write()方法撬陵,最后在調(diào)用entry.verify()進行驗證。這個entry就是前面創(chuàng)建的JarVerifier.VerifierEntry對象网缝。來看一下JarVerifier.VerifierEntry.write()方法做了什么:

@Override
public void write(byte[] buf, int off, int nbytes) {
    digest.update(buf, off, nbytes);
}

就是對數(shù)據(jù)進行hash巨税。再來看一下JarVerifier.VerifierEntry.verify()方法做了什么:

void verify() {
    byte[] d = digest.digest();
    if (!MessageDigest.isEqual(d, Base64.decode(hash))) {
        throw invalidDigest(JarFile.MANIFEST_NAME, name, name);
    }
    verifiedEntries.put(name, certChains);
}

該方法就是將文件的Hash與MANIFEST中對應文件的Hash值進行比對,一致的話則將文件名和證書鏈添加到verifiedEntries中粉臊;不一致的話就調(diào)用JarVerifier.invalidDigest()方法拋出SecurityException異常草添,如下:

private static SecurityException invalidDigest(String signatureFile, String name, 
          String jarName) {
    throw new SecurityException(signatureFile + " has invalid digest for " + name +
                " in " + jarName);
}

到這里,第三步扼仲,即校驗APK所有文件是否有被篡改远寸,也已完成。再次回到PackageParser.collectCertificates()方法中繼續(xù)看:

        for (ZipEntry entry : toVerify) {
                final Certificate[][] entryCerts = loadCertificates(jarFile, entry);
                if (ArrayUtils.isEmpty(entryCerts)) {
                    throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                            "Package " + apkPath + " has no certificates at entry "
                            + entry.getName());
                }
                final Signature[] entrySignatures = convertToSignatures(entryCerts);

                if (pkg.mCertificates == null) {
                    pkg.mCertificates = entryCerts;
                    pkg.mSignatures = entrySignatures;
                    pkg.mSigningKeys = new ArraySet<PublicKey>();
                    for (int i=0; i < entryCerts.length; i++) {
                        pkg.mSigningKeys.add(entryCerts[i][0].getPublicKey());
                    }
                } else {
                    if (!Signature.areExactMatch(pkg.mSignatures, entrySignatures)) {
                        throw new PackageParserException(
                                INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, "Package " + apkPath
                                        + " has mismatched certificates at entry "
                                        + entry.getName());
                    }
                }
            }

loadCertificates()之后的代碼屠凶,主要就是判斷該APK是否原來安裝過驰后,如果沒安裝過,則保存該APK的簽名信息矗愧;如果安裝過灶芝,則比對前后兩次安裝的簽名信息,如果簽名信息一致唉韭,則繼續(xù)安裝夜涕;如果前后簽名不一致,則拋出異常INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES異常属愤。所以平時我們在開發(fā)或者測試過程中安裝應用時女器,如果拋出該異常,則說明已經(jīng)有該包名的應用安裝在設(shè)備上住诸,且簽名與你現(xiàn)在要安裝的不同驾胆。必須先卸載已安裝在設(shè)備上的才能繼續(xù)安裝涣澡。(INCONSISTENT就是不一致的意思)

0x04 小結(jié)

至此,Android的應用驗簽過程總算分析完了俏拱,可以看到暑塑,驗簽的過程剛好和簽名的過程是相反的吼句,下面總結(jié)一下:

  1. 所有的驗簽動作都是在JarVerifier這個類里面完成的锅必;
  2. 在JarVerifier.verifyCertificate()方法中完成了以下兩步:
    1. 使用CERT.RSA校驗CERT.SF,看CERT.SF是否被篡改惕艳;
    2. 使用CERT.SF校驗MANIFEST.MF搞隐,看MANIFEST.MF是否被篡改;
  3. 在JarVerifier.VerifierEntry.verify()方法中完成最后一步:
    1. 使用MANIFEST.MF來校驗所有文件远搪,看有沒有文件被篡改劣纲,或者有沒有文件被刪
      除,又或者有沒有添加新的文件谁鳍。

0x05 參考文獻

http://blog.csdn.net/roland_sun/article/details/42029019
https://www.cnblogs.com/JeffreySun/archive/2010/06/24/1627247.html
http://netsecurity.51cto.com/art/201108/287971.htm

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末癞季,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子倘潜,更是在濱河造成了極大的恐慌绷柒,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件涮因,死亡現(xiàn)場離奇詭異废睦,居然都是意外死亡,警方通過查閱死者的電腦和手機养泡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進店門嗜湃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人澜掩,你說我怎么就攤上這事购披。” “怎么了肩榕?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵刚陡,是天一觀的道長。 經(jīng)常有香客問我点把,道長橘荠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任郎逃,我火速辦了婚禮哥童,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘褒翰。我一直安慰自己贮懈,他們只是感情好匀泊,可當我...
    茶點故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著朵你,像睡著了一般各聘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上抡医,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天躲因,我揣著相機與錄音,去河邊找鬼忌傻。 笑死大脉,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的水孩。 我是一名探鬼主播镰矿,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼俘种!你這毒婦竟也來了秤标?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤宙刘,失蹤者是張志新(化名)和其女友劉穎苍姜,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體荐类,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡怖现,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了玉罐。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屈嗤。...
    茶點故事閱讀 40,110評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖吊输,靈堂內(nèi)的尸體忽然破棺而出饶号,到底是詐尸還是另有隱情,我是刑警寧澤季蚂,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布茫船,位于F島的核電站,受9級特大地震影響扭屁,放射性物質(zhì)發(fā)生泄漏算谈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一料滥、第九天 我趴在偏房一處隱蔽的房頂上張望然眼。 院中可真熱鬧,春花似錦、人聲如沸扼雏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鲸匿。三九已至爷怀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間带欢,已是汗流浹背运授。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留洪囤,地道東北人徒坡。 一個月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像瘤缩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子伦泥,可洞房花燭夜當晚...
    茶點故事閱讀 45,047評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 對于 Android 開發(fā)者而言剥啤, APK 簽名的重要性不言而喻。Android 7.0 后 APK 簽名已經(jīng)從基...
    Cavabiao閱讀 9,889評論 7 30
  • 1不脯、網(wǎng)絡請求——裸奔的數(shù)據(jù) 無論是網(wǎng)頁還是APP府怯,都不可避免與后臺服務進行訪問,可能從服務器獲取數(shù)據(jù)或者提交數(shù)據(jù)到...
    dfqin閱讀 9,955評論 0 15
  • 很多人寫文章防楷,喜歡把什么行業(yè)現(xiàn)狀啊牺丙,研究現(xiàn)狀啊什么的寫了一大通,感覺好像在寫畢業(yè)論文似的复局,我這不廢話冲簿,先直接上幾個...
    龐哈哈哈12138閱讀 2,938評論 1 8
  • 1. Android 的簽名保護機制到底是什么? Android 系統(tǒng)禁止更新安裝簽名不一致的 Apk亿昏,如果我們修...
    騰訊bugly閱讀 2,681評論 0 2
  • 一峦剔、Android APK打包流程 ** 資源預編譯 **為每一個非assert資源生成一個ID并保存在一個R文件...
    DevSiven閱讀 716評論 0 5