Android 真的是开源的吗?
之前分析过 Android 系统中的进程间通信逆向,即基于 Binder 拓展的以及 AIDL 描述的 IPC。了解 Android 系统的话应该知道在 8.0 之后,/dev/binder
拓展多出了两个域,分别是 /dev/hwbinder
和 /dev/vndbinder
。其中 hwbinder 主要用于 HIDL 接口的通信,而 vndbinder 则是专注于 vendor 进程之间的 AIDL 通信。
本文主要关注的是硬件部分。具体来说,就是作为一个 OEM/ODM 厂商,如何将自己的硬件添加到自己的 ROM 之中;以及作为一个安全工程师,如何对厂商的硬件驱动进行(逆向)分析。其实这两个问题的本质是一致的,即要求了解 Android 硬件开发和集成流程。
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;
硬件模块 (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 语言实现继承的一种典型方式。
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 中定义了其标准拓展接口和数据类型,比如打开/关闭摄像头、设置参数、数据回调等等。
HAL 是最初的硬件抽象方案,在 Android 8 中已经废弃并被 HIDL 取代。HIDL 和 AIDL 类似,都是一种接口描述语言 (HAL interface definition language),用来描述硬件的接口。HIDL 设计的初衷是更新 frameworks 时避免重新编译 HAL,后者可以由厂商单独编译并在 vendor 分区中单独更新,此外还支持完善的版本管理。
为了了解开发流程,我现在就是一个厂商的 BSP 工程师。这里假设要创建一个名为 demo 的硬件驱动,并且以华为的 Nexus 6P 为例进行开发。这里不赘述编译 AOSP 的具体过程,只专注于 HIDL 相关部分。
首先是创建 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 进行调用。这里我们采用后者。
#include "Demo.h"
#include <iostream>
namespace android {
namespace hardware {
namespace demo {
namespace V1_0 {
namespace implementation {
// Methods from IDemo follow.
Return<void> Demo::foo(const hidl_string& name, foo_cb _hidl_cb) {
std::cout << "Demo::foo()" << std::endl;
_hidl_cb(name);
return Void();
}
Return<int32_t> Demo::bar(int32_t a, int32_t b) {
std::cout << "Demo::bar()" << std::endl;
return int32_t { a + b };
}
Return<void> Demo::baz() {
std::cout << "Demo::baz()" << std::endl;
return Void();
}
// Methods from ::android::hidl::base::V1_0::IBase follow.
//IDemo* HIDL_FETCH_IDemo(const char* /* name */) {
// return new Demo();
//}
} // namespace implementation
} // namespace V1_0
} // namespace demo
} // namespace hardware
} // 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。
在非测试版本中,SELinux 的权限可能导致服务端无法注册或者客户端无法和服务端进行交互,因此需要添加对应的标签和权限。
添加 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。
创建一个新的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 bootimage
在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
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 示例,便于理解硬件创建和调用的流程,这对于固件驱动逆向而言也是必要的知识。