짱짱해커가 되고 싶은 나

[안드로이드 취약점 진단] 3-6. 루팅 탐지 및 우회 본문

모바일

[안드로이드 취약점 진단] 3-6. 루팅 탐지 및 우회

동로시 2022. 6. 19. 03:03

루팅(Rooting)

모바일 기기에서 구동되는 안드로이드 OS 상에서 최상의 권한 (root)을 얻음으로 해당 기기의 생산자/판매자 측에서 걸어 놓은 제약을 해제하는 행위

-> 루팅을 하면? 슈퍼 유저의 권한으로 하드웨어 성능 조작, 제조사 및 통신사 기본 apk 삭제, 시스템 권한을 이용한 디바이스 조작, 디바이스 내부 민감 정보 접근 등 가능

ps. 기본적으로 금융권 apk는 루팅된 기기에서의 앱 실행을 차단, 핀테크 기술을 제공하는 역시 루팅 허용 X

 

루트 노출 및 우회 취약점

안드로이드

  • 보안상의 이유로 루트 권한을 막아 놓음
  • 애플리케이션 실행 시 각 프로그램마다 권한 부여, 독립적으로 동작
    -> 순정 안드로이드는 애플리케이션에서 할 수 있는 행위 제한

루팅 체크 주요 경로

  • /system/bin/su
  • /system/xibn/su
  • /system/app/superuser.apk
  • /data/data/com.noshufou.android.su

루팅 체크 방법

  • 파일 이름 기반 탐지
  • su 명령어를 실행해서 확인
    - which를 통해 존재 확인
    - su - 명령어의 결과로 확인 (존재하면 에러X, 존재하지 않으면 에러)
    (일반적으로 su 파일이나 superuser.apk 등 루팅 시 생성되는 파일 이름을 기반으로 탐지)

 

인시큐어뱅크 - Root Detection and Bypass

PostLogin

로그인을 하게 되면 현재 접속한 기기가 루팅되었는지를 출력해준다.

관련 코드를 보면 다음과 같이 /system/app/Superuser.apk가 존재하는지 또는 /system/bin/which 명령어를 통해 su 명령어가 존재하는지를 확인한다.

(jadx는 doesSUexist를 제대로 디컴파일 못해줘서 추가적으로 bytecode viewer를 사용했다. 근데 얘도,,좀)

 

	private boolean doesSUexist() { //()Z
         TryCatch0: L0 to L1 handled by L15: java/lang/Throwable
         TryCatch1: L0 to L1 handled by L17: Type is null.
         TryCatch2: L2 to L3 handled by L15: java/lang/Throwable
         TryCatch3: L2 to L3 handled by L17: Type is null.
         TryCatch4: L4 to L5 handled by L15: java/lang/Throwable
         TryCatch5: L4 to L5 handled by L17: Type is null.
         TryCatch6: L6 to L7 handled by L15: java/lang/Throwable
         TryCatch7: L6 to L7 handled by L17: Type is null.
         TryCatch8: L8 to L9 handled by L15: java/lang/Throwable
         TryCatch9: L8 to L9 handled by L17: Type is null.
         TryCatch10: L10 to L11 handled by L15: java/lang/Throwable
         TryCatch11: L10 to L11 handled by L17: Type is null.
             iconst_1
             istore 1
             aconst_null
             astore 2
             aconst_null
             astore 3
         // start TCB0, start TCB1
         L0 {
             invokestatic java/lang/Runtime.getRuntime()Ljava/lang/Runtime;
             iconst_2
             anewarray java/lang/String
             dup
             iconst_0
             ldc "/system/xbin/which" (java.lang.String)
             aastore
             dup
             iconst_1
             ldc "su" (java.lang.String)
             aastore
             invokevirtual java/lang/Runtime.exec([Ljava/lang/String;)Ljava/lang/Process;
             astore 4
         }
         // end TCB0, end TCB1
         L1 {
             aload 4
             astore 3
             aload 4
             astore 2
         }
         // start TCB2, start TCB3
         L2 {
             new java/io/BufferedReader
             astore 5
         }
         // end TCB2, end TCB3
         L3 {
             aload 4
             astore 3
             aload 4
             astore 2
         }
         // start TCB4, start TCB5
         L4 {
             new java/io/InputStreamReader
             astore 6
         }
         // end TCB4, end TCB5
         L5 {
             aload 4
             astore 3
             aload 4
             astore 2
         }
         // start TCB6, start TCB7
         L6 {
             aload 6
             aload 4
             invokevirtual java/lang/Process.getInputStream()Ljava/io/InputStream;
             invokespecial java/io/InputStreamReader.<init>(Ljava/io/InputStream;)V
         }
         // end TCB6, end TCB7
         L7 {
             aload 4
             astore 3
             aload 4
             astore 2
         }
         // start TCB8, start TCB9
         L8 {
             aload 5
             aload 6
             invokespecial java/io/BufferedReader.<init>(Ljava/io/Reader;)V
         }
         // end TCB8, end TCB9
         L9 {
             aload 4
             astore 3
             aload 4
             astore 2
         }
         // start TCB10, start TCB11
         L10 {
             aload 5
             invokevirtual java/io/BufferedReader.readLine()Ljava/lang/String;
             astore 6
         }
         // end TCB10, end TCB11
         L11 {
             aload 6
             ifnull L13
             iload 1
             istore 7
             aload 4
             ifnull L12
             aload 4
             invokevirtual java/lang/Process.destroy()V
             iload 1
             istore 7
         }
         L12 {
             iload 7
             ireturn
         }
         L13 {
             aload 4
             ifnull L14
             aload 4
             invokevirtual java/lang/Process.destroy()V
         }
         L14 {
             iconst_0
             istore 7
             goto L12
         }
         // handle TCB0, handle TCB2, handle TCB4, handle TCB6, handle TCB8, handle TCB10
         L15 {
             astore 2
             aload 3
             ifnull L16
             aload 3
             invokevirtual java/lang/Process.destroy()V
         }
         L16 {
             iconst_0
             istore 7
             goto L12
         }
         // handle TCB1, handle TCB3, handle TCB5, handle TCB7, handle TCB9, handle TCB11
         L17 {
             astore 3
             aload 2
             ifnull L18
             aload 2
             invokevirtual java/lang/Process.destroy()V
         }
         L18 {
             aload 3
             athrow
         }
   	}
    
    //jadx-gui
    private boolean doesSUexist() {
        /*
            r7 = this;
            r2 = 0
            r0 = 1
            r1 = 0
            java.lang.Runtime r3 = java.lang.Runtime.getRuntime()     // Catch:{ Throwable -> 0x0039, all -> 0x0042 }
            r4 = 2
            java.lang.String[] r4 = new java.lang.String[r4]     // Catch:{ Throwable -> 0x0039, all -> 0x0042 }
            r5 = 0
            java.lang.String r6 = "/system/xbin/which"
            r4[r5] = r6     // Catch:{ Throwable -> 0x0039, all -> 0x0042 }
            r5 = 1
            java.lang.String r6 = "su"
            r4[r5] = r6     // Catch:{ Throwable -> 0x0039, all -> 0x0042 }
            java.lang.Process r2 = r3.exec(r4)     // Catch:{ Throwable -> 0x0039, all -> 0x0042 }
            java.io.BufferedReader r3 = new java.io.BufferedReader     // Catch:{ Throwable -> 0x0049, all -> 0x0042 }
            java.io.InputStreamReader r4 = new java.io.InputStreamReader     // Catch:{ Throwable -> 0x0049, all -> 0x0042 }
            java.io.InputStream r5 = r2.getInputStream()     // Catch:{ Throwable -> 0x0049, all -> 0x0042 }
            r4.<init>(r5)     // Catch:{ Throwable -> 0x0049, all -> 0x0042 }
            r3.<init>(r4)     // Catch:{ Throwable -> 0x0049, all -> 0x0042 }
            java.lang.String r3 = r3.readLine()     // Catch:{ Throwable -> 0x0049, all -> 0x0042 }
            if (r3 == 0) goto L_0x0032
            if (r2 == 0) goto L_0x0031
            r2.destroy()
        L_0x0031:
            return r0
        L_0x0032:
            if (r2 == 0) goto L_0x0037
            r2.destroy()
        L_0x0037:
            r0 = r1
            goto L_0x0031
        L_0x0039:
            r0 = move-exception
            r0 = r2
        L_0x003b:
            if (r0 == 0) goto L_0x0040
            r0.destroy()
        L_0x0040:
            r0 = r1
            goto L_0x0031
        L_0x0042:
            r0 = move-exception
            if (r2 == 0) goto L_0x0048
            r2.destroy()
        L_0x0048:
            throw r0
        L_0x0049:
            r0 = move-exception
            r0 = r2
            goto L_0x003b
        */
        throw new UnsupportedOperationException("Method not decompiled: com.android.insecurebankv2.PostLogin.doesSUexist():boolean");
    }
    
    //실제 코드
    private boolean doesSUexist() {
        Process process = null;
        try {
            process = Runtime.getRuntime().exec(new String[] { "/system/bin/which", "su" });
            BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()));
            if (in.readLine() != null) return true;
            return false;
        } catch (Throwable t) {
            return false;
        } finally {
            if (process != null) process.destroy();
        }

    }

    private boolean doesSuperuserApkExist(String str) {
        return Boolean.valueOf(new File("/system/app/Superuser.apk").exists()).booleanValue();
    }
    
    /* access modifiers changed from: package-private */
    public void showRootStatus() {
        if (doesSuperuserApkExist("/system/app/Superuser.apk") || doesSUexist()) {
            this.root_status.setText("Rooted Device!!");
        } else {
            this.root_status.setText("Device not Rooted!!");
        }
    }

 

 

/system/app/에 가면 SuperUser.apk가 현재 nox에는 없다.

xbin과 bin에는 which와 su 명령어가 존재한다.

/system 디렉터리에 있는 파일은 only-read 상태로 마운트 되어 있어서 수정하려면 remount를 해야한다.

remount는 adb shell에서 su 명령어로 관리자 권한을 상승시키고 remount를 진행하면 된다.

-> 이후 우회하기 위해서는 SuperUser.apk가 있다면 apk이름을 바꾸거나 su 파일의 이름을 바꿔준다.

결과적으로 루팅 검증을 위한 su 명령어를 사용하지 못하고 Device not Rooted가 뜨는 것을 확인할 수 있다.

 

SuperSU.apk

SuperSU.apk는 루팅된 기기에서 루트 권한을 사용하는 앱들을 편리하게 관리할 수 있게 하고, 기존에 루팅되어 있던 디바이스를 정상 디바이스로 돌려주는 unrooting 기능을 제공한다.

 

Superuser.apk

로직은 다음과 같다.

디바이스의 OS 정보 확인 -> 적절한 경로 리턴 -> su 명령어로 실행할 서브 프로세스 생성 -> 실제 디바이스의 /system/xbin에 su 명령어 복사 & /system/bin에 심볼릭 링크 생성

 

대응방안
  • 루팅된 디바이스 탐지 -> 즉시 종료
  • 루팅 방지 소스 코드 로직이 노출되지 않도록 문자열 난독화를 적용해 어떤 명령어와 관련 파일을 검사하는지 모르게!
  • 루팅을 지속적으로 유지하는 것을 막기 위해 무결성 검증
  • 디컴파일 방지 솔루션 적용
Comments