diff --git a/Android.mk b/Android.mk index b23b09b..7bd51bb 100644 --- a/Android.mk +++ b/Android.mk @@ -2,12 +2,14 @@ LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := su -LOCAL_SRC_FILES := su.c activity.cpp +LOCAL_SRC_FILES := su.c db.c activity.c utils.c - -LOCAL_C_INCLUDES += external/sqlite/dist -LOCAL_SHARED_LIBRARIES := liblog libsqlite libandroid_runtime +LOCAL_STATIC_LIBRARIES := \ + liblog \ + libc \ LOCAL_MODULE_PATH := $(TARGET_OUT_OPTIONAL_EXECUTABLES) +LOCAL_MODULE_TAGS := eng debug +LOCAL_FORCE_STATIC_EXECUTABLE := true -include $(BUILD_EXECUTABLE) +include $(BUILD_EXECUTABLE) \ No newline at end of file diff --git a/README b/README index f1bc7a2..27d8f58 100644 --- a/README +++ b/README @@ -17,3 +17,4 @@ Branches for this repo support the following Android versions: Branches marked with '-dev' are in work and should probably not be used. I will push to those channels as I develop new versions so they may be broken from time to time. * I have yet to personally test superuser on any honeycomb devices, therefore I cannot guarantee it's compatibility +Run diff --git a/activity.c b/activity.c new file mode 100644 index 0000000..47fae4d --- /dev/null +++ b/activity.c @@ -0,0 +1,78 @@ +/* +** Copyright 2010, Adam Shanks (@ChainsDD) +** Copyright 2008, Zinx Verituse (@zinxv) +** +** 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 +#include +#include + +#include "su.h" + +int send_intent(const struct su_context *ctx, + const char *socket_path, int allow, const char *action) +{ + char command[PATH_MAX]; + + sprintf(command, "/system/bin/am broadcast -a '%s' --es socket '%s' --ei caller_uid '%d' --ei allow '%d' --ei version_code '%d' > /dev/null", + action, socket_path, ctx->from.uid, allow, VERSION_CODE); + + // before sending the intent, make sure the (uid and euid) and (gid and egid) match, + // otherwise LD_LIBRARY_PATH is wiped in Android 4.0+. + // Also, sanitize all secure environment variables (from linker_environ.c in linker). + + /* The same list than GLibc at this point */ + static const char* const unsec_vars[] = { + "GCONV_PATH", + "GETCONF_DIR", + "HOSTALIASES", + "LD_AUDIT", + "LD_DEBUG", + "LD_DEBUG_OUTPUT", + "LD_DYNAMIC_WEAK", + "LD_LIBRARY_PATH", + "LD_ORIGIN_PATH", + "LD_PRELOAD", + "LD_PROFILE", + "LD_SHOW_AUXV", + "LD_USE_LOAD_BIAS", + "LOCALDOMAIN", + "LOCPATH", + "MALLOC_TRACE", + "MALLOC_CHECK_", + "NIS_PATH", + "NLSPATH", + "RESOLV_HOST_CONF", + "RES_OPTIONS", + "TMPDIR", + "TZDIR", + "LD_AOUT_LIBRARY_PATH", + "LD_AOUT_PRELOAD", + // not listed in linker, used due to system() call + "IFS", + }; + const char* const* cp = unsec_vars; + const char* const* endp = cp + sizeof(unsec_vars)/sizeof(unsec_vars[0]); + while (cp < endp) { + unsetenv(*cp); + cp++; + } + + // sane value so "am" works + setenv("LD_LIBRARY_PATH", "/vendor/lib:/system/lib", 1); + setegid(getgid()); + seteuid(getuid()); + return system(command); +} diff --git a/activity.cpp b/activity.cpp deleted file mode 100644 index 4b21abd..0000000 --- a/activity.cpp +++ /dev/null @@ -1,113 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -extern "C" { -#include "su.h" -#include -#include -} - -using namespace android; - -static const int BROADCAST_INTENT_TRANSACTION = IBinder::FIRST_CALL_TRANSACTION + 13; - -static const int NULL_TYPE_ID = 0; - -static const int VAL_STRING = 0; -static const int VAL_INTEGER = 1; - -static const int START_SUCCESS = 0; - -int send_intent(struct su_initiator *from, struct su_request *to, const char *socket_path, int type) -{ - char sdk_version_prop[PROPERTY_VALUE_MAX] = "0"; - property_get("ro.build.version.sdk", sdk_version_prop, "0"); - - int sdk_version = atoi(sdk_version_prop); - - sp sm = defaultServiceManager(); - sp am = sm->checkService(String16("activity")); - assert(am != NULL); - - Parcel data, reply; - data.writeInterfaceToken(String16("android.app.IActivityManager")); - - data.writeStrongBinder(NULL); /* caller */ - - /* intent */ - if (type == 0) { - data.writeString16(String16("com.noshufou.android.su.REQUEST")); /* action */ - } else { - data.writeString16(String16("com.noshufou.android.su.NOTIFICATION")); /* action */ - } - data.writeInt32(NULL_TYPE_ID); /* Uri - data */ - data.writeString16(NULL, 0); /* type */ - data.writeInt32(0); /* flags */ - if (sdk_version >= 4) { - // added in donut - data.writeString16(NULL, 0); /* package name - DONUT ONLY, NOT IN CUPCAKE. */ - } - data.writeString16(NULL, 0); /* ComponentName - package */ - data.writeInt32(0); /* Categories - size */ - if (sdk_version >= 7) { - // added in eclair rev 7 - data.writeInt32(0); - } - { /* Extras */ - data.writeInt32(-1); /* dummy, will hold length */ - int oldPos = data.dataPosition(); - data.writeInt32(0x4C444E42); // 'B' 'N' 'D' 'L' - { /* writeMapInternal */ - data.writeInt32(4); /* writeMapInternal - size */ - - data.writeInt32(VAL_STRING); - data.writeString16(String16("caller_uid")); - data.writeInt32(VAL_INTEGER); - data.writeInt32(from->uid); - - data.writeInt32(VAL_STRING); - data.writeString16(String16("desired_uid")); - data.writeInt32(VAL_INTEGER); - data.writeInt32(to->uid); - - data.writeInt32(VAL_STRING); - data.writeString16(String16("desired_cmd")); - data.writeInt32(VAL_STRING); - data.writeString16(String16(to->command)); - - data.writeInt32(VAL_STRING); - data.writeString16(String16("socket")); - data.writeInt32(VAL_STRING); - data.writeString16(String16(socket_path)); - } - int newPos = data.dataPosition(); - data.setDataPosition(oldPos - 4); - data.writeInt32(newPos - oldPos); /* length */ - data.setDataPosition(newPos); - } - - data.writeString16(NULL, 0); /* resolvedType */ - - data.writeInt32(-1); /* Not sure what this is for, but it prevents a warning */ - - data.writeStrongBinder(NULL); /* resultTo */ - data.writeInt32(-1); /* resultCode */ - data.writeString16(NULL, 0); /* resultData */ - - data.writeInt32(-1); /* resultExtras */ - - data.writeString16(String16("com.noshufou.android.su.RESPOND")); /* perm */ - data.writeInt32(0); /* serialized */ - data.writeInt32(0); /* sticky */ - data.writeInt32(-1); - - status_t ret = am->transact(BROADCAST_INTENT_TRANSACTION, data, &reply); - if (ret < START_SUCCESS) return -1; - - return 0; -} diff --git a/db.c b/db.c new file mode 100644 index 0000000..52f9673 --- /dev/null +++ b/db.c @@ -0,0 +1,58 @@ +/* +** Copyright 2010, Adam Shanks (@ChainsDD) +** +** 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 +#include +#include +#include + +#include "su.h" + +int database_check(const struct su_context *ctx) +{ + FILE *fp; + char allow = '-'; + char *filename = malloc(snprintf(NULL, 0, "%s/%u-%u", REQUESTOR_STORED_PATH, ctx->from.uid, ctx->to.uid) + 1); + sprintf(filename, "%s/%u-%u", REQUESTOR_STORED_PATH, ctx->from.uid, ctx->to.uid); + if ((fp = fopen(filename, "r"))) { + LOGD("Found file"); + char cmd[PATH_MAX]; + fgets(cmd, sizeof(cmd), fp); + int last = strlen(cmd) - 1; + LOGD("this is the last character %u of the string", cmd[5]); + if (cmd[last] == '\n') { + cmd[last] = '\0'; + } + LOGD("Comparing %c %s, %u to %s", cmd[last - 2], cmd, last, get_command(&ctx->to)); + if (strcmp(cmd, get_command(&ctx->to)) == 0) { + allow = fgetc(fp); + } + fclose(fp); + } else if ((fp = fopen(REQUESTOR_STORED_DEFAULT, "r"))) { + LOGD("Using default"); + allow = fgetc(fp); + fclose(fp); + } + free(filename); + + if (allow == '1') { + return DB_ALLOW; + } else if (allow == '0') { + return DB_DENY; + } else { + return DB_INTERACTIVE; + } +} diff --git a/su.c b/su.c index c498f94..d36eaed 100644 --- a/su.c +++ b/su.c @@ -1,4 +1,19 @@ -#define LOG_TAG "su" +/* +** Copyright 2010, Adam Shanks (@ChainsDD) +** Copyright 2008, Zinx Verituse (@zinxv) +** +** 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 #include @@ -11,44 +26,22 @@ #include #include #include - #include #include #include - +#include #include #include #include +#include #include -#include - -extern char* _mktemp(char*); /* mktemp doesn't link right. Don't ask me why. */ - #include "su.h" +#include "utils.h" -/* Ewwww. I'm way too lazy. */ -static const char socket_path_template[PATH_MAX] = REQUESTOR_CACHE_PATH "/.socketXXXXXX"; -static char socket_path_buf[PATH_MAX]; -static char *socket_path = NULL; -static int socket_serv_fd = -1; -static char shell[PATH_MAX]; -static unsigned req_uid = 0; - -static sqlite3 *db = NULL; - -static struct su_initiator su_from = { - .pid = -1, - .uid = 0, - .bin = "", - .args = "", -}; - -static struct su_request su_to = { - .uid = AID_ROOT, - .command = DEFAULT_COMMAND, -}; +/* Still lazt, will fix this */ +static char socket_path[PATH_MAX]; static int from_init(struct su_initiator *from) { @@ -57,6 +50,7 @@ static int from_init(struct su_initiator *from) int fd; ssize_t len; int i; + int err; from->uid = getuid(); from->pid = getppid(); @@ -69,9 +63,10 @@ static int from_init(struct su_initiator *from) return -1; } len = read(fd, args, sizeof(args)); + err = errno; close(fd); if (len < 0 || len == sizeof(args)) { - PLOGE("Reading command line"); + PLOGEV("Reading command line", err); return -1; } @@ -113,6 +108,24 @@ static int from_init(struct su_initiator *from) return 0; } +static void populate_environment(const struct su_context *ctx) +{ + struct passwd *pw; + + if (ctx->to.keepenv) + return; + + pw = getpwuid(ctx->to.uid); + if (pw) { + setenv("HOME", pw->pw_dir, 1); + setenv("SHELL", ctx->to.shell, 1); + if (ctx->to.login || ctx->to.uid) { + setenv("USER", pw->pw_name, 1); + setenv("LOGNAME", pw->pw_name, 1); + } + } +} + static void socket_cleanup(void) { unlink(socket_path); @@ -121,19 +134,17 @@ static void socket_cleanup(void) static void cleanup(void) { socket_cleanup(); - if (db) sqlite3_close(db); } static void cleanup_signal(int sig) { socket_cleanup(); - exit(sig); + exit(128 + sig); } -static int socket_create_temp() +static int socket_create_temp(char *path, size_t len) { - int fd, err; - + int fd; struct sockaddr_un sun; fd = socket(AF_LOCAL, SOCK_STREAM, 0); @@ -142,41 +153,32 @@ static int socket_create_temp() return -1; } - for (;;) { - memset(&sun, 0, sizeof(sun)); - sun.sun_family = AF_LOCAL; - strcpy(socket_path_buf, socket_path_template); - socket_path = _mktemp(socket_path_buf); - snprintf(sun.sun_path, sizeof(sun.sun_path), "%s", socket_path); + memset(&sun, 0, sizeof(sun)); + sun.sun_family = AF_LOCAL; + snprintf(path, len, "%s/.socket%d", REQUESTOR_CACHE_PATH, getpid()); + snprintf(sun.sun_path, sizeof(sun.sun_path), "%s", path); - if (bind(fd, (struct sockaddr*)&sun, sizeof(sun)) < 0) { - if (errno != EADDRINUSE) { - PLOGE("bind"); - return -1; - } - } else { - break; - } - } + /* + * Delete the socket to protect from situations when + * something bad occured previously and the kernel reused pid from that process. + * Small probability, isn't it. + */ + unlink(sun.sun_path); - if (chmod(sun.sun_path, 0600) < 0) { - PLOGE("chmod(socket)"); - unlink(sun.sun_path); - return -1; - } - - if (chown(sun.sun_path, req_uid, req_uid) < 0) { - PLOGE("chown(socket)"); - unlink(sun.sun_path); - return -1; + if (bind(fd, (struct sockaddr*)&sun, sizeof(sun)) < 0) { + PLOGE("bind"); + goto err; } if (listen(fd, 1) < 0) { PLOGE("listen"); - return -1; + goto err; } return fd; +err: + close(fd); + return -1; } static int socket_accept(int serv_fd) @@ -204,314 +206,392 @@ static int socket_accept(int serv_fd) return fd; } -static int socket_receive_result(int serv_fd, char *result, ssize_t result_len) +static int socket_send_request(int fd, const struct su_context *ctx) +{ + size_t len; + size_t bin_size, cmd_size; + char *cmd; + +#define write_token(fd, data) \ +do { \ + uint32_t __data = htonl(data); \ + size_t __count = sizeof(__data); \ + size_t __len = write((fd), &__data, __count); \ + if (__len != __count) { \ + PLOGE("write(" #data ")"); \ + return -1; \ + } \ +} while (0) + + write_token(fd, PROTO_VERSION); + write_token(fd, PATH_MAX); + write_token(fd, ARG_MAX); + write_token(fd, ctx->from.uid); + write_token(fd, ctx->to.uid); + bin_size = strlen(ctx->from.bin) + 1; + write_token(fd, bin_size); + len = write(fd, ctx->from.bin, bin_size); + if (len != bin_size) { + PLOGE("write(bin)"); + return -1; + } + cmd = get_command(&ctx->to); + cmd_size = strlen(cmd) + 1; + write_token(fd, cmd_size); + len = write(fd, cmd, cmd_size); + if (len != cmd_size) { + PLOGE("write(cmd)"); + return -1; + } + return 0; +} + +static int socket_receive_result(int fd, char *result, ssize_t result_len) { ssize_t len; - for (;;) { - int fd = socket_accept(serv_fd); - if (fd < 0) - return -1; - - len = read(fd, result, result_len-1); - if (len < 0) { - PLOGE("read(result)"); - return -1; - } - - if (len > 0) - break; + len = read(fd, result, result_len-1); + if (len < 0) { + PLOGE("read(result)"); + return -1; } - result[len] = '\0'; return 0; } -static sqlite3 *database_init() +static void usage(int status) { - sqlite3 *db; - - if (mkdir(REQUESTOR_DATABASES_PATH, 0771) >= 0) { - chown(REQUESTOR_DATABASES_PATH, req_uid, req_uid); - } - - if (sqlite3_open(REQUESTOR_DATABASE_PATH, &db) != SQLITE_OK) { - LOGE("Couldn't open database"); - return NULL; - } - - if (sqlite3_exec(db, - "PRAGMA journal_mode = delete;", - NULL, - NULL, - NULL - ) != SQLITE_OK) { - LOGE("Could not set journal mode"); - sqlite3_close(db); - return NULL; - } - - chmod(REQUESTOR_DATABASE_PATH, 0660); - chown(REQUESTOR_DATABASE_PATH, req_uid, req_uid); - - if (sqlite3_exec(db, - "CREATE TABLE IF NOT EXISTS apps (_id INTEGER, uid INTEGER, package TEXT, name TEXT, exec_uid INTEGER, exec_cmd TEXT, allow INTEGER, PRIMARY KEY (_id), UNIQUE (uid,exec_uid,exec_cmd));", - NULL, - NULL, - NULL - ) != SQLITE_OK) { - LOGE("Couldn't create apps table"); - sqlite3_close(db); - return NULL; - } + FILE *stream = (status == EXIT_SUCCESS) ? stdout : stderr; + + fprintf(stream, + "Usage: su [options] [--] [-] [LOGIN] [--] [args...]\n\n" + "Options:\n" + " -c, --command COMMAND pass COMMAND to the invoked shell\n" + " -h, --help display this help message and exit\n" + " -, -l, --login pretend the shell to be a login shell\n" + " -m, -p,\n" + " --preserve-environment do not change environment variables\n" + " -s, --shell SHELL use SHELL instead of the default " DEFAULT_SHELL "\n" + " -v, --version display version number and exit\n" + " -V display version code and exit,\n" + " this is used almost exclusively by Superuser.apk\n"); + exit(status); +} - if (sqlite3_exec(db, - "CREATE TABLE IF NOT EXISTS logs (_id INTEGER, app_id INTEGER, date INTEGER, type INTEGER, PRIMARY KEY (_id));", - NULL, - NULL, - NULL - ) != SQLITE_OK) { - LOGE("Couldn't create logs table"); - sqlite3_close(db); - return NULL; - } +static void deny(const struct su_context *ctx) +{ + char *cmd = get_command(&ctx->to); - return db; + send_intent(ctx, "", 0, ACTION_RESULT); + LOGW("request rejected (%u->%u %s)", ctx->from.uid, ctx->to.uid, cmd); + fprintf(stderr, "%s\n", strerror(EACCES)); + exit(EXIT_FAILURE); } -enum { - DB_INTERACTIVE, - DB_DENY, - DB_ALLOW -}; - -static int database_check(sqlite3 *db, struct su_initiator *from, struct su_request *to) +static void allow(const struct su_context *ctx) { - char sql[4096]; - char *zErrmsg; - char **result; - int nrow,ncol; - int allow; - struct timeval tv; + char *arg0; + int argc, err; + + umask(ctx->umask); + send_intent(ctx, "", 1, ACTION_RESULT); + + arg0 = strrchr (ctx->to.shell, '/'); + arg0 = (arg0) ? arg0 + 1 : ctx->to.shell; + if (ctx->to.login) { + int s = strlen(arg0) + 2; + char *p = malloc(s); - sqlite3_snprintf( - sizeof(sql), sql, - "SELECT _id,allow FROM apps WHERE uid=%u AND exec_uid=%u AND exec_cmd='%q';", - (unsigned)from->uid, to->uid, to->command - ); + if (!p) + exit(EXIT_FAILURE); - if (strlen(sql) >= sizeof(sql)-1) - return DB_DENY; - - if (sqlite3_get_table(db, sql, &result, &nrow, &ncol, &zErrmsg) != SQLITE_OK) { - LOGE("Database check failed with error message %s", zErrmsg); - return DB_DENY; + *p = '-'; + strcpy(p + 1, arg0); + arg0 = p; } - - if (nrow == 0 || ncol != 2) - return DB_INTERACTIVE; - - if (strcmp(result[0], "_id") == 0 && strcmp(result[1], "allow") == 0) { - if (strcmp(result[3], "1") == 0) { - allow = DB_ALLOW; - } else { - allow = DB_DENY; - } - gettimeofday(&tv, NULL); - sqlite3_snprintf( - sizeof(sql), sql, - "INSERT OR IGNORE INTO logs (app_id,date,type) VALUES (%s,(%ld*1000)+(%ld/1000),%s);", - result[2], tv.tv_sec, tv.tv_usec, result[3] - ); - sqlite3_exec(db, sql, NULL, NULL, NULL); - return allow; + + /* + * Set effective uid back to root, otherwise setres[ug]id will fail + * if ctx->to.uid isn't root. + */ + if (seteuid(0)) { + PLOGE("seteuid (root)"); + exit(EXIT_FAILURE); } - sqlite3_free_table(result); - - return DB_INTERACTIVE; -} + populate_environment(ctx); -static int check_notifications(sqlite3 *db) -{ - char sql[4096]; - char *zErrmsg; - char **result; - int nrow,ncol; - int notifications; - - sqlite3_snprintf( - sizeof(sql), sql, - "SELECT value FROM prefs WHERE key='notifications';" - ); - - if (sqlite3_get_table(db, sql, &result, &nrow, &ncol, &zErrmsg) != SQLITE_OK) { - LOGE("Notifications check failed with error message %s", zErrmsg); - return 0; + if (setresgid(ctx->to.uid, ctx->to.uid, ctx->to.uid)) { + PLOGE("setresgid (%u)", ctx->to.uid); + exit(EXIT_FAILURE); + } + if (setresuid(ctx->to.uid, ctx->to.uid, ctx->to.uid)) { + PLOGE("setresuid (%u)", ctx->to.uid); + exit(EXIT_FAILURE); } - - if (nrow == 0 || ncol != 1) - return 0; - - if (strcmp(result[0], "value") == 0 && strcmp(result[1], "1") == 0) - return 1; - - return 0; -} -static void deny(void) -{ - struct su_initiator *from = &su_from; - struct su_request *to = &su_to; +#define PARG(arg) \ + (ctx->to.optind + (arg) < ctx->to.argc) ? " " : "", \ + (ctx->to.optind + (arg) < ctx->to.argc) ? ctx->to.argv[ctx->to.optind + (arg)] : "" - LOGW("request rejected (%u->%u %s)", from->uid, to->uid, to->command); - fprintf(stderr, "%s\n", strerror(EACCES)); - exit(-1); -} + LOGD("%u %s executing %u %s using shell %s : %s%s%s%s%s%s%s%s%s%s%s%s%s%s", + ctx->from.uid, ctx->from.bin, + ctx->to.uid, get_command(&ctx->to), ctx->to.shell, + arg0, PARG(0), PARG(1), PARG(2), PARG(3), PARG(4), PARG(5), + (ctx->to.optind + 6 < ctx->to.argc) ? " ..." : ""); -static void allow(int notifications) -{ - struct su_initiator *from = &su_from; - struct su_request *to = &su_to; - char *exe = NULL; - - if (notifications) - send_intent(&su_from, &su_to, "", 1); - - if (!strcmp(shell, "")) { - strcpy(shell , "/system/bin/sh"); - } - exe = strrchr (shell, '/') + 1; - setgroups(0, NULL); - setresgid(to->uid, to->uid, to->uid); - setresuid(to->uid, to->uid, to->uid); - LOGD("%u %s executing %u %s using shell %s : %s", from->uid, from->bin, to->uid, to->command, shell, exe); - if (strcmp(to->command, DEFAULT_COMMAND)) { - execl(shell, exe, "-c", to->command, (char*)NULL); - } else { - execl(shell, exe, "-", (char*)NULL); + argc = ctx->to.optind; + if (ctx->to.command) { + ctx->to.argv[--argc] = ctx->to.command; + ctx->to.argv[--argc] = "-c"; } + ctx->to.argv[--argc] = arg0; + execv(ctx->to.shell, ctx->to.argv + argc); + err = errno; PLOGE("exec"); - exit(-1); + fprintf(stderr, "Cannot execute %s: %s\n", ctx->to.shell, strerror(err)); + exit(EXIT_FAILURE); } int main(int argc, char *argv[]) { + struct su_context ctx = { + .from = { + .pid = -1, + .uid = 0, + .bin = "", + .args = "", + }, + .to = { + .uid = AID_ROOT, + .login = 0, + .keepenv = 0, + .shell = DEFAULT_SHELL, + .command = NULL, + .argv = argv, + .argc = argc, + .optind = 0, + }, + }; struct stat st; - char buf[64], *result; - int i; - int dballow; - - for (i = 1; i < argc; i++) { - if (!strcmp(argv[i], "-c")) { - if (++i < argc) { - su_to.command = argv[i]; - } else { - deny(); - } - } else if (!strcmp(argv[i], "-s")) { - if (++i < argc) { - strcpy(shell, argv[i]); - } else { - deny(); - } - } else if (!strcmp(argv[i], "-v")) { - printf("%s\n", VERSION); - exit(-1); - } else if (!strcmp(argv[i], "-")) { - ++i; + int socket_serv_fd, fd; + char buf[64], *result, debuggable[PROPERTY_VALUE_MAX]; + char enabled[PROPERTY_VALUE_MAX], build_type[PROPERTY_VALUE_MAX]; + char cm_version[PROPERTY_VALUE_MAX];; + int c, dballow, len; + struct option long_opts[] = { + { "command", required_argument, NULL, 'c' }, + { "help", no_argument, NULL, 'h' }, + { "login", no_argument, NULL, 'l' }, + { "preserve-environment", no_argument, NULL, 'p' }, + { "shell", required_argument, NULL, 's' }, + { "version", no_argument, NULL, 'v' }, + { NULL, 0, NULL, 0 }, + }; + char *data; + unsigned sz; + + while ((c = getopt_long(argc, argv, "+c:hlmps:Vv", long_opts, NULL)) != -1) { + switch(c) { + case 'c': + ctx.to.command = optarg; break; - } else { + case 'h': + usage(EXIT_SUCCESS); + break; + case 'l': + ctx.to.login = 1; + break; + case 'm': + case 'p': + ctx.to.keepenv = 1; break; + case 's': + ctx.to.shell = optarg; + break; + case 'V': + printf("%d\n", VERSION_CODE); + exit(EXIT_SUCCESS); + case 'v': + printf("%s\n", VERSION); + exit(EXIT_SUCCESS); + default: + /* Bionic getopt_long doesn't terminate its error output by newline */ + fprintf(stderr, "\n"); + usage(2); } } - if (i < argc-1) { - deny(); + if (optind < argc && !strcmp(argv[optind], "-")) { + ctx.to.login = 1; + optind++; } - if (i == argc-1) { + /* username or uid */ + if (optind < argc && strcmp(argv[optind], "--")) { struct passwd *pw; - pw = getpwnam(argv[i]); + pw = getpwnam(argv[optind]); if (!pw) { - su_to.uid = atoi(argv[i]); + char *endptr; + + /* It seems we shouldn't do this at all */ + errno = 0; + ctx.to.uid = strtoul(argv[optind], &endptr, 10); + if (errno || *endptr) { + LOGE("Unknown id: %s\n", argv[optind]); + fprintf(stderr, "Unknown id: %s\n", argv[optind]); + exit(EXIT_FAILURE); + } } else { - su_to.uid = pw->pw_uid; + ctx.to.uid = pw->pw_uid; + } + optind++; + } + if (optind < argc && !strcmp(argv[optind], "--")) { + optind++; + } + ctx.to.optind = optind; + + if (from_init(&ctx.from) < 0) { + deny(&ctx); + } + + // we can't simply use the property service, since we aren't launched from init and + // can't trust the location of the property workspace. find the properties ourselves. + data = read_file("/default.prop", &sz); + get_property(data, debuggable, "ro.debuggable", "0"); + free(data); + + data = read_file("/system/build.prop", &sz); + get_property(data, cm_version, "ro.cm.version", ""); + get_property(data, build_type, "ro.build.type", ""); + free(data); + + data = read_file("/data/property/persist.sys.root_access", &sz); + if (data != NULL) { + len = strlen(data); + if (len >= PROPERTY_VALUE_MAX) + memcpy(enabled, "1", 2); + else + memcpy(enabled, data, len + 1); + free(data); + } else + memcpy(enabled, "1", 2); + + ctx.umask = umask(027); + + // CyanogenMod-specific behavior + if (strlen(cm_version) > 0) { + // only allow su on debuggable builds + if (strcmp("1", debuggable) != 0) { + LOGE("Root access is disabled on non-debug builds"); + deny(&ctx); + } + + // enforce persist.sys.root_access on non-eng builds + if (strcmp("eng", build_type) != 0 && + (atoi(enabled) & 1) != 1 ) { + LOGE("Root access is disabled by system setting - enable it under settings -> developer options"); + deny(&ctx); } - } - from_init(&su_from); + // disallow su in a shell if appropriate + if (ctx.from.uid == AID_SHELL && (atoi(enabled) == 1)) { + LOGE("Root access is disabled by a system setting - enable it under settings -> developer options"); + deny(&ctx); + } + } - if (su_from.uid == AID_ROOT) - allow(0); + if (ctx.from.uid == AID_ROOT || ctx.from.uid == AID_SHELL) + allow(&ctx); if (stat(REQUESTOR_DATA_PATH, &st) < 0) { PLOGE("stat"); - deny(); + deny(&ctx); } if (st.st_gid != st.st_uid) { - LOGE("Bad uid/gid %d/%d for Superuser Requestor application", (int)st.st_uid, (int)st.st_gid); - deny(); + LOGE("Bad uid/gid %d/%d for Superuser Requestor application", + (int)st.st_uid, (int)st.st_gid); + deny(&ctx); } - req_uid = st.st_uid; - - if (from_init(&su_from) < 0) { - deny(); + mkdir(REQUESTOR_CACHE_PATH, 0770); + if (chown(REQUESTOR_CACHE_PATH, st.st_uid, st.st_gid)) { + PLOGE("chown (%s, %ld, %ld)", REQUESTOR_CACHE_PATH, st.st_uid, st.st_gid); + deny(&ctx); } - if (mkdir(REQUESTOR_CACHE_PATH, 0771) >= 0) { - chown(REQUESTOR_CACHE_PATH, req_uid, req_uid); + if (setgroups(0, NULL)) { + PLOGE("setgroups"); + deny(&ctx); } - - db = database_init(); - if (!db) { - deny(); + if (setegid(st.st_gid)) { + PLOGE("setegid (%lu)", st.st_gid); + deny(&ctx); + } + if (seteuid(st.st_uid)) { + PLOGE("seteuid (%lu)", st.st_uid); + deny(&ctx); } - dballow = database_check(db, &su_from, &su_to); - int notifications = check_notifications(db); + dballow = database_check(&ctx); switch (dballow) { - case DB_DENY: deny(); - case DB_ALLOW: allow(notifications); + case DB_DENY: deny(&ctx); + case DB_ALLOW: allow(&ctx); case DB_INTERACTIVE: break; - default: deny(); + default: deny(&ctx); } - - socket_serv_fd = socket_create_temp(); + + socket_serv_fd = socket_create_temp(socket_path, sizeof(socket_path)); if (socket_serv_fd < 0) { - deny(); + deny(&ctx); } signal(SIGHUP, cleanup_signal); signal(SIGPIPE, cleanup_signal); signal(SIGTERM, cleanup_signal); + signal(SIGQUIT, cleanup_signal); + signal(SIGINT, cleanup_signal); signal(SIGABRT, cleanup_signal); atexit(cleanup); - if (send_intent(&su_from, &su_to, socket_path, 0) < 0) { - deny(); + if (send_intent(&ctx, socket_path, -1, ACTION_REQUEST) < 0) { + deny(&ctx); } - if (socket_receive_result(socket_serv_fd, buf, sizeof(buf)) < 0) { - deny(); + fd = socket_accept(socket_serv_fd); + if (fd < 0) { + deny(&ctx); + } + if (socket_send_request(fd, &ctx)) { + deny(&ctx); + } + if (socket_receive_result(fd, buf, sizeof(buf))) { + deny(&ctx); } + close(fd); close(socket_serv_fd); socket_cleanup(); result = buf; +#define SOCKET_RESPONSE "socket:" + if (strncmp(result, SOCKET_RESPONSE, sizeof(SOCKET_RESPONSE) - 1)) + LOGW("SECURITY RISK: Requestor still receives credentials in intent"); + else + result += sizeof(SOCKET_RESPONSE) - 1; + if (!strcmp(result, "DENY")) { - deny(); + deny(&ctx); } else if (!strcmp(result, "ALLOW")) { - allow(notifications); + allow(&ctx); } else { LOGE("unknown response from Superuser Requestor: %s", result); - deny(); + deny(&ctx); } - deny(); + deny(&ctx); return -1; } diff --git a/su.h b/su.h index 2052eab..4cc5485 100644 --- a/su.h +++ b/su.h @@ -1,18 +1,52 @@ +/* +** Copyright 2010, Adam Shanks (@ChainsDD) +** Copyright 2008, Zinx Verituse (@zinxv) +** +** 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 SU_h #define SU_h 1 -#define REQUESTOR_PACKAGE "com.noshufou.android.su" -#define REQUESTOR_CLASS "SuRequest" +#ifdef LOG_TAG +#undef LOG_TAG +#endif +#define LOG_TAG "su" -#define REQUESTOR_DATA_PATH "/data/data/" REQUESTOR_PACKAGE +#define REQUESTOR "com.noshufou.android.su" +#define REQUESTOR_DATA_PATH "/data/data/" REQUESTOR #define REQUESTOR_CACHE_PATH REQUESTOR_DATA_PATH "/cache" -#define REQUESTOR_DATABASES_PATH REQUESTOR_DATA_PATH "/databases" -#define REQUESTOR_DATABASE_PATH REQUESTOR_DATABASES_PATH "/permissions.sqlite" +#define REQUESTOR_STORED_PATH REQUESTOR_DATA_PATH "/files/stored" +#define REQUESTOR_STORED_DEFAULT REQUESTOR_STORED_PATH "/default" + +/* intent actions */ +#define ACTION_REQUEST REQUESTOR ".REQUEST" +#define ACTION_RESULT REQUESTOR ".RESULT" + +#define DEFAULT_SHELL "/system/bin/sh" + +#ifdef SU_LEGACY_BUILD +#define VERSION_EXTRA "l" +#else +#define VERSION_EXTRA "" +#endif -#define DEFAULT_COMMAND "/system/bin/sh" +#define VERSION "3.2" VERSION_EXTRA +#define VERSION_CODE 18 -#define VERSION "2.3.1-efg" +#define DATABASE_VERSION 6 +#define PROTO_VERSION 0 struct su_initiator { pid_t pid; @@ -23,21 +57,47 @@ struct su_initiator { struct su_request { unsigned uid; + int login; + int keepenv; + char *shell; char *command; + char **argv; + int argc; + int optind; }; -extern int send_intent(struct su_initiator *from, struct su_request *to, const char *socket_path, int type); +struct su_context { + struct su_initiator from; + struct su_request to; + mode_t umask; +}; + +enum { + DB_INTERACTIVE, + DB_DENY, + DB_ALLOW +}; + +extern int database_check(const struct su_context *ctx); + +extern int send_intent(const struct su_context *ctx, + const char *socket_path, int allow, const char *action); + +static inline char *get_command(const struct su_request *to) +{ + return (to->command) ? to->command : to->shell; +} #if 0 #undef LOGE -#define LOGE(fmt,args...) fprintf(stderr, fmt , ## args ) +#define LOGE(fmt,args...) fprintf(stderr, fmt, ##args) #undef LOGD -#define LOGD(fmt,args...) fprintf(stderr, fmt , ## args ) +#define LOGD(fmt,args...) fprintf(stderr, fmt, ##args) #undef LOGW -#define LOGW(fmt,args...) fprintf(stderr, fmt , ## args ) +#define LOGW(fmt,args...) fprintf(stderr, fmt, ##args) #endif -#define PLOGE(fmt,args...) LOGE(fmt " failed with %d: %s" , ## args , errno, strerror(errno)) -#define PLOGEV(fmt,err,args...) LOGE(fmt " failed with %d: %s" , ## args , err, strerror(err)) +#define PLOGE(fmt,args...) LOGE(fmt " failed with %d: %s", ##args, errno, strerror(errno)) +#define PLOGEV(fmt,err,args...) LOGE(fmt " failed with %d: %s", ##args, err, strerror(err)) #endif diff --git a/utils.c b/utils.c new file mode 100644 index 0000000..ec6444b --- /dev/null +++ b/utils.c @@ -0,0 +1,103 @@ +/* +** Copyright 2012, The CyanogenMod 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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +/* reads a file, making sure it is terminated with \n \0 */ +char* read_file(const char *fn, unsigned *_sz) +{ + char *data; + int sz; + int fd; + + data = 0; + fd = open(fn, O_RDONLY); + if(fd < 0) return 0; + + sz = lseek(fd, 0, SEEK_END); + if(sz < 0) goto oops; + + if(lseek(fd, 0, SEEK_SET) != 0) goto oops; + + data = (char*) malloc(sz + 2); + if(data == 0) goto oops; + + if(read(fd, data, sz) != sz) goto oops; + close(fd); + data[sz] = '\n'; + data[sz+1] = 0; + if(_sz) *_sz = sz; + return data; + +oops: + close(fd); + if(data != 0) free(data); + return 0; +} + +int get_property(const char *data, char *found, const char *searchkey, const char *not_found) +{ + char *key, *value, *eol, *sol, *tmp; + if (data == NULL) goto defval; + int matched = 0; + sol = strdup(data); + while((eol = strchr(sol, '\n'))) { + key = sol; + *eol++ = 0; + sol = eol; + + value = strchr(key, '='); + if(value == 0) continue; + *value++ = 0; + + while(isspace(*key)) key++; + if(*key == '#') continue; + tmp = value - 2; + while((tmp > key) && isspace(*tmp)) *tmp-- = 0; + + while(isspace(*value)) value++; + tmp = eol - 2; + while((tmp > value) && isspace(*tmp)) *tmp-- = 0; + + if (strncmp(searchkey, key, strlen(searchkey)) == 0) { + matched = 1; + break; + } + } + int len; + if (matched) { + len = strlen(value); + if (len >= PROPERTY_VALUE_MAX) + return -1; + memcpy(found, value, len + 1); + } else goto defval; + return len; + +defval: + len = strlen(not_found); + memcpy(found, not_found, len + 1); + return len; +} \ No newline at end of file diff --git a/utils.h b/utils.h new file mode 100644 index 0000000..92e1ace --- /dev/null +++ b/utils.h @@ -0,0 +1,24 @@ +/* +** Copyright 2012, The CyanogenMod 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 _UTILS_H_ +#define _UTILS_H_ + +/* reads a file, making sure it is terminated with \n \0 */ +char* read_file(const char *fn, unsigned *_sz); + +int get_property(const char *data, char *found, const char *searchkey, const char *not_found); +#endif \ No newline at end of file