长亭百川云 - 文章详情

纯鸿蒙应用安全开发指南-ServiceExtensionAbility

银针安全

112

2024-07-13

一. 概述

在本系列的前两篇文章《安全初探》《Web组件安全》中(见公众号),我们了解了UIAbility/PageAbility以及WebView组件的安全风险。今天我们继续介绍在鸿蒙中的后台服务 ServiceExtensionAbility 及其开发中需要注意的安全风险。ServiceExtensionAbility主要用于后台运行的不提供用户交互界面的服务。目前,ServiceExtensionAbility 能力的使用需要应用标记为系统应用,三方应用即非系统应用需要使用后台任务。

随着系统的演进发展,HarmonyOS先后提供了两种应用模型:

  • FA(Feature Ability)模型:HarmonyOS早期版本开始支持的模型,已经不再主推。

  • Stage模型:HarmonyOS 3.1 Developer Preview版本开始新增的模型,是目前主推且会长期演进的模型。在该模型中,由于提供了AbilityStage、WindowStage等类作为应用组件和Window窗口的“舞台”,因此称这种应用模型为Stage模型。

Stage模型的组件分类包括:

  • UIAbility组件,包含UI界面,提供展示UI的能力,主要用于和用户交互。

  • ExtensionAbility组件:提供特定场景(如卡片、输入法)的扩展能力,满足更多的使用场景。

image.png

ServiceExtensionAbility是SERVICE类型的ExtensionAbility组件,提供后台服务能力,其内部持有了一个ServiceExtensionContext,通过ServiceExtensionContext提供了丰富的接口供外部使用。ServiceExtensionAbility可以被其他组件启动或连接,并根据调用者的请求信息在后台处理相关事务。ServiceExtensionAbility支持以启动和连接两种形式运行,系统应用可以调用startServiceExtensionAbility()方法启动后台服务,也可以调用connectServiceExtensionAbility()方法连接后台服务,而三方应用只能调用connectServiceExtensionAbility()方法连接后台服务。启动和连接后台服务的差别:

  • 启动:AbilityA启动ServiceB,启动后AbilityA和ServiceB为弱关联,AbilityA退出后,ServiceB可以继续存在。

  • 连接:AbilityA连接ServiceB,连接后AbilityA和ServiceB为强关联,AbilityA退出后,ServiceB也一起退出。

此处有如下细节需要注意:

  • 若Service只通过connect的方式被拉起,那么该Service的生命周期将受客户端控制,当客户端调用一次connectServiceExtensionAbility()方法,将建立一个连接,当客户端退出或者调用disconnectServiceExtensionAbility()方法,该连接将断开。当所有连接都断开后,Service将自动退出。

  • Service一旦通过start的方式被拉起,将不会自动退出,系统应用可以调用stopServiceExtensionAbility()方法将Service退出。

  • 只能在主线程线程中执行connect/disconnect操作,不要在Worker、TaskPool等子线程中执行connect/disconnect操作。

**说明:**当前不支持三方应用实现ServiceExtensionAbility,笔者猜测应该是借鉴Android的经验,避免服务后台一直运行。如果三方开发者想要实现后台处理相关事务的功能,可以使用后台任务,具体请参见后台任务。三方应用的UIAbility组件可以通过Context连接系统提供的ServiceExtensionAbility。出于安全隐私考虑,三方应用需要在前台获焦的情况下才能连接系统提供的ServiceExtensionAbility,避免service长期在后台监听。

二. ServiceExtensionAbility生命周期

ServiceExtensionAbility提供了onCreate()、onRequest()、onConnect()、onDisconnect()和onDestroy()生命周期回调,根据需要重写对应的回调方法。下图展示了ServiceExtensionAbility的生命周期

  • onCreate 服务被首次创建时触发该回调,开发者可以在此进行一些初始化的操作,例如注册公共事件监听等。说明:如果服务已创建,再次启动该ServiceExtensionAbility不会触发onCreate()回调。

  • onRequest 当另一个组件调用startServiceExtensionAbility()方法启动该服务组件时,触发该回调。执行此方法后,服务会启动并在后台运行。每调用一次startServiceExtensionAbility()方法均会触发该回调。

  • onConnect 当另一个组件调用connectServiceExtensionAbility()方法与该服务连接时,触发该回调。开发者在此方法中,返回一个远端代理对象(IRemoteObject),客户端拿到这个对象后可以通过这个对象与服务端进行RPC通信,同时系统侧也会将该远端代理对象(IRemoteObject)储存。后续若有组件再调用connectServiceExtensionAbility()方法,系统侧会直接将所保存的远端代理对象(IRemoteObject)返回,而不再触发该回调。

  • onDisconnect 当最后一个连接断开时,将触发该回调。客户端死亡或者调用disconnectServiceExtensionAbility()方法可以使连接断开。

  • onDestroy 当不再使用服务且准备将其销毁该实例时,触发该回调。开发者可以在该回调中清理资源,如注销监听等。

三. 实现一个后台服务

编译安装官方DEMO

在开始之前,我们需要了解如下准备工作,因为只有系统应用才允许实现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架构;二是特权应用未将签名指纹配置到设备的特权管控白名单。分别对应两个解决方案:

  1. 连接设备执行命令查看支持的ABI列表
` 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"],`    `},`

修改完成后重新编译安装。

  1. 问题是由于应用使用了应用特权,但应用的签名文件发生变化后未将新的签名指纹重新配置到设备的特权管控白名单文件install_list_capability.json中,需要将应用的指纹利用设备root权限添加到设备的信任指纹列表。所以这里是普通开发人员没办法做到的。这里我们通过如下步骤添加:

    a. 获取新的签名指纹

i.  在项目级build-profile.json5文件中,signingConfigs字段内的profile的值即为签名文件的存储路径。 

ii.  打开该签名文件(后缀为.p7b),打开后在文件内搜索“development-certificate”,将“-----BEGIN CERTIFICATE-----”和“-----END CERTIFICATE-----”以及中间的信息拷贝到新的文本中,注意换行并去掉换行符,保存为一个新的.cer文件,如命名为xxxx.cer,新的.cer文件格式如下图

iii. 使用keytool工具(在DevEco Studio安装目录下的jbr/bin文件夹内),执行如下命令通过.cer文件获取证书指纹的SHA256值,去掉冒号后的签名指纹为:`D60047C391C5E3CFE1B617671FF67135A83AEA87F4F1CD31D4C797FC54153EBD`

` 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,这里就存在着老生常谈的问题,对于传入的参数是否存在:

  1. 逻辑问题,如参数伪造造成进一步的逻辑问题等;

  2. 使用传入参数的前对其合法性校验,如空指针校验/路径穿越等;

  3. 若从参数中获取url并用于webview加载,那么需要严格的白名单校验,详细攻防参考我们上一篇文章;

  4. 使用StartAbility等方法拉起参数中指定的Ability,造成类似 LaunchAnywhere 的漏洞;

  5. 接口使用不准确,如下回调的回调时机以及回调的次数需要掌握透彻,防止错误使用产生问题:

  • onCreate 服务被首次创建时触发该回调,开发者可以在此进行一些初始化的操作,例如注册公共事件监听等。

  • onRequest 当另一个组件调用startServiceExtensionAbility()方法启动该服务组件时,触发该回调。执行此方法后,服务会启动并在后台运行。每调用一次startServiceExtensionAbility()方法均会触发该回调。

  • onConnect 当另一个组件调用connectServiceExtensionAbility()方法与该服务连接时,触发该回调。开发者在此方法中,返回一个远端代理对象(IRemoteObject),客户端拿到这个对象后可以通过这个对象与服务端进行RPC通信,同时系统侧也会将该远端代理对象(IRemoteObject)储存。后续若有组件再调用connectServiceExtensionAbility()方法,系统侧会直接将所保存的远端代理对象(IRemoteObject)返回,而不再触发该回调。

  • onDisconnect 当最后一个连接断开时,将触发该回调。客户端死亡或者调用disconnectServiceExtensionAbility()方法可以使连接断开。

  • onDestroy 当不再使用服务且准备将其销毁该实例时,触发该回调。开发者可以在该回调中清理资源,如注销监听等。

  1. 等等

注:当服务连接成功后,会在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的校验方式:

通过callerUid识别客户端应用

通过调用 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以及包名

getCallingUid
`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进行处理,相关代码如下,procbinder_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

getBundleNameByUid

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获取innerBundleInfoinnerBunderInfo在应用安装时被设置,最后得到bundleName。结合i的权限校验方式大致知道获取的uid是否可信以及uid对应的bundlename能否伪造是问题的关键。通过走读了 getCallingUid 和 GetBundleNameByUid 的实现,我们知道uidIPC通信过程中,由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端获取伪造信息的可能性就不存在了。

通过callerTokenId对客户端进行鉴权

通过调用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的实现

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应用存在的安全风险。

五. 参考

  1. https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/application-models/serviceextensionability.md

  2. https://gitee.com/openharmony/app\_samples/tree/master/ability/ServiceExtAbility

  3. https://developer.huawei.com/consumer/cn/forum/topic/0203143504490236581

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2