개발자가 의도하지 않은 방식으로 앱을 동작시킬 수 있음
private fun doFridaPortCheck() {
if (!checkPortLock) {
checkPortLock = true
val ret = fridaDetector.checkPort()
thread(start = true) {
handler.post {
val t = view?.findViewById<Button>(R.id.btnFridaPortCheck)
if (ret) {
t?.text = resources.getString(R.string.msg_Detected)
t?.setBackgroundResource(R.drawable.button_detected)
} else {
t?.text = resources.getString(R.string.msg_Passed)
t?.setBackgroundResource(R.drawable.button_passed)
}
}
checkPortLock = false
}
}
}
public boolean checkPort() {
for (int i = 26000; i < 27500; i++) {
try {
Socket s = new Socket("127.0.0.1", i);
if (s.isConnected()) {
return true;
}
} catch (IOException ignore) {
}
}
return false;
}
checkPort 함수는 socket 객체를 통해 26000~27499까지 총 1500개 포트에 대해 열려있는지 검사하는데 열려있는 포트가 하나라도 존재하면 Frida가 탐지되었다고 판단하고 true를 반환한다. 26000~27499 포트가 일반적으로 기기 내의 Frida 서버가 Frida 클라이언트와 통신하기 위해 주로 사용하는 포트인데 Frida의 기본 설정 포트는 27042이고, 임의로 디폴트 포트를 변경했을 가능성이 있기 때문에 보통 이에 근접한 포트 대역을 전부 검사함
generic_x86:/data/local/tmp # netstat -ntpl | grep frida
tcp 0 0 127.0.0.1:27042 0.0.0.0:* LISTEN 12965/frida-server-15.2.2-android-x86
tcp 0 0 127.0.0.1:27042 127.0.0.1:46926 ESTABLISHED 12965/frida-server-15.2.2-android-x86
generic_x86:/data/local/tmp # ./frida-server-15.2.2-android-x86 -l 0.0.0.0:27000 &
[1] 13014
generic_x86:/data/local/tmp # ps
USER PID PPID VSZ RSS WCHAN ADDR S NAME
root 12899 4204 12916 3220 __ia32_co+ 0 S sh
root 12967 1 8764 2500 __skb_wai+ 0 S logcat
root 13014 12899 71688 49696 do_sys_po+ 0 S frida-server-15.2.2-android-x86
root 13016 13014 8764 2988 __skb_wai+ 0 S logcat
root 13033 12899 12584 3672 0 0 R ps
Frida 포트 검사를 수행하는 checkPort 함수의 반환 값을 조작하는 등 다양한 방법으로 우회할 수 있습니다.
public boolean checkPort() {
for (int i = 26000; i < 27500; i++) {
try {
Socket s = new Socket("127.0.0.1", i);
if (s.isConnected()) {
return true;
}
} catch (IOException ignore) {
}
}
return false;
}
checkPort 함수의 반환 값은 Frida 포트 탐지 여부를 의미함. 결국 함수가 항상 false를 반환하도록 후킹해야 한다.
checkPort 함수의 반환 값은 Socket을 통해 특정 포트에 성공적으로 연결 되는지 여부를 나타냄. Socket 클래스의 생성자를 후킹해서 연결을 시도할 주소와 포트를 중간에 변경한다면, 앱 제작자가 의도하지 않은 주소와 포트로 연결하도록 할 수 있으며 이를 통해 로컬 호스트 내의 열려있는 Frida 포트에 아예 접속하지 않도록 해서 우회할 수 있음
function modifyCheckPortRet() {
Java.perform(function() {
var FridaDetector = Java.use("android.com.dream_detector.FridaDetector");
FridaDetector.checkPort.implementation = function() {
return false;
};
});
}
modifyCheckPortRet();
Java.use 사용해서 Frida 내에서 FridaDetector 클래스에 접근할 수 있도록 Wrapper를 제공받고 이를 통해 클래스 내의 checkPort 함수에 접근함. 이 때 implementation을 사용하면 Frida 스크립트로 구현한 함수로 해당 함수를 덮을 수 있음. 따라서 함수의 반환 값이 항상 false인 함수를 구현해 checkPort 함수를 우회함
function modifySocketParameter() {
Java.perform(function() {
var Socket = Java.use("java.net.Socket");
var constructor = Socket.$init.overload('java.lang.String', 'int');
constructor.implementation = function(hostname, port) {
if (hostname == "127.0.0.1" && 26000 <= port && port < 27500) {
port = 1234;
}
return constructor.call(this, hostname, port);
};
});
}
modifySocketParameter();
Socket 클래스는 다른 엔드포인트와 통신하기 위한 소켓 클라이언트가 구현된 클래스. 만약 앱이 Socket을 이용해서 127.0.0.1의 26000~27499 범위의 포트에 접속을 시도하는 요청이 발생한다면 이는 Frida 탐지를 위한 요청으로 판단될 수 있다. 해당 클래스의 생성자를 후킹해 다른포트로 변경한다면 Frida 포트에 접속하지 않아서 우회 가능
Frida 내에서 클래스의 생성자는 $init을 통해 접근할 수 있다. 따라서 $init의 implementation을 덮는 방식으로 생성자를 후킹할 수 있다. Socket 클래스는 다양한 형태의 파라미터를 받을 수 있도록 생성자가 오버로딩으로 선언되어 있는데 만약 Frida에서 오버로딩으로 선언된 함수를 후킹하고 싶을 때에는 overload(signature)를 통해 어떤 타입을 가지는 함수를 후킹할지 선택할 수 있다. checkPort 함수의 경우 문자열과 정수를 인자로 받는 생성자를 사용했기 때문에 overload('java.lang.String', 'int') 를 통해 후킹할 생성자를 선택할 수 있다.
후킹을 통해 파라미터를 변경했다면 원본 함수를 다시 호출해주어야 한다. 원본 함수는 call 함수를 통해 후킹되지 않은 본래의 함수를 호출할 수 있습니다. 이 때 주의해야할 점은 첫 번째 인자로 반드시 this를 넘겨야 합니다.
private fun doFridaPathCheck() {
if (!checkPathLock) {
checkPathLock = true
val ret = fridaDetector.checkPath()
thread(start = true) {
handler.post {
val t = view?.findViewById<Button>(R.id.btnFridaPathCheck)
if (ret) {
t?.text = resources.getString(R.string.msg_Detected)
t?.setBackgroundResource(R.drawable.button_detected)
} else {
t?.text = resources.getString(R.string.msg_Passed)
t?.setBackgroundResource(R.drawable.button_passed)
}
}
checkPathLock = false
}
}
}
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().contains("frida"))
found.set(true);
});
if (found.get()) {
return true;
}
} catch (IOException ignore) {
}
return false;
}
generic_x86_64_arm64:/ # chmod 777 /data/local/tmp
generic_x86_64_arm64:/ # find / -name "*frida*"
/data/local/tmp/frida-server-15.0.18-android-x86_64
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().contains("frida"))
found.set(true);
});
if (found.get()) {
return true;
}
} catch (IOException ignore) {
}
return false;
}
checkPath 함수의 반환 값은 Frida 파일 탐지 여부를 의미함. 따라서 해당 함수가 항상 False를 반환하도록 후킹한다면 Frida 관련 파일이 있어도 이를 탐지되지 않은 것처럼 조작할 수 있다.
checkPath 함수의 반환 값은 path변수 중 frida 라는 문자열이 포함되어 있는지 여부를 통해 결정된다. 따라서 String.contains 함수를 후킹하여 만약 frida 라는 문자열이 파라미터로 전달되면, 이를 조작하여 다른 문자열이 전달되도록 할 수 있다.
function modifyCheckPathRet() {
Java.perform(function() {
var FridaDetector = Java.use("android.com.dream_detector.FridaDetector");
FridaDetector.checkPath.implementation = function() {
return false;
};
});
}
modifyCheckPathRet();
Java.use 사용해서 Frida 내에서 FridaDetector 크래스에 접근할 수 있도로고 Wrapper를 제공받음. 제공받은 Wrapper를 통해 클래스 내의 checkPath 함수에 접근할 수 있음. 후킹 할 함수의 implementation을 자바스크립트 함수로 덮는 방식 사용함. 따라서 checkPath 함수의 implementation에 항상 False를 반환하도록 작성된 함수를 전달하여 checkPath 함수가 매번 False를 반환하도록 후킹
function modifyStrContainsParameter() {
Java.perform(function() {
var String_ = Java.use("java.lang.String");
String_.contains.overload('java.lang.CharSequence').implementation = function(s) {
if (s == "frida")
s = "**NOT_EXIST**";
return String_.contains.overload('java.lang.CharSequence').call(this, s);
};
});
}
modifyStrContainsParameter();
Strings.contains는 문자열 객체 내의 특정 문자열이 포함되어있는지 검사하기 위한 함수이다. Dream-detector 앱은 디렉토리 내의 파일 이름 중 frida라는 문자열이 존재하는지 검사하기 위해 해당 함수를 사용한다. 일반적인 앱이 frida라는 문자열이 특정 문자열 내에 존재하는지 확인할 경우가 거의 없기 때문에 frida라는 문자열이 포함되었는지 검사한다면 이는 Frida 경로 탐지를 위한 것이라고 볼 수 있다.
String 클래스의 Wrapper를 제공받고, contains 함수를 후킹하여 만약 파라미터로 전달된 값이 frida와 일치한다면 이를 NOT_EXIST 라는 문자열로 변경하도록 함수를 후킹한다. String.contains 함수는 다양한 형태의 파라미터를 받을 수 있도록 오버로딩으로 선언되어 있다. 만약 Frida에서 오버로딩으로 선언된 함수를 후킹하고 싶을 때에는 overload(signature)를 통해 어떤 타입을 가지는 함수를 후킹할지 선택할 수 있다. checkPath 함수의 경우 CharSequence 타입을 인자로 받는 함수를 사용했기 때문에 overload('java.lang.CharSequence') 를 통해 후킹할 함수를 선택할 수 있다.
파라미터를 임의의 문자열로 변경하였으니, 원본 함수를 다시 호출해주어야 한다. 원본 함수는 call 함수를 통해 후킹되지 않은 본래의 함수를 호출할 수 있다. 이 때 주의해야할 점은 첫 번째 인자로 반드시 this를 넘겨야 한다.
private fun doFridaModuleCheck() {
handler.post {
val t = view?.findViewById<Button>(R.id.btnFridaModuleCheck)
if (fridaDetector.checkModule()) {
t?.text = resources.getString(R.string.msg_Detected)
t?.setBackgroundResource(R.drawable.button_detected)
} else {
t?.text = resources.getString(R.string.msg_Passed)
t?.setBackgroundResource(R.drawable.button_passed)
}
}
}
public boolean checkModule() {
File f = new File("/proc/self/maps");
try {
FileInputStream fio = new FileInputStream(f);
StringBuilder resultStringBuilder = new StringBuilder();
try (BufferedReader br = new BufferedReader(new InputStreamReader(fio))) {
String line;
while ((line = br.readLine()) != null) {
resultStringBuilder.append(line).append("\n");
}
fio.close();
} catch (IOException ignore) {
}
return resultStringBuilder.toString().contains("frida");
} catch (FileNotFoundException ignore) {
}
return false;
}
프로세스의 메모리 맵 정보를 가지고 있는 /proc/self/maps 파일에서 frida 문자열의 여부를 검사합니다. /proc/self/maps 파일은 보편적으로 어느 공유 라이브러리들이 함께 로드되어있고, 베이스 주소를 찾기 위해 이용합니다.
일반적으로 Frida를 통해 앱이 실행되면 Frida 서버는 앱 프로세스 내의 frida-agent-{arch}.so 라이브러리를 로드합니다. 따라서 Frida를 통해 실행된 앱은 반드시 메모리 맵 내에 frida 라는 이름이 포함됩니다. Frida 모듈 탐지는 이와 같이 앱 프로세스의 메모리 맵 파일을 참조하여 Frida가 함께 실행 중인지 탐지합니다.
generic_x86_64_arm64:/data/local/tmp # ./frida-server-15.0.8-android-x86_64
$ frida -D emulator-5554 -f android.com.dream_detector --no-pause
public boolean checkModule() {
File f = new File("/proc/self/maps");
try {
FileInputStream fio = new FileInputStream(f);
StringBuilder resultStringBuilder = new StringBuilder();
try (BufferedReader br
= new BufferedReader(new InputStreamReader(fio))) {
String line;
while ((line = br.readLine()) != null) {
resultStringBuilder.append(line).append("\n");
}
fio.close();
} catch (IOException ignore) {
}
return resultStringBuilder.toString().contains("frida");
} catch (FileNotFoundException ignore) {
}
return false;
}
checkModule 함수의 반환 값은 Frida 모듈 탐지 여부를 의미함. 해당 함수가 항상 false를 반환하도록 후킹해준다면 Frida 관련 포트가 열려있더라도 이를 탐지되지 않은 것처럼 조작할 수 있음
fridaDetector.checkModule 함수가 내부적으로 사용하는 BufferedReader.readLine 함수를 후킹하여 반환 값 내에 frida라는 문자열이 포함되어있는지 여부를 조작할 수 있음. 이를 통해 메모리 맵 내에 Frida 모듈이 로드되지 않은 것처럼 보이게 할 수 있음
function modifyCheckModuleRet(){
Java.perform(function() {
var FridaDetector = Java.use("android.com.dream_detector.FridaDetector");
FridaDetector.checkModule.implementation = function() {
return false;
};
});
}
modifyCheckModuleRet();
Java.use 함수를 사용해 Frida 내에서 FridaDetector 클래스에 접근할 수 있도록 Wrapper를 제공받는다. 제공받은 Wrapper를 통해 클래스 내의 checkModule 함수에 접근할 수 있다. checkPath 함수의 implementation에 항상 False를 반환하도록 작성된 함수를 전달하여 checkModule 함수가 매번 False를 반환하도록 후킹할 수 있다.
function modifyReadLineRet(){
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/maps") {
var BufferedReader = Java.use("java.io.BufferedReader");
var readLine = BufferedReader.readLine.overload();
readLine.implementation = function() {
var ret = readLine.call(this);
if (ret.includes("frida"))
ret = "";
return ret;
};
}
return constructor.call(this, pathname);
};
});
}
modifyReadLineRet();
BufferedReader.readLine 함수는 스트림으로부터 개행으로 끝나는 한 줄의 문자열을 읽을 때 사용하는 함수이다. Dream-detector 앱은 메모리 맵 파일의 내용을 전부 읽기 위해 해당 함수를 사용한다. BufferedReader.readLine 함수는 Frida를 탐지할 때 외에도 앱에서 일반적으로 많이 사용하는 함수입니다. 따라서 frida라는 다섯 문자열이 포함되어 있다고 해서 무조건 frida 문자열을 지우도록 후킹하면 앱이 원래 수행해야 하는 동작도 문제가 생길 가능성이 존재합니다.
이를 위해 File 클래스의 생성자를 먼저 후킹한 후, 생성자로 전달된 파일 이름이 /proc/self/maps인 경우에만 BufferedReader.readLine 함수를 후킹하도록 작성하였습니다.
한가지 더 주의해야할 점은 기존 후킹을 통한 파라미터 변조와 반대로 이번에는 반환 값을 변조해야 하기 때문에 원본 함수를 먼저 호출하고, 반환 값 내에 frida라는 문자열이 존재하는지 검사하는 방식으로 후킹해야 합니다.
Check Emulator 기능이 에뮬레이터를 탐지하기 위해 일반적으로 사용되는 방법
1. Emulator 속성 검사 기능 : 에뮬레이터 탐지를 위해 시스템 속성을 검사함
private fun doEmulatorPropertiesCheck() {
val emulatorDetector = EmulatorDetector(context)
handler.post {
val t = view?.findViewById<Button>(R.id.btnEmulatorPropertiesCheck)
if (emulatorDetector.checkProperties()) {
t?.text = resources.getString(R.string.msg_Detected)
t?.setBackgroundResource(R.drawable.button_detected)
} else {
t?.text = resources.getString(R.string.msg_Passed)
t?.setBackgroundResource(R.drawable.button_passed)
}
}
}
public boolean checkProperties() {
for (String propertyName : Constants.knownEmulatorProperties.keySet()) {
String propertyValue = Utils.getProp(mContext, propertyName);
String property_seek = Constants.knownEmulatorProperties.get(propertyName);
if ("".equals(property_seek) && !"".equals(propertyValue)) {
return true;
}
if (!"".equals(property_seek)) {
assert propertyValue != null;
if (property_seek != null && propertyValue.contains(property_seek)) {
return true;
}
}
}
return false;
}
public final class Utils {
public static String getProp(Context context, String property) {
try {
ClassLoader classLoader = context.getClassLoader();
@SuppressLint("PrivateApi") Class<?> systemProperties = classLoader.loadClass("android.os.SystemProperties");
Method get = systemProperties.getMethod("get", String.class);
Object[] params = new Object[1];
params[0] = property;
return (String) get.invoke(systemProperties, params);
} catch (Exception ignore) {
}
return null;
}
}
public static final Map<String, String> knownEmulatorProperties = new HashMap<String, String>() {{
put("init.svc.qemu-props", "");
put("qemu.hw.mainkeys", "");
put("qemu.sf.fake_camera", "");
put("qemu.sf.lcd_density", "");
put("ro.bootloader", "unknown");
put("ro.kernel.android.qemud", "");
put("ro.kernel.qemu.gles", "");
put("ro.kernel.qemu", "");
put("ro.product.device", "generic");
put("ro.product.model", "sdk");
put("ro.product.name", "sdk");
put("ro.serialno", "EMULATOR");
}};
시스템 속성 리스트는 Map 형태로 정의되어 있고 키-값 형태로 선언되어 있다. 값이 빈 문자열("")인 경우 에뮬레이터 환경이 아닌 경우에는 존재해서는 안되는 시스템 속성을 의미하고, 값이 존재할 때는 시스템 속성이 해당 값을 포함하고 있으면 에뮬레이터임을 의미함
AVD는 Qemu를 기반으로 동작하기때문에 에뮬레이터 관련 시스템 속성인 init.svc.qemu-props, qemu.hw.mainkeys, qemu.sf.fake_camera 등의 속성이 존재한다. 따라서 탐지 함수는 Constants.knownEmulatorProperties에 정의된 시스템 속성들을 탐지하여 앱이 에뮬레이터에서 동작하고 있다고 판단한다. 만약 에뮬레이터가 아니거나 해당 속성이 존재하지 않으면 동일한 속성을 생성한다.
generic_x86_64_arm64:/ # setprop ro.serialno EMULATOR
checkProperties함수에서 시스템 속성 값이 정의된 값과 다르다면 false를 반환하고 디버깅 관련 파일이 탐지 되지 않은 것으로 판단함. 따라서 우회 가능
해당 함수는 기기의 시스템 속성을 읽어올 때 Utils.getProp함수를 사용합니다. getProp 함수는 시스템 속성에서 전달받은 인자와 동일한 키를 찾아 해당 키에 대한 값을 반환합니다. 만약 시스템 속성에 전달받은 인자와 동일한 키가 없다면 null을 반환합니다. 따라서 getProp함수의 반환 값을 정의된 시스템 속성 리스트에 맞춰, 리스트에 값이 없는 속성 키라면 null을 반환하고, 특정 값이 있는 속성 키라면 특정 값을 포함하지 않는 값을 반환하여 checkProperties함수를 우회할 수 있습니다.
function modifyCheckPropertiesRet(){
Java.perform(function() {
var EmulatorDetector = Java.use("android.com.dream_detector.EmulatorDetector");
EmulatorDetector.checkProperties.implementation = function() {
return false;
};
});
}
modifyCheckPropertiesRet();
먼저 Java.use 함수를 사용해 프리다 내에서 EmulatorDetector 클래스에 접근할 수 있도록 Wrapper를 제공받음. 이를 통해 클래스 내의 checkProperties 함수에 접근할 수 있음. checkProperties 함수의 implementation에 항상 false를 반환하도록 작성된 함수를 전달하여 checkProperties함수가 매번 False를 반환하도록 후킹
function modifyGetPropRet(){
var knownEmulatorProperties = {
"init.svc.qemu-props": "",
"qemu.hw.mainkeys": "",
"qemu.sf.fake_camera": "",
"qemu.sf.lcd_density": "",
"ro.bootloader": "unknown",
"ro.kernel.android.qemud": "",
"ro.kernel.qemu.gles": "",
"ro.kernel.qemu": "",
"ro.product.device": "generic",
"ro.product.model": "sdk",
"ro.product.name": "sdk",
"ro.serialno": "EMULATOR"
};
Java.perform(function() {
var Utils = Java.use("android.com.dream_detector.Utils");
Utils.getProp.implementation = function(context, property) {
for (var prop in knownEmulatorProperties){
if (property == prop){
if(knownEmulatorProperties.prop == ""){
return null;
}
}
}
return Utils.getProp.call(this, context, "nothing");
}
});
}
modifyGetPropRet();
Java.use 함수를 사용해 Frida 내에서 android.com.dream_detector.Utils클래스에 접근할 수 있도록 Wrapper를 제공받음. 속성 값을 반환해주는 getProp함수에 전달된 인자값이 에뮬레이터 관련 속성인 knownEmulatorProperties의 키와 동일하다면 존재하지 않아야하는 속성의 경우 null을 반환하고, 속성 값이 지정된 경우 다른 속성 값이 도출될 수 있도록 getProp에 nothing이라는 이름의 속성 값을 가져오도록 함. 해당 함수에서 null이 반환되면 checkProperties함수는 false를 반환하기때문에, 이를 통해 checkProperties함수를 우회할 수 있다.
private fun doEmulatorFilesCheck() {
val emulatorDetector = EmulatorDetector(context)
handler.post {
val t = view?.findViewById<Button>(R.id.btnEmulatorFilesCheck)
if (emulatorDetector.checkFiles()) {
t?.text = resources.getString(R.string.msg_Detected)
t?.setBackgroundResource(R.drawable.button_detected)
} else {
t?.text = resources.getString(R.string.msg_Passed)
t?.setBackgroundResource(R.drawable.button_passed)
}
}
}
public boolean checkFiles() {
return checkFiles(Constants.knownGenyFiles)
|| checkFiles(Constants.knownAndyFiles)
|| checkFiles(Constants.knownNoxFiles)
|| checkFiles(Constants.knownQemuPipes)
|| checkFiles(Constants.knownx86Files);
}
private boolean checkFiles(String[] files) {
for (String file : files) {
File f = new File(file);
if (f.exists()) {
return true;
}
}
return false;
}
checkFiles 함수는 오버로딩되어 파라미터를 받지 않는 함수와 문자열 배열 파라미터를 받는 두 개의 함수가 존재한다. 이 중 Fragment에서 호출한 함수는 파라미터를 받지 않는 함수이다. 파라미터를 받지 않는 checkFiles함수는 android/com/dream_detector/Constants.java에 별도로 정의된 에뮬레이터용 파일 리스트를 가져와 파라미터를 받는 checkFiles함수로 전달한다. 파라미터를 받는 checkFiles함수는 반복문을 돌며 파라미터로 전달된 문자열 리스트 중 존재하는 파일이 있는지 검사한다. 해당 파일 리스트는 실 기기에서 동작할 때는 존재하지 않지만, 에뮬레이터를 통해 안드로이드를 구동시킬 때에만 존재하는 파일들을 모아둔 것으로 만약 해당 파일이 시스템 내에 존재한다면 에뮬레이터라고 판단할 수 있다.
public static final String[] knownGenyFiles = {
"/dev/socket/genyd",
"/dev/socket/baseband_genyd"
};
public static final String[] knownQemupipes = {
"/dev/socket/qemud",
"/dev/qemu_pipe"
};
public static final String[] knownx86Files = {
"ueventd.android_x86.rc",
"x86.prop",
"ueventd.ttVM_x86.rc",
...
};
public static final String[] knownAndyFiles = {
"fstab.andy",
"ueventd.andy.rc"
};
public static final String[] knownNoxFiles = {
"fstab.nox",
"init.nox.rc",
"ueventd.nox.rc"
};
일반적으로 많이 사용하는 에뮬레이터로는 지니모션, 앤디, 녹스, Qemu가 있는데 AVD의 경우 Qemu를 사용한다. Constants에는 에뮬레이터별로 탐지할 파일들이 정의되어 있다.
AVD는 Qemu를 기반으로 동작하기때문에 Qemu 에뮬레이터 관련 파일인 /dev/qemu_pipe 파일이 존재합니다. 따라서 탐지 함수는 Constants.knownQemupipes에 정의된 /dev/qemu_pipe파일을 탐지하여 에뮬레이터에서 동작하고 있다고 판단한다. 만약 에뮬레이터가 아니거나 해당 파일이 존재하지 않는 경우, 동일한 파일명을 생성한다.
generic_x86_64_arm64:/ # touch /dev/qemu_pipe
checkFiles 함수의 반환 값은 에뮬레이터 파일 탐지 여부를 의미한다. 따라서 해당 함수가 항상 False를 반환하도록 후킹해준다면 에뮬레이터 관련 파일이 존재하더라도 이를 탐지되지 않은 것처럼 조작할 수 있다.
emulatorDetector.checkFiles 함수가 파라미터를 받는 checkFiles 함수로 파일 리스트를 전달할 때 중간에 후킹하여 존재하지 않는 파일만 모아둔 리스트로 변경할 수 있다. 이를 통해 checkFiles 함수 내에서는 무조건 존재하지 않는 파일만 검사하여 해당 함수를 우회할 수 있다.
function modifyCheckFilesRet() {
Java.perform(function() {
var EmulatorDetector = Java.use("android.com.dream_detector.EmulatorDetector");
EmulatorDetector.checkFiles.overload().implementation = function() {
return false;
};
});
}
modifyCheckFilesRet();
Java.use 함수를 사용해 프리다 내에서 EmulatorDetector 클래스에 접근할 수 있도록 Wrapper를 제공받아 Wrapper를 통해 클래스 내의 checkFiles 함수에 접근할 수 있음. checkPath 함수의 implementation에 항상 False를 반환하도록 작성된 함수를 전달하여 checkFiles 함수가 매번 False를 반환하도록 후킹
유의할 것은 checkFiles 함수는 오버로딩 된 함수이기 때문에 프리다에서도 이를 명시해주어야 함. 프리다에서 오버로딩으로 선언된 함수를 후킹하고 싶을 때에는 overload(signature)를 통해 어떤 타입을 가지는 함수를 후킹할지 선택할 수 있음. 우리가 후킹할 checkFiles 함수는 파라미터를 받지 않기 때문에 "overload()"를 붙여서 명시할 수 있다.
function modifyCheckFilesParam() {
Java.perform(function() {
var EmulatorDetector = Java.use("android.com.dream_detector.EmulatorDetector");
var checkFiles = EmulatorDetector.checkFiles.overload('[Ljava.lang.String;');
checkFiles.implementation = function (files) {
var emulator_files_not_exists = [ "/not_exist", "/bypass_emulator_check" ];
return checkFiles.call(this, emulator_files_not_exists);
};
});
}
modifyCheckFilesParam();
파라미터를 받는 checkFiles 함수는 파일 이름 리스트를 전달받는다. 따라서 이를 후킹하여 다른 리스트로 변경해 항상 False를 반환하도록 후킹할 수 있다. 이번엔 파라미터를 받는 함수를 후킹할 것이기 때문에 overload('[Ljava.lang.String;')를 이용해 후킹할 함수를 선택할 수 있다. 이후 존재하지 않는 파일만 모아둔 emulator_files_not_exists 리스트를 생성하고 원본 함수를 호출하여 우회할 수 있다. 원본 함수는 call 함수를 통해 후킹되지 않은 본래의 함수를 호출할 수 있고, 이 때 주의해야할 점은 첫 번째 인자로 반드시 this를 넘겨야 한다.