首页
社区
课程
招聘
[原创]从0开始实现一个简易的主动调用框架
2021-10-29 14:32 25350

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

2021-10-29 14:32
25350

<!-- @import "[TOC]" {cmd="toc" depthFrom=1 depthTo=6 orderedList=false} -->

 

<!-- code_chunk_output -->

<!-- /code_chunk_output -->

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

总述

根据寒冰老师的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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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,

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
    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中的函数,看一下效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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权限,否则读不了我们的插件

1
2
3
unzip app-debug.apk
cd lib/arm64-v8a/
adb push libnative-lib.so /sdcard/r0.so

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

调用Native函数

由于部分Native函数的参数较难构造,所以这里可以hook住,它的注册函数然后通过更改参数的方式来进行主动调用,使用之前搞得inlinehook框架,
https://bbs.pediy.com/thread-269757.htm

1
2
3
4
5
6
7
8
9
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *unused) {
 
 
    mainfun("_ZN3art9ArtMethod14RegisterNativeEPKvb", "libart.so",
            reinterpret_cast<void *>(regist));
 
    return JNI_VERSION_1_4;
 
}

然后再过滤函数名,如果是我们的函数就改掉它的注册值,由于我的inlinehook框架不完善所以这里暂且只能使用x18做栈的传值的方式更改参数

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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 ) {
        //    _ZN3art9ArtMethod19GetObsoleteDexCacheEv
return "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


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞3
打赏
分享
最新回复 (1)
雪    币: 120
活跃值: (110)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
牛牛很牛 2021-11-18 18:16
2
0
不错学习了
游客
登录 | 注册 方可回帖
返回