외로운 Nova의 작업실
insecurebankv2 - 루팅 탐지 우회 본문
- 취약점 소개
안드로이드 애플리케이션은 보안상의 이유로 루트 권한을 막아놓았습니다. 그래서 애플리케이션들은 각 프로그램마다 권한을 부여받아 독립적으로 동작합니다. 이러한 제한을 풀거나 우회하기 위해서는 시스템 권한을 루팅으로 획득해야합니다. 기기를 루팅하면 슈퍼유저의 권한으로 하드웨어 성능 조작, 기본 애플리케이션 삭제 등등을 할 수 있고, 디바이스 내부의 민감한 정보에 접그할 수도 있습니다. 따라서 금융권 애플리케이션들은 기본적으로 루팅된 기기에서의 앱 실행을 차단합니다. 또한, 최근 카드정보를 디바이스에 저장해 간단하게 결제하는 핀테크기술이 이슈가되면서 루팅된 기기는 보안 위협으로 감지하는 사례가 늘고 있습니다.
- 취약점 진단 과정
먼저 앱을 키고 로그인을 하게되면 사전 환경 구축에서 루팅을 해놓아서 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
'Mobile App Penetesting > Android App Vulnerability' 카테고리의 다른 글
insecurebankv2 - 취약한 암호화 실행 (0) | 2023.05.15 |
---|---|
insecurebankv2 - 안전하지 않은 콘텐츠 프로바이더 접근 (0) | 2023.05.13 |
insecurebankv2 - 액티비티 컴포넌트 취약점 (0) | 2023.05.11 |
insecurebankv2 - 로컬 암호화 이슈 취약점 (0) | 2023.05.08 |
insecurebankv2 - 취약한 인증 메커니즘 취약점 (0) | 2023.05.07 |