逆向一款收費版的開發工具

qiang_github發表於2018-11-17

0、工具介紹

工具的功能展示圖

工具介紹這款輔助開發工具還是挺有用的,我看應用寶的下載量有幾萬了,當前版本號 3.1.0 。這個應用能夠開啟手機上的一些開發中常用的設定,應用資訊提取,以及反編譯的功能。所以週末的時候研究了一下。應用寶下載地址 。其中我感覺比較有用的功能就是 檢視Activity的歷史,所以想研究下他的檢視Activity的歷史的功能是如何實現的,但是他的檢視Activity歷史的功能是收費的,那我們看一下能不能繞過收費功能。

1、繞過收費功能的基本思路

這個app在我的角度看來有兩個破解點:

第一種思路就是硬剛,看下圖,在點選這些灰色按鈕的時候,普通版這些功能是不可用的,那麼本地在初始化這個介面的時候肯定有驗證當前app是不是已經授權了,如果沒有授權就不可用,那麼我們可以反轉這個驗證過程理論上來說就可以實現破解功能的使用了了(並且這個app不需要登陸,那麼驗證過程肯定也不會太難。)我大概看了下程式碼,這個介面是一個RecyclerView,在初始化的時候會根據授權狀態例項化不同的資料物件,但是這個玩意混淆的有點煩,沒再繼續往下跟蹤。

image.png

第二種破解方案,看下圖,思考一下,這個地方填寫啟用碼,如果啟用碼正確那麼就能啟用,所以說這裡要和伺服器進行通訊。然後伺服器會驗證啟用碼是不是有效,我們可以隨便填一個啟用碼,然後抓包看一下這個啟用動作和伺服器通訊的過程。這個網路協議一般情況下也就是兩種,一種是TCP協議,一種是HTTP協議。先用Charles抓一下看看能不能抓到。

image.png

我隨便填寫了個 1234567890 看下抓包http的請求和返回結果:

Request:
GET /common/v?c=1234567890&p=cn.trinea.android.developertools&v=310&l=zh_CN&t=1541382122260 HTTP/1.1
//-------------------------華麗分割線---------------------------//
Response:
{
	"code": 11,
	"message": "activationCode length is illegal"
}
複製程式碼

看下這個message 它說啟用碼太短,那麼多長才算正常長度呢?後面我買了了個啟用碼,發現這個啟用碼長度為32個字元,正好是個md5的轉成BigInteger的長度。這是後話。。。

這~~~這個授權的請求和和驗證已經算是非常簡單的了,請求和返回資料都是http協議,而且都是明文,而且請求體沒有校驗引數。這裡猜測一下哈,客戶端拿到這麼個請求結果就能判斷是不是已經啟用。理論上來說啟用成功和啟用失敗的返回的json格式是一致的,當然也不排除會有不同的情況存在。先不管,先按啟用成功和失敗的返回結構一樣來猜測,那麼一般我們用返回的code來判斷。這個code一般為錯誤碼,而且一般來說0,1,-1 是有很特殊含義的。這裡我們嘗試用0,1,-1 來替換上面的11 。(這裡只是按照一種正向開發的方式推測)

那麼怎麼能讓手機收到被修改後的json呢?我用的Charles的URL的對映功能 選單欄Tools —>Map Local Settings,然後新增一條對映規則,大概長這麼個樣子: 

image.png

那個ss.json 檔案裡面我們放如下字串:

{
	"code": 0,
	"message": "activationCode length is illegal"
}
複製程式碼

其實到這裡呢,這個app就已經破解完了,理論上來說付費功能就可以使用了。能這麼做主要是其一本地驗證太過於簡單,而且驗證的地方太少就一次驗證。

2、修改反編譯後的Smali程式碼繞過授權驗證

理論上來說所有的發起網路請求都點選啟用按鈕開始的,那麼我們就從啟用的這個按鈕下手,看下啟用按鈕觸發後都進行了什麼操作,以及app是如何處理網路返回的資料的。

通過ADM 找到,這個啟用按鈕的 id,如下圖所示:

image.png

然後去 public.xml 裡面找到 name 為a1 並且 type 為id的值 然後在as 裡面全域性搜尋下id 這個值,發現並不多,一共就是四個,如下圖:

AS中全域性搜尋 0x7f09001b 的結果

找一下規律,這個16進位制的數全域性一共能夠搜尋到四處,雖然這四個在四個不同的檔案裡面,但是他們對應的變數名都是一樣的都是active,那麼依然是全域性搜尋:active

Smali中全域性搜尋 active 的結果

然後我們就通過這個按鈕id的引用鎖定了這個按鈕的點選事件,其實尋找點選事件的方式很多,針對從這個app的角度來說,如果你輸入一個字元,會有一個toast,說“啟用碼長度不能小於9位”,然後可以從這個提示入手,依然能定位到 d.b.a.c.h.b.a 這個類。如果從尋找點選事件的角度來說的話,通過Xposed或者 通過Android Device Monitor 裡面的 Method Profiling 方法都能獲取到。

//.class public Ld/b/a/c/h/b/a; (d.b.a.c.h.b.a)類裡面
    public void onClick(View view) {
        if (this.g.a()) {
            int id = view.getId();
            if (id == this.c.getId()) {
                d.b.a.c.j.a.a(this.e, "pud", true);
                d.b.a.c.h.a.a.a(getActivity());
            } else if (id == this.b.getId()) {
                dismiss();
            } else if (id == d.b.a.c.h.j.a.active) {    //下面是核心程式碼
                Object obj = this.a.getText().toString();
                if (!TextUtils.isEmpty(obj)) {
                    String trim = obj.trim();
                    if (!TextUtils.isEmpty(trim) && trim.length() >= 10) {   //這裡會判斷輸入的字串長度
                        i b = b();    //關鍵的就是他了
                        if (b == null) {
                            ad.a(this.e, c.payable_is_null, 1);    //這應該是個log
                            CrashReport.postCatchedException(new TrineaUploadException("payable is null in DonationVersionDialogFragment", new NullPointerException()));
                            return;
                        }
                        b.d(trim);    //呼叫 i 的 d方法也就是 c.b.c 類裡面的d(String str)方法
                        dismiss();
                    }
                }
                ad.b(this.e, c.code_length_tip, Integer.valueOf(9));   //提示啟用碼不能小於9位
            }
        }
    }
複製程式碼

c.b.c 類裡面的d(String str)方法

如果本地簡單的驗證成功會把字串發往伺服器,網路請求是這個類。看裡面的網路成功的回撥,這個回撥匿名物件對應的Smali 檔案為.class Ld/b/a/c/h/a$1;

網路請求及其返回

找到 Smali對應的檔案,先反轉這個if(找到 Smali裡面的程式碼 把if-eqz改成if-nez),我們試試能不能破解。測試發現反轉了if就能破解了、然後就沒有必要再分析其他程式碼的含義了。重新打包簽名就可以使用了。

注意:apktool重打包會失敗。解決方法看後面

d.b.a.c.h.a$1.smali

處理到這一步,其實已經算是破解了收費功能,但是他裡面還有個啟用碼長度限制,那麼這裡購買一個真正的啟用碼看看,發現他是32個字元。哈哈32個字元~恩 md5。那這裡其實自己隨機生成一個字串,md5一下就可以,然後把我們的程式碼插入到 d.b.a.c.h.a(String,String,b) 方法開始的時候,修改第二個引數。就可以了。

然後我們破解這個10的限制,基本流程如下:

  • 這個10 (Smali 裡面搜尋 0xa) 有兩個地方驗證到了、一個是:圖形化介面 d.b.a.c.h.b.a 一個是網路請求類 d.b.a.c.h.a
  • 我們把驗證長度都改成1(也就是0x1),這樣只要隨便輸入一個字串就能驗證成功了
  • 但是好像這樣有bug,也就是本地還有地方用到了這個啟用碼,並且對長度有限制
  • 那也好說,我們傳送的時候,把使用者輸入的啟用碼md5一下。
  • 然後就可以啦

上面的難點是如何修改原來的程式碼,把攔截使用者輸入的啟用碼,修改後再傳送給伺服器。

我們在AndroidStudio中建立一個工具類,把包名改成上面和上面的類同一個包名

package d.b.a.c.h;

import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * Author: liuqiang
 * Date: 2018-11-03
 * Time: 13:42
 * Description:獲取一個字串的md5的字串的表現形式類似於:
 * 914674d1b303467d54c0673893a19ab3 個形式
 */
public class bbch {

    public static String getHash(String s) {
        try {

            MessageDigest md5 = MessageDigest.getInstance("MD5");
            md5.update(s.getBytes("UTF-8"));
            byte[] md5Array = md5.digest();
            //byte[]通常我們會轉化為十六進位制的32位長度的字串
            return new BigInteger(1, md5Array).toString(16);
        } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
            return "1234567890";
        }
    }
}
複製程式碼

我們把這個類通過as編譯成Smali檔案,然後在d/b/a/c/h/a類的a方法的開頭處引用如下程式碼:

#d.b.a.c.h.a 類裡面的方法
.method public a(Ljava/lang/String;Ljava/lang/String;Ld/b/a/c/b;)V
    .locals 8

    const/4 v0, 0x0
#在這裡引用我們自定義的工具類
    invoke-static {p2}, Ld/b/a/c/h/bbch;->getHash(Ljava/lang/String;)Ljava/lang/String;
#從而修改第二個引數的值
    move-result-object p2

    .line 57
    #.....省略其他程式碼
.end method
複製程式碼

如果翻譯成java程式碼如下圖所示

注入自己程式碼後的樣子

然後再重新打包簽名,執行app。選擇 使用支付寶購買-->填寫任意啟用碼--->點選啟用按鈕。就能使用啦~~~

3、 遇到的幾個錯誤

重打包失敗。這個我還真的不知道啥原因,不過猜測是因為這個app的資源混淆的問題。即使 使用apktool d -r xxx.apk命令不反編譯資原始檔重打包執行之後也是出錯,那麼只好去用Smali/baksmali 去搞了。

具體步驟如下:

//前提條件 會 baksmali/smali 的基本操作
//用zip的方式解壓apk
//把 classes.dex 單獨 搞出來 
//然後執行 baksmali d classes.dex -o out
//然後classes.dex 會被反編譯成 Smali檔案存放到 -o 指定的目錄
//修改 out 中的對應檔案
//然後把Smali編譯成dex 執行: smali a out/ -o ./out/classes.dex
//然後把out 資料夾中的 classes.dex 覆蓋上面解壓的apk中的dex
//然後刪除原來的簽名檔案
//然後用zip壓縮這些檔案,重新命名apk
//然後簽名這個apk
複製程式碼

4、學到了一個混淆操作

我們知道一般情況下Activity的子類是不能混淆的,但是呢這句話說的不完全。確切的說應該是在Manifest檔案中註冊的Activity是不能混淆的。因為Manifest檔案中要寫一個Activity的class的路徑的字串。如果原始的類被混淆了,而字串沒有修改,那麼Android系統在做安全驗證的時候就會找不到Activity,那麼就沒辦法通過安全驗證。但是設想一下如果我們的繼承關係是這個樣子的:

MainActivity--->BaseActivity ---->Activity
複製程式碼

那麼試問,這個BaseActivity是不是可以混淆,經過驗證這個是可以混淆的。因為BaseActivity 不需要在Manifest檔案中註冊,而MainActivity需要。那麼我們變通一下。把這個繼承鏈加長

a--->MainActivity--->BaseActivity ---->Activity
複製程式碼

這個時候,a 類的實現是這個樣子的:

public class a extends MainActivity {
}
複製程式碼

其實a裡面啥也沒有,就是個佔位符,這個a並不是混淆生成的,而是我們本來就把這個類命名為a。並且在Manifest檔案裡面我們就註冊這個a類。那麼試問這個時候,MainActivity 和 BaseActivity是不是就可以參與混淆。而這個時候我們的所有業務依然在MainActivity裡面實現,a僅僅是個看起來像是混淆名字的佔位符而已。。。。

5、總結一下

其實這個app去除授權碼校驗很簡單,通過Charles代理就可以解決,但是這個app還是有很多可以借鑑的地方的,看下知識點:

  • 網路抓包的工具的使用
  • Charles 對映修改網路返回的資料
  • 逆向過程中如何定位點選事件的實現類
  • Android Device Monitor 的使用
  • AndroidStudio全域性搜尋功能
  • baksmali/smali 的使用,和重打包的方式
  • 這個應用的Activity“混淆了”,這裡所謂的混淆是一種偽的混淆
  • 如何在原來的程式碼中插入自己的程式碼

然後就可以反編譯程式碼學習他是如何記錄Activity的歷史的了。

最後

本文的目的只有一個就是學習逆向分析技巧關注Android產品的安全。從分析上來看,混淆還是很有必要的,好像這個應用裡面還做了資源混淆。但是這個app沒有做簽名驗證,沒有做網路加密,http協議用的明文,請求引數沒有做校驗,應用沒有加固。如果我們想要更好的防護自己的app可以從這幾方面下手。如果有人利用本文技術進行非法操作帶來的後果都是操作者自己承擔,和本文以及本文作者沒有任何關係。開發不易如果感覺工具好用,請聯絡作者購買。

相關文章