我們知道,在system/bin下面有很多可執(zhí)行文件,包括但不限于iptables等需要root權限才可以操作,我們不可能為了部分需求將整機root處理,這時就需要單獨為我們特定的APP建立一個獲取root權限的通道。
那么在一臺普通user版本的手機上誰有root權限呢?答案是init.rc里的service!所以我們優(yōu)先考慮從這里打開突破口。同時想一想為什么install這種就不需要root權限呢?從源碼中我們發(fā)現了兩個很重要的文件Installd.cpp與InstallerConnection.java,第一感覺是系統(tǒng)使用了LocalSocket實現了跨進程?沒錯,APP所在的進程肯定是沒有root權限的,它必然需要將自己的需求告知一個已經擁有root權限的進程,所以跨進程是必須的。再看init.rc,這里也有install的想關配置,我們看到它在這里配置了socket,那么流程也就明白了,init.rc啟動了一個service同時配置了一個socket,然后在installd.cpp中進行socket的監(jiān)聽,當客戶端向該socket發(fā)送信息后,這個擁有root權限的服務端將得到客戶端的請求。
根據如上分析,我們參照源碼實現讓app可以執(zhí)行iptables -L這條簡單的但卻需要root權限的命令
frameworks\native\cmds\mysocketservice添加我們的模塊
mysocket_service.cpp
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <string>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/un.h>
#include <cutils/sockets.h>
#include <utils/Log.h>
#include <android/log.h>
#include <fcntl.h>
#define LOG_TAG "SOCKET_SERVER"
#define SOCKET_PATH "mysocket"
#define BUFFER_MAX 1024
using namespace std;
static int readx(int s, void *_buf, int count)
{
char *buf = (char *)_buf;
int n = 0, r;
if (count < 0) return -1;
while (n < count) {
r = read(s, buf + n, count - n);
if (r < 0) {
if (errno == EINTR) continue;
ALOGE("read error: %s\n", strerror(errno));
return -1;
}
if (r == 0) {
ALOGE("eof\n");
return -1; /* EOF */
}
n += r;
}
return 0;
}
static int writex(int s, const void *_buf, int count)
{
const char *buf = (const char *) _buf;
int n = 0, r;
if (count < 0) return -1;
while (n < count) {
r = write(s, buf + n, count - n);
if (r < 0) {
if (errno == EINTR) continue;
ALOGE("write error: %s\n", strerror(errno));
return -1;
}
n += r;
}
return 0;
}
static string execCMD(char* command)
{
string result="";
FILE *fpRead;
strcat(command," 2>&1");
fpRead = popen(command, "r");
ALOGI("command = %s\n", command);
char buf[1024];
while(fgets(buf,sizeof(buf),fpRead)!=NULL)
{
result+=buf;
ALOGI("buf = %s\n", buf);
}
if(fpRead!=NULL)
pclose(fpRead);
return result;
}
int main(const int argc, const char *argv[]) {
char buf[BUFFER_MAX];
struct sockaddr addr;
socklen_t alen;
int lsocket, s;
ALOGI("socketserver firing up\n");
lsocket = android_get_control_socket(SOCKET_PATH);
if (lsocket < 0) {
ALOGE("Failed to get socket from environment: %s\n", strerror(errno));
exit(1);
}
if (listen(lsocket, 5)) {
ALOGE("Listen on socket failed: %s\n", strerror(errno));
exit(1);
}
fcntl(lsocket, F_SETFD, FD_CLOEXEC);
for (;;) {
alen = sizeof(addr);
s = accept(lsocket, &addr, &alen);
if (s < 0) {
ALOGE("Accept failed: %s\n", strerror(errno));
continue;
}
fcntl(s, F_SETFD, FD_CLOEXEC);
ALOGI("new connection\n");
for (;;) {
unsigned short count;
if (readx(s, &count, sizeof(count))) {
ALOGE("failed to read size\n");
break;
}
if ((count < 1) || (count >= BUFFER_MAX)) {
ALOGE("invalid size %d\n", count);
break;
}
if (readx(s, buf, count)) {
ALOGE("failed to read command\n");
break;
}
buf[count] = 0;
ALOGI("buf = %s count = %d\n", buf, count);
string result=execCMD(buf);
count=result.length();
if (writex(s, &count, sizeof(count))) return -1;
if (writex(s, result.c_str(), count)) return -1;
}
ALOGI("closing connection\n");
close(s);
}
return 0;
}
當socket配置好后,開機后將在/dev/socket此目錄下生成相應的socket節(jié)點,我們看到這里使用了read和write函數對此節(jié)點進行讀寫,因為這個socket實現跨進程的本質就是上層客戶端發(fā)指令寫這個節(jié)點,而服務端先讀取節(jié)點即可知道客戶端發(fā)來的消息具體是什么。讀取出消息指令后,我們這里使用popen函數執(zhí)行這個shell命令。需要注意的是讀取到客戶端消息后,我們在其后追回一個字符串:" 2>&1",目的是將標準錯誤同時重定向到文件,否則popen只能得到一個標準輸出。
配置Android.mk
LOCAL_PATH:= $(call my-dir)
svc_c_flags = \
-Wall -Wextra \
include $(CLEAR_VARS)
LOCAL_SHARED_LIBRARIES := liblog libselinux
LOCAL_SRC_FILES := mysocket_service.cpp
LOCAL_CFLAGS += $(svc_c_flags)
LOCAL_MODULE := mysocket
LOCAL_MODULE_TAGS := optional
LOCAL_CLANG := true
include $(BUILD_EXECUTABLE)
build\target\product\base.mk加入項目編譯
PRODUCT_PACKAGES += mysocket
system\core\rootdir\init.rc配置socket
service mysocket /system/bin/mysocket
class main
socket mysocket stream 0666 root system
oneshot
external\sepolicy配置SELinux權限
file_contexts
/dev/socket/mysocket u:object_r:mysocket_st:s0
mysocket.te
allow mysocket_st mysocket_st:sock_file create_file_perms;
type_transition mysocket_st socket_device:sock_file mysocket_st;
system_server.te
allow system_server mysocket_st:sock_file rw_file_perms;
init.te
neverallow init {fs_type }:file execute_no_trans;
allow init system_file:file execute_no_trans;
allow init shell_exec:file execute_no_trans;
allow init init:rawip_socket {create getopt};
system_app.te
allow system_app mysocket_st:sock_file rw_file_perms;
file.te
type mysocket_st, file_type;
需要注意的是:不同的指令所需要的SELinux權限不同,我這里僅僅針對iptables的相關命令配置了權限,當執(zhí)行ip等其它指令時依然可能顯示permission denied,所以具體問題還需要具體分析。另外在配置權限時要注意系統(tǒng)添加的neverallow語句,避免權限沖突。
客戶端的調用
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import android.os.SystemClock;
import android.util.Slog;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Represents a connection to {@code installd}. Allows multiple connect and
* disconnect cycles.
*
* @hide for internal use only
*/
public class SocketClient {
private static final String TAG = "MySocketClient";
private static final boolean LOCAL_DEBUG = true;
private InputStream mIn;
private OutputStream mOut;
private LocalSocket mSocket;
private byte buf[] = new byte[1024];//初始大小,如果返回的結果超出大小則重新初始化
public SocketClient() {
}
public synchronized String transact(String cmd) {
String result=null;
if (LOCAL_DEBUG) {
Slog.i(TAG, "send: '" + cmd + "'");
}
if (connect()&&writeCommand(cmd)) {
final int replyLength = readReply();
if (replyLength > 0) {
result = new String(buf, 0, replyLength);
if (LOCAL_DEBUG) {
Slog.i(TAG, "recv: '" + result + "'");
}
} else {
if (LOCAL_DEBUG) {
Slog.i(TAG, "fail");
}
}
}else{
if (LOCAL_DEBUG) {
Slog.i(TAG, "connect or write command failed!");
}
}
return result;
}
public String execute(String cmd) {
return transact(cmd);
}
private boolean connect() {
if (mSocket != null) {
return true;
}
Slog.i(TAG, "connecting...");
try {
mSocket = new LocalSocket();
LocalSocketAddress address = new LocalSocketAddress("mysocket",
LocalSocketAddress.Namespace.RESERVED);
mSocket.connect(address);
mIn = mSocket.getInputStream();
mOut = mSocket.getOutputStream();
} catch (IOException ex) {
disconnect();
return false;
}
return true;
}
public void disconnect() {
Slog.i(TAG, "disconnecting...");
try {
if (mSocket != null)
mSocket.close();
} catch (IOException ex) {
}
try {
if (mIn != null)
mIn.close();
} catch (IOException ex) {
}
try {
if (mOut != null)
mOut.close();
} catch (IOException ex) {
}
mSocket = null;
mIn = null;
mOut = null;
}
private boolean readFully(byte[] buffer, int len) {
int off = 0, count;
if (len < 0)
return false;
while (off != len) {
try {
count = mIn.read(buffer, off, len - off);
if (count <= 0) {
Slog.i(TAG, "read error " + count);
break;
}
off += count;
} catch (IOException ex) {
Slog.i(TAG, "read exception");
break;
}
}
Slog.i(TAG, "read " + len + " bytes");
if (off == len)
return true;
disconnect();
return false;
}
private int readReply() {
if (!readFully(buf, 2)) {
return -1;
}
final int len = (((int) buf[0]) & 0xff) | ((((int) buf[1]) & 0xff) << 8);
if ((len < 1)) {
Slog.i(TAG, "invalid reply length (" + len + ")");
disconnect();
return -1;
}
if(len > buf.length){
buf = new byte[len]; //重新擴容
}
if (!readFully(buf, len)) {
return -1;
}
return len;
}
private boolean writeCommand(String cmdString) {
final byte[] cmd = cmdString.getBytes();
final int len = cmd.length;
if ((len < 1) || (len > buf.length)) {
return false;
}
buf[0] = (byte) (len & 0xff);
buf[1] = (byte) ((len >> 8) & 0xff);
try {
mOut.write(buf, 0, 2);
mOut.write(cmd, 0, len);
} catch (IOException ex) {
Slog.i(TAG, "write error");
disconnect();
return false;
}
return true;
}
}
import android.app.Activity;
import android.os.Bundle;
import android.text.method.ScrollingMovementMethod;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
public class MainActivity extends Activity {
private TextView tv_result;
private EditText et_cmd;
private Button bt_exec;
private String cmd=null;
private SocketClient client;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv_result=(TextView) findViewById(R.id.textview);
et_cmd= (EditText)findViewById(R.id.editText);
bt_exec=(Button)findViewById(R.id.button);
tv_result.setMovementMethod(ScrollingMovementMethod.getInstance());
client=new SocketClient();
bt_exec.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
cmd=et_cmd.getText().toString();
final String result=client.execute(cmd);
if(result!=null)
tv_result.setText(result);
else
tv_result.setText("null");
}
});
}
@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
if(client!=null)
client.disconnect();
}
}
運行結果如下圖所求:
