안드로이드 앱 보호 솔루션 우회

DOUIK·2022년 7월 28일
1

Android

목록 보기
4/7

환경

대상 앱 : Dream-detector.apk
Frida 버전 : 15.2.2 android x86
기기 정보 : Pixel 4XL
API 버전 : 30

검사 항목

Check Rooting

기기가 루팅되었는지 검사합니다.

  • Rooting package list check: 루팅 관련 패키지를 검사합니다.
  • Rooting binaries list check: 루팅 관련 바이너리를 검사합니다.

Check Frida

앱 내에서 프리다가 동작 중인지 검사합니다.

  • Frida port check: 프리다가 주로 사용하는 포트 대역을 검사합니다.
  • Frida path check: 프리다가 주로 사용하는 경로를 검사합니다.
  • Frida module check: 앱 메모리 내에 프리다 모듈이 로드되어있는지 검사합니다.

Check Emulator

앱이 에뮬레이터 위에서 동작 중인지 검사합니다.

  • Emulator File Check: 에뮬레이터 관련 파일을 검사합니다.
  • Emulator Properties Check: 에뮬레이터 관련 시스템 속성을 검사합니다.

Check Debugging

앱이 디버깅 중인지 검사합니다.

  • Debugging TracerPid check: 앱의 TracerPid를 이용해 디버깅을 검사합니다.
  • Debugging Gdb Path Check: gdb가 기기 내에 존재하는지 경로를 검사합니다.
  • Debugging Property Check: 디버깅 관련 시스템 속성을 검사합니다.

TLS Pinner

인증서 변조를 통한 TLS MITM 공격을 검사합니다.

  • 서버의 인증서가 Root CA에 의해 신뢰할 수 있는지 검사합니다.
  • 서버의 인증서가 앱 내에 별도로 고정한 해시와 같은지 검사합니다.

실행

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 ]->

위와 같이 뜨면 성공

Rooting 파일 검사 우회

👀 기기가 루팅되어 있으면 안드로이드에서 기본적으로 지원하는 내장 보안 기능이 약화되어 다양한 보안 위험을 발생시킬 수 있음 따라서 금융 관련 앱과 같이 높은 보안성이 요구되는 앱은 기기의 루팅 여부를 탐지해 앱의 기능을 사용하지 못하게 하거나 앱 동작을 중단시킴

Dream-detector의 Check Rooting 기능은 루팅 탐지를 위해 2가지 방법 구현

  1. Rooting 파일 검사 : 루팅 탐지를 위해 루트 권한을 획득할 수 있는 특정 바이너리 탐지
  2. Rooting 패키지 검사 : 루팅 탐지를 위해 루트 권한이 필요하거나 루트 권한을 획득할 수 있는 특정 앱 패키지가 기기에 설치되어 있는지 검사

doRootingBinaryCheck 함수 분석

기능 분석은 일반적으로 MainActivity부터 차례로 분석함. MainActivity에는 각각의 탐지 기능들에 대한 버튼과 버튼 클릭과 함께 변경될 UI를 Fragment 객체로 동작시키는 것이 확인됨

MainActivity

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()
        }

RootingFragment.doRootingBinaryCheck()

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)
            }
        }
    }
}
  • line 9 rootingDetector.checkSuBinary() : 루팅에 사용되는 파일이 기기에 있는지 확인

checkSuBinary 함수 분석

RootingDetector 클래스에 루팅 탐지를 위한 함수들이 구현되어 있고 그 중 checkSuBinary 함수는 파일을 확인해서 루팅을 탐지함

  1. Constants.knownSuDirectories에 정의된 탐지할 파일 이름을 반복문을 돌며 하나씩 directory에 저장
  2. Constants.knownSuBinaries에 저장된 경로를 반복문을 돌며 하나씩 filename 변수로 불러와, directory과 결합하여 온전한 파일 경로로 f라는 파일 객체를 생성
  3. 만약 f라는 파일이 존재한다면 f.exists 함수를 통해 파일의 존재 여부를 확인함. 존재하면 true, 존재하지 않으면 반복문 다 돌고 나와서 false 반환

검사할 파일명 및 파일경로

그래서 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"
    };
  • SuDirectory 경로에 있는 su, busybox, magisk라는 3가지 파일명을 검사함
    • busybox : unix 유틸리티를 제공하는 실행 파일, root 권한 획득 가능
    • su : user id를 통해 권한을 바꾸는 실행 파일, root 권한으로 변경 가능
    • magisk : 앱에 root 권한으로 접근, 부트 이미지 리패키징, read-only 저장소 수정
  • 각각 파일들은 모두 루트 권한을 획득할 때 주로 사용하는 파일

Rooting 파일 검사 우회 아이디어

함수 반환값 조작

checkSuBinary 함수 반환 할 때 f.exists 함수가 true면 true를 반환함. 이 f.exists()의 반환값을 조작해서 if문으로 진입하지 못하게 하거나, 존재할 경우의 return 값을 0이나 false로 조작해서 우회

우회 스크립트 작성

checkSuBinary 반환값 변경

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();
  • 결과 : PASSED

Rooting 패키지 검사 우회

doRootingPackageCheck 함수 분석

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)
            }
        }
    }
  • line 5 rootingDetector.checkPackage : 루팅과 관련있는 앱 설치 여부 확인

checkPackage 함수 분석

Rooting 패키지 검사 기능은 미리 정의되어있는 앱 패키지 목록을 기반으로 루팅 관련 패키지의 설치 여부를 검사함

RootingDetector.checkPackage()

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;
}
  1. 탐지할 패키지 명이 정의된 Constants.knownRootAppsPackages를 packages에 저장
  2. Context 클래스에 getPackageManager() 함수를 통해 패키지 매니저(pm) 객체를 만듦
  3. packages에 저장된 탐지 패키지 목록을 반복문으로 하나씩 packageName으로 저장함
  4. getPackageInfo 함수로 패키지 명에 해당하는 패키지 정보를 가져옴. 이 때 pm을 통해 정보를 가져올 수 있다면 기기 내에 해당 패키지가 설치되어 있음을 의미하므로 true를 반환함. 반대로 설치되지 않았으면 NotFound 예외가 발생하고 false를 반환함

검사할 패키지 명

Constants.knownRootAppsPackages

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"
};
  • knownRootAppsPackages에 정의된 패키지들은 루트 권한이 필요하거나 루팅 하기 위해 필요한 앱 등 루팅과 관련된 앱으로 이 패키지가 기기 내에 존재한다면 해당 기기는 루팅되었을 확률이 높음

Rooting 패키지 검사 우회 아이디어

함수 반환값 변경

checkPackage 함수는 루팅 관련 패키지가 탐지되면 true를 리턴하고 반대는 false를 리턴함
따라서 함수가 무조건 false로 반환하도록 후킹해서 조작 가능

검사할 패키지 명 변경

정해진 리스트 기반으로 패키지 명을 검사하기 때문에 해당 변수를 의미없는 문자열을 가진 배열로 변경하면 루팅 관련 패키지가 설치되어도 탐지 할 수 없음

우회 스크립트 작성

checkRootingPackage 반환값 변경

getPackageInfo를 통해 루팅 관련 패키지 존재여부를 검사할 때 RootDetector클래스의 checkPackage 함수 반환값을 사용함. 함수의 반환값이 False라면 디버깅 상태가 아닌 것으로 간주하고 PASSED.를 출력한다.

Bypass_Rooting_checkPackage_modifyCheckPackageRet.js

function modifyCheckPackageRet() {
    Java.perform(function() {
        var RootingDetector = Java.use("android.com.dream_detector.RootingDetector");
        RootingDetector.checkRootingPackage.implementation = function() {
            return false;
        };
    });
}

modifyCheckPackageRet();
  1. Java.use 함수 사용해서 Frida 내에 RootingDetector 클래스에 접근할 수 있도록 Wrapper를 제공받음. 제공받은 Wrapper를 통해 클래스 내의 checkRootingPackage 함수에 접근할 수 있음
  2. Frida에서 자바 단의 함수를 후킹할 때에는 일반적으로 클래스 Wrapper를 제공받은 후에 후킹할 함수의 implementation을 자바스크립트 함수로 덮는 방식을 사용함
  3. 따라서 RootingDetector.checkRootingPackage.implementation에 새로운 함수를 정의하는 방식으로 checkRootingPackage 메소드를 후킹할 수 있음
  4. 새로 정의하는 함수가 항상 false를 반환하도록 구현해서 루팅 탐지를 우회함

Constants.knownRootAppsPackages 값 변경

knownRootAppsPackages에 정의된 검사할 패키지 목록은 getPackageInfo의 인자로 전달된다. 따라서 해당 변수에 정의된 패키지 명을 의미없는 문자열로 변경하면 checkPackage함수는 패키지가 존재하지 않는 것으로 판단하고 PASSED.를 출력함

Bypass_Rooting_checkPackage_modifyKnownPackagesVal.js

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();
  1. android.com.dream_detector.Constants 클래스에 접근할 수 있도록 Java.use를 사용해 Wrapper를 제공받음
  2. knownRootAppsPackages변수는 문자열 배열이므로 Java.array를 사용하여 새로운 문자열 타입의 배열을 만들어줌. 이 때 루팅 관련 앱을 모두 탐지 할 수 없도록 빈 문자열 하나만을 포함하는 배열로 만듦
  3. 만들어진 배열을 knownRootAppsPackages변수에 대입하면 checkPackage함수는 getPackageInfo 함수에 빈 문자열 배열을 인자로 전달하기 때문에 루팅 관련 페이지가 존재하지 않는다고 판단해 항상 false를 반환함

0개의 댓글