實現(xiàn) APK 保護(hù)時常見的坑和解決方案

對 APK 進(jìn)行保護(hù)是我們經(jīng)常需要做的事膀钠,而且似乎也是每個公司必備的技能了。在使用如 ProGuard,DexGuard 等常見的產(chǎn)品之余捏顺,也有很多公司自行研發(fā)了一些保護(hù)的方案核偿,專門來針對自家產(chǎn)品做出保護(hù)诚欠,比如說我司也開發(fā)了專門防止二次打包的工具。

在開發(fā)這款產(chǎn)品漾岳,并用于實戰(zhàn)的過程中轰绵,也發(fā)現(xiàn)了很多坑,下面一一細(xì)數(shù)過來尼荆,希望對同樣也希望開發(fā)一款 APK 保護(hù)類產(chǎn)品的人們能有所啟發(fā)左腔。

坑一: 簽名校驗

本來以為簽名校驗是一件很簡單的事,不就是兩個字符串比較一下么捅儒,但是事實上這么做的話液样,可能會被坑得家都不認(rèn)識振亮,在 Java 層校驗簽名自不必說,反編譯后 smali 代碼一改你就完了鞭莽。而自作聰明把簽名校驗放到 JNI 層也會有問題坊秸,之前我遇到的最典型的問題是 JNI 取簽名會比 Java 取出來的少一位(原因至今不明,也有一些手機(jī)實測下來兩端取到的簽名一樣)澎怒,這樣的簽名比較就永遠(yuǎn)無法通過褒搔。

解決方案:在兩端分別取指定字節(jié)處的數(shù)值,而不是比較整個字符串喷面,比較整個字符串也比較容易被人抓著了星瘾,內(nèi)存中一個長達(dá) 1K 的字符串太容易引起注意了。

坑二:依然是簽名校驗

上面說了一個完整的簽名字符串放在內(nèi)存里面是非常不安全的惧辈,那么怎么才是安全的琳状?

在這里我們需要用到編程語言的一些特性:

class Sig {
  private:
      string c0;
      string c1;
      string c2;
      ...
};

記得每個 string 里面其實只存一到二個字符用來校驗就好了,而且也沒必要把全部字符串存入盒齿,以節(jié)省校驗需要的時間成本(另一方面是 string 對象的開銷也較大念逞,但是為了安全就忍了)。

恩边翁,你問為什么不用 struct肮柜?自己試試就知道了,有一款神器叫 IDA倒彰,一試便知审洞。

坑三:JNI 庫的保護(hù)

辛辛苦苦寫出一個 JNI 庫,用它來校驗 APK 的各種屬性待讳,這是一條不錯的路子芒澜,但是萬一別人把 JNI 剝離了呢? 剝離的方法很簡單创淡,直接刪掉 so 文件痴晦,并且找到加載該 so 的 System.loadLibrary() 語句一并刪除,最后通過編譯找到閃退處琳彩,去掉調(diào)用部分的代碼即可誊酌。那么如何實際防止 JNI 庫被剝離?

這里我的解決方案是用一些黑科技露乏,一方面隨機(jī)生成 so 的加載代碼碧浊,并插入各個類中,以實現(xiàn)隨機(jī)的 so 加載與校驗瘟仿,往往當(dāng)你插入的校驗代碼超過 100 處箱锐,而且每一處的命名與調(diào)用方法都不一樣的時候,反編譯的人就沒啥耐心改了劳较,甚至他會懷疑這個庫是否對其他的業(yè)務(wù)也起到作用驹止。

另一方面浩聋,加載 so 的代碼使用一些變形,比如使用以下代碼:

var a = "l", b = "o", c = "a", d = "d", e = "i", f = "b", g = "r", h = "y", i = "n"
var aa = "j", bb = "a", cc = "v", dd = "n", ee = "g", ff = "s", gg = "t", hh = "e", ii = "m"
var aaa = "."
var x = "$a$b$c$d${a.toUpperCase()}$e$f$r$c$g$h"
var s = Class.forName("$aa$bb$cc$bb$aaa$a$c$dd$ee$aaa${ff.toupperCase()}$h$ff$gg$hh$ii")
var ss = Class.forName("$aa$bb$cc$bb$aaa$a$c$dd$ee$aaa${ff.toUpperCase()}$gg$rr$e$i$ee")
var yy = "$ff$hh"
var v = s.getMethod(x, ss)
v.invoke(null, yy)

然后這段代碼經(jīng)過編譯后臊恋,生成的 smali 代碼是基本上不可能看懂的衣洁,就算一處看懂,還有 N 處抖仅,如果這些變量四散定義在程序各處闸与,并且被多次調(diào)用的話,也是任何人都不敢輕易刪除的岸售,這樣就直接的隱藏了 loadLibrary 的過程。

當(dāng)然這只是一種做法厂画,還有其他的做法凸丸,比如說在其他業(yè)務(wù)相關(guān)的 JNI 里也插入校驗代碼,甚至 JNI 之間實現(xiàn)相互調(diào)用袱院,都可以盡最大可能防止 JNI 被剝離屎慢。關(guān)鍵還是生成的代碼,其變量名稱要隨機(jī)忽洛,盡可能的造成混亂腻惠,否則被找出了規(guī)律就悲劇了,另外生成的代碼結(jié)構(gòu)也盡可能不一樣欲虚,否則容易被 IDE 提示要重構(gòu)(不要懷疑集灌,大部分反編譯的人在搞到代碼后都會重建一個工程然后上 IDE 的),你保護(hù)的意圖也就明顯了复哆。

坑四:smali 代碼注入

講到保護(hù) APK 那必定是要修改 smali 代碼的欣喧,不管以何種形式的保護(hù),都無法避免梯找,而我之前設(shè)計的方案唆阿,由于要注入大量類和方法,因此對 MultiDex 就有了很高的要求锈锤,單純的往 smali 里面注入是行不通的驯鳖,經(jīng)常會出現(xiàn)一個 dex 文件超出 65535 個方法的問題。

解決方案只有一個久免,那就是設(shè)計一個比較牛X的處理類的移動的方法浅辙,先針對一個 dex 內(nèi)的方法數(shù)進(jìn)行判斷,然后加上要注入的方法數(shù)阎姥,看是否超過 65535摔握,若是超過,則需要將一部分注入的內(nèi)容移到后續(xù)的 dex 中丁寄,甚至還需要以 smali_classes* 的形式新建一個 dex氨淌。

在這個過程中我遇到過很多坑泊愧,比如說 Android 5.0 后,可以不用 MultiDex盛正,而是將所有的方法都壓在一個 dex 文件內(nèi)删咱,這個情況下,如果你確定 SDK Target 是 21 以上豪筝,那么可以無視 dex 的要求痰滋,而若是 SDK Target 是 21 以下,那么就必須手動進(jìn)行 dex 拆分续崖。而拆分的時候又要注意敲街,Application 類和用作 Luancher 的 Activity 必須在第一個 dex 內(nèi),于是又多出了要解析 AndroidManifest.xml 的需求严望,而且還要補(bǔ)足 Application 內(nèi)缺失的代碼多艇,比如說以下的:

protected void attachBaseContext(Context base) {  
    super.attachBaseContext(base);  
    MultiDex.install(this);  
}  

坑五:Magic Number

與我溝通過的人都知道,我喜歡用 Magic Number像吻,因為這是可以最大程度讓開發(fā)者自由發(fā)揮的東西峻黍,對 Magic Number 進(jìn)行校驗也是相當(dāng)?shù)淖杂桑牡煤蒙踔量梢詫崿F(xiàn)如下效果:

也就是 zip 格式被破壞了拨匆,無法進(jìn)行解壓姆涩,而 Android 系統(tǒng)依然可以識別這個程序。而尋找 Magic Number 的過程可謂血淚史惭每,一開始取好的地址偏移的數(shù)值骨饿,在不同版本的 Android 上面會帶來不同的解析行為,因此改 zip 頭部并不是一個好主意台腥。在反復(fù)的尋找 Magic Number 可寫的偏移過程中样刷,也并沒有發(fā)現(xiàn)什么可循的規(guī)律,只是知道了某幾個地址可寫览爵。而且也許再下個版本的 APK 就不讓這么寫了置鼻, 找通用的方案實在是自找麻煩。如果不是非常有信心去折騰 Magic Number蜓竹,還是消停點的好箕母。

坑六:在代碼混淆的基礎(chǔ)上繼續(xù)做保護(hù)

如 Proguard 等保護(hù)類產(chǎn)品,會對 APP 的代碼進(jìn)行混淆處理俱济,以實現(xiàn)反編譯后代碼難以讀懂的效果嘶是。而若是還不放心,想在這層保護(hù)上繼續(xù)保護(hù)的話蛛碌,就會面臨很多問題聂喇,比如說類名沖突。原本的類名經(jīng)過混淆后,可能就變成了 abcd 等無意義的字符希太,而我們要注入的代碼也是經(jīng)過了人肉混淆的克饶,很可能還是寫死的,可以設(shè)想一下反編譯后得到 a.java誊辉,而后又注入了一個邏輯完全不同的 a.java 會發(fā)生什么矾湃。

要解決這樣的問題,首先我們要有一套算法堕澄,比如說遍歷要注入的 package邀跃,分析它下面已有的類,然后動態(tài)的去生成自己要注入的類名蛙紫。在這個過程中依然需要注意文件系統(tǒng)的問題拍屑,如果是在 Linux 下執(zhí)行這些操作,你可以在遍歷完大寫字母后坑傅,再次遍歷小寫字母僵驰,而在 Mac 上干這事就不太妙了,除非你把你的 Mac 硬盤做成大小寫敏感的裁蚁,否則很可能要跪。另外再多提一句继准,有些混淆過的 APK 在 Mac 上進(jìn)行反編譯后會有文件缺失的情況枉证,從而無法再進(jìn)行打包,一定程度上歸功于大小寫不敏感的文件系統(tǒng)移必,換到 Linux 上操作就不會丟了室谚。

光是有這種的算法還不夠,如果正好你計算的類是 JNI 的加載類呢崔泵,這個時候類名一變秒赤,JNI 加載一定會失敗。當(dāng)然辦法還是有的憎瘸,比如說根據(jù)生成的類名入篮,重新編譯 JNI 庫,所以通常情況下幌甘,JNI 都是最后才編譯的潮售,根據(jù)注入的代碼的情況收集到一大堆信息,然后才可以弄出 so 來锅风。

額外說幾句酥诽,如果要注入完整的 kotlin 框架以幫助實現(xiàn)讓反編譯器出錯,那么 kotlin 的方法數(shù)大概是 6800 左右皱埠,隨著版本的更新肮帐,方法數(shù)緩慢增加,我自己是直接留了 8000 的空間边器,也就是說當(dāng)前 dex 方法數(shù)加上 8000 是否大于 65535训枢,若大于則直接進(jìn)下一個 dex 繼續(xù)運算托修,這個情況下還是保守一點的好,防止打包失敗肮砾。

另外 Magic Number 的問題诀黍,千萬不要只打一套固定的,容易被人抓了規(guī)律仗处,大部分有經(jīng)驗的人一看 zip 解壓失敗眯勾,就知道你動了手腳了。比較好的辦法是寫一套算法來生成多套 Magic Number婆誓,生次打包都隨機(jī)打其中一套吃环,然后 JNI 可以通過同樣的算法進(jìn)行遍歷校驗。每次在變化的(并且找不出變化規(guī)律的)值也容易對人造成混亂洋幻。

最后的最后郁轻,一句廢話:任何保護(hù)手段都是增加成本,畢竟你的程序還是要能在 Android 系統(tǒng)內(nèi)運行文留,它必須符合系統(tǒng)的規(guī)矩好唯,因此還是會被反編譯的,只是反編譯的成本燥翅,二次打包的成本骑篙,是否在技術(shù)手段下足以完成阻止而已。不要對通用的保護(hù)手段抱太大的希望森书,自己做一套并保持更新才是王道靶端。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市凛膏,隨后出現(xiàn)的幾起案子杨名,更是在濱河造成了極大的恐慌,老刑警劉巖猖毫,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件台谍,死亡現(xiàn)場離奇詭異,居然都是意外死亡吁断,警方通過查閱死者的電腦和手機(jī)典唇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來胯府,“玉大人介衔,你說我怎么就攤上這事÷钜颍” “怎么了炎咖?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我乘盼,道長升熊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任绸栅,我火速辦了婚禮级野,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘粹胯。我一直安慰自己蓖柔,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布风纠。 她就那樣靜靜地躺著况鸣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪竹观。 梳的紋絲不亂的頭發(fā)上镐捧,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機(jī)與錄音臭增,去河邊找鬼懂酱。 笑死,一個胖子當(dāng)著我的面吹牛誊抛,可吹牛的內(nèi)容都是我干的列牺。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼芍锚,長吁一口氣:“原來是場噩夢啊……” “哼昔园!你這毒婦竟也來了蔓榄?” 一聲冷哼從身側(cè)響起并炮,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎甥郑,沒想到半個月后逃魄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡澜搅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年伍俘,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片勉躺。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡癌瘾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出饵溅,到底是詐尸還是另有隱情妨退,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站咬荷,受9級特大地震影響冠句,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜幸乒,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一懦底、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧罕扎,春花似錦聚唐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至宴咧,卻和暖如春根灯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背掺栅。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工烙肺, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人氧卧。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓桃笙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親沙绝。 傳聞我的和親對象是個殘疾皇子搏明,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容