Use Android SAF (storage access framework) to restrict the access of game anti harmony (pride of Eden) / Android data directory

Posted by Pickle on Tue, 21 Dec 2021 00:40:59 +0100

Recently, I was playing a mobile game. The pride of Eden and some vertical paintings in the Japanese service client were slightly exposed. After the national service went online, it was harmoniously (added cloth)

But the great gods of nga always have a way to solve it. A big man provides an anti harmony method:

It is roughly to extract the resource file of the daily service client and replace the resource file of the national service client to complete the anti harmony of role drawing.

However, there are still many players at the bottom of the post, which is difficult to anti harmony, because some mobile phone manufacturers prohibit a lot of permissions for the safety of users, including the permission of users to access the data directory, which makes users have to root the mobile phone before they can enter the data directory for operation, which greatly improves the operation cost of anti harmony.

I happened to see an article about using SAF (Storage Access Framework) framework to access Android data directory recently( https://blog.csdn.net/qq_17827627/article/details/113931692 )Then my interest was brought up.

Make a one click anti harmony tool to facilitate the majority of players.

1, First, apply for all file management permissions

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission
        android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
        tools:ignore="ScopedStorage" />

Second, you need an Intent to open the SAF file management interface and ask for permission

    //Gets permissions for the specified directory
    fun startFor(path: String, context: Activity, REQUEST_CODE_FOR_DIR: Int) {
        val uri = changeToUri(path)
        val parse: Uri = Uri.parse(uri)
        val intent = Intent("android.intent.action.OPEN_DOCUMENT_TREE")
        intent.addFlags(
            Intent.FLAG_GRANT_READ_URI_PERMISSION
                    or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                    or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
                    or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
        )
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, parse)
        }
        context.startActivityForResult(intent, REQUEST_CODE_FOR_DIR)
    }

Prompt the user before requesting permission, and let the user enter the next interface and click the button at the bottom

Apply for directory permission of android/data:

        btn_antiHarmony.setOnClickListener {

            AlertDialog.Builder(this)
                .setTitle("Game resource directory permission application")
                .setMessage("Please click the "use this folder" button at the bottom of the next pop-up interface to grant us the necessary permissions to access the game resource directory.")
                .setPositiveButton("determine") { dialogInterface, i ->
                    FileUriUtils.startFor("android/data", this, REQUEST_CODE_FOR_DIR)
                }
                .setNegativeButton("cancel", DialogInterface.OnClickListener { dialogInterface, i -> })
                .show()
        }

 

 

3, Receive callback

    //Return authorization status
    override fun onActivityResult(requestCode: Int, resultCode: Int, @Nullable data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        var uri: Uri?
        if (data == null) {
            return
        }
        uriTree = data.data
        if (requestCode == REQUEST_CODE_FOR_DIR && data.data.also { uri = it } != null) {
            contentResolver.takePersistableUriPermission(
                uriTree!!, Intent.FLAG_GRANT_READ_URI_PERMISSION
                        or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            ) 
            startAntiHarmony()
        }
    }

When we return from the previous interface, we will know whether we have successfully obtained permission
Use your own variables to record data Data, which stores the authorized folder path

4, Detection before anti harmony

    var filter: List<DocumentFile> = mutableListOf()
    fun startAntiHarmony() {
        val root = DocumentFile.fromTreeUri(this, uriTree!!)
        filter = root!!.listFiles().filter {
            it.uri.path!!.endsWith(GUANFU_PACKAGE)
                    || it.uri.path!!.endsWith(BLIBLI_PACKAGE)
        }
        if (filter.isNullOrEmpty()) {

            ll_progress.visibility = View.GONE
            btn_antiHarmony.isEnabled = true
            btn_antiHarmony.setText("One key anti harmony")

            Snackbar.make(
                this, ll_rootView, "The pride of national service Eden is not installed on your mobile phone, so anti harmony cannot be carried out.",
                BaseTransientBottomBar.LENGTH_LONG
            ).setAction("determine", View.OnClickListener { }).show()
            return
        }

        // Start copying files into the game directory
            ll_progress.visibility = View.VISIBLE
            progressBar.progress = 0
            btn_antiHarmony.isEnabled = false
            btn_antiHarmony.setText("In progress...")

            startThread()

    }

First, DocumentFile The fromtreeuri () method is an api of the system. It can convert the uri we obtained earlier into a DocumentFile object. Because we adopt the SAF scheme, we must and can only operate the file through DocumentFile later.

The root object obtained at this time is actually the object of the android/data root directory.

At this point, we need to query all the folders in the current folder and find the game directory.

        val GUANFU_PACKAGE = "com.eastgalaxy.ydydja.android"
        val BLIBLI_PACKAGE = "com.bilibili.ydydja.bili"

These two are the pride of Eden, the national service game directory, because the national service has two channels, two clients, one is the official service downloaded by taptap and the other is the B service downloaded by BiliBili, which we can support.

If neither is found, there are only three possibilities:

  1. The user has not installed the national service client
  2. The user has installed the game, but has never started it (the game is only installed, not started, resources will not be initialized, and this directory will not be created)
  3. In the second step, when we pop up the data directory and ask the user for permission, the user clicks into the lower folder, so the uri we finally get is not the android/data root directory, so we can't search the game directory here.

5: Start anti harmony

Before that, you must first import the role drawing resource file of the daily service into the project.

First, copy the resource files in assets to the user's sd card. Because files in assets cannot be used directly as file objects.

After copying, we are ready to replace. Filter is the game directory we screened and found earlier. Because the game has two channels, the filter may have two elements, which need to be considered.

    fun startThread() {

        Thread(Runnable {
            // Start copying yourself first
            copyAssetsFiles(this, "eden_jp", filesDir.path + "/eden_jp")

            val sourceDF = DocumentFile.fromFile(File("file:///android_asset/eden_jp"))
            Log.i(TAG, "sourceUri = ${sourceDF.uri}")


            filter.forEachIndexed { index, documentFile ->
                Log.i(TAG, "index = $index, filterUri = ${documentFile.uri} ")
            }
            Log.i(TAG, "isGrantAndroidData = ${isGrantAndroidData()}")

            Log.i(TAG, "Self replication succeeded")
            filter.forEach {
                val targetDF = it.findFile("files")!!.findFile("Config")!!.findFile("res")!!
                Log.i(TAG, "Start replacement")
                Log.i(TAG, "Target directory: ${targetDF.uri}")

                val copyDirectoryWithContent = FileManager(this).copyDirectoryWithContent(
                    RawFile(
                        Root.DirRoot(File(filesDir.path + "/eden_jp")),
                        BadPathSymbolResolutionStrategy.ThrowAnException
                    ), ExternalFile(
                        this,
                        BadPathSymbolResolutionStrategy.ThrowAnException,
                        Root.DirRoot(CachingDocumentFile(this, targetDF))
                    ), true, updateFunc = { x, y ->
                        handler.sendMessage(handler.obtainMessage(0, x, y))
                        Log.i(TAG, "updateFunc : x=$x, y=$y")
                        return@copyDirectoryWithContent false
                    })
                Log.i(TAG, "Replacement complete, copyDirectoryWithContent = $copyDirectoryWithContent")

                Snackbar.make(
                    this, ll_rootView, "Since your mobile phone is equipped with multiple national service clients, the next version of anti harmony will begin soon",
                    BaseTransientBottomBar.LENGTH_LONG
                ).setAction("determine", View.OnClickListener { }).show()
            }

            // Delete self replicating resources
            FileUtil.deleteFile(filesDir.path + "/eden_jp")

            handler.sendEmptyMessage(1)
        }).start()
    }

The source of replacement is the file that has just been copied. The target of replacement needs to be found level by level with the findFile method.

Then you can start replacing.

For the file operation here, I used a big man's library on github, https://github.com/xbl3/Fuck-Storage-Access-Framework_K1rakishou

Because we need to copy and replace the original File system to the SAF DocumentFile system, the api provided by SAF can not be said to be difficult to use, but can only be said to be very difficult to use.

After using this library, it is very simple.

The updateFunc callback method can tell us how many files are in total and how many files are currently processed, so that we can easily display the progress bar to users.

6: Refresh display

    val handler: Handler = object : Handler() {
        override fun handleMessage(msg: Message) {
            when (msg.what) {
                0 -> {
                    progressBar.setProgress(msg.arg1)
                    progressBar.max = msg.arg2
                    tv_jindu.text = msg.arg1.toString() + "/" + msg.arg2.toString()
                }

                1 -> {
                    AlertDialog.Builder(this@MainActivity)
                        .setTitle("Modification succeeded!")
                        .setMessage("The pride of Eden has succeeded in anti harmony! Please enter the game to view! To prevent anti harmony failure caused by subsequent game updates, please join qq Enjoy permanent free updates!")
                        .setPositiveButton("determine") { dialogInterface, i ->

                        }
                        .show()
                    ll_progress.visibility = View.GONE
                    btn_antiHarmony.isEnabled = true
                    btn_antiHarmony.setText("One key anti harmony")
                }
            }
        }
    }

Finally, after the modification is successful, you can promote a wave of qq groups, ha ha.

The only disadvantage is that file replacement is slow.

Software effect:

 

 

 

 

Although it doesn't make money, it's still happy to help others~

At present, there are more than 100 people in my group. However, on second thought, it is so easy for users to grant me permission to the android/data directory and operate the data files of any application at will. Is it also a great risk? It would be unthinkable for a malicious application to gain such permission..

Topics: Android kotlin