Debugger 탐지 우회

DOUIK·2022년 7월 30일
0

Android

목록 보기
6/7

Debugger TracerPid 검사

디버깅 탐지를 위해 TracerPid를 검사함

  • TracerPid : 프로세스 상태정보 필드 중 하나로, 해당 프로세스를 디버깅하고 있는 프로세스의 id를 나타내며 디버깅 되고 있지 않으면 TracerPid의 필드는 0임

코드 분석

checkProcStatus 함수 분석

DebuggingDetector.checkProcStatus()

public Boolean checkProcStatus() {
    File f = new File("/proc/self/status");
    try {
        FileInputStream fio = new FileInputStream(f);
        try (BufferedReader br
                     = new BufferedReader(new InputStreamReader(fio))) {
            String line;
            while ((line = br.readLine()) != null) {
                if (line.contains("TracerPid:")) {
                    return Integer.parseInt(line.substring(11)) != 0;
                }
            }
            fio.close();
        } catch (IOException ignore) {
        }
    } catch (FileNotFoundException ignore) {
    }
    return false;
}
  1. BufferedReader 클래스의 readLine 함수를 사용해서 프로세스 상태 정보를 저장하는 /proc/self/status 파일을 불러옴
  2. 해당 파일을 한 줄씩 읽어서 TracerPid 필드를 찾음. 만약 해당 필드가 없다면 디버깅되지 않은 것으로 판단하고 false를 반환함
  3. 만약 TracerPid 값이 0이 아니라면 디버깅 중인 것으로 판단해 true를, 0이라면 디버깅 중이 아닌 것으로 판단해 false를 반환함

환경 설정

TracerPid는 앱에 부착되어 있는 프로세스의 id를 기록함. 디버거와 같은 프로세스가 앱에 부착되면 디버거의 pid가 TracerPid 필드의 값으로 기록된다. 따라서 strace와 같은 디버깅 유틸리티를 사용하더라도 TracerPid의 값이 strace의 pid로 변경되어 디버깅 상태라고 판단하게 됨.

strace
strace는 프로세스에서 생성하는 시스템 콜을 측정하고, 시스템 호출이 무엇을 반환하는지 확인할 수 있는 디버깅 유틸리티이다.
더 자세한 설명

설정 방법

  1. strace를 Dream detector 앱에 부착
strace -fp `pgrep dream_detector`

strace 옵션
-f : fork 시스템 콜의 결과로 생성된 자식 프로세스를 추적한다.
-p pid : 지정한 PID로 프로세스를 추적한다.

  • pgrep : ps + grep. 프로세스를 이름 기반으로 검색하며 PID 값을 출력함

우회 아이디어

1. 함수 반환값 조작

TracerPid의 값을 가져오는 Integer.parseInt(line.substring(11))의 반환값을 0으로 조작하거나, checkProcStatus 함수 자체의 반환값을 false로 고정해 우회

2. 읽어온 파일 내용 조작

readLine으로 프로세스 상태 정보 파일을 한 줄씩 읽을 때 이 함수를 후킹해서 내용에 "TracerPid:0" 이라는 라인을 강제로 주입함

우회 스크립트 작성

1. checkProcStatus 함수 반환값 변조

Bypass_Debug_TracerPid_modifyCheckProcStatusRet.js

function modifyCheckProcStatusRet(){
    Java.perform(function() {
        var DebuggerDetector = Java.use("android.com.dream_detector.DebuggingDetector");
    
        DebuggerDetector.checkProcStatus.implementation = function() {
            return Java.use("java.lang.Boolean").$new(false);
        };
    });
}

modifyCheckProcStatusRet();

Java.use 함수로 Frida 내에서 DebuggingDetector 클래스에 접근할 수 있도록 Wrapper를 제공받고 이를 통해 클래스 내의 checkProcStatus 함수에 접근해서 implementation을 사용해 frida 스크립트로 구현한 함수로 해당 함수를 덮음. 반환값이 항상 false인 함수를 구현해 checkProcStatus 함수를 우회함

2. BufferedReader.readLine 결과 변조

Bypass_Debug_TracerPid_modifyFileContent.js

function modifyFileContent(){
    Java.perform(function() {
        var File = Java.use("java.io.File");
        var constructor = File.$init.overload('java.lang.String');
    
        constructor.implementation = function(pathname) {
            if (pathname == "/proc/self/status") {
                var BufferedReader = Java.use("java.io.BufferedReader");
                var readLine = BufferedReader.readLine.overload();
    
                readLine.implementation = function() {
                    var ret = readLine.call(this);
                    if (ret.includes("TracerPid:"))
                        ret = "TracerPid: 0";
                        console.log(ret);
                    return ret;
                };
            }
            console.log(pathname);
            return constructor.call(this, pathname);
        };
    });
}

modifyFileContent();

Java.use 함수로 java.io.File 클래스에 접근할 수 있도록 Wrapper를 제공받고 이를 통해 클래스 내의 File 클래스의 생성자를 오버로딩함. 오버로딩한 생성자를 implementation을 사용해 frida 스크립트로 구현함. 이를 통해서 File 객체 생성시에 String 값이 인자로 전달되면 frida 스크립트로 구현한 생성자를 통해 객체가 생성되고 File 객체 생성시 인자로 전달된 pathname이 /proc/self/status라면, 후킹하고자 하는 checkProcStatus 함수 내의 File 객체 생성 시점과 동일한 시점임을 알 수 있다. 이 때 Java.use 함수를 사용해서 Java.io.BufferdReader 클래스의 Wrapper를 가져와 이를 통해 readLine 함수를 오버로드해서 함수 실행결과에 "TracerPid: 0"을 추가함.

Debugger 경로 검사

디버깅 탐지를 위해 디버깅용 파일 유무 검사

코드 분석

checkPath 함수 분석

DebuggingDetector.checkPath()

public boolean checkPath() {
    // AccessDeniedException
    String dirName = "/data/local/tmp";
    try {
        AtomicBoolean found = new AtomicBoolean(false);
        Files.list(new File(dirName).toPath())                
                .limit(200)
                .forEach(path -> {
                    if (path.toString().toLowerCase(Locale.ROOT).contains("gdb"))   
                        found.set(true);
                });
        if (found.get()) {         
            return true;
        }
    } catch (IOException ignored) {
    }
    return false;
}
  1. 안드로이드 임시 경로인 /data/local/tmp 디렉토리를 File 객체를 통해 접근합니다.
  2. Files.list 함수를 통해 디렉토리 내의 파일 중 gdb라는 문자열을 포함한 파일 이름을 가진 경우 found변수의 값을 true로 세팅합니다.
  3. found 변수값이 True인 경우, gdb문자열이 포함된 파일명을 가진 파일이 탐지된 것으로 간주하여 True를 반환하고, found값이 False인 경우, 탐지되지 않은 것으로 판단하고 False를 반환합니다.

환경 설정

설정 방법

  1. /data/local/tmp에 권한 부여
chmod 777 /data/local/tmp
  1. 파일명에 gdb 문자열을 포함한 파일 생성
touch /data/local/tmp/gdb

우회 아이디어

1. 함수 반환값 조작

heckPath함수에서 파일명에 gdb가 있는지 검사한 결과 False라면 디버깅 관련 파일이 탐지되지 않은 것으로 판단합니다. 따라서 checkPath 자체의 반환값을 고정적으로 False로 만들어준다면 디버깅 관련 파일이 존재하더라도 이를 탐지되지 않은 것처럼 조작할 수 있습니다.

2. 검사 파일 경로 조작

heckPath 함수에서 디버깅 파일의 유무를 검사할 경로에 접근할 때에 File 객체 생성 인자로 경로를 전달합니다. 이 때 전달하는 경로를 안드로이드 임시 경로인 /data/local/tmp 디렉토리가 아닌 다른 디렉토리로 경로를 조작하면 gdb 파일이 임시 경로에 존재해도 탐지할 수 없게됩니다.

우회 스크립트 작성

1. checkPath 반환 값 변조

Bypass_Debug_checkPath_modifyCheckPathRet.js

function modifyCheckPathRet(){
    Java.perform(function() {
        var DebuggerDetector = Java.use("android.com.dream_detector.DebuggingDetector");
        DebuggerDetector.checkPath.implementation = function() {
            return false;
        };
    });
}
modifyCheckPathRet();

기존 checkPath 함수를 implementation을 사용해서 위 함수로 덮어씀

2. File 클래스 인자 변조

Bypass_Debug_checkPath_modifyFilePath.js

function modifyFilePath(){
    Java.perform(function() {
        var File = Java.use("java.io.File");
        var constructor = File.$init.overload('java.lang.String');
    
        constructor.implementation = function(pathname) {
            if (pathname == "/data/local/tmp")
                return constructor.call(this, "/not_exists");
            return constructor.call(this, pathname);
        };
    });
}
modifyFilePath();

디버깅 파일 유무 검사 경로는 File 클래스의 인자(pathname)로 전달됨. pathname이 /data/local/tmp이면, 후킹하고자 하는 checkProcStatus 함수 내의 File 객체 생성 시점과 동일한 시점임을 알 수 있다. 이 때 "/not_exists" 문자열을 인자로 전달하면서 구현된 생성자로 File 객체 생성을 시도하면 File 객체가 접근하는 경로는 기존의 안드로이드 임시 경로가 아니라 "/not_exists" 경로에 접근한다. 이 경로는 기기 내에 없는 경로이므로 무조건 false를 반환한다.

Debugger 속성 검사

디버깅 탐지를 위해 시스템 속성을 검사함

코드 분석

checkProp 함수 분석

DebuggingDetector.checkProp()

@RequiresApi(api = Build.VERSION_CODES.O)
public boolean checkProp() {
    String property_value = Utils.getProp(mContext, "ro.debuggable");              
    return property_value != null && property_value.equals("1");                   
}
  1. 기기의 시스템 속성을 확인하는 getProp함수에 인자로 ro.debuggable을 전달하여 해당 필드값을 property_value 변수에 저장합니다.
  2. property_value 변수가 null이 아니고, 1이면 디버깅 되고 있는 상태로 간주하고 True를 반환합니다. 만약 null이거나 1이 아니라면 디버깅되지 않은 상태로 판단하고 False를 반환합니다.

환경 설정

AVD에서는 기본적으로 ro.debuggable 값이 1로 설정되어 있음. AVD에서 시스템 속성은 getprop 명령으로 확인할 수 있다.

getprop ro.debuggable
1

만약 실 기기에서 실습을 진행중이거나 타 에뮬레이터를 통해 실습하는 경우, 값이 1로 설정되어 있지 않을 수도 있습니다. ro.debuggable 시스템 속성은 read-only 속성이기 때문에 일반적인 방법으로는 값 변경이 불가능합니다. 따라서 /dev/__properties__/ 에 존재하는 시스템 속성 관련 파일 값을 직접 변경하는 방법을 사용해야 합니다.

설정 방법

  1. 시스템 속성 변경 툴 (https://github.com/liwugang/android_properties) 다운로드 및 AVD 기기 내부에 저장

  2. ro.debuggable속성 값을 1로 설정

adb shell
$ su
# getprop ro.debuggable
0
# /data/local/tmp/system_properties_x86_64 ro.debuggable 1
set ro.debuggable == 1 success
[ro.debuggable]:[1]
# getprop ro.debuggable
1

우회 아이디어

1. 함수 반환값 조작

checkProp 함수에서 시스템 속성에 ro.debuggable 필드 값이 1인지 검사한 결과 1이 아닌 값이거나 null이라면 False를 반환합니다. checkProp함수의 반환값이 False라면 디버깅 관련 파일이 탐지되지 않은 것으로 판단합니다. 따라서 checkProp 함수 자체의 반환값을 False로 고정하여 해당 검사를 우회할 수 있습니다.

2. 함수 인자 조작

해당 함수는 기기의 시스템 속성을 읽어올 때 Utils.getProp함수를 사용합니다. getProp함수는 시스템 속성에서 전달받은 인자와 동일한 키를 찾아 해당 키에 대한 값을 반환합니다. 만약 시스템 속성에 전달받은 인자와 동일한 키가 없다면 null을 반환합니다. 따라서 getProp함수 호출 시에 전달되는 인자를 변조하여 존재하지 않는 속성을 전달하면 getProp의 반환값이 null이 되면서 checkProp함수를 우회할 수 있습니다.

우회 스크립트 작성

1. checkProp 반환 값 변조

Bypass_Debug_checkProp.js

function modifyCheckPropRet() {
    Java.perform(function() {
        var DebuggerDetector = Java.use("android.com.dream_detector.DebuggingDetector");
    
        DebuggerDetector.checkProp.implementation = function() {
            return false;
        };
    });
}
modifyCheckPropRet();

2. Utils.getProp 인자 변조

Bypass_Debug_checkProp.js

function modifyGetPropParam() {
    Java.perform(function() {
        var Utils = Java.use("android.com.dream_detector.Utils");
    
        Utils.getProp.implementation = function(context, property) {
            if (property == "ro.debuggable") 
                return Utils.getProp.call(this, context, "not_exist");
            return Utils.getProp.call(this, context, property);
        }
    });
}
modifyGetPropParam();

getProp함수 호출 시 전달된 인자값이 ro.debuggable이라면, 후킹하고자하는 checkProp함수 내의 getProp호출 시점과 동일한 시점임을 알 수 있다. 이 때, ro.debuggable대신 not_exist 문자열을 함수의 인자로 전달하면 getProp함수는 존재하지 않는 시스템 속성인 not_exist 를 찾게되면서, null을 반환해서 우회할 수 있다.

0개의 댓글