Comprehensive Guide to Anti-Debugging Techniques in Android Reverse Engineering

Comprehensive Guide to Anti-Debugging Techniques in Android Reverse Engineering

It has been a while since I last wrote an article. Recently, I have been working on several bank projects, and most financial apps have anti-debugging measures. Today, I would like to share some anti-debugging strategies that I frequently encounter in these projects. The following methods are based on practical experiences, and I welcome readers to contribute any additional anti-debugging strategies.

Thanks: Special thanks to the experts from the Kanxue Forum, CSDN (Xiaodao Security), CSDN (Bagaboy), CSDN (iopoint), and others for their articles.

  • 1: Port Number Detection, targeting android_server or frida_server ports

IDA Dynamic Debugging Steps:

1. Create an emulator (preferably use a real device)
2. In IDA, find android_server (in the dbgsrv directory)
3. Push the android_server file to the phone: adb push filename /data/local/tmp
4. Open a cmd window: run android_server   
      1) adb shell to connect to the phone   
      2) Give the highest permissions: su    
      3) Navigate to /data/local/tmp: cd /data/local/tmp    
      4) Give android_server the highest permissions: chmod 777 android_server    
      5) Check if android_server has permissions: ls -l    
      6) Run android_server: ./android_server (default port: 23946)    
      Supplement: Run android_server and change the port: ./android_server -p port_number
5. Port forwarding: adb forward tcp:port_number tcp:port_number (the previously forwarded port number remains the same)
6. Open DDMS: observe the program's port number
7. Suspend the program: adb shell am start -D -n package_name/class_name    
Example: adb shell am start -D -n com.example.javandk1/.MainActivity    
Supplement: At this point, observe DDMS; a red bug appears in front of the debugged program;
8. In IDA, check three items    
    1) Open IDA, select debugger - second item - Remote ARMlinux (fourth item)   
    2) Add hostname and port: hostname: host number (default 127.0.0.1) port: port number (previously used by android_server or the forwarded port number)   
    3) Process list appears: select the program to debug (you can ctrl+f to search for the package name)   
    4) After entering, check three items:    
        Suspend on process entry point    
        Suspend on thread start/exit    
        Suspend on library load/unload
Supplement: You can directly F9 (there's a triangle in the upper left corner) to run the program and let go; you can also execute step nine directly, then run the program in IDA

IDA Anti-Debugging Strategy: Monitor the android_server file port information, default 23946 (frida anti-debugging similarly uses frida_server port 27042); for IDA, read /proc/net/tcp to find the default 23946 port for IDA remote debugging (or execute the command netstat -apn). If it is in a listening state, it indicates that debugging may exist.

void anti_serverport() {
    const int bufsize=512;
    char filename[bufsize];
    char line[bufsize];
    int pid =getpid();
    
    sprintf(filename,"/proc/net/tcp");
    FILE* fd=fopen(filename,"r");
    
    if(fd!=NULL){
    while(fgets(line,bufsize,fd)){ 
    if (strncmp(line, "5D8A", 4)==0){   // 5D8A in decimal is 23946
    int ret = kill(pid, SIGKILL);
                }
            }
        }
    fclose(fd);
}

Bypass Strategy: Port forwarding (change default connection ports for IDA, frida, etc.).

  • 2: Debugger Name Detection (Traverse processes to find similar android_server, gdbserver, gdb, etc. debugging processes)

Anti-Debugging Strategy: Detect traces of program execution, also applicable to detection of similar tools, such as package files, binary files, library files, processes, temporary files, etc. In this example, the target is fridaserver, which communicates with frida via TCP. At this point, we can use Java to traverse the running process list to check if fridaserver is running.

public boolean checkRunningProcesses() {
  boolean returnValue = false;
  // Get currently running application processes
  List list=manager.getRunningServices(300);
    if(list != null){
    String tempName;
    for(int i=0;i

However, the method of detecting the name “fridaserver” is sometimes unreliable. Frida’s various modes are used for injection, so we can utilize the point where frida is mapped to memory during execution. The most direct method is to check the loaded libraries one by one.

char line[512];
FILE* fp;
fp = fopen("/proc/self/maps", "r");
if (fp) {
    while (fgets(line, 512, fp)) {
        if (strstr(line, "frida")) {
            /* Evil library is loaded. Do something… */
        }
    }
    fclose(fp);
    } else {
       /* Error opening /proc/self/maps. If this happens, something is off. */
    }
}

Alternatively, a deeper anti-debugging strategy is to scan for frida’s library features in memory, using the string “LIBFRIDA” which appears in all versions of frida-gadget and frida-agent. The following code scans all executable segments found in /proc/self/maps:

static char keyword[] = "LIBFRIDA";
num_found = 0;
int scan_executable_segments(char * map) { 
    char buf[512];
    unsigned long start, end;
    sscanf(map, "%lx-%lx %s", &start, &end, buf); 
    if (buf[2] == 'x') {
        return (find_mem_string(start, end, (char*)keyword, 8) == 1);
    } else {
        return 0;
    }
}
 
void scan() {
    if ((fd = my_openat(AT_FDCWD, "/proc/self/maps", O_RDONLY, 0)) >= 0) {
 
    while ((read_one_line(fd, map, MAX_LINE)) > 0) {
 
       if (scan_executable_segments(map) == 1) {
 
            num_found++;
        }
    }
    if (num_found > 1) {
        /* Frida Detected */
    }
}
}

Bypass Strategy: Rename features containing “frida”, or use a frida-server that avoids detection: https://github.com/hzzheyang/strongR-frida-android/releases

  • 3: Using ptrace function for anti-debugging

Linux system debuggers like gdb are implemented through the ptrace system call. In Android hardening, ptrace itself is a commonly used anti-debugging measure to prevent debuggers from attaching. The principle of ptrace anti-debugging is that a process can only be attached by one process. When an application uses ptrace anti-debugging, for example, when attaching, this phenomenon occurs.

Check the process in /proc/self/status; you will see that TracerPid is not 0; its value is the pid of the parent process that attached it, which is the zygote process.

The principle is simple; just add ptrace(PTRACE_TRACEME);.

Bypass Strategy: There are many methods to bypass it, the most common is to use ptrace to bypass ptrace.

  1. Use ptrace to attach to the zygote process.
  2. Intercept zygote’s fork call, and when the fork child process, get the current child process name, and determine if it is the application we want. If so, save the child process pid.
  3. After obtaining the child process pid, intercept the system call of the child process to determine if this system call is ptrace and that the parameter is PTRACE_TRACEME.
  4. When the specified system call is intercepted, modify the call parameters to make ptrace(PTRACE_TRACEME); fail.
// Intercept the fork of the zygote process
        if (ptrace(PTRACE_SETOPTIONS, pid, (void *)1, (void *)(PTRACE_O_TRACEFORK))) {
            printf("FATAL ERROR: ptrace(PTRACE_SETOPTIONS, ...);");
            return -1;
        }
        ptrace(PTRACE_CONT, pid, (void *)1, 0);
        int t;
        int stat;
        int child_pid = 0;
        for (;;) {
            t = waitpid(-1, &stat, __WALL | WUNTRACED);
 
            // Check if the current fork program is the specified application
            if (t != 0 && t == child_pid) {
                if (debug > 1)
                    printf(".");
                char fname[256];
                sprintf(fname, "/proc/%d/cmdline", child_pid);
                int fp = open(fname, O_RDONLY);
                if (fp < 0) {
                    ptrace(PTRACE_SYSCALL, child_pid, 0, 0);
                    continue;
                }
                read(fp, fname, sizeof(fname));
                close(fp);
 
                if (strcmp(fname, appname) == 0) {
                    if (debug)
                        printf("zygote -> %s\n", fname);
 
                    // Detach from zygote
                    ptrace(PTRACE_DETACH, pid, 0, (void *)SIGCONT);
 
                    // Now perform on new process
                    pid = child_pid;
                    break;
                } else {
                    ptrace(PTRACE_SYSCALL, child_pid, 0, 0);
                    continue;
                }
            }
if (zygote) {
        // After obtaining the specified process pid, intercept its system_call
        ptrace(PTRACE_SYSCALL, pid, 0, 0);
        while (1) {
            waitpid(pid, NULL, 0);
            // Modify call parameters before the system call
            hookSysCallBefore(pid);
            ptrace(PTRACE_SYSCALL, pid, 0, 0);
 
            waitpid(pid, NULL, 0);
            // Modify system call result
            hookSysCallAfter(pid);
            ptrace(PTRACE_SYSCALL, pid, 0, 0);
        }
    }
void hookSysCallBefore(pid_t pid) {
    struct pt_regs regs;
    int sysCallNo = 0;
 
    ptrace(PTRACE_GETREGS, pid, NULL, &regs);
    sysCallNo = getSysCallNo(pid, &regs);
 
    if (sysCallNo == __NR_ptrace) {
        regs.ARM_r0 = 0; // Fake a successful return value
        ptrace(PTRACE_SETREGS, pid, NULL, regs);
        printf("__NR_ptrace: %d\n", regs.ARM_r0);
    }
}
 
void hookSysCallAfter(pid_t pid) {
    struct pt_regs regs;
    int sysCallNo = 0;
 
    ptrace(PTRACE_GETREGS, pid, NULL, &regs);
    sysCallNo = getSysCallNo(pid, &regs);
 
    if (sysCallNo == __NR_ptrace) {
        regs.ARM_r0 = PTRACE_PEEKTEXT;
        ptrace(PTRACE_SETREGS, pid, NULL, regs);
        printf("__NR_ptrace: %ld\n", regs.ARM_r0);
    }
}
  • 4: Using Code Execution Time Detection (This situation is rarely encountered in projects)

The execution time of code in debugging and non-debugging states is different; if the two time values differ significantly, it indicates that the code flow has been debugged. This is because the debugger stopped to observe the execution of this segment of code step by step, thus the execution time of this part of the code far exceeds that of normal execution time. Of course, the most important thing is to obtain the system time; there are many ways to obtain system time; here is one:

int getTimeOfDay(struct timeval *tv, struct timezone *tz);
void anti_debug() {
    int pid = getpid();
    struct timeval t1;
    struct timeval t2;
    struct timeval tz;
    getTimeOfDay(&t1, &tz);
    getTimeOfDay(&t2, &tz);
    int TimeOff = (t2.tv_sec) - (t1.tv_sec);
    if (TimeOff > 1) {
        int ret = kill(pid, SIGKILL);
        return;
    }
}

Bypass Strategy: Reverse analyze to find the time detection function, manually nop it or modify the return value.

  • 5: Using inotify to monitor the file system for anti-debugging

For example: /proc/pid/maps, /proc/pid/mem, /proc/pid/pagemem

Background Introduction: In Linux, inotify can monitor file system events (open, read/write, delete, etc.), source reference: https://linux.die.net/man/7/inotifyinotify. The most common APIs are as follows:

  1. inotify_init: A system call used to create an inotify instance and returns a file descriptor pointing to that instance.
  2. inotify_add_watch: Adds monitoring for files or directories and specifies which events to monitor.
  3. read: Reads cached event information containing one or more event information.
  4. inotify_rm_watch: Removes monitoring items from the monitoring list.

By monitoring key file changes in the /proc/pid folder (maps read, mem read, etc.), if you want to view a process’s virtual address space or want to dump memory, it will trigger open or read events. As long as these events are received, it indicates that the process is being debugged, directly kill the main process. The main code is as follows:

// Fork child process calling this function and passing the parent process pid
void AntiDebug(int *ppid) {
    char buf[1024], readbuf[MAXLEN];
    int pid, wd, ret, len, i;
    int fd;
    fd_set readfds;
    // Prevent debugging of the child process
    ptrace(PTRACE_TRACEME, 0, 0, 0);
    fd = inotify_init();
    sprintf(buf, "/proc/%d/maps", ppid);
    
    // wd = inotify_add_watch(fd, "/proc/self/mem", IN_ALL_EVENTS);
    wd = inotify_add_watch(fd, buf, IN_ALL_EVENTS);
    if (wd < 0) {
        LOGD("can't watch %s", buf);
        return;
    }
    while (1) {
        i = 0;
        // Note to initialize the fd_set
        FD_ZERO(&readfds);
        FD_SET(fd, &readfds);
        // First parameter fixed +1, second parameter is the read fdset, third is the write fdset, last is the waiting time
        // Last NULL means blocking
        ret = select(fd + 1, &readfds, 0, 0, 0);
        if (ret == -1)
            break;
        if (ret) {
            len = read(fd, readbuf, MAXLEN);
            while (i < len) {
                // The returned buf may contain multiple inotify_event
                struct inotify_event *event = (struct inotify_event*)&readbuf[i];
                LOGD("event mask %d\n", (event->mask & IN_ACCESS) || (event->mask & IN_OPEN));
                // Here monitor read and open events
                if ((event->mask & IN_ACCESS) || (event->mask & IN_OPEN)) {
                    LOGD("kill!!!!\n");
                    // If the event occurs, kill the parent process
                    int ret = kill(ppid, SIGKILL);
                    LOGD("ret = %d", ret);
                    return;
                }
                i += sizeof(struct inotify_event) + event->len;
            }
        }
    }
inotify_rm_watch(fd, wd);
close(fd);
}

Bypass Strategy: Similar to before, find the detection method and nop it.

  • Referenceshttps://bbs.kanxue.com/thread-272452.htm#msg_header_h3_7
    https://wuyaogexing.com/70/366676.html
    https://blog.csdn.net/c_kongfei/article/details/113242964
    https://blog.csdn.net/jinganggiao/article/details/126932091
    https://blog.csdn.net/iopoint/article/details/118518300

🌟 Welcome to students learning Android mobile security, reverse engineering, and those preparing for mobile security (Android) interviews to join my knowledge circle. The atmosphere is good, and there are many dry goods, plus monthly incentives waiting for you to participate!

Comprehensive Guide to Anti-Debugging Techniques in Android Reverse Engineering

Leave a Comment

Your email address will not be published. Required fields are marked *