APK反逆向之一:監控debug

xcllch 8年前發布 | 6K 次閱讀 安卓開發 Android開發 移動開發

在開發和逆向過程中很多時候都需要動態調試,開發時候可以用開發 android 的 IDE進行調試,native層也可用調試,Android Studio早就可以進行 native 的debug調試了。但是在 release 后的 apk 如果還檢測到了 debug 調試,那么說明該 apk 正被破解。

0x00 簡介

在 apk 被調試的時候,有很多特征可以檢測到,比如 hook so的時候需要分析 maps文件確定內存加載的位置,還有調試器很 android 設備進行接口通訊需要開啟端口映射。這些特征都可以被作為檢測 debug 的一種手段。

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

檢測 debug 是為了防止應用被逆向動態分析,所以檢測的方法也都是采用 native 開發提高被逆向的成本。

0x01 debug開關

debug 開關默認在編譯 release 版本的時候自己會關閉,但是你還是可以通過顯示的設置把他打開。但是如果你這么干了,估計你老板要打死你。

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 版本基本沒有會開啟的除非失誤。

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},所以必須每次都在循環內復制。我也不知道為什么,試了好久。

 

來自:https://segmentfault.com/a/1190000007160384

 

 本文由用戶 xcllch 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!