diff options
Diffstat (limited to 'utils')
37 files changed, 2409 insertions, 0 deletions
diff --git a/utils/shell-as/Android.bp b/utils/shell-as/Android.bp new file mode 100644 index 000000000..96dc1c9d5 --- /dev/null +++ b/utils/shell-as/Android.bp @@ -0,0 +1,107 @@ +// Copyright (C) 2023 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +cc_binary { + name: "shell-as", + cflags: [ + "-Wall", + "-Werror", + "-Wextra", + ], + srcs: [ + "*.cpp", + ":shell-as-test-app-apk-cpp", + ], + header_libs: ["libcutils_headers"], + static_executable: true, + static_libs: [ + "libbase", + "libcap", + "liblog", + "libseccomp_policy", + "libselinux", + ], + arch: { + arm: { + srcs: ["shell-code/*-arm.S"] + }, + arm64: { + srcs: ["shell-code/*-arm64.S"] + }, + x86: { + srcs: ["shell-code/*-x86.S"] + }, + x86_64: { + srcs: ["shell-code/*-x86_64.S"] + } + } +} + +// A simple app that requests all non-system permissions and contains no other +// functionality. This can be used as a target for shell-as to emulate the +// security context of the most privileged possible non-system app. +android_app { + name: "shell-as-test-app", + manifest: ":shell-as-test-app-manifest", + srcs: ["app/**/*.java"], + sdk_version: "9", + certificate: ":shell-as-test-app-cert", +} + +// https://source.android.com/docs/core/ota/sign_builds#release-keys +// Generated by running: +// $ANDROID_BUILD_TOP/development/tools/make_key \ +// shell-as-test-app-key \ +// '/C=US/ST=California/L=Mountain View/O=Android/OU=Android/CN=Android/emailAddress=android@android.com +android_app_certificate { + name: "shell-as-test-app-cert", + certificate: "shell-as-test-app-key", +} + +genrule { + name: "shell-as-test-app-manifest", + srcs: [ + ":permission-list-normal", + "AndroidManifest.xml.template" + ], + cmd: "$(location gen-manifest.sh) " + + "$(location AndroidManifest.xml.template) " + + "$(location :permission-list-normal) " + + "$(out)", + out: ["AndroidManifest.xml"], + tool_files: ["gen-manifest.sh"], +} + +// A source file that contains the contents of the above shell-as-test-app APK +// embedded as an array. +cc_genrule { + name: "shell-as-test-app-apk-cpp", + srcs: [":shell-as-test-app"], + cmd: "(" + + " echo '#include <stddef.h>';" + + " echo '#include <stdint.h>';" + + " echo '';" + + " echo 'namespace shell_as {';" + + " echo 'const uint8_t kTestAppApk[] = {';" + + " $(location toybox) xxd -i < $(in);" + + " echo '};';" + + " echo 'void GetTestApk(uint8_t **apk, size_t *length) {';" + + " echo ' *apk = (uint8_t*) kTestAppApk;';" + + " echo ' *length = sizeof(kTestAppApk);';" + + " echo '}';" + + " echo '} // namespace shell_as';" + + ") > $(out)", + out: ["test-app-apk.cpp"], + tools: ["toybox"] +} diff --git a/utils/shell-as/AndroidManifest.xml.template b/utils/shell-as/AndroidManifest.xml.template new file mode 100644 index 000000000..07e89b186 --- /dev/null +++ b/utils/shell-as/AndroidManifest.xml.template @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2023 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.google.tools.security.shell_as"> + + PERMISSIONS + + <application + android:allowBackup="true" + android:label="Shell-As Test App"> + <activity android:name=".MainActivity"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/utils/shell-as/OWNERS b/utils/shell-as/OWNERS new file mode 100644 index 000000000..431db9920 --- /dev/null +++ b/utils/shell-as/OWNERS @@ -0,0 +1,4 @@ +# Code owners for shell-as + +willcoster@google.com +cdombroski@google.com diff --git a/utils/shell-as/README.md b/utils/shell-as/README.md new file mode 100644 index 000000000..e0f6f93f2 --- /dev/null +++ b/utils/shell-as/README.md @@ -0,0 +1,33 @@ +# shell-as + +shell-as is a utility that can be used to execute a binary in a less privileged +security context. This can be useful for verifying the capabilities of a process +on a running device or testing PoCs with different privilege levels. + +## Usage + +The security context can either be supplied explicitly, inferred from a process +running on the device, or set to a predefined profile. + +For example, the following are equivalent and execute `/system/bin/id` in the +context of the init process. + +```shell +shell-as \ + --uid 0 \ + --gid 0 \ + --selinux u:r:init:s0 \ + --seccomp system \ + /system/bin/id +``` + +```shell +shell-as --pid 1 /system/bin/id +``` + +The "untrusted-app" profile can be used to execute a binary with all the +possible privileges attainable by an untrusted app: + +```shell +shell-as --profile untrusted-app /system/bin/id +``` diff --git a/utils/shell-as/app/com/android/google/tools/security/shell_as/MainActivity.java b/utils/shell-as/app/com/android/google/tools/security/shell_as/MainActivity.java new file mode 100644 index 000000000..d5d178c2f --- /dev/null +++ b/utils/shell-as/app/com/android/google/tools/security/shell_as/MainActivity.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.google.tools.security.shell_as; + +import android.app.Activity; +import android.os.Bundle; + +/** An empty activity for the shell-as test app. */ +public class MainActivity extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } +} diff --git a/utils/shell-as/command-line.cpp b/utils/shell-as/command-line.cpp new file mode 100644 index 000000000..9a893c375 --- /dev/null +++ b/utils/shell-as/command-line.cpp @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "./command-line.h" + +#include <getopt.h> + +#include <iostream> +#include <string> + +#include "./context.h" +#include "./string-utils.h" + +namespace shell_as { + +namespace { +const std::string kUsage = + R"(Usage: shell-as [options] [<program> <arguments>...] + +shell-as executes a program in a specified Android security context. The default +program that is executed if none is specified is `/bin/system/sh`. + +The following options can be used to define the target security context. + +--verbose, -v Enables verbose logging. +--uid <uid>, -u <uid> The target real and effective user ID. +--gid <gid>, -g <gid> The target real and effective group ID. +--groups <gid1,2,..>, -G <1,2,..> A comma separated list of supplementary group + IDs. +--nogroups Specifies that all supplementary groups should + be cleared. +--selinux <context>, -s <context> The target SELinux context. +--seccomp <filter>, -f <filter> The target seccomp filter. Valid values of + filter are 'none', 'uid-inferred', 'app', + 'app-zygote', and 'system'. +--caps <capabilities> A libcap textual expression that describes + the desired capability sets. The only + capability set that matters is the permitted + set, the other sets are ignored. + + Examples: + + "=" - Clear all capabilities + "=p" - Raise all capabilities + "23,CAP_SYS_ADMIN+p" - Raise CAP_SYS_ADMIN + and capability 23. + + For a full description of the possible values + see `man 3 cap_from_text` (the libcap-dev + package provides this man page). +--pid <pid>, -p <pid> Infer the target security context from a + running process with the given process ID. + This option implies --seccomp uid_inferred. + This option infers the capability from the + target process's permitted capability set. +--profile <profile>, -P <profile> Infer the target security context from a + predefined security profile. Using this + option will install and execute a test app on + the device. Currently, the only valid profile + is 'untrusted-app' which corresponds to an + untrusted app which has been granted every + non-system permission. + +Options are evaluated in the order that they are given. For example, the +following will set the target context to that of process 1234 but override the +user ID to 0: + + shell-as --pid 1234 --uid 0 +)"; + +const char* kShellExecvArgs[] = {"/system/bin/sh", nullptr}; + +bool ParseGroups(char* line, std::vector<gid_t>* ids) { + // Allow a null line as a valid input since this method is used to handle both + // --groups and --nogroups. + if (line == nullptr) { + return true; + } + return SplitIdsAndSkip(line, ",", /*num_to_skip=*/0, ids); +} +} // namespace + +bool ParseOptions(const int argc, char* const argv[], bool* verbose, + SecurityContext* context, char* const* execv_args[]) { + char short_options[] = "+s:hp:u:g:G:f:c:vP:"; + struct option long_options[] = { + {"selinux", true, nullptr, 's'}, {"help", false, nullptr, 'h'}, + {"uid", true, nullptr, 'u'}, {"gid", true, nullptr, 'g'}, + {"pid", true, nullptr, 'p'}, {"verbose", false, nullptr, 'v'}, + {"groups", true, nullptr, 'G'}, {"nogroups", false, nullptr, 'G'}, + {"seccomp", true, nullptr, 'f'}, {"caps", true, nullptr, 'c'}, + {"profile", true, nullptr, 'P'}, + }; + int option; + bool infer_seccomp_filter = false; + SecurityContext working_context; + std::vector<gid_t> supplementary_group_ids; + uint32_t working_id = 0; + while ((option = getopt_long(argc, argv, short_options, long_options, + nullptr)) != -1) { + switch (option) { + case 'v': + *verbose = true; + break; + case 'h': + std::cerr << kUsage; + return false; + case 'u': + if (!StringToUInt32(optarg, &working_id)) { + return false; + } + working_context.user_id = working_id; + break; + case 'g': + if (!StringToUInt32(optarg, &working_id)) { + return false; + } + working_context.group_id = working_id; + break; + case 'c': + working_context.capabilities = cap_from_text(optarg); + if (working_context.capabilities.value() == nullptr) { + std::cerr << "Unable to parse capabilities" << std::endl; + return false; + } + break; + case 'G': + supplementary_group_ids.clear(); + if (!ParseGroups(optarg, &supplementary_group_ids)) { + std::cerr << "Unable to parse supplementary groups" << std::endl; + return false; + } + working_context.supplementary_group_ids = supplementary_group_ids; + break; + case 's': + working_context.selinux_context = optarg; + break; + case 'f': + infer_seccomp_filter = false; + if (strcmp(optarg, "uid-inferred") == 0) { + infer_seccomp_filter = true; + } else if (strcmp(optarg, "app") == 0) { + working_context.seccomp_filter = kAppFilter; + } else if (strcmp(optarg, "app-zygote") == 0) { + working_context.seccomp_filter = kAppZygoteFilter; + } else if (strcmp(optarg, "system") == 0) { + working_context.seccomp_filter = kSystemFilter; + } else if (strcmp(optarg, "none") == 0) { + working_context.seccomp_filter.reset(); + } else { + std::cerr << "Invalid value for --seccomp: " << optarg << std::endl; + return false; + } + break; + case 'p': + if (!SecurityContextFromProcess(atoi(optarg), &working_context)) { + return false; + } + infer_seccomp_filter = true; + break; + case 'P': + if (strcmp(optarg, "untrusted-app") == 0) { + if (!SecurityContextFromTestApp(&working_context)) { + return false; + } + } else { + std::cerr << "Invalid value for --profile: " << optarg << std::endl; + return false; + } + infer_seccomp_filter = true; + break; + default: + std::cerr << "Unknown option '" << (char)optopt << "'" << std::endl; + return false; + } + } + + if (infer_seccomp_filter) { + if (!working_context.user_id.has_value()) { + std::cerr << "No user ID; unable to infer appropriate seccomp filter." + << std::endl; + return false; + } + working_context.seccomp_filter = + SeccompFilterFromUserId(working_context.user_id.value()); + } + + *context = working_context; + if (optind < argc) { + *execv_args = argv + optind; + } else { + *execv_args = (char**)kShellExecvArgs; + } + return true; +} + +} // namespace shell_as diff --git a/utils/shell-as/command-line.h b/utils/shell-as/command-line.h new file mode 100644 index 000000000..4bf495f42 --- /dev/null +++ b/utils/shell-as/command-line.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SHELL_AS_COMMAND_LINE_H_ +#define SHELL_AS_COMMAND_LINE_H_ + +#include "./context.h" + +namespace shell_as { + +// Parse command line options into a target security context and arguments that +// can be passed to ExecuteInContext. +// +// The value of execv_args will either point to a sub-array of argv or to a +// statically allocated default value. In both cases the caller should /not/ +// free the memory. +// +// Returns true on success and false if there is a problem parsing options. +bool ParseOptions(const int argc, char* const argv[], bool* verbose, + SecurityContext* context, char* const* execv_args[]); +} // namespace shell_as + +#endif // SHELL_AS_COMMAND_LINE_H_ diff --git a/utils/shell-as/context.cpp b/utils/shell-as/context.cpp new file mode 100644 index 000000000..ea7979bab --- /dev/null +++ b/utils/shell-as/context.cpp @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "./context.h" + +#include <private/android_filesystem_config.h> // For AID_APP_START. +#include <stdio.h> +#include <stdlib.h> + +#include <iostream> +#include <string> + +#include "./string-utils.h" +#include "./test-app.h" + +namespace shell_as { + +namespace { + +bool ParseIdFromProcStatusLine(char* line, uid_t* id) { + // The user and group ID lines of the status file look like: + // + // Uid: <real> <effective> <saved> <filesystem> + // Gid: <real> <effective> <saved> <filesystem> + std::vector<uid_t> ids; + if (!SplitIdsAndSkip(line, "\t\n ", /*num_to_skip=*/1, &ids) || + ids.size() < 1) { + return false; + } + *id = ids[0]; + return true; +} + +bool ParseGroupsFromProcStatusLine(char* line, std::vector<gid_t>* ids) { + // The supplementary groups line of the status file looks like: + // + // Groups: <group1> <group2> <group3> ... + return SplitIdsAndSkip(line, "\t\n ", /*num_to_skip=*/1, ids); +} + +bool ParseProcStatusFile(const pid_t process_id, uid_t* real_user_id, + gid_t* real_group_id, + std::vector<gid_t>* supplementary_group_ids) { + std::string proc_status_path = + std::string("/proc/") + std::to_string(process_id) + "/status"; + FILE* status_file = fopen(proc_status_path.c_str(), "r"); + if (status_file == nullptr) { + std::cerr << "Unable to open '" << proc_status_path << "'" << std::endl; + } + bool parsed_user = false; + bool parsed_group = false; + bool parsed_supplementary_groups = false; + while (true) { + size_t line_length = 0; + char* line = nullptr; + if (getline(&line, &line_length, status_file) < 0) { + free(line); + break; + } + if (strncmp("Uid:", line, 4) == 0) { + parsed_user = ParseIdFromProcStatusLine(line, real_user_id); + } else if (strncmp("Gid:", line, 4) == 0) { + parsed_group = ParseIdFromProcStatusLine(line, real_group_id); + } else if (strncmp("Groups:", line, 7) == 0) { + parsed_supplementary_groups = + ParseGroupsFromProcStatusLine(line, supplementary_group_ids); + } + free(line); + } + fclose(status_file); + return parsed_user && parsed_group && parsed_supplementary_groups; +} + +} // namespace + +bool SecurityContextFromProcess(const pid_t process_id, + SecurityContext* context) { + char* selinux_context; + if (getpidcon(process_id, &selinux_context) != 0) { + std::cerr << "Unable to obtain SELinux context from process " << process_id + << std::endl; + return false; + } + + cap_t capabilities = cap_get_pid(process_id); + if (capabilities == nullptr) { + std::cerr << "Unable to obtain capability set from process " << process_id + << std::endl; + return false; + } + + uid_t user_id = 0; + gid_t group_id = 0; + std::vector<gid_t> supplementary_group_ids; + if (!ParseProcStatusFile(process_id, &user_id, &group_id, + &supplementary_group_ids)) { + std::cerr << "Unable to obtain user and group IDs from process " + << process_id << std::endl; + return false; + } + + context->selinux_context = selinux_context; + context->user_id = user_id; + context->group_id = group_id; + context->supplementary_group_ids = supplementary_group_ids; + context->capabilities = capabilities; + return true; +} + +bool SecurityContextFromTestApp(SecurityContext* context) { + pid_t test_app_pid = 0; + if (!SetupAndStartTestApp(&test_app_pid)) { + std::cerr << "Unable to install test app." << std::endl; + return false; + } + return SecurityContextFromProcess(test_app_pid, context); +} + +SeccompFilter SeccompFilterFromUserId(uid_t user_id) { + // Copied from: + // frameworks/base/core/jni/com_android_internal_os_Zygote.cpp + return user_id >= AID_APP_START ? kAppFilter : kSystemFilter; +} + +} // namespace shell_as diff --git a/utils/shell-as/context.h b/utils/shell-as/context.h new file mode 100644 index 000000000..17a8cca85 --- /dev/null +++ b/utils/shell-as/context.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SHELL_AS_CONTEXT_H_ +#define SHELL_AS_CONTEXT_H_ + +#include <selinux/selinux.h> +#include <sys/capability.h> + +#include <memory> +#include <optional> +#include <vector> + +namespace shell_as { + +// Enumeration of the possible seccomp filters that Android may apply to a +// process. +// +// This should be kept in sync with the policies defined in: +// bionic/libc/seccomp/include/seccomp_policy.h +enum SeccompFilter { + kAppFilter = 0, + kAppZygoteFilter = 1, + kSystemFilter = 2, +}; + +typedef struct SecurityContext { + std::optional<uid_t> user_id; + std::optional<gid_t> group_id; + std::optional<std::vector<gid_t>> supplementary_group_ids; + std::optional<char *> selinux_context; + std::optional<SeccompFilter> seccomp_filter; + std::optional<cap_t> capabilities; +} SecurityContext; + +// Infers the appropriate seccomp filter from a user ID. +// +// This mimics the behavior of the zygote process and provides a sane default +// method of picking a filter. However, it is not 100% accurate since it does +// not assign the app zygote filter and would not return an appropriate value +// for processes not started by the zygote. +SeccompFilter SeccompFilterFromUserId(uid_t user_id); + +// Derives a complete security context from a given process. +// +// If unable to determine any field of the context this method will return false +// and not modify the given context. +bool SecurityContextFromProcess(pid_t process_id, SecurityContext* context); + +// Derives a complete security context from the bundled test app. +// +// If unable to determine any field of the context this method will return false +// and not modify the given context. +bool SecurityContextFromTestApp(SecurityContext* context); + +} // namespace shell_as + +#endif // SHELL_AS_CONTEXT_H_ diff --git a/utils/shell-as/elf-utils.cpp b/utils/shell-as/elf-utils.cpp new file mode 100644 index 000000000..8a82555be --- /dev/null +++ b/utils/shell-as/elf-utils.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <elf.h> +#include <stdio.h> + +#include <iostream> +#include <string> + +#include "./elf.h" + +namespace shell_as { + +namespace { +// The base address of a PIE binary when loaded with ASLR disabled. +#if defined(__arm__) || defined(__aarch64__) +constexpr uint64_t k32BitImageBase = 0xAAAAA000; +constexpr uint64_t k64BitImageBase = 0x5555555000; +#else +constexpr uint64_t k32BitImageBase = 0x56555000; +constexpr uint64_t k64BitImageBase = 0x555555554000; +#endif +} // namespace + +bool GetElfEntryPoint(const pid_t process_id, uint64_t* entry_address, + bool* is_arm_mode) { + uint8_t elf_header_buffer[sizeof(Elf64_Ehdr)]; + std::string exe_path = "/proc/" + std::to_string(process_id) + "/exe"; + FILE* exe_file = fopen(exe_path.c_str(), "rb"); + if (exe_file == nullptr) { + std::cerr << "Unable to open executable of process " << process_id + << std::endl; + return false; + } + + int read_size = + fread(elf_header_buffer, sizeof(elf_header_buffer), 1, exe_file); + fclose(exe_file); + if (read_size <= 0) { + std::cerr << "Unable to read executable of process " << process_id + << std::endl; + return false; + } + + const Elf32_Ehdr* file_header_32 = (Elf32_Ehdr*)elf_header_buffer; + const Elf64_Ehdr* file_header_64 = (Elf64_Ehdr*)elf_header_buffer; + // The first handful of bytes of a header do not depend on whether the file is + // 32bit vs 64bit. + const bool is_pie_binary = file_header_32->e_type == ET_DYN; + + if (file_header_32->e_ident[EI_CLASS] == ELFCLASS32) { + *entry_address = + file_header_32->e_entry + (is_pie_binary ? k32BitImageBase : 0); + } else if (file_header_32->e_ident[EI_CLASS] == ELFCLASS64) { + *entry_address = + file_header_64->e_entry + (is_pie_binary ? k64BitImageBase : 0); + } else { + return false; + } + + *is_arm_mode = false; +#if defined(__arm__) + if ((*entry_address & 1) == 0) { + *is_arm_mode = true; + } + // The entry address for ARM Elf binaries is branched to using a BX + // instruction. The low bit of these instructions indicates the instruction + // set of the code that is being jumped to. A low bit of 1 indicates thumb + // mode while a low bit of 0 indicates ARM mode. + *entry_address &= ~1; +#endif + + return true; +} + +} // namespace shell_as diff --git a/utils/shell-as/elf-utils.h b/utils/shell-as/elf-utils.h new file mode 100644 index 000000000..eba40f303 --- /dev/null +++ b/utils/shell-as/elf-utils.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SHELL_AS_ELF_H_ +#define SHELL_AS_ELF_H_ + +#include <sys/types.h> + +namespace shell_as { + +// Sets entry_address to the process's entry point. +// +// This method assumes that PIE binaries are executing with ADDR_NO_RANDOMIZE. +// +// The is_arm_mode flag is set to true IFF the architecture is 32bit ARM and the +// expected instruction set for code located at the entry address is not-thumb. +// It is false for all other cases. +bool GetElfEntryPoint(const pid_t process_id, uint64_t* entry_address, + bool* is_arm_mode); +} // namespace shell_as + +#endif // SHELL_AS_ELF_H_ diff --git a/utils/shell-as/execute.cpp b/utils/shell-as/execute.cpp new file mode 100644 index 000000000..3ef529252 --- /dev/null +++ b/utils/shell-as/execute.cpp @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "./execute.h" + +#include <linux/securebits.h> +#include <linux/uio.h> +#include <seccomp_policy.h> +#include <sys/capability.h> +#include <sys/personality.h> +#include <sys/prctl.h> +#include <sys/ptrace.h> +#include <sys/wait.h> +#include <unistd.h> + +#include <iostream> +#include <memory> + +#include "./elf-utils.h" +#include "./registers.h" +#include "./shell-code.h" + +namespace shell_as { + +namespace { + +// Capabilities are implemented as a 64-bit bit-vector. Therefore the maximum +// number of capabilities supported by a kernel is 64. +constexpr cap_value_t kMaxCapabilities = 64; + +bool DropPreExecPrivileges(const shell_as::SecurityContext* context) { + // The ordering here is important: + // (1) The platform's seccomp filters disallow setresgiud, so it must come + // before the seccomp drop. + // (2) Adding seccomp filters must happen before setresuid because setresuid + // drops some capabilities which are required for seccomp. + if (context->group_id.has_value() && + setresgid(context->group_id.value(), context->group_id.value(), + context->group_id.value()) != 0) { + std::cerr << "Unable to set group id: " << context->group_id.value() + << std::endl; + return false; + } + if (context->supplementary_group_ids.has_value() && + setgroups(context->supplementary_group_ids.value().size(), + context->supplementary_group_ids.value().data()) != 0) { + std::cerr << "Unable to set supplementary groups." << std::endl; + return false; + } + + if (context->seccomp_filter.has_value()) { + switch (context->seccomp_filter.value()) { + case shell_as::kAppFilter: + set_app_seccomp_filter(); + break; + case shell_as::kAppZygoteFilter: + set_app_zygote_seccomp_filter(); + break; + case shell_as::kSystemFilter: + set_system_seccomp_filter(); + break; + } + } + + // This must be set prior to setresuid, otherwise that call will drop the + // permitted set of capabilities. + if (prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0) != 0) { + std::cerr << "Unable to set keep capabilities." << std::endl; + return false; + } + + if (context->user_id.has_value() && + setresuid(context->user_id.value(), context->user_id.value(), + context->user_id.value()) != 0) { + std::cerr << "Unable to set user id: " << context->user_id.value() + << std::endl; + return false; + } + + // Capabilities must be reacquired after setresuid since it still modifies + // capabilities, but it leaves the permitted set intact. + if (context->capabilities.has_value()) { + // The first step is to raise all the capabilities possible in all sets + // including the inheritable set. This defines the superset of possible + // capabilities that can be passed on after calling execve. + // + // The reason that all capabilities are raised in the inheritable set is due + // to a limitation of libcap. libcap may not contain a capability definition + // for all capabilities supported by the kernel. If this occurs, it will + // silently ignore requests to raise unknown capabilities via cap_set_flag. + // + // However, when parsing a cap_t from a text value, libcap will treat "all" + // as all possible 64 capability bits as set. + cap_t all_capabilities = cap_from_text("all+pie"); + if (cap_set_proc(all_capabilities) != 0) { + std::cerr << "Unable to raise inheritable capability set." << std::endl; + cap_free(all_capabilities); + return false; + } + cap_free(all_capabilities); + + // The second step is to raise the /desired/ capability subset in the + // ambient capability set. These are the capabilities that will actually be + // passed to the process after execve. + if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0) != 0) { + std::cerr << "Unable to clear ambient capabilities." << std::endl; + return false; + } + cap_t desired_capabilities = context->capabilities.value(); + for (cap_value_t cap = 0; cap < kMaxCapabilities; cap++) { + // Skip capability values not supported by the kernel. + if (!CAP_IS_SUPPORTED(cap)) { + continue; + } + cap_flag_value_t value = CAP_CLEAR; + if (cap_get_flag(desired_capabilities, cap, CAP_PERMITTED, &value) == 0 && + value == CAP_SET) { + if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap, 0, 0) != 0) { + std::cerr << "Unable to raise capability " << cap + << " in the ambient set." << std::endl; + return false; + } + } + } + + // The final step is to raise the SECBIT_NOROOT flag. The kernel has special + // case logic that treats root calling execve differently than other users. + // + // By default all bits in the permitted set prior to calling execve will be + // raised after calling execve. This would ignore the work above and result + // in the process to have all capabilities. + // + // Setting the SECBIT_NOROOT disables this special casing for root and + // causes the kernel to treat it as any other UID. + int64_t secure_bits = prctl(PR_GET_SECUREBITS, 0, 0, 0, 0); + if (secure_bits < 0 || + prctl(PR_SET_SECUREBITS, secure_bits | SECBIT_NOROOT, 0, 0, 0) != 0) { + std::cerr << "Unable to raise SECBIT_NOROOT." << std::endl; + return false; + } + } + return true; +} + +uint8_t ReadChildByte(const pid_t process, const uintptr_t address) { + uintptr_t data = ptrace(PTRACE_PEEKDATA, process, address, nullptr); + return ((uint8_t*)&data)[0]; +} + +void WriteChildByte(const pid_t process, const uintptr_t address, + const uint8_t value) { + // This is not the most efficient way to write data to a process. However, it + // reduces code complexity of handling different word sizes and reading and + // writing memory that is not a multiple of the native word size. + uintptr_t data = ptrace(PTRACE_PEEKDATA, process, address, nullptr); + ((uint8_t*)&data)[0] = value; + ptrace(PTRACE_POKEDATA, process, address, data); +} + +void ReadChildMemory(const pid_t process, uintptr_t process_address, + uint8_t* bytes, size_t byte_count) { + for (; byte_count != 0; byte_count--, bytes++, process_address++) { + *bytes = ReadChildByte(process, process_address); + } +} + +void WriteChildMemory(const pid_t process, uintptr_t process_address, + uint8_t const* bytes, size_t byte_count) { + for (; byte_count != 0; byte_count--, bytes++, process_address++) { + WriteChildByte(process, process_address, *bytes); + } +} + +// Executes shell code in a target process. +// +// The following assumptions are made: +// * The process is currently being ptraced and that the process has already +// stopped. +// * The shell code will raise SIGSTOP when it has finished as signal that +// control flow should be handed back to the original code. +// * The shell code only alters registers and pushes values onto the stack. +// +// Execution is performed by overwriting the memory under the current +// instruction pointer with the shell code. After the shell code signals +// completion the original register state and memory are restored. +// +// If the above assumptions are met, then this function will leave the process +// in a stopped state that is equivalent to the original state. +bool ExecuteShellCode(const pid_t process, const uint8_t* shell_code, + const size_t shell_code_size) { + REGISTER_STRUCT registers; + struct iovec registers_iovec; + registers_iovec.iov_base = ®isters; + registers_iovec.iov_len = sizeof(REGISTER_STRUCT); + ptrace(PTRACE_GETREGSET, process, 1, ®isters_iovec); + + std::unique_ptr<uint8_t[]> memory_backup(new uint8_t[shell_code_size]); + ReadChildMemory(process, PROGRAM_COUNTER(registers), memory_backup.get(), + shell_code_size); + WriteChildMemory(process, PROGRAM_COUNTER(registers), shell_code, + shell_code_size); + + // Execute the shell code and wait for the signal that it has finished. + ptrace(PTRACE_CONT, process, NULL, NULL); + int status; + waitpid(process, &status, 0); + if (status >> 8 != SIGSTOP) { + std::cerr << "Failed to execute SELinux shellcode." << std::endl; + return false; + } + + ptrace(PTRACE_SETREGSET, process, 1, ®isters_iovec); + WriteChildMemory(process, PROGRAM_COUNTER(registers), memory_backup.get(), + shell_code_size); + return true; +} + +bool SetProgramCounter(const pid_t process_id, uint64_t program_counter) { + REGISTER_STRUCT registers; + struct iovec registers_iovec; + registers_iovec.iov_base = ®isters; + registers_iovec.iov_len = sizeof(REGISTER_STRUCT); + if (ptrace(PTRACE_GETREGSET, process_id, 1, ®isters_iovec) != 0) { + return false; + } + PROGRAM_COUNTER(registers) = program_counter; + if ((ptrace(PTRACE_SETREGSET, process_id, 1, ®isters_iovec)) != 0) { + return false; + } + return true; +} + +bool StepToEntryPoint(const pid_t process_id) { + bool is_arm_mode; + uint64_t entry_address; + if (!GetElfEntryPoint(process_id, &entry_address, &is_arm_mode)) { + std::cerr << "Not able to determine Elf entry point." << std::endl; + return false; + } + if (is_arm_mode) { + // TODO(willcoster): If there is a need to handle ARM mode instructions in + // addition to thumb instructions update this with ARM mode shell code. + std::cerr << "Attempting to run an ARM-mode binary. " + << "shell-as currently only supports thumb-mode. " + << "Bug willcoster@ if you run into this error." << std::endl; + return false; + } + + int expected_signal = 0; + size_t trap_code_size = 0; + std::unique_ptr<uint8_t[]> trap_code = + GetTrapShellCode(&expected_signal, &trap_code_size); + std::unique_ptr<uint8_t[]> backup(new uint8_t[trap_code_size]); + + // Set a break point at the entry point declared by the Elf file. When a + // statically linked binary is executed this will be the first instruction + // executed. + // + // When a dynamically linked binary is executed, the dynamic linker is + // executed first. This brings .so files into memory and resolves shared + // symbols. Once this process is finished, it jumps to the entry point + // declared in the Elf file. + ReadChildMemory(process_id, entry_address, backup.get(), trap_code_size); + WriteChildMemory(process_id, entry_address, trap_code.get(), trap_code_size); + ptrace(PTRACE_CONT, process_id, NULL, NULL); + int status; + waitpid(process_id, &status, 0); + if (status >> 8 != expected_signal) { + std::cerr << "Program exited unexpectedly while stepping to entry point." + << std::endl; + std::cerr << "Expected status " << expected_signal << " but encountered " + << (status >> 8) << std::endl; + return false; + } + + if (!SetProgramCounter(process_id, entry_address)) { + return false; + } + WriteChildMemory(process_id, entry_address, backup.get(), trap_code_size); + return true; +} + +} // namespace + +bool ExecuteInContext(char* const executable_and_args[], + const shell_as::SecurityContext* context) { + // Getting an executable running in a lower privileged context is tricky with + // SELinux. The recommended approach in the documentation is to use setexeccon + // which sets the context on the next execve call. + // + // However, this doesn't work for unprivileged processes like untrusted apps + // in Android because they are not allowed to execute most binaries. + // + // To work around this, ptrace is used to inject shell code into the new + // process just after it has executed an execve syscall. This shell code then + // sets the desired SELinux context. + pid_t child = fork(); + if (child == 0) { + // Disabling ASLR makes it easier to determine the entry point of the target + // executable. + personality(ADDR_NO_RANDOMIZE); + + // Drop the privileges that can be dropped before executing the new binary + // and exit early if there is an issue. + if (!DropPreExecPrivileges(context)) { + exit(1); + } + + ptrace(PTRACE_TRACEME, 0, NULL, NULL); + raise(SIGSTOP); // Wait for the parent process to attach. + execv(executable_and_args[0], executable_and_args); + } else { + // Wait for the child to reach the SIGSTOP line above. + int status; + waitpid(child, &status, 0); + if ((status >> 8) != SIGSTOP) { + // If the first status is not SIGSTOP, then the child aborted early + // because it was not able to set the user and group IDs. + return false; + } + + // Break inside the child's execv call. + ptrace(PTRACE_SETOPTIONS, child, NULL, + PTRACE_O_TRACEEXEC | PTRACE_O_EXITKILL); + ptrace(PTRACE_CONT, child, NULL, NULL); + waitpid(child, &status, 0); + if (status >> 8 != (SIGTRAP | PTRACE_EVENT_EXEC << 8)) { + std::cerr << "Failed to execute " << executable_and_args[0] << std::endl; + return false; + } + + // Allow the dynamic linker to run before dropping to a lower SELinux + // context. This is required for executing in some very constrained domains + // like mediacodec. + // + // If the context was dropped before the dynamic linker runs, then when the + // linker attempts to read /proc/self/exe to determine dynamic symbol + // information, SELinux will kill the binary if the domain is not allowed to + // read the binary's executable file. + // + // This happens for example, when attempting to run any toybox binary (id, + // sh, etc) as mediacodec. + if (!StepToEntryPoint(child)) { + std::cerr << "Something bad happened stepping to the entry point." + << std::endl; + return false; + } + + // Run the SELinux shellcode in the child process before the child can + // execute any instructions in the newly loaded executable. + if (context->selinux_context.has_value()) { + size_t shell_code_size; + std::unique_ptr<uint8_t[]> shell_code = GetSELinuxShellCode( + context->selinux_context.value(), &shell_code_size); + bool success = ExecuteShellCode(child, shell_code.get(), shell_code_size); + if (!success) { + return false; + } + } + + // Resume and detach from the child now that the SELinux context has been + // updated. + ptrace(PTRACE_DETACH, child, NULL, NULL); + waitpid(child, nullptr, 0); + } + return true; +} + +} // namespace shell_as diff --git a/utils/shell-as/execute.h b/utils/shell-as/execute.h new file mode 100644 index 000000000..2e8f51189 --- /dev/null +++ b/utils/shell-as/execute.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SHELL_AS_EXECUTE_H_ +#define SHELL_AS_EXECUTE_H_ + +#include "context.h" + +namespace shell_as { + +// Executes a command in the given security context. +// +// The executable_and_args parameter must contain at least two values. The first +// value is the path to the executable to run and the last value must be null. +// Additional arguments are passed to the executable as command line options. +// +// Returns true if the executable was run and false otherwise. +bool ExecuteInContext(char* const executable_and_args[], + const SecurityContext* context); +} // namespace shell_as + +#endif // SHELL_AS_EXECUTE_H_ diff --git a/utils/shell-as/gen-manifest.sh b/utils/shell-as/gen-manifest.sh new file mode 100755 index 000000000..9dc4d1142 --- /dev/null +++ b/utils/shell-as/gen-manifest.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +# Copyright (C) 2023 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Generates an AndroidManifest.xml file from a template by replacing the line +# containing the substring, 'PERMISSIONS', with a list of permissions defined in +# another text file. + +set -e + +if [ "$#" != 3 ]; +then + echo "usage: gen-manifest.sh AndroidManifest.xml.template" \ + "permissions.txt AndroidManifest.xml" + exit 1 +fi + +readonly template="$1" +readonly permissions="$2" +readonly output="$3" + +echo "template = $1" + +# Print the XML template file before the line containing PERMISSIONS. +sed -e '/PERMISSIONS/,$d' "$template" > "$output" + +# Print the permissions formatted as XML. +sed -r 's!(.*)! <uses-permission android:name="\1"/>!g' "$permissions" >> "$output" + +# Print the XML template file after the line containing PERMISSIONS. +sed -e '1,/PERMISSIONS/d' "$template" >> "$output" diff --git a/utils/shell-as/registers.h b/utils/shell-as/registers.h new file mode 100644 index 000000000..6f7af6c54 --- /dev/null +++ b/utils/shell-as/registers.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SHELL_AS_REGISTERS_H_ +#define SHELL_AS_REGISTERS_H_ + +#if defined(__aarch64__) + +#define REGISTER_STRUCT struct user_pt_regs +#define PROGRAM_COUNTER(regs) (regs.pc) + +#elif defined(__i386__) + +#include "sys/user.h" +#define REGISTER_STRUCT struct user_regs_struct +#define PROGRAM_COUNTER(regs) (regs.eip) + +#elif defined(__x86_64__) + +#include "sys/user.h" +#define REGISTER_STRUCT struct user_regs_struct +#define PROGRAM_COUNTER(regs) (regs.rip) + +#elif defined(__arm__) + +#define REGISTER_STRUCT struct user_regs +#define PROGRAM_COUNTER(regs) (regs.ARM_pc) + +#endif + +#endif // SHELL_AS_REGISTERS_H_ diff --git a/utils/shell-as/shell-as-main.cpp b/utils/shell-as/shell-as-main.cpp new file mode 100644 index 000000000..880cf1c91 --- /dev/null +++ b/utils/shell-as/shell-as-main.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <iostream> +#include <memory> +#include <string> + +#include "./command-line.h" +#include "./context.h" +#include "./execute.h" + +int main(const int argc, char* const argv[]) { + bool verbose = false; + auto context = std::make_unique<shell_as::SecurityContext>(); + char* const* execute_arguments = nullptr; + if (!shell_as::ParseOptions(argc, argv, &verbose, context.get(), + &execute_arguments)) { + return 1; + } + + if (verbose) { + std::cerr << "Dropping privileges to:" << std::endl; + std::cerr << "\tuser ID = " + << (context->user_id.has_value() + ? std::to_string(context->user_id.value()) + : "<no value>") + << std::endl; + + std::cerr << "\tgroup ID = " + << (context->group_id.has_value() + ? std::to_string(context->group_id.value()) + : "<no value>") + << std::endl; + + std::cerr << "\tsupplementary group IDs = "; + if (!context->supplementary_group_ids.has_value()) { + std::cerr << "<no value>"; + } else { + for (auto& id : context->supplementary_group_ids.value()) { + std::cerr << id << " "; + } + } + std::cerr << std::endl; + + std::cerr << "\tSELinux = " + << (context->selinux_context.has_value() + ? context->selinux_context.value() + : "<no value>") + << std::endl; + + std::cerr << "\tseccomp = "; + if (!context->seccomp_filter.has_value()) { + std::cerr << "<no value>"; + } else { + switch (context->seccomp_filter.value()) { + case shell_as::kAppFilter: + std::cerr << "app"; + break; + case shell_as::kAppZygoteFilter: + std::cerr << "app-zygote"; + break; + case shell_as::kSystemFilter: + std::cerr << "system"; + break; + } + } + std::cerr << std::endl; + + std::cerr << "\tcapabilities = "; + if (!context->capabilities.has_value()) { + std::cerr << "<no value>"; + } else { + std::cerr << "'" << cap_to_text(context->capabilities.value(), nullptr) + << "'"; + } + std::cerr << std::endl; + } + + return !shell_as::ExecuteInContext(execute_arguments, context.get()); +} diff --git a/utils/shell-as/shell-as-test-app-key.pk8 b/utils/shell-as/shell-as-test-app-key.pk8 Binary files differnew file mode 100644 index 000000000..df9254543 --- /dev/null +++ b/utils/shell-as/shell-as-test-app-key.pk8 diff --git a/utils/shell-as/shell-as-test-app-key.x509.pem b/utils/shell-as/shell-as-test-app-key.x509.pem new file mode 100644 index 000000000..4e5efc980 --- /dev/null +++ b/utils/shell-as/shell-as-test-app-key.x509.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIECzCCAvOgAwIBAgIUNyI1+/ZDui4r+jp6uy/aVRBpeR0wDQYJKoZIhvcNAQEL +BQAwgZQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH +DA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRy +b2lkMRAwDgYDVQQDDAdBbmRyb2lkMSIwIAYJKoZIhvcNAQkBFhNhbmRyb2lkQGFu +ZHJvaWQuY29tMB4XDTE5MTIwNTIyNDEwMloXDTQ3MDQyMjIyNDEwMlowgZQxCzAJ +BgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFp +biBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRyb2lkMRAwDgYD +VQQDDAdBbmRyb2lkMSIwIAYJKoZIhvcNAQkBFhNhbmRyb2lkQGFuZHJvaWQuY29t +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyzHGxhfDk4VzImAGQyFV +5+tb7Dgl9TTg57t8/LwMQX4abjB9o6tPwtZl757m4oLP8HCpjbI/kX5Wk4hLmNQ/ +I4AHG+LhCJGlz3nqBAJJxYoM//+3tUSLrq0ypuHMXNDPI5HGgE3hhzZbA9iWuGNB +7in+bHuhPFUq8e5og6piy3s3f77GB8QXzJEKyO2FhQR1Do8t4UdRji7TWR+USHqw +WBj/CyrpLJMwbr4Mx4YRN0JXUlFX1X/66ENonX4QZXofeiWDv5qgwFbbzgu9FLFN +imDeeCzU0mtYEKQpmZOdEaWclkT8IzUPwPMdawEq3Wj8nutoma5CztYl+OO9BgJC +LQIDAQABo1MwUTAdBgNVHQ4EFgQUqyxwI0Khq+xKbEGG3NCpN01wsaMwHwYDVR0j +BBgwFoAUqyxwI0Khq+xKbEGG3NCpN01wsaMwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAQEAIx4k1g1jDQs3ekseXMvz0V+O9AArWOEmwkIcA6EISvfC +dJ0DpmgRbZyvi0FowzOGYIZJ0Uwh4uwxETTHBQkvKoFdByukaasfX0p8axYVslT1 +87RrQDSA8fDp9K7d4kG3iXX16H5WJ0O/sI3UkZevZzVjXcoqSHA2CltGZv/EXPAh +dwGL5OupiiJcCV4ISSgh9PHswH1tGASdg3nqFqQLZrCYZE3pyLdsiDTQADlBMpZ4 +dH7kbh8McSA/OM2Fp1y05oecYVzKOzJ/I4SLhbSGLRLHvSg9fNiJPoKm46leQtFV +OVtzzBt6TKITRIhA8VVo45U0gVGUwlj/4BCKQLsJpA== +-----END CERTIFICATE----- diff --git a/utils/shell-as/shell-code.cpp b/utils/shell-as/shell-code.cpp new file mode 100644 index 000000000..bdadf6c5a --- /dev/null +++ b/utils/shell-as/shell-code.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "./shell-code.h" + +#include <sys/mman.h> +#include <sys/types.h> + +#define PAGE_START(addr) ((uintptr_t)addr & ~(PAGE_SIZE - 1)) + +// Shell code that sets the SELinux context of the current process. +// +// The shell code expects a null-terminated SELinux context string to be placed +// immediately after it in memory. After the SELinux context has been changed +// the shell code will stop the current process with SIGSTOP. +// +// This shell code must be self-contained and position-independent. +extern "C" void __setcon_shell_code_start(); +extern "C" void __setcon_shell_code_end(); + +// Shell code that stops execution of the current process by raising a signal. +// The specific signal that is raised is given in __trap_shell_code_signal. +// +// This shell code can be used to inject break points into a traced process. +// +// The shell code must not modify any registers other than the program counter. +extern "C" void __trap_shell_code_start(); +extern "C" void __trap_shell_code_end(); +extern "C" int __trap_shell_code_signal; + +namespace shell_as { + +namespace { +void EnsureShellcodeReadable(void (*start)(), void (*end)()) { + mprotect((void*)PAGE_START(start), + PAGE_START(end) - PAGE_START(start) + PAGE_SIZE, + PROT_READ | PROT_EXEC); +} +} // namespace + +std::unique_ptr<uint8_t[]> GetSELinuxShellCode( + char* selinux_context, size_t* total_size) { + EnsureShellcodeReadable(&__setcon_shell_code_start, &__setcon_shell_code_end); + + size_t shell_code_size = (uintptr_t)&__setcon_shell_code_end - + (uintptr_t)&__setcon_shell_code_start; + size_t selinux_context_size = strlen(selinux_context) + 1 /* null byte */; + *total_size = shell_code_size + selinux_context_size; + + std::unique_ptr<uint8_t[]> shell_code(new uint8_t[*total_size]); + memcpy(shell_code.get(), (void*)&__setcon_shell_code_start, shell_code_size); + memcpy(shell_code.get() + shell_code_size, selinux_context, + selinux_context_size); + return shell_code; +} + +std::unique_ptr<uint8_t[]> GetTrapShellCode(int* expected_signal, + size_t* total_size) { + EnsureShellcodeReadable(&__trap_shell_code_start, &__trap_shell_code_end); + + *expected_signal = __trap_shell_code_signal; + + *total_size = + (uintptr_t)&__trap_shell_code_end - (uintptr_t)&__trap_shell_code_start; + std::unique_ptr<uint8_t[]> shell_code(new uint8_t[*total_size]); + memcpy(shell_code.get(), (void*)&__trap_shell_code_start, *total_size); + return shell_code; +} +} // namespace shell_as diff --git a/utils/shell-as/shell-code.h b/utils/shell-as/shell-code.h new file mode 100644 index 000000000..9c88d165a --- /dev/null +++ b/utils/shell-as/shell-code.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SHELL_AS_SHELL_CODE_H_ +#define SHELL_AS_SHELL_CODE_H_ + +#include <selinux/selinux.h> +#include <sys/types.h> + +#include <memory> + +#include "context.h" + +namespace shell_as { + +// Returns shell code that when executed will set the current process's SElinux +// context to the given value and then SIGSTOP itself. +std::unique_ptr<uint8_t[]> GetSELinuxShellCode( + char* selinux_context, size_t* total_size); + +// Returns shell code that when executed will halt the current process and raise +// a signal. The specific signal is returned in the expected_signal argument. +std::unique_ptr<uint8_t[]> GetTrapShellCode(int* expected_signal, + size_t* total_size); +} // namespace shell_as + +#endif // SHELL_AS_SHELL_CODE_H_ diff --git a/utils/shell-as/shell-code/constants-arm.S b/utils/shell-as/shell-code/constants-arm.S new file mode 100644 index 000000000..10db630d9 --- /dev/null +++ b/utils/shell-as/shell-code/constants-arm.S @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Arm specific constants. + +.equ SYS_OPEN, 0x000005 +.equ SYS_CLOSE, 0x000006 +.equ SYS_WRITE, 0x000004 +.equ SYS_KILL, 0x000025 +.equ SYS_GETPID, 0x000014 +.equ SYS_MPROTECT, 0x00007d diff --git a/utils/shell-as/shell-code/constants-arm64.S b/utils/shell-as/shell-code/constants-arm64.S new file mode 100644 index 000000000..a9dec758b --- /dev/null +++ b/utils/shell-as/shell-code/constants-arm64.S @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Arm64 specific constants. + +.equ SYS_OPENAT, 0x38 +.equ SYS_CLOSE, 0x39 +.equ SYS_WRITE, 0x40 +.equ SYS_KILL, 0x81 +.equ SYS_GETPID, 0xAC +.equ SYS_MPROTECT, 0xE2 diff --git a/utils/shell-as/shell-code/constants-x86.S b/utils/shell-as/shell-code/constants-x86.S new file mode 100644 index 000000000..afa9d1472 --- /dev/null +++ b/utils/shell-as/shell-code/constants-x86.S @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// x86 specific constants. + +.equ SYS_WRITE, 0x04 +.equ SYS_OPEN, 0x05 +.equ SYS_CLOSE, 0x06 +.equ SYS_GETPID, 0x14 +.equ SYS_KILL, 0x25 +.equ SYS_MPROTECT, 0x7d diff --git a/utils/shell-as/shell-code/constants-x86_64.S b/utils/shell-as/shell-code/constants-x86_64.S new file mode 100644 index 000000000..0bf95cc75 --- /dev/null +++ b/utils/shell-as/shell-code/constants-x86_64.S @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// x86-64 specific constants. + +.equ SYS_WRITE, 0x01 +.equ SYS_OPEN, 0x02 +.equ SYS_CLOSE, 0x03 +.equ SYS_GETPID, 0x27 +.equ SYS_KILL, 0x3e +.equ SYS_MPROTECT, 0x0a diff --git a/utils/shell-as/shell-code/constants.S b/utils/shell-as/shell-code/constants.S new file mode 100644 index 000000000..9e2a238d5 --- /dev/null +++ b/utils/shell-as/shell-code/constants.S @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Architecture independent constants. + +.equ AT_FDCWD, -100 +.equ O_WRONLY, 1 + +// Possible memory access modes for mprotect. +.equ PROT_NONE, 0 +.equ PROT_READ, 1 +.equ PROT_WRITE, 2 +.equ PROT_EXEC, 4 + +.equ SIGILL, 4 +.equ SIGTRAP, 5 +.equ SIGSTOP, 19 diff --git a/utils/shell-as/shell-code/selinux-arm.S b/utils/shell-as/shell-code/selinux-arm.S new file mode 100644 index 000000000..0c9480f9b --- /dev/null +++ b/utils/shell-as/shell-code/selinux-arm.S @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Shell code that sets the current SELinux context to a given string. +// +// The desired SELinux context is appended to the payload as a null-terminated +// string. +// +// After the SELinux context has been updated the current process will raise +// SIGSTOP. + +#include "./shell-code/constants.S" +#include "./shell-code/constants-arm.S" + +.thumb + +.globl __setcon_shell_code_start +.globl __setcon_shell_code_end + +__setcon_shell_code_start: + // Ensure that the context and SELinux /proc file are readable. This assumes + // that the max length of these two strings is shorter than 0x1000. + // + // mprotect(context & ~0xFFF, 0x2000, PROT_READ | PROT_EXEC) + mov r7, SYS_MPROTECT + adr r0, context + movw r2, 0xF000 + movt r2, 0xFFFF + and r0, r0, r2 + mov r1, 0x2000 + mov r2, (PROT_READ | PROT_EXEC) + swi 0 + + // r10 = open("/proc/self/attr/current", O_WRONLY, O_WRONLY) + mov r7, SYS_OPEN + adr r0, selinux_proc_file + mov r1, O_WRONLY + mov r2, O_WRONLY + swi 0 + mov r10, r0 + + // r11 = strlen(context) + mov r11, 0 + adr r0, context +strlen_start: + ldrb r1, [r0, r11] + cmp r1, 0 + beq strlen_done + add r11, r11, 1 + b strlen_start +strlen_done: + + // write(r10, context, r11) + mov r7, SYS_WRITE + mov r0, r10 + adr r1, context + mov r2, r11 + swi 0 + + // close(r10) + mov r7, SYS_CLOSE + mov r0, r10 + swi 0 + + // r0 = getpid() + mov r7, SYS_GETPID + swi 0 + + // kill(r0, SIGSTOP) + mov r7, SYS_KILL + mov r1, SIGSTOP + swi 0 + +selinux_proc_file: + .asciz "/proc/thread-self/attr/current" + +context: +__setcon_shell_code_end: diff --git a/utils/shell-as/shell-code/selinux-arm64.S b/utils/shell-as/shell-code/selinux-arm64.S new file mode 100644 index 000000000..4e8c49296 --- /dev/null +++ b/utils/shell-as/shell-code/selinux-arm64.S @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Shell code that sets the current SELinux context to a given string. +// +// The desired SELinux context is appended to the payload as a null-terminated +// string. +// +// After the SELinux context has been updated the current process will raise +// SIGSTOP. + +#include "./shell-code/constants.S" +#include "./shell-code/constants-arm64.S" + +.globl __setcon_shell_code_start +.globl __setcon_shell_code_end + +__setcon_shell_code_start: + // Ensure that the context and SELinux /proc file are readable. This assumes + // that the max length of these two strings is shorter than 0x1000. + // + // mprotect(context & ~0xFFF, 0x2000, PROT_READ | PROT_EXEC) + mov x8, SYS_MPROTECT + adr X0, __setcon_shell_code_end + and x0, x0, ~0xFFF + mov x1, 0x2000 + mov x2, (PROT_READ | PROT_EXEC) + svc 0 + + // x10 = openat(AT_FDCWD, "/proc/self/attr/current", O_WRONLY, O_WRONLY) + mov x8, SYS_OPENAT + mov x0, AT_FDCWD + adr x1, selinux_proc_file + mov x2, O_WRONLY + mov x3, O_WRONLY + svc 0 + mov x10, x0 + + // x11 = strlen(context) + mov x11, 0 + adr x0, context +strlen_start: + ldrb w1, [x0, x11] + cmp w1, 0 + b.eq strlen_done + add x11, x11, 1 + b strlen_start +strlen_done: + + // write(x10, context, x11) + mov x8, SYS_WRITE + mov x0, x10 + adr x1, context + mov x2, x11 + svc 0 + + // close(x10) + mov x8, SYS_CLOSE + mov x0, x10 + svc 0 + + // x0 = getpid() + mov x8, SYS_GETPID + svc 0 + + // kill(x0, SIGSTOP) + mov x8, SYS_KILL + mov x1, SIGSTOP + svc 0 + +selinux_proc_file: + .asciz "/proc/thread-self/attr/current" + +context: +__setcon_shell_code_end: diff --git a/utils/shell-as/shell-code/selinux-x86.S b/utils/shell-as/shell-code/selinux-x86.S new file mode 100644 index 000000000..81c150f13 --- /dev/null +++ b/utils/shell-as/shell-code/selinux-x86.S @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Shell code that sets the current SELinux context to a given string. +// +// The desired SELinux context is appended to the payload as a null-terminated +// string. +// +// After the SELinux context has been updated the current process will raise +// SIGSTOP. + +#include "./shell-code/constants.S" +#include "./shell-code/constants-x86.S" + +.globl __setcon_shell_code_start +.globl __setcon_shell_code_end + +__setcon_shell_code_start: + + // x86 does not have RIP relative addressing. To work around this, relative + // calls are used to obtain the runtime address of a label. Once the location + // of one label is known, other labels can be addressed relative to the known + // label. + call constant_relative_address +constant_relative_address: + pop %esi + + // Ensure that the context and SELinux /proc file are readable. This assumes + // that the max length of these two strings is shorter than 0x1000. + // + // mprotect(context & ~0xFFF, 0x2000, PROT_READ | PROT_EXEC) + mov $SYS_MPROTECT, %eax + mov $~0xFFF, %ebx + and %esi, %ebx + mov $0x2000, %ecx + mov $(PROT_READ | PROT_EXEC), %edx + int $0x80 + + // ebx = open("/proc/self/attr/current", O_WRONLY, O_WRONLY) + mov $SYS_OPEN, %eax + lea (selinux_proc_file - constant_relative_address)(%esi), %ebx + mov $O_WRONLY, %ecx + mov $O_WRONLY, %edx + int $0x80 + mov %eax, %ebx + + // write(ebx, context, strlen(context)) + xor %edx, %edx + leal (context - constant_relative_address)(%esi), %ecx +strlen_start: + movb (%ecx, %edx), %al + test %al, %al + jz strlen_done + inc %edx + jmp strlen_start +strlen_done: + mov $SYS_WRITE, %eax + int $0x80 + + // close(ebx) + mov $SYS_CLOSE, %eax + int $0x80 + + // ebx = getpid() + mov $SYS_GETPID, %eax + int $0x80 + mov %eax, %ebx + + // kill(ebx, SIGSTOP) + mov $SYS_KILL, %eax + mov $SIGSTOP, %ecx + int $0x80 + +selinux_proc_file: + .asciz "/proc/self/attr/current" + +context: +__setcon_shell_code_end: diff --git a/utils/shell-as/shell-code/selinux-x86_64.S b/utils/shell-as/shell-code/selinux-x86_64.S new file mode 100644 index 000000000..94fc876c6 --- /dev/null +++ b/utils/shell-as/shell-code/selinux-x86_64.S @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Shell code that sets the current SELinux context to a given string. +// +// The desired SELinux context is appended to the payload as a null-terminated +// string. +// +// After the SELinux context has been updated the current process will raise +// SIGSTOP. + +#include "./shell-code/constants.S" +#include "./shell-code/constants-x86_64.S" + +.globl __setcon_shell_code_start +.globl __setcon_shell_code_end + +__setcon_shell_code_start: + + // Ensure that the context and SELinux /proc file are readable. This assumes + // that the max length of these two strings is shorter than 0x1000. + // + // mprotect(context & ~0xFFF, 0x2000, PROT_READ | PROT_EXEC) + mov $SYS_MPROTECT, %rax + lea context(%rip), %rdi + and $~0xFFF, %rdi + mov $0x2000, %rsi + mov $(PROT_READ | PROT_EXEC), %rdx + syscall + + // rdi = open("/proc/self/attr/current", O_WRONLY, O_WRONLY) + mov $SYS_OPEN, %eax + lea selinux_proc_file(%rip), %rdi + mov $O_WRONLY, %rsi + mov $O_WRONLY, %rdx + syscall + mov %rax, %rdi + + // write(rdi, context, strlen(context)) + xor %rdx, %rdx + lea context(%rip), %rsi +strlen_start: + movb (%rsi, %rdx), %al + test %al, %al + jz strlen_done + inc %rdx + jmp strlen_start +strlen_done: + mov $SYS_WRITE, %rax + syscall + + // close(rdi) + mov $SYS_CLOSE, %rax + syscall + + // rdi = getpid() + mov $SYS_GETPID, %rax + syscall + mov %rax, %rdi + + // kill(rdi, SIGSTOP) + mov $SYS_KILL, %rax + mov $SIGSTOP, %rsi + syscall + +selinux_proc_file: + .asciz "/proc/self/attr/current" + +context: +__setcon_shell_code_end: diff --git a/utils/shell-as/shell-code/trap-arm.S b/utils/shell-as/shell-code/trap-arm.S new file mode 100644 index 000000000..8bb347411 --- /dev/null +++ b/utils/shell-as/shell-code/trap-arm.S @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "./shell-code/constants.S" + +.thumb + +.globl __trap_shell_code_start +.globl __trap_shell_code_end +.globl __trap_shell_code_signal + +__trap_shell_code_start: +bkpt +__trap_shell_code_end: + +__trap_shell_code_signal: +.int SIGTRAP diff --git a/utils/shell-as/shell-code/trap-arm64.S b/utils/shell-as/shell-code/trap-arm64.S new file mode 100644 index 000000000..90063ffee --- /dev/null +++ b/utils/shell-as/shell-code/trap-arm64.S @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "./shell-code/constants.S" + +.globl __trap_shell_code_start +.globl __trap_shell_code_end +.globl __trap_shell_code_signal + +__trap_shell_code_start: +hlt 0 +__trap_shell_code_end: + +__trap_shell_code_signal: +.int SIGILL diff --git a/utils/shell-as/shell-code/trap-x86.S b/utils/shell-as/shell-code/trap-x86.S new file mode 100644 index 000000000..1669bb8ee --- /dev/null +++ b/utils/shell-as/shell-code/trap-x86.S @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "./shell-code/constants.S" + +.globl __trap_shell_code_start +.globl __trap_shell_code_end +.globl __trap_shell_code_signal + +__trap_shell_code_start: +int $0x03 +__trap_shell_code_end: + +__trap_shell_code_signal: +.int SIGTRAP diff --git a/utils/shell-as/shell-code/trap-x86_64.S b/utils/shell-as/shell-code/trap-x86_64.S new file mode 100644 index 000000000..1669bb8ee --- /dev/null +++ b/utils/shell-as/shell-code/trap-x86_64.S @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "./shell-code/constants.S" + +.globl __trap_shell_code_start +.globl __trap_shell_code_end +.globl __trap_shell_code_signal + +__trap_shell_code_start: +int $0x03 +__trap_shell_code_end: + +__trap_shell_code_signal: +.int SIGTRAP diff --git a/utils/shell-as/string-utils.cpp b/utils/shell-as/string-utils.cpp new file mode 100644 index 000000000..8977f73e0 --- /dev/null +++ b/utils/shell-as/string-utils.cpp @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "./string-utils.h" + +#include <errno.h> +#include <stdlib.h> +#include <string.h> + +namespace shell_as { + +bool StringToUInt32(const char* s, uint32_t* i) { + uint64_t value = 0; + if (!StringToUInt64(s, &value)) { + return false; + } + if (value > UINT_MAX) { + return false; + } + *i = value; + return true; +} + +bool StringToUInt64(const char* s, uint64_t* i) { + char* endptr = nullptr; + // Reset errno to a non-error value since strtoul does not clear errno. + errno = 0; + *i = strtoul(s, &endptr, 10); + // strtoul will return 0 if the value cannot be parsed as an unsigned long. If + // this occurs, ensure that the ID actually was zero. This is done by ensuring + // that the end pointer was advanced and that it now points to the end of the + // string (a null byte). + return errno == 0 && (*i != 0 || (endptr != s && *endptr == '\0')); +} + +bool SplitIdsAndSkip(char* line, const char* separators, int num_to_skip, + std::vector<uid_t>* ids) { + if (line == nullptr) { + return false; + } + + ids->clear(); + for (char* id_string = strtok(line, separators); id_string != nullptr; + id_string = strtok(nullptr, separators)) { + if (num_to_skip > 0) { + num_to_skip--; + continue; + } + + gid_t id; + if (!StringToUInt32(id_string, &id)) { + return false; + } + ids->push_back(id); + } + return true; +} + +} // namespace shell_as diff --git a/utils/shell-as/string-utils.h b/utils/shell-as/string-utils.h new file mode 100644 index 000000000..f4910894a --- /dev/null +++ b/utils/shell-as/string-utils.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SHELL_AS_STRING_UTILS_H_ +#define SHELL_AS_STRING_UTILS_H_ + +#include <unistd.h> + +#include <vector> + +namespace shell_as { + +// Parses a string into an unsigned 32bit int value. Returns true on success and +// false otherwise. +bool StringToUInt32(const char* s, uint32_t* i); + +// Parses a string into a unsigned 64bit int value. Returns true on success and +// false otherwise. +bool StringToUInt64(const char* s, uint64_t* i); + +// Splits a line of uid_t/guid_t values by a given separator and returns the +// integer values in a vector. +// +// The separators string may contain multiple characters and is treated as a set +// of possible separating characters. +// +// If num_to_skip is non-zero, then that many entries will be skipped after +// splitting the line and before parsing the values as integers. This is useful +// if the line has a prefix such as "Gid: 1 2 3 4". +// +// Returns true on success and false otherwise. +bool SplitIdsAndSkip(char* line, const char* separators, int num_to_skip, + std::vector<uid_t>* ids); + +} // namespace shell_as + +#endif // SHELL_AS_STRING_UTILS_H_ diff --git a/utils/shell-as/test-app.cpp b/utils/shell-as/test-app.cpp new file mode 100644 index 000000000..84fedcab6 --- /dev/null +++ b/utils/shell-as/test-app.cpp @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "./test-app.h" + +#include <fcntl.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> + +#include <iostream> +#include <string> + +#include "./string-utils.h" + +namespace shell_as { + +// Returns a pointer to bytes of the test app APK along with the length in bytes +// of the APK. +// +// This function is defined by the shell-as-test-app-apk-cpp genrule. +void GetTestApk(uint8_t **apk, size_t *length); + +namespace { + +// The staging path for the test app APK. +const char kTestAppApkStagingPath[] = "/data/local/tmp/shell-as-test-app.apk"; + +// Writes the test app to a staging location and then installs the APK via the +// 'pm' utility. The app is granted runtime permissions on installation. Returns +// true if the app is installed successfully. +bool InstallTestApp() { + uint8_t *apk = nullptr; + size_t apk_size = 0; + GetTestApk(&apk, &apk_size); + + int staging_file = open(kTestAppApkStagingPath, O_WRONLY | O_CREAT | O_TRUNC, + S_IRUSR | S_IWUSR); + if (staging_file == -1) { + std::cerr << "Unable to open staging APK path." << std::endl; + return false; + } + + size_t bytes_written = write(staging_file, apk, apk_size); + close(staging_file); + if (bytes_written != apk_size) { + std::cerr << "Unable to write entire test app APK." << std::endl; + return false; + } + + const char cmd_template[] = "pm install -g %s > /dev/null 2> /dev/null"; + char system_cmd[sizeof(cmd_template) + sizeof(kTestAppApkStagingPath) + 1] = + {}; + sprintf(system_cmd, cmd_template, kTestAppApkStagingPath); + return system(system_cmd) == 0; +} + +// Uninstalls the test app if it is installed. This method is a no-op if the app +// is not installed. +void UninstallTestApp() { + system( + "pm uninstall com.android.google.tools.security.shell_as" + " > /dev/null 2> /dev/null"); +} + +// Starts the main activity of the test app. This is necessary as some aspects +// of the security context can only be inferred from a running process. +bool StartTestApp() { + return system( + "am start-activity " + "com.android.google.tools.security.shell_as/" + ".MainActivity" + " > /dev/null 2> /dev/null") == 0; +} + +// Obtain the process ID of the test app and returns true if it is running. +// Returns false otherwise. +bool GetTestAppProcessId(pid_t *test_app_pid) { + FILE *pgrep = popen( + "pgrep -f " + "com.android.google.tools.security.shell_as", + "r"); + if (!pgrep) { + std::cerr << "Unable to execute pgrep." << std::endl; + return false; + } + + char pgrep_output[128]; + memset(pgrep_output, 0, sizeof(pgrep_output)); + int bytes_read = fread(pgrep_output, 1, sizeof(pgrep_output) - 1, pgrep); + pclose(pgrep); + if (bytes_read <= 0) { + // Unable to find the process. This may happen if the app is still starting + // up. + return false; + } + return StringToUInt32(pgrep_output, (uint32_t *)test_app_pid); +} +} // namespace + +bool SetupAndStartTestApp(pid_t *test_app_pid) { + UninstallTestApp(); + + if (!InstallTestApp()) { + std::cerr << "Unable to install test app." << std::endl; + return false; + } + + if (!StartTestApp()) { + std::cerr << "Unable to start and obtain test app PID." << std::endl; + return false; + } + + for (int i = 0; i < 5; i++) { + if (GetTestAppProcessId(test_app_pid)) { + return true; + } + sleep(1); + } + return false; +} +} // namespace shell_as diff --git a/utils/shell-as/test-app.h b/utils/shell-as/test-app.h new file mode 100644 index 000000000..866bbfb33 --- /dev/null +++ b/utils/shell-as/test-app.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SHELL_AS_TEST_APP_H_ +#define SHELL_AS_TEST_APP_H_ + +#include <sys/types.h> + +namespace shell_as { + +// Installs and launches the embedded shell-as test app. The test app requests +// and is granted all non-system permissions defined by the OS. The test_app_pid +// parameter is set to the process ID of the running test app. Returns true if +// successful. +bool SetupAndStartTestApp(pid_t *test_app_pid); +} + +#endif // SHELL_AS_TEST_APP_H_ |