hi,粉丝朋友们!
今天刚好使用了一下新版本的scrcpy,发现了竟然新版本scrcpy支持了音频,这个不得不说让我十分的感兴趣,这里我刚好给出一下相关的调研结果。
更多framework干货知识手把手教学
Log.i("qq群",“422901085”);
1、scrcpy获取音频方式
大家都知道要获取系统的声音一般需要通过类是如下接口获取系统声音,类是我们说的内录:
builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX);
但是这个REMOTE_SUBMIX是需要权限的,因为这个毕竟属于一个高危险的行为,你想一个任意第三方app即可以随意来内录你的手机系统声音
所以scrcpy可以获取声音了,其实还是和安卓系统权限漏洞有关系拉(可能也不是漏洞,只是被scrcpy刚好发现了可以这样做)
2、scrcpy音频获取原理分析
经过scrcpy代码分析发现其实实现原理方式就是这种
builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX);录制方式:
private static AudioRecord createAudioRecord() {
AudioRecord.Builder builder = new AudioRecord.Builder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
builder.setContext(FakeContext.get());
}
builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX);
builder.setAudioFormat(createAudioFormat());
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, FORMAT);
// This buffer size does not impact latency
builder.setBufferSizeInBytes(8 * minBufferSize);
return builder.build();
}
但是这个调用其实很简单,就是关键在与权限,为啥这个scrcpy可以获取到这个REMOTE_SUBMIX
<!-- Allows an application to capture audio output.
Use the {@code CAPTURE_MEDIA_OUTPUT} permission if only the {@code USAGE_UNKNOWN}),
{@code USAGE_MEDIA}) or {@code USAGE_GAME}) usages are intended to be captured.
<p>Not for use by third-party applications.</p> -->
<permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT"
android:protectionLevel="signature|privileged|role" />
明显看出这里需要platform签名和privileged等
但是scrcpy都不具有这些
那么是怎么实现可以正常调用呢?
这里我经过去相关权限
@Override
@PermissionCheckerManager.PermissionResult
public int checkPermission(@NonNull String permission,
@NonNull AttributionSourceState attributionSourceState, @Nullable String message,
boolean forDataDelivery, boolean startDataDelivery, boolean fromDatasource,
int attributedOp) {
Objects.requireNonNull(permission);
Objects.requireNonNull(attributionSourceState);
final AttributionSource attributionSource = new AttributionSource(
attributionSourceState);
android.util.Log.i("lsm","checkPermission permission " + permission + " attributionSource = " + attributionSource);
final int result = checkPermission(mContext, mPermissionManagerServiceInternal,
permission, attributionSource, message, forDataDelivery, startDataDelivery,
fromDatasource, attributedOp);
// Finish any started op if some step in the attribution chain failed.
if (startDataDelivery && result != PermissionChecker.PERMISSION_GRANTED
&& result != PermissionChecker.PERMISSION_SOFT_DENIED) {
if (attributedOp == AppOpsManager.OP_NONE) {
finishDataDelivery(AppOpsManager.permissionToOpCode(permission),
attributionSource.asState(), fromDatasource);
} else {
finishDataDelivery(attributedOp, attributionSource.asState(), fromDatasource);
}
}
return result;
}
加入打印后如下:
03-07 18:09:07.367 2240 2597 I lsm : checkPermission permission android.permission.CAPTURE_AUDIO_OUTPUT attributionSource = AttributionSource { uid = 2000, packageName = com.android.shell, attributionTag = null, token = android.os.BinderProxy@584a956, next = null }
发现居然这里检测的uid 2000这个大家都可以理解,因为scrcpy本身user就是shell,但是这里的packageName = com.android.shell,是从哪里来的?
这里就需要看看,哪怕是这个包名又怎么可以获取CAPTURE_AUDIO_OUTPUT呢?
1、这里先解决第一个为啥packageName = com.android.shell
在s上其实就是靠这个
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
builder.setContext(FakeContext.get());
}
FakeContext代码如下:
public final class FakeContext extends ContextWrapper {
public static final String PACKAGE_NAME = "com.android.shell";
public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29
private static final FakeContext INSTANCE = new FakeContext();
public static FakeContext get() {
return INSTANCE;
}
private FakeContext() {
super(null);
}
@Override
public String getPackageName() {
return PACKAGE_NAME;
}
@Override
public String getOpPackageName() {
return PACKAGE_NAME;
}
@TargetApi(Build.VERSION_CODES.S)
@Override
public AttributionSource getAttributionSource() {
AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID);
builder.setPackageName(PACKAGE_NAME);
return builder.build();
}
}
所以综合大概就知道原来是调用时候传递了一个Context进去
当然同时也会有疑问,那么如果不传递是否可以?android 12就没有传递,为啥也可以,其实这里我经过验证哪怕屏蔽
builder.setContext(FakeContext.get());其实也是可以正常有一个packageName = com.android.shell
这样来看好像这个Context设置就不是最关键的,那肯定还有其他地方进行设置,经过寻找发现这这里也进行设置,如果Context为null时候:
private AudioRecord(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
int sessionId, @Nullable Context context,
int maxSharedAudioHistoryMs) throws IllegalArgumentException {
//省略
AttributionSource attributionSource = (context != null)
? context.getAttributionSource() : AttributionSource.myAttributionSource();
//省略
}
public static @NonNull AttributionSource myAttributionSource() {
//省略
try {
if (uid == Process.SHELL_UID) {
android.util.Log.i("lsm","uid Process.SHELL_UID packageName = "+ AppGlobals.getPackageManager().getPackagesForUid(uid)[0]);
}
//这里最为关键,或根据uid从AppGlobals.getPackageManager().getPackagesForUid查询的第一个作为packageName
return new AttributionSource.Builder(uid)
.setPackageName(AppGlobals.getPackageManager().getPackagesForUid(uid)[0])
.build();
} catch (Exception ignored) {
}
throw new IllegalStateException("Failed to resolve AttributionSource");
}
加入对应打印如下:
03-07 18:09:07.356 4700 4730 I lsm : uid Process.SHELL_UID packageName = com.android.shell
2、为啥com.android.shell就可以获取权限?
com.android.shell其实是Shell.apk这个Shell.apk当然属于platform签名,那么也就可以说明已经可以得到系统授权了,当然那也要manifest中有对应的权限声明才可以阿,哈哈,其实这样真相如下:
那么scrcpy整体可以支持音频就解密了