디버깅(DEBUGGING)

디버깅은 애플리케이션의 동작을 분석하고, 오류(버그)를 찾아 수정하는 과정을 의미합니다.

보통 개발자들이 앱을 개발할 때 오류를 찾기 위해 디버깅을 수행하지만, 보안 연구자나 해커들도 앱의 내부 동작을 분석하거나 취약점을 찾기 위해 활용합니다.

디버깅 방법으로는 ADB, 프리다(함수 후킹), 네이티브 디버깅(GDB, LLDB, IDA, etc.) 등의 방법이 있습니다.

일반적으로 모바일 앱에서는 프로그램을 악용할 수 없도록 안티 디버깅 기능을 사용하고 있는데요.   ANDITER 앱을 통해 안티 디버깅 기법과 우회 방법에 대해 알아보도록 하겠습니다.


1. 안티 디버깅(ANTI DEBUGGING)  

1-1. BYPASS TracerPID : TracerPid 비정상 값 탐지 방식

/proc/self/status | grep TracerPid 커맨드를 입력하여 디버깅 여부 확인하는 방식입니다. 이 값이 0일 경우에는 안드로이드 기기가 현재 디버깅 상태가 아니라는 것을 의미합니다. 만약 해당 기기가 디버깅 상태라면, TracePid 값은 현재 해당 프로세스(앱)를 디버깅 하고 있는 프로세스의 ID를 표시하게 됩니다.

c1q:/ # cat /proc/3855/status | grep Tracer
TracerPid:      0

strace 를 이용한 TracerPid 값 조작

strace는 애플리케이션이 실행될 동안 시스템 콜 함수들과 시그널 처리를 추적할 수 있는 디버그 도구로, 실행중인 애플리케이션 대상으로 사용 가능하며 사용 시 TracerPid 값이 strace 프로세스 Pid 값으로 변경됩니다. strace 바이너리는 아래 URL에서 다운로드 받을 수 있습니다.

c1q:/data/local/tmp # ./strace -f -p `pgrep anditer`
Process 3855 attached with 25 threads
 
[ . . . ]
 
c1q:/ $ cat /proc/3855/status | grep Trace
TracerPid:      3855

소스코드를 살펴보면 isCheckDebuggerState 함수에서 프로세스의 상태정보(/proc/self/status)를 기반으로 디버깅 여부를 탐지하는 것을 볼 수 있는데요. “TracerPid” 값이 0이 아닌 임의의 값이거나, “Status” 값이 T(Stopped)로 설정되어 있으면 디버깅 중인 것으로 판단합니다.

따라서, isCheckDebuggerState 후킹을 통해 리턴 값을 변조하거나 StringsKt.contains$default(...) 함수의 인자(“TracerPid”, “state”)를 임의의 덤프 값으로 덮어씌우면 우회가 가능합니다.


1-2. BYPASS DEBUGGABLE : build.prop[debuggable] 비정상 값 탐지 방식

안드로이드 앱은 아래 2가지 경우에 애플리케이션 디버깅을 통해 동적 분석이 가능합니다.  

  • (1). Andoridmanifest.xml 파일에서 android:debuggable="true"로 설정된 경우  
  • (2). /system/build.prop 파일에서 ro.debuggable=1로 설정된 경우

먼저, 기기에서 ro.debuggable 값을 확인하는 방법은 다음과 같습니다.

# (1) ro.debuggable
c1q:/data/local/tmp # cat /system/build.prop | grep -i ro.debuggable
ro.debuggable=0
 
# (2) debuggable
c1q:/ # getprop | grep -i debuggable
[ro.debuggable]: [0]

아래 이미지를 보면 isCheckDebuggable 함수에서 new getSystemProperty().prop("ro.debuggable")를 생성한 뒤 ro.debuggable 속성 값을 검증하는 안티 디버깅 로직이 존재합니다.


따라서, 아래와 같이 후킹 코드를 작성하여 우회해 볼 수 있겠습니다.
(StringsKt 클래스의 equals$default 함수를 후킹하면 됩니다.)

Java.perform(function () {
    var StringsKt = Java.use('kotlin.text.StringsKt');
    StringsKt.equals$default.overload('java.lang.String', 'java.lang.String', 'boolean', 'int', 'java.lang.Object').implementation = function(str1, str2, z, i, obj) { 
        var stackTrace = Java.use('android.util.Log').getStackTraceString(Java.use('java.lang.Exception').$new());
        
        if (stackTrace.includes('com.playground.anditer.DebuggerDetector.isCheckDebuggable')) {
            console.log(`Orignial StringsKt.equals$default(${str1}, ${str2}, ${z}, ${i}, ${obj}) => ${this.equals$default(str1, str2, z, i, obj)}`);
            return this.equals$default(0, str2, z, i, obj)
        }
        return this.equals$default(str1, str2, z, i, obj)
    }
 
});

테스트 시 기기에 설정된 ro.debuggable 값이 0으로 설정되어 있었기 때문에  ro.debuggable 값이 1이라는 가정 하에 실습을 진행했습니다.



1-3. BYPASS DEBUG TOOLS : 디버깅 도구 탐지 방식

대표적인 안드로이드 디버깅 도구로는 JEB, IDA, GDA, Ghidra가 있는데요!~ JEB의 경우에는 디버깅을 하여도 TracerPid 값이 변하지 않는다는 특징이 있다고 합니다. 때문에 특정 사용자가 JEB으로 디버깅을 시도할 때는 TracerPid 값이 아니라 JEB에서 사용되는 특정 라이브러리 파일을 탐지하는 방식으로 안티 디버깅을 시도합니다. 라이브러리 파일(.so)을 확인하기 위해서는 /proc/self/maps 파일을 확인해봐야 하는데요. /proc/self/maps는 해당 프로세스가 맵핑된 메모리 주소 공간을 의미하는데, 이 경로를 살펴보면 앱에서 사용된 .so 파일을 확인해 볼 수 있습니다.

c1q:/ # cat /proc/3855/maps
[ . . . ]
d527000-7f9d528000 r--p 00006000 fd:17 103                            /apex/com.android.art/lib64/libnativehelper.so
7f9d528000-7f9d529000 rw-p 00006000 fd:17 103                            /apex/com.android.art/lib64/libnativehelper.so
[ . . . ]

만약 아래의 소스코드와 같이 특정 라이브러리의 존재 여부를 확인하는 코드가 있다면 아래의 방법으로 우회를 시도해 볼 수 있습니다.

  • (1) isCheckCallPPID 함수 후킹 (리턴 값 변조)
  • (2) isFile().exists() 함수 후킹 (리턴 값 변조)
  • (3) StringsKt.contains$default 함수 후킹 (인자 값 변조(덤프 값 전달))

1-4. BYPASS DEVELOP MODE : 개발자 모드 탐지 방식

1-5. BYPASS DEBUGGING MODE : USB 디버깅 모드 탐지 방식

DEVELOP/DEBUGGING MODE는 안티 디버깅에 사용되는 방식이 동일했으므로 함께 진행하겠습니다.

안티 디버깅(DEVELOP/DEBUGGING MODE) 코드

설정 항목 (Setting Name)값 (Value)상태 (Status)설명
adb_enabled1활성화USB 디버깅(ADB) 사용 가능
adb_enabled0비활성화USB 디버깅(ADB) 차단
development_settings_enabled1활성화설정 메뉴에 ‘개발자 옵션’ 표시
development_settings_enabled0비활성화설정 메뉴에서 ‘개발자 옵션’ 숨김


위 함수들은 아래의 자바스크립트 코드로 간단히 우회할 수 있습니다.

Java.perform(function() {
    var Settings_Global = Java.use('android.provider.Settings$Global');
    Settings_Global.getInt.overload('android.content.ContentResolver', 'java.lang.String', 'int').implementation = function (resolver, name, def) {
        console.log(`Settings_Global.getInt(${resolver}, ${""}, ${def}) => ${this.getInt(resolver, "", def)}`);
        return this.getInt(resolver, "", def);
    }
})


1-6. BYPASS CONNECT USB : USB 연결 탐지 방식

이번에는 isCheckConnectUSB() 함수에서 USB 연결 여부를 확인하고 있습니다.
이 또한 함수를 후킹하여 리턴 값을 변조하는 방식으로 쉽게 우회가 가능합니다.
isCheckConnectUSB() 함수 혹은 this.context.registerReceiver(...) 함수의 리턴 값을 변조하면 됩니다.


Java.perform(function () {
    var Context = Java.use("android.content.Context");
    var DebuggerDetector = Java.use('com.playground.anditer.DebuggerDetector');
 
    DebuggerDetector.isCheckConnectUSB.implementation = function() {
        console.log(this.isCheckConnectUSB());
        return false;
    }
});


REFERENCE

  1. https://github.com/naroSEC/Anditer?tab=readme-ov-file
  2. https://www.igloo.co.kr/security-information/anditer%eb%a5%bc-%ed%99%9c%ec%9a%a9%ed%95%9c-%ec%95%88%eb%93%9c%eb%a1%9c%ec%9d%b4%eb%93%9c-%ec%9c%84%ed%98%91-%ed%83%90%ec%a7%80-%eb%b0%8f-%ec%9a%b0%ed%9a%8c-%eb%b0%a9%ec%95%88-part-2-%eb%94%94/