ARDC灭屏投屏探秘
背景
最近使用 ARDC(B2973) 时发现在视图菜单里有「灭屏投屏」选项,点击后,手机的屏幕会熄灭,而 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 Mode 为 0。
根据日志里的线索,在 aospxref 上搜索 SurfaceComposerClient
相关的信息:
打开 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, ¶m) != 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, ¶m) != 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
的使用没有官方文档,但是 源码 里说得比较清楚:
Usage: app_process [java-options] cmd-dir start-class-name [options]
源码 里也对参数做了说明:
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
中指定 dex
或 apk
文件:
# 使用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.kt
被 app_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) {
}
}
}
}
客户端连接服务端后发送任意消息即可。
本文 Demo 仅供参考。
对于 app_process
的使用可以参考一个开源项目:Shizuku。
总结
从笔者最初对 ARDC 「灭屏投屏」实现原理的好奇心,到在 Logcat 里查找可疑日志,再从 AOSP 里一点点的摸索跟踪源码,找到灭屏的调用 API,最后再利用 app_process
对 APP 进行提权。
在 AOSP 源码摸索时,学习了 SurfaceFlinger
如何与上层应用建立连接,通信的一些知识,其实也是使用的 Binder 通信机制,对 Binder 通信也是一种学习。
有了这个功能再也不用担心老板看见我玩手机了,”摸鱼“走起😎。