逆向so_记一次APP的so层算法逆向(七)
“?前言:初學逆向 請多多指教 好累 感覺每天這樣肝 人有點受不了了...”
????學習到的內容
—
1、新學習到IDA的一些分析時候的小技巧
2、算法還原代碼實現的練習(有個參數沒有分析出來,后面知道了會補上的)
3、在Frida中使用命令行調試的方便方法
分析過程
—
APP登陸界面:
請求包:
POST /api/adult/check_guest HTTP/1.1Content-Type: application/x-www-form-urlencodedUser-Agent: Dalvik/2.1.0 (Linux; U; Android 8.1.0; Pixel Build/OPM1.171019.011)Host: service.kuyangsh.cnConnection: Keep-AliveAccept-Encoding: gzipContent-Length: 1108a=6ih0KN8TFL5%2FQT%2FN3JY63ZovsWQyeIxHjBLHp1GGwjUNYVaGJLC%2FYZenRFKbeqsgIoI4rD1atROr%0Ajkl1p7eobNMARMel19oiGkl5hRD72vOn9zyNbERMe8Cj3b24Ru4wc2mWbbnamKVKPepkaa2mqpJl%0Akp4%2Fa5udSz0UbDR6cTwLCRWeKb60H%2Fir4vzZv1OfwQF%2FXJQsuBmxH2F6wp9CJkk9WYchx4LQU%2FS3%0AjQfpQY2iZWwsmHAiyZGVsfZIgXTvhJygpT8vH268Py5JspYZoho0RRrx4BjUfs5boif6rEMpd5PC%0AiZTLhIPovbPpoZQJC7d%2BWPFtwjPT7Ljyzgz5QxtctnZqa4qMMkfFwAIeA7lj2wJZeZBD%2F%2BoU5R45%0AMEFK6OMrMXB1M%2BC4dBt7Rd384SYhe%2BAEp0gKNHGkrpxWImFcPAaalajyijs6V4Wl4res9CWuEE5W%0Ayj5ehtmPc6uZGuo5ns6THfDwnho8BiFOnH0QoqJEeyVdTsCzOiMwfBJJldB7qbsTfUlLbSnpq4tf%0AurPbVMVKQbk4ui1XgDH5v%2FptUHirNK0IHT%2BBms8wQ%2BSX3BcMLKFiWI0OBAjUydqcJpIi0sPSWpfh%0A2k1nmMlvPnljfc7P12iF8nFHoFWRYQPVie46K%2Bhd4%2FttkyrZ4Gy3WM6zWdmnED3h6CCgZ4rEe0DB%0AN5Dj8lJJbsOAE%2FxWcYDguyj8WkUET3yLB%2FBZQx%2BsOn9otWzjwROdhfi6V7OObXZ5XoGUIDffKaFu%0ADnvTinsAh%2FvjcSDVq%2BHyD%2FzUeRceMf6uQruBhHRzikSb2Oz0Zfxld7rqmWYZ8aIBe1DMRJuXecB%2F%0AAsBu4VVuVVfQf4hlCpVNmKnX6huuMkHtCptCLaD0pkmuY7X7OEfCsudtFIco%2F7gXaQ5aXgfCs7GJ%0AzAMxfpCRm4vnF0kc8nQ4OWlexOV5t65k%2B4eDt8wY91%2BIFHcq%2BIwZPR3e41oeKriHlbsPdocNOkeg%0AeyUw%2FXlAY97IpZA%3D%0A返回包:
{ "ret": 0, "msg": "沒有錯誤", "data": { "needs_check": "0", "needs_bind": "0", "needs_guest_bind": 0, "user_is_guest": -1 }}這里定位很簡單,直接全局搜索關鍵詞:"a" 就可以了,因為可以看到POST數據包中只有一個字段就是a,但是這里需要帶上雙引號,來到如下
可以看到最關鍵的加密代碼就是:String a = m3385a((Map) hashMap, initFromXML.getAppKey());
initFromXML.getAppKey則正常返回一個定值
跟進m3385a函數可以發現,定義如下:
public static String m3385a(Map<String, String> map, String str) { if (C1074a.f2394c || TivicloudController.f2343a) { map.put("testing", "1"); } try { JSONObject jSONObject = new JSONObject(map); Debug.m3346d("params : " + jSONObject.toString()); String encode = URLEncoder.encode(jSONObject.toString(), "UTF-8"); String encryptString = EncryptUtil.encryptString(encode + str); JSONObject jSONObject2 = new JSONObject(); try { jSONObject2.put("sign", encryptString); jSONObject2.put(C0882di.C0883a.DATA, encode); } catch (JSONException e) { Debug.m3354w((Exception) e); } return new String(Base64.encode(EncryptUtil.nativeAES(jSONObject2.toString()), 0), "UTF-8"); } catch (UnsupportedEncodingException e2) { Debug.m3354w((Exception) e2); return null; } }注意:這里發現JSONObject是處于org.json.JSONObject,系統自帶的庫中的類,這里也能作為一種hook的方法定位
這里可以直接對m3385a這個函數進行觀察,流程就是將傳入的數據先進行一個url編碼,然后拼接initFromXML.getAppKey作為參數,調用EncryptUtil.encryptString
繼續跟到encryptString中去,代碼如下,那么也就需要進libgavesec.so中的nativeEncrypt函數進行分析
這里遇到了一個問題,比如下面的兩個函數和注入代碼,一個函數會調用native中的函數,但是如果我hook?encryptString這個函數,返回值寫的是調用native層的nativeEncrypt函數,這樣的寫法就會導致程序結束,具體原因不知道
public static String encryptString(String str) { return nativeEncrypt(str); } private static native String nativeEncrypt(String str); Java.perform(function () { var EncryptUtil = Java.use("com.tivicloud.utils.EncryptUtil"); EncryptUtil.encryptString.implementation = function (a) { send("EncryptUtil.encryptString args[0]: " + a); var result = this.nativeEncrypt(a); return result; } });因為上面那樣寫就導致程序結束,所以這里就直接hook?nativeEncrypt,驚奇的發現這樣子就不會導致程序結束了
setImmediate(function(){ Java.perform(function () { var EncryptUtil = Java.use("com.tivicloud.utils.EncryptUtil"); EncryptUtil.nativeEncrypt.implementation = function (a) { send("EncryptUtil.nativeEncrypt args[0]: " + a); var result = this.nativeEncrypt(a); return result; } });});這里可以對EncryptUtil.encryptString(encode + str),java層進行一次hook來獲取對應的參數先
[*] EncryptUtil.encryptString args[0]: {"mobile_operator":"","app_versioncode":"4","app_version":"2.1.0.22277","connection_type":"WIFI","os_version":"8.1.0","version_code":"4","version_name":"2.1.0.22277","os_lang":"zh","sdk_version":"3.1.4","package_name":"com.lm.lm","imei":"359906070277673","os_name":"Android","lang":"zh","udid":"10e1ef4509a2340eb0276bb99d186506","nudid":"155c2b70-fc97-4005-abc7-6d6c4329ed23","app_id":"10054","channel_id":"10006","tdid":"39fb282a761c17e80c069332fc6a2ffa9"}fff18b83431fa3a83b9de80c1e413bde那么的initFromXML.getAppKey值就為fff18b83431fa3a83b9de80c1e413bde
然后接著繼續跟native層進行分析,將libgavesec.so拖入到IDA中,并且找到對應的函數
1、在通過導出library庫之后,有些jni的函數無法識別參數,你可以直接右鍵該函數選擇Force call type來進行重新分析,一般都可以成功識別參數
2、有時候ida中的強制轉換類型太多,可以右鍵選擇Show casts來隱藏強制轉換,然后進行分析
主要進行了encrypt函數的調用
接著就是來到加密函數encrypt中進行分析
先是進行一次sha1加密
所以這里要hook兩個地方
encrypt:Base + 0x1C8C ,獲取第一個參數
SHA1::Result:Base + 0x2AA4,獲取該函數調用完之后的第二個參數的結果
這里分享的frida的調試方法,通過命令行注入js腳本進入到frida的命令行中進行操作
frida -RF -l hooktest.js1、通過主動調用獲取對應的類
2、調用類對應的靜態/非靜態方法調試數據的時候會很方便,代碼如下:
function data_test() { Java.perform(function () { var EncryptUtil = Java.use("com.tivicloud.utils.EncryptUtil"); var res = EncryptUtil.nativeEncrypt('%7B%22mobile_operator%22%3A%22%22%2C%22app_versioncode%22%3A%224%22%2C%22app_version%22%3A%222.1.0.22277%22%2C%22connection_type%22%3A%22WIFI%22%2C%22os_version%22%3A%228.1.0%22%2C%22source%22%3A%22SDK%22%2C%22user_id%22%3A%2216073535%22%2C%22os_lang%22%3A%22zh%22%2C%22sdk_version%22%3A%223.1.4%22%2C%22imei%22%3A%22359906070277673%22%2C%22os_name%22%3A%22Android%22%2C%22login_token%22%3A%2285700bcdf4a41164ab7406e8445479ed%22%2C%22lang%22%3A%22zh%22%2C%22udid%22%3A%2210e1ef4509a2340eb0276bb99d186506%22%2C%22nudid%22%3A%22155c2b70-fc97-4005-abc7-6d6c4329ed23%22%2C%22app_id%22%3A%2210054%22%2C%22channel_id%22%3A%2210006%22%2C%22tdid%22%3A%2239fb282a761c17e80c069332fc6a2ffa9%22%7Dfff18b83431fa3a83b9de80c1e413bde'); console.log(res); });}接著上面的分析,打印了sha1加密過后的數據v12變量和最終的Sign值(這里也就是nativeEncrypt的返回值),你會發現結果是不一樣的
接著你會看到下面的循環的操作,所以sha1加密的結果應該被二次修改了,這里有兩個大循環的操作
首先第一段循環:
SHA1::Result((SHA1 *)&v13, (unsigned int *)v12);// sha1加密的結果v4 = 0LL; // long long v5 = 7;do // 進行循環操作 { v6 = *(_DWORD *)&v12[v4]; // 0X0C v7 = &dest_cstr_1[v5]; while ( 1 ) { v8 = v6 & 0xF; // 0X0C v6 >>= 4; // 0X00 *v7-- = hexDigits[v8]; // if ( !(v5 & 7) ) break; --v5; } *(_DWORD *)&v12[v4] = v6; // 賦值操作 v4 += 4LL; v5 += 15; } while ( v4 != 20 );你會發現hexDigits是data段的一段數據:
最后分析,其實就是將其原封不動的轉換為16進制的字符,然后一起拼接起來,分析注釋如下:
然后就是第二段循環了,循環次數也可以看出來會對每個16進制字符進行處理,循環次數為40次
hook了char2hexInt處理過后的數據會發現,char2hexInt這個函數才是二次處理的函數
do { v10 = char2hexInt(dest_cstr_1[v9]); dest_cstr_1[v9] = hexDigits[(signed int)((unsigned __int64)char2hexInt(a211034f8af4e6b[v9]) ^ v10)]; //異或的值為v10 ++v9; }還需要char2hexInt的參數來觀察,該地址為0x1B34,HOOK結果如下
其實就是兩個返回值進行異或處理,比如0x2^0x2就為0,最后還會通過hexDigits數組轉成對應的16進制在內存中保存為0x30,因為字符 '0' 對應的ASCII 十六進制就是 0x30 是相等的!
最終的sign值分析的注釋:
sign這里也分析完了,還有個整體加密的分析,發現整體會進行一次Base64.encode,但是nativeAES可以跟進去觀察
發現該函數依舊是native層的函數,所以這里繼續去看分析
來到如下進行分析,看到名字就知道是AES算法加密了,這里的話用findcrypt插件通過特征碼也可以進行識別
然后接著就是先對傳入的數據進行字符串復制
然后就是AES對象和密鑰的初始化,這里的AES需要hook,來確認v19的值,還有該函數最終的返回值dest_array_bytes的地址來進行打印
AES::AES:0x3274,Cipher:0x397C,然后hook到的數據如下,一個是AES密鑰,最終要進行AES字符串加密的字符串
最終hook的代碼:
function hook_test() { Java.perform(function () { var EncryptUtil = Java.use("com.tivicloud.utils.EncryptUtil"); EncryptUtil.nativeEncrypt.implementation = function (a) { console.log("=============================") // console.log("EncryptUtil.nativeEncrypt args[0] 被加密的字符串: ", a); var result = this.nativeEncrypt(a); console.log("Sign的結果:", result); return result; } var b64 = Java.use("android.util.Base64"); var str = Java.use("java.lang.String"); EncryptUtil.nativeAES.implementation = function (a) { console.log("=============================") // console.log("EncryptUtil.nativeEncrypt args[0] 被加密的字符串: ", a); var result = this.nativeAES(a); console.log("base64數據", str.$new(b64.encode(result, 0))); return result; } var libgavesec = Module.findBaseAddress("libgavesec.so"); // encrypt Interceptor.attach(libgavesec.add(0x1C8C), { onEnter: function (args) { console.log("encrypt args[0] 被加密的字符串: ", Memory.readCString(args[0])); this.args1 = args[1]; }, onLeave: function (retVal) { console.log("encrypt args[1] 處理過后的數據:", hexdump(this.args1, { offset: 0, length: 32, header: true, ansi: false })); } }) // SHA1::Result Interceptor.attach(libgavesec.add(0x2AA4), { onEnter: function (args) { // console.log("SHA1::Result args[1] 加密前的數據", hexdump(args[1], { // offset: 0, // length: 64, // header: true, // ansi: false // })); this.args1 = args[1]; }, onLeave: function (retVal) { console.log("SHA1::Result args[1] 加密后的數據", hexdump(this.args1, { offset: 0, length: 32, header: true, ansi: false })); } }) // char2hexInt Interceptor.attach(libgavesec.add(0x1B34), { onEnter: function (args) { // console.log("char2hexInt 參數:", args[0]); this.args1 = args[1]; }, onLeave: function (retVal) { // console.log("char2hexInt 返回值:", retVal); } }) // AES::AES Interceptor.attach(libgavesec.add(0x3274), { onEnter: function (args) { console.log("AES密鑰:", hexdump(args[1], { offset: 0, length: 32, header: true, ansi: false })); }, onLeave: function (retVal) { } }) // AES::Cipher Interceptor.attach(libgavesec.add(0x397C), { onEnter: function (args) { console.log("AES::Cipher args[1]:", Memory.readCString(args[1])); this.args1 = args[1]; }, onLeave: function (retVal) { } }); });};setImmediate(function () { Java.perform(function () { hook_test(); });});簡單的加密代碼的實現:
from Crypto.Cipher import AESfrom urllib import parsefrom binascii import b2a_hex, a2b_hex, b2a_base64import hashlib"""aes加密算法ECB模式"""class Aes128_(object): def __init__(self): self.key = b"14ca829f017c0357" self.mode = AES.MODE_ECB def add_to_16(self, text): if len(text.encode('utf-8')) % 16: add = 16 - len(text.encode('utf-8')) % 16 else: add = 0 text = text + ("\0"*add) # 明文 + \00填充 return text.encode('utf-8') def encrypt(self, text): text = self.add_to_16(text) cryptos = AES.new(self.key, self.mode) cipher_text = cryptos.encrypt(text) # return b2a_hex(cipher_text) return b2a_base64(cipher_text) def decrypto(self, text): cryptor = AES.new(self.key, self.mode) plain_text = cryptor.decrypt(a2b_hex(text)) return bytes.decode(plain_text).rstrip('\0')def getSign(data): sign = '' a211034f8af4e6b = '211034f8af4e6b9546c19ae13ed099553319b6c3' for i in range(40): print(hex(int(data[i], 16) ^ int(a211034f8af4e6b[i], 16))) sign += str(hex(int(data[i], 16) ^ int(a211034f8af4e6b[i], 16)))[2:3] return signif __name__ == "__main__": data = bytes(parse.quote('{"mobile_operator":"","app_versioncode":"4","app_version":"2.1.0.22277","connection_type":"WIFI","os_version":"8.1.0","source":"SDK","user_id":"16073535","os_lang":"zh","sdk_version":"3.1.4","imei":"359906070277673","os_name":"Android","login_token":"9f6037c931db9a9cfcbb991966fad614","lang":"zh","udid":"10e1ef4509a2340eb0276bb99d186506","nudid":"155c2b70-fc97-4005-abc7-6d6c4329ed23","app_id":"10054","channel_id":"10006","tdid":"39fb282a761c17e80c069332fc6a2ffa9"}fff18b83431fa3a83b9de80c1e413bde'), encoding="utf-8") sha1 = hashlib.sha1(data) sha1_data = sha1.hexdigest() sign = getSign(sha1_data) aes_data = str(Aes128_().encrypt('{"sign":"'+sign+'","data": "' + parse.quote('{"mobile_operator":"","app_versioncode":"4","app_version":"2.1.0.22277","connection_type":"WIFI","os_version":"8.1.0","source":"SDK","user_id":"16073535","os_lang":"zh","sdk_version":"3.1.4","imei":"359906070277673","os_name":"Android","login_token":"9f6037c931db9a9cfcbb991966fad614","lang":"zh","udid":"10e1ef4509a2340eb0276bb99d186506","nudid":"155c2b70-fc97-4005-abc7-6d6c4329ed23","app_id":"10054","channel_id":"10006","tdid":"39fb282a761c17e80c069332fc6a2ffa9"}"}')), encoding='utf8').replace("\n", "") print(aes_data)這個其實不是最終的代碼,因為login_token沒有分析出來,感覺有點難,我繼續試試,可以的話再寫一篇!
總結
以上是生活随笔為你收集整理的逆向so_记一次APP的so层算法逆向(七)的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 编程 音量键_盘点市面上那些千元级高逼格
- 下一篇: 蒲公英煮水喝的功效与作用、禁忌和食用方法
