查看原文
其他

从0开始实现一个简易的主动调用框架

r0ysue 看雪学苑 2022-07-01


本文为看雪论坛优秀文章
看雪论坛作者ID:r0ysue


总述


根据寒冰老师的frida manage和0.0.0.0大佬的内置frida rom,发现可以直接用System.load加绝对路径的方式将so加载到内存中,那么这样我们就可以在app启动之前执行代码,那么就可以执行hook等一系列操作了,代码都能自己定制那么就没那么多特征可以检测了。

加载方案


这里依然以ActivityThread中的函数handleBindApplication作为加载时机,我这里想到一个简单的方案,就是将sdcard中的so复制到程序的私有目录,然后修改文件的读写权限,通过System.load来用绝对路径加载so,然后通过JNI_Onload或init来执行代码,这里贴一下寒冰老师分析handleBindApplication的代码(太清楚了)。
https://bbs.pediy.com/thread-252630.htm
private void handleBindApplication(AppBindData data) { //step 1: 创建LoadedApk对象 data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo); ... //step 2: 创建ContextImpl对象; final ContextImpl appContext = ContextImpl.createAppContext(this, data.info); //step 3: 创建Instrumentation mInstrumentation = new Instrumentation(); //step 4: 创建Application对象;在makeApplication函数中调用了newApplication,在该函数中又调用了app.attach(context),在attach函数中调用了Application.attachBaseContext函数 Application app = data.info.makeApplication(data.restrictedBackupMode, null); mInitialApplication = app; //step 5: 安装providers List<ProviderInfo> providers = data.providers; installContentProviders(app, providers); //step 6: 执行Application.Create回调 mInstrumentation.callApplicationOnCreate(app);}

我们可以从这里找一个时机来加载我们的so,我这里选择了创建ContextImpl对象之后直接加载我们的so,这里代码大部分来自0.0.0.0大佬的内置frida。
public static void mycopy(String srcFileName, String trcFileName) { InputStream in = null; OutputStream out = null; try { // in = File.open(srcFileName); in = new FileInputStream(srcFileName); out = new FileOutputStream(trcFileName); byte[] bytes = new byte[1024]; int i; while ((i = in.read(bytes)) != -1) out.write(bytes, 0, i); } catch (IOException e) { e.printStackTrace(); } finally { try { if (in != null) in.close(); if (out != null){ out.flush(); out.close(); } } catch (IOException e) { e.printStackTrace(); } } }private void handleBindApplication(AppBindData data) {...... final ContextImpl appContext = ContextImpl.createAppContext(this, data.info); ContextImpl context = appContext; ActivityManager mAm = (ActivityManager) context.getSystemService("activity"); String activity_packageName = mAm.getRunningTasks(1).get(0).topActivity.getPackageName();//获得私有目录 if (activity_packageName.indexOf("com.android") < 0) {//不包括系统目录 String tagPath = "/data/data/" + activity_packageName + "/r0.so";//64位so的目录 String tagPath2 = "/data/data/" + activity_packageName + "/r032.so";//32位的so目录 File file1 = new File(tagPath); File file2 = new File(tagPath2); mycopy("/sdcard/r0.so", tagPath);//复制so到私有目录 mycopy("/sdcard/r032.so", tagPath2); int perm = FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IRWXO; FileUtils.setPermissions(tagPath, perm, -1, -1);//将权限改为777 FileUtils.setPermissions(tagPath2, perm, -1, -1);// com.android.systemui if (file1.exists()) { Log.e("r0ysue", System.getProperty("os.arch"));//判断是64位还是32位 if (System.getProperty("os.arch").indexOf("64") >= 0) { System.load(tagPath); file1.delete();//用完就删否则不会更新 } else { System.load(tagPath2); file2.delete(); } }}......}


java函数调用方案


那么我们的so可以通过System.load加载到内存中了,我们要如何主动调用函数呢?其实就是在于我们要如何在so层执行java或者native方法,所以前面我写了inlinehook,javahook,都可以集成到这套方案里面,只要将我们制作的so放到/sdcard/r0.so,那么所有的逻辑都可以由我们来控制。

比如我这里举一个例子,能否在每个程序打开前输出其进程名,这里我们就可以通过反射的方式来执行java中的函数,看一下效果:
const char * getprocessname(JNIEnv* env){ jclass ActivityThread=env->FindClass("android/app/ActivityThread"); jmethodID currentProcessName=env->GetStaticMethodID(ActivityThread,"currentProcessName","()Ljava/lang/String;"); jstring name= static_cast<jstring>(env->CallStaticObjectMethod(ActivityThread,currentProcessName)); const char * name1=env->GetStringUTFChars(name,0); return name1; }JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *unused) { JNIEnv* env= nullptr; vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_4); const char* myname= getprocessname(env); __android_log_print(6,"r0ysue","i am from %s",myname); return JNI_VERSION_1_4;}

进入app目录,将app编译后解压,将我们的so送入指定目录当然要打开sdcard权限,否则读不了我们的插件。
unzip app-debug.apkcd lib/arm64-v8a/adb push libnative-lib.so /sdcard/r0.so

随便打开一个app看一下效果,不错还能用。


调用Native函数


由于部分Native函数的参数较难构造,所以这里可以hook住,它的注册函数然后通过更改参数的方式来进行主动调用,使用之前搞的inlinehook框架。
https://bbs.pediy.com/thread-269757.htm
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *unused) { mainfun("_ZN3art9ArtMethod14RegisterNativeEPKvb", "libart.so", reinterpret_cast<void *>(regist)); return JNI_VERSION_1_4; }

然后再过滤函数名,如果是我们的函数就改掉它的注册值,由于我的inlinehook框架不完善所以这里暂且只能使用x18做栈的传值的方式更改参数。
void regist(void* a,void* b,int c){ const char* ds=getartmethod((unsigned int *)a);if(strstr(ds,"xxxxxx")){//过滤函数名 st=b; long* d= reinterpret_cast<long *>(myreplace);//替换的注册函数 __android_log_print(6,"r0ysue","%p",d); __asm__("str %[input_n], [X18,#0x28]\r\n"//修改registernative的第二个参数,我这里用x18传的调用栈 :[result_m] "=r" (d) :[input_n] "r" (d) ); __android_log_print(6,"r0ysue","register %s",ds);} } void* myreplace(void* a,void* b,int c,void* d){JNIEnv* env= static_cast<JNIEnv *>(a);jobject aa= static_cast<jobject>((docomm(st))(a, b, reinterpret_cast<void *>(c), d));//直接调用原函数if(aa!= nullptr) {//由于返回值是一个jstring判断一下 env->SetObjectArrayElement(static_cast<jobjectArray>(d), 2, env->NewStringUTF("4")); jobject opp= static_cast<jobject>((docomm(st))(a, b, reinterpret_cast<void *>(c), d));//调用将第2个字符串改为4之后的字符串 __android_log_print(6,"r0ysue"," 111111111111 %s",env->GetStringUTFChars( static_cast<jstring>(opp),0));//这个jobject数组是一个长度为5的字符串数组,就是说有5个字符串我随便改一个就好 const char *ss = getclassname(env,myclass); if(strstr(ss,"String")) {//由于返回值是一个jstring判断一下再打印printobjearry(env,(jobjectArray)d);//打印jobject数组 __android_log_print(6, "r0ysue", " ssssssss int:%x %s", c,env->GetStringUTFChars( static_cast<jstring>(aa), 0)); } } return aa;} void printobjearry (JNIEnv* env,jobjectArray a){//打印jobjet数组 int size=env->GetArrayLength(static_cast<jarray>(a)); for(int n=0;n<size;n++) { jobject ax=env->GetObjectArrayElement(static_cast<jobjectArray>(a), n); if(ax!=0) __android_log_print(6, "r0ysue", " ssssssss %d: %s",n, getclassname(env, ax)); }} const char * getartmethod(unsigned int *a1){//ArtMethod中的getName函数直接从ida复制过来的 __int64 v12; // x20 __int64 v13; // x0 _QWORD *v14; // x8 __int64 v15; // x9 char *v16; // x8 const char *result; // x0 unsigned int **v18; // x8 unsigned int *v19; // x9 unsigned int *v20; // x9 int v21; // w10 const char *v22; // x9 const char *v23; // x8 unsigned int **v24; // x20 __int64 v25; // x0 __int64 v26; // x0 unsigned int **v27; // x20 __int64 v28; // x0 __int64 v29; // x0 unsigned int *v30; // [xsp+48h] [xbp+18h] unsigned int *v31; // [xsp+48h] [xbp+18h] v12 = a1[3]; if ( (a1[1] & 0x40000) != 0 ) { // _ZN3art9ArtMethod19GetObsoleteDexCacheEvreturn "cxzcxzcxz"; } else v13 = *(unsigned int *)(*a1 + 0x10LL); v14 = *(_QWORD **)(v13 + 16); v15 = *(unsigned int *)(v14[12] + 8 * v12 + 4); if ( (_DWORD)v15 == -1 ) return 0LL; v16 = (char *)(v14[1] + *(unsigned int *)(v14[9] + 4 * v15)); result = v16 + 1; if ( (*v16 & 0x80000000) != 0 ) { if ( (v16[1] & 0x80000000) != 0 ) { if ( (v16[2] & 0x80000000) != 0 ) { v21 = v16[3]; v22 = v16 + 4; v23 = v16 + 5; if ( v21 >= 0 ) result = v22; else result = v23; } else { result = v16 + 3; } } else { result = v16 + 2; } } return result;}

看一下效果,嗯效果不错,改完之后主动调用的结果为11111标签下的值和之前的值不一样。


总结


可以成功的调用任意的Java函数和Native函数(当然so里面的函数也是和Native函数一样),Native函数有一个难点就是要在它注册之后再调用,所以我直接选择了hook libart.so 中的RegisterNative函数,其实比较难解决的也是classloader的问题,有的动态加载的dex需要很困难才能拿到jclass(当然我们也可以模仿frida实现一个枚举classloader也是很简单的),而且这个插件的写法过于麻烦,但是稳定性是较高的而且完全自己定制就可以随意的更改指纹比较难检测到。



参考资料

https://bbs.pediy.com/thread-266767.htm

https://bbs.pediy.com/thread-252630.htm




 


看雪ID:r0ysue

https://bbs.pediy.com/user-home-799845.htm

*本文由看雪论坛 r0ysue  原创,转载请注明来自看雪社区



# 往期推荐

1.强网拟态线上mobile的两道wp

2.Android APP漏洞之战——权限安全和安全配置漏洞详解

3.钉钉邀请上台功能分析

4.Android APP漏洞之战——Activity漏洞挖掘详解

5.少量虚假控制流混淆后的算法还原案例

6.PHP反序列化漏洞基础





球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存