“ 前言:初学逆向 请多多指教 好累 感觉每天这样肝 人有点受不了了...”
学习到的内容
—
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.js
1、通过主动调用获取对应的类
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没有分析出来,感觉有点难,我继续试试,可以的话再写一篇!