不少人寫文章,喜歡把什麼行業現狀啊,研究現狀啊什麼的寫了一大通,感受好像在寫畢業論文似的,我這不廢話,先直接上幾個圖,感覺一下。
php
第一張圖是在把代碼注入到地圖裏面,啓動首頁的時候彈出個浮窗,下載網絡的圖片,蒼老師大家不會不認識吧?html
第二張圖是微信運動步數做弊,6不6?
ok,那咱們從頭提及vue
Android 的反編譯,相信你們都應該有所瞭解,apktool、JEB 等工具
咱們先看一下 Apk 文件的結構吧,以下圖:java
1.META-INF:簽名文件(這個是如何生成的後面會提到)
2.res:資源文件,裏面的 xml 格式文件在編譯過程當中由文本格式轉化爲二進制的 AXML 文件格式
3.AndroidManifest.xml:android 配置文件,編譯過程依然被轉換爲 AXML 格式
4.classes.dex:java 代碼編譯後產生的相似字節碼的文件(dalvik 字節碼)
5.resources.arsc:具備 id 值資源的索引表(asserts 文件夾中的資源不會生成索引)
6.其餘文件:可由開發者本身添加,諸如 assets 等,或者 lib(native so 代碼)等目錄android
(Android 編譯打包過程分析參看:http://blog.csdn.net/luoshengyang/article/details/8744683)
c++
apk的核心邏輯主要在 classes.dex 中,破解和二次打包也基本上對這個文件作手腳,因此對這個文件的保護也尤其重要。
git
上圖爲通常 Apk 的破解過程(windows 畫圖工具畫的比較搓)github
咱們首先用 apktool 工具反編譯:java -jar apktool.jar d -f xxx.apk outDir(PS:outDir 不寫會在當前目錄輸出)
反編譯後的目錄結構以下:
算法
這裏,res 裏的 xml 和 manifset.xml 都已是解出後的 xml 了,不是 axml 格式了,res 目錄裏的 values 目錄下的 public.xml 能夠看到資源對應的 idwindows
若是命令 java -jar apktool.jar d -f再加入 -r 表明資源文件不反編譯,上圖的目錄中將依然有resources.arsc,xml 都是 axml 格式的,也找不到 public.xml
其實咱們主要關注的是 smali 這個目錄,裏面是按照 android 程序編寫的時候 java 文件的目錄格式生成的,可是裏面的文件並非 java 格式的,而是 smali 格式的,相似 MainActivity.smali。
那麼什麼是 smali 文件呢?
1.Smali 是 Android 的 Dalvik 虛擬機所使用的一種 dex 格式的中間語言
2.能夠理解爲,C 語言和彙編語言的編譯與反編譯,把 smali 理解爲一種彙編語言
咱們能夠打開一個 smali 文件看看,咱們可使用 notepad++ 打開,而後定一下 smali 語法的高亮顯示
將下面內容保存到 C:\Users\用戶名\AppData\Roaming\Notepad++下,文件名爲 userDefineLang.xml
<NotepadPlus> <UserLang name="smali" ext="smali"> <Settings> <Global caseIgnored="no" /> </Settings> <KeywordLists> <Keywords name="Delimiters"></Keywords> <Keywords name="Folder+"></Keywords> <Keywords name="Folder-"></Keywords> <Keywords name="Operators">' ! " ( ) , ; : @ [ ] { }</Keywords> <Keywords name="Comment">0#</Keywords> <Keywords name="Words1">move move/from16 move/16 move-wide move-wide/from16 move-wide/16 move-object move-object/from16 move-object/16 move-result move-result-wide move-result-object move-exception return-void return return-wide return-object const/4 const/16 const const/high16 const-wide/16 const-wide/32 const-wide const-wide/high16 const-string const-string/jumbo const-class monitor-enter monitor-exit check-cast instance-of array-length new-instance new-array filled-new-array filled-new-array/range fill-array-data throw goto goto/16 goto/32 packed-switch sparse-switch cmpl-float cmpg-float cmpl-double cmpg-double cmp-long if-eq if-ne if-lt if-ge if-gt if-le if-eqz if-nez if-ltz if-gez if-gtz if-lez aget aget-wide aget-object aget-boolean aget-byte aget-char aget-short aget-short aput aput-wide aput-object aput-boolean aput-byte aput-char aput-short iget iget-wide iget-object iget-boolean iget-char iget-short iput iput-wide iput-object iput-boolean iput-byte iput-char iput-short sget sget-wide sgetobject sget-boolean sget-byte sget-char sget-short sput sput-wide sput-object sput-boolean sput-byte sput-char sput-short invoke-virtual invoke-super invoke-direct invoke-static invoke-interface invoke-virtual/range invoke-super/range invoke-direct/range invoke-static/range invoke-interface/range neg-int not-int neg-long neg-float neg-double int-tolong int-tofloat int-to-double long-to-int long-to-float long-to-double float-to-int float-to-long double-to-double double-to-int double-to-long double-to-float int-to-byte int-to-char int-to-short add-int sub-int mul-int div-int rem-int and-int or-int xor-int shl-int shr-int ushr-int add-long sub-long mul-long div-long rem-long and-long or-long xor-long shl-long shr-long ushr-long add-float sub-float mul-float div-float rem-float add-double sub-double mul-double div-double rem-double add-int/2addr sub-int/2addr mul-int/2addr div-int/2addr rem-int/2addr and-int/2addr or-int/2addr xor-int/2addr shl-int/2addr shr-int/2addr usnhr-int/2addr add-long/2addr sub-long/2addr mul-long/2addr div-long/2addr rem-long/2addr and-long/2addr or-long/2addr xor-long/2addr shl-long/2addr shr-long/2addr ushr-long/2addr add-float/2addr sub-float/2addr mul-float/2addr div-float/2addr rem-float/2addr add-double/2addr mul-double/2addr div-double/2addr rem-double/2addr add-int/lit16 rsub-int mul-int/lit16 div-int/lit16 and-int.lit16 or-int/lit16 xor-int/lit16 and-int/lit8 mul-int/lit8 div-int/lit8</Keywords> <Keywords name="Words2">.method .annotation .end .line .prologue .implements .super .class .source .locals .parameter .field .local .restart</Keywords> <Keywords name="Words3">public annotation method protected static final field private synthetic local</Keywords> <Keywords name="Words4">Z V I F</Keywords> </KeywordLists> <Styles> <WordsStyle name="DEFAULT" styleID="11" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" /> <WordsStyle name="FOLDEROPEN" styleID="12" fgColor="FF0000" bgColor="FFFFFF" fontName="" fontStyle="0" /> <WordsStyle name="FOLDERCLOSE" styleID="13" fgColor="FF0000" bgColor="FFFFFF" fontName="" fontStyle="0" /> <WordsStyle name="KEYWORD1" styleID="5" fgColor="FF8040" bgColor="FFFFFF" fontName="Consolas" fontStyle="1" fontSize="10" /> <WordsStyle name="KEYWORD2" styleID="6" fgColor="91A62D" bgColor="FFFFFF" fontName="Consolas" fontStyle="2" fontSize="10" /> <WordsStyle name="KEYWORD3" styleID="7" fgColor="004080" bgColor="FFFFFF" fontName="Consolas" fontStyle="0" fontSize="10" /> <WordsStyle name="KEYWORD4" styleID="8" fgColor="FF0000" bgColor="FFFFFF" fontName="Consolas" fontStyle="0" fontSize="10" /> <WordsStyle name="COMMENT" styleID="1" fgColor="FF8080" bgColor="FFFFFF" fontName="Consolas" fontStyle="2" fontSize="10" /> <WordsStyle name="COMMENT LINE" styleID="2" fgColor="008000" bgColor="FFFFFF" fontName="Consolas" fontStyle="2" fontSize="10" /> <WordsStyle name="NUMBER" styleID="4" fgColor="D9006C" bgColor="FFFFFF" fontName="Consolas" fontStyle="0" fontSize="10" /> <WordsStyle name="OPERATOR" styleID="10" fgColor="008040" bgColor="FFFFFF" fontName="" fontStyle="0" /> <WordsStyle name="DELIMINER1" styleID="14" fgColor="AF2BFF" bgColor="FFFFFF" fontName="" fontStyle="0" /> <WordsStyle name="DELIMINER2" styleID="15" fgColor="AF2BFF" bgColor="FFFFFF" fontName="" fontStyle="0" /> <WordsStyle name="DELIMINER3" styleID="16" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" /> </Styles> </UserLang> </NotepadPlus>
能夠參看 http://www.ourunix.org/post/117.html 操做
打開 MainActivity.smali 文件,頭三行代碼大體以下:
.class public Lcom/example/hacktest/MainActivity; .super Landroid/app/Activity; .source "MainActivity.java"
smali 語法這裏就不介紹了,本身查資料就好 smali 文件語法參考 http://my.oschina.net/xiahuawuyu/blog/57146
這裏舉個例子,咱們寫個程序,一個 edittext 一個 button,當 edit 裏面輸入正確的文字的時候,點擊 button 纔會彈出正確的 toast。
咱們看一下,反編譯後的關鍵代碼
能夠看到這是一個參數爲 string,返回值爲 boolean 名叫 check 的函數,當輸入爲「11」的時候才返回 true。
咱們能夠把第一個紅框位置的 if-eqz 改爲 if-nez,這樣你輸入除了11的任何字符都會返回 true。
或者把第二個紅框位置的 0x1,改爲 0x0,(0表明 true),這樣這個函數無論輸入什麼都返回 true。
OK,這樣咱們就改完了,咱們從新編譯:java -jar apktool.jar b -f outDir xxx.apk
(PS:xxx.apk 能夠不寫,會在 outDir 裏生成 dist 目錄,編譯好的 Apk 在這裏面)
簽名:能夠網上下載工具 autoSign,使用方法略。
安裝 Apk 後驗證,經過。
可是事情並不老是如咱們所願,有些 Apk 會作一些盜版檢測機制,就是爲了防止二次重打包
以手機暴風影音爲例,當你按照上述步驟反編譯,從新編譯,簽名以後,進入 APP 會出現這個頁面,沒法正常使用
由於你並無這個 APP 的正版簽名文件(關於簽名相關的東西,在後面我再仔細講)
那麼這個原理是什麼呢,咱們大膽猜想一下,無非就是和上一個例子相似的 check 函數,兩個值的對比,那麼這個值必定是簽名。
咱們先經過方法拿到正版手機暴風影音的簽名 md5,而後在反編譯後的代碼中搜索一下這個值。
然而並無搜到,再換個思路,咱們搜索獲取簽名的這個函數,從而定位關鍵代碼,獲取應用簽名的 java 代碼相似是:
PackageInfo pi = context.getPackageManager.().getPackageInfo(packname,packageManager.GET_SIGNATURES); 對應的smali代碼相似是: Landroid/content/pm/PackageInfo;->signatures:[Landroid/content/pm/Signature
咱們再搜用這段代碼搜索,在 StormUtils2.smali 裏面找到了,發如今函數 getSignInfo 裏面,繼續跟蹤到 checkPiracy 函數。
看到這個函數發現就和上例中的 check 函數相似了,改一下返回值爲 true 就行了。
咱們再仔細看看這個函數,發現關鍵的簽名 md5 值被拆開存放了,因此咱們纔沒有搜到,這也是防範搜索的一個舉措吧(雖然我以爲並沒什麼用)
const-string/jumbo v3, "dbbf60f096b326003" const-string/jumbo v0, "c388a350d1578d5"
好的,修改後,咱們再從新編譯、簽名,驗證經過。
(PS:關於簽名檢測的除了 java 層的,可能還有再 so 裏面校驗的和服務器驗證的方式,在 so 裏的用 IDA 打開 so 跟蹤修改,服務器驗證的抓包查看,再模擬發包重放攻擊就行了,這裏就不具體介紹了)
android 系統禁止更新安裝簽名不一致的 apk,若是咱們修改了 apk 又用別的簽名文件簽名,確定是不一致的。
咱們從簽名工具 autoSign 分析,看一下 sign.bat 文件內容:
@ECHO OFF
Echo Auto-sign Created By Dave Da illest 1
Echo Update.zip is now being signed and will be renamed to update_signed.zip
java -jar signapk.jar testkey.x509.pem testkey.pk8 update.apk update_signed.apk
Echo Signing Complete
Pause
看一下 java -jar signapk.jar testkey.x509.pem testkey.pk8 update.apk update_signed.apk 這行的意義:
以testkey.x509.pem 這個公鑰文件和 testkey.pk8 這個私鑰文件對 update.apk 進行簽名,簽名後保存爲 update_signed.apk
咱們能夠看到簽名前和簽名後比較,簽名後的文件中多了一個文件夾「META-INF」,裏面有三個文件 MANIFEST.MF 、 CERT.SF 、 CERT.RSA
咱們經過 jd-gui 工具打開 signapk.jar,找到 main 函數,經過這個函數跟蹤代碼
1.addDigestsToManifest 這個函數,遍歷 apk 中全部文件,對非文件夾非簽名文件的文件逐個生成 SHA1 數字簽名信息,再 base64 編碼
而後再寫入 MANIFEST.MF 文件中,生成文件以下:
Manifest-Version: 1.0
Created-By: 1.0 (Android)
Name: res/drawable-xhdpi/ic_launcher.png
SHA1-Digest: AfPh3OJoypH966MludSW6f1RHg4=
Name: AndroidManifest.xml
SHA1-Digest: NaPhUBH5WO7uGk/CfRu/SHsCvW0=
Name: res/drawable-mdpi/ic_launcher.png
SHA1-Digest: RRxOSvpmhVfCwiprVV/wZlaqQpw=
Name: res/drawable-hdpi/ic_launcher.png
SHA1-Digest: Nq8q3HeTluE5JNCBpVvNy3BXtJI=
Name: res/layout/activity_main.xml
SHA1-Digest: kxwMyILwF2K+n9ziNhcQqcCGWIU=
Name: resources.arsc
SHA1-Digest: q7Ystu6WoSWih53RGKXtE3LeTdc=
Name: classes.dex
SHA1-Digest: Ao1WOs5PXMxsWTDsjSijS2tfnHo=
Name: res/drawable-xxhdpi/ic_launcher.png
SHA1 生成的摘要信息,若是你修改了某個文件,apk 安裝校驗時,取到的該文件的摘要與 MANIFEST.MF 中對應的摘要不一樣,則安裝不成功
2.接下來對以前生成的 manifest 使用 SHA1withRSA 算法, 用私鑰簽名,writeSignatureFile 這個函數,最後生成 CERT.SF 文件,以下:
Signature-Version: 1.0
Created-By: 1.0 (Android)
SHA1-Digest-Manifest: pNZ9UXN9GMqTgqAwKD6uEN6aD34=
Name: res/drawable-xhdpi/ic_launcher.png
SHA1-Digest: cIga++hy5wqjHl9IHSfbg8tqCug=
Name: AndroidManifest.xml
SHA1-Digest: oRzzLkwuvxC78suvJcAEvTqcjSA=
Name: res/drawable-mdpi/ic_launcher.png
SHA1-Digest: VY7kOF8E3rn8EUTvQC/DcBEN6kQ=
Name: res/drawable-hdpi/ic_launcher.png
SHA1-Digest: stS7pUucSY0GgAVoESyO3Y7SanU=
Name: res/layout/activity_main.xml
SHA1-Digest: Yr3img6SqiKB+1kwcg/Fga2fwcc=
Name: resources.arsc
SHA1-Digest: j1g8I4fI9dM9hAFKEtS9dHsqo5E=
Name: classes.dex
SHA1-Digest: Sci9MmGXNGnZ1d04rCrEEV7MWn4=
Name: res/drawable-xxhdpi/ic_launcher.png
用私鑰經過 RSA 算法對 manifest 裏的摘要信息進行加密,安裝的時候只能經過公鑰解密,解密以後才能得到正確的摘要,再對比
3.最後就是如何生成 CERT.RSA,打開這個文件看到的是亂碼,說明整個文件都被編碼加密了,並且這個文件和公鑰有關
從源碼中看出他是經過 PKCS7 將整個文件加密了
總結:1.簽名只是對完整性和簽名發佈機構的校驗機制 2.不能阻止 apk 被修改,只是簽名沒法保持一致 3.不一樣私鑰對應着不一樣的公鑰,實質上不一樣的公鑰就表明了不一樣的簽名
咱們從獲取下面一段代碼開始分析:
packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES ); Signature[] signs = packageInfo.signatures; md5 = getMD5Str(signs[ 0].toByteArray()); context.getPackageManager()其實拿到的是ApplicationPackageManager
看一下 context 的實現類 contextImpl 裏的 getPackageManager()
@Override public PackageManager getPackageManager() { if (mPackageManager != null) { return mPackageManager ; } IPackageManager pm = ActivityThread.getPackageManager (); if (pm != null) { // Doesn't matter if we make more than one instance. return (mPackageManager = new ApplicationPackageManager(this, pm)); } return null ; }
能夠看到 ActivityThread 中方法 getPackageManager 獲取 IPackageManager
繼續看 ActivityThread 的代碼:
public static IPackageManager getPackageManager() { if (sPackageManager != null) { //Slog.v("PackageManager", "returning cur default = " + sPackageManager); return sPackageManager ; } IBinder b = ServiceManager.getService("package"); //Slog.v("PackageManager", "default service binder = " + b); sPackageManager = IPackageManager.Stub.asInterface(b); //Slog.v("PackageManager", "default service = " + sPackageManager); return sPackageManager; }
看源碼知道是經過 ServiceManager.getService(「package」);獲取 PackageManagerService 並獲得 IBinder對象,而後經過 asInterface 函數取得接口類 IPackageManager 實例
而後做爲參數構造 ApplicationPackageManager
再看 ApplicationPackageManager 這個類裏的方法:
@Override public PackageInfo getPackageInfo(String packageName, int flags) throws NameNotFoundException { try { PackageInfo pi = mPM.getPackageInfo(packageName, flags, mContext.getUserId()); if (pi != null) { return pi; } } catch (RemoteException e) { throw new RuntimeException( "Package manager has died" , e); } throw new NameNotFoundException(packageName); }
這裏的mPM就是上面構造函數傳進來的 IPackageManager,就能夠調用 PackageManagerService 的方法了
下圖是 PackagerManager 靜態類結構圖:
ok,看完上圖的結構圖,繼續跟代碼 ,看到這裏是 mPM 繼續調用的getPackageInfo(代理模式),經過進程通訊到 PackageManagerService 中執行響應操做
@Override public PackageInfo getPackageInfo(String packageName, int flags, int userId) { if (!sUserManager.exists(userId)) return null; enforceCrossUserPermission(Binder.getCallingUid (), userId, false, "get package info"); // reader synchronized (mPackages) { PackageParser.Package p = mPackages.get(packageName); if (DEBUG_PACKAGE_INFO) Log.v (TAG, "getPackageInfo " + packageName + ": " + p); if (p != null) { return generatePackageInfo(p, flags, userId); } if((flags & PackageManager. GET_UNINSTALLED_PACKAGES ) != 0) { return generatePackageInfoFromSettingsLPw(packageName, flags, userId); } } return null ; }
這裏 mPackages 是 hashMap,其調用put方法的時機是在 scanPackageLI 方法中,而 scanPackageLI 的調用地方是在程序安裝和替換函數中,還有就是 scanDirLi 中,代碼略。
scanDirLi 是用來掃描一些系統目錄的的,在 PackageManagerService 的構造函數中調用的
File dataDir = Environment. getDataDirectory(); mAppDataDir = new File(dataDir, "data"); mAppInstallDir = new File(dataDir, "app"); mAppLibInstallDir = new File(dataDir, "app-lib" ); mAsecInternalPath = new File(dataDir, "app-asec" ).getPath(); mUserAppDataDir = new File(dataDir, "user"); mDrmAppPrivateInstallDir = new File(dataDir, "app-private" );
上面是掃描的位置↑↑↑↑↑↑↑
PackageManagerService 處理各類應用的安裝、卸載、管理等工做,開機時由 systemServer 啓動此服務。就是說以前安裝過的應用或者系統應用信息都會在開機掃描過程當中存到 mPackages 這個 hashMap 中。開機後用戶的安裝操做也會一樣存到這個 hashMap 裏面。
繼續看 getPackageInfo,調用的 generatePackageInfo , 裏面調用的是 PackageParser 中的 generatePackageInfo,繼續跟
這個函數的代碼比較長,只貼出部分關鍵代碼:
if ((flags&PackageManager. GET_SIGNATURES ) != 0) { int N = (p.mSignatures != null) ? p.mSignatures.length : 0; if (N > 0) { pi.signatures = new Signature[N]; System.arraycopy (p.mSignatures, 0, pi. signatures, 0 , N); } } return pi;
這段代碼以前主要是作一些複製的操做,就是 new 一個 PackageInfo,而後把 PackageParser.Package 對象中的一些內容複製到這個 PackageInfo 中
從這段代碼能夠看出來,最終獲得的 signatures 信息就是中 p(PackageParser.Package )中的成員 mSignatures 中得來的。
好了,如今就是看這個PackageParser.Package是從哪來的了,經過跟蹤代碼,installPackageLI 和 scanPackageLI 中的 final PackageParser.Package pkg = pp.parsePackag (tmpPackageFile, null, mMetrics, parseFlags);這行代碼只是生成了 pkg,並無賦值裏面的 mSignatures,繼續跟蹤,找到函數 collectCertificatesLI,找到 pp.collectCertificates(pkg , parseFlags)
PS:最終又進行了一系列的跟代碼,找到了 JarVerifier.java 這個類的 readCertificates 這個就是用來讀取.RSA 文件的,最終咱們看到的裏面的代碼是經過 loadCertificates 取得的 certs 賦值給了 pkg.mSignatures
至此,獲取簽名的全部邏輯就算是簡單的過一遍了,如下是簡略流程圖:(發現 ppt 畫圖比 Windows 畫圖工具好用多了,哈哈)
在跟代碼的過程當中也找到了簽名對比的函數 compareSignatures ,有空本身看看就行了。
OK,繞了這麼久咱們終於找到源頭了,獲取簽名就是在 META-INF 中尋找,並解析。試想一下,若是咱們修改了這個函數,讓他解析原來正版的 META-INF 中的 CERT.RSA 文件,這樣就能夠僞造爲真正的簽名了。
那麼咱們就想到了 HOOK,(不少人都是從看雪論壇上找到的一篇文章看到的 http://bbs.pediy.com/showthread.php?t=186054) hook 的原來簡單來講就是,找到原函數和新函數的指針位置,而後兌換內容,將新函數替代原函數。
關於 hook 呢,網上也有不少框架可使用,好比:
1.Cydia substrate : http://www.cydiasubstrate.com/
2.Xposed : http://repo.xposed.info/
網上也有不少教程能夠看看
烏雲(wooyun)上有一篇頗有意思的教程,就是利用 hook 進行微信運動做弊,原帖地址:http://drops.wooyun.org/tips/8416
下圖就是我用了上面的方法產生的效果,還差點被微信部門的人請去喝茶。
這裏用的是 Xposed 框架,原理就是 hook 了手機的計步傳感器的隊列函數,而後把步數的返回值每步乘1000返回,前提是,你的手機硬件自己有計步傳感器功能,這裏微信運動裏面列出了支持的手機列表:https://kf.qq.com/touch/sappfaq/151013AvyyeQ151013r63qmq.html?platform=15。好像最高就是98800了,多是微信作了步數限制吧
我這裏用的是小米4聯通版,發現雖然是1000基數的加,可是好像隔了好久才變化,估計又是 MIUI 作了一些省電策略,傳感器的採集作了對齊吧?
Xposed 框架,不少玩機愛好者,會拿它修改一些主題,字體之類的,或者系統界面,定製本身想要的系統插件等等。然而,也有缺點,須要手機root,並且這個框架,還有可能讓手機變磚,還有的系統可能對這個框架支持的很差,或者不支持。XDA 論壇裏面也有不少大神把 Xposed 對某些機型作了適配,大神通常都是說,若是手機變磚他們不負責,哈哈。
正式因爲這些框架的諸多不便,root 等等的問題,因而就有了一些非 root 的 hook 的黑科技,好比阿里巴巴的開源框架 Dexposed(https://github.com/alibaba/dexposed)其也是根據 Xposed 框架修改而來的,不過看 github 上他們也很久沒更新了。
也有像其餘我的寫的和這種比較相似的框架,這裏就不介紹了。可是這類框架的缺點就是,只能在該進程下hook,不能全局 hook,即只對這個進程的應用起做用,不能對另外一個應用起做用,優勢是能夠 hook 自定義函數也能hook 系統函數,而且不用 root 和重啓。阿里用這個框架來打在線熱補丁。
那對於 APP 內部簽名校驗的就不用再搜相應的代碼了,直接 hook 就一步到位了,android.app.ApplicationPackageManager 這個類的 getPackageInfo 這個方法直接把正確的簽名返回就行了,接下來咱們就須要把 hook 的代碼注入到某個 APP 裏就行了。
開篇的時候有個圖片就是我在騰訊地圖裏面注入了一個蒼老師的圖片其實就是,本身寫了個 imageloader,用來下載網絡圖片,再寫個 activity 或者 dialog 來承載這個 imageview,而後編譯,再反編譯,取出相應的smali等文件,好比貼到已經反編譯好的騰訊地圖的裏面,把開啓這個蒼老師圖片下載的啓動代碼放到合適位置,最後再把騰訊地圖從新打包簽名,就ok 了。
hook 代碼也是同理注入,驗證一下,成功(我這塊寫的比較粗略,代碼比較多,只說思路了)那麼這種代碼注入和 hook 相結合的方式能幹什麼呢,咱們也不妨搞出點事情來。一樣咱們仍是進行微信運動做弊的事情,其實不少運動類的軟件均可以把本身的數據同步到微信運動裏,好比小米手環,樂動力,悅動圈等等。
那咱們就先拿其中一個開刀吧:
通過一系列的跟蹤代碼定位,最終定位到了這個類 cn.ledongli.ldl.cppwrapper.DailyStats 裏的 f 方法(f 是由於代碼混淆了)而後咱們注入並 hook 方法,讓它返回66666,ok,咱們看到了以下效果:
而後咱們在應用裏面登錄微信帳號,和對接到微信運動的功能,發現很差用,是由於,微信裏面作了對應用的簽名校驗,應用的簽名已經變了
因此咱們只能破解微信了(悶聲做大死),一樣注入 hook 代碼,讓微信獲取應用的簽名的時候取得正確簽名,關鍵代碼:
if( packageName.equals( "cn.ledongli.ldl")){ if ( result instanceof PackageInfo) { PackageInfo info = (PackageInfo) result; info. signatures[0] = new Signature( myHexLedongli); param.setResult( info); } }
再把這個盜版的微信從新打包簽名,從新進行應用的同步數據操做,再進微信運動看看,是否是已經66666了。至此做弊完成。
♂♂♂♂♂♂♂♂♂♂我是畫風不一樣的分割線♂♂♂♂♂♂♂♂♂♂♂♂
說了這麼多破解的,也該聊聊防破解的了
google 最先給的就是代碼混淆的方案,其實通常的混淆只是下降了代碼的可讀性,讓你對反編譯出來的函數命名等不知道什麼意思,不過解讀出來只是時間問題。後來還有資源混淆的,可是意義不大。
後來有了核心代碼用 C 實現,寫成 SO,加花指令的辦法,這個辦法確實會阻止一大部分人的繼續破解,可是對於常常作逆向的工程師來講也不是什麼難題。
其實作這麼多大多數軟件的初衷就是不想軟件被盜版,而後被注入亂七八糟的廣告,或者被盜取信息等,後來就有了盜版檢測機制。好比:JAVA 層的簽名校驗,NDK 層校驗,分段存放簽名 Hash 串,服務器校驗等等,可是這些方法我都在上面說了破解方法
如今國內的通常應用市場都有對 APP 簽名的檢測,在你下載的時候會告訴你這個 APP 是否是盜版的,從而讓用戶區分出來。可是應用市場本身自己又被盜版了怎麼辦呢?
再後來,就有了像360加固保和騰訊的樂固等產品,so 作了加密,真正的 dex 也藏起來了,不過我的以爲,就算真正的 dex 也須要變成 odex 了,root 的手機取到 odex,再轉回 dex,就能拿到真正的 dex(雖然我沒試過,可是我以爲多是一個思路),因此這個方法就更難破解了
雖然加固產品很厲害,可是也會有他的缺陷,Android 系統不斷的更新升級,也許就換了某些模式等等,好比 ART 剛出來的時候,加固保加固後的 Apk,在 ART 模式運行下就會 Crash。這些加固產品要不斷的適配各類型號的手機,CPU 類型,運行模式等等,因此不少 APP 爲了考慮兼容性,他們也不會輕易去加固本身的產品。
♂♂♂♂♂♂♂♂♂♂我是畫風不一樣的分割線♂♂♂♂♂♂♂♂♂♂♂♂
關於逆向破解 Android 應用,我以爲耐心很重要吧,代碼跟來跟去確實很枯燥,總結幾點小技巧吧
1.信息反饋:經過界面的一些彈出信息,界面特色尋找突破點
2.特徵函數:好比搜 Toast,Log,getSignature 等
3.代碼注入:把 toast 或者 log 函數注入到程序中,跟蹤位置
4.打印堆棧:插入 new Exception(「定位」).printStackTrace();
5.網絡抓包:經過抓包獲得的關鍵字段,在代碼中定位
寫在後面:
這篇文章整理了有一段時間了,以爲仍是應該寫出來,也不是什麼高深的技術文章,就是我的總結的一點心得而已。
關於破解應用不少人可能會去破解別人的應用注入廣告來獲取利益,也有可能盜取別人的信息。
不過咱們做爲有節操的開發工程師,應該本着瑞雪的精神看待技術,學習技術,而不是亂♂搞。可是咱們也應該知道,咱們的應用有可能會被別人怎麼搞……
最後推薦一本資料書,你們能夠有空看看。
若是你以爲內容意猶未盡,若是你想了解更多相關信息,請掃描如下二維碼,關注咱們的公衆帳號,能夠獲取更多技術類乾貨,還有精彩活動與你分享~
騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!