Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 手把手教你逆向分析 Android 程序

手把手教你逆向分析 Android 程序

編輯:關於Android編程

很多人寫文章,喜歡把什麼行業現狀啊,研究現狀啊什麼的寫了一大通,感覺好像在寫畢業論文似的,我這不廢話,先直接上幾個圖,感受一下。
\

第一張圖是在把代碼注入到地圖裡面,啟動首頁的時候彈出個浮窗,下載網絡的圖片,蒼老師你們不會不認識吧?

第二張圖是微信運動步數作弊,6不6?
ok,那我們從頭說起

1.反編譯

Android 的反編譯,相信大家都應該有所了解,apktool、JEB 等工具
我們先看一下 Apk 文件的結構吧,如下圖:

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 編譯打包過程分析參看:http://blog.csdn.net/luoshengyang/article/details/8744683)
\

apk的核心邏輯主要在 classes.dex 中,破解和二次打包也基本上對這個文件做手腳,所以對這個文件的保護也尤為重要。
vc+06qOpPC9wPg0KPHA+ztLDx8rXz8jTwyBhcGt0b29sILmkvt+3tLHg0uujumphdmEgLWphciBhcGt0b29sLmphciBkIC1mIHh4eC5hcGsgb3V0RGlyKFBTOm91dERpciCyu9C0u+HU2rWxx7DEv8K8yuSz9ik8YnIgLz4NCre0seDS67rztcTEv8K8veG5ucjnz8KjujxiciAvPg0KPGltZyBhbHQ9"" src="/uploadfile/Collfiles/20160517/20160517090128282.png" title="\" />

這裡,res 裡的 xml 和 manifset.xml 都已經是解出後的 xml 了,不是 axml 格式了,res 目錄裡的 values 目錄下的 public.xml 可以看到資源對應的 id

如果命令 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

<code><notepadplus>
  <userlang name="smali" ext="smali">
    <settings>
      <global caseignored="no">
    </global></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">
    </wordsstyle></wordsstyle></wordsstyle></wordsstyle></wordsstyle></wordsstyle></wordsstyle></wordsstyle></wordsstyle></wordsstyle></wordsstyle></wordsstyle></wordsstyle></wordsstyle></styles>
   </userlang>
</notepadplus>
</code>

可以參看 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 跟蹤修改,服務器驗證的抓包查看,再模擬發包重放攻擊就好了,這裡就不具體介紹了)

2. Android 的簽名保護機制到底是什麼?

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

EXIT

看一下 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-Digest: GVIfdEOBv4gEny2T1jDhGGsZOBo=

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

SHA1-Digest: KKqaLh/DVvFp+v1KoaDw7xETvrI=

用私鑰通過 RSA 算法對 manifest 裡的摘要信息進行加密,安裝的時候只能通過公鑰解密,解密之後才能獲得正確的摘要,再對比

3.最後就是如何生成 CERT.RSA,打開這個文件看到的是亂碼,說明整個文件都被編碼加密了,而且這個文件和公鑰有關
從源碼中看出他是通過 PKCS7 將整個文件加密了

總結:1.簽名只是對完整性和簽名發布機構的校驗機制 2.不能阻止 apk 被修改,只是簽名無法保持一致 3.不同私鑰對應著不同的公鑰,實質上不同的公鑰就代表了不同的簽名

3. Android 系統如何獲取簽名

我們從獲取下面一段代碼開始分析:

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, 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 裡就好了。

4. 關於如何注入?

開篇的時候有個圖片就是我在騰訊地圖裡面注入了一個蒼老師的圖片其實就是,自己寫了個 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.網絡抓包:通過抓包得到的關鍵字段,在代碼中定位

寫在後面:
這篇文章整理了有一段時間了,覺得還是應該寫出來,也不是什麼高深的技術文章,就是個人總結的一點心得而已。

關於破解應用很多人可能會去破解別人的應用注入廣告來獲取利益,也有可能盜取別人的信息。

不過我們作為有節操的開發工程師,應該本著瑞雪的精神看待技術,學習技術,而不是亂♂搞。但是我們也應該知道,我們的應用有可能會被別人怎麼搞……

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved