主角:java
描述:android
思路:shell
工具:服務器
其餘:網絡
爲了方便截手機的圖,寫了一個下面的 bat 腳本架構
@echo off set file_name=/sdcard/sc_%RANDOM%.png adb shell screencap -p %file_name% adb pull %file_name% . adb shell rm %file_name%
過程:app
先在 Android Killer 中打開該 APK,打開 Manifest,看到 application 標籤裏沒有 android:debuggable="true"
屬性(release 版本默認是沒有的),添加上後保存(Android Killer 不會自動保存),而後編譯,adb install "C:\Program Files\AndroidKiller\projects\CrackMe\Bin\CrackMe_killer.apk"
將其安裝在手機上。ide
打開應用,使用一下,點擊一個視頻,嘗試拖動,會獲得以下信息工具
主要信息,「不能快進更多」,轉 Unicode 後用 Android Killer 搜索,結果以下post
很幸運,有且只有一處,VideoView$1.smali,是一個內部類,推斷是個拖動的監聽器之類的,Android Killer 定位到代碼位置,向上看代碼,尋找代碼分支,首先看到的是
可是調用的參數是 videoisfinish
,用這個去判斷最大時長,感受不大對,以 cond_1
做爲線索繼續向上, 不難看出來,這是一個或判斷,跟到
cond_1
後發現其調用了 invoke-virtual {p1, v0, v1}, Lcn/jzvd/JZMediaInterface;->seekTo(J)V
是進度調整代碼無疑了,隨便更改或判斷的一處便可,這裏把 if-le p1, v0, :cond_1
改成 goto :cond_1
。
Android Killer 保存,編譯,從新安裝。
OK,能夠拖動了,可是發現課程拖動完後外面的課程總進度並無變(這多是犯的第一個大錯,已經沒有辦法再驗證了,進度條之因此沒變是由於課程數目多,兩節課過短因此並無計入),看來不只在這裏作了校驗,打開 JEB,Bytecode/Hierarchay,定位到 VideoView,Decompile。該類擴展自 JZVideoPlayerStandard,雙擊查看源碼,包 cn.jzvd,像是國人作的庫,Google 之,果真,GitHub 上的開源項目。速覽一遍 VideoView(後來證實在這裏犯了第二個大錯,主要驗證代碼以及服務器同步應該都是在這裏)沒有什麼發現,大體認爲是對父類方法的重寫(有一點能夠證實我在這裏的推測是錯誤的,上面對於進制跳轉的代碼是在這裏作的驗證)。
類名上 x 查看交叉引用,除了自身就指向 VideoPlayerActivity,如今把主要精力集中在 VideoPlayerActivity,通讀代碼,其使用 SharedPreference 獲取與保存一些數據,網絡相關的都是些可有可無的操做,並無進度同步的代碼,這讓我很不解,惟一的一處不知道內部作了什麼的就只有 jcVideoPlayerStandard 這個 VideoView 對象(因此爲何不進去看看?)。
第一個思路進行不下去了。
下來看一看 jiaozivideoplayer 的 ReadMe,發現其提供了倍速播放的功能,在於 JZMediaManager.setSpeed(.)
方法,打開 JEB 查看方法列表,發現 JZMediaManager 並無 setSpeed(.) 方法
推測是版本不一致,打開 GitHub,現版本爲 6.x,打開 JZMediaManager,確實有 setSpeed(.) 方法,定位至 5.x 版本,發現 5.x 版本的時候還叫 JCMediaManager,6.2 版本是一年前的,此時也確實沒有調速方法。
發現文檔裏有一句,基於 MediaPlayer。可是 MediaPlayer 6.0+ 起就提供了調速功能,嘗試直接修改播放器代碼達到倍速效果,使用 Android Killer 搜索 MediaPlayer
,與 JZVideoPlayer 有關的以下
很遺憾,JZMediaPlayer 並無建立MediaPlayer 對象,驚喜的是 JZMediaSystem 裏有 MediaPlayer 對象
在 JZMediaSystem 的 prepare(.) 開始處添加代碼倍速播放代碼
invoke-virtual {v0}, Landroid/media/MediaPlayer;->getPlaybackParams()Landroid/media/PlaybackParams; move-result-object v1 const/high16 v2, 0x41200000 # 10.0f invoke-virtual {v1, v2}, Landroid/media/PlaybackParams;->setSpeed(F)Landroid/media/PlaybackParams; move-result-object v1 invoke-virtual {v0, v1}, Landroid/media/MediaPlayer;->setPlaybackParams(Landroid/media/PlaybackParams;)V
保存,編譯,安裝,發現並無用。用 Android Studio 動態調試一下 Smali 代碼,給 prepare() 打上斷點,發現該方法根本沒有調用
方案二失敗。
Burp Suite 抓包,發現接口參數都異常簡單,決定本身寫一個簡單的工具直接模擬 Android 端調用服務器接口,這個方法比較簡單。
須要格外注意這裏的請求頭,我的不喜歡用 OkHttp,因此本身的封裝請求類以下,主要是模擬請求頭的添加(失敗重試機制沒有放在這裏是一個很大的失誤)
package com.seliote.crackcjyykt.network; import com.seliote.crackcjyykt.exception.SetCookieParseException; import com.seliote.crackcjyykt.util.Constants; import com.seliote.crackcjyykt.util.HttpUtils; import com.seliote.crackcjyykt.util.Pair; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; /** * Package: com.seliote.crackcjyykt.network * FileName: CjyyktHttpUtils * Describe: CJYYKT 專用 Http 請求工具,自動設置請求頭與 Cookie * Author: seliote * Email: seliote@hotmail.com * Date: 2018/12/30 14:48 */ @SuppressWarnings("WeakerAccess") public class CjyyktHttpUtils { private final Map<String, String> COOKIE_MAP; public CjyyktHttpUtils() { COOKIE_MAP = new LinkedHashMap<>(); } /** * 獲取已存儲的全部 Cookie * @return 已存儲的全部 Cookie */ @SuppressWarnings("unused") public Map<String, String> getCookies() { return COOKIE_MAP; } /** * 以請求頭中字符串形式返回全部 Cookie 的字符串表示 * @return 請求頭中字符串形式返回全部 Cookie 的字符串表示 */ public String getCookieString() { StringBuilder stringBuilder = new StringBuilder(); for (Map.Entry<String, String> entry : COOKIE_MAP.entrySet()) { //noinspection StringConcatenationInsideStringBufferAppend stringBuilder.append(entry.getKey() + "=" + entry.getValue() + ";"); } if (stringBuilder.length() > 1) { return stringBuilder.substring(0, stringBuilder.length() -1); } return stringBuilder.toString(); } /** * 添加 Cookie * * @param aKey Cookie 鍵值 * @param aValue Cookie 值 */ public void addCookie(@NotNull String aKey, @NotNull String aValue) { if (aValue.equals("") || aValue.equals("\"\"")) { deleteCookie(aKey); return; } COOKIE_MAP.put(aKey, aValue); } /** * 刪除 Cookie * * @param aKey 要刪除的 Cookie 名 * @return 已刪除的 Cookie 的值,若是不存在,返回 null */ @SuppressWarnings({"UnusedReturnValue"}) @Nullable public String deleteCookie(@NotNull String aKey) { return COOKIE_MAP.remove(aKey); } /** * 模擬 CJYYKT Android 端 Get 方式請求 * * @param aUrl 請求的 URL * @return 服務器的返回值 * @throws IOException 鏈接或讀取異常 */ public String get(@NotNull String aUrl) throws IOException { Pair<Map<String, List<String>>, byte[]> pair = HttpUtils.get( aUrl, 10000, 10000, generateHeader("GET") ); handleHeader(pair.getFirst()); return new String(unGzip(pair.getSecond()), StandardCharsets.UTF_8); } /** * 模擬 CJYYKT Android 端 Post 方式請求 * * @param aUrl 請求的 URL * @param aPostBody 請求體 * @return 服務器的返回值字符串形式 * @throws IOException 鏈接或讀取異常 */ public String post(@NotNull String aUrl, @Nullable String aPostBody) throws IOException { Pair<Map<String, List<String>>, byte[]> pair = HttpUtils.post( aUrl, 10000, 10000, generateHeader("POST"), aPostBody, StandardCharsets.UTF_8 ); handleHeader(pair.getFirst()); return new String(unGzip(pair.getSecond()), StandardCharsets.UTF_8); } public String post(@NotNull String aUrl, @Nullable Map<String, String> aPostBodyMap) throws IOException { return post(aUrl, mapToPostBody(aPostBodyMap)); } /** * Map 生成 key1=value1&key2=value2 相似形式的字符串,轉換完成後清空參數 Map * @param aPostBody 須要格式化的參數 Map * @return 格式化後的字符串 */ public String mapToPostBody(Map<String, String> aPostBody) { StringBuilder result = new StringBuilder(); for (Map.Entry<String, String> entry : aPostBody.entrySet()) { result.append(entry.getKey()); result.append("="); result.append(entry.getValue()); result.append("&"); } // 可千萬別放下面了 aPostBody.clear(); if (result.length() > 1) { // substring(..) 返回的是一個 String... return result.substring(0, result.length() - 1); } return result.toString(); } /** * 生成請求頭 * * @param aMethod POST 或 GET * @return 生成的請求頭 */ public Map<String, String> generateHeader(String aMethod) { // 這個值和 Cookie 裏的 SESSION 相等,可是不知道他爲何要單獨寫一個請求頭? String headerSession = COOKIE_MAP.getOrDefault(Constants.Api.Header.SESSION, ""); String headerCookie = Constants.Api.Header.COOKIE_JLXCKID + "=" + COOKIE_MAP.getOrDefault(Constants.Api.Header.COOKIE_JLXCKID, "") + ";SESSION=" + COOKIE_MAP.getOrDefault(Constants.Api.Header.COOKIE_SESSION, ""); Map<String, String> map = new LinkedHashMap<>(); map.put(Constants.Api.Header.SESSION, headerSession); map.put(Constants.Api.Header.COOKIE, headerCookie); map.put(Constants.Api.Header.IP, Constants.Api.Header.IP_VALUE); map.put(Constants.Api.Header.VERSION, Constants.Api.Header.VERSION_VALUE); map.put(Constants.Api.Header.CONNECTION, Constants.Api.Header.CONNECTION_VALUE); // OkHttp 自動設置 Content-Encoding: GZIP 並解壓返回值,如今須要本身去解壓返回值 map.put(Constants.Api.Header.ACCEPT_ENCODING, Constants.Api.Header.ACCEPT_ENCODING_VALUE); map.put(Constants.Api.Header.USER_AGENT, Constants.Api.Header.USER_AGENT_VALUE); // POST 請求的話須要多一個 Content-Type 請求頭 if (aMethod.equalsIgnoreCase("POST")) { map.put(Constants.Api.Header.CONTENT_TYPE, Constants.Api.Header.CONTENT_TYPE_VALUE); } return map; } /** * 處理請求頭,讀取並添加 Cookie * * @param aHeader HttpUtils.get(...) 或 HttpUtils.post(...) 返回的 Pair 的請求頭部分 */ @SuppressWarnings("UnnecessaryContinue") public void handleHeader(Map<String, List<String>> aHeader) { // 沒有直接取出 Set-Cookie List,由於以後可能還要作其餘處理 for (Map.Entry<String, List<String>> entry : aHeader.entrySet()) { // HTTP 響應的第一行響應頭信息被存在了 null => ${value} 中 if (entry.getKey() == null) { continue; } else if (entry.getKey().equalsIgnoreCase("Set-Cookie")) { String regex = "^([^=]*)=([^;]*).*$"; Pattern pattern = Pattern.compile(regex); for (String setCookie : entry.getValue()) { Matcher matcher = pattern.matcher(setCookie.trim()); if (matcher.matches()) { addCookie(matcher.group(1).trim(), matcher.group(2).trim()); } else { throw new SetCookieParseException("Set-Cookie 匹配出錯:" + setCookie); } } } } } /** * 解壓 GZIP 壓縮的數據 * * @param aBytes 須要解壓的 byte[] * @return 解壓後的 byte[] * @throws IOException 讀取出錯時拋出 */ public byte[] unGzip(byte[] aBytes) throws IOException { // 若是是在抓包,BurpSuite 會自動解壓縮 Gzip 流 if (Constants.Config.DEBUG) { return aBytes; } ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(aBytes); GZIPInputStream gzipInputStream = new GZIPInputStream(byteArrayInputStream); byte[] buffer = new byte[1024]; int length; while ((length = gzipInputStream.read(buffer)) != -1) { byteArrayOutputStream.write(buffer, 0, length); } byte[] result = byteArrayOutputStream.toByteArray(); byteArrayOutputStream.close(); byteArrayInputStream.close(); gzipInputStream.close(); return result; } }
固然這也是須要抓包的,啓動類還須要加上這個才行
if (Constants.Config.DEBUG) { System.setProperty("http.proxyHost", "127.0.0.1"); System.setProperty("https.proxyHost", "127.0.0.1"); System.setProperty("http.proxyPort", "8888"); System.setProperty("https.proxyPort", "8888"); }
而後就是獲取數據循環調用接口,除了發現接口命名混亂外基本沒有什麼其餘問題了,實際測試也是正常的,能夠刷課了。
總結: 原本簡簡單單的 Android 逆向硬是被本身的幾個重大失誤搞成了這樣,之後必定耐心看完代碼再下結論。還有就是要結合抓包和逆向,單搞一個信息不徹底