最近想了解下移動端app是如何與服務端進行交互的,就順手下了一個某app抓下http包。誰知抓下來的http居然都長這個模樣:
POST /ca?qrt=***LoginHTTP/1.1
Content-Type:application/x-www-form-urlencoded
Content-Length:821
Host:client.XXX.com
Connection:Keep-Alive
c=B946D7CF7B9E5B589F8DE6BC9E5DACF08DA6B35DA7A899DCA5AD5A58ADDAD5E66363589EF2D385B1ACACABDAD5E65F63589EF2D3F5B43EA7ACE36DFCA5383EABE7D4F1403E379FF0DEFCAD9FABA7E6E1F243A7AAAC74E380A4A09E6593D3FEA4ABA8ACF0DDF0AEAAB3A7EAE9FBA4A09E5F97D3FEA49EA09E9B97A9A4B69EADE7E1F6B0AC9EA0DA94A95E57609EF2D3B55E659EA0DA94B55F9EB69EDAD5E668689EB6DA98AA6E576E62939DE6A69E616D868CB673636162DAEBE6AEA2ACA2E486F3AD9EA09EA898A0A4B69EABE8E1F3B29EA09EA998A0A4B69EADE3E385AE3DA7ABDB6FF4B93A9F3DEFE3FBA537ACAD77D486AD37ADABE77383B4B4ACB4DAD5E66E9EB69EA886AF634061599F97E6A69E676394D3FEA4ACACACE8E1F4B2ACACACE8E1F4B2AC9EA0DA9CAAA4B69E9EDCD3B269589EB6DADFF4B2ACABACE3E9E675&b=AD178E267CA8666F636D6C54ADC1B3AEAD736574696950776D71A9BEACADA7A8727762A8C0AA67656172736A6D676F706E6369837F726C71A5AAA4756C656963ADC1A6797870615E676E7063666C756CAC7E&ext=&v=alex
這是我在嘗試登陸時抓包獲取的唯一http包,顯而易見POST數據中的c參數是包含登錄信息的,但是為什么長這個模樣?為了得到答案,我開啟了我一周的Android動態調試和靜態分析學習之旅。
這篇文章將通過這段字符串原文的過程,向各位介紹幾種非常好用的Android調試工具以及它們的一些簡單用法。
拿到一個apk最常規的做法應該是就是,反編譯查看一下java源碼了。
用apktool反編譯得到smali(其實主要是為了看AndroidManifest.xml):
/*我用的apktool是2.0.0-Beta9,命令和常見的1.x版本的命令有所不同*/
apktool d XXX.apk -o ./doc
/*我用的apktool是2.0.0-Beta9,命令和常見的1.x版本的命令有所不同*/
apktooldXXX.apk-o./doc
然后用的dex2jar工具將apk反編譯為jar,并通過JD-GUI來查看java源碼:
dex2jar.shXXX.apk
在apktool反編譯的目錄中我們可以翻看AndroidManifest.xml來了解apk文件的基本結構,我從中首先找到主Activity的所在:
<activityandroid:configChanges=”keyboardHidden|orientation”android:exported=”true”android:name=”com.XXX.NoteActivity”android:screenOrientation=”portrait”android:[email protected]:style/Theme.Black.NoTitleBar.Fullscreen”><intent-filter><actionandroid:name=”android.intent.action.MAIN”/><categoryandroid:name=”android.intent.category.LAUNCHER”/><categoryandroid:name=”android.intent.category.MULTIWINDOW_LAUNCHER”/></intent-filter>
可以從上面的內容看出主Activity是com.XXX.NoteActivity這個類所定義的,然后通過JD-GUI打開dex2jar反編譯后的jar包,查看NoteActivity。
先來看onCreate中的操作,發現使用produard對apk進行了混淆了。這種混淆雖然不會影響我們反編譯出來的代碼內容,但是由于對類名、函數名、變量名進行了隨機命名,導致我們閱讀代碼的過程比較痛苦。
我的目的很明確,就是定位到處理提交數據的代碼,沒有必要去花大量的時間來閱讀被混淆的代碼,所以我決定使用動態跟蹤程序運行的軌跡來定位我想要獲得的代碼。
雖然要用動態的方法來定位,但是還是需要簡單的閱讀java源碼來確定提交數據的大概處理方式。
我的運氣還是不錯,網絡傳輸部分的代碼并沒有被混淆。大體看了一下這些代碼,發現和一般的app一樣,客戶端和服務端的數據交互也是使用json的格式進行的,并且使用了阿里開源的fastjson類來處理json內容。
了解了以上的這些情況,我決定通過跟蹤JSONObject這個類來定位處理提交數據的位置。
這里推薦一個分析app的神器——Andbug,雖然不能用來單步調試,但是動態跟蹤app中各種線程調用棧、類調用棧、方法調用棧,斷點獲取當前內存中變量內容等功能還是非常實用的。
廢話不多說了,來看操作吧,先獲取要動態分析的app進程ID:
adb shell ps
進程ID 445,使用Andbug掛載該進程,并使用classes命令查找fastjson類的全路徑:
andbug shell -p 435 classes JSONObject andbug shell -p435 classes JSONObject
這里提示一個使用classes和method命令查找的小技巧。我們在Andbug的shell環境下使用classes時很容易由于class過多而導致沒辦法看到所有的class。這是我們可以在終端環境下使用classes命令配合more來一點點的查看,就像這樣: andbug classes -p 435|more
然后我們使用class-trace命令來對這個類進行跟蹤:
class-trace com.alibaba.fastjson.JSONObject
在app中隨便觸發一個會提交請求的事件,調用過程在終端中完美的呈現了出來:
可以看到調用過程都用到了com.XXX.net.task.CommonTask中的方法,打開這個類的java源碼第一眼就看到這段代碼:
#!java
protectedHttpEntity buildHttpEntity()
{
if(this.hostUrl.indexOf(“?”)>0)
this.hostUrl=(this.hostUrl+“&qrt=”+this.networkTask.param.key.getDesc());
while(true)
{
Stringstr=String.valueOf(this.networkTask.param.ke);
StringBuilderlocalStringBuilder1=newStringBuilder();
localStringBuilder1.append(“c=”+chrome(str));
localStringBuilder1.append(“&”);
StringBuilderlocalStringBuilder2=newStringBuilder(“b=”);
BaseParamlocalBaseParam=this.networkTask.param.param;
SerializerFeature[]arrayOfSerializerFeature=newSerializerFeature[1];
arrayOfSerializerFeature[0]=SerializerFeature.WriteTabAsSpecial;
localStringBuilder1.append(NetworkParam.convertValue(JSON.toJSONString(localBaseParam,arrayOfSerializerFeature),str));
if((this.networkTask.param.param instanceofHotelBookParam))
{
HotelBookParamlocalHotelBookParam=(HotelBookParam)this.networkTask.param.param;
if(localHotelBookParam.vouchParam!=null)
dealVouchRequest(localStringBuilder1,localHotelBookParam.vouchParam);
}
localStringBuilder1.append(“&”);
localStringBuilder1.append(“ext=”+NetworkParam.convertValue(XXXApp.getContext().ext,str));
localStringBuilder1.append(“&v=alex”);
this.networkTask.param.url=localStringBuilder1.toString();
try
{
StringEntitylocalStringEntity=newStringEntity(this.networkTask.param.url);
returnlocalStringEntity;
this.hostUrl=(this.hostUrl+“?qrt=”+this.networkTask.param.key.getDesc());
}
catch(UnsupportedEncodingExceptionlocalUnsupportedEncodingException)
{
cl.m();
}
}
returnnull;
}
結合之前的抓包,這應該就是我要找的地方了。從中找到處理c參數的代碼,看到調用了com.XXX.net.task.AbstractHttpTask.chrome對參數值進行了處理,跟進chrome方法:
#!java
protectedStringchrome(StringparamString)
{
JSONObjectlocalJSONObject=gcc(paramString);
Stringstr=“60001058″.substring(0,4)+“lex”;
returnNetworkParam.convertValue(localJSONObject.toString(),str);
}
繼續趕進到convertValue方法:
#!java
publicstaticStringconvertValue(StringparamString1,StringparamString2)
{
if(TextUtils.isEmpty(paramString1))
return "";
if(paramString2==null)
paramString2=“”;
try
{
Stringstr=URLEncoder.encode(Goblin.e(paramString1,paramString2),“utf-8″);
returnstr;
}
catch(ThrowablelocalThrowable)
{
localThrowable.printStackTrace();
}
return "";
}
感覺的勝利的曙光越來越近了,這個Goblin.e應該就是最后的加密方法了吧,誰知打開這個文件(內心一萬只草泥馬在狂奔):
#!java
packageXXX.lego.utils;
importcom.XXX.XXXApp;
publicclassGoblin
{
static
{
try
{
System.loadLibrary(“goblin_2_5″);
return;
}
catch(UnsatisfiedLinkErrorlocalUnsatisfiedLinkError1)
{
try
{
System.load(“/data/data/”+XXXApp.getContext().getPackageName()+“/lib/lib”+“goblin_2_5″+“.so”);
return;
}
catch(UnsatisfiedLinkErrorlocalUnsatisfiedLinkError2)
{
}
}
}
publicstaticnativeStringSHR();
publicstaticnativeStringd(StringparamString1,StringparamString2);
publicstaticnativeStringdPoll(StringparamString);
publicstaticnativeStringda(StringparamString);
publicstaticnativeStringdn(byte[]paramArrayOfByte,StringparamString);
publicstaticnativebyte[]dn1(byte[]paramArrayOfByte,StringparamString);
publicstaticnativeStringduch(StringparamString);
publicstaticnativeStringe(StringparamString1,StringparamString2);
publicstaticnativeStringePoll(StringparamString);
publicstaticnativeStringea(StringparamString);
publicstaticnativebyte[]eg(byte[]paramArrayOfByte);
publicstaticnativeStringes(StringparamString);
publicstaticnativeintgetCrc32(StringparamString);
publicstaticnativeStringgetPayKey();
publicstaticnativeStringve(StringparamString);
}
居然把加密方法寫到了so文件中!難道要去看ARM匯編?
既然這個so文件中有加密函數,那是不是就應該有解密函數,那我應該還是可以偷懶的吧。
我們在上面看到的e函數肯定是用來加密的,那那個d函數是不是用來解密的(encode和decode)?
自己本地創建一個app,并且創建一個XXX.lego.utils包,添加一個Goblin.java文件,把我們剛剛看到的Goblin源碼粘貼進去。然后在app的一個Activity中導入Goblin,并在OnCreate中調用d函數來嘗試解密。部分代碼如下:
#!java
Stringc=“B946D7CF7B9E5B589F8DE6BC9E5DACF08DA6B35DA7A899DCA5AD5A58ADDAD5E66363589EF2D385B1ACACABDAD5E65F63589EF2D3F5B43EA7ACE36DFCA5383EABE7D4F1403E379FF0DEFCAD9FABA7E6E1F243A7AAAC74E380A4A09E6593D3FEA4ABA8ACF0DDF0AEAAB3A7EAE9FBA4A09E5F97D3FEA49EA09E9B97A9A4B69EADE7E1F6B0AC9EA0DA94A95E57609EF2D3B55E659EA0DA94B55F9EB69EDAD5E668689EB6DA98AA6E576E62939DE6A69E616D868CB673636162DAEBE6AEA2ACA2E486F3AD9EA09EA898A0A4B69EABE8E1F3B29EA09EA998A0A4B69EADE3E385AE3DA7ABDB6FF4B93A9F3DEFE3FBA537ACAD77D486AD37ADABE77383B4B4ACB4DAD5E66E9EB69EA886AF634061599F97E6A69E676394D3FEA4ACACACE8E1F4B2ACACACE8E1F4B2AC9EA0DA9CAAA4B69E9EDCD3B269589EB6DADFF4B2ACABACE3E9E675″;
Stringp2=“6000lex”;
Stringtest=Goblin.d(c,p2);
System.out.println(test);
天不遂人愿啊!解密出錯,看來真的要去看ARM匯編了。。。。。。
由于app自帶的加密數據,我們不知道原來的樣子,所以要自己構造一個字符串加密,來調試。修改上面的app代碼如下:
#!java
Stringjson=“json{/”test/”:/”test1/”,/”test4/”:/”test1/”,/”test5/”:/”test1/”,/”test6/”:/”test1/”,/”test3/”:/”test1/”,/”test2/”:/”test1/”,/”test1/”:/”test1/”}”;
Stringp2=“6000lex”;
Stringtest=Goblin.e(c,p2);
System.out.println(test);
生成代碼如下:
C0990850B69C969E92C19C9DC5CDD9F0FAB99AD3960CE3F6D2CFB0AA9AE904F6C0A099A68DFFD0C1EE978CA985CAD2FCF6CD92A7C4FCE106CCC99CC39C11F7F3D0A9BDAD8AE3E0D9C3BC898EB8DCD3F2BB8B8BB3C010D9DAFAB99AD3960FE30CD2CFB0AA9AEC04E0C0A099A68DFFD0D7EE978CA985CED2B5
好了準備活動完成了,下面我們開始動態跟蹤之旅吧。在《Android軟件安全與逆向分析》中提供的動態分析工具是IDA pro 6.1以上版本,這個我在調試過程中發現加載很慢。雖然加載完成后,能夠跟著IDA生成的流程圖來調試很爽,但是加載成功率實在是太低了。所以,我放棄了用IDA進行動態調試,而是選擇了 這個號稱移動端Onllydbg的gikdbg來進行調試,同時配合IDA的流程圖。
gikdbg使用參考《gikdbg.art系列教程2.1-調試so動態庫》這篇blog很容易上手,這里也就不多說了。
調試跟蹤過程很枯燥,也沒什么可以說的,我們直接看結過吧。
通過反復的動態跟蹤,確定下面這個循環是加密的關鍵:
可以看出加密方法比較簡單,對于源數據的每一個字符與0×45進行異或,然后jia0x24,最后再加上硬編碼在so文件的一串key中的一個字符。根據匯編逆向出來的python代碼如下:
result=""
i=0
while(i<len(json)):
char=json[i]^69
j=i
ifj>len(key):
j=j%len(key)
encode=int(ord(char))+36+key[i]
result+=encode
i+=1
在加密完數據后,會在數據頭部添加一個8個字符(32位)的校驗數據,校驗算法使用的是adler32。由此可以推出解密算法,代碼如下:
defdecode():
ejson=‘B69C969E92C19C9DC5CDD9F0FAB99AD3960CE3F6D2CFB0AA9AE904F6C0A099A68DFFD0C1EE978CA985CAD2FCF6CD92A7C4FCE106CCC99CC39C11F7F3D0A9BDAD8AE3E0D9C3BC898EB8DCD3F2BB8B8BB3C010D9DAFAB99AD3960FE30CD2CFB0AA9AEC04E0C0A099A68DFFD0D7EE978CA985CED2B5′
key=‘cBHO06GYkxNModVyAtXiGzlPETyS5KUL8gE4′
i=0
result=”
while(i<len(ejson)):
j=i/2
char=ejson[i:i+2]
ifj<len(key):
k=key[j]
else:
k=key[j%len(key)]
i=i+2
c=int(char,16)-int(ord(k))
ifc<0:
c+=128
c=c-36
ifc<0:
c+=128
c=c^69
dchar=chr(c)
result+=dchar
printresult
decode()
其中ejson中的內容為,我們使用e函數加密后獲得的內容剔除前八位,解密效果如下:
不過悲劇的是用這個解密方法沒辦法解密前面我抓包獲取的數據。。。。。。
郁悶之心無以言表啊!!!
不過這個過程還是很有意義的,了解了Android各種姿勢的動態調試方法。這里再次回顧一些這個過程。
首先通過反編譯獲取smali和java代碼進行靜態分析,發現代碼被混淆后,明確自己的最終目標——找到處理提交請求的方法,然后進行動態跟蹤。動態跟蹤和靜態分析結合定位出處理提交請求的幾個類,翻看這些類的代碼,來找到最終我們想找的方法。
在發現處理方法使用了so文件中的函數,通過自己構造app來分別調用so中的各個函數,試圖從中找到直接的解密函數。
在so中沒有找到解密函數的情況下,通過動態調試與靜態查看匯編,分析出加密算法,并寫出解密工具。
【1】《Assembly Programming Principles》 【2】《Android動態逆向分析工具(一)——Andbug之基本操作》 【3】《Android動態逆向分析工具(四)—— Andbug補充調試功能》 【4】《gikdbg.art系列教程2.1-調試so動態庫》