編輯:關於Android編程
微信裡面有個“附近的人”,相信大家都知道,大概的原理就是調用騰訊地圖的sdk進行定位,然後把個人的定位信息發到微信服務器上進行處理,再把一定范圍內的人的信息返回並用列表的形式顯示出來。
因為剛踏入逆向分析這行,所以抱著學習的態度,研究一下大公司的東西,漲一下知識,嗯,本次的案例就是分析騰訊地圖中定位請求數據的加解密,以及搭建簡易的APK模擬實現定位請求。
為了分析這一案例,是要掌握一定的分析工具的,其中包括APKTOOL, Sublime-Text, Xposed框架,Wireshark等。
嗯,作為分析筆記,我就不細說了,只是將分析的步驟一步步地列舉出來。
首先,下載微信APK,利用APKTOOL在DOS下敲命令生成smali文件夾,這個過程我就不說了,前面博客已介紹過APKTOOL的使用方法。對於初學者來說,他們喜歡看到直接明了的源代碼,所以他們會喜歡用dex2jar去反編譯,並利用jd-gui去查看代碼,包括我當初也是如此,但現在我為什麼不用dex2jar和jd-gui去分析呢?再往下看看
然後用smali文件查看工具Sublime-Text 3打開其剛剛生成的smali文件夾,如下圖:
嗯,反編譯出來的文件數量令人感到窒息的,是不是有點感到逆向分析的痛苦了?要在如此多的smali文件裡找到對應的類,並定位到關鍵的代碼,這是需要一定的技巧的,不然你就只能碼海撈針了。順便一提,這貨混淆得太好了,裡面的可讀性非常差,而且通過smali可以看到代碼是有一定的復雜性的,特別多goto語句,所以如果用jd-gui浏覽代碼的話,是很不准確的,而且有一些內部類jd-gui是不會顯示出來的等等。。。總而言之,smali的表達基本是正確的,但jd-gui轉變過程並沒有那麼智能,所以jd-gui所表達的信息很大程度是錯的,只能輔助用,不能依賴。
既然我們要找的是地圖地位請求,那麼肯定涉及到網絡,那麼又怎麼能少了抓包過程呢?好,進入微信,打開wireshark,在點擊“附近的人”按鈕同時觀察wireshark抓到的包,主要是Http協議的包。很好抓,如下圖:
接著,我們右鍵打開選項,並選擇“追蹤流”,查看Http請求數據的TCP流,並嘗試去從請求中找到關鍵字符串:
可以從抓到的包看到,在請求行和請求頭中有我們想要的關鍵字符串,至於請求體就不用細看了,已被加密過,但我們的目的就是要分析其加解密原理。
SO,接下來一步就是通過關鍵字符串,在碼海中定位到關鍵的代碼。
先回到剛才用Sublime-Text打開smali文件夾的界面,在界面中找到ct包(包名已被混淆,實際就是集成地圖sdk的jar包),在該包下的所有smali文件查找剛才抓包的關鍵字符串,這裡只用了“/loc?c=1&mars=0&obs=1”中的“mars”進行查找。
我們可以看到,在搜索結果裡找到了該字符串在哪裡被調用了,很好,就在br.smali這個文件中,那麼我們找到br.smali文件,並打開它:
然後,可以看到字符串是在br.smali中的a(II)方法中調用的,那麼我們就繼續順籐摸瓜~根據smali語法,構造搜索字符串“Lct/br;->a(II)”, 通過這個字符串,我們可以利用Find in Floder的搜索功能,知道在哪裡調用了這個函數:
不錯,已經找到了核心文件bz.smali,我就先說了吧,該類有兩個內部類一個bz$1,另一個bz$a(美元符號代表內部類),bz$1是一個繼承了Runnable的一個線程類,bz$a就是一個封裝數據的類,包括加密後的數據和請求地址。
因為我們是要找到加解密的原理,所以在找關鍵代碼的時候,要多留心帶有String類,和byte[]數組參數的函數,這些往往就是我們的Target~
so,不妨大致浏覽一下bz.smali裡涉及到String和byte[]的函數:
看到紅色圈出來的代碼了嗎,那就是關鍵代碼了。我就先直接說了吧,1:String為數據明文 , 2. 3:明文.getBytes("gbk")得到byte[], 4:b$a類的a方法,該方法就是利用java中的DeflaterOutputStream壓縮處理, 5:將壓縮後的byte[]作為參數放到 bz類的a方法中處理,該方法內還調用了so庫的本地方法進行異或加密,這個後面會說明, 6:初始化一個bz$a類,該類封裝了加密後的數據byte[]和地址String。
當初始化bz$a類後,它會被壓進一個LinkedBlockingQueue隊列中,即調用其offer方法;然後在bz$1線程類,再次take出來,將其byte[] get出來,作為Http請求體的數據。
那麼,有人就問,為什麼你就確定一定會調用這個函數呢?Good Question,別看我說得那麼輕松,實際在通過關鍵字符找關鍵代碼的同時,有必要通過Xposed框架的hook技術來驗證是否調用了這個函數,又或者hook這個函數,查看其參數和參數的變化等,這也是很重要的。Xposed框架使用前面博客有介紹,這裡不費口水了。
下面就是我通過Xposed框架中的一個模塊,hook到這個函數的:
從上圖可以看到,我hook了bz的函數a,並獲得其第一個參數String,並打印在Log中,可見確實是一段明文,這段明文是Json格式的,裡面包含了各種信息,包括手機imei, imsi,mac, 以及基站和周邊wifi的信息。
這樣一來,我們就可以確定這個函數確實是被調用了。
明文getBytes("gbk")調用了壓縮處理的函數, b$a->a([B)[B (smali格式的調用),那麼我們可以利用jd-gui打開b.smali文件,並查看其內部類a中的a方法(丫的,混淆得太惡心了,都是什麼abcd什麼鬼的~),切記,不要依賴jd-gui,上面的信息只能作為大概參考:
從紅色圈住的代碼看到,就是DeflaterOutputStream的壓縮處理,該a方法的下面還有一個b方法,處理的原理反過來的,即用InflaterInputStream對http的返回數據進行解壓縮處理:
除了對數據進行解壓縮處理外,還有一個異或加密,我們上面提到的步驟5:將壓縮後的byte[]作為參數放到 bz類的a方法中處理,該方法內還調用了so庫的本地方法進行異或加密,實際上可以通過順籐摸瓜找到最終函數:
調用了e類的 o方法,該方法是native方法,用一般的方法是看不了so庫內的代碼的,但IDA PRO貌似是可以看其匯編代碼分析的,這個是有一定難度的。不過,這裡我們可以假定它裡面的處理只是一個簡單的異或加密,那麼我們就可以修改參數,將byte[]全部置0,通過異或運算找出其key。這是關鍵點,通過000000^key=key。
因為 e的o函數調用內嵌在bz的a([BI)方法裡,所以我們只需要hook外面那層函數就可以了,注意,這裡的bz有兩個a方法,混淆得惡心,但參數是不一樣的:
我這裡new了一個長度為100,值全為0的byte[],key和0異或等於key本身,那麼我們看看是否這個本地方法裡的處理是不是異或?
從Log中可以看到,before是0,after是8個字節為一循環體的字節數組,顯然,這是個以 8個字節為key的異或加密,很好,看臉破解了就省事了,哈哈~
至此,整個數據從明文到密文就分析完成了,至於http返回後的數據實際上只需要解壓操作就出明文了,不再需要異或處理一次,所以就不講返回數據的處理了。
接下來,數據處理完了,那麼就分析是怎麼發送請求的。
還記得上面抓包的那個圖不?可以在請求數據中看到請求頭有關鍵字符串“Halley-sdk.......”,通過這個字符串,在sublime text中“find in folder”,在文件夾內搜索到該關鍵字符串,然後順籐摸瓜,後面你懂的,找到文件後,分析一下smali,大概就是用了apache接口的HttpClient進行一個請求而已。
那麼,到這裡,我們需要梳理一下,直接上流程圖吧:
好了,有了思路,那麼就用java進行模擬吧,我是在eclipse上模擬的,當然,也可以在Android Studio上寫個簡單APK。
eclipse中我用了兩種Http請求,一種是HttpClient,地圖sdk中用得方法,需要導入httpclient,httpcore, httpmine三個jar包,貌似還需要有個logging的jar包,都需要到Apache的官網下載;第二種是HttpURLConnection,這個java本身提供,雖然實現起來稍麻煩,但還是可以的。之所以要用第二種方法,是因為Android Studio把第一種方法中的四個包都導進去的時候由於工具本身bug會導致打包不成功,生成不了APK,很郁悶~
下面把代碼貼出來:
public class MainActivity extends Activity { private Button btnRequest; private TextView tvRequest, tvResponse; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvRequest=(TextView)findViewById(R.id.tv_request_data); tvResponse=(TextView)findViewById(R.id.tv_response_data); btnRequest=(Button)findViewById(R.id.btn_request); btnRequest.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { tvRequest.setText(generateJsonObject().toString()); tvResponse.setText(""); new Thread(new Runnable() { @Override public void run() { try{ URL url=new URL("http://lbs.map.qq.com/loc?c=1&mars=0&obs=1"); HttpURLConnection httpConn=(HttpURLConnection)url.openConnection(); httpConn.setDoOutput(true); httpConn.setDoInput(true); httpConn.setUseCaches(false); httpConn.setRequestMethod("POST"); httpConn.setRequestProperty("Connection", "Keep-Alive"); DataOutputStream dos=new DataOutputStream(httpConn.getOutputStream()); dos.write(xorEncrypt(deflater(generateJsonObject().toString().getBytes("gbk")))); dos.flush(); dos.close(); int resultCode=httpConn.getResponseCode(); Log.d("zz", "resultCode:"+resultCode); if(resultCode==HttpURLConnection.HTTP_OK){ Log.d("zz", "http response ok"+resultCode); final ByteArrayOutputStream baos=new ByteArrayOutputStream(); byte[] buffer=new byte[1024]; InputStream is=httpConn.getInputStream(); int i=0; while((i=is.read(buffer))!=-1){ baos.write(buffer, 0, i); } tvResponse.post(new Runnable() { @Override public void run() { try{ tvResponse.setText(new String(inflater(baos.toByteArray()), "gbk")); }catch (Exception e){ } } }); } }catch (Exception e){ } } }).start(); } }); } //拼接一個明文JSONObect private JSONObject generateJsonObject(){ JSONObject dataJson=new JSONObject(); try{ dataJson.put("version", "4.5.9.3"); dataJson.put("address", 1); dataJson.put("source", 203); dataJson.put("access_token", "9d9e8211360ba90b8fbda2fcdbed21"); dataJson.put("app_name", "wechat"); dataJson.put("app_label", "微信_6.3.18"); dataJson.put("bearing", 1); dataJson.put("control", 2); dataJson.put("pstat", 5); dataJson.put("wlan", getWlanInfo()); dataJson.put("attribute", getAttributeInfo()); dataJson.put("location", new JSONObject()); dataJson.put("cells", getCellsInfo()); dataJson.put("wifis", getWifisInfo()); }catch (JSONException jsonException){ } return dataJson; } private JSONObject getWlanInfo(){ WifiManager wifiManager=(WifiManager)getSystemService(Context.WIFI_SERVICE); WifiInfo wifiInfo=wifiManager.getConnectionInfo(); int rssi=wifiInfo.getRssi(); String ssid=wifiInfo.getSSID(); String bssid=wifiInfo.getBSSID(); JSONObject wlanJson=new JSONObject(); try{ wlanJson.put("mac", bssid); wlanJson.put("rssi", rssi); wlanJson.put("ssid", ssid); }catch (JSONException e){ } return wlanJson; } private JSONObject getAttributeInfo(){ WifiManager wifiManager=(WifiManager)getSystemService(Context.WIFI_SERVICE); WifiInfo wifiInfo=wifiManager.getConnectionInfo(); String mac=wifiInfo.getMacAddress().replace(":", ""); TelephonyManager telephonyManager=(TelephonyManager)getSystemService(TELEPHONY_SERVICE); String imei=telephonyManager.getDeviceId(); String imsi=telephonyManager.getSubscriberId(); JSONObject attributeJson=new JSONObject(); try{ attributeJson.put("mac", mac); attributeJson.put("imsi", imsi); attributeJson.put("imei", imei); attributeJson.put("qq", ""); attributeJson.put("phonenum", ""); }catch (JSONException e){ } return attributeJson; } private JSONArray getCellsInfo(){ TelephonyManager telephonyManager=(TelephonyManager)getSystemService(TELEPHONY_SERVICE); JSONArray jsonArray=new JSONArray(); JSONObject cellJson=new JSONObject(); try { cellJson.put("mcc", 460); cellJson.put("mnc", 0); cellJson.put("lac", 42360); cellJson.put("cellid", 45370604); cellJson.put("rss", -88); cellJson.put("seed", 1); }catch (JSONException e){ } jsonArray.put(cellJson); return jsonArray; } private JSONArray getWifisInfo(){ JSONArray jsonArray=new JSONArray(); WifiManager wifiManager=(WifiManager)getSystemService(Context.WIFI_SERVICE); wifiManager.startScan(); Listwifis=wifiManager.getScanResults(); for(ScanResult wifi:wifis){ JSONObject wifiJson=new JSONObject(); try{ wifiJson.put("mac", wifi.BSSID); wifiJson.put("rssi", wifi.level); jsonArray.put(wifiJson); }catch (JSONException e){ } } return jsonArray; } //壓縮 public byte[] deflater(byte[] bytes) throws IOException{ if (bytes == null) return null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); DeflaterOutputStream dos = new DeflaterOutputStream(baos); dos.write(bytes, 0, bytes.length); dos.finish(); dos.flush(); dos.close(); return baos.toByteArray(); } //解壓 public static byte[] inflater(byte[] bytes) throws IOException{ if (bytes == null) return null; ByteArrayInputStream bais = new ByteArrayInputStream(bytes); InflaterInputStream iis = new InflaterInputStream(bais); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int i = 0; while((i=iis.read(buffer))!=-1) { baos.write(buffer, 0, i); } baos.flush(); return baos.toByteArray(); } //異或加密 public byte[] xorEncrypt(byte[] bytes){ byte[] key=new byte[]{-67, 39, -29, 21, 51, 115, 19, -105}; byte[] encryptedBytes=new byte[bytes.length]; for(int i=0; i 布局文件:
記得加上權限:
運行效果:
本篇主要是對 google推出的性能優化典范 進行一個通篇的整理… 主要在於一些具體的優化技巧、至於 60fps、掉幀、gc、內存抖動、阈值…
本次博客主要介紹一個天氣效果的實現過程最近公司的項目加入天氣模塊,需要實現下面的效果: 然後根據自己的構想實現了下面的效果: 下面會詳
使用SlidingTabLayout需要准備2個類,分別是 SlidingTabLayout,與SlidingTabStrip,,放進項目中時只用修改下包名即可。 效果制
摘自網上的android生命周期圖: cocos2dx-2.X前後台切換分析,基於android平台:1、從後台進入前台項目的activity一般繼承自Coco