週末小實驗 反組譯漫畫人 apk

原本只是在寫爬蟲啊 反組譯看的眼睛好痛QQ

Gordon Ueng
6 min readOct 23, 2018

故事是這樣的

我之前都是用漫畫人在手機上看漫畫

最近發現一個很好用的漫畫 foss android app — Tachiyomi

他是一個漫畫爬蟲大集合

上有各種神奇的 source ,也可以直接看日文生肉

目前中文的來源他只有爬動漫之家

但是最近在重看醫龍 動漫之家沒有

所以想說來開個 pr 把漫畫人加進去

這樣就可以不用看一堆廣告惹(?

沒想到寫一寫還跑去反組譯人家的 apk

來記錄一下這個豆頁痛的過程還有用到的工具

寫一個 Tachiyomi extension

這個 app 模組化的很不錯

所以要自己在新增漫畫的 source 不算太難

使用的是 kotlin ,寫起來比 java 有趣多了

他的 extensions 都在這個 repo

複製現有的 extension 改個名字就可以開始惹

以漫畫人為例,語系是 zh ,名字叫 manhuaren

先修改 src/zh/manhuaren/build.gradle

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'

ext {
appName = 'Tachiyomi: Manhuaren'
pkgNameSuffix = 'zh.manhuaren'
extClass = '.Manhuaren'
extVersionCode = 1
libVersion = '1.2'
}

apply from: "$rootDir/common.gradle"

然後就可以開始在 src/zh/manhuaren/src/eu/kanade/tachiyomi/extension/zh/manhuaren/Manhuaren.kt 寫主要的爬蟲邏輯了

主要有兩種爬法

第一種是直接 parse 漫畫網站的 html,第二種是看他有沒有 api 可以用

分別要繼承 en.kanade.tachiyomi.source.online裡面的 ParsedHttpSource 以及 HttpSource

然後分別實做

{popularManga,latestUpdates,searchManga}{Request,Parse}
{mangaDetails,chapterListParse,pageList,imageUrl}Parse

總共 10 個 method

命名還算是清楚

參考一下別的 extension 很快就能理解他的用途

找出漫畫人的 api

他的 web 只有用到一個 api,其他頁面只能直接 parse html

所以 J 邊我想用漫畫人的 app 去找出其他 api,感覺比較有趣

先用 mitmproxy 開一個 http proxy 讓手機去連

就可以找出他的 api endpoint

以熱門漫畫為例,url 跟 GET 的參數長這樣

url = 'http://mangaapi.manhuaren.com/v2/manga/getCategoryMangas'params = {
'start': '0',
'limit': '20',
'sort': '0',
'subCategoryType': '0',
'subCategoryId': '0',
'gsm': 'md5',
'gft': 'json',
'gts': '2018-10-22+20:46:22',
'gak': 'android_manhuaren2',
'gat': '',
'gaui': '191909801',
'gui': '191909801',
'gut': '0',
'gsn': '3ea9c09bab84b1d25cfdc4e328a1a7bd'
}

試改了一下 startlimit 結果直接 error

仔細觀察就發現gsn 這個參數是隨著其他參數變動的 MD5 hash

於是就需要反組譯一下他的 apk 看看這個 hash 是怎麼產生的

反組譯漫畫人的 apk

這邊使用的是 jadx 這個工具

通常反組譯出來的變數都很醜

像是很多地方變數都叫做 a

又會因為 scope 不同而代表不同的意思

所以 trace 的有點痛苦 QQ

以下是精簡過的流程

一開始先搜尋 gsn 可以找到這個

public class a {
public static final String k = "gsn";
public static final String l = "gsm";
public static final String m = "gft";
public static final String n = "gts";
public static final String o = "gak";
public static final String p = "gat";
public static final String q = "gaui";
public static final String r = "gui";
public static final String s = "gut";

然後用 a.k 下去搜尋

找到他在建構 http get 的參數長這個樣子

bVar.b("gsm", a.w);
bVar.b(a.m, a.x);
bVar.b(a.n, com.ilike.cartoon.module.sync.a.e());
bVar.b(a.o, a.y);
bVar.b(a.p, "");
bVar.b(a.q, com.ilike.cartoon.module.save.z.f() + "");
bVar.b(a.r, com.ilike.cartoon.module.save.z.b() + "");
bVar.b(a.s, com.ilike.cartoon.module.save.z.a() + "");
bVar.b(a.k, a.a(bVar.c()));

最後一行的 a.k 就是 "gsn"

後面的 a.a()就是我們要找的 hash 產生的方法

也就是下面這一段

public static String a(Map<String, String> map) {
Object[] toArray = map.keySet().toArray();
Arrays.sort(toArray);
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(C);
stringBuilder.append("GET");
for (Object obj : toArray) {
stringBuilder.append(obj);
try {
a(map, stringBuilder, obj);
} catch (UnsupportedEncodingException e) {
}
}
stringBuilder.append(C);
return aa.d(stringBuilder.toString());
}

最後把他改成 kotlin 就完成惹

結語

原本以為只是一個簡單的爬蟲

結果花了一整個晚上在研究 a.ab.a 是什麼😢

--

--