Magisk

Root —— 玩家与厂商间的博弈

Android 从诞生之日起就高举着开源的大旗,这也是它成功的原因之一。而它的开放性也成功的吸引到了一大批爱折腾的人,从而诞生出了 root(此处特制 Android 中的 root)。

根据 Wikipedia 的释义,root 指的是使用户取得 Android 操作系统的超级用户(Super User)许可权的技术。用户通过 root,可以越过手机制造商的限制,卸载手机制造商预装在手机中某些应用,以及运行一些需要超级用户权限的应用程序。同时,root 也可能会让手机变得“不安全”(并不是说 root 使手机变得不安全,而是一些用户的使用习惯会使 root 后的手机变得危险)。

但是从棉花糖(Android 6.0)开始,Google 基本阻止了以前版本中最流行的 root 方法 —— 即,将 su守护程序 放置到 /system 分区,并在启动时取得所需的权限。道高一尺,魔高一丈,于是就出现了 systemless 的 root 方式,因为它不采取任何方式修改 /system 分区。

出于增加安全性的考虑,Google 推出了 SafetyNet 这样的检测,以确保 Android Pay 等一些 App 的安全运行,玩家不得不在 root 权限和一些有价值的 App 之间作出选择。

这个时候 Magisk 诞生了。

Magisk

什么是 Magisk

Magisk 是出自一位台湾学生 @topjohnwu 开发的 Android 框架,是一个通用的第三方 systemless 接口,通过这样的方式实现一些较强大的功能。

看似很简单的一个框架,甚至与大名鼎鼎的 Xposed 框架在功能性上有点重复。很多人批评 Magisk的模块太少了,想替代 Xposed 根本不可能(在那个 Xposed Framework for Android 7.0 难产的时代,很多人将 Magisk 看是做是 Xposed 的替代品)。这是不正确的,因为 Magisk 从来没有想过要代替 Xposed ,Magisk 与 Xposed 是可以互相兼容的,你甚至可以通过 Magisk 来安装 Xposed(安装 Xposed 后就不能绕过 SafetyNet 了)。

Magisk 的厉害之处在于它实现了一种绕过 SafetyNet 使用 root 的方法。

实现原理:由于它是通过启动时在 boot 中创建钩子,把 /data/magisk.img 挂载到 /magisk,构建出一个在 system 基础上能够自定义替换,增加以及删除的文件系统,所有操作都在启动的时候完成,实际上并没有对 /system 分区进行修改(即 systemless 接口,以不触动 /system 的方式修改 /system)。

功能

截至目前版本(v14.0),Magisk 可以实现的功能包括:

  • 集成 root(MagiskSU)
  • root 和 Magisk 的日志功能
  • Magisk Hide(隐藏 Magisk 的 root 权限,针对 Snapchat、Android Pay、PokémonGo、Netflix 等)
  • 为广告屏蔽应用提供 systemless hosts 支持
  • 通过 SafetyNet 检查
  • Magisk 功能模块

支持的版本:Android 5.0+

安装方法

安装 Magisk 需要解锁 Bootloader 并刷入第三方 Recovery。所以每个品牌的手机都或多或少的有点不一样,这里只介绍一个标准的流程,具体操作方法请自行 Google(只需要 Google 你使用的手机解锁 Bootloader 和刷入第三方 Recovery 的方法就可以了,其他的安我说的做)。

  1. 解锁手机 Bootloader(BL)
    方法:自行 Google
  2. 刷入第三方 Recovery(例如 TWRP)
    方法:自行 Google
  3. 下载官方 Magisk 包,然后通过第三方 Recovery 刷入
    方法:首先将下好的包放入手机的硬盘中(你可以使用 QQ数据线 也可以使用 XX手机助手,whatever)然后,进入第三方 Recovery(以 twrp 为例),安装刷机包 -> 找到我让你放在硬盘中的那个包(后缀为 .zip)-> 滑动滑块,开始刷机 -> 刷好后立即重启
  4. 享受完整 Magisk 的 systemless root 和神奇的 Magisk 模块
    重启后找到一个名为 Magisk Manager(图标是一个面具,绿色背景),这是 Magisk 的管理程序,你可以在这里下载、安装、升级、卸载你的 Magisk 和 Magisk 模块。
  5. 卸载 Magisk
    卸载 Magisk 有两种方法:在 Magisk Manager 中卸载,或者通过第三方 Recovery 刷入卸载包卸载。通过 Magisk Manager 卸载很好理解,通过第三方 Recovery 卸载的意思是刷一个名为 Magisk-uninstaller.zip 的刷机包,方法和刷 Magisk 一样。两种方法我都没试过。
一些推荐的功能模块

App Systemizer

这是一个能把用户 App 挂载为系统 App 的模块,如 Google Play 服务、绿色守护、蟒蛇音效等。

Magisk SELinux Permissive Script

使 Android 的 SELinux 默认以 Permissive 运行,关于 SELinux 模式的介绍,请点击这里

ViPER4Android FX

大名鼎鼎的蝰蛇音效的 Magisk 模块,需要配合 VIPERFX 的管理器使用,请在 XDA 论坛搜索下载。关于ViPER4Android

Xposed

强大的 Xposed 框架的 systemless 实现,关于 Xposed 的介绍点击这里

待续...

一些资源

如果你不知道这么找 Magisk 或者 VIPERFX。我这里提供了一些资源。不能保证是最新的。

  • Magisk_v17.1.zip

    Magisk 卡刷包,版本:17.1

  • VIPERFX

    ViPER4Android FX 的管理程器,版本:2.5.0.5

  • ViPER4Android_full.zip

    我收集的蝰蛇音效的音效配置、脉冲反馈 和 DDC。完整版,质量良莠不齐

  • ViPER4Android.zip

    还是音效配置、脉冲反馈 和 DDC。但这是我精选过的版本,也是目前再用的版本

参考链接


java.lang.IllegalArgumentException: The AJP Connector is configured with secretRequired="true" but the secret attribute is either null or "".

ubuntu 18.04 .5 升级到 ubuntu 20.04.2 之后,发现 Tomcat 9.0.31 长时间没办法启动,观察日志,发现如下错误信息:

08-Feb-2021 22:09:23.792 SEVERE [main] org.apache.catalina.util.LifecycleBase.handleSubClassException Failed to start component [Connector[AJP/1.3-8009]]
        org.apache.catalina.LifecycleException: Protocol handler start failed
                at org.apache.catalina.connector.Connector.startInternal(Connector.java:1038)
                at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
                at org.apache.catalina.core.StandardService.startInternal(StandardService.java:438)
                at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
                at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:930)
                at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
                at org.apache.catalina.startup.Catalina.start(Catalina.java:633)
                at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
                at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
                at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
                at java.base/java.lang.reflect.Method.invoke(Method.java:566)
                at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:343)
                at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:478)
        Caused by: java.lang.IllegalArgumentException: The AJP Connector is configured with secretRequired="true" but the secret attribute is either null or "". This combination is not valid.
                at org.apache.coyote.ajp.AbstractAjpProtocol.start(AbstractAjpProtocol.java:264)
                at org.apache.catalina.connector.Connector.startInternal(Connector.java:1035)
                ... 12 more

这个是由于在升级系统的时候,选择保留老版本的配置文件,这样就导致,如果 Tomcat 配置了通过 AJP 方式与Apache通信的情况下,会报告上面的错误信息。

新增 secretRequired 的目的是为了解决 AJP  端口暴露在公网的情况下,存在 AJP File Read/Inclusion in Apache Tomcat (CVE-2020-1938) and Undertow (CVE-2020-1745) 漏洞,由于 Tomcat AJP 协议设计上存在缺陷,攻击者通过 Tomcat AJP Connector 可以读取或包含 Tomcat 上所有 webapp 目录下的任意文件,例如可以读取 webapp 配置文件或源代码。此外在目标应用有文件上传功能的情况下,配合文件包含的利用还可以达到远程代码执行的危害。

连接方必须在连接的时候,传入正确的 secretRequired 才能与 Tomcat 通信,相当于通信需要的密码了。

网上很多人都是直接设置

<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" secretRequired="" />

来解决问题的,也就是设置 secretRequired 为空字符,但是这样会导致攻击方不需要传递密码就可以通信了,因此诱发远程攻击漏洞。

正确的做法其实是不允许远程用户直接通过 Tomcat AJP 协议通信,也就是在设置绑定的 IP 地址为本地地址 127.0.0.1。如下:

<Connector port="8009" protocol="AJP/1.3" address="127.0.0.1" redirectPort="8443" secretRequired=""/>

参考链接


Android中的@SmallTest,@MediumTest和@LargeTest注解的目的是什么?

Android中的@SmallTest,@MediumTest和@LargeTest注解的目的是什么?

例如:

@SmallTest
public void testStuff() {
    TouchUtils.tapView(this, anEditTextView);
    sendKeys("H E L P SPACE M E PERIOD");
    assertEquals("help me.", anEditTextView.getText().toString());
}

参考下图:

继续阅读Android中的@SmallTest,@MediumTest和@LargeTest注解的目的是什么?

Robolectric 3.x编写屏幕分辨率/多语言/资源文件相关测试用例

在编写 Android 测试用例的时候,有时候我们需要涉及到屏幕分辨率相关测试用例。

比如不同分辨率得到不同的像素数值,可以参考如下:

@RunWith(RobolectricTestRunner.class)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"})
@Config(sdk = Build.VERSION_CODES.P)
public class DimensUnitTest {

    @Test
    @Config(qualifiers = "w480dp-h800dp")
    public void dimens800x480_isCorrect() {
        final Context context = RuntimeEnvironment.application;
        final double ref100DpBase = 42.666667;

        float expectDp = 110;
        double dips = context.getResources().getDimension(R.dimen.dimen_110);

        assertEquals(ref100DpBase * expectDp / 100, dips, delta);

        for (Map.Entry<Integer, Double> entry : expectDimens.entrySet()) {
            dips = context.getResources().getDimension(entry.getKey());
            assertEquals(ref100DpBase * entry.getValue() / 100, dips, delta);
        }
    }
	
}

比如不同语言得到不同的字符串,可以参考如下:

@RunWith(RobolectricTestRunner.class)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"})
@Config(sdk = Build.VERSION_CODES.P)
public class LangUnitTest {

    /**
     * 使用qualifiers加载对应的资源文件
     *
     * @throws Exception
     */
    @Config(qualifiers = "zh-rCN")
    @Test
    public void testString() throws Exception {
        final Context context = RuntimeEnvironment.application;
        assertThat(context.getString(R.string.app_name), is("单元测试Demo"));
    }
	
}

其他相关的测试参数,参考 Device Configuration

注意需要在 build.gradle 中增加资源包含信息,否则在测试的时候会找不到指定的资源文件,默认只测试代码,被测试的资源文件不打包进入应用。

参考如下:

    testOptions {
        unitTests {
            includeAndroidResources = true
            all {
                //命令行下 单元测试可能卡住的问题
                jvmArgs '-noverify'
                //robolectric外部指定下载资源链接的参数,使用 -D 参数指定 bash gradlew clean build -Drobolectric.dependency.repo.url=http://127.0.0.1/jcenter
                systemProperty 'robolectric.dependency.repo.url', System.getProperty("robolectric.dependency.repo.url")
                systemProperty 'robolectric.dependency.repo.id', System.getProperty("robolectric.dependency.repo.id")
            }
        }
    }

参考链接


代码高亮插件Crayon Syntax Highlighter在PHP7.4报错解决办法

在新版的WordPress中,系统已经多次提示升级PHP,考虑到新版本更高效更安全,所以决定升级。

可是,升级完成后,网站出现大量报错,报错信息如下:

Warning: preg_replace(): Compilation failed: invalid range in character class at offset 4 in /var/www/wordpress/wp-content/plugins/crayon-syntax-highlighter/crayon_langs.class.php on line 340
 
Warning: preg_replace(): Compilation failed: invalid range in character class at offset 4 in /var/www/wordpress/wp-content/plugins/crayon-syntax-highlighter/crayon_langs.class.php on line 340
 
Warning: preg_replace(): Compilation failed: invalid range in character class at offset 4 in /var/www/wordpress/wp-content/plugins/crayon-syntax-highlighter/crayon_langs.class.php on line 340
 
Warning: preg_replace(): Compilation failed: invalid range in character class at offset 4 in /var/www/wordpress/wp-content/plugins/crayon-syntax-highlighter/crayon_langs.class.php on line 340
 
Warning: preg_replace(): Compilation failed: invalid range in character class at offset 4 in /var/www/wordpress/wp-content/plugins/crayon-syntax-highlighter/crayon_langs.class.php on line 340
 
Warning: preg_replace(): Compilation failed: invalid range in character class at offset 4 in /var/www/wordpress/wp-content/plugins/crayon-syntax-highlighter/crayon_langs.class.php on line 340
 
Warning: preg_replace(): Compilation failed: invalid range in character class at offset 4 in /var/www/wordpress/wp-content/plugins/crayon-syntax-highlighter/crayon_langs.class.php on line 340

继续阅读代码高亮插件Crayon Syntax Highlighter在PHP7.4报错解决办法

Android单元测试--使用DummyActivity

在编写Android测试用例的时候,有时候我们需要测试与Activity相关的功能,同时又没办法直接调用被测试代码中的Activity的时候,我们需要创建DummyActivity的方式来进行。

我们希望这个DummyActivity只在测试代码中存在,相关的资源也只存在于测试代码里面使用,不侵入主代码。

可以参考如下布局进行项目的处理。

androidTest/
├── AndroidManifest.xml
├── java
│   └── com
│       └── mobibrw
│           └── lib_phone
│               ├── MainActivityTest.java
│               ├── TestActivity.java
│               └── actions
│                   ├── HintViewAction.java
│                   ├── KeepHintViewAction.java
│                   └── SetTextViewAction.java
└── res
    ├── layout
    │   └── activity_main.xml
    └── values
        └── styles.xml

上述代码同样适用于Android资源文件相关的测试逻辑。

只是需要注意的是,当引入资源的时候,我们需要使用 {module_package}.test.R 的方式进行引入,否则代码中会提示找不到资源文件。

参考链接


Intel官方温度监控软件:Intel Power Gadget

Intel Power Gadget是Intel官方出的一款软件,可供用户查看、记录CPU和GPU的信息,比如温度、耗电、频率、使用率等。

下载地址:https://software.intel.com/en-us/articles/intel-power-gadget

Intel Power Gadget支持macOS、Windows和Linux,但是最新版的已经不支持Windows 7系统,可以下载3.0.7版本。

intel-power-gadget.dmg

继续阅读Intel官方温度监控软件:Intel Power Gadget

测试Activity的onSaveInstanceState/onRestoreInstanceState

最近遇到一个应用崩溃问题,这个问题是由于在 ActivityonSaveInstanceState 中进行了数据的保存,然后在 onRestoreInstanceState 进行解析的时候出现崩溃。

实际测试的时候,发现当内存充足的时候,非常难稳定的诱发 ActivityonSaveInstanceState 事件。

早期的版本,可以通过 ActivityManagerNative.getDefault().setAlwaysFinish 来强制系统在 Activity 切换到后台之后,立即触发 onSaveInstanceState

参考代码如下:

// Updates the system Always Finish setting
private void writeFinishOptions()
{
    try
    {
        // Due to restrictions related to hidden APIs, need to emulate the line below
        // using reflection:
        // ActivityManagerNative.getDefault().setAlwaysFinish(mAlwaysFinish);
        final Class   classActivityManagerNative = Class.forName("android.app.ActivityManagerNative");
        final Method     methodGetDefault = classActivityManagerNative.getMethod("getDefault");
        final Method     methodSetAlwaysFinish = classActivityManagerNative.getMethod("setAlwaysFinish", new Class[] {boolean.class});
        final Object     objectInstance = methodGetDefault.invoke(null);
        methodSetAlwaysFinish.invoke(objectInstance, new Object[]{mAlwaysFinish});
    }
    catch (Exception ex)
    {
        showAlert("Could not set always finish:\n\n" + ex, "Error");
    }
}
 
// Gets the latest AlwaysFinish value from the system and
// updates the checkbox
private void updateFinishOptions()
{
    mAlwaysFinish = Settings.System.getInt(getContentResolver(), Settings.System.ALWAYS_FINISH_ACTIVITIES, 0) != 0;
    mAlwaysFinishCB.setChecked(mAlwaysFinish);
}

但是遗憾的是,新系统比如 Android 8 等系统上,在真机环境中已经没办法通过上述的方法进行诱发了。系统会直接抛出异常,或者设置无效。

真机环境,可以尝试在 开发人员选项 中设置开启 “不保留活动” 按钮,如下图所示:

继续阅读测试Activity的onSaveInstanceState/onRestoreInstanceState

Android获取导航栏/状态栏/键盘的高度和状态

最近 Android 11 系统兼容性测试的时候,发现界面适配异常(标题栏沉浸式部分),已经无法通过网上流行的,通过反射方法获取状态栏的高度了。

于是翻了一下以前公共库里的代码,发现是使用如下代码获取状态栏高度的:

/** 
* 获取状态栏高度
* */  
int statusBarH = 0;  
try {  
    Class<?> clazz = Class.forName("com.android.internal.R$dimen");  
    Object object = clazz.newInstance();  
    int height = Integer.parseInt(clazz.getField("status_bar_height").get(object).toString());  
    statusBarH = getResources().getDimensionPixelSize(height);  
} catch (Exception e) {  
    e.printStackTrace();  
}  
return statusBarH;

其实从 Android 9 开始,就已经对通过反射调用非公开 API 的方式进行警告了。

Android 11以前的系统,只是给出警告, Android 11直接抛出了调用异常。

于是搜索一下,发现网上已经更改成如下方法了:

int statusBarH = 0;  
//获取status_bar_height资源的ID  
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");  
if (resourceId > 0) {  
    //根据资源ID获取响应的尺寸值  
    statusBarH = getResources().getDimensionPixelSize(resourceId);  
}  
return statusBarH;

仔细观察两个方法,会发现,其实两者都是获取了系统里的状态栏使用的某个资源的高度信息,然后作为状态栏的高度信息

另外,网上流传的另一段代码

Rect frame = new Rect();
getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);
int statusBarHeight = frame.top;

不能解决小屏模式下的显示问题,在小屏模式下得到的偏移并不是正确的状态栏高度

这样就引发一个问题,那就是如果非官方的系统UI,比如小米,华为等自定义的UI,不使用这个资源文件,或者根本就没有这个资源文件,那么获取到的高度信息不就是不正确的了吗?

继续阅读Android获取导航栏/状态栏/键盘的高度和状态