一、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"
完结撒花!!!