Android SAF实现外置SD卡的写入JAVA层与JNI层hook

文章资讯 2020-01-15 11:40:47

Android SAF实现外置SD卡的写入JAVA层与JNI层hook

JAVA层SAF核心代码
通过DocumentFile来实现写入,Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)发送请求docUri,然后onActivityResult来得到并储存docUri,通过SharedPreference来实现共享。已经封装在静态类SafFile.java中。

获取外置sd卡根目录DocumentFile

public static DocumentFile getBaseDocumentFile(final Context context, final SharedPreferences share) {
        if(context==null) {
            Log.e(LOGTAG, "SafFile.getBaseDocumentFile context is null!");
            return null;
        }
        if(share==null){
            Log.e(LOGTAG, "SafFile.getBaseDocumentFile share is null!");
            return null;
        }       
        DocumentFile base = null;
        Uri docUri = null;
        final String p = share.getString("docUri", null);
        if (p != null)
            docUri = Uri.parse(p);
        base = DocumentFile.fromTreeUri(context, docUri);
        return base;
    }

注意DocumentFile CreateFile好像不只能在本目录下创建,要一层一层往里面走

public static DocumentFile getTargetDirDocumentFile(final DocumentFile base, String path) {
        DocumentFile target = null;
        if (base == null) {
            Log.e(LOGTAG, "SafFile.getTargetDirDocumentFile base is null!");
            return null;
        }
        if(path==null) path="";

        path = path.replace("\", "/");
        final String paths[] = path.split("/");
        int i;
        final int end = paths[paths.length - 1].length() > 0 ? paths.length - 1 : paths.length - 2;
        for (i = 0; i < end; i++) {
            // Log.i(LOGTAG, "getTar... path["+String.valueOf(i)+"], "+paths[i]);
            if (paths[i].equals(base.getName())) {
                if (i >= end - 1) {
                    // Log.i(LOGTAG, "getTar... "+path+" end="+paths[paths.length-1]+" "+ paths[end]);
                    return base;
                }
                i++;
                break;
            }
        }
        // Log.i(LOGTAG, "getTarget... "+base.getName()+" "+path);
        target = base.findFile(paths[i++]);
        // Log.i(LOGTAG, "target, "+ target.getName());
        for (; i < end; i++) {
            if (target == null)
                break;
            // Log.i(LOGTAG, "getTar..., "+path+" "+ target.getName());
            target = target.findFile(paths[i]);
        }
        return target;
    }

获得OutputStream

public static OutputStream getOutputStreamSaf(final Context context, final DocumentFile base, final String path,
            final boolean append) {
        if(context==null) {
            Log.e(LOGTAG, "SafFile.getOutputStreamSaf context is null!");
            return null;
        }
        if(base==null){
            Log.e(LOGTAG, "SafFile.getOutputStreamSaf base is null!");
            return null;
        }

        OutputStream out = null;
        final String mode = append ? "wa" : "w";
        // Log.i(LOGTAG, "getOut.. "+ path +" "+mode);
        final DocumentFile df2 = createFileSaf(base, path, append);
        if (df2 == null) {
            return null;
        }
        try {
            out = context.getContentResolver().openOutputStream(df2.getUri(), mode);
        } catch (final Exception e) {
            Log.e(LOGTAG, "SafFile.getOutputStreamSaf " + e.getClass().getName());
        }
        return out;
    }

获取文件描述符

public static int getFdSaf(final Context context, final DocumentFile base, final String path, final String mode) {
        if(context==null) {
            Log.e(LOGTAG, "SafFile.getFdSaf context is null!");
            return 0;
        }
        if(base==null){
            Log.e(LOGTAG, "SafFile.getFdSaf base is null!");
            return 0;
        }

        ParcelFileDescriptor pfd = null;
        boolean append = false;
        DocumentFile df2 = null;

        if (mode.indexOf('+') != -1 || mode.indexOf('a') != -1)
            append = true;
        if (mode.indexOf('w') == -1)
            append = true;
        df2 = createFileSaf(base, path, append);
        if (df2 == null) {

            Log.e(LOGTAG, "SafFile.getFdSaf, " + path + " error!");
            return 0;
        }
        try {
            pfd = context.getContentResolver().openFileDescriptor(df2.getUri(), mode);
        } catch (final Exception e) {
            Log.e(LOGTAG, "SafFile.getFdSaf " + e.getClass().getName());
        }
        if (pfd == null)
            return 0;
        return pfd.detachFd();
    }

3.JNI层hook核心代码
这里用到了xhook架构,原理上是运行的时候来替换目标动态库的.got表到自己编译的函数地址,通过JNI来调用JAVA层我们写好通过SAF机制得到的文件描述符。

初始化要JNI中要调用的JAVA方法,class为"com/yurisizuku/utils/SafFile"

void nativeInitSafJavaCallbacks(JNIEnv* env, jclass clazz)
{
    LOGI("In nativeInitSafJavaCallbacks start!");
    g_javaGetFD=(*env)->GetStaticMethodID(env, clazz, "getFD", "(Ljava/lang/String;Ljava/lang/String;I)I");
    g_javaMkdir=(*env)->GetStaticMethodID(env, clazz, "mkdir", "(Ljava/lang/String;Ljava/lang/String;I)I");
    g_javaRemove = (*env)->GetStaticMethodID(env, clazz, "remove", "(Ljava/lang/String;Ljava/lang/String;)I");
    LOGI("In nativeInitSafJavaCallbacks finished!");
}

xhook架构的hook fopen等函数

void nativeHookFile(JNIEnv* env, jclass clazz, jstring hooksoStr, jstring soPath)
{
    char buf[100];
    char *cstr_hooksoStr = jstr2cstr(env, hooksoStr);
    LOGI("nativeHookFile, %s n", cstr_hooksoStr);
    char *cstr_soPath = jstr2cstr(env, soPath);
    if(cstr_soPath && strlen(cstr_soPath))
    {
        if (!dlopen(cstr_soPath, RTLD_LAZY)) //dlopen in advance
        LOGE("dlopen(%s,%d) error!n", cstr_soPath,RTLD_LAZY);
        else LOGI("dlopen(%s,%d) success !n", cstr_soPath,RTLD_LAZY);
    }

    if (xhook_register(cstr_hooksoStr, "fopen", fopen_saf, NULL))
        LOGE("xhook fopen register failed!");
    else LOGI("xhook fopen register successed!");

    if (xhook_register(cstr_hooksoStr, "mkdir", mkdir_saf, NULL))
        LOGE("xhook mkdir register failed!n");
    else LOGI("xhook mkdir register successed!");

    if (xhook_register(cstr_hooksoStr, "remove", remove_saf, NULL))
        LOGE("xhook remove register failed!n");
    else LOGI("xhook remove register successed!");

    xhook_refresh(0);
    free(cstr_hooksoStr);
    LOGI("nativeHookFile xhook finished!");
    if(cstr_soPath) free(cstr_soPath);
}```

## fopen的hook, 调用java层我们写好的getFD再用fdopen文件可写
```C
FILE *fopen_saf(const char *pathname, const char *mode)
{
    FILE* fp=NULL;
    JNIEnv* env = NULL;
    (*g_vm)->AttachCurrentThread(g_vm, &env, NULL);
    if(!env)
    {
        LOGE("fopen_asf, env AttachCurrentThread failed!n");
        return fopen(pathname, mode);
    }
    int mode2=0;
    if(mode[0] == 'w') mode2=1;

    fp = fopen(pathname, mode);
    if(!(fp || mode2 == 0 || errno != EACCES))
    {
        char buf[PATH_MAX_LEN];
        getcwd(buf, PATH_MAX_LEN);
        //LOGI("before fopen(%s, %s), cwd=%sn", pathname, mode, buf);    
        jstring s_pathname = (*env)->NewStringUTF(env, pathname);
        jstring s_curdir = (*env)->NewStringUTF(env, buf);

        int fd = (*env)->CallStaticIntMethod(env, g_javaClass, g_javaGetFD, s_curdir, s_pathname, mode2 );
        (*env)->DeleteLocalRef(env, s_curdir);
        (*env)->DeleteLocalRef(env, s_pathname);
        fp = fdopen(fd, mode);
        //LOGI("after fopen_saf(%s, %s),fp=%x, cwd=%sn", pathname, mode, (unsigned int)fp,buf);
    }
    return fp;
}