使用frida-gumjs将sekiro注入到任意app进程
使用frida-gumjs将sekiro注入到任意app进程
在之前的文章中,我们讨论了原生环境下,使用NPS和Sekiro在黑盒调用场景下的一些差别和优劣,其中提到Sekiro相比于NPS,对目标应用的代码有入侵性,在实际应用时,如果要向目标app进程注入sekiro,常用的途径一般是利用xposed模块,除此之外可能就需要解包添加代码。而本文重点介绍这一需求的另一种实现方式,即先将Sekiro打包成dex文件,然后通过Frida进行载入。当然,在这之前我们需要先将Frida进行持久化,实现的方式有两中,一是使用frida-inject实现脱离pc环境运行,二是借助Magisk的Riru模块将frida-gumjs引擎注入孵化器。由于第一种方案需要执行shell命令来启动app,因此我们暂不考虑。第二种实现方式见文章。
1.将Sekiro打包成Dex文件
这一步骤我们直接用Android Studio实现,新建一个空白的安卓项目,然后引入Sekiro库,如下
implementation 'com.virjar.sekiro.business:sekiro-business-api:1.4'
Sekiro新版本是从其商业版Fork而来,原先免费版已经停止维护,新版本的api使用了一些注解特性,由于我没有找到如何使用frida去实现Java的注解功能,因此这部分我们使用Java去实现,如下,我们硬编码实现Sekiro需要的SekiroRequestInitializer、RequestHandler两个接口
package com.virjar.sekiro.inject;
import com.virjar.sekiro.business.api.interfaze.HandlerRegistry;
import com.virjar.sekiro.business.api.interfaze.SekiroRequest;
import com.virjar.sekiro.business.api.interfaze.SekiroRequestInitializer;
public class BaseSekiroRequestInitializer implements SekiroRequestInitializer {
@Override
public void onSekiroRequest(SekiroRequest sekiroRequest, HandlerRegistry handlerRegistry) {
handlerRegistry.registerSekiroHandler(new BaseRequestHandler());
}
}
package com.virjar.sekiro.inject;
import com.virjar.sekiro.business.api.fastjson.JSONObject;
import com.virjar.sekiro.business.api.interfaze.Action;
import com.virjar.sekiro.business.api.interfaze.AutoBind;
import com.virjar.sekiro.business.api.interfaze.RequestHandler;
import com.virjar.sekiro.business.api.interfaze.SekiroRequest;
import com.virjar.sekiro.business.api.interfaze.SekiroResponse;
@Action("handler")
public class BaseRequestHandler implements RequestHandler {
@AutoBind
private String data;
//如需自定义实现 可以用frida去重写此方法
@Override
public void handleRequest(SekiroRequest sekiroRequest, SekiroResponse sekiroResponse) {
sekiroResponse.success("ok");
}
}
需要注意的是,上面的BaseRequestHandler放了一个空实现,在实际使用中,我们往往需要根据请求参数,执行目标app的方法,然后返回结果,这一功能可以通过Frida去Hook handleRequest方法实现。
项目准备完毕后,执行"Build-Rebuild Project",执行完毕后在app/build/intermediates/dex目录下能找到对应的dex文件,然后我们将dex文件推送到设备目录下,这个目录可以自已定,例如/data/local/tmp下
2.编写Frida脚本
Frida脚本比较简单,加载dex,然后启动Sekiro即可,例如:
Java.perform(function(){
Java.openClassFile("/data/local/tmp/sekiro.dex").load();
var id = "client:"+new Date().getTime();
var SekiroIns = Java.use('com.virjar.sekiro.business.api.SekiroClient').$new("youer-group",id , "youerhost", youerport);
SekiroIns.setupSekiroRequestInitializer(Java.use('com.virjar.sekiro.inject.BaseSekiroRequestInitializer').$new()).start();
log("sekiro 注入成功!");
});
如需自定义请求处理,则重载handleRequest即可,如下
var BaseRequestHandler = Java.use('com.virjar.sekiro.inject.BaseRequestHandler');
BaseRequestHandler.handleRequest.implementation = function(sekiroRequest,sekiroResponse)
{
//处理请求代码
}
3.测试连接
调用sekiro的api,查看对应group的在线client列表,如下图
4.快速调用
在上述基础上,我对dex做了一些修改,使其能够兼容一些方法调用,前提是被调用的方法的参数类型,返回值等都是基本类型。具体的实现效果是,对于符合要求的方法,frida脚本无需做任何修改,只要在sekiro的api中传入正确的参数即可。
1)入参结构体
例子
{
"classname": "com.xxy.test.MainActivity",
"method": "GetText",
"staticmethod": false,
"instancefuncentity": {
"classname": "com.xxy.test.MainActivity",
"method": "$new",
"parms": [
{
"value": "xxy",
"type": "java.lang.String"
}
]
},
"parms": [
{
"value": "xxy",
"type": "java.lang.String"
},
{
"value": "false",
"type": "boolean"
},
{
"value": "nb",
"type": "[B"
}
]
}
对应的Java实体类
package com.virjar.sekiro.inject;
import java.util.List;
//调用所需参数
public class CallEntity {
//待调用类名 全路径
public String classname;
//方法名
public String method;
//是否为静态方法
public boolean staticmethod;
//如果为非静态方法 此字段为获取对象的方法
public CallEntity instancefuncentity;
//参数列表
public List<CallParm> parms;
public class CallParm {
//参数 字符串值
public String value;
//参数类型
public String type;
}
}
2)修改代码实现
修改handle实现,将上述json内容解释并执行调用,由于传入的参数不定长,因此用到了大量反射实现
package com.virjar.sekiro.inject;
import com.virjar.sekiro.business.api.fastjson.JSONObject;
import com.virjar.sekiro.business.api.interfaze.Action;
import com.virjar.sekiro.business.api.interfaze.AutoBind;
import com.virjar.sekiro.business.api.interfaze.RequestHandler;
import com.virjar.sekiro.business.api.interfaze.SekiroRequest;
import com.virjar.sekiro.business.api.interfaze.SekiroResponse;
@Action("handler")
public class BaseRequestHandler implements RequestHandler {
@AutoBind
private String data;
//自定义实现 可以用frida去重写此方法
@Override
public void handleRequest(SekiroRequest sekiroRequest, SekiroResponse sekiroResponse) {
try {
CallEntity entity = JSONObject.parseObject(data,CallEntity.class);
sekiroResponse.success(CallMethods.CallMethod(entity,true));
} catch (Exception ex) {
sekiroResponse.failed(405, ex);
}
}
}
package com.virjar.sekiro.inject;
import org.json.JSONObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
public class CallMethods {
//实例保存在map中 不重复实例化
static HashMap<String,Object> InsMap = new HashMap<>();
//反射调用
public static Object CallMethod(CallEntity entity,boolean asstring) throws Exception {
int size = entity.parms == null?0:entity.parms.size();
Class[] parmtypes = new Class[size];
Object[] parmvalues = new Object[size];
//组装请求类型和参数列表 将字符串解释为java基本类型
for (int i = 0; i < size; i++) {
CallEntity.CallParm p = entity.parms.get(i);
parmtypes[i] = FridaType2Java(p.type);
parmvalues[i] = String2Target(p.value, p.type);
}
//目标类
Class call_class = FridaType2Java(entity.classname);
//反射method相关方法供后续使用
Method getDeclaredMethod = Class.class.getDeclaredMethod("getDeclaredMethod",String.class, Class[].class);
Method getMethod = Class.class.getDeclaredMethod("getMethod",String.class, Class[].class);
Method methodinvoke = Method.class.getDeclaredMethod("invoke",Object.class, Object[].class);
Method targetmethod = null;
Object result = null;
//如果是非静态函数调用 需要用到构造函数 $new是frida写法
if(entity.method.toLowerCase().equals("$new"))
{
if(!InsMap.containsKey(entity.classname))
{
Method con_method = Class.class.getDeclaredMethod("getConstructor", Class[].class);
Constructor con = (Constructor) con_method.invoke(call_class,new Object[]{parmtypes});
result = Constructor.class.getDeclaredMethod("newInstance",Object[].class).invoke(con,new Object[]{parmvalues});
InsMap.put(entity.classname,result);
}
else
{
result = InsMap.get(entity.classname);
}
}
else {
Object temp = getDeclaredMethod.invoke(call_class, new Object[]{entity.method,parmtypes});
if (temp == null) {
temp = getMethod.invoke(call_class, new Object[]{entity.method,parmtypes});
}
if (temp == null) {
throw new Exception("未找到调用方法或其重载");
}
targetmethod = (Method) temp;
Object ins = null;
//非静态方法需要实例化
if (!entity.staticmethod) {
if(entity.instancefuncentity == null||entity.instancefuncentity.classname == null||entity.instancefuncentity.classname.isEmpty())
{
throw new Exception("非静态方法需要提供获取实例的函数");
}
ins = CallMethod(entity.instancefuncentity,false);
if (ins == null) {
throw new Exception("调用所需实例对象获取失败");
}
}
result = methodinvoke.invoke(targetmethod, new Object[]{ins,parmvalues});
if(asstring)
{
result = Target2String(result,targetmethod.getReturnType().getName());
}
}
return result;
}
//frida 的类型方式转java
public static Class FridaType2Java(String tp) throws ClassNotFoundException {
Class type = null;
switch (tp) {
case "java.lang.String":
type = String.class;
break;
case "[B":
type = byte[].class;
break;
case "boolean":
type = boolean.class;
break;
case "int":
type = int.class;
break;
case "float":
type = float.class;
break;
case "double":
type = double.class;
break;
default:
type = Class.forName(tp);
break;
}
return type;
}
public static String Target2String(Object value, String type) {
String result = null;
System.out.println(type);
switch (type) {
case "[B":
result = new String((byte[]) value);
break;
default:
result = String.valueOf(value);
break;
}
return result;
}
//类型转换 支持几种基本类型
public static Object String2Target(String value, String type) {
Object result = null;
try {
switch (type) {
case "[B":
result = value.getBytes();
break;
case "boolean":
result = Boolean.parseBoolean(value.toLowerCase());
break;
case "float":
result = Float.parseFloat(value.toLowerCase());
break;
case "int":
result = Integer.parseInt(value);
break;
case "long":
result = Long.parseLong(value);
break;
case "double":
result = Double.parseDouble(value);
break;
case "java.lang.String":
default:
result = value;
break;
}
} catch (Exception e) {
result = value;
}
return result;
}
}
3)测试调用
重新Rebuild Project生成dex并推送到设备目录,待测试调用的方法如下
package com.xxy.test;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void Click(View v)
{
TextView tv = findViewById(R.id.tv);
tv.setText(GetText("xxy",true,"nb".getBytes()));
}
public static String GetText(String str1,boolean flag,byte [] bts)
{
return str1 + new String(bts) + String.valueOf(flag);
}
}
测试调用GetText方法,我们将传入的data参数值为:
{
"classname": "com.xxy.test.MainActivity",
"method": "GetText",
"staticmethod": true,
"instancefuncentity": {},
"parms": [
{
"value": "xxy",
"type": "java.lang.String"
},
{
"value": "true",
"type": "boolean"
},
{
"value": "nb666",
"type": "[B"
}
]
}
调用结果如下:
5.资源
已封装快速调用的Sekiro Dex文件
标题:使用frida-gumjs将sekiro注入到任意app进程
作者:Cubeeeee
地址:http://blog.nps.fuguicun.com/articles/2021/07/20/1626768588189.html