最近下載了某直播應用,其中某些直播間需要收費才能觀看如圖所示:
看到這里,激起了我濃厚的興趣,試圖將其破解。
1. 查找切入點
首先使用jadx打開這個apk,運氣很好沒有加固,甚至沒有混淆。
點擊文件-另存為Gradle項目,就可以在Android Studio中打開了,這樣使用順手的IDE可以更方便查看。
當然,這個項目是不能通過編譯的,但是可以方便我們接下來的修改。
根據(jù)開頭的對話框提示,可以查找其中的字符串來找到彈出這個對話框的位置。全局查找“本房間為收費房間”,發(fā)現(xiàn)并沒有結果。
再找“您可預覽”,這次找到了。
可以看到,在2個類中出現(xiàn)了這個字符串,分別是com.yunbao.phonelive.presenter.CheckVideoListPresenter
和com.yunbao.phonelive.presenter.LiveRoomCheckLivePresenter
。沒有混淆真的是太幸運了,根據(jù)類的名字就可以猜測出多半是第二個類,即LiveRoomCheckLivePresenter
中檢測收費情況。打開這個類,并且找到這個提示出現(xiàn)的地方,分別是類成員mHandler
和方法payTips()
中。
首先看payTips()
方法。這個方法只在LiveAudienceActivity
中調用過。看到這里,推測這個應用是使用的MVP結構,在Presenter中處理業(yè)務邏輯,而LiveRoomCheckLivePresenter
和LiveAudienceActivity
分別就是P和V。
查看調用payTips()
方法的地方:
發(fā)現(xiàn)是在LiveAudienceActivity
的checkLive
方法中創(chuàng)建LiveRoomCheckLivePresenter
時,通過一個Listener的回調傳入的。所以問題還是回到了LiveRoomCheckLivePresenter
這個類中。在LiveRoomCheckLivePresenter
中找到這個Listener,并查找其onLivePay回調的調用。順藤摸瓜查找下去:
↑↑↑注意,這里很重要,通過服務器返回的json數(shù)據(jù),獲取直播間類型mLiveType,根據(jù)這個類型來判斷是普通房間、收費房間還是密碼房間。
而checkLive
方法是在LiveAudienceActivity
的onCreate
中就調用,即進入這個Activity之后,就會調用這個checkLive
方法。所以理一下邏輯:
LiveAudienceActivity#onCreate()
-> LiveAudienceActivity#checkLive() // 進入Activity,檢查直播狀態(tài)
-> LiveRoomCheckLivePresenter#checkLive() // 調用Presenter來處理業(yè)務邏輯
-> HttpCallback#onSuccess() // 向服務器請求當前直播間數(shù)據(jù),通過請求到的判斷是否收費
-> LiveRoomCheckLivePresenter#forwardPayRoom() // 當前直播間為收費房間,進入收費邏輯
-> mActionListener#onLivePay() // 當前直播間為收費房間,進入收費邏輯
-> LiveRoomCheckLivePresenter#payTips() // 彈出收費對話框
通過理清以上邏輯,不難看出,需要修改的地方就是HttpCallback了。只要在這個判斷中,將收費房間變成普通房間即可。接下來就是喜聞樂見的改代碼時間。
2. 修改目標代碼
在1中已經知道了,在HttpCallback回調的switch判斷中動一下手腳就能達到目的,看看源代碼:
private HttpCallback mCheckLiveCallback = new HttpCallback() {
public void onSuccess(int code, String msg, String[] info) {
if (code != 0) {
ToastUtil.show(msg);
} else if (info.length > 0) {
JSONObject obj = JSON.parseObject(info[0]);
LiveRoomCheckLivePresenter.this.mLiveType = obj.getIntValue("type");
LiveRoomCheckLivePresenter.this.mLiveTypeVal = obj.getIntValue("type_val");
LiveRoomCheckLivePresenter.this.mLiveTypeMsg = obj.getString("type_msg");
switch (LiveRoomCheckLivePresenter.this.mLiveType) {
case 0:
LiveRoomCheckLivePresenter.this.forwardNormalRoom();
return;
case 1:
LiveRoomCheckLivePresenter.this.forwardPwdRoom();
return;
case 2:
case 3:
LiveRoomCheckLivePresenter.this.forwardPayRoom();
return;
default:
return;
}
}
}
public boolean showLoadingDialog() {
return true;
}
public Dialog createLoadingDialog() {
return DialogUitl.loadingDialog(LiveRoomCheckLivePresenter.this.mContext);
}
};
也就是說,在這個switch語句中
switch (LiveRoomCheckLivePresenter.this.mLiveType) {
case 0:
LiveRoomCheckLivePresenter.this.forwardNormalRoom();
return;
case 1:
LiveRoomCheckLivePresenter.this.forwardPwdRoom();
return;
case 2:
case 3:
LiveRoomCheckLivePresenter.this.forwardPayRoom();
return;
default:
return;
}
可以考慮以下幾個方案:
- 將
forwardPayRoom
和forwardPwdRoom
替換為forwardNormalRoom
即可; - 或者直接去掉這段判斷,直接調用
forwardNormalRoom
方法。 - 再或者,甚至可以修改
forwardPayRoom
的實現(xiàn):
public void forwardPayRoom() {
forwardNormalRoom();
}
當然,異曲同工不提。
將apktool.jar和待分析的xml.apk放在同一文件夾下,執(zhí)行java -jar apktool.jar d xml.apk
,apk被反編譯為一堆smali文件。當然我是看不懂smali代碼的,不過只用替換一下的話還是不難辦到。找到名為LiveRoomCheckLivePresenter.smali的文件,使用notepad++打開。Ctrl+F查找“forwardPayRoom”,發(fā)現(xiàn)一共有兩處:
顯然,第一處是這個方法的定義,略過不看;第二處這里應該就是上面的switch語句了。雖然看不懂代碼,但是可以看出上面三段幾乎是一模一樣的,對應了switch的三個條件;那么把下面調用forwardPayRoom
方法和forwardPwdRoom
方法的地方都改成forwardNormalRoom
不就行了嗎?事實證明的確可行。
PS:事實上,mCheckLiveCallback作為一個匿名類實現(xiàn),對應文件LiveRoomCheckLivePresenter$1.smali。其中包含了onSuccess方法的實現(xiàn),有100多行。其中switch的部分大概如下:
可以看出,在switch的三種情況下面,分別調用了
LiveRoomCheckLivePresenter
的access$300
、access400
、access$500
,即是LiveRoomCheckLivePresenter.smali中的那三個相似的方法。
3. 重新打包
修改保存之后,使用java -jar apktool.jar b xml
重新打包。然后進行簽名即可。
Apktool重打包Apk詳細介紹
將重新打包好的apk安裝好,發(fā)現(xiàn)能夠正常進入付費房間,并且之后不會彈出充值提示,說明修改成功。
4. 反思
本文示例了一個最基本最簡單的靜態(tài)反編譯重打包的例子。作為開發(fā)者,如何避免自己開發(fā)的應用被破解,或者使破解的成本大大增加?這是一個很大的話題,完全能夠寫一本書出來了。不過一些最基本的東西也是很有效的,如果能夠做到也算是加了多層保險了:
- 混淆
- 加固
- NDK
- 敏感操作的字符串替代
- 檢查簽名
- ……
能夠做到這些,雖不能保固若金湯,但也能讓相當一部分的破解者也只能望而卻步了。