在開發和逆向過程當中不少時候都須要動態調試,開發時候能夠用開發 android 的 IDE進行調試,native層也可用調試,Android Studio早就能夠進行 native 的debug調試了。可是在 release 後的 apk 若是還檢測到了 debug 調試,那麼說明該 apk 正被破解。java
原文連接: APK反逆向之一:監控debugandroid
在 apk 被調試的時候,有不少特徵能夠檢測到,好比 hook so的時候須要分析 maps文件肯定內存加載的位置,還有調試器很 android 設備進行接口通信須要開啓端口映射。這些特徵均可以被做爲檢測 debug 的一種手段。 git
下面介紹了幾種檢測 debug 的方式,有些案例只是介紹思路,具體的實現方式須要進行更改,例如監控 tcp 端口,須要改爲 service 形式在後臺運行。 github
檢測 debug 是爲了防止應用被逆向動態分析,因此檢測的方法也都是採用 native 開發提升被逆向的成本。bash
源碼地址:anti-reverseapp
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
單步調試的原理很簡單:檢測某段代碼執行的時間,動態調試的時候確定會在一些地方下斷點,若是一段代碼執行時間超過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"); } }
這裏的時間間隔能夠根據實際狀況做調整。
在 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
進行 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默認的端口。
/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},因此必須每次都在循環內複製。我也不知道爲何,試了很久。