小木曾雪菜 发表于 2020-1-14 22:04

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

本帖最后由 小木曾雪菜 于 2020-1-14 22:29 编辑

# 1. 前言
之前折腾了了一下MINE模拟器,发现SDL全是在JNI层`fopen`操作的,而安卓的SAF则是JAVA层通过DocumentFile和docUri来实现写入的。一种方法是通过去的File Descriptor然后传给JNI层,通过`fdopen`实现写入。于是成功在MINE模拟器添加外置sd卡写入功能,详见我在贴吧布的 (https://tieba.baidu.com/p/6441808720)。完整版源码我已经封装好了发布到github上https://github.com/YuriSizuku/AndroidSafFile,理论上通用。
!(http://tiebapic.baidu.com/forum/pic/item/d632733fb80e7bec877b639b382eb9389a506bdf.jpg)

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

## 获取外置sd卡根目录DocumentFile
```JAVA
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好像不只能在本目录下创建,要一层一层往里面走
```JAVA
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.length() > 0 ? paths.length - 1 : paths.length - 2;
      for (i = 0; i < end; i++) {
            // Log.i(LOGTAG, "getTar... path["+String.valueOf(i)+"], "+paths);
            if (paths.equals(base.getName())) {
                if (i >= end - 1) {
                  // Log.i(LOGTAG, "getTar... "+path+" end="+paths+" "+ paths);
                  return base;
                }
                i++;
                break;
            }
      }
      // Log.i(LOGTAG, "getTarget... "+base.getName()+" "+path);
      target = base.findFile(paths);
      // 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);
      }
      return target;
    }
```

## 获得OutputStream
```JAVA
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;
    }
```
## 获取文件描述符
```JAVA
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核心代码
这里用到了(https://github.com/iqiyi/xHook)架构,原理上是运行的时候来替换目标动态库的`.got`表到自己编译的函数地址,通过JNI来调用JAVA层我们写好通过SAF机制得到的文件描述符。

## 初始化要JNI中要调用的JAVA方法,class为`"com/yurisizuku/utils/SafFile"`
```C
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等函数
```C
void nativeHookFile(JNIEnv* env, jclass clazz, jstring hooksoStr, jstring soPath)
{
    char buf;
    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 == 'w') mode2=1;
   
    fp = fopen(pathname, mode);
    if(!(fp || mode2 == 0 || errno != EACCES))
    {
      char buf;
      getcwd(buf, PATH_MAX_LEN);
      //LOGI("before fopen(%s, %s), cwd=%s\n", 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=%s\n", pathname, mode, (unsigned int)fp,buf);
    }
    return fp;
}
```

# Reference
https://stackoverflow.com/questions/30593964/how-to-access-android-lollipop-documentfile-files-via-ndk/31677287
https://developer.android.com/guide/topics/providers/document-provider.html?hl=zh-cn

小木曾雪菜 发表于 2020-1-15 09:53

用户名被谁抢注 发表于 2020-1-15 08:22
不知道怎么实现?楼主发的是apk包?还是什么文件?只会安装apk。

这个是实现外置sd卡写入的封装,如果是自己的app,直接在源码里添加jave文件即可。如果用了native code的文件访问,则需要loadlibrary('yurihook')。如果想改造其他的app,则需要在合适的位置注入smali和把so注入

小木曾雪菜 发表于 2020-1-15 11:22

fabrice 发表于 2020-1-15 11:17
楼主 请问这个主要是什么应用场景? 看了下还是不太明白外置sd卡 难道非系统应用都不能使用?

这个是安卓系统的限制,4.4以后非系统应用outputstram.write已经没有写入外置sd卡权限了,我们需要用docuri授权来适配。本文提供了一直适配的封装,同时支持原生层的hook

甘愿堕落 发表于 2020-1-14 23:35

随然看不懂是干嘛的顶一下{:301_997:}

小木曾雪菜 发表于 2020-1-14 23:49

甘愿堕落 发表于 2020-1-14 23:35
随然看不懂是干嘛的顶一下

简单的说就是安卓4.4之后非系统应用无法写入外置sd卡,要靠DocumentFile等SAF框架写入。于是我就用java封装了一下api,然后hook native code .got段, 通过java来获得文件描述符,fdopen来进行文件的草果

甘愿堕落 发表于 2020-1-15 00:03

小木曾雪菜 发表于 2020-1-14 07:49
简单的说就是安卓4.4之后非系统应用无法写入外置sd卡,要靠DocumentFile等SAF框架写入。于是我就用java封 ...

我的6.0系统还非系统应用也可以用外置sd卡{:301_977:}

小木曾雪菜 发表于 2020-1-15 00:21

甘愿堕落 发表于 2020-1-15 00:03
我的6.0系统还非系统应用也可以用外置sd卡

可以,我测试了三星s7就是6.0, s9 8.0都可行。小米note4x不可行,因为rom阉割了docUri的包

用户名被谁抢注 发表于 2020-1-15 08:22

不知道怎么实现?楼主发的是apk包?还是什么文件?只会安装apk。{:1_901:}

pp_03301 发表于 2020-1-15 08:30

学习一下,点赞~~

henrysu 发表于 2020-1-15 09:04

感谢分享!!

fabrice 发表于 2020-1-15 11:17

楼主 请问这个主要是什么应用场景? 看了下还是不太明白外置sd卡 难道非系统应用都不能使用?
页: [1] 2 3
查看完整版本: Android SAF实现外置SD卡的写入(JAVA层与JNI层hook)