一、Frida介绍 摘抄自吾爱破解大佬:《安卓逆向这档事》十三、是时候学习一下Frida一把梭了(上) - 吾爱破解 - 52pojie.cn
1.什么是Frida? Frida 是一款开源的动态插桩工具,可以插入一些代码到原生App的内存空间去动态地监视和修改其行为,支持Windows、Mac、Linux、Android或者iOS,从安卓层面来讲,可以实现Java层和Native层Hook操作。
Hook操作就是一个类似动态插桩的功能,可以在app执行的任意过程中插入或修改一些代码以及参数和返回值,来修改程序的执行逻辑。
2.Frida原理及重要组件 frida注入的原理就是找到目标进程,使用ptrace跟踪目标进程获取mmap,dlpoen,dlsym等函数库的偏移获取mmap在目标进程申请一段内存空间将在目标进程中找到存放frida-agent-32/64.so的空间启动执行各种操作由agent去实现
组件名称
功能描述
frida-gum
提供了inline-hook的核心实现,还包含了代码跟踪模块Stalker,用于内存访问监控的MemoryAccessMonitor,以及符号查找、栈回溯实现、内存扫描、动态代码生成和重定位等功能
frida-core
fridahook的核心,具有进程注入、进程间通信、会话管理、脚本生命周期管理等功能,屏蔽部分底层的实现细节并给最终用户提供开箱即用的操作接口。包含了frida-server、frida-gadget、frida-agent、frida-helper、frida-inject等关键模块和组件,以及之间的互相通信底座
frida-gadget
本身是一个动态库,可以通过重打包修改动态库的依赖或者修改smali代码去实现向三方应用注入gadget,从而实现Frida的持久化或免root
frida-server
本质上是一个二进制文件,类似于前面学习到的android_server,需要在目标设备上运行并转发端口,在Frida hook中起到关键作用
3.Frida与Xposed的对比
工具
优点
缺点
Xposed
直接编写Java代码,Java层hook方便,可打包模块持久化hook
环境配置繁琐,兼容性较差,难以Hook底层代码。
Frida
配置简单,免重启hook。支持Java层和Native层的hook操作
持久化hook相对麻烦
4.Frida环境配置 1、安装Python 我的python是3.8版本
frida` 是 Frida 工具链的核心组件,实际上是一个跨平台的动态分析框架。它通过在目标进程中注入 JavaScript 代码,使得开发者可以直接与目标应用进行交互,修改其行为或收集调试信息。Frida 支持多种平台,包括 Windows、macOS、Linux、Android、iOS 等。
frida-tools 是 Frida 的一组命令行工具,目的是为了让用户可以方便地使用 Frida 进行操作。它封装了 Frida 的功能,提供了简单易用的命令行接口,帮助用户进行动态分析和调试。
需要安装和pyton对应版本的frida。
这里安装的是frida是14.2.18,frida-tools是9.2.5(需要在官网查询后安装指定版本https://github.com/frida/frida/releases/tag/14.2.18 )
frida和Android以及python的对应关系:
1 2 3 4 frida12.3 .6 Android5-6 python3.7 frida12.8 .0 Android7-8 python3.8 frida14+ Android9+ python3.8 frida15+ Android12 也可以用
因为测试机piexl xl是Android10,因此使用的python是3.8,firda是14+。
3.安装frida-server 同样在relaease页面,找到frida-server-14.2.18-android-arm64.xz下载后push到安卓的data/local/tmp下,改名fr(这里是因为有时候app会对frida名字进行检查),并添加可执行权限,hook的时候需要启动该fr,并且不能关闭命令框 。
5.Frida使用 1.基础指令 1.frida-ps -U 查看当前手机运行的进程 2.frida-ps –help 查看help指令
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 frida-ps 使用方式: frida-ps [选项] 选项: -h, -D ID, -U, -R, -H HOST, 与HOST进行TLS通信,期望的CERTIFICATE 设置心跳包间隔(秒),或设置为0 以禁用(默认为-1 ,根据传输方式自动选择) 设置与 添加与 -O FILE, 包含额外命令行选项的文本文件 -a, -i, -j,
2.操作模式:
操作模式
描述
优点
主要用途
CLI(命令行)模式
通过命令行直接将JavaScript脚本注入进程中,对进程进行操作
便于直接注入和操作
在较小规模的操作或者需求比较简单的场景中使用
RPC模式
使用Python进行JavaScript脚本的注入工作,实际对进程进行操作的还是JavaScript脚本,可以通过RPC传输给Python脚本来进行复杂数据的处理
在对复杂数据的处理上可以通过RPC传输给Python脚本来进行,有利于减少被注入进程的性能损耗
在大规模调用中更加普遍,特别是对于复杂数据处理的需求
3.注入模式与启动命令:
注入模式
描述
命令或参数
优点
主要用途
Spawn模式
将启动App的权利交由Frida来控制,即使目标App已经启动,在使用Frida注入程序时还是会重新启动App
在CLI模式中,Frida通过加上 -f 参数指定包名以spawn模式操作App
适合于需要在App启动时即进行注入的场景,可以在App启动时即捕获其行为
当需要监控App从启动开始的所有行为时使用
Attach模式
在目标App已经启动的情况下,Frida通过ptrace注入程序从而执行Hook的操作
在CLI模式中,如果不添加 -f 参数,则默认会通过attach模式注入App
适合于已经运行的App,不会重新启动App,对用户体验影响较小
在App已经启动,或者我们只关心特定时刻或特定功能的行为时使用
Spawn模式:
1 frida -U -f 进程名 -l hook. js
attach模式 :
frida_server自定义端口
1 2 3 4 5 frida server 默认端口:27042 //有时app会占用该端口以绕过frida,此时需要自定义一个端口 taimen: / $ sutaimen: / # cd data/local/tmp/ taimen: /data/local/tmp # ./fs1280 -l 0.0.0.0:6666
logcat |grep "D.zj2595"日志捕获adb connect 127.0.0.1:62001模拟器端口转发
4.基础语法
API名称
描述
Java.use(className)
获取指定的Java类并使其在JavaScript代码中可用。
Java.perform(callback)
确保回调函数在Java的主线程上执行。
Java.choose(className, callbacks)
枚举指定类的所有实例。
Java.cast(obj, cls)
将一个Java对象转换成另一个Java类的实例。
Java.enumerateLoadedClasses(callbacks)
枚举进程中已经加载的所有Java类。
Java.enumerateClassLoaders(callbacks)
枚举进程中存在的所有Java类加载器。
Java.enumerateMethods(targetClassMethod)
枚举指定类的所有方法。
5.日志输出语法区别
日志方法
描述
区别
console.log()
使用JavaScript直接进行日志打印
多用于在CLI模式中,console.log()直接输出到命令行界面,使用户可以实时查看。在RPC模式中,console.log()同样输出在命令行,但可能被Python脚本的输出内容掩盖。
send()
Frida的专有方法,用于发送数据或日志到外部Python脚本
多用于RPC模式中,它允许JavaScript脚本发送数据到Python脚本,Python脚本可以进一步处理或记录这些数据。
6.Hook框架模板 1 2 3 4 5 6 function main ( ){ Java .perform (function ( ){ hookTest1 (); }); } setImmediate (main);
6.Frida常用API 1.Hook普通方法、打印参数和修改返回值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function hookTest1 ( ){ var utils = Java .use ("类名" ); utils.method .implementation = function (a, b ){ a = 123 ; b = 456 ; var retval = this .method (a, b); console .log (a, b, retval); return retval; } }
2.Hook重载参数 这里需要注意,hook重载函数的时候需要加overload指定,不知道也可以直接运行,frida的报错会给出提示。
1 2 3 4 5 6 7 8 9 10 11 12 function hookTest2 ( ){ var utils = Java .use ("com.zj.wuaipojie.Demo" ); utils.Inner .overload ('com.zj.wuaipojie.Demo$Animal' ,'java.lang.String' ).implementation = function (a,b ){ b = "aaaaaaaaaa" ; this .Inner (a,b); console .log (b); } }
3.Hook构造函数 1 2 3 4 5 6 7 8 9 function hookTest3 ( ){ var utils = Java .use ("com.zj.wuaipojie.Demo" ); utils.$init .overload ('java.lang.String' ).implementation = function (str ){ console .log (str); str = "52" ; this .$init(str); } }
4.Hook字段 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function hookTest5 ( ){ Java .perform (function ( ){ var utils = Java .use ("com.zj.wuaipojie.Demo" ); utils.staticField .value = "我是被修改的静态变量" ; console .log (utils.staticField .value ); Java .choose ("com.zj.wuaipojie.Demo" , { onMatch : function (obj ){ obj._privateInt .value = "123456" ; obj.privateInt .value = 9999 ; }, onComplete : function ( ){ } }); }); }
5.Hook内部类 1 2 3 4 5 6 7 8 9 10 11 function hookTest6 ( ){ Java .perform (function ( ){ var innerClass = Java .use ("com.zj.wuaipojie.Demo$innerClass" ); console .log (innerClass); innerClass.$init .implementation = function ( ){ console .log ("eeeeeeee" ); } }); }
6.枚举所有的类与类的所有方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function hookTest7 ( ){ Java .perform (function ( ){ Java .enumerateLoadedClasses ({ onMatch : function (name,handle ){ if (name.indexOf ("com.zj.wuaipojie.Demo" ) !=-1 ){ console .log (name); var clazz =Java .use (name); console .log (clazz); var methods = clazz.class .getDeclaredMethods (); console .log (methods); } }, onComplete : function ( ){} }) }) }
7.枚举所有方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function hookTest8 ( ){ Java .perform (function ( ){ var Demo = Java .use ("com.zj.wuaipojie.Demo" ); var methods =Demo .class .getDeclaredMethods (); for (var j=0 ; j < methods.length ; j++){ var methodName = methods[j].getName (); console .log (methodName); for (var k=0 ; k<Demo [methodName].overloads .length ;k++){ Demo [methodName].overloads [k].implementation = function ( ){ for (var i=0 ;i<arguments .length ;i++){ console .log (arguments [i]); } return this [methodName].apply (this ,arguments ); } } } }) }
8.主动调用 静态方法
1 2 var ClassName =Java .use ("com.zj.wuaipojie.Demo" ); ClassName .privateFunc ("传参" );
非静态方法
1 2 3 4 5 6 7 8 9 10 11 12 var ret = null ; Java .perform (function ( ) { Java .choose ("com.zj.wuaipojie.Demo" ,{ onMatch :function (instance ){ ret=instance.privateFunc ("aaaaaaa" ); }, onComplete :function ( ){ } }); })
这里讲一个CTF中安卓逆向的技巧,如果app内存在一个flag_activity,但是main_activity中没有对该函数进行调用,可以使用如下命令启动该activity。
1 2 3 adb shell su am start -n com.droidlearn.activity_travel/.FlagActivity
7.frida辅助逆向分析嘟嘟牛 1.抓包分析 首先使用HttpCanary抓包嘟嘟牛,输入手机号和密码后点击登录按钮,抓到login包如下:
1 2 3 4 5 6 7 8 9 10 POST /api/user/login HTTP/1.1 If-Modified-Since: Sun, 24 Nov 2024 13 :31 :54 GMT Content-Type : application/json; charset=utf-8 User-Agent: Dalvik/2.1 .0 (Linux; U; Android 10 ; Pixel XL Build/QP1A.191005 .007 .A3) Host: api.dodovip.com Connection: Keep-Alive Accept-Encoding: gzip Content-Length: 250 {"Encrypt" :"NIszaqFPos1vd0pFqKlB42Np5itPxaNH\/\/FDsRnlBfgL4lcVxjXii\/UNcdXYMk0Egsgxk5jT\/u++\nFScUC0CWLqgQd5Ch0CGNlWbC31xZEvotZVwDxb4yfwXObnJclSJqRmUD9r4JeX2iP8wa2TLNGQ7v\nlHkV7rgE82NSubt\/+Bq3PrOC+sdWZSZooFXh19hqgYFyxQlXOBPLFCzBPku+q5Er5o4JZov\/\n" }
这里的Encrypt是post请求的内容,因此我们要逆向分析他的加密流程,并复现加密算法,最后复现协议,使用python的request发包即可模拟实现用户登录操作,以此来实现批量登录 。
2.静态分析&frida动态辅助分析 1、首先使用jadx打开,通过搜索字符串Encrypt来定位。
找到如下两处,一个addRequestMap,另一个paraMap,由于不知道具体是调用的哪个方法或者都调用了,因此使用frida来辅助分析。
用frida实现两个函数并加入console.log打印内容来分析程序是否执行了该函数。
hook代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Java .perform (function ( ){ var jsonRequest = Java .use ("com.dodonew.online.http.JsonRequest" ); jsonRequest.paraMap .implementation = function (addMap ) { console .log ("paraMap params: " + addMap); var res = this .addRequestMap (addMap); return res; } jsonRequest.addRequestMap .overload ('java.util.Map' , 'int' ).implementation = function (addMap,a ) { var hashmap = Java .cast (addMap,Java .use ("java.util.HashMap" )) console .log ("addRequestMap params: " + hashmap.toString ()); var res = this .addRequestMap (addMap,a); return res; } })
frida输出如下:
1 addRequestMap params : {loginImei=Androidnull , equtype=ANDROID , userPwd=123456 , username=17860581502 }
因此,可以得知程序只走了addRequestMap函数。
2、定位到addRequestMap,静态分析代码
源代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void addRequestMap (Map<String, String> addMap, int a) { String time = System.currentTimeMillis() + "" ; if (addMap == null ) { addMap = new HashMap <>(); } addMap.put("timeStamp" , time); String code = RequestUtil.paraMap(addMap, Config.BASE_APPEND, "sign" ); String encrypt = RequestUtil.encodeDesMap(code, this .desKey, this .desIV); JSONObject obj = new JSONObject (); try { obj.put("Encrypt" , encrypt); this .mRequestBody = obj + "" ; } catch (JSONException e) { e.printStackTrace(); } }
由上方addRequestMap的输出可知,参数就是一个map集合,包括固定的loginImei=Androidnull, equtype=ANDROID以及后边的用户名(其实就是手机号)和密码。
这里的大致逻辑是获取当前时间,并将时间加入到集合中。之后调用RequestUtil.paraMap函数对addMap进行处理,获取一个code。
之后,把code传给RequestUtil.encodeDesMap进行Des加密。
3、跟进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 "" ; } }
首先使用frida hook该函数获取参数内容。
hook代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 var requestUtil = Java .use ("com.dodonew.online.http.RequestUtil" );requestUtil.paraMap .overload ('java.util.Map' , 'java.lang.String' , 'java.lang.String' ).implementation = function (addMap,key,str ) { var hashmap = Java .cast (addMap,Java .use ("java.util.HashMap" )) console .log ("requestUtil.paraMap param1: " + hashmap.toString ()) console .log ("requestUtil.paraMap param2: " + key) console .log ("requestUtil.paraMap param3: " + str) var res = this .paraMap (addMap,key,str); console .log ("requestUtil.paraMap res: " + res) return res; }
输出如下:
1 2 3 4 requestUtil.paraMap param1 : {timeStamp=1732455562761 , loginImei=Androidnull , equtype=ANDROID , userPwd=123456 , username=17860581502 } requestUtil.paraMap param2 : sdlkjsdljf0j2fsjk requestUtil.paraMap param3 : sign requestUtil.paraMap res : {"equtype" :"ANDROID" ,"loginImei" :"Androidnull" ,"sign" :"FDA8AF1573CEDF84AF1DCC521609E92F" ,"timeStamp" :"1732455562761" ,"userPwd" :"123456" ,"username" :"17860581502" }
且每次运行都一样,可知参数2和参数3是固定的字符串。
之后分析逻辑,大致为将map集合中各项以字符串方式拼接起来后进行md5加密,之后再转大写得到checkCode,并把checkCode作为值,sign作为键放到map里,返回map。
4、跟进md5验证
frida hook一下md5函数,看看requestUtil.paraMap返回时的参数sign是不是多个拼接后的md5_大写。
hook代码如下:
1 2 3 4 5 6 7 8 9 var utils = Java .use ("com.dodonew.online.util.Utils" )utils.md5 .implementation = function (a ) { console .log ("md5 param: " + a) var res = this .md5 (a) console .log ("md5 res: " + res) return res }
输出如下:
1 2 md5 param : equtype=ANDROID &loginImei=Androidnull &timeStamp=1732455562761 &userPwd=123456 &username=17860581502 &key=sdlkjsdljf0j2fsjk md5 res : fda8af1573cedf84af1dcc521609e92f
可知参数确实是各项拼接的,并且使用CyberChef等验证可知为正常的md5,未经魔改。
5、分析RequestUtil.encodeDesMap函数
源代码如下:
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 public static String encodeDesMap (String data, String desKey, String desIV) { try { DesSecurity ds = new DesSecurity (desKey, desIV); return ds.encrypt64(data.getBytes("UTF-8" )); } catch (Exception e) { e.printStackTrace(); return "" ; } } public class DesSecurity { Cipher deCipher; Cipher enCipher; public DesSecurity (String key, String iv) throws Exception { if (key == null ) { throw new NullPointerException ("Parameter is null!" ); } InitCipher(key.getBytes(), iv.getBytes()); } private void InitCipher (byte [] secKey, byte [] secIv) throws Exception { MessageDigest md = MessageDigest.getInstance("MD5" ); md.update(secKey); DESKeySpec dsk = new DESKeySpec (md.digest()); SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES" ); SecretKey key = keyFactory.generateSecret(dsk); IvParameterSpec iv = new IvParameterSpec (secIv); this .enCipher = Cipher.getInstance("DES/CBC/PKCS5Padding" ); this .deCipher = Cipher.getInstance("DES/CBC/PKCS5Padding" ); this .enCipher.init(1 , key, iv); this .deCipher.init(2 , key, iv); } public String encrypt64 (byte [] data) throws Exception { return Base64.encodeToString(this .enCipher.doFinal(data), 0 ); } public byte [] decrypt64(String data) throws Exception { return this .deCipher.doFinal(Base64.decode(data, 0 )); } }
分析加密逻辑,大致为首先对传入的key进行md5加密,之后初始化了一个SecretKeyFactory对象来进行EES加密,模式为DES/CBC/PKCS5Padding,最后的encrypt64即使用该DES加密器通过key,iv对data进行DES加密并转成base64字节串。
那么,首先hook一下encodeDesMap得到参数key和iv。
hook代码如下:
1 2 3 4 5 6 7 8 9 10 requestUtil.encodeDesMap .overload ('java.lang.String' , 'java.lang.String' ,'java.lang.String' ).implementation = function (code,key,iv ) { console .log ("encodeDesMap param1: " + code) console .log ("encodeDesMap param2: " + key) console .log ("encodeDesMap param3: " + iv) var res = this .encodeDesMap (code,key,iv) console .log ("encodeDesMap res: " + res) return res }
输出如下:
1 2 3 4 5 6 encodeDesMap param1 : {"equtype" :"ANDROID" ,"loginImei" :"Androidnull" ,"sign" :"FDA8AF1573CEDF84AF1DCC521609E92F" ,"timeStamp" :"1732455562761" ,"userPwd" :"123456" ,"username" :"17860581502" } encodeDesMap param2 : 65102933 encodeDesMap param3 : 32028092 encodeDesMap res : NIszaqFPos 1vd0pFqKlB42Np5itPxaNH FScUC 0CWLqgQd5Ch0CGNlWbC31xZEvotZVwDxb4yfwXObnJclSJqRmUD9r4JeX2iP8wa2TLNGQ7vlHkV7rgE82NSubt/+Bq3PrOC +sdWZSZooFXh19hqgYFyxQlXOBPLFCzBPku+q5Er5o4JZov/
可以发现这里最后的返回值就是我们抓包获得的post的内容(略微不一样是因为抓包得到的有些字符进行了url转义编码),并且key和iv也是程序中的已初始化变量,是固定的。
那么到此,我们就一步步分析完了所有的加密流程。
3.算法还原 接下来就模拟app的逻辑来还原加密算法。
首先是将时间戳和一些输入信息以及一些固有值组成一个map集合。
之后使用字符串拼接各键值对并进行md5加密后转大写获取sign值,再加入到map集合中。
将集合转成字符串,通过DES进行加密,这里的key是经过md5加密后的(由于是DES加密,所以会取前八位 )。
DES加密后转成base64字节串。
这里选择使用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 import base64from Crypto.Cipher import DESfrom Crypto.Util.Padding import padimport hashlibtime = "1732455562761" username = "17860581502" password = "123456" key = "65102933" iv = "32028092" def get_sign (username,password ): data = "equtype=ANDROID&loginImei=Androidnull&timeStamp=" + time + "&userPwd=" + password + "&username=" + username + "&key=sdlkjsdljf0j2fsjk" code = hashlib.md5(data.encode('utf-8' )).hexdigest() return str (code).upper() def DesEncode (code,key,iv ): data = code.encode('utf-8' ) key_md5 = hashlib.md5(key.encode('utf-8' )).hexdigest() key_byte = bytes .fromhex(key_md5) iv_byte = iv.encode('utf-8' ) cipher = DES.new(key_byte[:8 ], DES.MODE_CBC, iv_byte) padded_data = pad(data, DES.block_size) ciphertext = cipher.encrypt(padded_data) ciphertext_base64 = base64.b64encode(ciphertext) return ciphertext_base64 code = get_sign(username,password) code_ = '{"equtype":"ANDROID","loginImei":"Androidnull","sign":"' + code + '","timeStamp":"' + time + '","userPwd":"' + password + '","username":"' + username + '"}' res_byte = DesEncode(code_,key,iv) res_string = res_byte.decode('utf-8' ) print (res_string)
完成,结果与抓到的请求包中的数据一致!
4.协议还原 最后进行协议还原,使用requests进行post发包模拟用户登录操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 import requestsimport jsonurl = "http://api.dodovip.com/api/user/login" data = json.dumps({"Encrypt" : res_string}) print (data)headers = { "content-type" : "application/json; charset=utf-8" , "User-Agent" : "Dalvik/2.1.0 (Linux; U; Android 10; Pixel XL Build/QP1A.191005.007.A3)" } r = requests.post(url = url,data = data,headers = headers) recv = r.text print (recv)
得到的recv输出如下:
1 2v+DC2gq7RuAC8PE5GZz5wH3/y9ZVcWhFwhDY9L19g9iEd075+Q7xwewvfIN0g0ec/NaaF43/S0=
对比抓包得到的响应体内容,两者一致。
对该响应的再调用DES解密得到如下:
DES解密代码如下:
1 2 3 4 5 6 7 8 9 10 11 def DesDecode (code,key,iv ): data = code.encode('utf-8' ) data = base64.b64decode(data) key_md5 = hashlib.md5(key.encode('utf-8' )).hexdigest() key_byte = bytes .fromhex(key_md5) iv_byte = iv.encode('utf-8' ) cipher = DES.new(key_byte[:8 ], DES.MODE_CBC, iv_byte) decrypted_data = cipher.decrypt(data) decrypted_data = unpad(decrypted_data, DES.block_size) return decrypted_data.decode('utf-8' ) print (DesDecode(recv,key,iv))
结果如下:
1 {"code" :-1 ,"message" :"账号或密码错误" ,"data" :{}}
如果,post发送的data不对,recv输出如下:
1 2 2v+DC2gq7RsxFUyk6XO+ZO/zrkhUqGNy 解密后为 {"code" :4 ,"message" :4 }
另外,后端可能对时间做了检测,如果请求包中的时间戳与当前实际时间相差太大的话,即使伪造的Encrypt数据正确,也不会成功。
此时recv输出如下:
1 2 2v+DC2gq7Rs2vBLjHBwgrO0gyauGMTE6 解密后为 {"code" :2 ,"message" :2 }
5.完整exp: 1、hook.js
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 Java .perform (function ( ){ var jsonRequest = Java .use ("com.dodonew.online.http.JsonRequest" ); jsonRequest.paraMap .implementation = function (addMap ) { console .log ("paraMap params: " + addMap); var res = this .addRequestMap (addMap); return res; } jsonRequest.addRequestMap .overload ('java.util.Map' , 'int' ).implementation = function (addMap,a ) { var hashmap = Java .cast (addMap,Java .use ("java.util.HashMap" )) console .log ("addRequestMap params: " + hashmap.toString ()); var res = this .addRequestMap (addMap,a); return res; } var utils = Java .use ("com.dodonew.online.util.Utils" ) utils.md5 .implementation = function (a ) { console .log ("md5 param: " + a) var res = this .md5 (a) console .log ("md5 res: " + res) return res } var requestUtil = Java .use ("com.dodonew.online.http.RequestUtil" ); requestUtil.paraMap .overload ('java.util.Map' , 'java.lang.String' , 'java.lang.String' ).implementation = function (addMap,key,str ) { var hashmap = Java .cast (addMap,Java .use ("java.util.HashMap" )) console .log ("requestUtil.paraMap param1: " + hashmap.toString ()) console .log ("requestUtil.paraMap param2: " + key) console .log ("requestUtil.paraMap param3: " + str) var res = this .paraMap (addMap,key,str); console .log ("requestUtil.paraMap res: " + res) return res; } requestUtil.encodeDesMap .overload ('java.lang.String' , 'java.lang.String' , 'java.lang.String' ).implementation = function (code,key,iv ) { console .log ("encodeDesMap param1: " + code) console .log ("encodeDesMap param2: " + key) console .log ("encodeDesMap param3: " + iv) var res = this .encodeDesMap (code,key,iv) console .log ("encodeDesMap res: " + res) return res } })
2、main.py
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 import base64from Crypto.Cipher import DESfrom Crypto.Util.Padding import padfrom Crypto.Util.Padding import unpadimport hashlibimport requestsimport jsontime = "1732458560882" //这里需要用frida hook获取到的时间戳替换,否则返回{"code" :2 ,"message" :2 } username = "17860581502" password = "123456" key = "65102933" iv = "32028092" def get_sign (username,password ): data = "equtype=ANDROID&loginImei=Androidnull&timeStamp=" + time + "&userPwd=" + password + "&username=" + username + "&key=sdlkjsdljf0j2fsjk" code = hashlib.md5(data.encode('utf-8' )).hexdigest() return str (code).upper() def DesEncode (code,key,iv ): data = code.encode('utf-8' ) key_md5 = hashlib.md5(key.encode('utf-8' )).hexdigest() key_byte = bytes .fromhex(key_md5) iv_byte = iv.encode('utf-8' ) cipher = DES.new(key_byte[:8 ], DES.MODE_CBC, iv_byte) padded_data = pad(data, DES.block_size) ciphertext = cipher.encrypt(padded_data) return base64.b64encode(ciphertext) def DesDecode (code,key,iv ): data = code.encode('utf-8' ) data = base64.b64decode(data) key_md5 = hashlib.md5(key.encode('utf-8' )).hexdigest() key_byte = bytes .fromhex(key_md5) iv_byte = iv.encode('utf-8' ) cipher = DES.new(key_byte[:8 ], DES.MODE_CBC, iv_byte) decrypted_data = cipher.decrypt(data) decrypted_data = unpad(decrypted_data, DES.block_size) return decrypted_data.decode('utf-8' ) code = get_sign(username,password) code_ = '{"equtype":"ANDROID","loginImei":"Androidnull","sign":"' + code + '","timeStamp":"' + time + '","userPwd":"' + password + '","username":"' + username + '"}' print (code_)res_byte = DesEncode(code_,key,iv) res_string = res_byte.decode('utf-8' ) print (res_string)url = "http://api.dodovip.com/api/user/login" data = json.dumps({"Encrypt" : res_string}) print (data)headers = { "content-type" : "application/json; charset=utf-8" , "User-Agent" : "Dalvik/2.1.0 (Linux; U; Android 10; Pixel XL Build/QP1A.191005.007.A3)" } r = requests.post(url = url,data = data,headers = headers) recv = r.text print (recv)print (DesDecode(recv,key,iv))
6.使用算法转发 当然也可以使用frida的算法转发,就是对于一些比较复杂的加密逻辑,我们直接通过frida主动调用获得加密后的值(sign),就不需要逆向分析加密逻辑了,然后通过转发发送到python里,在python里通过爬虫实现批量处理的操作。这里需要使用到python的frida库,而frida-server是使用的frida-tools。
完整代码 :
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 import base64import hashlibfrom Crypto.Cipher import DESfrom Crypto.Util.Padding import padfrom Crypto.Util.Padding import unpadimport jsonimport fridaimport requestsjsCode = """ function hookTest(username,password){ var result; Java.perform(function(){ var time = new Date().getTime(); var str = Java.use('java.lang.String'); var signData = str.$new("equtype=ANDROID&loginImei=Androidnull&timeStamp=" + time + "&userPwd=" + password + "&username=" + username + "&key=sdlkjsdljf0j2fsjk") //md5 var utils = Java.use("com.dodonew.online.util.Utils") var sign = utils.md5(signData).toUpperCase(); console.log('sign: ',sign); var encryptData = '{"equtype":"ANDROID","loginImei":"Androidnull","sign":"' + sign + '","timeStamp":"' + time + '","userPwd":"' + password + '","username":"' + username + '"}' var requestUtil = Java.use("com.dodonew.online.http.RequestUtil"); //requestUtil.encodeDesMap var Encrypt = requestUtil.encodeDesMap(encryptData,'65102933','32028092'); //传入encryptData、key和iv console.log('Encrypt: ',Encrypt); result = Encrypt; }); return result; } rpc.exports = { hooktest : hookTest //这里进行导出供python接收 } """ process = frida.get_device_manager().add_remote_device('192.168.0.104:27042' ).attach("com.dodonew.online" ) script = process.create_script(jsCode) script.load() result = script.exports.hooktest('17860581502' , '123456' ) url = "http://api.dodovip.com/api/user/login" data = json.dumps({"Encrypt" : result}) print (data)headers = { "content-type" : "application/json; charset=utf-8" , "User-Agent" : "Dalvik/2.1.0 (Linux; U; Android 10; Pixel XL Build/QP1A.191005.007.A3)" } r = requests.post(url=url, data=data, headers=headers) recv = r.text print (recv)def DesDecode (code,key,iv ): data = code.encode('utf-8' ) data = base64.b64decode(data) key_md5 = hashlib.md5(key.encode('utf-8' )).hexdigest() key_byte = bytes .fromhex(key_md5) iv_byte = iv.encode('utf-8' ) cipher = DES.new(key_byte[:8 ], DES.MODE_CBC, iv_byte) decrypted_data = cipher.decrypt(data) decrypted_data = unpad(decrypted_data, DES.block_size) return decrypted_data.decode('utf-8' ) key = "65102933" iv = "32028092"
完结撒花!!!