외로운 Nova의 작업실

insecurebankv2 - 루팅 탐지 우회 본문

Mobile App Penetesting/Android App Vulnerability

insecurebankv2 - 루팅 탐지 우회

Nova_ 2023. 5. 12. 22:18

- 취약점 소개

안드로이드 애플리케이션은 보안상의 이유로 루트 권한을 막아놓았습니다. 그래서 애플리케이션들은 각 프로그램마다 권한을 부여받아 독립적으로 동작합니다. 이러한 제한을 풀거나 우회하기 위해서는 시스템 권한을 루팅으로 획득해야합니다. 기기를 루팅하면 슈퍼유저의 권한으로 하드웨어 성능 조작, 기본 애플리케이션 삭제 등등을 할 수 있고, 디바이스 내부의 민감한 정보에 접그할 수도 있습니다. 따라서 금융권 애플리케이션들은 기본적으로 루팅된 기기에서의 앱 실행을 차단합니다. 또한, 최근 카드정보를 디바이스에 저장해 간단하게 결제하는 핀테크기술이 이슈가되면서 루팅된 기기는 보안 위협으로 감지하는 사례가 늘고 있습니다.

 

- 취약점 진단 과정

먼저 앱을 키고 로그인을 하게되면 사전 환경 구축에서 루팅을 해놓아서 Rooted Device라고 뜹니디.

이 문자열을 Device not Rooted!!로 변경해보겠습니다. 먼저, 루팅탐지를 어떻게 하는지 알아보기위해 PostLogin 소스를 가져오겠습니다.

package com.android.insecurebankv2;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Button;
import android.widget.TextView;
import java.io.File;

public class PostLogin extends Activity {
   Button changepasswd_button;
   TextView root_status;
   Button statement_button;
   Button transfer_button;
   String uname;

   private boolean doesSUexist() {
      // $FF: Couldn't be decompiled
   }

   private boolean doesSuperuserApkExist(String var1) {
      boolean var2 = true;
      if (!Boolean.valueOf((new File("/system/app/Superuser.apk")).exists())) {
         var2 = false;
      }

      return var2;
   }

   public void callPreferences() {
      this.startActivity(new Intent(this, FilePrefActivity.class));
   }

   protected void changePasswd() {
      Intent var1 = new Intent(this.getApplicationContext(), ChangePassword.class);
      var1.putExtra("uname", this.uname);
      this.startActivity(var1);
   }

   protected void onCreate(Bundle var1) {
      super.onCreate(var1);
      this.setContentView(2130968606);
      this.uname = this.getIntent().getStringExtra("uname");
      this.root_status = (TextView)this.findViewById(2131558528);
      this.showRootStatus();
      this.transfer_button = (Button)this.findViewById(2131558525);
      this.transfer_button.setOnClickListener(new 1(this));
      this.statement_button = (Button)this.findViewById(2131558526);
      this.statement_button.setOnClickListener(new 2(this));
      this.changepasswd_button = (Button)this.findViewById(2131558527);
      this.changepasswd_button.setOnClickListener(new 3(this));
   }

   public boolean onCreateOptionsMenu(Menu var1) {
      this.getMenuInflater().inflate(2131623938, var1);
      return true;
   }

   public boolean onOptionsItemSelected(MenuItem var1) {
      boolean var3 = true;
      int var2 = var1.getItemId();
      if (var2 == 2131558557) {
         this.callPreferences();
      } else if (var2 == 2131558558) {
         Intent var4 = new Intent(this.getBaseContext(), LoginActivity.class);
         var4.addFlags(67108864);
         this.startActivity(var4);
      } else {
         var3 = super.onOptionsItemSelected(var1);
      }

      return var3;
   }

   void showRootStatus() {
      boolean var1;
      if (!this.doesSuperuserApkExist("/system/app/Superuser.apk") && !this.doesSUexist()) {
         var1 = false;
      } else {
         var1 = true;
      }

      if (var1) {
         this.root_status.setText("Rooted Device!!");
      } else {
         this.root_status.setText("Device not Rooted!!");
      }

   }

   protected void viewStatment() {
      Intent var1 = new Intent(this.getApplicationContext(), ViewStatement.class);
      var1.putExtra("uname", this.uname);
      this.startActivity(var1);
   }
}

showRootStatus() 함수가 루팅탐지함수인걸로 보입니다. 관련 함수들도 한번 다 같이 보겠습니다.

private boolean doesSUexist() {
      // $FF: Couldn't be decompiled
   }

   private boolean doesSuperuserApkExist(String var1) {
      boolean var2 = true;
      if (!Boolean.valueOf((new File("/system/app/Superuser.apk")).exists())) {
         var2 = false;
      }

      return var2;
   }

void showRootStatus() {
      boolean var1;
      if (!this.doesSuperuserApkExist("/system/app/Superuser.apk") && !this.doesSUexist()) {
         var1 = false;
      } else {
         var1 = true;
      }

      if (var1) {
         this.root_status.setText("Rooted Device!!");
      } else {
         this.root_status.setText("Device not Rooted!!");
      }

   }

보게되면 /system/app/Superuser.apk 파일이 있는지 검사합니다. 만약 있으면 var2에 true가 저장되고 만약 없다면 false가 저장되게 됩니다. 또한, doesSUexist함수가 디컴파일이 되지않았으므로 smali 코드를 봐보겠습니다.

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 2
             aconst_null
             astore 4
             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 5
         }
         // end TCB0, end TCB1
         L1 {
             aload 5
             astore 3
             aload 5
             astore 4
         }
         // start TCB2, start TCB3
         L2 {
             new java/io/BufferedReader
             astore 7
         }
         // end TCB2, end TCB3
         L3 {
             aload 5
             astore 3
             aload 5
             astore 4
         }
         // start TCB4, start TCB5
         L4 {
             new java/io/InputStreamReader
             astore 6
         }
         // end TCB4, end TCB5
         L5 {
             aload 5
             astore 3
             aload 5
             astore 4
         }
         // start TCB6, start TCB7
         L6 {
             aload 6
             aload 5
             invokevirtual java/lang/Process.getInputStream()Ljava/io/InputStream;
             invokespecial java/io/InputStreamReader.<init>(Ljava/io/InputStream;)V
         }
         // end TCB6, end TCB7
         L7 {
             aload 5
             astore 3
             aload 5
             astore 4
         }
         // start TCB8, start TCB9
         L8 {
             aload 7
             aload 6
             invokespecial java/io/BufferedReader.<init>(Ljava/io/Reader;)V
         }
         // end TCB8, end TCB9
         L9 {
             aload 5
             astore 3
             aload 5
             astore 4
         }
         // start TCB10, start TCB11
         L10 {
             aload 7
             invokevirtual java/io/BufferedReader.readLine()Ljava/lang/String;
             astore 6
         }
         // end TCB10, end TCB11
         L11 {
             aload 6
             ifnull L13
             iload 2
             istore 1
             aload 5
             ifnull L12
             aload 5
             invokevirtual java/lang/Process.destroy()V
             iload 2
             istore 1
         }
         L12 {
             iload 1
             ireturn
         }
         L13 {
             aload 5
             ifnull L14
             aload 5
             invokevirtual java/lang/Process.destroy()V
         }
         L14 {
             iconst_0
             istore 1
             goto L12
         }
         // handle TCB0, handle TCB2, handle TCB4, handle TCB6, handle TCB8, handle TCB10
         L15 {
             astore 4
             aload 3
             ifnull L16
             aload 3
             invokevirtual java/lang/Process.destroy()V
         }
         L16 {
             iconst_0
             istore 1
             goto L12
         }
         // handle TCB1, handle TCB3, handle TCB5, handle TCB7, handle TCB9, handle TCB11
         L17 {
             astore 3
             aload 4
             ifnull L18
             aload 4
             invokevirtual java/lang/Process.destroy()V
         }
         L18 {
             aload 3
             athrow
         }
     }

위 코드중 중요한 부분으로 보겠습니다.

 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;

/system/xbin/which 명령어로 su 명령어를 찾는 걸 exec()함수로 호출하는 걸로 해석 할 수 있습니다. 즉, insecurebankv2는 /system/app/Superuser.apk파일이 있다면 루팅된걸로 감지하고 su명령어가 있다면 루팅된걸로 감지함을 알수 있습니다. 이제 이것을 없애보도록 하겠습니다.

 

<루팅 우회>

/system/app/superuser.apk 파일이 있는지 확인해봅시다. 

cd /system/app
ls -al

nox앱플레이어의 경우에는 없는 걸로 확인됩니다. 그럼 su 명령어가 있는지 봐보겠습니다.

cd /system/xbin
ls -al

su 명령어가 있음을 알 수 있습니다. 이를 sx이름으로 변경해보겠습니다. 이때 권한이 필요하므로 마운트를 다시해줍니다.

mount -o remount,rw /system

이후 이름도 변경해줍니다.

mv ./su ./sx

잘 변경이 되었습니다. 다시 앱으로 돌아가서 다시 껏다가 켜보겠습니다.

루팅 우회가 되었습니다.

 

- 취약점 대응 방안

1. 문자열 난독화, 무결성 검증, 디컴파일 방지 솔루션을 적용하여루팅 탐지 소스 코드로직이 노출되지않게 해야합니다. 

2. 만약 루팅이 감지되었다면 사용하지 못하도록 즉시 종료시켜야합니다. 

 

- 루팅 탐지할때 확인해보는 것들

금융앱이나 게임 앱에서는 다음과 같은 네가지 경로를 주로 체크합니다.

/system/bin/su
/system/xbin/su
/system/app/Superuser.apk
/data/data/com.noshufou.android.su
Comments