Android安全之使用root权限绕过检测机制,强行自动允许应用的悬浮窗/应用后台弹出界面等权限 - Go语言中文社区

Android安全之使用root权限绕过检测机制,强行自动允许应用的悬浮窗/应用后台弹出界面等权限


前言

看完这篇文章你将会学会

  • 当你看到一个系统界面, 如何精确查找此界面对应的源码和其功能实现原理 (重点)
  • 分析权限检测机制原理, 以及如何绕过 (重点)
  • linux 下执行so文件
  • linux 下执行dex文件 (重点,核心)
  • jar转dex, dex转jar
  • 如何让一整段java代码运行在root进程中 (重点,核心)
  • 如何在linux下/root进程下,拿不到context时, 如何调用系统的一些getSystemService API (重点,核心)
  • 教你如何免写反射, 直接调用系统API的方式
  • 这么牛逼? 当然求个关注了啊

分析源码

该技术可以通过root权限,绕过权限检测机制, 在后台实现静默地自动授予任意app的任意权限, 没错: 自动授予[任意]app 的 [任意] 权限, 就是这么可怕!

以悬浮窗权限为例, 下面来说下我的研究的过程和最终解决方案:

大概以前在安卓4.x-6.x时代,android原生的悬浮窗权限是默认允许的,导致悬浮窗锁机应用病毒流行, 后来国内MIUI flyme等系统自己加了个悬浮窗权限, 当时的适配方案是反射判断AppOpsManager.checkOp(24) 返回权限授予情况 //24为悬浮窗权限.

好像大家都是这么写, 难道就没人问过为什么是24 , 为什么24就代表悬浮窗权限了? 原因请看我之前的帖子:[答案在反射到的常量表里]
Android 权限适配 从此第三方系统新增的权限无法判断状态的问题得到解决! 如MIUI自启动, 后台弹出界面权限等

该方法后来逐渐被遗弃, 到了android 6.x后 , 原生提供了一个办法来判断悬浮窗权限

Settings.canDrawOverlays(context)

我们来看一下 canDrawOverlays的源码 , 跟踪方法栈到最后一层:

public static boolean isCallingPackageAllowedToPerformAppOpsProtectedOperation(Context context,
            int uid, String callingPackage, boolean throwException, int appOpsOpCode, String[]
            permissions, boolean makeNote) {
        AppOpsManager appOpsMgr = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE);
        int mode = AppOpsManager.MODE_DEFAULT;
        if (makeNote) {
            mode = appOpsMgr.noteOpNoThrow(appOpsOpCode, uid, callingPackage);
        } else {
            mode = appOpsMgr.checkOpNoThrow(appOpsOpCode, uid, callingPackage);
        }
        switch (mode) {
            case AppOpsManager.MODE_ALLOWED:
                return true;
            case AppOpsManager.MODE_DEFAULT:
                return true;
        }
 		//略...
    }

发现 canDrawOverlays的实现原理也是通过AppOpsManager的内部函数实现!

通过上面的AppOpsManager.checkOp(24)和 Settings.canDrawOverlays(context)实现原理 可知
关键在于这个AppOpsManager类, 我们来看下源码有没有 setOp() 这样的函数. (源码直达)
看来看去 只有setMode 比较像是设置权限的操作, 但始终不确定.

于是… 打开安卓模拟器, 通过请求一个悬浮窗权限弹出到权限设置界面, 然后抓取当前fragment;类名称, 然后下载或在线android 源码中搜索这个界面的源码, 查看其悬浮窗授权开关的实现原理.
步骤分别如下:
找到界面
在这里插入图片描述
抓取当前fragment名称 通过软件 开发者助手电脑版 / 开发者助手手机版(手机版需要root, 且不稳定可能无法抓取)
在这里插入图片描述
或通过dumpsys activity top 或界面资源文字搜索源码定位到Fragment名称等办法
以上找到这个设置界面的fragment后, 开始前往这里搜源码: 在线安卓源码搜索
在这里插入图片描述
最终找到了这个类 DrawOverlayDetails.java
看了下代码, 非常易懂:
在这里插入图片描述
没错了, 就是通过setMode函数来授予权限的, 而且AppOpsManager.OP_SYSTEM_ALERT_WINDOW 的值 刚好就是24, 悬浮窗权限.,而mode值为 AppOpsManager.MODE_ALLOWED代表允许, 该函数需要输入op值(24) , uid ,包名,和 mode值

那么反射一下调用setMode不就得了? 真这么简单我还写这个文章干啥啊…
方式会报安全异常, 需要系统权限才能调用这个setMode函数.
此时我们回到AppOpsManager的setMode内部实现原理, 发现其其实是通过 service实现远程通讯调用的
在这里插入图片描述
在这里插入图片描述
然后再搜到IAppOpsService的实现, AppOpsService.java

这里说下我个人搜系统源码的经验, 毕竟我们没有研究透彻系统源码,总不能看到一个类就知道它的实现类的什么吧
现在说下我的办法:
1.AaaaManager的实现类或远程通讯类通常是 IAaaa , AaaaImpl, IAaaaService AaaaService
2.通常在同一路径下, 所以可以查看与 AaaaManager同路径下的相似类
3.实在找不到可以导入到Android Studio, 用快捷键Ctrl+H看父子关系, 也可以按住Ctrl用鼠标点击类名跳转

可以看到,实现中第一行就是检测权限
在这里插入图片描述
可以看到 只要calling pid和当前pid一致, 就可以跳过检查, 这样就只能通过xposed 去hook实现了.因为xposed是同一个进程下执行代码, 这里研究的是如何通过root去实现, 没法做到, 除非利用root进程注入技术 (这篇文章先不聊这个,因为稳定性巨差) 所以不急, 继续看 enforcePermission的实现原理
跟踪找到ContextImpl.java类 里
在这里插入图片描述
在这里插入图片描述
这里写的是 activitymanager为空的时候, 此时如果是root 用户或system用户 就可以免除授权检查直接授予通过

其中UID :(ROOT_UID = 0; SYSTEM_UID = 1000;)

为什么这里这么判断? 可能是linux下的进程 是没有activity的, 也无法获取上下文Context , 在linux获取activity是null的 (这里你可能看不懂, 等会再说.)

总结一下,
1.AppOpsManager的实现类是IAppOpsService, 其远程实现是AppOpsService, 所以可以反射调用setMode 进行授权

2 通过步骤1前, 需要系统uid=0 或1000的权限, 那么想办法授予本软件的root权限, 但Runtime.exec(“su”)后 只是这个返回Process类的输出流其写入的命令行参数执行的环境才是uid=0的进程环境, 在此之外, 整个app 的uid并不是0, 而是不变., 简单来说 app的uid不受改变, 不受exec(“su”) 授权后的影响, 而且你没法直接在命令行里输入java去执行, 更别说反射AppOpsService 了

要在linux下执行代码, 做过linux编译或开发的都知道有个办法, 就是 编译c/c++成二进制可执行文件, 保留其main函数, 然后复制到linux 系统下 , 然后执行 便能输出个简单 的 hello world.
而安卓就是linux, 可以把文件复制到如/data/local/tmp/xxx.a 然后执行.
这里贴下我之前的笔记图片, 我就不细说了, 如果想要学习, 麻烦百度下别的文章
在这里插入图片描述
没错你看到标题了没! 执行dex ! 以上看看就好了 不是本文的主要知识, 重头戏在下面:

在linux下执行dex 输出hello world

原理是利用安卓系统内置的工具: app_process

1.在IntelliJ IDEA里新建一个java工程 并打包jar 执行测试

新建后 新建一个类

public class AppOpsManagerCompat {
    public static void main(String[] args)  {
        System.out.println("hello world");
    }
}

注意: 为了方便测试, main函数内记得打印一行 hello world 之类的
然后打包成jar文件, 操作步骤如下:
在这里插入图片描述
配置好后开始构建生成jar包, 步骤如下:
在这里插入图片描述
最后jar包路径生成在这个位置下:
在这里插入图片描述
为了确定无误, 可以使用jadx/jd-gui 等反编译工具查看jar包的内容, 这里要注意下MANIFEST.MF文件描述的mainclass是否正确. 否则可能在生成配置中没有设置main类
在这里插入图片描述
当然也可以通过java -jar AppOpsManagerTest.jar 来确认这个jar包可成功执行
在这里插入图片描述

2.使用工具把jar转成dex )

基本命令 dx --dex --output
步骤
找到你的sdk中的 dx.bat 或 dx.jar

D:Androidsdkbuild-tools27.0.2dx.bat
执行

D:Androidsdkbuild-tools27.0.2dx.bat --dex --output  appops.dex  AppOpsManagerTest.jar

为了方便, 可以把dx命令配置到系统环境变量

3.验证dex(可选)

使用 jadx, jeb2 等反编译工具 对dex进行反编译 查看dex是否是自己刚才写的那样

4.复制dex到 Android手机中.

复制到 /system/bin下 或 /data/local/tmp/下 (建议后者, 前者可能有些手机有system锁 无法复制进去)

5.确认dex文件的权限

查看权限情况
ls -l

修改权限全满
chmod 777 /data/local/tmp/appops.dex

6.执行dex
//插入手机 然后利用adb push 把dex复制到手机中 (可选, 可自己手动复制)
adb push D:AndroidIDEAProjectsAppOpsManagerTestoutartifactsAppOpsManagerTest_jarappops.dex

//复制appops.dex到  /data/local/tmp/ 下
adb shell "su -c 'cp -rf /sdcard/appops.dex /data/local/tmp/'"

//修改权限全满
adb shell "su -c 'chmod 777  /data/local/tmp/appops.dex'"

//运行dex
adb shell "su -c 'app_process -Djava.class.path=/data/local/tmp/appops.dex  /data/local/tmp com.mx.appops.AppOpsManagerCompat'" 

//输出hello world  略, 和 'java -jar ' 执行的结果一样.

linux下无法获取Context的解决办法

问题解决了, 现在可以在linux下执行java 代码(dex) 了, 现在开始尝试
反射调用setMode函数

…然而…
…卧槽…
在这里插入图片描述
上下文Context 怎么拿? 于是我找遍了系统没有发现静态的上下文提供反射用 (静态的Context会容易内存泄露)
好吧…只好研究下 getSystemService的原理了, 看看能不能免context也能拿到service/manager
最后发现 ContextImpl.java 关键位置:
在这里插入图片描述
发现在SystemServiceRegistry.java类中, 创建了AppOpsManager的源码
在这里插入图片描述
并且通过 ServiceManager.getServiceOrThrow/getService 和 asInterface取得服务, 这个过程不需要上下文Context的参与, (看不懂的, 还记得bindService aidl是如何和界面通讯的吗. )

IBinder b = ServiceManager.getService(Context.APP_OPS_SERVICE);
IAppOpsService service = IAppOpsService.Stub.asInterface(b);
service.setMode(...)

而IAppOpsService 又是AppOpsManager的内部实现, 也有setMode 方法!..
所以模仿上面的代码反射执行就可以了!
这次真的不绕圈了, 我知道前面的流程到处跑…
好的, 那么就反射吧…
反射吧…

反射代码: 略…

'简易’实现反射

对于这个我有个好办法, 可以让反射简单点. 不过需要有点ROM知识,不会的可能会更复杂…但这个办法可以通用, 以后做别的app如xposed开发可以学我这个办法, 不用写大量的反射代码. 不想学的,自己手动写反射代码去吧.就那么几行.

新建个android studio 自带的 安卓10虚拟机, (安卓10虚拟机不需要合并odex/vdex,而且默认有root)
或… 下载雷电模拟器, 在设置里启用root权限. 雷电模拟器是安卓5.x 可能有些api拿不到别怪我. , 当然自己会合并dex可以跳过我这个步骤.
1.提取/system/framework/framework.jar
2.改后缀zip解压看有没有dex, 没有dex 请合并dex或按我上面的步骤做.不要盲目跳过
3.解压后的dex文件 用dex2jar工具 反编译为 jar包 丢到idea工程中并导入
在这里插入图片描述
切记, 直接提取出来的framework.jar, 不经过dex2jar是无法在idea识别的
然后代码直接写即可
在这里插入图片描述
看, 直接使用系统api 没有任何报错, 完全不需要写反射.

注意: 图中的uid 改成你的需要申请悬浮窗权限app的uid, 你的app的uid可通过 Binder.getCallingUid()获取, 注意我说的是在你app 代码中获取 , 不是写在这例子中, 这例子中Binder.getCallingUid()会返回0, 因为执行的时候uid是0 可免权限, 这也算能绕过权限的原因

最终实现

如果上面不知道我说的啥玩意…行吧…
我把它写成一个可通过main函数传参的方式

package com.mx.appops;

import android.Manifest;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.Uri;
import android.os.*;
import com.android.internal.app.IAppOpsService;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;


public class AppOpsManagerCompat {

    //运行示例 adb shell "su -c 'app_process -Djava.class.path=/data/local/tmp/appops.dex  /data/local/tmp com.mx.appops.AppOpsManagerCompat'" -packagename=包名 -op=24  -mode=0
    public static void main(String[] args) {
        if (args.length == 0) {
            System.out.println(false);
            return;
        }
        String packagename = null;
        int uid = 0;
        int mode = -1;
        int op = -1;
        for (String arg : args) {
            if (arg.startsWith("-") && arg.contains("=")) {
                String type = arg.substring(arg.indexOf("-") + 1, arg.indexOf("=")).trim();
                String value = arg.substring(arg.indexOf("=") + 1).trim();
                switch (type) {
                    case "packagename":
                        packagename = value;
                        break;
                    case "mode":
                        mode = Integer.parseInt(value);
                        break;
                    case "op":
                        op = Integer.parseInt(value);
                        break;
                    case "uid":
                        uid = Integer.parseInt(value);
                        break;
                }
            }
        }
        if (packagename == null || packagename.isEmpty() || op == -1 || mode == -1) {
            System.out.println(false);
            return;
        }
        try {
            IBinder iBinder = ServiceManager.getService(Context.APP_OPS_SERVICE);
            IAppOpsService iAppOpsService = IAppOpsService.Stub.asInterface(iBinder);
            if(uid<=0) {//如果不传uid 那么通过包名获取uid
                //这里仍然是采用相同的技术:  免上下文Context获取到PackageManager
                IPackageManager ipm = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
                PackageInfo packageInfo = ipm.getPackageInfo(packagename, 0, 0);
                uid = packageInfo.applicationInfo.uid;
            }
            iAppOpsService.setMode(op, uid, packagename, mode);
            iAppOpsService.noteOperation(op, uid, packagename);
            System.out.println(true);
        } catch (Exception e) {
            System.out.println(false);
        }

    }

}

注意注释, -packagename=包名 -op=24 -mode=0 为参数 , uid可以不传, 会通过包名获取, 这样简单了吧.

嗯, 现在打包成dex 执行吧 (执行前, 先把悬浮窗授权开关关掉再开,再关 为什么这么做? 等会说)
在这里插入图片描述
返回成功! 然后打开系统设置看看… 悬浮窗权限开关自动开启了 ! 而且打开app的确可以使用悬浮窗了!

把命令集成到app中

然后你可以把这些命令行操作封装到你的app中去, 举例

public static boolean setMode(Context context, int code, int uid, String packageName, int mode,ShellUtils.OnShellExecStreamListener listener)
	{
		String dexName = "a12w2314.dex";
		String tmpDir  = "/data/local/tmp/";
		File   dexFile = new File(tmpDir, dexName);
		if (!dexFile.getParentFile().exists())
		{
			dexFile.getParentFile().mkdirs();
		}
		dexFile = new File(tmpDir, dexName);
		if (!dexFile.getParentFile().exists())
		{
			ShellUtils.exec("mkdir " + tmpDir, null);
		}

		if (!dexFile.exists() || dexFile.length() <= 0)
		{
			try
			{
				AssetManager assets = context.getAssets();
				InputStream  open   = assets.open(dexName);
				File         file   = new File(context.getFilesDir(), dexName);
				if (!file.getParentFile().exists())
				{
					boolean mkdirs = file.getParentFile().mkdirs();
				}
				FileOutputStream outputStream = new FileOutputStream(file);
				byte[]           buff         = new byte[2048];
				int              len;
				while ((len = open.read(buff)) != -1)
				{
					outputStream.write(buff, 0, len);
				}
				outputStream.flush();
				outputStream.close();
				open.close();
				if (file.exists())
				{
					ShellUtils.exec("cp -rf " + file.getAbsolutePath() + " " + tmpDir + " && chmod 777 " + tmpDir + dexName, null);
				}
			} catch (Exception e)
			{
				e.printStackTrace();
			}
		}
		if (!dexFile.exists())
		{
			dexFile = new File(tmpDir, dexName);
		}
		if (dexFile.exists())
		{
			String cmd = "app_process -Djava.class.path=" + tmpDir + dexName + "  " + tmpDir + " com.mx.appops.AppOpsManagerCompat -packagename=" + packageName + " -op=" + code + " -uid=" + uid + " -mode=" + mode;
			ShellUtils.exec(cmd, listener);
			//这里未必成功, 需要监听dex中syso输出的成功值,dex已经写了 但是在这边不想写监听 以后再说,凑合用吧
			return true;
		}
		return false;
	}

以上代码:
从app的Asset文件夹中复制dex文件出来到app私有目录下, 利用cp -rf 再复制到/data/local/tmp下, 修噶权限, 然后执行修改权限命令, 并传入参数
ShellUtils.exec(cmd, listener); 是执行shell的工具类, 这里不贴了 网上找吧

填坑

前面说了,把悬浮窗授权开关关掉再开,再关 为什么这么做? 亲测小米MIUI是系统安全中心管理这个权限, 并把它存入数据库, 我们通过前面的root+反射修改权限的方式的确修改了权限, 但是MIUI系统安全中心管理这个app没刷新, 所以测试的时候最好刷新下. 不过这个对用户没影响. 权限该开还是开, 只不过第三方系统有可能看到界面显示是关的.

还有就是前面说的, linux执行dex 下无法获取上下文, activity, uid为0 等原因如图
在这里插入图片描述
输出结果:
在这里插入图片描述

本文只演示了op值为24 的悬浮窗权限静默root开启的方案, 其它权限全部通用 , 我试了后台弹出界面权限也是可以的, 还有那些快捷方式创建权限, 电话 短信修改, xxxxx 都是可以通过我上面的方法改的, 把op值改下即可, op值表如何获取可以看源码, 第三方权限op值如何获取可以看我之前的帖子 Android 权限适配 从此第三方系统新增的权限无法判断状态的问题得到解决! 如MIUI自启动, 后台弹出界面权限等

愣着干嘛, 收藏+关注+加群啊! 加QQ群418263790 一起研究一些骚操作技术吧.

本文纯原创技术, 全网独一无二 , 转载请著名来处 (抄袭更可耻)

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/u014418171/article/details/103868878
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢