最近华为发布会官宣将在下一代移除对安卓的支持也是引发了不少的关注,也不断有消息称各大top应用厂商将推出纯鸿蒙版的应用。纯鸿蒙作为一个全新、多设备、多场景的操作系统,为开发者们提供了一个全新的平台,能够构建适用于各种设备的应用程序,从智能手机到家居设备,再到车载系统等,随着不断的发展,其安全问题也会随之而来。在这篇文章中,我们将以开源的Openharmony来探讨一些纯鸿蒙应用的基础知识,从安全的角度来分享一些纯鸿蒙应用可能存在的问题,当然我也是仍在学习中的初学者,希望通过以后的学习记录能给纯鸿蒙感兴趣的开发者或者安全爱好者传达一些安全的了解以及开发上需要注意的地方,希望能给大家带来一点帮助 : )
本文将从Openharmony源码编译到编写运行第一个纯鸿蒙应用,介绍应用间的数据交互及参数传递的载体,最后以一个null want的例子来介绍开发中可能存在的风险。笔者也是纯鸿蒙的初学者,文章中如有错误还请不吝指正,如有笔者未考虑到的风险面也请指出,后续加强补充共同学习:)
# 初始化master分支源码仓
repo init -u https://gitee.com/openharmony/manifest.git -b master --no-repo-verify
# 同步源码
repo sync -c
repo forall -c 'git lfs pull'
# 配置编译环境
./build/build_scripts/env_setup.sh
# 安装依赖
bash build/prebuilts_download.sh
# 准备工作完成后开始编译源码。这里有两种方式:
# 1. 使用Openharmony提供的工具hb进行编译,提供交互式编译配置
# 执行hb set设置编译的平台
root@ubuntu ~/Huawei/aMaster hb set
OHOS Which os_level do you need? (Use arrow keys)
mini
small
❯ standard
# 选择标准系统后,选择具体的开发板类型,这里选择rk3568
OHOS Which os_level do you need? standard
OHOS Which product do you need? (Use arrow keys)
unionman
unionpi_tiger
ohemu
qemu-arm-linux-min
qemu-arm64-linux-min
qemu-x86_64-linux-min
qemu-arm-linux-headless
hihope
❯ rk3568
rk3568_mini_system
dayu210
ipcamera
kaihong
khdvk_3566b
isoftstone
zhiyuan
yangfan
osware
imx8mm
hisilicon
hispark_taurus_standard
hispark_phoenix
watchos
built-in
system_arm_default
ohos-sdk
system_arm64_default
# 最后执行hb build即可开始编译,可能会用到的一些参数hb build -h
...
-b BUILD_TYPE, --build_type BUILD_TYPE
-f, --full full code compilation
-n, --ndk compile ndk
--gn-args GN_ARGS specifies gn build arguments, eg: --gn-args="foo="bar" enable=true blah=7"
--fast-rebuild it will skip prepare, preloader, gn_gen steps so we can enable it only when there is no change for gn related script
...
# 第二种可以通过build.sh编译
./build.sh --product-name rk3568 -f
# 生成的文件
➜ images pwd
~/out/rk3568/packages/phone/images
➜ images ll
total 755M
-rw-r--r-- 1 root root 64M Jul 27 09:24 boot_linux.img
-rw-r--r-- 1 root root 50M Jul 27 10:21 chip_prod.img
-rwxr-xr-x 1 root root 8.4K Jul 27 09:24 config.cfg
-rw-r--r-- 1 root root 12M Jul 27 10:21 eng_system.img
-rw-r--r-- 1 root root 445K Jul 27 09:24 MiniLoaderAll.bin
-rwxr-xr-x 1 root root 756 Jul 27 09:24 parameter.txt
-rw-rw-r-- 1 root root 2.3M Jul 27 10:21 ramdisk.img
-rw-r--r-- 1 root root 5.4M Jul 27 09:24 resource.img
-rw-r--r-- 1 root root 50M Jul 27 10:21 sys_prod.img
-rw-r--r-- 1 root root 1.5G Jul 27 10:21 system.img
-rwxr-xr-x 1 root root 4.0M Jul 27 09:24 uboot.img
-rw-rw-r-- 1 root root 15M Jul 27 10:21 updater.img
-rw-r--r-- 1 root root 1.4G Jul 27 10:21 userdata.img
-rw-r--r-- 1 root root 256M Jul 27 10:21 vendor.img
最后保持生成镜像文件的默认配置烧录到开发板即可
开机
OpenHarmony提供了一套UI开发框架,即方舟开发框架(ArkUI框架)。方舟开发框架可为开发者提供应用UI开发所必需的能力,比如多种组件、布局计算、动画能力、UI交互、绘制等。下面尝试在烧录的系统上编写第一个AtkTS应用。首先参考官方文档(https://gitee.com/openharmony/docs/blob/master/zh-cn/device-dev/quick-start/quickstart-ide-env-win.md)安装IDE环境。然后新建项目,选择 "Empty Ability"
在保持默认生成的情况下可以直接在开发板上安装运行
至此成功编译运行了第一个鸿蒙应用。回头看一下这个工程的目录接口
AppScope > app.json5:应用的全局配置信息。如:应用包名、版本号、应用图标、应用名称和依赖的SDK版本号等。
entry:OpenHarmony工程模块,编译构建生成一个HAP包。
src > main > ets:用于存放ArkTS源码。
src > main > ets > entryability:应用/服务的入口。
src > main > ets > pages:应用/服务包含的页面。
src > main > resources:用于存放应用/服务所用到的资源文件,如图形、多媒体、字符串、布局文件等。
src > main > module.json5:模块配置文件。主要包含HAP包的配置信息、应用/服务在具体设备上的配置信息以及应用/服务的全局配置信息。
build-profile.json5:当前的模块信息 、编译信息配置项,包括buildOption、targets配置等。
hvigorfile.ts:模块级编译构建任务脚本,开发者可以自定义相关任务和代码实现。
oh_modules:用于存放三方库依赖信息。
build-profile.json5:应用级配置信息,包括签名signingConfigs、产品配置products等。
hvigorfile.ts:应用级编译构建任务脚本。
上述工程中的 UI
和功能代码都在 EntryAbility.ts
和 Index.etx
中。编译生成的文件为 hap
文件,同样可以使用压缩软件解压它
开发态和打包后的结构对比(图摘自developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/multi-hap-build-view-0000001427744536-V2)
其中pack.info描述了当前 hap
的属性,由IDE编译生成。module.json如下,由工程中的 app.json5
和 module.json5
合成
{
"app": {
"apiReleaseType": "Release",
"bundleName": "com.example.helloworld",
"debug": true,
"icon": "$media:app_icon",
"iconId": 16777217,
"label": "$string:app_name",
"labelId": 16777216,
"minAPIVersion": 9,
"targetAPIVersion": 9,
"vendor": "example",
"versionCode": 1000000,
"versionName": "1.0.0"
},
"module": {
"abilities": [
{
"description": "$string:EntryAbility_desc",
"descriptionId": 16777218,
"exported": true,
"icon": "$media:icon",
"iconId": 16777222,
"label": "$string:EntryAbility_label",
"labelId": 16777219,
"name": "EntryAbility",
"skills": [
{
"actions": [
"action.system.home"
],
"entities": [
"entity.system.home"
]
}
],
"srcEntry": "./ets/entryability/EntryAbility.ts",
"startWindowBackground": "$color:start_window_background",
"startWindowBackgroundId": 16777221,
"startWindowIcon": "$media:icon",
"startWindowIconId": 16777222
}
],
"compileMode": "esmodule",
"deliveryWithInstall": true,
"dependencies": [],
"description": "$string:module_desc",
"descriptionId": 16777220,
"deviceTypes": [
"phone"
],
"installationFree": false,
"mainElement": "EntryAbility",
"name": "entry",
"pages": "$profile:main_pages",
"type": "entry",
"virtualMachine": "ark9.0.0.0"
}
}
从这个配置文件中可以得到许多应用相关的配置信息,挑一些介绍一下
"mainElement" : 标识hap的入口ability名称或者extension名称
"abilities" : 是一个数组,存放当前模块中所有的ability元能力的配置信息,其中可以有多个ability。Ability是应用所具备能力的抽象。(后文介绍)
"srcEntry" : 当前模块的入口文件路径
"deviceTypes" : 该标签标识hap可以运行在哪类设备上,标签值采用字符串数组的表示。
"exported" : ability是否可以被其他应用程序调用,true表示可以被其它应用调用, false表示不可以被其它应用调用
"skills" : 标识能够接收的action值的集合,取值通常为系统预定义的action值,也允许自定义
"entities" : 标识能够接收的Want的Action值的集合,取值通常为系统预定义的action值,也允许自定义
"actions" : 标识能够接收Want的Entity值的集合
安全规范相关的我们更关注如下的配置信息
"debug" : 标示应用是否可调试
"supportBackup" : 标识应用是否支持备份和恢复
"cleartextTraffic" : 是否允许使用明文网络流量
"cleartextPermitted" : 是否允许使用明文网络流量,与上述标签同在时,以此为准
"abilities" :
"exported" : ability是否可以被其他应用程序调用,true表示可以被其它应用调用, false表示不可以被其它应用调用
"skills" : 标识能够接收的action值的集合,取值通常为系统预定义的action值,也允许自定义
"entities" : 标识能够接收的Want的Action值的集合,取值通常为系统预定义的action值,也允许自定义
"actions" : 标识能够接收Want的Entity值的集合
程序代码被编译在 .abc
文件中,是二进制数据内容,可以使用 out/rk3568/clang_x64/arkcompiler/runtime_core/ark_disasm
将 abc
文件反编译成文本格式的方舟字节码文件(ark_disasm需要通过源码自行编译)。
上文多次提及的ability,这里简单介绍一些。Ability是应用所具备的能力的抽象,一个应用可以包含一个或多个Ability。Ability分为两种类型:FA(Feature Ability)和PA(Particle Ability)。FA/PA是应用的基本组成单元,能够实现特定的业务功能。FA有UI界面,而PA无UI界面。Page模板是FA唯一支持的模板,用于提供与用户交互的能力。我们可以简单理解为一个应用的每一个页面都对应一个 page ability
。在上文我们的第一个鸿蒙应用中的index.ets即对应了UI界面,可以看到只是打印了"Hello World",我们在页面中添加一个按钮
@Entry
@Component
struct Index {
@State message: string = 'Hello World'
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
// 添加按钮,以响应用户点击
Button() {
Text('Button')
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
.type(ButtonType.Capsule)
.margin({
top: 20
})
.backgroundColor('#0D9FFB')
.width('40%')
.height('5%')
}
.width('100%')
}
.height('100%')
}
}
效果如下
EntryAbility.ts的实现如下
import UIAbility from '@ohos.app.ability.UIAbility';
import hilog from '@ohos.hilog';
import window from '@ohos.window';
export default class EntryAbility extends UIAbility {
onCreate(want, launchParam) {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
}
onDestroy() {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage) {
// Main window is created, set main page for this ability
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err, data) => {
if (err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
});
}
onWindowStageDestroy() {
// Main window is destroyed, release UI related resources
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}
onForeground() {
// Ability has brought to foreground
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
}
onBackground() {
// Ability has back to background
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
}
}
从上面的代码中我们看到 EntryAbility
继承自 UIAbility
,当用户打开、切换和返回到对应应用时,应用中的UIAbility实例会在其生命周期的不同状态之间转换。UIAbility类提供了一系列回调,通过这些回调可以知道当前UIAbility实例的某个状态发生改变,会经过UIAbility实例的创建和销毁,或者UIAbility实例发生了前后台的状态切换。UIAbility的生命周期包括Create、Foreground、Background、Destroy四个状态,如下图
除了上图中的几个生命周期事件,我们在EntryAbility.ts中还看到了onWindowStageCreate回调,onWindowStageCreate在创建window stage时被调用,可以通过window.WindowStage的接口执行页面加载等操作。
至此我们浅入浅出了鸿蒙的图形应用开发,下面如果我们想要在两个应用间完成调用交互,还需要理解一下鸿蒙中的新的名字 Want
。Want
是对象间信息传递的载体,可以用于应用组件间的信息传递。Want的使用场景之一是作为startAbility的参数,其包含了指定的启动目标,以及启动时需携带的相关数据,如bundleName和abilityName字段分别指明目标Ability所在应用的包名以及对应包内的Ability名称。当AbilityA启动AbilityB并需要传入一些数据给AbilityB时,Want可以作为一个数据载体将数据传给AbilityB。
显式Want : 在启动Ability时指定了abilityName和bundleName的Want称为显式Want。
当有明确处理请求的对象时,通过提供目标Ability所在应用的包名信息(bundleName),并在Want内指定abilityName便可启动目标Ability。显式Want通常用于在当前应用开发中启动某个已知的Ability。
let wantInfo = {
deviceId: '', // deviceId为空表示本设备
bundleName: 'com.example.myapplication',
abilityName: 'FuncAbility',
}
隐式Want :在启动Ability时未指定abilityName的Want称为隐式Want。
当请求处理的对象不明确时,希望在当前应用中使用其他应用提供的某个能力(通过skills标签定义),而不关心提供该能力的具体应用,可以使用隐式Want。例如使用隐式Want描述需要打开一个链接的请求,而不关心通过具体哪个应用打开,系统将匹配声明支持该请求的所有应用。
let wantInfo = {
// uncomment line below if wish to implicitly query only in the specific bundle.
// bundleName: 'com.example.myapplication',
action: 'ohos.want.action.search',
// entities can be omitted
entities: [ 'entity.system.browsable' ],
uri: 'https://www.test.com:8080/query/student',
type: 'text/plain',
};
关于 Want
更详细的参数请参考 https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/want-overview-0000001478340877-V3
了解了上文中的知识后,我们创建第二个应用
并设置为如下属性,导出的且支持SECONDABILITYSECONDSLICEACTION的action
{
"name": "FuncAbility",
"srcEntry": "./ets/entryability/FuncAbility.ts",
"description": "$string:FuncAbility_desc",
"icon": "$media:icon",
"label": "$string:FuncAbility_label",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"actions": [
"SECOND_ABILITY_SECOND_SLICE_ACTION"
]
}
]
},
继续在其uiability中打印出外部app传入的数据信息
export default class FuncAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
hilog.info(0x0000, 'testTag', '%{public}s', 'FuncAbility onCreate');
hilog.info(0x0000, 'testTag', '%{public}s', want.action);
let infox = want.parameters['info']
hilog.info(0x0000, 'testTag', '%{public}s', infox);
}
非常简单的一个app,现在我们回到上文中的helloworld应用,并为其中的button添加点击事件跳转到第二个app的second page页面
// 添加按钮,以响应用户点击
Button() {
Text('Button')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.onClick(() => {
let context = getContext(this) as common.UIAbilityContext;
let wantInfo = {
deviceId: '', // deviceId为空表示本设备
action: 'SECOND_ABILITY_SECOND_SLICE_ACTION',
moduleName: '', // moduleName非必选
parameters: { // 自定义信息
info: '来自Hello world应用',
},
}
// context为调用方UIAbility的UIAbilityContext
context.startAbility(wantInfo).then(() => {
console.log('testTag', `Succeeded in starting second.`);
}).catch((err) => {
console.error('testTag', `Failed to open start ability. Code is ${err.code}, message is ${err.message}`);
})
})
}
当在helloworld应用中点击按钮,将跳转到第二个应用的second page页面并打印出相应的数据信息
基础的开发知识已经了解了,那么显而易见的问题就是want的校验等。
want中取数据未判空,对获取的数据的处理过程存在未捕获异常等,将会造成应用拒绝访问
业务逻辑问题,比如want中取出url后使用webview加载,白名单校验存在问题,或文件路径校验问题等
隐式启动Ability时,存在被恶意应用劫持的风险,若携带敏感数据则可能导致数据泄露的风险
startAbilityForResult安全问题,若被启动方未进行安全校验则可能泄露敏感信息等
等其他业务逻辑问题。我们继续在上面第二个应用中的uiability中添加一些从want中获取参数,但是未判空进行处理的情况做一个demo演示(关于want空指针的问题,可能导致应用的被恶意应用攻击而无法正常使用)。漏洞代码如下
export default class FuncAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
hilog.info(0x0000, 'testTag', '%{public}s', 'FuncAbility onCreate');
let infox = want.parameters['info']
hilog.info(0x0000, 'testTag', '%{public}s', infox['unexist'])
}
在调用方应用中编写如下代码,注释掉want中的parameters信息
let wantInfo = {
deviceId: '', // deviceId为空表示本设备
action: 'SECOND_ABILITY_SECOND_SLICE_ACTION',
moduleName: '', // moduleName非必选
parameters: { // 自定义信息
//info: '来自Hello world应用',
},
}
不出预料通过startAtivity再次拉起应用时目标程序将会crash,日志信息保存在/data/log/faultlog目录下
关于这里可能存在的安全问题主要还是集中在应用处理外部的want数据,所以这里的空指针我们只要添加一处校验即可
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
hilog.info(0x0000, 'testTag', '%{public}s', 'FuncAbility onCreate');
hilog.info(0x0000, 'testTag', '%{public}s', want.action);
let infox = want.parameters['info']
if( infox === undefined || !infox.hasOwnProperty('unexist') )
{
hilog.info(0x0000, 'testTag', '参数非法');
}else{
hilog.info(0x0000, 'testTag', '%{public}s', infox['unexist']);
}
hilog.info(0x0000, 'testTag', '%{public}s', 'FuncAbility onCreate over');
}
或者,添加try catch都可以起到防护的效果
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
hilog.info(0x0000, 'testTag', '%{public}s', 'FuncAbility onCreate');
hilog.info(0x0000, 'testTag', '%{public}s', want.action);
let infox = want.parameters['info']
try{
hilog.info(0x0000, 'testTag', '%{public}s', infox['unexist']);
}catch(err){
hilog.info(0x0000, 'testTag', '参数非法');
}
hilog.info(0x0000, 'testTag', '%{public}s', 'FuncAbility onCreate over');
}
在这篇文章里,我们介绍了openharmony应用的一些基础知识,并手动编写了一个存在问题的demo进行演示和修补,希望能对开发者和安全人员带来一些开发或者测试上的帮助。笔者也是纯鸿蒙的初学者,文章中如有错误还请不吝指正,如有笔者未考虑到的风险面也请指出,后续会加强补充,共同学习 : )
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/multi-hap-build-view-0000001427744536-V2
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V2/want-overview-0000001478340877-V2
https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/want-overview-0000001478340877-V3