逆向目标: 登录接口请求参数Encrypt逆向分析
1、抓包分析 点击登录通过charles抓包,看到登录接口的请求参数中存在一个加密参数Encrypt
2、通过jadx定位加密位置 jadx打开apk文件,直接通过搜索关键字的方式进行定位 这里搜索出来很多结果,优先去看跟apk包名相关的位置,这里发现了两处可疑的位置,点击进入到源码位置
3、通过frida hook的方式确定是否是加密位置 以上的Encrypt参数是通过encrypt进行赋值的,encrypt在上方进行的赋值
1 String encrypt = RequestUtil.encodeDesMap(code, this.desKey, this.desIV);
我们可以通过hook RequestUtil类下的encodeDesMap方法来得到encrypt的值
hook代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function hook ( ) { let RequestUtil = Java .use ("com.dodonew.online.http.RequestUtil" ); RequestUtil ["encodeDesMap" ].overload ('java.lang.String' , 'java.lang.String' , 'java.lang.String' ).implementation = function (data, desKey, desIV ) { console .log (`RequestUtil.encodeDesMap is called: data=${data} , desKey=${desKey} , desIV=${desIV} ` ); let result = this ["encodeDesMap" ](data, desKey, desIV); console .log (`RequestUtil.encodeDesMap result=${result} ` ); return result; }; } function main ( ) { Java .perform (function ( ) { hook () }) } main ()
hook结果如下:
1 2 3 4 5 6 RequestUtil.encodeDesMap is called: data={"equtype":"ANDROID","loginImei":"Android010139023152113","sign":"6390CC1EE2406073DB9FEF4E9CB80A29","timeStamp":"1747969001281","userPwd":"123456","username":"13456798976"}, desKey=65102933, desIV=32028092 RequestUtil.encodeDesMap result=NIszaqFPos1vd0pFqKlB42Np5itPxaNH//FDsRnlBfgL4lcVxjXiiyqIScii6OFy//cZnTrYzQcr ozbW4axiV+SeLV71aIQ4AeSd5UD0yf5ogdESrlwVkyfuDkmwz3FX/lKWbALMS5qEp0jbw2vtbEni E8tVSrSd3H0yGIPI8M+3YsMqBRVm+Sy6XFGe+QmHprBkLA+cSz1iZP/tKcwFi/zeVAfchRVIJq7m 1A/eQyI=
我们对比charles的抓包结果可以发现,这里就是加密的位置,那么我们接下来就需要去重点分析RequestUtil类下的encodeDesMap方法
4、逆向分析 通过frida hook RequestUtil.encodeDesMap方法的hook结果分析入参
1 RequestUtil.encodeDesMap is called: data={"equtype":"ANDROID","loginImei":"Android010139023152113","sign":"6390CC1EE2406073DB9FEF4E9CB80A29","timeStamp":"1747969001281","userPwd":"123456","username":"13456798976"}, desKey=65102933, desIV=32028092
我们多调用几次登录接口可以发现equtype和loginImei参数是固定的,timeStamp参数是时间戳,userPwd和username分别是用户输入的密码和用户名,那么现在需要找到sign参数加密的位置 在encrypt的上方可以看到sign,我们直接跟进去RequestUtil.paraMap方法看一下,跟进去后看到该方法的源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static String paraMap (Map<String, String> addMap, String append, String sign) { try { Set<String> keyset = addMap.keySet(); StringBuilder builder = new StringBuilder (); List<String> list = new ArrayList <>(); for (String keyName : keyset) { list.add(keyName + "=" + addMap.get(keyName)); } Collections.sort(list); for (int i = 0 ; i < list.size(); i++) { builder.append(list.get(i)); builder.append("&" ); } builder.append("key=" + append); String checkCode = Utils.md5(builder.toString()).toUpperCase(); addMap.put("sign" , checkCode); String result = new Gson ().toJson(sortMapByKey(addMap)); Log.w(AppConfig.DEBUG_TAG, result + " result" ); return result; } catch (Exception e) { e.printStackTrace(); return "" ; } }
很明显sign的值是checkCode的值,checkCode的值通过Utils.md5加密得到的,那么我们先编写frida hook脚本hook一下Utils.md5接收的参数是什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function hook3 ( ) { let Utils = Java .use ("com.dodonew.online.util.Utils" ); Utils ["md5" ].implementation = function (string ) { console .log (`Utils.md5 is called: string=${string} ` ); let result = this ["md5" ](string); console .log (`Utils.md5 result=${result} ` ); return result; }; } function main ( ) { Java .perform (function ( ) { hook3 () }) } main ()
hook的结果为
1 2 Utils.md5 is called: string=equtype=ANDROID&loginImei=Android010139023152113&timeStamp=1747981884623&userPwd=123456&username=13456798976&key=sdlkjsdljf0j2fsjk Utils.md5 result=00cfb423f3e7c8894f25f5069f4fbd7d
根据上面的代码我们可以发现sign的值就是将Map中的键值对循环遍历出来然后通过key=value的形式中间拼接一个&符号得到最终的字符串,然后将字符串进行md5加密转为大写,只不过在进行字符串拼接前往Map中添加了一个key键,值为sdlkjsdljf0j2fsjk,那么我们就需要确认一下Utils.md5加密是否是一个标准的md5加密,我们可以通过frida主动调用的方式来确定
1 2 3 4 5 6 7 8 9 10 function main ( ) { Java .perform (function ( ) { let Utils = Java .use ("com.dodonew.online.util.Utils" ); console .log (Utils ["md5" ]('1' )); }) } main ()
那么可以确定Utils.md5就是一个标准的md5加密
接下来hook一下paraMap方法,看看paraMap方法最终返回的结果是什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 function hook2 ( ) { let RequestUtil = Java .use ("com.dodonew.online.http.RequestUtil" ); RequestUtil ["paraMap" ].overload ('java.util.Map' , 'java.lang.String' , 'java.lang.String' ).implementation = function (addMap, append, sign ) { console .log (`RequestUtil.paraMap is called: append=${append} , sign=${sign} ` ); console .log ("addMap content:" ); let entrySet = addMap.keySet (); let iterator = entrySet.iterator (); while (iterator.hasNext ()) { var keyStr = iterator.next () var valueStr = addMap.get (keyStr) console .log (` ${keyStr} => ${valueStr} ` ); } let result = this ["paraMap" ](addMap, append, sign); console .log (`RequestUtil.paraMap result=${result} ` ); return result; }; } function main ( ) { Java .perform (function ( ) { hook2 () }) } main ()
hook结果:
1 2 3 4 5 6 7 8 RequestUtil.paraMap is called: append=sdlkjsdljf0j2fsjk, sign=sign addMap content: timeStamp => 1747982496340 loginImei => Android010139023152113 equtype => ANDROID userPwd => 123456 username => 13456798976 RequestUtil.paraMap result={"equtype":"ANDROID","loginImei":"Android010139023152113","sign":"2205707C5A4F0944792BD57E8257C68F","timeStamp":"1747982496340","userPwd":"123456","username":"13456798976"}
通过hook paraMap方法可以看到最终方法返回的结果是一个字符串,该字符串是将加密后的sign参数push到addMap中,然后将addMap参数转为对应的字符串
最终回到RequestUtil.encodeDesMap方法,点击进入到encodeDesMap方法中 跟进DesSecurity类中看看 发现这是一个des加密方式,只是key又被经过了一次md5的加密,在刚才hook RequestUtil.encodeDesMap的时候就已经知道了key的值为65102933,iv的值为32028092,那么我们只需要进行算法还原就行了
python算法还原 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 import base64from urllib.parse import urlencodeimport hashlibimport jsonfrom Crypto.Cipher import DESfrom Crypto.Util.Padding import pad, unpadfrom Crypto.Hash import MD5class DESCipher : def __init__ (self, sec_key: bytes , sec_iv: bytes ): md5 = MD5.new() md5.update(sec_key) des_key = md5.digest()[:8 ] self.en_cipher = DES.new(des_key, DES.MODE_CBC, sec_iv) self.de_cipher = DES.new(des_key, DES.MODE_CBC, sec_iv) def encrypt64 (self, data: bytes ) -> str : from Crypto.Util.Padding import pad encrypted = self.en_cipher.encrypt(pad(data, DES.block_size)) return base64.b64encode(encrypted).decode('utf-8' ) def decrypt (self, ciphertext: bytes ) -> bytes : decrypted = self.de_cipher.decrypt(ciphertext) return unpad(decrypted, DES.block_size) data = {"equtype" : "ANDROID" , "loginImei" : "Android010139023152113" , "timeStamp" : "1747973461904" , "userPwd" : "123456" , "username" : "13456798976" } query_string = urlencode(data) final_string = query_string + "&key=sdlkjsdljf0j2fsjk" print (final_string)sign = hashlib.md5(final_string.encode('utf-8' )).hexdigest().upper() print (sign)data['sign' ] = sign print (data)sorted_dict = dict (sorted (data.items())) code = json.dumps(sorted_dict).replace(" " , "" ) print (code)sec_key = b"65102933" sec_iv = b"32028092" cipher = DESCipher(sec_key, sec_iv) encrypted_b64 = cipher.encrypt64(code.encode()) print ("Base64 加密结果:" , encrypted_b64)
最终只需要将入参保持一致,然后把最终的结果和frida hook的结果对比一下,结果一致的话就说明算法还原的没问题