An unauthenticated attacker can easily install malicious software through USB without using adb fastboot.
REC-22: Remove support for adb shell.
REC-23: Disable magic command for adb/fastboot or protect its activation with a device-unique key generated randomly
若没有对adb、fastboot进行限制操作,就需要对其进行加密或直接移除,adb加密已在其他文档中描述,下文以SDX12为例主要讲解如何去移除fastboot。
进入fastboot分软件方法和硬件方法,硬件的话主要是通过按键组合来进入fastboot模式,软件的话通常是执行重启命令携带fastboot模式参数,然后重启就可以进入fastboot模式。
如上所述,软件进入fastboot的方式是执行重启命令时携带fastboot模式参数,就可以重启进入fastboot模式。而支持这种软重启的命令有很多,我们需要对其进行一一处理,先根据我们的排查结果列举出软重启或软关机方式:
其中3、4、5中的sys_reboot、sys_shutdown、powerapp应用属于同一个应用powerapp的不同分身;1是reboot应用;2、7都属于系统reboot脚本,1、2最终调用的是sys_reboot,7调用的是sys_shutdown;6是adb reboot命令,最终执行系统调用。又sys_shutdown和shutdown是关机操作,因此后续我们主要对这三类重启应用进行fastboot限制。
sys_reboot、sys_shutdown、powerapp三个应用都是powerapp的不同分身,功能完全一致,其代码在sdx12-ap/system/core/powerapp,在编译install阶段会先install powerapp到/sbin,然后创建软链接sys_reboot、sys_shutdown:
powerapp代码架构如下,只有一个c文件,其他均为相关脚本:
sdx12-ap/system/core/powerapp$ tree -L 1
.
├── configure.ac
├── Makefile.am
├── powerapp.c
├── power_config.service
├── reboot
├── reboot-bootloader
├── reboot-cookie
├── reboot-recovery
├── reset_reboot_cookie
├── reset_reboot_cookie.service
├── shutdown
└── start_power_config
0 directories, 12 files
powerapp.c中会处理sys_reboot、sys_shutdown等命令重启,以及检测按键事件,确定是重启、关机还是休眠,具体逻辑如下:
sdx12-ap\system\core\powerapp\powerapp.c
int main(int argc, char *argv[])
{
…
char *arg1 = NULL;
char *cmd_name = basename(argv[0]);
if(argc > 1)
arg1 = argv[1];
if (!strcmp(cmd_name, "sys_reboot"))//sys_reboot命令处理
{
system("rmmod wlan.ko");
sys_shutdown_or_reboot(1, arg1);
return 1;
}
else if (!strcmp(cmd_name, "sys_shutdown"))//sys_shutdown命令处理
{
sys_shutdown_or_reboot(0, arg1);
return 2;
}
fd = open(KEY_INPUT_DEVICE, O_RDONLY);//按键事件处理
…
while ((n = read(fd, &ev, sizeof(struct input_event))) > 0) {
…
if (ev.type == EV_KEY && ev.code == KEY_POWER && ev.value == 1)
{
memcpy(&then, &ev.time, sizeof(struct timeval));
}
else if (ev.type == EV_KEY && ev.code == KEY_POWER && ev.value == 0)
{
memcpy(&now, &ev.time, sizeof(struct timeval));
duration = diff_timestamps(&then, &now);
if (duration > POWER_OFF_TIMER)
{
powerapp_shutdown();
}
else
{
suspend_or_resume();
}
}
}
return 0;
}
从上面可以看出,按键只会重启或关机,不会涉及到进入fastboot模式,因此我们只需要关注sys_reboot、sys_shutdown的处理,在main函数中是直接调用了sys_shutdown_or_reboot()函数,并传入对应参数:
void sys_shutdown_or_reboot(int reboot, char *arg1)
{
int cmd = LINUX_REBOOT_CMD_POWER_OFF;
int n = 0;
if (reboot)
{
if (arg1)
cmd = LINUX_REBOOT_CMD_RESTART2;
else
cmd = LINUX_REBOOT_CMD_RESTART;
}
if (cmd == LINUX_REBOOT_CMD_RESTART2 && strncmp(arg1, "recovery", 9) == 0)
{
set_fota_cookie();
}
n = syscall(SYS_reboot, LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2, cmd, arg1);
if (n < 0)
{
fprintf(stderr, "reboot system call failed %d (%s)\n", errno, strerror(errno));
}
}
可以看到sys_shutdown最终执行的是power off即关机流程,而sys_reboot执行的是重启流程,重启又分重启无参数、重启带参数recovery、重启带其他参数,最终都执行的是系统调用。因此我们可以在这个函数中进行bootloader命令过滤,方案如下:当输入sys_reboot时检测携带的参数是否为bootloader,若是,则打印Not support bootloader直接退出:
void sys_shutdown_or_reboot(int reboot, char *arg1)
{
int cmd = LINUX_REBOOT_CMD_POWER_OFF;
int n = 0;
if (reboot)
{
if (arg1)
cmd = LINUX_REBOOT_CMD_RESTART2;
else
cmd = LINUX_REBOOT_CMD_RESTART;
}
if (cmd == LINUX_REBOOT_CMD_RESTART2 && strncmp(arg1, "recovery", 9) == 0)
{
set_fota_cookie();
}
if (cmd == LINUX_REBOOT_CMD_RESTART2 && strncmp(arg1, "bootloader", 10) == 0)
{
printf("Not support bootloader.");
return;
}
n = syscall(SYS_reboot, LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2, cmd, arg1);
if (n < 0)
{
fprintf(stderr, "reboot system call failed %d (%s)\n", errno, strerror(errno));
}
}
或
int main(int argc, char *argv[])
{
…
char *arg1 = NULL;
char *cmd_name = basename(argv[0]);
if(argc > 1)
arg1 = argv[1];
if (!strcmp(cmd_name, "sys_reboot"))//sys_reboot命令处理
{
printf("arg1:%s.",arg1);
if (!strcmp(arg1, "bootloader"))
{
printf("Not support bootloader.");
return 0;
}
system("rmmod wlan.ko");
sys_shutdown_or_reboot(1, arg1);
return 1;
}
else if (!strcmp(cmd_name, "sys_shutdown"))//sys_shutdown命令处理
{
sys_shutdown_or_reboot(0, arg1);
return 2;
}
fd = open(KEY_INPUT_DEVICE, O_RDONLY);//按键事件处理
…
while ((n = read(fd, &ev, sizeof(struct input_event))) > 0) {
…
if (ev.type == EV_KEY && ev.code == KEY_POWER && ev.value == 1)
{
memcpy(&then, &ev.time, sizeof(struct timeval));
}
else if (ev.type == EV_KEY && ev.code == KEY_POWER && ev.value == 0)
{
memcpy(&now, &ev.time, sizeof(struct timeval));
duration = diff_timestamps(&then, &now);
if (duration > POWER_OFF_TIMER)
{
powerapp_shutdown();
}
else
{
suspend_or_resume();
}
}
}
return 0;
}
reboot-bootloader是个sh脚本,执行脚本会先往/data/reboot-cookie写入1,再执行reboot脚本:
#! /bin/sh
echo 1 > /data/reboot-cookie
reboot
reboot脚本会判断/data/reboot-cookie值是多少,再执行sys_reboot进入fastboot、recovery或直接重启:
if [[ `cat /data/reboot-cookie` = 1 ]]; then
sys_reboot bootloader
elif [[ `cat /data/reboot-cookie` = 2 ]]; then
sys_reboot recovery
else
sys_reboot
fi
我们在1.1做的修改是也是符合这种场景的。
reboot代码在sdx12-ap\system\core\reboot\目录下。reboot代码结构如下:
sdx12-ap/system/core/reboot$ tree
.
├── Android.mk
└── reboot.c
0 directories, 2 files
reboot.c中会处理相关reboot命令:
int main(int argc, char *argv[])
{
…
const char *cmd = "reboot";
char *optarg = "";
opterr = 0;
do {
int c;
c = getopt(argc, argv, "p");
if (c == -1) {
break;
}
switch (c) {
case 'p':
cmd = "shutdown";
break;
case '?':
fprintf(stderr, "usage: %s [-p] [rebootcommand]\n", argv[0]);
exit(EXIT_FAILURE);
}
} while (1);
if(argc > optind + 1) {
fprintf(stderr, "%s: too many arguments\n", argv[0]);
exit(EXIT_FAILURE);
}
if (argc > optind)
optarg = argv[optind];
prop_len = snprintf(property_val, sizeof(property_val), "%s,%s", cmd, optarg);
if (prop_len >= sizeof(property_val)) {
fprintf(stderr, "reboot command too long: %s\n", optarg);
exit(EXIT_FAILURE);
}
ret = property_set(ANDROID_RB_PROPERTY, property_val);
if(ret < 0) {
perror("reboot");
exit(EXIT_FAILURE);
}
// Don't return early. Give the reboot command time to take effect
// to avoid messing up scripts which do "adb shell reboot && adb wait-for-device"
while(1) { pause(); }
fprintf(stderr, "Done\n");
return 0;
}
#define ANDROID_RB_PROPERTY "sys.powerctl"也就是设置了sys.powerctl属性为“bootloader”或“recovery”,此属性的改变,触发init rc的调用:
system/core/rootdir/init.rc
on property:sys.powerctl=*
powerctl ${sys.powerctl}
按init的调用过程,即是在init里调用了do_powerctl 函数:
system/core/init/builtins.cpp
int do_powerctl(int nargs, char **args)
{
…
res = expand_props(command, args[1], sizeof(command));
…
if (strncmp(command, "shutdown", 8) == 0) {
cmd = ANDROID_RB_POWEROFF;
len = 8;
} else if (strncmp(command, "reboot", 6) == 0) {
cmd = ANDROID_RB_RESTART2;
len = 6;
} else {
ERROR("powerctl: unrecognized command '%s'\n", command);
return -EINVAL;
}
if (command[len] == ',') {
char prop_value[PROP_VALUE_MAX] = {0};
reboot_target = &command[len + 1];
if ((property_get("init.svc.recovery", prop_value) == 0) &&
(strncmp(reboot_target, "keys", 4) == 0)) {
ERROR("powerctl: permission denied\n");
return -EINVAL;
}
} else if (command[len] == '\0') {
reboot_target = "";
} else {
ERROR("powerctl: unrecognized reboot target '%s'\n", &command[len]);
return -EINVAL;
}
return android_reboot(cmd, 0, reboot_target);
}
在do_powerctl中又调用android_reboot:
system/core/libcutils/android_reboot.c
int android_reboot(int cmd, int flags UNUSED, const char *arg)
{
int ret;
sync();
remount_ro();
switch (cmd) {
case ANDROID_RB_RESTART:
ret = reboot(RB_AUTOBOOT);
break;
case ANDROID_RB_POWEROFF:
ret = reboot(RB_POWER_OFF);
break;
case ANDROID_RB_RESTART2:
system("rmmod wlan.ko");
ret = syscall(__NR_reboot, LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2,
LINUX_REBOOT_CMD_RESTART2, arg);
break;
default:
ret = -1;
}
return ret;
}
最终还是执行的系统调用和powerapp一样。因此我们可以在这个函数中进行bootloader命令过滤,方案1如下:当输入reboot时检测携带的参数是否为bootloader,若是,则打印Not support bootloader直接退出:
int main(int argc, char *argv[])
{
…
const char *cmd = "reboot";
char *optarg = "";
opterr = 0;
do {
int c;
c = getopt(argc, argv, "p");
if (c == -1) {
break;
}
switch (c) {
case 'p':
cmd = "shutdown";
break;
case '?':
fprintf(stderr, "usage: %s [-p] [rebootcommand]\n", argv[0]);
exit(EXIT_FAILURE);
}
} while (1);
if(argc > optind + 1) {
fprintf(stderr, "%s: too many arguments\n", argv[0]);
exit(EXIT_FAILURE);
}
if (argc > optind)
optarg = argv[optind];
printf("optarg:%s.",optarg);
if (!strcmp(optarg, "bootloader"))
{
printf("Not support bootloader.");
return 0;
}
prop_len = snprintf(property_val, sizeof(property_val), "%s,%s", cmd, optarg);
if (prop_len >= sizeof(property_val)) {
fprintf(stderr, "reboot command too long: %s\n", optarg);
exit(EXIT_FAILURE);
}
ret = property_set(ANDROID_RB_PROPERTY, property_val);
if(ret < 0) {
perror("reboot");
exit(EXIT_FAILURE);
}
// Don't return early. Give the reboot command time to take effect
// to avoid messing up scripts which do "adb shell reboot && adb wait-for-device"
while(1) { pause(); }
fprintf(stderr, "Done\n");
return 0;
}
方案2如下:在do_powerctl函数中进行拦截,当检测到reboot命令后面携带的参数是bootloader时,删除参数,让命令变为仅reboot:
system/core/init/builtins.cpp
int do_powerctl(int nargs, char **args)
{
…
res = expand_props(command, args[1], sizeof(command));
…
if (strncmp(command, "shutdown", 8) == 0) {
cmd = ANDROID_RB_POWEROFF;
len = 8;
} else if (strncmp(command, "reboot", 6) == 0) {
cmd = ANDROID_RB_RESTART2;
len = 6;
} else {
ERROR("powerctl: unrecognized command '%s'\n", command);
return -EINVAL;
}
if (command[len] == ',') {
char prop_value[PROP_VALUE_MAX] = {0};
reboot_target = &command[len + 1];
if ((property_get("init.svc.recovery", prop_value) == 0) &&
(strncmp(reboot_target, "keys", 4) == 0)) {
ERROR("powerctl: permission denied\n");
return -EINVAL;
}
if(strncmp(reboot_target, "bootloader", 10) == 0)
{
reboot_target = "";
}
} else if (command[len] == '\0') {
reboot_target = "";
} else {
ERROR("powerctl: unrecognized reboot target '%s'\n", &command[len]);
return -EINVAL;
}
return android_reboot(cmd, 0, reboot_target);
}
这两种方式均可,最后结果就是执行reboot bootloader就只是重启,而不会进入fastboot模式,验证符合预期。
adb代码在sdx12-ap\system\core\adb,目录结构:
当在电脑命令行窗口中输入adb 命令时,会先执行adb客户端,客户端拿到命令之后,会发送给adb服务端,server再将命令传给Daemon,最后在手机上执行。假如在手机上安装一个应用,会有一个返回信息,会将信息传递给adb服务器,adb 在给客户端,最后显示在命令行。
因此发送adb reboot,模组内adb会按照如下流程进行处理,首先service_to_fd中对于每个adb指令起线程处理,reboot线程reboot_service:
sdx12-ap\system\core\adb\services.cpp
int service_to_fd(const char *name)
{
…
else if(!strncmp(name, "reboot:", 7)) {
void* arg = strdup(name + 7);
if (arg == NULL) return -1;
ret = create_service_thread(reboot_service, arg);
}
…
}
reboot_service中调用reboot_service_impl:
void reboot_service(int fd, void* arg)
{
if (reboot_service_impl(fd, static_cast<const char*>(arg))) {
// Don't return early. Give the reboot command time to take effect
// to avoid messing up scripts which do "adb reboot && adb wait-for-device"
…
}
reboot_service_impl中会将reboot携带的参数传入android_reboot函数进行重启:
static bool reboot_service_impl(int fd, const char* arg) {
…
const char* const recovery_dir = "/cache/recovery";
const char* const command_file = "/cache/recovery/command";
// Ensure /cache/recovery exists.
if (adb_mkdir(recovery_dir, 0770) == -1 && errno != EEXIST) {
D("Failed to create directory '%s': %s\n", recovery_dir, strerror(errno));
return false;
}
…
reboot_arg = "recovery";
}
sync();
char property_val[PROPERTY_VALUE_MAX];
int ret = snprintf(property_val, sizeof(property_val), "reboot,%s", reboot_arg);
…
// Call android_reboot instead of passing args to init.
#if ADB_REBOOT_ENABLED
ret = android_reboot(ANDROID_RB_RESTART2, 0, arg);
#else
ret = property_set(ANDROID_RB_PROPERTY, property_val);
…
#endif
return true;
}
android_reboot同1.3章节,最终执行系统调用。因此我们也有两种方案对于bootloader命令过滤,方案1如下:当输入adb reboot时检测携带的参数是否为bootloader,若是,则打印Not support bootloader直接退出:
static bool reboot_service_impl(int fd, const char* arg) {
…
const char* const recovery_dir = "/cache/recovery";
const char* const command_file = "/cache/recovery/command";
// Ensure /cache/recovery exists.
if (adb_mkdir(recovery_dir, 0770) == -1 && errno != EEXIST) {
D("Failed to create directory '%s': %s\n", recovery_dir, strerror(errno));
return false;
}
…
reboot_arg = "recovery";
}
sync();
if (strcmp(reboot_arg, "bootloader") == 0)
{
WriteFdFmt(fd, "reboot failed: not support bootloader\n");
return false;
}
char property_val[PROPERTY_VALUE_MAX];
int ret = snprintf(property_val, sizeof(property_val), "reboot,%s", reboot_arg);
…
// Call android_reboot instead of passing args to init.
#if ADB_REBOOT_ENABLED
ret = android_reboot(ANDROID_RB_RESTART2, 0, arg);
#else
ret = property_set(ANDROID_RB_PROPERTY, property_val);
…
#endif
return true;
}
方案2:在android_reboot中过滤bootloader命令,当符合条件时,直接讲arg置为空,即直接重启
sdx12-ap\system\core\libcutils\android_reboot.c
int android_reboot(int cmd, int flags UNUSED, const char *arg)
{
int ret;
sync();
remount_ro();
switch (cmd) {
case ANDROID_RB_RESTART:
ret = reboot(RB_AUTOBOOT);
break;
case ANDROID_RB_POWEROFF:
ret = reboot(RB_POWER_OFF);
break;
case ANDROID_RB_RESTART2:
system("rmmod wlan.ko");
if (!strcmp(arg, "bootloader"))
{
memset(arg, 0, sizeof(arg));
}
ret = syscall(__NR_reboot, LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2,
LINUX_REBOOT_CMD_RESTART2, arg);
break;
default:
ret = -1;
}
return ret;
}
方案2在1.3中也可适用。
硬件上通常会预留key组合来进入fastboot或edl等模式,这些逻辑处理通常在lk阶段aboot中完成,代码在sdx12-ap\bootable\bootloader\lk\app\aboot,目录结构:
sdx12-ap/bootable/bootloader/lk/app/aboot$ tree -L 1
.
├── aboot.c
├── bootimg.h
├── devinfo.h
├── fastboot.c
├── fastboot.h
├── fastboot_test.c
├── fastboot_test.h
├── mdtp.c
├── mdtp_defs.c
├── mdtp_defs.h
├── mdtp_fs.c
├── mdtp_fs.h
├── mdtp_fuse.c
├── mdtp.h
├── mdtp_lk_ut.c
├── mdtp_ui.c
├── meta_format.h
├── recovery.c
├── recovery.h
├── rules.mk
└── sparse_format.h
0 directories, 21 files
aboot.c就包含了按键、boot模式等控制:
sdx12-ap\bootable\bootloader\lk\app\aboot\aboot.c
void aboot_init(const struct app_descriptor *app)
{
…
/*
* Check power off reason if user force reset,
* if yes phone will do normal boot.
*/
if (is_user_force_reset())
goto normal_boot;
/* Check if we should do something other than booting up */
if (keys_get_state(KEY_VOLUMEUP) && keys_get_state(KEY_VOLUMEDOWN))
{
dprintf(ALWAYS,"dload mode key sequence detected\n");
reboot_device(EMERGENCY_DLOAD);
dprintf(CRITICAL,"Failed to reboot into dload mode\n");
boot_into_fastboot = true;//按键进入fastboot
}
if (!boot_into_fastboot)
{
unsigned vloop = 50;
extern int uart_getc(int port, bool wait);
while(vloop--)
{
if(0x1B == uart_getc(0, 0))
{
boot_into_fastboot = true;//串口输入进入fastboot
goto normal_boot;
}
udelay(5000);
}
if (keys_get_state(KEY_HOME) || keys_get_state(KEY_VOLUMEUP))
boot_into_recovery = 1;
if (!boot_into_recovery &&
(keys_get_state(KEY_BACK) || keys_get_state(KEY_VOLUMEDOWN)))
{
boot_into_fastboot = true; //按键进入fastboot
}
}
#if NO_KEYPAD_DRIVER
if (fastboot_trigger())
{
boot_into_fastboot = true;
}
#endif
#if USE_PON_REBOOT_REG
reboot_mode = check_hard_reboot_mode();
#else
reboot_mode = check_reboot_mode();
#endif
if (reboot_mode == RECOVERY_MODE)
{
boot_into_recovery = 1;
}
else if(reboot_mode == FASTBOOT_MODE)
{
boot_into_fastboot = true;//重启标识进入fastboot
}
…
}
为了去除进入fastboot模式,我们可以在aboot初始化过程中拿掉所有fastboot入口,无论硬件还是软件判断,修改如下:
sdx12-ap\bootable\bootloader\lk\app\aboot\aboot.c
void aboot_init(const struct app_descriptor *app)
{
…
/*
* Check power off reason if user force reset,
* if yes phone will do normal boot.
*/
if (is_user_force_reset())
goto normal_boot;
/* Check if we should do something other than booting up */
if (keys_get_state(KEY_VOLUMEUP) && keys_get_state(KEY_VOLUMEDOWN))
{
dprintf(ALWAYS,"dload mode key sequence detected\n");
reboot_device(EMERGENCY_DLOAD);
dprintf(CRITICAL,"Failed to reboot into dload mode\n");
//boot_into_fastboot = true;//按键进入fastboot
}
if (!boot_into_fastboot)
{
unsigned vloop = 50;
extern int uart_getc(int port, bool wait);
while(vloop--)
{
if(0x1B == uart_getc(0, 0))
{
//boot_into_fastboot = true;//串口输入进入fastboot
goto normal_boot;
}
udelay(5000);
}
if (keys_get_state(KEY_HOME) || keys_get_state(KEY_VOLUMEUP))
boot_into_recovery = 1;
if (!boot_into_recovery &&
(keys_get_state(KEY_BACK) || keys_get_state(KEY_VOLUMEDOWN)))
{
//boot_into_fastboot = true; //按键进入fastboot
}
}
#if NO_KEYPAD_DRIVER
if (fastboot_trigger())
{
//boot_into_fastboot = true;
}
#endif
#if USE_PON_REBOOT_REG
reboot_mode = check_hard_reboot_mode();
#else
reboot_mode = check_reboot_mode();
#endif
if (reboot_mode == RECOVERY_MODE)
{
boot_into_recovery = 1;
}
else if(reboot_mode == FASTBOOT_MODE)
{
//boot_into_fastboot = true;//重启标识进入fastboot
}
…
}
编译验证生效,通过上面几个章节的修改,fastboot模式全部拿掉了,不会再通过软件或硬件的方式进入到fastboot模式。
满足Penetration测试移除fastboot要求。