안드로이드·갤럭시·AOS 애플리케이션 취약점진단/모의해킹 무료 강의 학식(hagsig)
가. 취약점 정의
- 안드로이드는 사용자가 최상위 권한(root)을 획득할 수 없도록 되어있으나, 루팅(rooting)을 통해 최상위 권한을 획득할 수 있다.
- 루팅된 디바이스에서 앱이 동작할 경우 보안을 우회하는 등의 행위가 가능해서 개인정보 유출 등의 침해사고가 발생할 수 있다.
- 루팅된 디바이스에서 앱이 동작하면 강제종료 되게끔 보안조치를 하여야 한다.
- 허술하게 루팅 탐지 로직을 구현할 경우 *런타임 조작
과 *smali 파일
의 *달빅 바이트 코드
변조등의 방법으로 쉽게 우회할 수 있다.
*런타임 조작 : 앱 실행 중에 동작을 조작하여 정상적인 결과가 아닌 공격자가 원하는 결과가 되도록 하는 것
*smali 파일 : An assembler/disassembler for Android’s dex format의 약자로 DEX 바이너리를 사람이 읽을 수 있도록 표현한 언어
*달빅 바이트 코드 : 안드로이드 런타임에서 구동되는 Java 바이트 코드로 smali 파일에 기입되어 있음
- 상용 솔루션을 쓰지 않는 이상 완벽한 대응방법은 없겠지만 보안조치를 복합적으로 적용하면 루팅 탐지 우회를 매우 어렵게 하여 레벨이 낮은 해커들의 공격에는 충분히 대응할 수 있다.
나. 취약점 대응방안(조치방법)
1. 루팅 탐지 기능 추가
- 루팅된 환경에서 디바이스가 동작중인지 확인할 수 있는 방법 중 대표적인 방법은 아래와 같다.
- 아래의 방법 중 하나만 사용하는게 아니라 복합적으로 사용하여 루팅 탐지 우회 시 최대한 어렵게 하여야한다.
- 함수/변수명은 그대로 가져다 쓰는게 아닌 최대한 추측이 불가능한 문구로 변경하여 사용하는 것이 좋다.
1.1. 루팅된 환경에서만 사용할 수 있는 명령어의 실행결과를 통한 루팅 탐지 방법
- su, which 등과 같은 루팅된 환경에서만 사용가능한 명령어를 실행하고 그 결과값으로 루팅 여부를 판단한다.
private boolean checkSuExists() {
Process process = null;
try {
process = Runtime.getRuntime().exec(new String[] {"/system /xbin/which", "su"});
BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line = in.readLine();
process.destroy();
return line != null;
} catch (Exception e) {
if (process != null) {
process.destroy();
}
return false;
}
}
1.2. Test Key 존재 확인을 통한 루팅 탐지 방법
- 루팅이 되지 않은 정상적인 환경에서는 release-key를 찾을 수 있게되고 루팅된 환경에서는 test-key를 찾을 수 있다.
private boolean detectTestKeys() {
String buildTags = android.os.Build.TAGS;
return buildTags != null && buildTags.contains("test-keys");
}
1.3. Google OTA 인증서
- 루팅된 환경에서는 *Google OTA(Over-The-Air)
인증서가 존재하지 않으므로, 파일 존재여부를 통해 루팅 여부를 확인할 수 있다.
*OTA : 새로운 펌웨어, 설정, 암호화 키를 휴대전화와 같은 장치에 무선으로 배포하기 위한 방식을 말하며, 비휘발성 메모리에 탑재되어 있다.
public static boolean checkOTACerts() {
String OTAPath = "/etc/security/otacerts.zip";
File f = new File(OTAPath);
boolean fileExists = f.exists();
if (fileExists) {
return true;
}
else {
return false;
}
}
1.4. 루팅된 환경에서만 존재하는 바이너리(파일) 존재여부를 통한 루팅 탐지 방법
- 루팅 환경에서 사용하는 *바이너리
가 존재하는지 확인하여 루팅 여부를 확인할 수 있다.
*루팅 환경에 존재하는 바이너리 : busybox, su, magisk, superuser.apk 등
private String[] binaryPaths = {
"/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/",
"/system/app/",
"/cache",
"/data",
"/dev"
};
private boolean checkForBusyBoxBinary() {
return checkForBinary("busybox");
}
private boolean checkForSuBinary() {
return checkForBinary("su");
}
private boolean checkForMagiskBinary() {
return checkForBinary("magisk");
}
private boolean checkSuperuserApkBinary() {
return checkForBinary("Superuser.apk");
}
private boolean checkForBinary(String filename) {
for (String path : binaryPaths) {
File f = new File(path, filename);
boolean fileExists = f.exists();
if (fileExists) {
return true;
}
}
return false;
}
1.5. 루팅 시 설치되는 패키지의 존재 유무를 통한 루팅 탐지 방법
- 루팅 시 설치되는 패키지가 존재하는지 확인하여 루팅 여부를 판단할 수 있다.
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"
};
public static boolean checkRootFilesAndPackages(Context context) {
boolean result = false;
for(String string1: knownRootAppsPackages) {
if(isPackageInstalled(string1, context)) {
result = true;
break;
}
}
return result;
}
private static boolean isPackageInstalled(String packagename, Context context){
PackageManager pm = context.getPackageManager();
try {
pm.getPackageInfo(packagename, PackageManager.GET_ACTIVITIES);
return true;
} catch (NameNotFoundException e) {
return false;
}
}
1.6. 루팅 시 설치되는 앱의 존재 유무를 통한 루팅 탐지 방법
- 루팅 시 설치되는 앱이 존재하는지 확인하여 루팅 여부를 판단할 수 있다.
public static final String[] knownDangerousAppsPackages = {
"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_"
};
public boolean detectPotentiallyDangerousApps(String[] additionalDangerousApps) {
ArrayList packages = new ArrayList<>();
packages.addAll(Arrays.asList(Const.knownDangerousAppsPackages));
if (additionalDangerousApps!=null && additionalDangerousApps.length>0){
packages.addAll(Arrays.asList(additionalDangerousApps));
}
return isAnyPackageFromListInstalled(packages);
}
1.7. Rooting Cloaking 앱 설치 유무로 루팅 탐지 방법
- 타 앱에서 루팅여부를 판단할 수 없도록 루팅 상태를 숨길수 있는 은폐 앱(Cloaking App)들이 설치되어 있는 경우가 있다.
- 이럴경우 역으로 Cloaking App이 설치되어 있는지 확인하여 루팅 여부를 판단할 수 있다.
public static final String[] knownRootCloakingPackages = {
"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"
};
public boolean detectRootCloakingApps(String[] additionalRootCloakingApps){
ArrayList packages = new ArrayList<> (Arrays.asList(Const.knownRootCloakingPackages));
if (additionalRootCloakingApps!=null && additionalRootCloakingApps.length>0){
packages.addAll(Arrays.asList(additionalRootCloakingApps));
}
return isAnyPackageFromListInstalled(packages);
}
private boolean isAnyPackageFromListInstalled(List packages){
boolean result = false;
PackageManager pm = mContext.getPackageManager();
for (String packageName : packages) {
try {
pm.getPackageInfo(packageName, 0);
QLog.e(packageName + " ROOT management app detected!");
result = true;
} catch (PackageManager.NameNotFoundException e) {
// Exception thrown, package is not installed into the system }
}
return result;
}
1.8. 특정 디렉터리의 쓰기권한을 통한 루팅 탐지 방법
- 안드로이드 운영체제는 특정 디렉터리에 쓰기 권한이 제한되어 있다.
- 루팅된 환경에서는 이런 디렉터리에도 쓰기 권한으로 접근이 가능하므로 이를 통해 루팅 여부를 확인할 수 있다.
public static final String[] pathsThatShouldNotBeWrtiable = {
"/",
"/data",
"/system",
"/system/bin",
"/system/sbin",
"/system/xbin",
"/vendor/bin",
"/sys",
"/sbin",
"/etc",
"/proc",
"/dev"
}
public static boolean checkForRWPaths() {
boolean result = false;
String[] lines = mountReader();
for (String line : lines) {
String[] args = line.split(" ");
if (args.length < 4){
continue;
}
String mountPoint = args[1];
String mountOptions = args[3];
for(String pathToCheck: Constants.pathsThatShouldNotBeWrtiable) {
if (mountPoint.equalsIgnoreCase(pathToCheck)) {
for (String option : mountOptions.split(",")){
if (option.equalsIgnoreCase("rw")){
result = true;
break;
}
}
}
}
}
return result;
}
1.9. build.prop 설정값 확인을 통한 루팅 탐지 방법
- Android 기기의 build.prop 파일에는 운영 기기 전체에 적용되는 시스템 속성과 빌드 정보가 포함되어 있다.
- 정상적인 환경에서의 속성과 루팅된 환경에서의 *속성의 값
이 다르므로 이를통해 루팅 여부를 확인할 수 있다.
*ro.debuggable = 1 이면 루팅된 환경이라 판단
*ro.secure = 0 이면 루팅된 환경이라 판단
private String[] propsReader() {
try {
InputStream inputstream = Runtime.getRuntime().exec("getprop").getInputStream();
if (inputstream == null) return null;
String propVal = new Scanner(inputstream).useDelimiter("\\A").next();
return propVal.split("\n");
} catch (IOException | NoSuchElementException e) {
QLog.e(e);
return null;
}
}
public boolean checkForDangerousProps() {
final Map dangerousProps = new HashMap<>();
dangerousProps.put("ro.debuggable", "1");
dangerousProps.put("ro.secure", "0");
boolean result = false;
String[] lines = propsReader();
if (lines == null){
return false;
}
for (String line : lines) {
for (String key : dangerousProps.keySet()) {
if (line.contains(key)) {
String badValue = dangerousProps.get(key);
badValue = "[" + badValue + "]";
if (line.contains(badValue)) {
QLog.v(key + " = " + badValue + " detected!");
result = true;
}
}
}
}
return result;
}
2. 소스코드 난독화 적용
- 안드로이드 앱은 APK파일 디컴파일을 통해 누구든지 소스코드를 확인할 수 있다. 소스코드가 평문으로 저장되어 있으면 스말리 코드를 분석하는데 많은 도움이 되므로 *프로가드(Proguard)와 같은 소스코드 난독화 프로그램을 이용하여 소스코드를 쉽게 분석할 수 없도록 한다.
*프로가드 : 안드로이드 스튜디오에서 기본적으로 제공하는 소스코드 난독화 도구
3. 무결성 검증 기능 적용
- 앱의 Signing 키를 검증하는 등의 방식을 사용하여 무결성 검증 기능을 추가한다.
- 변조된 코드로 동작하는 리패키징 앱 실행 시 강제종료되도록 하여야 하며, 웹 통신으로 키를 검증하는 방식은 중간자공격에 의해 우회될 수 있으므로 주의하여야 한다.
4. 안티 디버깅 기능 적용
- 무결성 검증 기능을 디버깅 툴을 이용해 우회할 수 없도록 안티디버깅 기능을 적용한다.
https://seo-security.tistory.com/19
다. 취약점 실습
1. 분석 대상 앱 설치
- 실습을 위해 인시큐어뱅크 애플리케이션을 설치한다.
※ 인시큐어뱅크 앱이 설치되어있지 않은 사람은 아래의 글을 참고하여 앱을 설치하길 바란다.
2023.04.10 - [Mobile App 취약점 진단/AOS App 진단] - 인시큐어뱅크 앱 설치 및 실행 방법 정리(InsecureBankv2)
2. 진단 대상 앱 apk 파일 추출
- 아래의 링크를 참고하여 인시큐어뱅크 앱으로부터 apk 파일을 추출한다.
안드로이드 앱(app)으로부터 apk 파일 추출 방법 정리
3. APK파일 디컴파일
- 아래의 게시글을 참고하여 추출한 APK파일을 디컴파일 한다.
[AOS App 취약점 진단 · 모의해킹] - 안드로이드 APK 파일 디컴파일/리패키징 방법
4. extractNativeLibs 속성 확인
- 리패키징 홈 디렉토리에 존재하는 AndroidManifest.xml 파일을 메모장으로 열어 extractNativeLibs 속성이 false로 설정되어 있는지 확인한다.
- extractNativeLibs 속성이 false로 설정되어 있을경우, 앱 변조 후 리패키징시 오류가 발생하므로 true로 변경하여야 한다
#extractNativeLibs 예시
<application
android:extractNativeLibs="false">
</application>Copy
5. 루팅 탐지 포인트 탐지
- 본인이 변조하고자 하는 기능을 정한뒤 소스코드를 분석하여 스말리 코드의 어느 부분을 변조하면 되는지 탐색하는 과정이다.
※ 만약 본인이 무결성 검증 기능이 존재하는지 확인만 하고 싶은 거라면 아래의 모든 과정을 생략하고 APK파일을 리패키징한 다음 설치 및 실행하여 정상 동작하는지만 확인하면 된다.
- 본글에서는 인시큐어뱅크 앱 로그인 후 메인화면에 출력되는 Text 문구를 변조하여 볼 것이다.
#인시큐어뱅크 기본 계정정보(default ID/PW)
username : dinesh
password : Dinesh@123$
username : jack
password : Jack@123$Copy
6. 코드 분석
- 아래의 링크에 접속하여 jadx-gui-x.x.x-with-jre-win.zip 파일을 다운로드한 뒤, 압축을 해제한다.
https://github.com/skylot/jadx/releases/tag/v1.4.7
- jadx-gui-x.x.x.exe를 실행한 뒤 위에서 추출한 인시큐어뱅크 apk파일을 로드한 다음, "Rooted Device!!"을 검색한다.
- 소스코드를 분석하니 boolean 타입의 isrooted 변수 값이 true/false에 따라 루팅탐지 여부를 판단하는 것으로 확인된다.
*조건1 : /system/app/Superuser.apk
파일 존재 여부
*조건2 : su
명령어 사용가능 여부
- 아래의 *경로에 진입하뒤 PostLogin.smali 파일을 메모장으로 연다.
*경로 : InsecureBankv2\smali\com\android\insecurebankv2\PostLogin.smali
- 메서드 명 "showRootStatus()"을 검색하여 소스코드 분석을 통해 알아낸 변조 포인트를 찾아낸다.
- smile 코드를 분석한 내용은 아래와 같다.
422 .method showRootStatus()V ← 메소드 명과 리턴타입을 명시함.
423 .locals 3 ← 메소드 내부에서 사용되는 레지스터 갯수를 명시함.
424
425 .prologue ← 메소드 내부에의 코드 시작을 알림.
426 const/4 v1, 0x1 ← v1에 1(true)값을 저장.
427
428 .line 86 ← 소스코드의 86번 라인인 것을 명시함.
429 const-string v2, "/system/app/Superuser.apk" ← "/system/app/Superuser.apk" 문자열을 v2에 저장
430
431 invoke-direct {p0, v2}, Lcom/android/insecurebankv2/PostLogin;->doesSuperuserApkExist(Ljava/lang/String;)Z ← PostLogin에서 doesSuperuserApkExist 함수를 v2 인자로 호출.
432
433 move-result v2 ← 호출한 함수의 리턴값을 v2에 저장.
434
435 if-nez v2, :cond_0 ← v2의 값이 0과 다르면 cond_0 지점으로 이동.
436
437 .line 87 ← 소스코드의 87번 라인인 것을 명시함.
438 invoke-direct {p0}, Lcom/android/insecurebankv2/PostLogin;->doesSUexist()Z ← PostLogin에서 doesSUexist 함수를 호출.
439
440 move-result v2 ← 호출한 함수의 리턴값을 v2에 저장.
441
442 if-eqz v2, :cond_1 ← v2의 값이 0과 같으면 cond_1 지점으로 이동.
443
444 :cond_0 ← goto의 도착지점 cond_0를 명시함.
445 move v0, v1 ← v1의 값을 v0으로 이동.
446
447 .line 88 ← 소스코드의 88번 라인인 것을 명시함.
448 .local v0, "isrooted":Z ← isrooted를 v0으로 명명.
449 :goto_0 ← goto의 도착지점 goto_0를 명시함.
450 if-ne v0, v1, :cond_2 ← v0과 v1의 값이 다르다면 cond_2 지점으로 이동.
451
452 .line 90 ← 소스코드의 90번 라인인 것을 명시함.
453 iget-object v1, p0, Lcom/android/insecurebankv2/PostLogin;->root_status:Landroid/widget/TextView; ← PostLogin에서 p0의 인스턴스필드 TextView를 v1으로 가져온다.
454
455 const-string v2, "Rooted Device!!" ← v2에 "Rooted Device!!"를 저장한다.
456
457 invoke-virtual {v1, v2}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V ← TextView에서 setText 함수를 v2 인자로 호출. 화면에 "Rooted Device!!" 문자열이 표시된다.
458
459 .line 96 ← 소스코드의 96번 라인인 것을 명시함.
460 :goto_1 ← goto의 도착지점 goto_1을 명시함.
461 return-void ← 메소드의 결과값을 리턴하며 메소드를 종료함.
462
463 .line 87 ← 소스코드의 87번 라인인 것을 명시함.
464 .end local v0 # "isrooted":Z ← isrooted 인스턴스 v0을 해제함.
465 :cond_1 ← goto의 도착지점 cond_1을 명시함.
466 const/4 v0, 0x0 ← v0에 0(false)를 저장함.
467
468 goto :goto_0 ← goto_0 지점으로 이동함.
469
470 .line 94 ← 소스코드의 94번 라인인 것을 명시함.
471 .restart local v0 # "isrooted":Z ← isrooted 인스턴스 v0을 재할당함.
472 :cond_2 ← goto의 도착지점 cond_2를 명시함.
473 iget-object v1, p0, Lcom/android/insecurebankv2/PostLogin;->root_status:Landroid/widget/TextView; ← PostLogin에서 p0의 인스턴스필드 TextView를 v1으로 가져온다.
474
475 const-string v2, "Device not Rooted!!" ← v2에 "Device not Rooted!!"를 저장한다.
476
477 invoke-virtual {v1, v2}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V ← TextView에서 setText 함수를 v2 인자로 호출. 화면에 "Device not Rooted!!" 문자열이 표시된다.
478
479 goto :goto_1 ← goto_1 지점으로 이동함.
480 .end method ← 메소드의 끝을 명시함.
- 코드를 분석한 결과 첫 번째 분기점의 분기조건과 조건에 따른 이동지점을 루팅이 탐지되지 않았을 때 실행되는 지점으로 변조한다면 루팅탐지 우회를 할 수 있을 것 같다.
- 아래와 같이 코드를 변경하여 루팅탐지가 될 경우 제일 마지막 부분으로 보내 루팅탐지 기능을 우회하도록 변조한다.
- v1에 기본적으로 1(true)이 저장되어 있고 v2에는 루팅된 환경에서 실행시켰으므로 1(true)이 저장되어 있을 것이다.
- 둘이 동일한 값을 가졌으므로, 동일한 값을 가졌을때 cond_2로 이동하라고 아래와 같이 수정한다.
- 아래의 게시글을 참고하여 변조한 파일을 리패키징/인증서를 씌운다.
[AOS App 취약점 진단 · 모의해킹] - 안드로이드 APK 파일 디컴파일/리패키징 방법
- 변조한 APK파일을 디바이스에 *설치한다.
*Nox일 경우 드래그 앤 드롭으로 간편하게 설치할 수 있고, 물리적 디바이스일 경우 adb install 명령어를 통해 설치할 수 있다.
- 변조한 APK파일을 실행하면 이전과는 다르게 본인이 변조한 문구가 출력되는 것을 확인할 수 있다.
- 프리다를 통한 런타임 조작으로 루팅탐지를 우회하고 싶다면 아래의 글을 참고하길 바란다.
라. 참고 URL
https://philosopher-chan.tistory.com/355
https://saltlee.tistory.com/155
https://www.appknox.com/blog/root-detection-techniques
https://stackoverflow.com/questions/44235138/root-check-for-android-device
'Mobile App 취약점 진단 · 모의해킹 > AOS App 취약점 진단 · 모의해킹' 카테고리의 다른 글
Android UnCrackable Level 2 문제풀이 (0) | 2024.03.02 |
---|---|
Android UnCrackable Level 1 문제풀이 (0) | 2024.02.25 |
[AOS 취약점 진단] 15강 - 애플리케이션 패칭/앱 무결성 검증 취약점 (0) | 2024.02.19 |
[런타임 조작 실습] FridaLab 설치 및 문제풀이 과정 정리 (0) | 2024.02.19 |
[AOS 취약점 진단] 14강 - 런타임 조작 취약점 점검(Frida/ADB) (0) | 2024.02.19 |