最近开发Android app需要用到root权限去调用一些shell程序,接触过Linux的同学知道用su这个命令,su的意思是switch user,切换用户。然而在我调用su的时候,授权管理器总会弹出确认提示,更操蛋的是我手机安装的授权管理器即使设置了自动授权所有请求都不起作用!火了!干脆自己找su源码去改写个来用。

搜索了一下,发现superuser的su源码:


/*
** 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <dirent.h>
#include <errno.h>
#include <sys/stat.h>

#include <unistd.h>
#include <time.h>

#include <pwd.h>

#include <sqlite3.h>

#define DBPATH "/data/data/com.koushikdutta.superuser/databases/superuser.sqlite"

static int g_puid;

static void printRow(int argc, char** argv, char** azColName)
{
        int i;
        for (i = 0; i < argc; i++)
        {
                printf("%s: %s\n", azColName[i], argv[i]);
        }
}

typedef struct whitelistCallInfo whitelistCallInfo;
struct whitelistCallInfo
{
        sqlite3* db;
        int count;
};

static int whitelistCallback(void *data, int argc, char **argv, char **azColName)
{       
        whitelistCallInfo* callInfo = (whitelistCallInfo*)data;
        // note the count
        int count = atoi(argv[2]);
        callInfo->count = count;
        // remove whitelist entries that are expired
        if (count - 1 <= 0)
        {
                char remove[1024];
                sprintf(remove, "delete from whitelist where _id='%s';", argv[0]);
                sqlite3_exec(callInfo->db, remove, NULL, NULL, NULL);
                return 0;
        }

        char update[1024];
        sprintf(update, "update whitelist set count=%d where _id='%s';", count, argv[0]);
        sqlite3_exec(callInfo->db, update, NULL, NULL, NULL);
        return 0;
}

static int checkWhitelist()
{
        sqlite3 *db;
        int rc = sqlite3_open_v2(DBPATH, &db, SQLITE_OPEN_READWRITE, NULL);
        if (!rc)
        {
                char *errorMessage;
                char query[1024];
                sprintf(query, "select * from whitelist where _id=%d limit 1;", g_puid);
                struct whitelistCallInfo callInfo;
                callInfo.count = 0;
                callInfo.db = db;
                rc = sqlite3_exec(db, query, whitelistCallback, &callInfo, &errorMessage);
                if (rc != SQLITE_OK)
                {
                        sqlite3_close(db);
                        return 0;
                }
                sqlite3_close(db);
                return callInfo.count;
        }
        sqlite3_close(db);
        return 0;
}

static int executionFailure(char *context)
{
        fprintf(stderr, "su: %s. Error:%s\n", context, strerror(errno));
        return -errno;
}

static int permissionDenied()
{
        // the superuser activity couldn't be started
        printf("su: permission denied\n");
        return 1;
}

int main(int argc, char **argv)
{
        struct stat stats;
        struct passwd *pw;
        int uid = 0;
        int gid = 0;

        int ppid = getppid();
        char szppid[256];
        sprintf(szppid, "/proc/%d", ppid);
        stat(szppid, &stats);
        g_puid = stats.st_uid;

        // lets make sure the caller is allowed to execute this
        if (!checkWhitelist())
        {
                char sysCmd[1024];
                sprintf(sysCmd, "am start -a android.intent.action.MAIN -n com.koushikdutta.superuser/com.koushikdutta.superuser.SuperuserRequestActivity --ei uid %d --ei pid %d > /dev/null", g_puid, ppid);
                if (system(sysCmd))
                        return executionFailure("am.");

                int found = 0;
                int i;
                for (i = 0; i < 10; i++)
                {
                        sleep(1);
                        // 0 means waiting for user input
                        // > 0 means yes/always
                        // < 0 means no
                        int checkResult = checkWhitelist();
                        if (checkResult > 0)
                        {
                                found = 1;
                                break;
                        }
                        else if (checkResult < 0)
                        {
                                // user hit no
                                return permissionDenied();
                        }
                }

                if (!found)
                        return permissionDenied();
        }

        if(setgid(gid) || setuid(uid)) 
                return permissionDenied();

        char *exec_args[argc + 1];
        exec_args[argc] = NULL;
        exec_args[0] = "sh";
        int i;
        for (i = 1; i < argc; i++)
        {
                exec_args[i] = argv[i];
        }
        execv("/system/bin/sh", exec_args);
        return executionFailure("sh");
}

才百来行代码,来阅读下吧!

。。。

看明白了,浅浅地了解了思路,大概就是先看看当前进程的uid是不是在白名单上,如果在就马上去拿权限,如果不在那就调用am start启动授权管理器的activity来让用户选择是否通过。

从代码上看拿权限的关键点就是setuid(uid)和setgid(gid)了,这两函数就是把当前进程设置为属于对应的用户和用户组。看代码上下文得知uid和gid的值都是0,其实就是root的id值嘛。设置完之后,那就用execv运行/system/bin/sh程序打开一个新的sh进程,新的sh进程就有root权限,并且还能在此进程上面运行各种其他程序或者执行shell了。


好吧,我不需要验证白名单不需要用户确认,直接拿到root来执行shell就好了。那我就改写下代码,那些浮云都删掉。

int main(int argc, char **argv)
{
        int uid = 0;
        int gid = 0;

        if(setgid(gid) || setuid(uid)) {
                printf("permission denied!");
                return 1;
        }

        char *exec_args[argc + 1];
        exec_args[argc] = NULL;
        exec_args[0] = "sh";
        int i;
        for (i = 1; i < argc; i++)
        {
                exec_args[i] = argv[i];
        }
        execv("/system/bin/sh", exec_args);
        return 1;
}


就这样就OK?!不行呢~编译出来的su程序setuid()一直不成功呢~PS:代码是绝对可行的呀!

好吧,点睛之笔来了!

原来要想调用setuid()是需要root权限的,但是我根本没有root权限又如何调用这个呀?很矛盾。

还漏了设置su文件的权限哦!

先感叹下,Linux的权限控制确实是复杂哦!

chmod 755 su

悲剧,还不行。

其实,Linux的文件权限还有组特殊权限的,要我的su能正常运行必须要设置两个特殊权限!这两权限的名称就叫setuid、setgid!

setuid权限就是让程序进程在特殊情况下可以使用该文件所有者的权限!我表达的有点别扭!

就是我su程序运行之后,

我想调用setuid(),

但是进程现在不是root用户的,

那么看看有没设置特殊权限setuid?

哎哟,有哦!

那么su文件的所有者是谁?

是root哦!

系统说:OK,你被批准了!

哦耶,setuid()设置成功!(setgid()同理)


setuid()和setgid()执行成功了!我们的进程就正式切换成root的了哦!调用execv()去创建我们新的sh进程!拿着root权限各种为所欲为!!!


文笔不好,关于文件特殊权限写得好像云里雾里的感觉!抛出参考文献吧!

点击打开链接 关于Linux特殊权限的一些知识。