非root Android設備上Tcpdump的實現


  通常我們在Android應用中執行某個命令時會使用“Runtime.getRuntime().exec("命令路徑")”這種方式,但是當我們執行抓包操作時,使用這條命令無論如何都不行,通過下面代碼打印結果發現,該命令一定要在root權限下才能執行。

BufferedReader brW = new BufferedReader(new InputStreamReader(p.getErrorStream())); while((str = brW.readLine()) != null) Log.d("cwmp", "w:"+str);

但是我們的Android設備(包括機頂盒、手機等)通常並沒有root過,apk的最高權限也只是system權限,這該怎么解決?首先我們要知道,方法總比問題多,在Android設備的/system/bin路徑下,我們會看到很多二進制文件,這些二進制文件可以獲得root權限。因此,我們可以通過C語言來實現抓包功能,通過NDK把該C代碼交叉編譯成二進制文件置於/system/bin路徑下,並賦予其root權限,此時,這個二進制文件就具備了抓包能力了。現在問題又來了,我們現在是想通過apk去調用這個抓包指定,抓包完成后又該怎么通知apk呢?其實,Android可以通過socket使底層與framework層進行通信,具體請參考博文android使用socket使底層和framework通信”。接下來我們將貼出關鍵實現代碼。

1、編寫socket服務端代碼fstiService.cpp,生成可執行腳本fstiService

 

#define SOCKET_NAME "fstiService" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "itv_assistance", __VA_ARGS__) #include <jni.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #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 <android/log.h> #include <unistd.h> #include <time.h> #include <sys/time.h> #include <pthread.h> pthread_t thread[2]; char s_time[10];        //抓包時間子串
char s_command[256];    //抓包指令子串 //抓包指令:system("/system/bin/tcpdump -v -w /sdcard/te.pcap");
//獲取進程tcpdump的進程號
int getPid() { //for Linux C //FILE *fp = popen("ps -e | grep \'tcpdump\' | awk \'{print $1}\'", "r"); //for Android(ARM) //FILE *fp = popen("ps | grep \'tcpdump\'", "r");
    FILE *fp = popen("ps | grep \'tcpdump\'", "r"); char buff[1024] = { 0 }; while (NULL != fgets(buff, sizeof(buff), fp)) ; //取消換行符(10)
    buff[strlen(buff) - 1] = '\0'; pclose(fp); char dst[5] = { 0 }; char *p = buff; char *q = dst; //每一行進程信息的第二個字符串為進程號
    while (*p != ' ') p++; while (*p == ' ') p++; while (*p != ' ') *(q++) = *(p++); *(q++) = '\0'; return atoi(dst); } //截取子串(抓包時間(秒):抓包命令)
void substring(char *time, char *command, char *src) { char *p = src; char *q = time; char *s = command; while (*p != '/') *(q++) = *(p++); *(q++) = '\0'; //如果Tcpdump命令已添加環境變量,則添加下行代碼 //否則刪除下一行代碼,client傳遞的參數格式必須為: num/tcpdump所在路徑
    p++; while (*p) *(s++) = *(p++); *(s++) = '\0'; } //抓包線程
void *thread1(void *arg) { system(s_command); } void *thread2(void *arg) { int i_time = atoi(s_time); int begin = time((time_t*) NULL); while (1) { if (time((time_t*) NULL) - begin < i_time) { //printf("當前時間(s):%ld\n", time((time_t*)NULL));
            continue; } else { int n = kill(getPid(), SIGKILL); LOGD("the kill process result is n=%d", n); break; } } return 0; } //創建子線程
void thread_create() { int temp; memset(&thread, 0, sizeof(thread)); if ((temp = pthread_create(&thread[0], NULL, thread1, NULL)) != 0) LOGD("create tcpdump thread failure"); else LOGD("create tcpdump thread success"); if ((temp = pthread_create(&thread[1], NULL, thread2, NULL)) != 0) LOGD("create count thread failure"); else LOGD("create count thread success"); } void thread_wait() { if (thread[0] != 0) { pthread_join(thread[0], NULL); LOGD("tcpdump thread has terminated"); } if (thread[1] != 0) { //pthread_join(thread[1], NULL);
        printf("counter thread has terminated"); } } /** * Native層Socket服務端 */
int main() { int connect_number = 6; int fdListen = -1, new_fd = -1; int ret; struct sockaddr_un peeraddr; socklen_t socklen = sizeof(peeraddr); int numbytes; char buff[256]; //這一步很關鍵,就是獲取init.rc中配置的名為 "fstiService" 的socket //獲取已綁定的socket,返回-1為錯誤情況
    fdListen = android_get_control_socket(SOCKET_NAME); if (fdListen < 0) { LOGD("failed to get socket '" SOCKET_NAME "' errno %d", errno); exit(-1); } /** * 方法說明:開始監聽(等待參數fdListen的socket連接,參數connect_number指定同時能處理的最大連接要求) * 如果連接數目達此上限則client端將收到ECONNREFUSED的錯誤。listen函數並未開始連接,只是設置 * socket為listen模式,真正接收client端連接的是accept()。通常listen()會在socket() * bind()之后調用,接着才調用accept(). * 返回值:成功返回0,失敗返回-1,錯誤原因存在errno中 */ ret = listen(fdListen, connect_number); LOGD("listen result %d", ret); if (ret < 0) { /** * perror(s)將一個函數發生錯誤的原因輸出到標准設備(stderr) * 參數s所指的字符串會先打印出,后面再加上錯誤原因字符串 */ perror("listen"); exit(-1); } /** * 方法說明:accept(int s, struct sockaddr * addr, socklen_t * addrlen)用來接受參數s的socket連接。 * socket必須先經bind()、listen()函數處理過,當有socket客戶端連接進來時會返回一個新的socket處理 * 代碼,往后的數據傳送與讀取就是經由新的socket處理,而原來參數s的socket能繼續使用accept()來接受新的 * 連接請求。連線成功時, 參數addr 所指的結構會被系統填入遠程主機的地址數據,參數addrlen為sockaddr的 * 結構長度。 * 返回值:成功返回新的socket處理代碼,失敗返回-1,錯誤原因存在於errno中。 */ new_fd = accept(fdListen, (struct sockaddr *) &peeraddr, &socklen); LOGD("accept_fd %d", new_fd); if (new_fd < 0) { LOGD("%d", errno); perror("accept error"); exit(-1); } //循環等待Socket客戶端發來消息
    while (1) { /** * 方法說明:recv(int s, void *buf, size_t len, unsigned int flags)用來接收 * 客戶端socket傳來的數據,並把數據存到由參數buf指向的內存空間,參數len為可接收數據的最大長度。 * 參數flags一般設0 * 返回值:失敗返回-1 */
        if ((numbytes = recv(new_fd, buff, sizeof(buff), 0)) == -1) { LOGD("%d", errno); perror("recv"); continue; } LOGD("the parameter received from socket client is %s", buff); if(strcmp(buff, "exit") != 0){ substring(s_time, s_command, buff); thread_create(); thread_wait(); } char result[10] = "successp"; /** * 方法說明:send(int s, const void *msg, size_t len, unsigned int flags) * 參數s為已建立好連接的socket,參數msg指向欲發送的數據內容,參數len為數據長度,flags一般置0. * 返回值:失敗返回-1,錯誤原因存在errno中 */
        int sendR = send(new_fd, result, strlen(result), 0); //apk退出后,buff中仍然緩存之前的調用命令,此時會額外再執行一次抓包,固下面代用重寫buff中數據
        strcpy(buff, "exit"); if (sendR == -1) { perror("send"); close(new_fd); exit(0); } } close(new_fd); close(fdListen); return 0; }

 

2、配置init.rc文件,添加如下配置

 

service fstiService /system/bin/fstiService socket fstiService stream 777 system system
class main
group root
user root

 

此處配置了一個名為fstiService”的服務,Android設備開機會自動啟動並運行/system/bin/fstiService這個腳本文件。服務端代碼完成后,我們需要將其編譯成可執行腳本fstiService,Android.mk內容如下:

 

LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) #指定該模塊在所有版本下都編譯 LOCAL_MODULE_TAGS :=optional LOCAL_MODULE := fstiService LOCAL_SRC_FILES := fstiService.cpp LOCAL_LDLIBS := -llog #編譯成動態庫 #include $(BUILD_SHARED_LIBRARY) #編譯成可執行文件 include $(BUILD_EXECUTABLE)

 

3、Android客戶端代碼

 

public class SocketClient { private final String SOCKET_NAME = "fstiService"; private LocalSocket client = null; private LocalSocketAddress address = null; private boolean isConnected = false; private int connectTime = 1; public SocketClient(){ client = new LocalSocket(); //A socket in the Android reserved namespace in /dev/socket. //Only the init process may create a socket here
        address = new LocalSocketAddress(SOCKET_NAME, LocalSocketAddress.Namespace.RESERVED); new ConnectSocketThread().start(); } /** * 發送消息 * @param msg * @return 返回Socket服務端消息回執 */
    public String sendMsg(String msg){ if(!isConnected) return "Connect failure"; try{ BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream())); PrintWriter out = new PrintWriter(client.getOutputStream()); out.println(msg); out.flush(); return in.readLine(); }catch(IOException e){ e.printStackTrace(); } return "Nothing Return"; } /** * Socket連接線程,若連接失敗會嘗試重連3次 * @author Administrator * */
    private class ConnectSocketThread extends Thread{ @Override public void run() { while(!isConnected && connectTime <= 3){ try{ sleep(1000); Log.d("itv_assistance", "Try to connect socket; ConnectTime: "+connectTime); client.connect(address); isConnected = true; }catch(Exception e){ connectTime++; isConnected = false; Log.d("itv_assistance", "Connect Failure"); } } } } /** * 關閉Socket */
    public void closeSocket(){ try{ client.close(); }catch(IOException e){ e.printStackTrace(); } } }

 

  至此,基於非root的Android設備上的抓包實現方法就完成了,接下來就是編譯系統進行測試了,這步我沒有親自去做,而是把fstiService腳本及init.rc配置文件的操作交給合作廠商來做了,apk是我們自己做的,經測試一切OK。

項目源碼下載

參考:

Linux下C語言多線程編程:http://www.cnblogs.com/chenyadong/archive/2011/10/25/2223610.html

C語言socket send()數據緩存問題:http://www.tuicool.com/articles/NvIjma


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM