跳至主要內容

ARDC灭屏投屏探秘

guodongAndroid大约 7 分钟

背景

最近使用 ARDC(B2973) 时发现在视图菜单里有「灭屏投屏」选项,点击后,手机的屏幕会熄灭,而 ARDC 的投屏不受影响,依然可以在 ARDC 中进行操作,这显然不是简单的锁屏,这就激起了笔者的好奇心,必须一探究竟。

ardc

手机基于HarmonyOS 2.0,Android 10,API 29 且未ROOT,ARDC本身也没有系统级别权限。

探秘

Log

首先想到的是点击「灭屏投屏」后通过 Logcat 查看日志输出,果然发现一些相关的可疑日志:

SurfaceComposerClient   app_process      Setting power mode 0 on display 0x76c4c873e0
SurfaceFlinger          surfaceflinger   SetPowerMode_0 on display 0.

以上日志显示:点击「灭屏投屏」后修改了设备的 Power Mode0

根据日志里的线索,在 aospxrefopen in new window 上搜索 SurfaceComposerClient 相关的信息:

search SurfaceComposerClient result

打开 SurfaceComposerClient.cpp 文件,通过搜索可以发现以下函数:

 void SurfaceComposerClient::setDisplayPowerMode(const sp<IBinder>& token, int mode) {
	ComposerService::getComposerService()->setPowerMode(token, mode);
}

在上述函数中继续调用 ComposerService::getComposerService()->setPowerMode 函数,其中 ComposerService 其实是 ISurfaceComposer,那么实际调用的就是 ISurfaceComposer#setPowerMode 函数:

virtual void setPowerMode(const sp<IBinder>& display, int mode)
 {
     Parcel data, reply;
     data.writeInterfaceToken(ISurfaceComposer::getInterfaceDescriptor());
     data.writeStrongBinder(display);
     data.writeInt32(mode);
     remote()->transact(BnSurfaceComposer::SET_POWER_MODE, data, &reply);
 }

ISurfaceComposer#setPowerMode 函数中,通过 Binder 机制远程调用了 SurfaceFlinger#setPowerMode 函数:

void SurfaceFlinger::setPowerMode(const sp<IBinder>& displayToken, int mode) {
    postMessageSync(new LambdaMessage([&]() NO_THREAD_SAFETY_ANALYSIS {
        const auto display = getDisplayDevice(displayToken);
        if (!display) {
            ALOGE("Attempt to set power mode %d for invalid display token %p", mode,
                  displayToken.get());
        } else if (display->isVirtual()) {
            ALOGW("Attempt to set power mode %d for virtual display", mode);
        } else {
            setPowerModeInternal(display, mode);
        }
    }));
}

SurfaceFlinger#setPowerMode 函数内部又调用了自身的 setPowerModeInternal 函数:

void SurfaceFlinger::setPowerModeInternal(const sp<DisplayDevice>& display, int mode) {
    if (display->isVirtual()) {
        ALOGE("%s: Invalid operation on virtual display", __FUNCTION__);
        return;
    }

    const auto displayId = display->getId();
    LOG_ALWAYS_FATAL_IF(!displayId);

    ALOGD("Setting power mode %d on display %s", mode, to_string(*displayId).c_str());

    int currentMode = display->getPowerMode();
    if (mode == currentMode) {
        return;
    }

    display->setPowerMode(mode);

    if (mInterceptor->isEnabled()) {
        mInterceptor->savePowerModeUpdate(display->getSequenceId(), mode);
    }

    if (currentMode == HWC_POWER_MODE_OFF) {
        // Turn on the display
        getHwComposer().setPowerMode(*displayId, mode);
        if (display->isPrimary() && mode != HWC_POWER_MODE_DOZE_SUSPEND) {
            mScheduler->onScreenAcquired(mAppConnectionHandle);
            mScheduler->resyncToHardwareVsync(true, getVsyncPeriod());
        }

        mVisibleRegionsDirty = true;
        mHasPoweredOff = true;
        repaintEverything();

        struct sched_param param = {0};
        param.sched_priority = 1;
        if (sched_setscheduler(0, SCHED_FIFO, &param) != 0) {
            ALOGW("Couldn't set SCHED_FIFO on display on");
        }
    } else if (mode == HWC_POWER_MODE_OFF) {
        // Turn off the display
        struct sched_param param = {0};
        if (sched_setscheduler(0, SCHED_OTHER, &param) != 0) {
            ALOGW("Couldn't set SCHED_OTHER on display off");
        }

        if (display->isPrimary() && currentMode != HWC_POWER_MODE_DOZE_SUSPEND) {
            mScheduler->disableHardwareVsync(true);
            mScheduler->onScreenReleased(mAppConnectionHandle);
        }

        getHwComposer().setPowerMode(*displayId, mode);
        mVisibleRegionsDirty = true;
        // from this point on, SF will stop drawing on this display
    } else if (mode == HWC_POWER_MODE_DOZE ||
               mode == HWC_POWER_MODE_NORMAL) {
        // Update display while dozing
        getHwComposer().setPowerMode(*displayId, mode);
        if (display->isPrimary() && currentMode == HWC_POWER_MODE_DOZE_SUSPEND) {
            mScheduler->onScreenAcquired(mAppConnectionHandle);
            mScheduler->resyncToHardwareVsync(true, getVsyncPeriod());
        }
    } else if (mode == HWC_POWER_MODE_DOZE_SUSPEND) {
        // Leave display going to doze
        if (display->isPrimary()) {
            mScheduler->disableHardwareVsync(true);
            mScheduler->onScreenReleased(mAppConnectionHandle);
        }
        getHwComposer().setPowerMode(*displayId, mode);
    } else {
        ALOGE("Attempting to set unknown power mode: %d\n", mode);
        getHwComposer().setPowerMode(*displayId, mode);
    }

    if (display->isPrimary()) {
        mTimeStats->setPowerMode(mode);
        mRefreshRateStats.setPowerMode(mode);
    }

    ALOGD("Finished setting power mode %d on display %s", mode, to_string(*displayId).c_str());
}

setPowerModeInternal 函数内部的第 10 行可以看到 Logcat 输出的第一行日志。

比较遗憾,笔者继续往下追踪后没有发现 Logcat 输出的第二行日志的代码,可能是 HarmonyOS 修改了 AOSP 的源码。

SurfaceControl

在上一节中,我们通过 Logcat 日志往下追踪代码,这些代码毕竟都是在 native 层,我们要使用的话不大方便,本节我们将往上追踪 native 层的调用,可以在 android_view_SurfaceControl.cpp 中发现:

static void nativeSetDisplayPowerMode(JNIEnv* env, jclass clazz, jobject tokenObj, jint mode) {
    sp<IBinder> token(ibinderForJavaObject(env, tokenObj));
    if (token == NULL) return;

    android::base::Timer t;
    SurfaceComposerClient::setDisplayPowerMode(token, mode);
    if (t.duration() > 100ms) ALOGD("Excessive delay in setPowerMode()");
}

其调用了 SurfaceComposerClient#setDisplayPowerMode 函数,而 nativeSetDisplayPowerMode 是一个 JNI 函数,在上层对应

android.view.SurfaceControl.java,在其中,我们也可以发现 setDisplayPowerMode 方法:

/**
 * @hide
 */
public static void setDisplayPowerMode(IBinder displayToken, int mode) {
    if (displayToken == null) {
        throw new IllegalArgumentException("displayToken must not be null");
    }
    nativeSetDisplayPowerMode(displayToken, mode);
}

此方法是个静态方法,我们可以直接调用,但是它需要 IBinder 参数。继续查看 SurfaceControl 类,可以发现它定义了一些 POWER_MODE 相关的常量,其中就有 POWER_MODE_OFF = 0 ,表示仅关闭显示器;还有一些辅助获取 IBinder 参数的方法:

/**
 * @hide
 */
public static long[] getPhysicalDisplayIds() {
    return nativeGetPhysicalDisplayIds();
}

/**
 * @hide
 */
public static IBinder getPhysicalDisplayToken(long physicalDisplayId) {
    return nativeGetPhysicalDisplayToken(physicalDisplayId);
}

/**
 * TODO(116025192): Remove this stopgap once framework is display-agnostic.
 *
 * @hide
 */
public static IBinder getInternalDisplayToken() {
    final long[] physicalDisplayIds = getPhysicalDisplayIds();
    if (physicalDisplayIds.length == 0) {
        return null;
    }
    return getPhysicalDisplayToken(physicalDisplayIds[0]);
}

至此,我们可以通过反射调用 SurfaceControl#getInternalDisplayToken 方法获取到 IBinder ,然后再反射调用

SurfaceControl#setDisplayPowerMode

app_process

经过上面的分析,当你兴高采烈地编写完反射代码,当然你还需要首先处理隐藏 API 无法反射的问题,然后运行程序点击按钮后,Logcat 直接会告诉你权限拒绝,日志输入如下:

SurfaceComposerClient   com.guodong.android.power     Setting power mode 0 on display 0x76c4c873e0
SurfaceFlinger          surfaceflinger   			  Permission Denial: can't access SurfaceFlinger pid=22253, uid=10189

没错,普通的 APP 无权调用 SurfaceFlinger 中的函数。

那么,你就会想 ARDC 也是一个普通的 APP,它是如何做到的?

既然 ARDC 可以做到,那必然是有相应的权限,让我们对比下 ADRC 和 我们程序的执行日志,可以发现一个不同点,SurfaceComposerClient 日志的执行进程不同:

  • ARDC:app_process 进程
  • 我们的程序:com.guodong.android.power 进程

熟悉 APP 提权的同学对 app_process 肯定不会陌生的。

app_process 正是 APP 提权的一种方式。

app_process 是 Android 上的一个原生程序,是 APP 进程的主入口点。它可以让虚拟机从 main() 方法开始执行一个 Java 程序,那么这个 Java 程序就会拥有一个较高的权限,从而可以执行一些特殊的操作

用法和参数

对于 app_process 的使用没有官方文档,但是 源码open in new window 里说得比较清楚:

Usage: app_process [java-options] cmd-dir start-class-name [options]

源码open in new window 里也对参数做了说明:

java-options     - 传递给 JVM 的参数
cmd-dir          - 暂时没有用,可以传 /system/bin 或 / 都行
start-class-name - 程序入口, main() 方法所在类的全限定名
options          - 可以是下面这些
                    --zygote 启动 zygote 进程用的
                    --start-system-server 启动系统服务(也是启动 zygote 进程的时候用的)
                    --application 启动应用程序
                    --nice-name=启动之后的进程名称,方便查找

CLASSPATH

与 Java 相似, Android 支持在环境变量 CLASSPATH 中指定类搜索路径,此外还可以在虚拟机参数中指定 -Djava.class.path=xxx 。但是, Android 使用 ART 环境运行 Java ,传统的 Java 字节码文件(.class) 是不能直接运行的,app_process 支持在 CLASSPATH 中指定 dexapk 文件:

# 使用CLASSPATH & dex
export CLASSPATH=/data/local/tmp/test.dex
app_process /system/bin com.guodong.android.power.shell.MainKt

# 使用 -Djava.class.path 和 apk
app_process -Djava.class.path=/data/app/com.guodong.android.power-mWiGBpbL6vEE0QKJqfMgyw==/base.apk / com.guodong.android.power.shell.MainKt

服务端

//  Main.kt

fun main() {

    thread {
        println("Server stated")

        val serverSocket = ServerSocket(7890)

        while (true) {
            val socket = serverSocket.accept()
            println("Receive client: ${socket.inetAddress.hostAddress}:${socket.port}")

            socket.getInputStream().bufferedReader().use { br ->
                val line = br.readLine()
                println("Main: $line")

                displayPowerMode()
            }
        }
    }

    Looper.prepare()
    Looper.loop()
}

private fun displayPowerMode() {
    val surfaceControlClass = SurfaceControl::class.java

    val getInternalDisplayTokenMethod =
        surfaceControlClass.getDeclaredMethod("getInternalDisplayToken")
    getInternalDisplayTokenMethod.isAccessible = true
    val token = getInternalDisplayTokenMethod.invoke(null)

    val method = surfaceControlClass.getDeclaredMethod(
        "setDisplayPowerMode",
        IBinder::class.java,
        Int::class.java
    )
    method.isAccessible = true

    method.invoke(null, token, 0)
}

启动服务端

编译并安装 APP 至手机。

adb shell

$ pm path com.guodong.android.power
package:/data/app/com.guodong.android.power-mWiGBpbL6vEE0QKJqfMgyw==/base.apk

$ app_process -Djava.class.path=/data/app/com.guodong.android.power-mWiGBpbL6vEE0QKJqfMgyw==/base.apk / com.guodong.android.power.shell.MainKt

Main.ktapp_process 执行启动后作为服务端接收客户端的连接,客户端发送任意消息被服务器接收到后就会执行 displayPowerMode 函数,函数内部就会反射调用 SurfaceControl#setDisplayPowerMode 方法,由于服务端由 app_process 启动,那么它就有很高的权限,所以反射可以执行成功。

客户端

private fun ActivityMainBinding.initView() {
    powerOff.setOnClickListener {
        thread {
            try {
                Socket("127.0.0.1", 7890).use {
                    it.getOutputStream().bufferedWriter().use { bw ->
                       bw.write("power off")
                    }
                }
            } catch (e: Exception) {
            }
        }
    }
}

客户端连接服务端后发送任意消息即可。

本文 Demoopen in new window 仅供参考。

对于 app_process 的使用可以参考一个开源项目:Shizukuopen in new window

总结

从笔者最初对 ARDC 「灭屏投屏」实现原理的好奇心,到在 Logcat 里查找可疑日志,再从 AOSP 里一点点的摸索跟踪源码,找到灭屏的调用 API,最后再利用 app_process 对 APP 进行提权。

在 AOSP 源码摸索时,学习了 SurfaceFlinger 如何与上层应用建立连接,通信的一些知识,其实也是使用的 Binder 通信机制,对 Binder 通信也是一种学习。

有了这个功能再也不用担心老板看见我玩手机了,”摸鱼“走起😎。