Android/Uncrackable

Uncrackable 2

앱 정보

앱을 실행시키면 이전 Uncrackable 1과 똑같이 루팅 감지를 하고 OK 버튼을 누르면 종료되는 것을 확인할 수 있다.

[그림 1] 앱 실행 직후 루팅 탐지

 

루팅 감지는 이전 단계와 똑같이 우회할 수 있으니 우회를 한 뒤 확인해보면 사용자 입력을 받을 수 있는 공간 하나와 VERFIY 버튼 하나로 구성되어 있음을 알 수 있다. 또한 이전과 똑같이 무작위 데이터 입력 후 버튼을 눌러보면 "Nope..."이라는 창을 확인할 수 있다.

[그림 2] 루팅 탐지 우회 이후 문자열 검증

 

앱 분석

verify()

public void verify(View view) {
        String str;
        String obj = ((EditText) findViewById(R.id.edit_text)).getText().toString();
        AlertDialog create = new AlertDialog.Builder(this).create();
        if (this.m.a(obj)) {
            create.setTitle("Success!");
            str = "This is the correct secret.";
        } else {
            create.setTitle("Nope...");
            str = "That's not it. Try again.";
        }
        create.setMessage(str);
        create.setButton(-3, "OK", new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialogInterface, int i) {
                dialogInterface.dismiss();
            }
        });
        create.show();
    }

verify() 함수를 분석하면 다음과 같은 정보를 알 수 있다.

  • 사용자가 입력한 값을 obj 변수에 저장한 후 CodeCheck 클래스 a() 함수에 인자로 넘긴다.
  • 만약 a() 함수의 반환 값이 true라면 "Success!"라는 타이틀을 설정하고 str 변수에 "This is the correct secret."를 저장한다. 그렇지 않다면 "Nope..."라는 타이틀로 설정한 후 str 변수에 "That's not it. Try again."를 저장한다.
  • 이전에 설정한 타이틀과 문자열로 경고창을 띄운다.

 

CodeCheck

public class CodeCheck {
    private native boolean bar(byte[] bArr);

    public boolean a(String str) {
        return bar(str.getBytes());
    }
}

CodeCheck 클래스를 분석하면 다음과 같은 정보를 알 수 있다.

  • a() 함수에서 인자로 넘어온 값을 바이트 형식으로 변환하여 native 함수인 bar() 함수에 인자로 넘겨준 후 해당 함수의 반환 값을 그대로 반환한다.

 

최종적으로 인증을 통과하기 위해서 native 함수를 분석해야 한다는 것을 알 수 있으므로 ida를 통해 라이브러리 파일을 분석한다.

실제 \lib\arm64-v8a\libfoo.so 파일을 확인해보면 다음과 같은 함수를 확인할 수 있다.

bool __fastcall Java_sg_vantagepoint_uncrackable2_CodeCheck_bar(__int64 a1, __int64 a2, __int64 a3)
{
  _BOOL8 result; // x0
  const char *v6; // x21
  char v7[24]; // [xsp+0h] [xbp-40h] BYREF
  __int64 v8; // [xsp+18h] [xbp-28h]

  result = 0LL;
  v8 = *(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
  if ( byte_1300C == 1 )
  {
    strcpy(v7, "Thanks for all the fish");
    v6 = (*(*a1 + 1472LL))(a1, a3, 0LL);
    result = (*(*a1 + 1368LL))(a1, a3) == 23 && !strncmp(v6, v7, 0x17u);
  }
  return result;
}

bar() 함수를 확인해보면 다음과 같은 정보를 알 수 있다.

  • v7 변수에 "Thanks for all the fish" 문자열을 strcpy() 함수를 통해 저장한다.
  • 변수 a1, a3를 사용하여 무언가 동작 후 v6 변수에 이를 저장한다.
  • 마지막으로 v6과 v7이 같으며, 변수 a1, a3와 어떤 동작을 한 반환 값이 23(아마 사용자 입력 값 길이가 23인지 확인하는 부분으로 예측)이면 result 변수에 1을 저장하여 이를 반환한다.

 

Exploit

1. Hard Coding

네이티브 함수인 bar() 내부 strcpy() 함수 인자 값에 하드 코딩되어 저장된 문자열을 사용하여 인증한다.

[그림 3] 하드 코딩을 이용한 인증 성공

 

2. Hooking

후킹을 통한 우회는 bar() 함수를 후킹 하여 리턴 값을 true로 고정하여 인증하는 방법이다.

Exploit Code

console.log("Script loaded successfully");
Java.perform(function x() {
    console.log("java perform function");
    var sys = Java.use('java.lang.System');
    sys.exit.implementation = function () {
        console.log("success");
    }
});

Interceptor.attach(
    Module.findExportByName("libfoo.so", "Java_sg_vantagepoint_uncrackable2_CodeCheck_bar"), {
        onLeave: function(retl) {
            retl.replace(1);
        }
    }
);

코드를 확인해보면 다음과 같다.

  • 위 코드는 이전 Uncrackable1에서 사용한 루팅 탐지 코드와 같다.
  • 이후 Interceptor.attach()를 통해 네이티브 함수를 후킹 하는데 이때 라이브러리와 함수명을 사용하여 해당 함수를 찾을 수 있도록 한다.
  • 마지막으로 onLeave로 종료될 때 리턴 값으로 1을 반환하도록 하면 우회를 할 수 있다.

실제 스크립트를 실행한 뒤 무작위 값을 입력 후 VERIFY 버튼을 누르면 다음과 같이 Success!라는 창을 확인할 수 있다.

[그림 4] 후킹을 이용한 인증 우회

 

Reference

'Android > Uncrackable' 카테고리의 다른 글

Uncrackable 1 - Secret String  (0) 2021.11.01
Uncrackable 1 - Root Detection  (0) 2021.11.01