변경이력: History
일자 | 구분 | 변경점 | 비고 |
---|---|---|---|
2025.05.11 | 최초작성 |
사전 정보: Prev Information
Anditer 와 Frida 가 정상적으로 설치 및 구성이 되었다면 ANDITER 앱을 실행 한 뒤 frida-ps -Ua 명령어를 통해 다음과 같이 ANDITER 프로세스 목록이 출력 됩니다.
D:\frida> uv run frida-ps -Ua
PID Name Identifier
---- -------- ----------------------
1234 ANDITER com.playground.anditer
대상 기기(Nox 등)에 frida 서버가 정상 동작하고 있고, 연결에 이상이 없다면 위와 같이 결과 값이 나온다.
요약 및 실습 목표: Summary or Goal
코드 분석을 통해 루팅 검증 우회 Frida 스크립트를 작성해보고, 실습을 통해 실제로 우회 과정과 결과를 직접 확인해 봅니다.
- 설치된 패키지 문자열 배열을 빈 값으로 변조하여 루팅 검증 우회
- 함수 자체가 반환하는 값을 항상 False로 변조하여 루팅 검증 우회
설명 또는 : Descriptions
루팅 여부 탐지 기법 중 설치된 패키지 목록을 기반으로 한 탐지 기법을 이해하고, 우회 원리와 방법을 살펴봅니다.
실제 환경에서는 패키지 기반 검증 한 가지 방법만으로 루팅 여부 탐지가 이루어지지는 않으나 실습 과정을 통해 방식을 이해하고 우회 할 수 있는 방법을 학습하여, 분석 대상 애플리케이션에 접근하기 위한 방법 탐색에 도움이 될 수 있을 것입니다.
실습 과정: Step By Step
Step 1) ANDITER 실행 후 Bypass Packages 의 설명 아이콘을 선택, 루팅 탐지 원리 및 방법에 대한 설명을 참고하고 Check 버튼을 눌러 응답 상태를 확인합니다. 초기 상태에서는 Magisk 같은 루팅 패키지가 설치되어 있지 않기 때문에 Success 응답이 나타나는 모습을 확인할 수 있을 겁니다.

Step 2) 실습을 위해 Check 선택 시 Fail 이 나타나도록 환경 구성을 위해 Github(github.com/topjohnwu/Magisk)에 방문한 뒤, Releases 링크를 클릭합니다.

Step 3) Releases 페이지의 Assets 영역에서 Magisk APK를 다운로드 받습니다.

Step 4) 다운로드 한 Magisk APK를 드래그하여 설치 후, 설치된 Magisk App이 동작되는지 확인하여줍니다. 실습을 위한 목적이기 때문에 Magisk 실행이후 "설치됨" 등 여부는 상관이 없으니 설치 자체에 의미를 두고 진행해주면 됩니다.

Step 5) Magisk 설치 전에는 Success 로 나타나던 응답이 Fail 로 변화된 것이 확인됩니다.

Step 6) Bytecode Viewer를 실행한 뒤에 Anditer APK를 열고 magisk 문자열을 검색하여 코드를 탐색합니다.

Step 7) com\playground\anditer\RootingDetector 코드에서 rootingPackages 변수에서 루팅 여부를 검증하기 위한 패키지 목록 배열을 확인할 수 있습니다.
배열의 목록이 패키지 목록으로 나열되어 있는 것으로 보아 추정할 때, 이 값에 해당되는 패키지가 설치되어 있다면 루팅이 된 것으로 처리될 것으로 보이고 이 값에 해당되지 않는 패키지라면 루팅 감지 처리에서 벗어날 수 있을 것으로 예상해 볼 수 있을 겁니다.
이 말인 즉, 우리가 Frida 스크립트 작성으로 루팅 여부 탐지를 우회하고자 할 때 이 값을 변경하는 방법도 하나의 대안으로 생각해볼 수 있을 겁니다.

Step 8) 코드를 좀 더 살펴보면서 앞서 확인한 rootingPackages 변수를 사용하고 있는 부분을 탐색해보면 isCheckRootingInstalled 라는 함수에 해당 변수를 참조하고 있음을 확인할 수 있습니다.
여기서 isCheckRootingInstalled 라는 함수는 반환 값이 boolean 으로 참(True) 또는 거짓(False)으로 루팅 패키지 설치 여부를 판단한다고 추정해볼 수 있을 것인데, 이 말인 즉 이 함수를 변조하여 항상 false 값을 반환하도록 변조하는 방안도 하나의 대안으로 생각해볼 수 있을 겁니다.

Step 9) 이제 우리는 앞서 수집한 정보를 바탕으로 패키지 기반의 루팅 여부 탐지를 우회하기 위한 Frida 스크립트를 만들어야 합니다. 첫 번째 작업으로 rootingPackages 배열을 빈 배열로 바꾸어 검증을 우회하는 코드를 작성해 보겠습니다.
우선 배열 변수를 변조하기 위한 코드 영역을 확인해 보면 다음과 같은 코드 블럭을 확인해 볼 수 있을 건데요.
public final class RootingDetector extends AppCompatActivity {
...
private String[] rootingPackages;
...
public RootingDetector(Context var1) {
...
this.rootingPackages = new String[]{"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", "eu.chainfire.supersu.pro", "me.phh.superuser", "com.android.rooting.apk", "com.playground.rooting"};
...
생성자를 통해서 String 형태의 배열 변수인 rootingPackages 에 값이 할당되는 모습을 볼 수 있습니다. 그리고 이 rootingPackages 변수의 접근자는 private로 외부에서 접근할 수 없도록 되어 있습니다.
이 경우 생성자가 호출되는 영역에서 rootingPackages 변수 값이 빈 배열로 대입되도록 변조를 시도해 볼 수 있겠지만, 만약 생성자가 스크립트가 동작하기 전에 먼저 수행된다면 영향을 미칠 수 없을 것입니다.
실제로 해당 영역은 Anditer 가 실행될 때 초기화가 이루어지도록 되어 있는 구성이 되어 있어 우리는 메모리 영역에 접근하여 이미 생성된 rootingPackages 변수를 직접적으로 변조하는 방법을 사용해 볼 것입니다.
Frida 스크립트에서 메모리 영역을 탐색하여 대응하려면 아래와 같은 코드를 활용해 볼 수 있습니다.
Java.choose(TargetClass, {
onMatch: function (instance) { // 객체를 찾으면 수행되는 영역
...
},
onComplete: function () { // 처리가 끝나면 수행되는 영역
...
}
})
위 코드 영역을 활용해 메모리 상에 있는 rootingPackages 변수 값을 변조하는 코드는 아래와 같이 작성을 해 볼 수 있을 겁니다.
Java.choose("com.playground.anditer.RootingDetector", {
onMatch: function (instance) { // 객체를 찾으면 수행되는 영역
console.log("[*] Memory Hook Called - RootingDetector.rootingPackages value");
if ('rootingPackages' in instance) { // instance 내에 rootingPackages 가 존재하면 수행
if (instance.rootingPackages.value != "") { // rootingPackages 값이 빈 값이 아니면 수행
// 이미 실행된 객체의 rootingBinaries 값을 빈 배열로 변경.
instance.rootingPackages.value = Java.array('java.lang.String', []);
console.log("[*] rootingPackages value modified to empty array at runtime.");
}
}
},
onComplete: function () { console.log(""); } // 처리가 끝나면 수행되는 영역
})
Step 10) 두 번째 방법으로 isCheckRootingInstalled 함수의 반환 값을 항상 false 값으로 반환하도록 하여 루팅 탐지를 우회하는 코드를 작성해보도록 하겠습니다.
public final boolean isCheckRootingInstalled() {
String[] var3 = this.rootingPackages;
int var2 = var3.length;
int var1 = 0;
while(var1 < var2) {
String var4 = var3[var1];
try {
PackageManager var5 = this.mContext.getPackageManager();
Intrinsics.checkNotNullExpressionValue(var5, "mContext.packageManager");
this.getPackageInfoCompat(var5, var4, 0);
return true;
} catch (PackageManager.NameNotFoundException var6) {
++var1;
}
}
return false;
}
코드를 볼 때 rootingPackages 배열을 활용해 반복문을 돌려 설치되어 있는 패키지 목록 중에 rootingPackages 배열에 있는 패키지가 존재할 경우 return true 를 반환하여 루팅 패키지가 설치되어 있다고 반환하는 코드를 분석할 수 있습니다. 만약 탐지 되지 않았다면 마지막 줄의 return false를 통해 설치되지 않았다는 응답 값을 반환하는 것으로 추정해볼 수 있겠죠.
이 말인 즉 이 isCheckRootingInstalled 함수의 반환 값이 항상 false 값으로 반환 되면 rootingPackages 배열과도 무관하게 루팅 탐지 우회가 가능할 것으로 예상해 볼 수 있습니다.
사실 우회 할 때는 이렇게 True 또는 False 의 값을 반환하는 함수 형태를 조작하는 것이 제일 단순한 방법이 되기도 합니다. 이러한 코드는 Frida에서 변조하기도 쉬운데 다음과 같은 코드 예문을 활용하면 됩니다.
{class}.{method(function)}.implementation = function () {
{code block}
}
이 블록을 활용하여 항상 false 값을 반환하도록 한 코드는 다음과 같을 겁니다.
var targetClass = Java.use("com.playground.anditer.RootingDetector");
targetClass.isCheckRootingInstalled.implementation = function () {
try {
console.log("[*] Called - RootingDetector.isCheckRootingInstalled() : Returned false");
return false;
} catch (e) { console.log(e); }
}
여기까지 진행해 볼 때 만약 직접적으로 스크립트 작성이 어려우신 분들이 계시다면 제가 작성하여 아래 첨부해 놓은 스크립트를 다운로드 받아 과정을 따라가 주시면 되겠습니다.
Step 11) 제공된 frida 스크립트에서는 변수를 변조(후킹)하는 과정을 보기 위한 valuableHooking 변수, 함수 자체를 변조(후킹)하는 과정을 보기 위한 functionHooking 변수가 포함되어 있는데 false 또는 true 로 값을 변조하여 동작을 확인해볼 수 있습니다.
둘 중에 하나라도 true 라면 패키지 설치 여부를 기반으로 한 루팅 탐지가 우회 되는 모습을 보실 수 있을 것인데, 마음 가는대로 true 또는 false로 변조하여 다음으로 넘어가겠습니다.

Step 12) 우리는 uv를 이용한 환경 구성을 수행했었기 때문에 uv run frida -U -N com.playground.anditer -l anditer_bypass_packages.js
와 같이 명령어를 통해 frida 스크립트를 실행합니다.
만약 정상적으로 동작이 되지 않는다면 uv가 설치되어 있지 않거나 또는 관련 패키지 설치가 되지 않은 상황일 수 있으므로 실습 환경 구성하기 부분으로 되돌아가 환경이 잘 구성되어 있는지 검토해주시면 되겠습니다.

Step 13) 이제 Anditer 가 실행되고 있는 에뮬레이터 환경으로 돌아와 Bypass Packages 의 Check 부분을 선택하면 패키지 기반 루팅 여부 탐지 부분이 우회 되어 Success 응답 값을 확인할 수 있습니다.

토론하기