APK逆方向の1つ:debugの監視


開発と逆方向の過程でダイナミックデバッグが必要な場合が多く、開発時にはandroidを開発したIDEでデバッグすることができ、native層もデバッグすることができ、Android Studioはとっくにnativeのdebugデバッグを行うことができるようになった.しかしrelease後のapkでdebugデバッグが検出された場合、apkが解読されていることを示します.
原文リンク:APK逆一向:モニタdebug
0 x 00概要
apkがデバッグされると、hook soの場合mapsファイルを分析してメモリロードの位置を特定する必要があり、デバッガがandroidデバイスでインタフェース通信を行うにはポートマッピングを開く必要があるなど、多くの特徴が検出されます.これらの特徴はいずれもdebugを検出する手段とすることができる.
以下にいくつかのdebugを検出する方法を紹介し、いくつかのケースは構想を紹介するだけで、具体的な実現方法は変更する必要があります.例えば、tcpポートを監視し、サービス形式に変更してバックグラウンドで実行する必要があります.
検出debugはアプリケーションの逆動解析を防止するためであるため,検出方法もnative開発を用いて逆コストを向上させる.
ソースアドレス:anti-reverse
0 x 01 debugスイッチ
debugスイッチはデフォルトでreleaseバージョンをコンパイルするときに自分で閉じますが、表示された設定で開くことができます.しかし、もしあなたがこのようにしたら、あなたのボスはあなたを殺すと思います.
releaseバージョンでdebugデバッグを開き、プロジェクトbuildを変更します.gradleのbuildTypeパラメータ: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バージョンはエラーを除いてほとんど開かれません.
0 x 02単一ステップ検出
単一ステップデバッグの原理は簡単である:あるセグメントのコード実行時間を検出し、動的デバッグ時に必ずいくつかの場所でブレークポイントをおり、あるセグメントのコード実行時間が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");
    }
}

ここの間隔は実際の状況に応じて調整することができる.
0 x 03モニタTarcePid
apkが添付されたプロセスの場合/proc/{pid}/status,/proc/{pid}/task/{pid}/statusファイルに添付プロセスのpid:TarcePid : 1212が保存されます.この2つのファイルの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

0 x 04モニタtcpポート
デバッグを行うと必ずポートマッピングが開き、比較的よく使われる逆ツールが開いているポートを監視することができます.もちろん、不正行為者もポートを変更することができます.しかし,検出手段を理解することが前提である.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は「5 D 8 A」、すなわち10進数の23946であり、idaのデフォルトのポートである.
0 x 05モニタmapsファイル/proc/{pid}/mapsファイルにはapp実行のロードされたメモリ情報が保存されている.すべてのmapsファイルがACCESSまたはOPEN操作されるのはリスクがあります.
inotifyでmapsファイルをモニタリングできます.ここではサブスレッドを使用してループモニタリングを行います.
ここでは、2つの方法でモニタリングを行い、1つのブロックの方法、1つの非ブロックの方法(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}に初期化されるので、毎回ループ内でコピーする必要があります.私もなぜか分かりませんが、長い間試してみました.