在本系列的前两篇文章《安全初探》《Web组件安全》中(见公众号),我们了解了UIAbility/PageAbility以及WebView组件的安全风险。今天我们继续介绍在鸿蒙中的后台服务 ServiceExtensionAbility 及其开发中需要注意的安全风险。ServiceExtensionAbility主要用于后台运行的不提供用户交互界面的服务。目前,ServiceExtensionAbility 能力的使用需要应用标记为系统应用,三方应用即非系统应用需要使用后台任务。
随着系统的演进发展,HarmonyOS先后提供了两种应用模型:
Stage模型的组件分类包括:
ServiceExtensionAbility是SERVICE类型的ExtensionAbility组件,提供后台服务能力,其内部持有了一个ServiceExtensionContext,通过ServiceExtensionContext提供了丰富的接口供外部使用。ServiceExtensionAbility可以被其他组件启动或连接,并根据调用者的请求信息在后台处理相关事务。ServiceExtensionAbility支持以启动和连接两种形式运行,系统应用可以调用startServiceExtensionAbility()方法启动后台服务,也可以调用connectServiceExtensionAbility()方法连接后台服务,而三方应用只能调用connectServiceExtensionAbility()方法连接后台服务。启动和连接后台服务的差别:
此处有如下细节需要注意:
connect
的方式被拉起,那么该Service的生命周期将受客户端控制,当客户端调用一次connectServiceExtensionAbility()方法,将建立一个连接,当客户端退出或者调用disconnectServiceExtensionAbility()方法,该连接将断开。当所有连接都断开后,Service将自动退出。start
的方式被拉起,将不会自动退出,系统应用可以调用stopServiceExtensionAbility()方法将Service退出。说明:当前不支持三方应用实现ServiceExtensionAbility,笔者猜测应该是借鉴Android的经验,避免服务后台一直运行。如果三方开发者想要实现后台处理相关事务的功能,可以使用后台任务,具体请参见后台任务。三方应用的UIAbility组件可以通过Context连接系统提供的ServiceExtensionAbility。出于安全隐私考虑,三方应用需要在前台获焦的情况下才能连接系统提供的ServiceExtensionAbility,避免service长期在后台监听。
ServiceExtensionAbility提供了onCreate()、onRequest()、onConnect()、onDisconnect()和onDestroy()生命周期回调,根据需要重写对应的回调方法。下图展示了ServiceExtensionAbility的生命周期
onCreate
服务被首次创建时触发该回调,开发者可以在此进行一些初始化的操作,例如注册公共事件监听等。说明:如果服务已创建,再次启动该ServiceExtensionAbility不会触发onCreate()回调。onRequest
当另一个组件调用startServiceExtensionAbility()方法启动该服务组件时,触发该回调。执行此方法后,服务会启动并在后台运行。每调用一次startServiceExtensionAbility()方法均会触发该回调。onConnect
当另一个组件调用connectServiceExtensionAbility()方法与该服务连接时,触发该回调。开发者在此方法中,返回一个远端代理对象(IRemoteObject),客户端拿到这个对象后可以通过这个对象与服务端进行RPC通信,同时系统侧也会将该远端代理对象(IRemoteObject)储存。后续若有组件再调用connectServiceExtensionAbility()方法,系统侧会直接将所保存的远端代理对象(IRemoteObject)返回,而不再触发该回调。onDisconnect
当最后一个连接断开时,将触发该回调。客户端死亡或者调用disconnectServiceExtensionAbility()方法可以使连接断开。onDestroy
当不再使用服务且准备将其销毁该实例时,触发该回调。开发者可以在该回调中清理资源,如注销监听等。在开始之前,我们需要了解如下准备工作,因为只有系统应用才允许实现ServiceExtensionAbility,所以我们需要将目标应用的签名指纹配置到设备的特权管控白名单,这种开发场景适用于设备提供商自行开发提供某些系统服务,或者三方应用开发者向设备提供商申请添加白名单。因为我们这里是本地测试,所以可以直接按后文步骤修改系统配置来完成:
这里以一个官方的sample为例:https://gitee.com/openharmony/app_samples/tree/master/ability/ServiceExtAbility将这个工程下载到本地后使用DevEcoStudio打开运行,由于DEMO是2年前的,官方SDK更新导致报错,修改如下
@ohos.application.AbilityStage 改为 @ohos.app.ability.AbilityStage
@ohos.application.Ability 改为 @ohos.app.ability.UIAbility
@ohos.application.ServiceExtensionAbility 改为 @ohos.app.ability.ServiceExtensionAbility
还需要修改一处,启动服务应该使用startServiceExtensionAbility
,而不是startAbility
,当然这个更改可能是因为版本更新的缘故,否则程序将报16000002
错误
this.context.startAbility(want).then((data) => {
改为
this.context.startServiceExtensionAbility(want).then((data) => {
这里修改后调用startServiceExtensionAbility
还是会报202错误,因为三方应用只能调用connectServiceExtensionAbility
方法连接后台服务,系统应用才可以调用startServiceExtensionAbility
启动后台服务。替换本地为Full SDK后,重新编译运行,报错如下
$ hdc install -r "D:\Research\openharmony\app_samples-master\ability\ServiceExtAbility\entry\build\default\outputs\default\entry-default-signed.hap"
03/22 14:23:32: Install Failed: [Info]App install path:D:\Research\openharmony\app_samples-master\ability\ServiceExtAbility\entry\build\default\outputs\default\entry-default-signed.hap, queuesize:0, msg:error: failed to install bundle. code:9568344 error: install parse profile prop check error.
AppMod finish
View detailed instructions.
Error while Deploy Hap
这个问题有两个原因,一是项目的编译配置中未设置开发板支持的cpu架构;二是特权应用未将签名指纹配置到设备的特权管控白名单。分别对应两个解决方案:
hdc_std -t '7001005458323933328a023ce2563800' shell
# param get const.product.cpu.abilist
default
// 返回default,那么继续查看/system目录下是否有lib64文件夹
# ls /system
app bin etc fonts lib profile usr
// 如上不存在,所以我们在项目的配置文件中添加abiFilter选项,设置为armeabi-v7a
修改项目的配置文件build-profile.json5,注意不是项目根目录下的
"externalNativeOptions": {
"abiFilters": ["armeabi-v7a"],
},
修改完成后重新编译安装。
keytool -printcert -file .\ExtSrv.cer
...
证书指纹:
SHA1: DD:4C:F8:BE:A7:3C:64:EA:E2:27:91:02:69:9F:51:64:F0:B8:A5:34
SHA256: D6:00:47:C3:91:C5:E3:CF:E1:B6:17:67:1F:F6:71:35:A8:3A:EA:87:F4:F1:CD:31:D4:C7:97:FC:54:15:3E:BD
...
b. 获取设备的特权管控白名单文件install_list_capability.json
i. 连接设备,执行如下命令查看设备的特权管控白名单文件install_list_capability.json
# find /system -name install_list_capability.json
/system/etc/app/install_list_capability.json
ii. 拉取并修改install_list_capability.json
hdc_std -t '7001005458323933328a023ce2563800' file recv /system/etc/app/install_list_capability.json
FileTransfer finish, Size:8353, File count = 1, time:11ms rate:759.36kB/s
c. 在install_list_capability.json文件中新增子项,将步骤1获取到的签名指纹配置到子项的app_signature中,如下
{
"bundleName": "ohos.samples.eTSServiceExtAbility",
"app_signature" : ["D60047C391C5E3CFE1B617671FF67135A83AEA87F4F1CD31D4C797FC54153EBD"],
"associatedWakeUp": true,
"keepAlive": true,
"allowAppUsePrivilegeExtension": true
},
配置说明如下
{
"install_list": [
{
"bundleName": "", // 包名
"singleton": true, // 应用安装到单用户下
"keepAlive": true, // 应用常驻
"runningResourcesApply": true, // 运行资源申请(CPU、事件通知、蓝牙等)
"associatedWakeUp": true, // FA模型应用被关联唤醒
"app_signature" : ["****"], // 当配置的证书指纹和hap的证书指纹一致才生效
"allowCommonEvent": [“usual.event.SCREEN_ON”, “usual.event.THERMAL_LEVEL_CHANGED”],
"allowAppDataNotCleared": true, // 不允许应用数据被删除
"allowAppMultiProcess": true, //允许应用多实例
"allowAppDesktopIconHide": true, //允许隐藏桌面图标
"allowAbilityPriorityQueried": true, //允许Ability配置查询优先级
"allowAbilityExcludeFromMissions": true, // 允许Ability不在任务栈中显示
"allowAppUsePrivilegeExtension": true, // 允许应用使用ServiceExtension、DataExtension
"allowFormVisibleNotify": true, // 允许桌面卡片可见
"allowAppShareLibrary": true, // 允许应用提供应用间HSP能力
"allowMissionNotCleared": true // 允许Ability在任务列表中配置不可移除
},
}
d. 将修改后的install_list_capability.json文件重新推到设备上,并重启设备
$ hdc_std -t '7001005458323933328a023ce2563800' shell mount -o rw,remount /
$ hdc_std -t '7001005458323933328a023ce2563800' file send .\install_list_capability.json /system/etc/app/install_list_capability.json
FileTransfer finish, Size:8668, File count = 1, time:19ms rate:456.21kB/s
至此,我们成功在RK3568的开发板上跑起了这个应用
编译运行官方的DEMO后,我们可以更方便的阅读以及调试其业务代码,其代码目录如下
├─ets
│ ├─Application // AbilityStage实例
│ ├─MainAbility // Main生命周期管理
│ ├─model // 服务连接/断开实现
│ ├─pages // 页面样式/逻辑
│ └─ServiceExtAbility // Service生命周期管理/Service接口实现
└─resources
├─base
│ ├─element
│ ├─media
│ └─profile
├─en
│ └─element
└─zh
└─element
我们关心的ServiceExtensionAbility的实现在./ServiceExtAbility/ServiceExtAbility.ts
中
//import Extension from '@ohos.application.ServiceExtensionAbility'
import ServiceExtensionAbility from '@ohos.app.ability.ServiceExtensionAbility';
import rpc from '@ohos.rpc'
import Logger from '../model/Logger'
const REQUEST_VALUE = 1;
const TAG: string = 'Demo'
//
class StubTest extends rpc.RemoteObject {
constructor(des) {
super(des);
}
onRemoteRequest(code, data, reply, option) {
Logger.log(`onRemoteRequest`);
if (code === REQUEST_VALUE) {
let optFir = data.readInt();
let optSec = data.readInt();
reply.writeInt(optFir + optSec);
Logger.info(TAG, `onRemoteRequest: opt: ${optFir}, opt2: ${optSec}`);
}
return true;
}
queryLocalInterface(descriptor) {
return null;
}
getInterfaceDescriptor() {
return "";
}
sendRequest(code, data, reply, options) {
return null;
}
getCallingPid() {
return REQUEST_VALUE;
}
getCallingUid() {
return REQUEST_VALUE;
}
attachLocalInterface(localInterface, descriptor){}
}
export default class ServiceExtAbility extends ServiceExtensionAbility {
onCreate(want) {
// Logger.info(TAG, 'Play local is null')
Logger.info(TAG, `onCreate, want: ${want.abilityName}`);
}
// 系统应用通过startServiceExtensionAbility()方法启动一个后台服务,onRequest()会被调用
onRequest(want, startId) {
Logger.info(TAG, `onRequest, want: ${want.abilityName}`);
}
onConnect(want) {
Logger.info(TAG, `onConnect , want: ${want.abilityName}`);
return new StubTest("test");
}
onDisconnect(want) {
Logger.info(TAG, `onDisconnect, want: ${want.abilityName}`);
}
onDestroy() {
Logger.info(TAG, `onDestroy`);
}
}
鸿蒙ServiceExtensionAbility服务端继承rpc.RemoteObject
并实现onRemoteRequest
方法来实现对外提供的功能,并在onConnect
回调里返回继承自rpc.RemoteObject的对象。客户端在onConnect
回调里接收到代理对象,并通过SendRequest
向服务端发起请求
onConnect: function (elementName, proxy) {
Logger.log(`onConnect success`);
if (proxy === null) {
Logger.error(`onConnect proxy is null`);
return;
}
let option = new rpc.MessageOption();
let data = new rpc.MessageParcel();
let reply = new rpc.MessageParcel();
data.writeInt(this.outObj.firstLocalValue);
data.writeInt(this.outObj.secondLocalValue);
proxy.sendRequest(REQUEST_CODE, data, reply, option).then((result) => {
Logger.log(`sendRequest: ${result}`);
let msg = reply.readInt();
Logger.log(`sendRequest:msg: ${msg}`);
this.outObj.remoteCallback(SUCCESS_CODE, msg);
}).catch((e) => {
Logger.error(`sendRequest error: ${e}`);
this.outObj.remoteCallback(ERROR_CODE, ERROR_CODE);
});
根据自身业务的需求,选择ServiceExtensionAbility
是否导出。如果是跨进程提供服务那么就必须要导出了,反之最好不用导出服务。相关配置如下,字段visible
// entry\src\main\module.json5
"extensionAbilities": [
{
"name": "ServiceExtAbility",
"icon": "$media:icon",
"description": "service",
"type": "service",
"visible": true, <============== true为导出,false为不导出
"srcEntrance": "./ets/ServiceExtAbility/ServiceExtAbility.ts"
}
]
不同于UIAbility,在调用方不带有abilityName
以及bundleName
的情况下,是不允许通过隐式want启动应用的 ServiceExtensionAbility。调用方传入的want参数中带有bundleName
则允许使用startServiceExtensionAbility
方法隐式 want 启动 ServiceExtensionAbility,且当前仅允许系统应用才能定义ServiceExtensionAbility,且只有系统应用才能调用startServiceExtensionAbility
,这些情况下,隐式或显式启动ServiceExtensionAbility的风险,如携带或者返回敏感数据/服务劫持等场景,条件苛刻,需要开发者根据具体业务场景具体评估风险。
对外暴露的接口onRemoteRequest
/onCreate
/onRequest
/onConnect
/onDisconnect
均会接收外部传入的参数want
,这里就存在着老生常谈的问题,对于传入的参数是否存在:
url
并用于webview加载,那么需要严格的白名单校验,详细攻防参考我们上一篇文章;StartAbility
等方法拉起参数中指定的Ability
,造成类似 LaunchAnywhere 的漏洞;注:当服务连接成功后,会在abilityConnections_
添加一条连接记录,作为ability
运行时数据保存在进程foundation
中。当有再次连接时,会在abilityConnections_
中搜索连接信息,找到后将返回该IRemoteObject
到js端。connectAbility
的代码时序图我们以分布式的连接过程为例如下(非分布式的时序图参考后文的IPC通信流程)
部分开发者需要使用ServiceExtension提供一些较为敏感的服务,因此需要对客户端身份进行校验,开发者可在IDL接口的stub端进行校验,IDL接口实现详见定义IDL接口(https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/application-models/serviceextensionability.md#%E5%AE%9A%E4%B9%89idl%E6%8E%A5%E5%8F%A3)。常见的对调用方身份校验的方法是校验包名,这里注意包名一定不要通过want参数的形式进行传递,比如:在want中添加parameters的参数pkgName将包名信息传递给服务端,服务端从want中解析出parameters参数pkgName包名进行身份校验,那么这种校验形同虚设,因为want的内容是客户端完全可控的,因此作为服务端,在获取客户端身份的时候,需要使用一个客户端不可控的方法来获取。此处推荐两种通过系统API的校验方式:
通过调用 getCallingUid()
接口获取客户端的uid,再调用getBundleNameByUid()
接口获取uid对应的bundleName
,从而识别客户端身份。此处需要注意的是getBundleNameByUid()
是一个异步接口,因此服务端无法将校验结果返回给客户端,这种校验方式适合客户端向服务端发起执行异步任务请求的场景,示例代码如下:
import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
import bundleManager from '@ohos.bundle.bundleManager';
import IdlServiceExtStub from './idl_service_ext_stub';
import Logger from '../utils/Logger';
import rpc from '@ohos.rpc';
import type { BusinessError } from '@ohos.base';
import type { insertDataToMapCallback } from './i_idl_service_ext';
import type { processDataCallback } from './i_idl_service_ext';
const ERR_OK = 0;
const ERR_DENY = -1;
const TAG: string = "[IdlServiceExtImpl]";
export default class ServiceExtImpl extends IdlServiceExtStub {
processData(data: number, callback: processDataCallback): void {
Logger.info(TAG, `processData: ${data}`);
let callerUid = rpc.IPCSkeleton.getCallingUid();
bundleManager.getBundleNameByUid(callerUid).then((callerBundleName) => {
Logger.info(TAG, 'getBundleNameByUid: ' + callerBundleName);
// 对客户端包名进行识别
if (callerBundleName !== 'com.samples.stagemodelabilitydevelop') { // 识别不通过
Logger.info(TAG, 'The caller bundle is not in trustlist, reject');
return;
}
// 识别通过,执行正常业务逻辑
}).catch((err: BusinessError) => {
Logger.info(TAG, 'getBundleNameByUid failed: ' + err.message);
});
}
insertDataToMap(key: string, val: number, callback: insertDataToMapCallback): void {
// 开发者自行实现业务逻辑
Logger.info(TAG, `insertDataToMap, key: ${key} val: ${val}`);
callback(ERR_OK);
}
}
我们简要分析以下这两个接口的底层实现,看一下服务端如何获取对端的uid以及包名
napi_value NAPI_getCallingUid(napi_env env, napi_callback_info info)
{
napi_value global = nullptr;
napi_get_global(env, &global);
napi_value napiActiveStatus = nullptr;
napi_get_named_property(env, global, "activeStatus_", &napiActiveStatus);
if (napiActiveStatus != nullptr) {
int32_t activeStatus = IRemoteInvoker::IDLE_INVOKER;
napi_get_value_int32(env, napiActiveStatus, &activeStatus);
if (activeStatus == IRemoteInvoker::ACTIVE_INVOKER) {
napi_value callingUid = nullptr;
napi_get_named_property(env, global, "callingUid_", &callingUid);
return callingUid;
}
}
uint32_t uid = getuid();
napi_value result = nullptr;
napi_create_int32(env, static_cast<int32_t>(uid), &result);
return result;
}
继续会调用 BinderInvoker::GetCallerUid,在pid == callerPid_ && pid != invokerInfo_.pid
的情况下即调用者和接收者非同一进程的情况下返回invokerInfo_
的uid信息
uid_t BinderInvoker::GetCallerUid() const
{
auto pid = getpid();
if (pid == callerPid_ && pid != invokerInfo_.pid) {
return invokerInfo_.uid;
}
return callerUid_;
}
为了理解如何获取调用者的相关进程信息,我们需要对整个IPC以及binder的通信流程有个大致的了解,数据流图如下callerPid_
和invokerInfo_
会在BinderInvoker
初始化的时候设置为getuid()
的信息,但是在整个IPC的通信过程中,会在服务端处理binder
上来数据的过程中设置为发送端的信息,相关代码如下
void BinderInvoker::OnTransaction(const uint8_t *buffer)
{
const binder_transaction_data *tr = reinterpret_cast<const binder_transaction_data *>(buffer);
auto binderAllocator = new (std::nothrow) BinderAllocator();
if (binderAllocator == nullptr) {
ZLOGE(LABEL, "BinderAllocator Creation failed");
return;
}
auto data = std::make_unique<MessageParcel>(binderAllocator);
data->ParseFrom(tr->data.ptr.buffer, tr->data_size);
if (tr->offsets_size > 0) {
data->InjectOffsets(tr->data.ptr.offsets, tr->offsets_size / sizeof(binder_size_t));
}
uint32_t &newflags = const_cast<uint32_t &>(tr->flags);
int isServerTraced = HitraceInvoker::TraceServerReceieve(static_cast<uint64_t>(tr->target.handle),
tr->code, *data, newflags);
const pid_t oldPid = callerPid_;
const pid_t oldRealPid = callerRealPid_;
const auto oldUid = static_cast<const uid_t>(callerUid_);
const uint64_t oldToken = callerTokenID_;
const uint64_t oldFirstToken = firstTokenID_;
uint32_t oldStatus = status_;
callerPid_ = tr->sender_pid;
callerUid_ = tr->sender_euid;
binder_transaction_data
数据结构同时被用户空间和binder内空间使用,在调用端向binder
发起BC_TRANSACTION
请求时,最终会走到内核中的binder_transaction
进行处理,相关代码如下,proc
为binder_proc
对象指向Binder实体对象的宿主进程
static void binder_transaction(struct binder_proc *proc,
struct binder_thread *thread,
struct binder_transaction_data *tr, int reply,
binder_size_t extra_buffers_size)
{
//// ...
#ifdef CONFIG_BINDER_TRANSACTION_PROC_BRIEF
t->async_from_pid = thread->proc->pid;
t->async_from_tid = thread->pid;
#endif
}
t->sender_euid = task_euid(proc->tsk); // 从进程的cred中获取并设置调用端的euid
#ifdef CONFIG_ACCESS_TOKENID
t->sender_tokenid = current->token;
t->first_tokenid = current->ftoken;
#endif /* CONFIG_ACCESS_TOKENID */
//// ...
}
同时在NAPIRemoteObject::OnRemoteRequest
设置activeStatus_
为IRemoteInvoker::ACTIVE_INVOKER
。
向foundation
进程加载运行的bundle manager service发送GET_NAME_FOR_UID
请求,最终调用BundleMgrHostImpl::GetNameForUid
处理,代码如下
ErrCode BundleMgrHostImpl::GetNameForUid(const int uid, std::string &name)
{
APP_LOGD("start GetNameForUid, uid : %{public}d", uid);
if (!BundlePermissionMgr::IsSystemApp() &&
!BundlePermissionMgr::VerifyCallingBundleSdkVersion(Constants::API_VERSION_NINE)) {
APP_LOGE("non-system app calling system api");
return ERR_BUNDLE_MANAGER_SYSTEM_API_DENIED;
}
if (!BundlePermissionMgr::VerifyCallingPermissionsForAll({Constants::PERMISSION_GET_BUNDLE_INFO_PRIVILEGED,
Constants::PERMISSION_GET_BUNDLE_INFO})) {
APP_LOGE("verify query permission failed");
return ERR_BUNDLE_MANAGER_PERMISSION_DENIED;
}
auto dataMgr = GetDataMgrFromService();
if (dataMgr == nullptr) {
APP_LOGE("DataMgr is nullptr");
return ERR_BUNDLE_MANAGER_INTERNAL_ERROR;
}
auto ret = dataMgr->GetNameForUid(uid, name);
if (ret != ERR_OK && isBrokerServiceExisted_) {
auto bmsExtensionClient = std::make_shared<BmsExtensionClient>();
ret = bmsExtensionClient->GetBundleNameByUid(uid, name);
if (ret != ERR_OK) {
return ERR_BUNDLE_MANAGER_INVALID_UID;
}
}
return ret;
}
从实现我们可以看到调用getBundleNameByUid
需要系统APP的权限,IsSystemApp
获取应用的tokenId判断是否为系统应用,tokenId为应用安装时系统生成随机数加一些标志信息,其中包括是否为系统应用的掩码。继续调用GetBundleNameByUid
,最终走到GetInnerBundleInfoByUid
,并从bundleIdMap_
中根据uid
获取innerBundleInfo
,innerBunderInfo
在应用安装时被设置,最后得到bundleName
。结合i的权限校验方式大致知道获取的uid
是否可信以及uid
对应的bundlename
能否伪造是问题的关键。通过走读了 getCallingUid 和 GetBundleNameByUid 的实现,我们知道uid
在IPC
通信过程中,由binder
负责维护, 并且是获取了调用方的euid
属性。这里我们是没办法伪造的。再看bundlename
,在安装了应用后,会调用BundleDataMgr::AddInnerBundleInfo
,在添加信息时会对bundlename
进行校验,代码如下
bool BundleDataMgr::AddInnerBundleInfo(const std::string &bundleName, InnerBundleInfo &info)
{
APP_LOGD("to save info:%{public}s", info.GetBundleName().c_str());
if (bundleName.empty()) {
APP_LOGW("save info fail, empty bundle name");
return false;
}
std::unique_lock<std::shared_mutex> lock(bundleInfoMutex_);
auto infoItem = bundleInfos_.find(bundleName);
if (infoItem != bundleInfos_.end()) {
APP_LOGW("bundleName: %{public}s : bundle info already exist", bundleName.c_str());
return false;
}
通过如上分析,从恶意应用的角度去欺骗service
端获取伪造信息的可能性就不存在了。
通过调用getCallingTokenId()
接口获取客户端的tokenID
,再调用verifyAccessTokenSync()
接口判断客户端是否有某个具体权限,由于当前不支持自定义权限,因此只能校验当前系统所定义的权限。示例代码如下:
import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
import bundleManager from '@ohos.bundle.bundleManager';
import IdlServiceExtStub from './idl_service_ext_stub';
import Logger from '../utils/Logger';
import rpc from '@ohos.rpc';
import type { BusinessError } from '@ohos.base';
import type { insertDataToMapCallback } from './i_idl_service_ext';
import type { processDataCallback } from './i_idl_service_ext';
const ERR_OK = 0;
const ERR_DENY = -1;
const TAG: string = "[IdlServiceExtImpl]";
export default class ServiceExtImpl extends IdlServiceExtStub {
processData(data: number, callback: processDataCallback): void {
console.info(TAG, `processData: ${data}`);
let callerTokenId = rpc.IPCSkeleton.getCallingTokenId();
let accessManger = abilityAccessCtrl.createAtManager();
// 所校验的具体权限由开发者自行选择,此处ohos.permission.GET_BUNDLE_INFO_PRIVILEGED只作为示例
let grantStatus = accessManger.verifyAccessTokenSync(callerTokenId, 'ohos.permission.GET_BUNDLE_INFO_PRIVILEGED');
if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_DENIED) {
Logger.info(TAG, `PERMISSION_DENIED`);
callback(ERR_DENY, data); // 鉴权失败,返回错误
return;
}
Logger.info(TAG, 'verify access token success.');
callback(ERR_OK, data + 1); // 鉴权通过,执行正常业务逻辑
};
insertDataToMap(key: string, val: number, callback: insertDataToMapCallback): void {
// 开发者自行实现业务逻辑
Logger.info(TAG, `insertDataToMap, key: ${key} val: ${val}`);
callback(ERR_OK);
}
}
同样我们看下verifyAccessTokenSync
的实现
最终会调用PermissionPolicySet::VerifyPermissionStatus
,代码如下,根据callerTokenId
获取目标进程的权限授予状态集合并进行权限校验,通过callerTokenId获取调用者的授权状态集合,最后通过permissionName进行校验,校验如下
int PermissionPolicySet::VerifyPermissionStatus(const std::string& permissionName)
{
Utils::UniqueReadGuard<Utils::RWLock> infoGuard(this->permPolicySetLock_);
for (const auto& perm : permStateList_) {
if (perm.permissionName != permissionName) {
continue;
}
if (!perm.isGeneral) {
ACCESSTOKEN_LOG_ERROR(LABEL, "tokenID: %{public}d, permission: %{public}s is not general",
tokenId_, permissionName.c_str());
return PERMISSION_DENIED;
}
if (IsPermGrantedBySecComp(perm.grantFlags[0])) {
ACCESSTOKEN_LOG_INFO(LABEL, "tokenID: %{public}d, permission is granted by seccomp", tokenId_);
return PERMISSION_GRANTED;
}
if (perm.grantStatus[0] != PERMISSION_GRANTED) {
ACCESSTOKEN_LOG_ERROR(LABEL, "tokenID: %{public}d, permission: %{public}s is not granted",
tokenId_, permissionName.c_str());
return PERMISSION_DENIED;
}
return PERMISSION_GRANTED;
}
// check if undeclared permission is granted by security component.
if (std::any_of(secCompGrantedPermList_.begin(), secCompGrantedPermList_.end(),
[permissionName](const auto& permission) { return permission == permissionName; })) {
return PERMISSION_GRANTED;
}
ACCESSTOKEN_LOG_DEBUG(LABEL, "tokenID: %{public}d, permission: %{public}s is undeclared",
tokenId_, permissionName.c_str());
return PERMISSION_DENIED;
}
这篇文章我们从开发者的角度分析了ServiceExtensionAbility的实现,以及实现过程中可能产生的安全风险。对于ServiceExtensionAbility应用目前来看需要应用标记为系统应用才可以安装运行,这对于开发来说存在一定的门槛,对于三方应用使用ServiceExtensionAbility提供的服务则无此门槛,所以从安全人员的角度也是可以从文中所述的角度发现ServiceExtensionAbility应用存在的安全风险。