대상 앱 : Dream-detector.apk
Frida 버전 : 15.2.2 android x86
기기 정보 : Pixel 4XL
API 버전 : 30
기기가 루팅되었는지 검사합니다.
앱 내에서 프리다가 동작 중인지 검사합니다.
앱이 에뮬레이터 위에서 동작 중인지 검사합니다.
앱이 디버깅 중인지 검사합니다.
인증서 변조를 통한 TLS MITM 공격을 검사합니다.
frida -D emulator-5554 -f android.com.dream_detector --no-pause
Spawned `android.com.dream_detector`. Resuming main thread!
[Android Emulator 5554::android.com.dream_detector ]->
위와 같이 뜨면 성공
👀 기기가 루팅되어 있으면 안드로이드에서 기본적으로 지원하는 내장 보안 기능이 약화되어 다양한 보안 위험을 발생시킬 수 있음 따라서 금융 관련 앱과 같이 높은 보안성이 요구되는 앱은 기기의 루팅 여부를 탐지해 앱의 기능을 사용하지 못하게 하거나 앱 동작을 중단시킴
Dream-detector의 Check Rooting 기능은 루팅 탐지를 위해 2가지 방법 구현
기능 분석은 일반적으로 MainActivity부터 차례로 분석함. MainActivity에는 각각의 탐지 기능들에 대한 버튼과 버튼 클릭과 함께 변경될 UI를 Fragment 객체로 동작시키는 것이 확인됨
findViewById<Button>(R.id.btnRootCheck).setOnClickListener {
findViewById<TextView>(R.id.txtHeader).text = resources.getString(R.string.title_Rooting)
val fm = supportFragmentManager
fm.popBackStack()
val ft = fm.beginTransaction()
val bf = RootingFragment()
ft.replace(R.id.main_fragment_view, bf)
ft.commit()
}
class RootingFragment : Fragment() {
val handler = Handler(Looper.getMainLooper())
...
@SuppressLint("ResourceAsColor")
private fun doRootingBinaryCheck() {
val rootingDetector = RootingDetector(context)
handler.post {
val t = view?.findViewById<Button>(R.id.btnRootingBinariesCheck)
if (rootingDetector.checkSuBinary()) {
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)
}
}
}
}
RootingDetector 클래스에 루팅 탐지를 위한 함수들이 구현되어 있고 그 중 checkSuBinary 함수는 파일을 확인해서 루팅을 탐지함
그래서 Constants.knownSuBinaries, Constants.knownSuDirectories는 무엇인가
(개발자가 지정해놓은것임)
public static final String[] knownSuDirectories = {
"/data/local/",
"/data/local/bin/",
"/data/local/xbin/",
"/sbin/",
"/su/bin/",
"/system/bin/",
"/system/bin/.ext/",
"/system/bin/failsafe/",
"/system/sd/xbin/",
"/system/usr/we-need-root/",
"/system/xbin/",
"/cache/",
"/data/",
"/dev/"
};
public static final String[] knownSuBinaries = {
"su",
"busybox",
"magisk"
};
checkSuBinary 함수 반환 할 때 f.exists 함수가 true면 true를 반환함. 이 f.exists()의 반환값을 조작해서 if문으로 진입하지 못하게 하거나, 존재할 경우의 return 값을 0이나 false로 조작해서 우회
Java.use를 사용해서 Frida 내에서 RootDetector 클래스에 접근할 수 있도록 Wrapper를 제공받음. 제공받은 Wrapper를 통해 클래스 내의 checkSuBinary 함수에 접근. 이 때 implementation을 사용하면 Frida 스크립트로 구현한 함수로 해당 함수를 덮을 수 있음.
function modifyCheckSuBinaryRet() {
Java.perform(function() {
var RootingDetector = Java.use("android.com.dream_detector.RootingDetector");
RootingDetector.checkSuBinary.implementation = function() {
return false;
};
})
}
modifyCheckSuBinaryRet();
private fun doRootingPackageCheck() {
val rootingDetector = RootingDetector(context)
handler.post {
val t = view?.findViewById<Button>(R.id.btnRootingPackagesCheck)
if (rootingDetector.checkRootingPackage()) {
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)
}
}
}
Rooting 패키지 검사 기능은 미리 정의되어있는 앱 패키지 목록을 기반으로 루팅 관련 패키지의 설치 여부를 검사함
public boolean checkRootingPackage() {
ArrayList<String> packages = new ArrayList<>(Arrays.asList(Constants.knownRootAppsPackages));
PackageManager pm = mContext.getPackageManager();
for (String packageName : packages) {
try {
pm.getPackageInfo(packageName, 0);
return true;
} catch (PackageManager.NameNotFoundException ignore) {
}
}
return false;
}
public static final String[] knownRootAppsPackages = {
"com.noshufou.android.su",
"com.noshufou.android.su.elite",
"eu.chainfire.supersu",
"com.koushikdutta.superuser",
"com.thirdparty.superuser",
"com.yellowes.su",
"com.topjohnwu.magisk",
"com.kingroot.kinguser",
"com.kingo.root",
"com.smedialink.oneclickroot",
"com.zhiqupk.root.global",
"com.alephzain.framaroot",
"com.koushikdutta.rommanager",
"com.koushikdutta.rommanager.license",
"com.dimonvideo.luckypatcher",
"com.chelpus.lackypatch",
"com.ramdroid.appquarantine",
"com.ramdroid.appquarantinepro",
"com.android.vending.billing.InAppBillingService.COIN",
"com.android.vending.billing.InAppBillingService.LUCK",
"com.chelpus.luckypatcher",
"com.blackmartalpha",
"org.blackmart.market",
"com.allinone.free",
"com.repodroid.app",
"org.creeplays.hack",
"com.baseappfull.fwd",
"com.zmapp",
"com.dv.marketmod.installer",
"org.mobilism.android",
"com.android.wp.net.log",
"com.android.camera.update",
"cc.madkite.freedom",
"com.solohsu.android.edxp.manager",
"org.meowcat.edxposed.manager",
"com.xmodgame",
"com.cih.game_cih",
"com.charles.lpoqasert",
"catch_.me_.if_.you_.can_",
"com.devadvance.rootcloak",
"com.devadvance.rootcloakplus",
"de.robv.android.xposed.installer",
"com.saurik.substrate",
"com.zachspong.temprootremovejb",
"com.amphoras.hidemyroot",
"com.amphoras.hidemyrootadfree",
"com.formyhm.hiderootPremium",
"com.formyhm.hideroot"
};
checkPackage 함수는 루팅 관련 패키지가 탐지되면 true를 리턴하고 반대는 false를 리턴함
따라서 함수가 무조건 false로 반환하도록 후킹해서 조작 가능
정해진 리스트 기반으로 패키지 명을 검사하기 때문에 해당 변수를 의미없는 문자열을 가진 배열로 변경하면 루팅 관련 패키지가 설치되어도 탐지 할 수 없음
getPackageInfo를 통해 루팅 관련 패키지 존재여부를 검사할 때 RootDetector클래스의 checkPackage 함수 반환값을 사용함. 함수의 반환값이 False라면 디버깅 상태가 아닌 것으로 간주하고 PASSED.를 출력한다.
function modifyCheckPackageRet() {
Java.perform(function() {
var RootingDetector = Java.use("android.com.dream_detector.RootingDetector");
RootingDetector.checkRootingPackage.implementation = function() {
return false;
};
});
}
modifyCheckPackageRet();
knownRootAppsPackages에 정의된 검사할 패키지 목록은 getPackageInfo의 인자로 전달된다. 따라서 해당 변수에 정의된 패키지 명을 의미없는 문자열로 변경하면 checkPackage함수는 패키지가 존재하지 않는 것으로 판단하고 PASSED.를 출력함
function modifyKnownPackagesVal() {
Java.perform(function() {
var Constants = Java.use("android.com.dream_detector.Constants");
var str_arr = Java.array('java.lang.String',[""]);
Constants.knownRootAppsPackages.value = str_arr;
});
}
modifyKnownPackagesVal();