安卓系统面经_安卓面经(13/20)SELinux框架超解析

牛客高级系列专栏:

安卓(安卓系统开发也要掌握)

嵌入式

  • 本人是2020年毕业于广东工业大学研究生:许乔丹,有国内大厂CVTE和世界500强企业安卓开发经验,该专栏整理本人从嵌入式Linux转Android系统开发过程中对常见安卓系统开发面试题的理解;
  • 1份外卖价格助您提高安卓面试准备效率,为您面试保驾护航!!

正文开始⬇

本篇主要讲述Android SELinux的基本概念,包含类型、属性、规则,Sepolicy的核心模块、关键文件,SELinux配置的步骤、调试验证方法等。

面试题预览

1.selinux是什么?简述其作用和优点⭐⭐⭐⭐⭐
2.怎么样开关SELinux?⭐⭐⭐
3.能说说你具体是怎么解决SELinux权限问题的吗?以及是这么调试的呢?⭐⭐⭐⭐⭐
4.SELinux如何保护Android系统的安全性?举例说明。⭐⭐⭐
5.如何检测Android设备上的SELinux状态?⭐⭐
6.对于Android应用程序,SELinux如何实现应用程序沙箱?⭐⭐⭐⭐
7.请解释SELinux策略文件中的标签(label)和它们的作用。⭐⭐⭐
8.如何通过Android的SELinux策略文件修改允许或拒绝的访问操作?⭐⭐⭐⭐
9.如何对SELinux策略文件进行自定义修改以加强Android系统的安全性?⭐⭐⭐⭐⭐
10.当Android应用程序尝试访问受保护的文件或系统资源时,SELinux将如何进行决策?⭐⭐
11.SELinux如何保护网络安全?举例说明。⭐⭐
12.如何使用Android的SELinux审计功能记录和监控Android设备上的安全事件?⭐⭐

1 概述

1.1. 概念

作为Android安全模型的一部分,Android使用安全增强型Linux(SELinux)对所有进程强制执行强制访问控制(MAC),甚至包括以Root/超级用户权限运行的进程(Linux功能)。借助SELinux,Android可以更好地保护和限制系统服务、控制对应用数据和系统日志的访问、降低恶意软件的影响,并保护用户免遭移动设备上的代码可能存在的缺陷的影响。

SELinux按照默认拒绝的原则运行:任何未经明确允许的行为都会被拒绝。SELinux可按两种全局模式运行:

  • 宽容模式:权限拒绝事件会被记录下来,但不会被强制执行(仅记录但不强制执行SELinux安全政策)
  • 强制模式:权限拒绝事件会被记录下来并强制执行。(强制执行并记录安全政策。如果失败,则显示为EPERM错误)

在选择强制执行级别时只能二择其一,您的选择将决定您的政策是采取操作,还是仅允许您收集潜在的失败事件。宽容模式在实现过程中尤其有用。

1.2 MAC和DAC

安全增强型Linux(SELinux)是适用于Linux操作系统的强制访问控制(MAC)系统。

作为MAC系统,它与Linux中用户非常熟悉的自主访问控制(DAC)系统不同。在DAC系统中,存在所有权的概念,即特定资源的所有者可以控制与该资源关联的访问权限。这种系统通常比较粗放,并且容易出现无意中提权的问题。MAC系统则会在每次收到访问请求时都先咨询核心机构,再做出决定。

1.3 类型、属性和规则(te)

Android依靠SELinux的类型强制执行(TE)组件来实施其政策。这表示所有对象(例如文件、进程或套接字)都具有相关联的类型。例如,默认情况下,应用的类型为untrusted_app。对于进程而言,其类型也称为域。可以使用一个或多个属性为类型添加注解。属性可用于同时指代多种类型。

对象会映射到类(例如文件、目录、符号链接、socket套接字),并且每个类的不同访问权限类型由权限表示。

例如,file类存在权限open。虽然类型和属性作为Android SELinux政策的一部分会进行定期更新,但权限和类是静态定义的,并且作为新Linux版本的一部分也很少进行更新。

政策规则采用以下格式:allow source target:class permissions;,其中:

  • source - 规则主题的类型(或属性)。谁正在请求访问权限?
  • 目标 - 对象的类型(或属性)。对哪些内容提出了访问权限请求?
  • 类 - 要访问的对象(例如,文件、套接字)的类型
  • 权限 - 要执行的操作(或一组操作,例如读取、写入)

规则的一个示例如下:这表示应用可以读取和写入带有app_data_file标签的文件。还有其他应用类型。例如,isolated_app用于清单中含有isolatedProcess=true的应用服务。

allow untrusted_app app_data_file:file { read write };

Android对涵盖应用的所有类型使用名为appdomain的属性,而不是对这两种类型重复同一规则:

# Associate the attribute appdomain with the type untrusted_app.
typeattribute untrusted_app, appdomain;

# Associate the attribute appdomain with the type isolated_app.
typeattribute isolated_app, appdomain;

allow appdomain app_data_file:file { read write };

当编写的规则指定了某个属性名称时,该名称会自动扩展为列出与该属性关联的所有域或类型。一些重要属性包括:

  • domain - 与所有进程类型相关联的属性
  • file_type - 与所有文件类型相关联的属性

1.3.1 宏的使用

特别是对于文件访问权限,有很多种权限需要考虑。例如,read权限不足以打开相应文件或对其调用stat。为了简化规则定义,Android提供了一组宏来处理最常见的情况

例如,若要添加open等缺少的权限,可以将上述规则改写为:

allow appdomain app_data_file:file rw_file_perms;

查看实用宏的更多示例:

  • system/sepolicy/public/global_macros
  • system/sepolicy/public/te_macros

尽可能使用宏,以降低因相关权限被拒而导致失败的可能性。定义类型后,需要将其与所代表的文件或进程相关联。

1.4. 安全上下文和类别(file_contexts)

调试SELinux政策或为文件添加标签时(通过file_contexts或运行ls -Z),可能会遇到安全上下文(也称为标签)。

例如 u:r:untrusted_app:s0:c15,c256,c513,c768。安全上下文的格式为:user:role:type:sensitivity[:categories]。

通常可以忽略上下文的user、role和sensitivity字段。

从Android S开始,类别被用于:

  • 分隔应用数据,使其不被其他应用访问。
  • 分隔不同实际用户的应用数据。

1.4.1. file_contexts解释说明

oem_lock u:object_r:oem_lock_service:s0

说明如下:

  • oem_lock:系统中具体资源,如服务名、设备名、文件目录等
  • u: selinux中唯一的用户
  • object_r:描述资源类型。r为进程资源,object_r非进程资源
  • oem_lock_service:资源在权限规则中所属代表
  • s0:selinux中权限级别,一般使用s0

1.5. te文件内容的语法规则

1.5.1 语法规则概述

rule_name source_type target_type : class perm_set

  • rule_name:赋予权限的规则,包含allow、dontaudit、auditallow、neverallow,命令不可以随意添加。
    • allow:允许某个进程执行某个动作
    • auditallow:audit含义就是记录某项操作。默认SELinux只记录那些权限检查失败的操作。 auditallow则使得权限检查成功的操作也被记录。注意,allowaudit只是允许记录,它和赋予权限没关系。赋予权限必须且只能使用allow语句。
    • dontaudit:对那些权限检查失败的操作不做记录。
    • neverallow:没有被allow到的动作默认就不允许执行的。neverallow只是显式地写出某个动作不被允许,如果添加了该动作的allow,则会编译错误
  • source_type:源类型,访问target_type的主体或主体集合(域),可自定义
  • target_type:目标类型。接受主体访问的客体或客体集合(域),可自定义
  • class:类别。客体资源类型,不同的资源类型具有不同访问权限,可自定义、可继承
  • perm_set:动作集。客体予以主体的权限说明。是class中具有的权限的子集 source_type、target_type使用type、typeattribute、attribute定义
  • attribute定义一个代表具有某种相中属性的集合(即域):attribute dev_type;
  • type定义代表一个或一类资源类型,并分配至不同属性(域)中:
# 定义一个类型,属于dev_type属性
type ttyMT_device, dev_type; 

###############以上定义可以拆分为两部分
# 仅定义一个类型
type ttyMT_device;
# 仅把ttyMT_device类型关联到dev_type属性
typeattribute ttyMT_device dev_type;
  • 属性间使用逗号,一个类型可以关联至多个属性:type oem_lock_service, system_api_service, system_server_service, service_manager_type;
  • class字段使用comm和class定义,comm定义的class可以被class定义的对象继承
common file {
	ioctl read write create getattr setattr lock relabelfrom relabelto
	append unlink link rename execute swapon quotaon mounton
}

class类型继承comm类型:

class dir
inherits file
{
	add_name
	remove_name
	reparent
	search
	rmdir
	open
	audit_access
	execmod
}

1.5.2 TE的正则表达式和集合

TE表达式里可以用“{}”来表示一个集合,如:

/*
  允许user_t对bin_t类型的文件和文件夹执行read,getattr操作
*/
allow user_t bin_t : { file dir } { read getattr }; 

/*
  允许domain对exec_type,sbin_t类型的文件执行execute的动作
*/
allow domain { exec_type sbin_t } : file execute; 

可以在集合里使用“*”,“-” 和 “~” 三个通配符
/*
  允许user_t对bin_t类型的文件和文件夹执行所有操作
*/
allow user_t bin_t : { file dir } *; 


/*
  允许user_t对bin_t类型的文件和文件夹执行除了read,getattr以外的所有操作
*/
allow user_t bin_t : { file dir } ~{ read getattr }; 

/*
  允许domain对exec_type类型的文件执行execute的动作,除了sbin_t以外
*/
allow domain {
 exec_type 
  -sbin_t 
} : file execute; 

特别地是,在修改neverallow的问题里,"-"通配符特别好用。示例:
在下面neverallow里排除cache_file,允许untrusted_apps对cache_file做{ create unllink}动作 alt

1.5.3 audit2allow工具

sudo apt install policycoreutils
adb shell logcat | grep avc > avc_log.txt
audit2allow -i avc_log.txt

1.6. 注意点

Android并不会使用SELinux提供的所有功能。注意以下几点:

  • AOSP中的大部分政策都是使用内核政策语言定义的。在使用通用中间语言(CIL)时,会存在一些例外情况
  • 不使用SELinux用户。唯一定义的用户是u。必要时,系统会使用安全上下文的类别字段表示实际用户
  • 不使用SELinux角色和基于角色的访问权限控制 (RBAC)。定义并使用了两个默认角色:r(适用于主题)和object_r(适用于对象)
  • 不使用SELinux敏感度。已始终设置好默认的s0敏感度
  • 不使用SELinux布尔值。一旦设备政策构建完成,该政策不再取决于设备状态。这简化了政策的审核和调试过程

2. SElinux实现

2.1. SElinux核心模块

Android的SElinux可以阅读system/sepolicy目录中的文件。这些文件在编译后会包含SELinux内核安全政策,并涵盖上游Android操作系统

通常情况下,不能直接修改system/sepolicy文件,但可以添加或修改自己的设备专用政策文件(位于/device/manufacturer/device-name/sepolicy目录中)。在Android 8.0及更高版本中,对这些文件所做的更改只会影响供应商目录中的政策。 无论是哪个Android版本,都仍需要修改以下文件:

2.1.1. SElinux相关模块

SELinux 的构建逻辑位于以下文件中:

  • external/selinux:外部SELinux项目,用于构建HOST命令行实用工具以编译SELinux政策和标签
  • external/selinux/libselinux:Android仅使用外部libselinux项目的一个子集,以及一些Android专用自定义内容(参阅external/selinux/README.android了解详情)
  • external/selinux/libsepol:
  • chkcon:确定安全环境对指定的二进制政策(主机可执行文件)是否有效
  • libsepol:用于操控二进制安全政策(主机静态/共享库、目标静态库)的 SELinux 库
  • external/selinux/checkpolicy:SELinux 政策编译器(主机可执行文件:checkpolicy、checkmodule和dispol)。依赖于libsepol
  • system/sepolicy:核心Android SELinux政策配置,包括上下文和政策文件。主要sepolicy构建逻辑也位于此处 (system/sepolicy/Android.mk)

2.2. SElinux关键文件

2.2.1. 上下文的描述文件(*_contexts)

可以在上下文的描述文件中为需要的对象指定标签

  • file_contexts:用于为文件分配标签,并且可供多种用户空间组件使用。在创建新政策时,需创建或更新该文件,以便为文件分配新标签。如需应用新的file_contexts,需重新构建文件系统映像,或对要重新添加标签的文件运行restorecon(比如重新加载权限restorecon -R /(对应权限目录))。在升级时,对file_contexts所做的更改会在升级过程中自动应用于系统和用户数据分区。此外,还可以通过以下方式使这些更改在升级过程中自动应用于其他分区:在以允许读写的方式装载相应分区后,将restorecon_recursive(重载权限)调用添加到init.board.rc文件中
  • property_contexts:用于为Android系统属性分配标签,以便控制哪些进程可以设置这些属性。在启动期间,init进程会读取此配置
  • service_contexts:用于为Android Binder服务分配标签,以便控制哪些进程可以为相应服务添加(注册)和查找(查询)Binder引用。在启动期间,servicemanager进程会读取此配置
  • genfs_contexts:用于为不支持扩展属性的文件系统(例如,proc或vfat)分配标签。此配置会作为内核政策的一部分进行加载,但更改可能对内核inode无效。要全面应用更改,需要重新启动设备,或卸载并重新装载文件系统。此外,通过使用context=mount选项,还可以为装载的特定系统文件(例如vfat)分配特定标签
  • seapp_contexts:用于为应用进程和/data/data目录分配标签。在每次应用启动时,zygote进程都会读取此配置;在启动期间,installd会读取此配置
  • mac_permissions.xml:用于根据应用签名和应用软件包名称(后者可选)为应用分配seinfo标记。随后,分配的seinfo标记可在seapp_contexts文件中用作密钥,以便为带有该seinfo标记的所有应用分配特定标签。在启动期间,system_server会读取此配置 简单来说:
  • file_contexts //系统中所有file_contexts安全上下文
  • seapp_contexts //app安全上下文
  • property_contexts //属性的安全上下文
  • service_contexts //service文件安全上下文
  • genfs_contexts //虚拟文件系统安全上下文

2.2.1.1 文件上下文file_contexts

Android8.0针对file_contexts引入了以下更改:

  • 为了避免启动期间在设备上产生额外的编译开销,file_contexts不再以二进制文件形式存在。而是可读的正则表达式文本文件,例如{property,service}_contexts(和7.0之前的版本一样)
  • file_contexts拆分成了两个文件:
  • plat_file_contexts
  • Android平台file_context,没有设备专用标签,例外情况是,必须准确标记/vendor分区的某些部分,以确保sepolicy文件正常运行
  • 必须位于设备上system分区中的/system/etc/selinux/plat_file_contexts下,并由init在启动时加载(与供应商file_context一起加载)
  • vendor_file_contexts
  • 设备专用file_context,通过合并file_contexts(位于设备的Boardconfig.mk文件中由BOARD_SEPOLICY_DIRS指向的目录下)进行构建
  • 必须安装到vendor分区中的/vendor/etc/selinux/vendor_file_contexts下,并由init在启动时加载(与平台file_context一起加载)

2.2.1.2 属性上下文property_contexts

在Android8.0中,property_contexts拆分成了两个文件:

  • plat_property_contexts
  • 没有设备专用标签的Android平台property_context
  • 必须位于system分区中的/system/etc/selinux/plat_property_contexts下,并由init在启动时加载(与供应商property_contexts一起加载)
  • vendor_property_contexts
  • 设备专用property_context,通过合并property_contexts(位于设备的Boardconfig.mk文件中由BOARD_SEPOLICY_DIRS指向的目录下)进行构建
  • 必须位于vendor分区中的/vendor/etc/selinux/vendor_property_contexts下,并由init在启动时加载(与平台property_context一起加载)

2.2.1.3. 服务上下文service_contexts

在Android8.0中,service_contexts拆分成了以下文件:

  • plat_service_contexts
  • servicemanager的Android平台专用service_context。service_context没有设备专用标签
  • 必须位于system分区中的/system/etc/selinux/plat_service_contexts下,并由servicemanager在启动时加载(与供应商service_contexts一起加载)
  • vendor_service_contexts
  • 设备专用service_context,通过合并service_contexts(位于设备的Boardconfig.mk文件中由BOARD_SEPOLICY_DIRS指向的目录下)进行构建
  • 必须位于vendor分区中的/vendor/etc/selinux/vendor_service_contexts下,并由servicemanager在启动时加载(与平台service_contexts一起加载)
  • 虽然servicemanager会在启动时查找此文件,但对于完全兼容的TREBLE设备,vendor_service_contexts绝不能存在。这是因为,vendor和system进程之间的所有交互都必须通过hwservicemanager/hwbinder发生
  • plat_hwservice_contexts
  • hwservicemanager的Android平台hwservice_context(没有设备专用标签)
  • 必须位于system分区中的/system/etc/selinux/plat_hwservice_contexts下,并由hwservicemanager在启动时加载(与vendor_hwservice_contexts一起加载)
  • vendor_hwservice_contexts
  • 设备专用hwservice_context,通过合并hwservice_contexts(位于设备的Boardconfig.mk文件中由BOARD_SEPOLICY_DIRS指向的目录下)进行构建
  • 必须位于vendor分区中的/vendor/etc/selinux/vendor_hwservice_contexts下,并由hwservicemanager在启动时加载(与plat_service_contexts一起加载)
  • vndservice_contexts
  • vndservicemanager的设备专用service_context,通过合并vndservice_contexts(位于设备的Boardconfig.mk中由BOARD_SEPOLICY_DIRS指向的目录下)进行构建
  • 此文件必须位于vendor分区中的/vendor/etc/selinux/vndservice_contexts下,并由vndservicemanager在启动时加载

2.2.1.4. Seapp 上下文seapp_contexts

在Android8.0中,seapp_contexts拆分成了两个文件:

  • plat_seapp_contexts
  • 没有设备专用更改的Android平台seapp_context
  • 必须位于system分区中的/system/etc/selinux/plat_seapp_contexts.下
  • vendor_seapp_contexts
  • 平台seapp_context的设备专用扩展,通过合并seapp_contexts(位于设备的Boardconfig.mk文件中由BOARD_SEPOLICY_DIRS指向的目录下)进行构建
  • 必须位于vendor分区中的/vendor/etc/selinux/vendor_seapp_contexts下

2.2.1.5. MAC权限mac_permissions

在Android8.0中,mac_permissions.xml拆分成了两个文件:

  • 平台mac_permissions.xml
  • 没有设备专用更改的Android平台mac_permissions.xml。
  • 必须位于system分区中的/system/etc/selinux/.下。
  • 非平台mac_permissions.xml
  • 平台mac_permissions.xml的设备专用扩展,通过mac_permissions.xml(位于设备的Boardconfig.mk文件中由BOARD_SEPOLICY_DIRS指向的目录下)进行构建
  • 必须位于vendor分区中的/vendor/etc/selinux/.下

2.2.2. BoardConfig.mk makefile引用

修改或添加政策文件和上下文的描述文件后,需要更新/device/manufacturer/device-name/BoardConfig.mkmakefile以引用sepolicy子目录和每个新的政策文件

# 引用目录
BOARD_SEPOLICY_DIRS += \
       <root>/device/manufacturer/device-name/sepolicy

# 单独引用
BOARD_SEPOLICY_UNION += \
       genfs_contexts \
       file_contexts \
       sepolicy.te

2.3. 实现步骤

2.3.1. 内核启用SElinux

配置CONFIG_SECURITY_SELINUX=y(例如kernel/msm-5.4/kernel/configs/android-base.config)

2.3.2. 更改kernel_cmdline参数(配置修改为permissive)

配置BOARD_KERNEL_CMDLINE := androidboot.selinux=permissive 仅适用于初始制定设备政策的情况。在拥有初始引导程序政策后,请移除此参数,以便将设备恢复强制模式,否则设备将无法通过CTS验证

2.3.3. 宽容模式启动系统查看所需权限

  • 在Ubuntu 14.04或更高版本中,请运行以下命令: adb shell su -c dmesg | grep denied | audit2allow -p out/target/product/BOARD/root/sepolicy
  • 在 Ubuntu 12.04 中,请运行以下命令:
adb pull /sys/fs/selinux/policy
adb logcat -b all | audit2allow -p policy

2.3.4. 评估警告的输出

评估与以下内容类似的警告的输出:init: Warning! Service name needs a SELinux domain defined; please fix!

2.3.5. 标识设备以及需要添加标签的其他新文件

2.3.6. 配置对象使用现有标签或新标签

查看*_contexts文件,了解之前是如何为内容添加标签的,然后根据对标签含义的了解分配一个新标签。 这个标签最好是能够融入到政策中的现有标签,但有时也需要使用新标签,而且还需要提供关于访问该标签的规则。将标签添加到相应的上下文的描述文件中

2.3.7. 标识应该拥有自己的安全域的域/进程

可能需要为每一项分别编写一个全新的政策。例如,从init衍生的所有服务都应该有自己的安全域。以下命令有助于查看保持运行的服务(不过所有服务都需要如此处理):

adb shell su -c ps -Z | grep init
adb shell su -c dmesg | grep 'avc: '

2.3.8. 查看init.device.rc发现没有域类型的域

在开发过程早期为其提供相应的域,以避免向init添加规则或将init访问权限与其自身政策中的访问权限混淆 通常会为指定的项目新建定义rc文件作一些操作,比如chmod、chown权限赋予、创建文件夹、启动service、重载selinux权限等等

2.3.9. 设置BOARD_CONFIG.mk

设置BOARD_CONFIG.mk以使用BOARD_SEPOLICY_*变量(通常不需要修改)

2.3.10 检查init.device.rc和fstab.device文件

确保每一次使用mount都对应一个添加了适当标签的文件系统,或者指定了context= mount选项

2.3.11. 查看每个拒绝事件

查看每个拒绝事件,并创建SELinux政策来妥善处理每个拒绝事件 实际进行权限修复修改

3. 自定义SELinux

集成基本级别的SELinux功能并全面分析结果后,可以添加自己的政策设置,以便涵盖对Android操作系统所做的自定义。这些政策必须仍然满足Android兼容性计划的要求,并且不得移除默认的 SELinux设置

制造商不得移除现有的SELinux政策,否则可能会破坏Android SELinux的实施方式及其管控的应用。这包括可能需要改进以遵守政策并正常运行的第三方应用。应用必须无需任何修改即可继续在启用了SELinux的设备上正常运行

3.1. 注意点

当开始自定义SELinux时,需注意:

  • 为所有新的守护进程编写SELinux政策
  • 尽可能使用预定义的域
  • 为作为init服务衍生的所有进程分配域
  • 在编写政策之前先熟悉相关的宏
  • 向AOSP提交对核心政策进行的更改
  • 不得创建不兼容的政策
  • 不得允许对最终用户政策进行自定义
  • 不得允许对移动设备管理 (MDM) 政策进行自定义
  • 不得恐吓违反政策的用户
  • 不得添加后门程序

3.2. 操作步骤

如果要自定义SELinux设置,则应格外谨慎,以免破坏现有应用。要开始使用,请按下列步骤操作:

  1. 使用最新的Android内核
  2. 采用最小权限原则
  3. 仅针对Android需要添加的内容调整SELinux政策。默认政策能够自动适用于Android开源项目代码库
  4. 将各个软件组件拆分成多个负责执行单项任务的模块(按模块、架构合理有效的划分sepolicy的配置)
  5. 创建将这些任务与无关功能隔离开来的 SELinux 政策
  6. 将这些政策放在/device/manufacturer/device-name/sepolicy目录中的*.te文件内(te是SELinux政策源代码文件使用的扩展名),然后使用BOARD_SEPOLICY变量将它们纳入到的build编译中
  7. 先将新域设为宽容域。为此,可以在该域的.te文件中使用宽容声明(调试手法)
  8. 分析结果并优化域定义
  9. 当userdebug版本中不再出现拒绝事件时,移除宽容声明(将模式从宽容模式切换成强制模式)

3.3. 声明宏编写示例

SELinux基于M4计算机语言,因此支持多种有助于节省时间的宏。

在以下示例中,所有域都被授予向/dev/null读写数据(write)的权限以及从/dev/zero读取数据(read)的权限

# Allow read / write access to /dev/null
allow domain null_device:chr_file { getattr open read ioctl lock append write};

# Allow read-only access to /dev/zero
allow domain zero_device:chr_file { getattr open read ioctl lock };

system/sepolicy/public/global_macros看到宏定义:

define(`x_file_perms', `{ getattr ex

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

Android系统面试题全解析 文章被收录于专栏

2020年研究生毕业后,工作重心由嵌入式Linux转为安卓系统,Android发展已经很多年,网上面向中初级Android系统开发的面经还比较少,也不够集中,因此梳理出本专栏,本专栏收集了本人工作中持续积累的众多安卓系统知识,持续更新中。

全部评论

相关推荐

点赞 4 评论
分享
牛客网
牛客企业服务