序
我一朋友最近在玩國(guó)內(nèi)某三四線廠商的不知名手游斩芭,跟我說(shuō)他已經(jīng)充了不少錢了,但最近有個(gè)充值活動(dòng)看起來(lái)很誘人,不知道要不要參加赘阀。我?guī)еp蔑的語(yǔ)氣回復(fù)他道:“這種破游戲有什么好充錢的,我分分鐘給你破解掉厂镇!”纤壁。于是就開(kāi)啟了這段hack之旅。(打臉之旅)
人生贏家
安裝之后發(fā)現(xiàn)這游戲?qū)崟r(shí)性要求不是特別高捺信,類似于幾年前流行的卡牌養(yǎng)成類游戲酌媒。目測(cè)網(wǎng)絡(luò)請(qǐng)求用的是http,于是打開(kāi)了Charles試著抓包看一下迄靠。果不其然秒咨,用的甚至都不是https。
看到結(jié)尾的aspx我猜這家公司很早以前應(yīng)該是做網(wǎng)站的掌挚,后來(lái)轉(zhuǎn)型做的游戲雨席。在我個(gè)人的認(rèn)知里,現(xiàn)在好像很少看到有公司用asp寫后臺(tái)以及用windows做服務(wù)器了吠式。
接著看這些接口都返回了什么陡厘。
在這里先介紹幾種常見(jiàn)的加密方式:des、aes特占、rsa糙置、base64、md5是目、sha谤饭。其中des和aes是對(duì)稱加密,所謂對(duì)稱加密是指加密的密鑰和解密的密鑰是同一個(gè)。rsa為非對(duì)稱加密揉抵,加密的密鑰和解密的密鑰并不相同亡容,https就使用了rsa。base64則是一種編碼方式冤今,更多的是幫助二進(jìn)制文件通過(guò)http傳輸闺兢。md5和sha應(yīng)該算是信息摘要,得到的結(jié)果一般是不可逆的戏罢。
上面這串東西末尾有兩個(gè)等號(hào)列敲,顯然是用base64編碼過(guò)了。找個(gè)在線網(wǎng)站直接解碼一下帖汞,我們得到了這么個(gè)東西戴而。
看來(lái)他們還對(duì)數(shù)據(jù)做了一層加密,我第一反應(yīng)覺(jué)得可能只是將數(shù)據(jù)對(duì)一串密鑰進(jìn)行了異或運(yùn)算(異或加密)翩蘸,因?yàn)檫@樣實(shí)現(xiàn)起來(lái)最簡(jiǎn)單所意。不管怎樣,這時(shí)我們都需要取到加密的密鑰才解密了催首。
既然網(wǎng)絡(luò)請(qǐng)求返回的都是密文扶踊,客戶端要解密,本地一定存有一份密鑰郎任。所以接下來(lái)我決定對(duì)客戶端進(jìn)行反編譯秧耗。因?yàn)楸旧硎莂ndroid開(kāi)發(fā),對(duì)android也比較熟悉舶治,就去他們官網(wǎng)下載了android客戶端分井。因?yàn)閍pk本質(zhì)上就是一個(gè)壓縮包,直接把.apk改成.zip霉猛。解開(kāi)后找到后綴為.dex的文件尺锚。android應(yīng)用最后是運(yùn)行在Dalvik虛擬機(jī)上的,他與jvm類似惜浅,而dex文件就相當(dāng)于jar包瘫辩。dex文件閱讀源碼并不方便,所以在這里我用了dex2jar把dex轉(zhuǎn)換成jar包坛悉,然后用jd-gui來(lái)預(yù)覽源碼伐厌。
整個(gè)過(guò)程比我想象的要順利的多,反編譯之后所有代碼一覽無(wú)遺裸影,這家公司甚至連混淆都沒(méi)做挣轨。不做混淆的結(jié)果就是我一眼就注意到了AesUtil.class這個(gè)類。原來(lái)他們采用的是aes加密空民。
可以看到密鑰寫在了Constants這個(gè)類里刃唐。到此為止,我就成功解開(kāi)了他們對(duì)網(wǎng)絡(luò)請(qǐng)求的所有加密界轩,這時(shí)我的內(nèi)心是這樣的:
陷入困境
接下來(lái)把抓包得到的密文解密后看到了他們請(qǐng)求的數(shù)據(jù)格式画饥,偽造一份重新加密后發(fā)送過(guò)去,但返回給我的卻是失敗浊猾《陡剩看了下請(qǐng)求頭User-Agent那邊設(shè)置了他們app的名字。設(shè)置好UA后重新發(fā)送請(qǐng)求葫慎,這次終于拿到了正確的數(shù)據(jù)衔彻。其實(shí)我覺(jué)得用UA做防御好像沒(méi)什么用,都抓你包來(lái)請(qǐng)求接口了偷办,肯定也會(huì)看到請(qǐng)求頭啊艰额。但后來(lái)仔細(xì)想了想他們應(yīng)該不是用來(lái)防御的,是用來(lái)區(qū)分應(yīng)用的???椒涯。
本來(lái)是想在代碼里直接找到他們所有的網(wǎng)絡(luò)接口的柄沮,但發(fā)現(xiàn)他們用了一個(gè)叫做corona的引擎,大概就是用lua寫android和ios游戲废岂,然后他們的網(wǎng)絡(luò)請(qǐng)求都是用lua寫的祖搓,雖然最后肯定還是會(huì)被編譯成c的二進(jìn)制文件或者是java的class文件,但我一時(shí)半會(huì)兒沒(méi)找到在哪兒湖苞,而且找到了可讀性可能也很差拯欧,所以還是決定通過(guò)抓包來(lái)測(cè)試他們的接口。
游戲第一天簽到會(huì)贈(zèng)送道具财骨,使用后可以增加10萬(wàn)金幣镐作,我們先從這個(gè)接口入手,看看能不能讓我們一夜奔小康隆箩。
根據(jù)字段的名稱可以知道cmd是指令滑肉,num是使用的數(shù)量,propID是道具的ID摘仅,guid是用戶id靶庙,cTime是時(shí)間戳,但是位數(shù)好像不太對(duì)娃属,測(cè)試后發(fā)現(xiàn)是減去了2016/01/01/0:00的時(shí)間戳六荒,hmVer比較重要,可以看到respose會(huì)返回一個(gè)hmVer且等于請(qǐng)求加一矾端,這個(gè)是他們服務(wù)器做的驗(yàn)證掏击,下次請(qǐng)求的hmVer需是上一次Response中的hmVer,但是經(jīng)過(guò)測(cè)試發(fā)現(xiàn)-999是一個(gè)特殊的hmVer可以無(wú)視這個(gè)規(guī)則秩铆,這應(yīng)該是他們?yōu)榱朔奖懔舻囊粋€(gè)后門吧砚亭。
很遺憾他們對(duì)數(shù)據(jù)做了一些判斷灯变,隨后我試了下num = 0,num = -1以及別的一些異常數(shù)據(jù)捅膘,也試了別的幾個(gè)接口添祸,他們的后端對(duì)于一些邊界條件和異常數(shù)據(jù)都做了處理還是比較細(xì)心的,看來(lái)接口這條路是走不通了寻仗,這時(shí)我的內(nèi)心是這樣的:
出現(xiàn)轉(zhuǎn)機(jī)
當(dāng)然有了所有的網(wǎng)絡(luò)接口已經(jīng)可以寫個(gè)自動(dòng)掛機(jī)的腳本了刃泌,但只是自動(dòng)掛機(jī)的話,我要怎么做天下第一署尤?耙替??回去繼續(xù)看源碼曹体,尋找別的突破口俗扇,這時(shí)看到了這么一段代碼。
看方法名就知道這是用來(lái)測(cè)試支付的箕别,顯然我手上這個(gè)包不會(huì)執(zhí)行到這段代碼狐援,我們得通過(guò)一些手段來(lái)逆天改命野瘦。
用dex2jar反編譯的包是不能修改代碼的帅矗,也不能重新編譯回去基协,所以這時(shí)候我們需要另一個(gè)工具:apktool
他能幫你把a(bǔ)pk反編譯成Dalvik運(yùn)行時(shí)所需的字節(jié)碼勋篓。我們通過(guò)修改字節(jié)碼來(lái)改變程序運(yùn)行的流程甚垦。Dalvik運(yùn)行的字節(jié)碼是.smali格式的利花,相當(dāng)于jvm的.class文件抄囚。在修改之前我們先補(bǔ)充一點(diǎn)smali的知識(shí)微酬。
Dalvik 是基于寄存器的绘趋,而寄存器是cpu的一部分,提供高速且有限的存儲(chǔ)颗管,用于暫存指令陷遮,數(shù)據(jù)和地址。在smali中p表示參數(shù)寄存器垦江,v表示本地寄存器帽馋。其中對(duì)于非靜態(tài)方法,p0是這個(gè)類的引用指針(這和java是一樣的比吭,這也是為什么在非靜態(tài)方法中能直接操作這個(gè)類绽族,因?yàn)槌钟辛怂闹羔槹∥梗銓慾ava時(shí)感知不到是因?yàn)榫幾g器幫你傳了這個(gè)東西)
下面這張表格展示了java的基本類型在smali中對(duì)應(yīng)的表示:
java | small |
---|---|
boolean | Z |
byte | B |
short | S |
char | C |
int | I |
long | J |
float | F |
double | D |
void | V |
class | L |
array | [ |
smali調(diào)用方法的幾種方式:
- invoke-static 顯而易見(jiàn)衩藤,調(diào)用靜態(tài)方法
- invoke-interface 顯而易見(jiàn)吧慢,調(diào)用接口
- invoke-direct 看名字直接調(diào)用,其實(shí)是指private方法和construct方法
- invoke-virtual 調(diào)用虛函數(shù)赏表,什么鬼检诗?其實(shí)每一個(gè)對(duì)象都關(guān)聯(lián)了一張?zhí)摵瘮?shù)表里面有他可以調(diào)用的一些public匈仗、protected、default(package作用域)方法
- invoke-super 這個(gè)是從他父類關(guān)聯(lián)的虛函數(shù)表里面去找方法
反編譯后找到支付方法所在的類逢慌,并找到支付這個(gè)方法(注意下我加的注釋)
.method public pay(Lxxx/PayParams;)V #參數(shù)包名xxx/PayParams悠轩,返回void
.locals 5 #申請(qǐng)了5個(gè)本地寄存器
.param p1, "data" #非靜態(tài)方法p0存著這個(gè)類的引用,p1是第一個(gè)參數(shù)涕癣,這里標(biāo)注了下,第一個(gè)參數(shù)名字為data
.prologue #標(biāo)記了程序起始位置
....
return-void #方法都是要有返回值的前标,在java中void不用寫return坠韩,是因?yàn)榫幾g器幫你添加了
.end method```
我們?cè)诔绦蜷_(kāi)始處直接調(diào)用測(cè)試支付的方法并且讓程序直接返回
```java
.method public pay(Lxxx/PayParams;)V
.locals 5 #這行可以去掉,我們并不需要本地寄存器來(lái)存東西
.param p1, "data" # Lxxx/PayParams;
.prologue
#調(diào)用私有方法checkPayResultTest
invoke-direct {p0, p1}, Lxxx/Pay;->checkPayResultTest(Lxxx/PayParams;)V
return-void #直接返回了炼列,后面代碼不執(zhí)行
....
.end method```
好了只搁,現(xiàn)在使用apktool重新build代碼,并重新對(duì)apk進(jìn)行簽名就能安裝了俭尖,安裝好之后進(jìn)商城點(diǎn)充值然后boom:
![](http://upload-images.jianshu.io/upload_images/2782970-e9a2d6ceaaf0ad9e.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
報(bào)了一個(gè)錯(cuò)氢惋,不慌,這是因?yàn)闆](méi)有在ui線程操作ui組件造成的稽犁。我們修改字節(jié)碼讓其在ui線程運(yùn)行就可以了焰望。但是這里我犯了一個(gè)錯(cuò)誤,我一開(kāi)始沒(méi)仔細(xì)看pay方法下面的字節(jié)碼已亥,其實(shí)下面是有調(diào)用到這個(gè)方法的熊赖。
```java
runOnUiThread(new Runnable() {
@Override
public void run() {
checkPayResultTest();
}
});
因?yàn)槲覀僴ew的Runnable是一個(gè)匿名內(nèi)部類,所以編譯器會(huì)自動(dòng)在pay下幫你生成一個(gè)靜態(tài)方法虑椎,以及這個(gè)內(nèi)部類震鹉,這和jvm是一樣的,都是從0開(kāi)始捆姜,依次遞增传趾,并且這個(gè)靜態(tài)方法和內(nèi)部類的數(shù)字是對(duì)應(yīng)的。
.method static synthetic access$1(Lxxx/CKPay;Lxxx/PayParams;)V
.locals 0
.prologue
.line 292
invoke-direct {p0, p1}, Lxxx/Pay;->checkPayResultTest(Lxxx/PayParams;)V
return-void
.end method```
所以對(duì)應(yīng)的我們找到Pay$1.smali泥技,這個(gè)命名規(guī)則與java是一樣的浆兰,類名 + $ + 內(nèi)部類名稱。
打開(kāi)后看到run方法非常長(zhǎng)珊豹,大概有300多行镊讼,所有的支付邏輯都在這里面了。本著幫他們優(yōu)化一下程序性能的想法平夜,我把300多行的代碼優(yōu)化成了3行:
virtual methods
.method public run()V
.locals 2 #這邊兩個(gè)本地寄存器就夠了
.prologue
#獲取到Pay的引用并且賦值到v0
iget-object v0, p0, Lxxx/Pay$1;->this$0:Lxxx/Pay;
#獲取到外部傳進(jìn)來(lái)的參數(shù)并賦值到v1
iget-object v1, p0, Lxxx/Pay$1;->val$tempData:Lxxx/PayParams;
#調(diào)用編譯器幫你生成的那個(gè)靜態(tài)方法
invoke-static {v0, v1}, Lxxx/Pay;->access$1(Lxxx/Pay;Lxxx/PayParams;)V
return-void
好了現(xiàn)在看著干凈多了蝶棋,支付這一塊的性能和內(nèi)存占用也得到了極大的提升, 我們重新構(gòu)建apk并且簽名『龆剩可能有的同學(xué)不會(huì)用命令行來(lái)簽名玩裙,我們先執(zhí)行以下代碼來(lái)生成一個(gè)簽名文件:
```shell
keytool -genkey -v -keystore 簽名文件名稱.keystore -keyalg RSA -keysize 2048 -validity 10000 -alias 別名
然后執(zhí)行以下代碼對(duì)apk進(jìn)行簽名:
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore 簽名文件名稱.keystore APK名稱.apk 別名
簽名完成后我們重新安裝一下兼贸,記得把之前安裝的先刪掉,包名相同簽名不同是裝不進(jìn)去的吃溅,adb install -r都不行溶诞。
選擇成功后彈出,請(qǐng)稍后决侈,待訂單驗(yàn)證成功后重新登錄螺垢,然后就沒(méi)有然后了。
尾聲
當(dāng)然故事到了這里并沒(méi)有結(jié)束赖歌,在閱讀他們?cè)创a的時(shí)候發(fā)現(xiàn)他們的客戶端程序猿還做了一件我所不能理解的事情——把他們的運(yùn)營(yíng)后臺(tái)網(wǎng)址寫進(jìn)了代碼里枉圃,于是我又試著從他們運(yùn)營(yíng)后臺(tái)找突破口。先試了下sql注入庐冯,并不起效果孽亲,然后打開(kāi)了Chrome的控制臺(tái),刷新了下頁(yè)面展父。
并沒(méi)有返回他們服務(wù)器的信息返劲,但可以看到cookie里有設(shè)置一個(gè)open.session.id應(yīng)該是通過(guò)這個(gè)id來(lái)判斷是否登錄的,是一串md5值栖茉,然而并沒(méi)有什么卵用篮绿。
然后我又點(diǎn)開(kāi)了Chrome控制臺(tái)的source標(biāo)簽看了下都有些什么文件,根據(jù)目錄結(jié)構(gòu)我感覺(jué)服務(wù)器是nodejs或者python的可能性比較大吕漂,然后看了下js基本都是些jquery和bootstrap的庫(kù)搔耕,直到common下兩個(gè)js文件引起了我的注意。
注釋里寫著所有權(quán)歸這家公司所有痰娱,還有作者的昵稱弃榨。然后我去搜索了一下作者的昵稱,搜到了一個(gè)博客梨睁。翻閱了一下最早的一篇文章是2008年的鲸睛,也就是說(shuō)這個(gè)作者至少是工作了8年的程序員。其中有一篇文章是作者的一個(gè)開(kāi)源項(xiàng)目坡贺,是基于多個(gè)別的開(kāi)源項(xiàng)目封裝的快速開(kāi)發(fā)平臺(tái)官辈,在github還有著4000+的star。然后在文檔里巨細(xì)無(wú)遺的寫了從后端到前端各個(gè)層級(jí)具體使用了什么技術(shù)遍坟。而且我還看到了和我反編譯的apk所相似的包名前綴拳亿,所以我可以肯定這位程序員是那家公司的員工,而且前端技術(shù)選型里所提到的幾個(gè)框架還有庫(kù)和他們運(yùn)營(yíng)后臺(tái)所用到的一摸一樣愿伴,所以他應(yīng)該是把他們運(yùn)營(yíng)后臺(tái)給開(kāi)源了吧???肺魁。順帶提一下,common下的兩個(gè)js都是些工具類隔节,而且在登陸頁(yè)只是引用了這兩個(gè)文件鹅经,我并沒(méi)有找到使用他們的地方寂呛。
這次hack之旅到這里就告一段落了,如果還要繼續(xù)下去的話瘾晃,可能我會(huì)去看一下他開(kāi)源的代碼贷痪。哦,對(duì)了蹦误,在他項(xiàng)目的文檔里還給了個(gè)測(cè)試的賬號(hào)密碼劫拢。我試了下并沒(méi)有登上去,但我相信賬號(hào)肯定是對(duì)的强胰。
總結(jié)
這個(gè)故事告訴我們這么幾個(gè)道理:
- 打包時(shí)一定要對(duì)代碼進(jìn)行混淆舱沧,必要的話最好再加上殼
- 密鑰什么的不要寫死在代碼里,可以選擇放在native的so包里哪廓,或者存本地?cái)?shù)據(jù)庫(kù)也行
- 單元測(cè)試還是有必要的狗唉,并且盡量把測(cè)試用例補(bǔ)全
- 不要為了方便而留后門
- 用不到的代碼都刪掉或者注釋掉
- 一些重要的邏輯都放到服務(wù)端去處理
- 不要把運(yùn)營(yíng)后臺(tái)地址寫進(jìn)代碼里初烘,而且運(yùn)營(yíng)后臺(tái)不要和app共用一個(gè)地址涡真,最好切斷外網(wǎng)訪問(wèn)運(yùn)營(yíng)后臺(tái)的途徑。
- 不要隨隨便便立flag
把運(yùn)營(yíng)后臺(tái)地址寫到代碼里真的是很危險(xiǎn)的事肾筐,如果登進(jìn)去了我不僅能對(duì)他們游戲數(shù)據(jù)做一些修改哆料,還有可能拿到他們整個(gè)數(shù)據(jù)庫(kù)的數(shù)據(jù)。一般情況下數(shù)據(jù)庫(kù)里前幾的賬號(hào)都是他們內(nèi)部人員的賬號(hào)吗铐,而這些賬號(hào)很有可能與他們的百度網(wǎng)盤东亦、支付寶、qq之類是同一個(gè)唬渗,我甚至能拿到他們公司比較機(jī)密的數(shù)據(jù)典阵。
最后說(shuō)兩句
其實(shí)在和朋友打賭之前,我完全沒(méi)有做過(guò)這種嘗試镊逝,我只是用自己這兩年儲(chǔ)備的知識(shí)和經(jīng)驗(yàn)作出對(duì)應(yīng)的分析和思考壮啊。大概過(guò)程是這個(gè)樣子的:利用網(wǎng)絡(luò)來(lái)改自己數(shù)據(jù)->抓包->有=號(hào)用base64解解看->解不出,還有一層加密->客戶端也要解->去反編譯客戶端->得到密鑰->測(cè)試他們接口->不可行->還有源碼撑蒜,去源碼找找線索->找到測(cè)試支付代碼->需要改字節(jié)碼歹啼,不太會(huì)->查語(yǔ)法,自己先寫個(gè)demo試一下->動(dòng)手改座菠,重新安裝->不可行->接著去源碼找線索->找到運(yùn)營(yíng)后臺(tái)->去運(yùn)營(yíng)后臺(tái)找線索->找到作者博客->找到后臺(tái)所用技術(shù)
整個(gè)過(guò)程還是挺流暢的狸眼,我想說(shuō)的其實(shí)是遇到問(wèn)題不要慌,去找對(duì)應(yīng)的解決方法就好了浴滴,如果找不到就換一個(gè)角度拓萌,那句話怎么說(shuō)來(lái)著,條條大路通羅馬升略。其實(shí)也不是特別難司志,有點(diǎn)工作經(jīng)驗(yàn)的程序員都能做到攻破一個(gè)做的不是很好的網(wǎng)站或者app甜紫,但這不是這篇文章的目的。我希望大家引以為戒骂远,養(yǎng)成良好的編碼習(xí)慣囚霸,不要留給別人可趁之機(jī)。