Sitemap
parkwow13

parkwow13

android developer

Android Rooting

12 min readJan 3, 2025

--

Press enter or click to view image in full size
Photo by Mario von Rotz on Unsplash

안드로이드는 4.3 이상부터 SELinux(Security-Enhanced Linux) 시스템을 사용한다.
SELinux 는 애플리케이션, 프로세스, 파일에 대한 강제 액세스 제어(MAC)를 적용하여 안드로이드는 시스템 서비스를 더 안전하게 보호하고, 제한하며, 악성 소프트웨어의 영향을 줄인다.

하지만 Rooting을 하게되면 SELinux 정책을 무시하고 제한된 파일이나 디렉토리에 접근을 할 수 있게된다.

안드로이드에서 루팅을 검사하는 방법을 통해 루팅되었을 경우 앱 접근을 차단하는 방식을 취하도록 한다.

안드로이드 루팅 탐지 방법

1. SystemProperties Check
2. su, busybox, magisk check
3. 쓰기 권한 확인
4. SELinux 상태확인

SystemProperties Check

SystemProperties 에 접근하여 특정 Properties 의 값이 변경되었는지 확인을 하여 루팅을 탐지한다.
일반적인 클래스 호출방식으로 SystemProperties 를 가져올 수는 없다.
리플렉션(reflection) 을 통해서 SystemProperties 에 접근하거나, android.os.Build 를 통해서 선언된 SystemProperties 의 속성을 확인하는 방법이 있다.

1. reflection

reflection을 통해 SystemProperties 에 접근하여 특정 Properties 의 값이 변경되었는지 확인을 한다.
일반적으로 ro.adb.secure 와 ro.secure 은 활성화되어있어야하기때문에 1로 나와야한다.

1. ro.secure
Android 시스템이 안전 모드로 실행 중인지 여부
1: 안전 모드로 실행 중
0: 시스템이 안전 모드로 실행되지 않았거나 수정되었음을 의미

2. ro.adb.secure
ADB연결이 보안 설정으로 보호되는지 여부
1: ADB가 보안 설정(인증된 키를 통한 연결)을 사용 중임을 의미
0: ADB 보안 설정이 비활성화되어 있고, 누구나 디바이스에 연결할 수 있음을 의미

@SuppressLint(“PrivateApi”)
fun reflectionSystemProperties(): Boolean {
val systemProperties = Class.forName(“android.os.SystemProperties”)
val getMethod = systemProperties.getMethod(“get”, String::class.java, String::class.java)
val adbSecureProperties = getMethod.invoke(null, “ro.adb.secure”, “unknown”)
val secureProperties = getMethod.invoke(null, “ro.secure”, “unknown”)
val tagProperties = getMethod.invoke(null, “ro.build.tags”, “unknown”)

Log.d(TAG, "adbSecureProperties : $adbSecureProperties")
Log.d(TAG, "secureProperties : $secureProperties")

if ((tagProperties != null && (tagProperties is String && tagProperties.contains(“test-keys”))) ||
adbSecureProperties != 1 ||
secureProperties != 1) {
return true
} else {
return false
}
}

위와 같이 reflection 을 사용하면 SystemProperties 에 접근이 가능하다.
정확하게 가져왔는지 확인을 해보자.

adb 를 통해 먼저 확인 한다.

adb shell getprop | grep -e ro.secure -e ro.adb.secure
[ro.adb.secure]: [1]
[ro.secure]: [1]

그리고 reflection 을 한 결과를 Log로 확인을 한다.

adbSecureProperties : 1
secureProperties : 1

실제 adb 에서 획득한 값과 같음을 확인 할 수 있다.

하지만 reflection 을 사용하게되면 여러가지 문제가 있다.

1. 버전문제
내부 API 버전이 달라지면서 특정 Android 버전부터는 동작하지 않을 가능성이 있다.

2. 난독화
Proguard/R8 을 사용할 경우 난독화로 인해 reflection 한 클래스나 메서드가 제거될 수 있다.
proguard-rules 에 난독화 제외를 시켜주면 해결이 가능하긴 하다.

그 외로도 다양한 문제가 있을 수도 있다.
물론 해결하면 되지만 지속적으로 관리를 해줘야하는 번거로움이 있다.

2. android.os.Build

안드로이드에서는 android.os.Build(이하 Build 클래스) 를 통해 일부 SystemProperties 의 속성을 확인 가능하다.
ro.adb.secure 는 Build 클래스에서 확인이 불가능하지만, ro.build.tags 를 통해 확인도 가능하다.

앱의 ro.build.tags 은 “release-keys” 를 기본으로 가진다.
루팅이 되었으면 ro.build.tags 값이 “test-keys” 로 변조되었을 가능성이 있다.

test-keys 를 사용하여 앱을 서명하게되면 앱의 보안적 요소가 사라질 수가 있어, 앱 내의 중요한 저장소에 접근하거나 데이터를 탈취당할 수가 있게된다.

Get parkwow13’s stories in your inbox

Join Medium for free to get updates from this writer.

그러므로 ro.build.tags 가 test-keys 로 변경되었는지 확인이 필요하다.

su, busybox, magisk check

su 권한은 관리자 권한을 획득하는 것으로 파일의 권한을 획득, 변경할 수가 있다.

busybox 는 unix (linux 의 상위) 의 명령을 실행할 수 있는 도구이다.
android 는 일부 unix 의 명령어를 포함하고있지않기때문에 루팅을 하더라도 원하는 동작을 수행하기 어려울 수가 있다.
특히 루트권한이 필요한 작업도 수행할 수 있기때문에 일부 루팅 도구들은 busybox 의 설치를 요구한다.
하지만 busybox 는 루팅도구가 아니기때문에 루팅이 되었다고 확신하기는 어렵다.
다만 루팅도구들의 다수는 busybox 를 요구하기때문에 알려진 루팅도구가 아닐 경우에는 busybox 가 설치되어있는지 유무로 판단할 수도 있다.
즉 루팅검사에 busybox 는 필수가 아닌 선택이다.

magisk 는 루팅기능을 제공하는 솔루션이다.
루트권한을 활성화하거나 시스템리스 루팅 즉 시스템 파티션을 수정하지않고 루팅 권한을 부여할 수가 있다.
또한 MagiskHide 기능으로 간단한 루팅 감지를 우회할 수도 있다.

위의 3가지를 검사하는 방법은 아래와 같다.

1. which 를 통해 명령어 확인
만약에 path 에 등록이 되었다면 which 를 통해 검사를 할 수가 있다.

2. file path 확인
일부 알려진 경로에 대해 파일들이 존재하는지 유무를 판단한다.
다만 설치경로는 달라 질 수가 있다.

1. 과 2. 을 모두 검사하는 방식으로 진행한다.

which 를 통해 명령어 실행 가능유무 판단

fun isBinaryAvailable(binaryName: String): Boolean {
return try {
val process = Runtime.getRuntime().exec(“which $binaryName”)
val reader = BufferedReader(InputStreamReader(process.inputStream))
val result = reader.readLine()
result != null && result.isNotEmpty()
} catch (e: Exception) {
false
}
}

공통 경로 설정

fun generalPath(): MutableList<String> {
return mutableListOf(
“/sbin/”,
“/system/bin/”,
“/system/bin/failsafe/”,
“/system/xbin/”,
“/system/sd/xbin/”,
“/data/local/”,
“/data/local/xbin/”,
“/data/local/bin/”
)
}

파일의 존재 유무 확인

fun checkFilePath(paths: List<String>): Boolean {
for (path in paths) {
if (File(path).exists()) {
return true
}
}
return false
}

su 관련 명령어 및 경로 확인

fun isSuDetected(): Boolean {
if (isBinaryAvailable(“su”)) return true
val paths = mutableListOf(
"/system/app/Superuser.apk",
"/su/bin/su"
)
paths.addAll(generalPath().map { it + "su" })
return checkFilePath(paths)
}

busybox 관련 명령어 및 경로 확인

fun isBusyboxDetected(): Boolean {
if (isBinaryAvailable(“busybox”)) return true
val paths = mutableListOf<String>()
paths.addAll(generalPath().map { it + “busybox” })
return checkFilePath(paths)
}

magisk 관련 명령어 및 경로 확인

fun isMagiskDetected(): Boolean {
if (isBinaryAvailable(“magisk”)) return true
val paths = mutableListOf(
“/sbin/.magisk/”,
“/data/magisk/”,
“/data/adb/modules/magisk”,
“/data/adb/magisk/”,
“/data/adb/magisk/magisk”,
“/sbin/magisk”,
“/data/magisk.img”,
“/data/adb/magisk.img”,
“/cache/magisk.img”,
“/system/bin/.magisk”,
“/system/xbin/.magisk”
)
return checkFilePath(paths)
}

쓰기 권한 확인

루팅된 디바이스에 권한이 필요한 파일또는 디렉토리에 쓰기권한이 열려있을 가능성이 있다.
이점을 활용하여 시스템 디렉토리에 파일을 생성하려고 했을 때 성공하는 지 확인하는 방법으로 검사를 할 수가 있다.

fun isSystemWritable(): Boolean {
return try {
val file = File(“/system/app/tempFile”)
file.createNewFile()
file.delete()
true
} catch (e: Exception) {
false
}
}

SELinux 상태확인

android 5.0 부터 SELinux 가 모두 Enforcing 모드로 설정되어있다.
SELinux 의 모드는 아래와 같다.

1. Enforcing : SELinux 의 보안정책을 필수로 사용하게 됩니다.
2. Permissive : SELinux 의 보안정책을 강제하진않지만, 로그를 남기도록 합니다.
3. disabled : SELinux 의 보안정책을 사용하지않습니다.

하지만 Enforcing 이 아니라고 루팅되었다고 가정할 수는 없다.
다만 가능성이 높기떄문에, 다른 루팅 감지와 같이 검사하여 루팅 가능성에 대한 신뢰성을 높여준다.

이와 같은 루팅 탐지 방법은 루팅으로 인한 앱 접근을 차단하는 등 조치를 원활하게 할 수 있도록 해주지만, 100% 신뢰를 할 수가 없다.
이미 루팅에 대한 방법과 우회방법들은 많이 알려져있지만, 이와 같은 루팅 탐지 방법으로 앱의 최소한의 방어책을 구축할 수는 있다.

만약 앱의 보안을 좀 더 높이고 싶다면 다양한 방법이 있고 Google Play Store 를 통해 앱을 배포하고있다면, Play Integrity API 를 사용하는 것을 추천한다.

Play Integrity API 는 앱의 무결성을 검사하여, 앱이 루팅, 변조되었는지 탐지를 한다.
다만 하루 최대 10,000개까지만 요청이 가능하고, 그 이상의 요청을 하기위해서는 Play Integrity API 의 증액 요청을 진행해야한다.
Google Play Console 에서 Play Integrity API 의 설정에서 진행하면 된다.

--