前言
之前分析过 Android 系统中的进程间通信逆向,即基于 Binder 拓展的以及 AIDL 描述的 IPC。了解 Android 系统的话应该知道在 8.0 之后,/dev/binder 拓展多出了两个域,分别是 /dev/hwbinder 和 /dev/vndbinder 。其中 hwbinder 主要用于 HIDL 接口的通信,而 vndbinder 则是专注于 vendor 进程之间的 AIDL 通信。
本文主要关注的是硬件部分。具体来说,就是作为一个 OEM/ODM 厂商,如何将自己的硬件添加到自己的 ROM 之中;以及作为一个安全工程师,如何对厂商的硬件驱动进行(逆向)分析。其实这两个问题的本质是一致的,即要求了解 Android 硬件开发和集成流程。
HAL
HAL 是 Hardware Abstraction Layer 的缩写,即硬件抽象层。从碎片化角度来说,作为系统的设计者,肯定是希望底层硬件按照类型整齐划一,而不是 Boardcom 实现一套、TI、ESP 又自己实现一套自己的 WIFi 接口;从商业角度说,硬件厂商自己硬件的软件(驱动)也是视为传家宝一样不希望被别人分析,所以要求操作系统可以无视自己的底层实现,只需要协商出统一的交互协议。
不论如何,多方交织的结果就是中间多了一层抽象。对于 Android 系统来说,这层抽象就是 HAL,虽然这并不是 Android 独有的概念。简而言之,Android HAL 就是定义了一个 .h 接口,并由硬件厂商拓展实现为动态链接库 .so,并使用约定的方式去加载和调用。
现在的时间已经来到了 Android 11,其实早在 Android 8 之后就已经弃用了曾经的 HAL 方式,不过由于碎片化原因,现在还有许多 IoT 设备等还是使用传统的 HAL 模式。另外出于对历史进展的研究,了解传统 HAL 也是有必要的。
传统 HAL (Legacy HALs) 的接口文件为 hardware/libhardware/include/hardware/hardware.h ,主要定义了三个结构,分别是:
struct hw_module_t;
struct hw_module_methods_t;
struct hw_device_t;
hw_module_t
硬件模块 (hardware module) 表示 HAL 中打包的实现,即输出的.so动态链接库文件。hw_module_t 结构中主要包括 tab、version、name、author 等信息字段以及一个 struct hw_module_methods_t *methods 字段。methods 中包括打开设备的函数指针,如下:
typedef struct hw_module_methods_t {
/** Open a specific device */
int (*open)(const struct hw_module_t* module, const char* id,
struct hw_device_t** device);
} hw_module_methods_t;
每个硬件模块动态库中都需要定义一个符号 HAL_MODULE_INFO_SYM,并且该符号的第一个字段是 hw_module_t 类型。也就是说,厂商可以拓展 hw_module_t 类型,增加自己的额外字段。比如某个摄像头硬件所定义的结构如下:
typedef struct camera_module {
hw_module_t common;
int (*get_number_of_cameras)(void);
int (*get_camera_info)(int camera_id, struct camera_info *info);
} camera_module_t;
这也是使用 C 语言实现继承的一种典型方式。
hw_device_t
device 用于抽象产品的某个具体硬件,比如对于某些摄像头模组,其硬件模块中就可能包括 2D 摄像头、3D 深度摄像头、红外摄像头等具体的 device。设备的结构基本元素如下:
/**
* Every device data structure must begin with hw_device_t
* followed by module specific public methods and attributes.
*/
typedef struct hw_device_t {
/** tag must be initialized to HARDWARE_DEVICE_TAG */
uint32_t tag;
/**
* Version of the module-specific device API. This value is used by
* the derived-module user to manage different device implementations.
*
* The module user is responsible for checking the module_api_version
* and device version fields to ensure that the user is capable of
* communicating with the specific module implementation.
*
* One module can support multiple devices with different versions. This
* can be useful when a device interface changes in an incompatible way
* but it is still necessary to support older implementations at the same
* time. One such example is the Camera 2.0 API.
*
* This field is interpreted by the module user and is ignored by the
* HAL interface itself.
*/
uint32_t version;
/** reference to the module this device belongs to */
struct hw_module_t* module;
/** padding reserved for future use */
#ifdef __LP64__
uint64_t reserved[12];
#else
uint32_t reserved[12];
#endif
/** Close this device */
int (*close)(struct hw_device_t* device);
} hw_device_t;
和模块一样,厂商也是通过继承拓展 device 结构来实现具体的设备。除了上面这些简单的标准属性,其实对于不同种类的硬件,也有特定的数据结构类型,见 Android HAL Reference。例如,对于摄像头类型的硬件,在 hardware/camera.h 中定义了其标准拓展接口和数据类型,比如打开/关闭摄像头、设置参数、数据回调等等。
HIDL
HAL 是最初的硬件抽象方案,在 Android 8 中已经废弃并被 HIDL 取代。HIDL 和 AIDL 类似,都是一种接口描述语言 (HAL interface definition language),用来描述硬件的接口。HIDL 设计的初衷是更新 frameworks 时避免重新编译 HAL,后者可以由厂商单独编译并在 vendor 分区中单独更新,此外还支持完善的版本管理。
开发流程
为了了解开发流程,我现在就是一个厂商的 BSP 工程师。这里假设要创建一个名为 demo 的硬件驱动,并且以华为的 Nexus 6P 为例进行开发。这里不赘述编译 AOSP 的具体过程,只专注于 HIDL 相关部分。
创建 HAL 接口
首先是创建 HAL 硬件抽象描述文件。
mkdir -p hardware/interfaces/demo/1.0/default
touch hardware/interfaces/demo/1.0/IDemo.hal
其中 Demo.hal 内容如下:
package android.hardware.demo@1.0;
interface IDemo {
foo(string name) generates (string result);
bar(int32_t a, int32_t b) generates (int32_t sum);
baz();
};
详细的 HAL 语法见: https://source.android.com/devices/architecture/hidl/code-style
生成实现代码
PACKAGE=android.hardware.demo@1.0
LOC=hardware/interfaces/demo/1.0/default/
# 生成 Demo.h / Demo.cpp 这两个文件为 Server 端实现
hidl-gen -o $LOC -Lc++-impl -randroid.hardware:hardware/interfaces \
-randroid.hidl:system/libhidl/transport $PACKAGE
# 生成 Android.bp 文件
hidl-gen -o $LOC -Landroidbp-impl -randroid.hardware:hardware/interfaces \
-randroid.hidl:system/libhidl/transport $PACKAGE
我们需要做的就是完成 Demo.cpp 的实现,赋予其具体的功能。
值得一提的是,由于 HIDL 是从 HAL 迁移过来的,因此为了平复厂商的心情方便慢慢移植,实现时支持 passthrough 模式,直接加载之前的 libdemo.so 完成实现。当然如果是新的硬件,还是建议将代码移植到 impl 中,这样的实现是 Binderized 的,即通过 IPC 进行调用。这里我们采用后者。
1 #include "Demo.h"
2 #include <iostream>
3
4 namespace android {
5 namespace hardware {
6 namespace demo {
7 namespace V1_0 {
8 namespace implementation {
9
10 // Methods from IDemo follow.
11 Return<void> Demo::foo(const hidl_string& name, foo_cb _hidl_cb) {
12 std::cout << "Demo::foo()" << std::endl;
13 _hidl_cb(name);
14 return Void();
15 }
16
17 Return<int32_t> Demo::bar(int32_t a, int32_t b) {
18 std::cout << "Demo::bar()" << std::endl;
19 return int32_t { a + b };
20 }
21
22 Return<void> Demo::baz() {
23 std::cout << "Demo::baz()" << std::endl;
24 return Void();
25 }
26
27
28 // Methods from ::android::hidl::base::V1_0::IBase follow.
29
30 //IDemo* HIDL_FETCH_IDemo(const char* /* name */) {
31 // return new Demo();
32 //}
33
34 } // namespace implementation
35 } // namespace V1_0
36 } // namespace demo
37 } // namespace hardware
38 } // namespace android
在创建文件时由于使用了 default 子目录,还需要更新接口的 Android.bp/mk,以便使用 AOSP 的 mmm 编译。
hardware/interfaces/update-makefiles.sh
mmm hardware/interfaces/demo
编译成功后,生成了两个我们主要关注的 so 库文件:
out/target/product/angler/system/lib/android.hardware.demo@1.0.so
out/target/product/angler/system/lib64/android.hardware.demo@1.0.so
out/target/product/angler/vendor/lib/hw/android.hardware.demo@1.0-impl.so
out/target/product/angler/vendor/lib64/hw/android.hardware.demo@1.0-impl.so
其中:
android.hardware.demo@1.0.so 是接口 so,由客户端使用;
android.hardware.demo@1.0-impl.so 是实现的 so,由服务端使用;
编译的规则可以参考生成的 Android.bp 文件。
后台服务
有了动态库,我们就可以编写实际的服务程序了。由于服务端使用的是 impl.so,那么就把服务端的代码也在 Demo.cpp 相同的目录中实现。首先是 service.cpp:
#define LOG_TAG "android.hardware.demo@1.0-service"
#include <android/hardware/demo/1.0/IDemo.h>
#include <hidl/HidlTransportSupport.h>
#include "Demo.h"
using android::hardware::demo::V1_0::IDemo;
using android::hardware::demo::V1_0::implementation::Demo;
using android::hardware::configureRpcThreadpool;
using android::hardware::joinRpcThreadpool;
using android::sp;
using android::status_t;
int main() {
// This function must be called before you join to ensure the proper
// number of threads are created. The threadpool will never exceed
// size one because of this call.
configureRpcThreadpool(1 /*threads*/, true /*willJoin*/);
sp<IDemo> demo = new Demo();
const status_t status = demo->registerAsService();
if (status != ::android::OK) {
return 1; // or handle error
}
// Adds this thread to the threadpool, resulting in one total
// thread in the threadpool. We could also do other things, but
// would have to specify 'false' to willJoin in configureRpcThreadpool.
joinRpcThreadpool();
return 1; // joinRpcThreadpool should never return
}
直接修改 bp 文件,增加一个 cc_binary 入口:
cc_binary {
name: "android.hardware.demo@1.0-service",
defaults: ["hidl_defaults"],
proprietary: true,
relative_install_path: "hw",
srcs: ["service.cpp"],
init_rc: ["android.hardware.demo@1.0-service.rc"],
shared_libs: [
"libhidlbase",
"libhidltransport",
"libutils",
"liblog",
"android.hardware.demo@1.0",
"android.hardware.demo@1.0-impl",
],
}
生成的可执行文件如下所示:
out/target/product/angler/vendor/bin/hw/android.hardware.demo@1.0-service
如果需要持久化的话,可以增加一个 rc 文件进行开机启动,在后面介绍 SELinux 的时候再详细说。
客户端
由于主要实现都在服务端中,因此客户端代码相对简单。
#define LOG_TAG "TEST_CLINET"
#include <android/hardware/demo/1.0/IDemo.h>
#include <log/log.h>
using android::hardware::demo::V1_0::IDemo;
using android::sp;
using android::hardware::hidl_string;
int main(){
sp<IDemo> demo = IDemo::getService();
if( demo == nullptr ){
ALOGE("Can't find IDemo service...");
return -1;
}
printf("found service @ %p\n", demo.get());
demo->foo("test_client", [&](hidl_string result) {
printf("ret: %s\n", result.c_str());
});
int ret = demo->bar(3, 4);
printf("3 + 4 = %d\n", ret);
demo->baz();
return 0;
}
Android.bp 文件:
cc_binary {
name: "test_client",
relative_install_path: "hw",
defaults: ["hidl_defaults"],
proprietary: true,
srcs: ["test_client.cpp"],
shared_libs: [
"libhidlbase",
"libhidltransport",
"libhwbinder",
"libutils",
"libcutils",
"liblog",
"android.hardware.demo@1.0",
],
}
编译成功后可以直接在 Android 中以 root 权限运行,如果是非 root 环境则会遇到一些权限错误,主要是 SELinux 相关的问题,因此需要配置好对应的 sepolicy。
sepolicy
在非测试版本中,SELinux 的权限可能导致服务端无法注册或者客户端无法和服务端进行交互,因此需要添加对应的标签和权限。
添加 rc 文件
添加 rc 文件的目的是让硬件服务可以开机启动,并且设置好对应的启动权限,这里的rc 文件路径为: /vendor/etc/init/android.hardware.demo@1.0-service.rc
service demo_hal_service /vendor/bin/hw/android.hardware.demo@1.0-service
class hal
user root
group root
seclabel u:r:demo:s0
setenv LD_LIBRARY_PATH /vendor/lib64/hw
更多 initrc 的语法见 system/core/init/README.md。
新建 domain
创建一个新的device/huawei/angler/sepolicy/hal_demo.te文件,内容如下:
# demo service
type demo, domain;
type demo_exec, exec_type, file_type;
init_daemon_domain(demo)
这是一个初始化的模板,新的 SELinux 规则可以添加到后面,一个方便搜集新规则的方式是先以 Permissive 模式启动,并通过 AOSP 提供的 audit2allow 等辅助脚本进行分析。
由于 sepolicy 在内存文件系统中,因此不能直接进行持久化修改,需要重新打包 boot.img 并刷机。
make bootimg
添加 Label
在device/huawei/angler/sepolicy/file_contexts文件中新增一行:
# Demo hal
/vendor/bin/hw/android\.hardware\.demo@1\.0-service u:object_r:demo_exec:s0
确保可执行文件被正确地打上对应的 SELinux Label。
在测试阶段,最好先修改platform/device/<vendor>/<target>/BoardConfig.mk文件,将系统设置为 Permissive 模式,等到 SELinux 相关规则添加完成后再恢复成 Enforcing。
BOARD_KERNEL_CMDLINE += androidboot.selinux=permissive
修改完后重新编译 boot.img 并烧写测试。由于华为的 vendor.img 是私有的,所以烧写完后重新 mount 修改就行了,不需要重新打包。
更新镜像并重启系统后,可以看到生效的 SELinux 规则:
$ ls -lZ /vendor/bin/hw/ | grep demo
-rwxr-xr-x 1 root shell u:object_r:demo_exec:s0 11032 2020-10-28 03:37 android.hardware.demo@1.0-service
$ ps -ef -Z | grep demo
u:r:demo:s0 system 412 1 0 00:54:18 ? 00:00:00 android.hardware.demo@1.0-service
manifest
VINTF (vendor interface object) 的作用是用来收集设备信息并生成可查询的API,使用 XML 格式表示。其中 device manifest 用来声明当前固件的接口,如下所示。
/vendor/manifest.xml:
<hal format="hidl">
<name>android.hardware.demo</name>
<transport>hwbinder</transport>
<version>1.0</version>
<interface>
<name>IDemo</name>
<instance>default</instance>
</interface>
</hal>
网上一些文章说如果没有在 manifest.xml 中定义,client 端是无法获取到 service 的,但是实际测试时仅仅遇到权限的问题,详见 VINTF/device manifest。
小结
本文介绍了 Android 中最为常见的两种硬件接口,传统 HAL 和 HIDL。其中 HAL 在 Android 8 中弃用,取而代之的是基于 IPC 的 HIDL 方案,后者同时支持 passthrough 模式兼容传统的 HAL,这也是很多厂商移植前的临时过渡方案。虽然使用 IPC 会在一定程度影响性能,但 HIDL 方案提供了许多优化的措施,比如通过共享内存快速消息队列(FMQ)进行数据交互。此外,我们还基于 HIDL 编写了一个简单的 demo 驱动以及配套的 service 和 client 示例,便于理解硬件创建和调用的流程,这对于固件驱动逆向而言也是必要的知识。