APK反逆向之一:監控debug

在開發和逆向過程當中不少時候都須要動態調試,開發時候能夠用開發 android 的 IDE進行調試,native層也可用調試,Android Studio早就能夠進行 native 的debug調試了。可是在 release 後的 apk 若是還檢測到了 debug 調試,那麼說明該 apk 正被破解。java

原文連接: APK反逆向之一:監控debugandroid

0x00 簡介

在 apk 被調試的時候,有不少特徵能夠檢測到,好比 hook so的時候須要分析 maps文件肯定內存加載的位置,還有調試器很 android 設備進行接口通信須要開啓端口映射。這些特徵均可以被做爲檢測 debug 的一種手段。 git

下面介紹了幾種檢測 debug 的方式,有些案例只是介紹思路,具體的實現方式須要進行更改,例如監控 tcp 端口,須要改爲 service 形式在後臺運行。 github

檢測 debug 是爲了防止應用被逆向動態分析,因此檢測的方法也都是採用 native 開發提升被逆向的成本。bash

源碼地址:anti-reverseapp

0x01 debug開關

debug 開關默認在編譯 release 版本的時候本身會關閉,可是你仍是能夠經過顯示的設置把他打開。可是若是你這麼幹了,估計你老闆要打死你。tcp

release 版本開啓 debug 調試,修改項目 build.gradle中 的 buildTypes 參數:debuggable true函數

android {
    buildTypes {
        release {
            debuggable true
            minifyEnabled false
            proguardFiles.add(file("proguard-rules.pro"))
            signingConfig = $("android.signingConfigs.myConfig")
        }
    }
}

獲取 debuggable 的值也很簡單經過API接口就能夠:工具

void detectOsDebug(){
    boolean connected = android.os.Debug.isDebuggerConnected();
    Log.d(TAG, "debugger connect status:" + connected);
}

這種方式獲取的值其實意義不大,發佈的 release 版本基本沒有會開啓的除非失誤。gradle

0x02 單步檢測

單步調試的原理很簡單:檢測某段代碼執行的時間,動態調試的時候確定會在一些地方下斷點,若是一段代碼執行時間超過2秒(這裏須要排除耗時的io讀寫等操做),則能夠認爲 apk 可能被動態分析。

示例代碼:

JNIEXPORT void single_step(){
    time(&start_time);
    //實際須要監控的代碼
    sleep(4);
    //---------------
    time(&end_time);

    LOGD("start time:%d, end time:%d", start_time, end_time);
    if(end_time - start_time > 2){
        LOGD("fit single_step");
    }
}

這裏的時間間隔能夠根據實際狀況做調整。

0x03 監控TarcePid

在 apk 被附加進程的時候在/proc/{pid}/status,/proc/{pid}/task/{pid}/status文件中會保存附件進程的 pid :TarcePid : 1212。只須要讀取這兩個文件中的 TarcePid 是否是爲0,若是不爲0則可能被附加了進程。

示例代碼:

void tarce_pid(char* path){
    char buf[BUFF_LEN];
    FILE *fp;
    int trace_pid = 0;
    fp = fopen(path, "r");
    if (fp == NULL) {
        LOGE("status open failed:[error:%d, desc:%s]", errno, strerror(errno));
        return;
    }

    while (fgets(buf, BUFF_LEN, fp)) {
        if (strstr(buf, "TracerPid")) {
            char *strok_rPtr, *temp;
            temp = strtok_r(buf, ":", &strok_rPtr);
            temp = strtok_r(NULL, ":", &strok_rPtr);
            trace_pid = atoi(temp);
            LOGD("%s, TarcePid:%d", path, trace_pid);
        }
    }

    fclose(fp);
    return;
}

JNIEXPORT void tarce_pid_monitor(){
    LOGD("tarce_pid_monitor");
    int pid = getpid();
    char path[BUFF_LEN];

    sprintf(path, "/proc/%d/status", pid);
    tarce_pid(path);

    sprintf(path, "/proc/%d/task/%d/status", pid, pid);
    tarce_pid(path);
}

檢測結果:

10-13 18:31:52.716 11538-11538/cc.gnaixx.detect_debug D/GNAIXX_NDK: tarce_pid_monitor
10-13 18:31:52.716 11538-11538/cc.gnaixx.detect_debug D/GNAIXX_NDK: /proc/11538/status, TarcePid:11669
10-13 18:31:52.716 11538-11538/cc.gnaixx.detect_debug D/GNAIXX_NDK: /proc/11538/task/11538/status, TarcePid:11669

0x04 監控tcp端口

進行 debug 調試必然會開啓端口映射,咱們能夠監控比較經常使用的逆向工具開啓的端口,固然做弊者也能夠修改端口。可是前提也是在瞭解了檢測手段下。Android中開啓的端口會保存在文件proc/net/tcp文件中。

示例代碼:

JNIEXPORT void tcp_monitor(JNIEnv *env, jclass thiz){
    LOGD("tcp_monitor");
    char buff[BUFF_LEN];

    FILE *fp;
    const char dir[] = "/proc/net/tcp";
    fp = fopen(dir, "r");
    if(fp == NULL){
        LOGE("file failed [errno:%d, desc:%s]", errno, strerror(errno));
        return;
    }
    while(fgets(buff, BUFF_LEN, fp)){
        if(strstr(buff, TCP_PORT) != NULL){
            LOGI("Line:%s", buff);
            fclose(fp);
            return;
        }
    }
}

這裏的 TCP_PORT 爲 "5D8A",也就是10進制的23946,這是ida默認的端口。

0x05 監控maps文件

/proc/{pid}/maps文件中保存了 app 運行的加載的內存信息。全部maps文件被進行ACCESS 或者 OPEN 操做都是有風險的。

能夠經過 inotify 對 maps 文件進行監控,這裏採用了子線程進行循環監控。

這裏採用兩種方式進行監控,一種阻塞的方式,一種非阻塞的方式(經過select)。

阻塞

代碼示例:

void *inotify_maps_block() {
    LOGD("start by block");
    int fd;                         //文件描述符
    int wd;                         //監視器標識符
    int event_len;                  //事件長度
    char buffer[EVENT_BUFF_LEN];    //事件buffer
    char map_path[PATH_LEN];        //監控文件路徑

    stop = 0;                       //初始化監控
    fd = inotify_init();
    pid_t pid = getpid();
    sprintf(map_path, "/proc/%d/", pid); //獲取當前APP maps路徑
    if (fd == -1) {
        LOGE("inotify_init [errno:%d, desc:%s]", errno, strerror(errno));
        return NULL;
    }
    wd = inotify_add_watch(fd, map_path, IN_ALL_EVENTS);  //添加監控 全部事件
    LOGD("add watch success path:%s", map_path);
    while (1) {
        if (stop == 1) break;       //中止監控

        event_len = read(fd, buffer, EVENT_BUFF_LEN);   //讀取事件
        if (event_len < 0) {
            LOGE("inotify_event read failed [errno:%d, desc:%s]", errno, strerror(errno));
            return NULL;
        }
        int i = 0;
        while (i < event_len) {
            struct inotify_event *event = (struct inotify_event *) &buffer[i];
            //過濾maps文件
            if (event->len && !strcmp(event->name, "maps")) {
                if (event->mask & IN_CREATE) {
                    LOGD("create: %s", event->name);
                }
                else if (event->mask & IN_DELETE) {
                    LOGD("delete: %s", event->name);
                }
                else if (event->mask & IN_MODIFY) {
                    LOGD("modified: %s", event->name);
                }
                else if (event->mask & IN_ACCESS) {
                    LOGD("access: %s", event->name);
                }
                else if (event->mask & IN_OPEN) {
                    LOGD("open : %s", event->name);
                }
                else {
                    LOGD("other event [name:%s, mask:%x]", event->name, event->mask);
                }
            }
            i += EVENT_SIZE + event->len;
        }
    }
    inotify_rm_watch(fd, wd);
    LOGD("rm watch");
    close(fd);
}

阻塞方法監控的是/proc/{pid}/文件夾,若是直接監控 maps 文件,可能形成沒法結束線程。若是正經常使用戶沒有對 maps 文件操做,那麼函數就會一直阻塞在 read() 方法。而監控 /proc/{pid} 文件夾,改文件夾下其餘文件會有操做,因此不會阻塞在read()

非阻塞

代碼示例:

void *inotify_maps_unblock() {
    LOGD("start by unblock");
    int fd;                         //文件描述符
    int wd;                         //監視器標識符
    int event_len;                  //事件長度
    char buffer[EVENT_BUFF_LEN];    //事件buffer
    char map_path[PATH_LEN];        //監控文件路徑

    fd_set fds;                     //fd_set
    struct timeval time_to_wait;    //超時時間
    stop = 0;

    //初始化監控
    fd = inotify_init();
    pid_t pid = getpid();
    sprintf(map_path, "/proc/%d/maps", pid); //獲取當前APP maps路徑
    if (fd == -1) {
        LOGE("inotify_init [errno:%d, desc:%s]", errno, strerror(errno));
        return NULL;
    }
    wd = inotify_add_watch(fd, map_path, IN_ALL_EVENTS);  //添加監控 全部事件
    LOGD("add watch success path:%s, fd:%d, wd:%d", map_path, fd, wd);

    while (1) {
        if (stop == 2) break;       //中止監控

        FD_ZERO(&fds);
        FD_SET(fd, &fds);

        //以前我把初始化放在循環外 第一次能夠阻塞,後面就直接跳過了
        time_to_wait.tv_sec = 3;
        time_to_wait.tv_usec = 0;

        int rev = select(fd + 1, &fds, NULL, NULL, &time_to_wait);//fd, readfds, writefds, errorfds, timeout:NULL阻塞, {0.0}直接過, timeout
        //int rev = select(fd + 1, &fds, NULL, NULL, NULL);//fd, readfds, writefds, errorfds, timeout:NULL阻塞, {0.0}直接過, timeout
        LOGD("select status_code: %d", rev);
        if (rev < 0) {
            //error
            LOGE("select failed [error:%d, desc:%s]", errno, strerror(errno));
        }
        else if (rev == 0) {
            //timeout
            LOGD("select timeout");
        }
        else {
            //
            event_len = read(fd, buffer, EVENT_BUFF_LEN);   //讀取事件
            if (event_len < 0) {
                LOGE("inotify_event read failed [errno:%d, desc:%s]", errno, strerror(errno));
                return NULL;
            }
            int i = 0;
            while (i < event_len) {
                //注意:這裏監控的是maps文件,因此event->name 參數爲空
                struct inotify_event *event = (struct inotify_event *) &buffer[i];
                if (event->mask & IN_CREATE) {
                    LOGD("create: %s", event->name);
                }
                else if (event->mask & IN_DELETE) {
                    LOGD("delete: %s", event->name);
                }
                else if (event->mask & IN_MODIFY) {
                    LOGD("modified: %s", event->name);
                }
                else if (event->mask & IN_ACCESS) {
                    LOGD("access: %s", event->name);
                }
                else if (event->mask & IN_OPEN) {
                    LOGD("open : %s", event->name);
                }
                else {
                    LOGD("other event [name:%s, mask:%x]", event->name, event->mask);
                }
                i += EVENT_SIZE + event->len;
            }
        }
    }
    close(fd);
    inotify_rm_watch(fd, wd);
    LOGD("rm watch");
}

經過 select() 來絕對阻塞方式,最後一個參數(timeval)控制超時時間:

  • NULL 阻塞與上面阻塞方式同樣

  • timeval 設置超時時間

timeval.tv_sec 爲秒數
timeval.tv_usec 爲微秒

timeval 每次調用過 select 方法會被初始化爲{0,0},因此必須每次都在循環內複製。我也不知道爲何,試了很久。

相關文章
相關標籤/搜索