디버깅 탐지를 위해 TracerPid를 검사함
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;
}
TracerPid는 앱에 부착되어 있는 프로세스의 id를 기록함. 디버거와 같은 프로세스가 앱에 부착되면 디버거의 pid가 TracerPid 필드의 값으로 기록된다. 따라서 strace와 같은 디버깅 유틸리티를 사용하더라도 TracerPid의 값이 strace의 pid로 변경되어 디버깅 상태라고 판단하게 됨.
strace
strace는 프로세스에서 생성하는 시스템 콜을 측정하고, 시스템 호출이 무엇을 반환하는지 확인할 수 있는 디버깅 유틸리티이다.
더 자세한 설명
strace -fp `pgrep dream_detector`
strace 옵션
-f : fork 시스템 콜의 결과로 생성된 자식 프로세스를 추적한다.
-p pid : 지정한 PID로 프로세스를 추적한다.
TracerPid의 값을 가져오는 Integer.parseInt(line.substring(11))의 반환값을 0으로 조작하거나, checkProcStatus 함수 자체의 반환값을 false로 고정해 우회
readLine으로 프로세스 상태 정보 파일을 한 줄씩 읽을 때 이 함수를 후킹해서 내용에 "TracerPid:0" 이라는 라인을 강제로 주입함
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 함수를 우회함
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"을 추가함.
디버깅 탐지를 위해 디버깅용 파일 유무 검사
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;
}
chmod 777 /data/local/tmp
touch /data/local/tmp/gdb
heckPath함수에서 파일명에 gdb가 있는지 검사한 결과 False라면 디버깅 관련 파일이 탐지되지 않은 것으로 판단합니다. 따라서 checkPath 자체의 반환값을 고정적으로 False로 만들어준다면 디버깅 관련 파일이 존재하더라도 이를 탐지되지 않은 것처럼 조작할 수 있습니다.
heckPath 함수에서 디버깅 파일의 유무를 검사할 경로에 접근할 때에 File 객체 생성 인자로 경로를 전달합니다. 이 때 전달하는 경로를 안드로이드 임시 경로인 /data/local/tmp 디렉토리가 아닌 다른 디렉토리로 경로를 조작하면 gdb 파일이 임시 경로에 존재해도 탐지할 수 없게됩니다.
function modifyCheckPathRet(){
Java.perform(function() {
var DebuggerDetector = Java.use("android.com.dream_detector.DebuggingDetector");
DebuggerDetector.checkPath.implementation = function() {
return false;
};
});
}
modifyCheckPathRet();
기존 checkPath 함수를 implementation을 사용해서 위 함수로 덮어씀
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를 반환한다.
디버깅 탐지를 위해 시스템 속성을 검사함
@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");
}
AVD에서는 기본적으로 ro.debuggable 값이 1로 설정되어 있음. AVD에서 시스템 속성은 getprop 명령으로 확인할 수 있다.
getprop ro.debuggable
1
만약 실 기기에서 실습을 진행중이거나 타 에뮬레이터를 통해 실습하는 경우, 값이 1로 설정되어 있지 않을 수도 있습니다. ro.debuggable 시스템 속성은 read-only 속성이기 때문에 일반적인 방법으로는 값 변경이 불가능합니다. 따라서 /dev/__properties__/ 에 존재하는 시스템 속성 관련 파일 값을 직접 변경하는 방법을 사용해야 합니다.
시스템 속성 변경 툴 (https://github.com/liwugang/android_properties) 다운로드 및 AVD 기기 내부에 저장
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
checkProp 함수에서 시스템 속성에 ro.debuggable 필드 값이 1인지 검사한 결과 1이 아닌 값이거나 null이라면 False를 반환합니다. checkProp함수의 반환값이 False라면 디버깅 관련 파일이 탐지되지 않은 것으로 판단합니다. 따라서 checkProp 함수 자체의 반환값을 False로 고정하여 해당 검사를 우회할 수 있습니다.
해당 함수는 기기의 시스템 속성을 읽어올 때 Utils.getProp함수를 사용합니다. getProp함수는 시스템 속성에서 전달받은 인자와 동일한 키를 찾아 해당 키에 대한 값을 반환합니다. 만약 시스템 속성에 전달받은 인자와 동일한 키가 없다면 null을 반환합니다. 따라서 getProp함수 호출 시에 전달되는 인자를 변조하여 존재하지 않는 속성을 전달하면 getProp의 반환값이 null이 되면서 checkProp함수를 우회할 수 있습니다.
function modifyCheckPropRet() {
Java.perform(function() {
var DebuggerDetector = Java.use("android.com.dream_detector.DebuggingDetector");
DebuggerDetector.checkProp.implementation = function() {
return false;
};
});
}
modifyCheckPropRet();
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을 반환해서 우회할 수 있다.