프리다 활용 (1)

옥영진·2020년 9월 1일
0

Frida

목록 보기
8/11
post-custom-banner

사전 준비

루팅 탐지 우회하는 스크립트를 인젝션 해보기 위해 웹 보안기구인 OWSAP에서 배포하는 앱을 하나 다운로드 받아 설치할 것이다. 이 앱은 보안 공부를 위해 일부러 취약하게 만든 앱이다.

https://github.com/OWASP/owasp-mstg/blob/master/Crackmes/Android/Level_01/UnCrackable-Level1.apk

위 링크에서 apk 파일을 다운로드 받아 nox_adb install 명령어를 사용하여 녹스 앱 플레이어에 설치한다.

시작하기 전에 위 캡처화면처럼 녹스 앱 플레이어 설정에서 ROOT켜기를 체크하여 루팅된 기기 상태로 만든다.

그 후에 설치한 앱을 실행하면 루팅된 기기에서는 사용할 수 없다는 경고창이 뜨고, OK 버튼을 클릭하면 종료된다. 이제부터 이 루팅된 기기에서 사용할 수 없도록 루팅 탐지 로직을 우회하여 루팅된 기기에서도 실행할 수 있게 프리다로 스크립트를 인젝션 해보자.

먼저 앱 패키지 이름부터 알아보자. 아래 캡처화면을 보면 나오듯이 패키지 이름은 owasp.mstg.uncrackable1 이다.

루팅 탐지 우회

루팅 탐지 로직을 확인하기 위해 jadx 로 앱 파일을 디컴파일 할 것이다. 루팅 탐지 로직이 어디에 있는지 찾기 위해 가장 먼저 시도해 볼 만한 것은 위에서 앱을 실행했을 때 나왔던 에러 메세지를 검색해보는 것이다.

Root detected! 메세지를 검색해본 결과 MainActivity 클래스 내에서 사용되고 있었고, 조건문을 확인해 보니 c.a(), c.b(), c.c() 중 하나라도 참이면 a() 메소드에 루팅 탐지 메세지 문자열을 인자 값으로 전달하면서 호출하는 것을 알 수 있다. 이 a() 메소드의 로직을 확인해 보자.

AlertDialog 클래스를 통해 알림 대화창을 만들고, 여기에 OnClickListener 인스턴스를 생성하며 OK 버튼을 클릭했을 때 앱 프로세스가 종료되도록 구현되어 있음을 알 수 있다. 따라서, 이 onClick() 메소드를 재작성하여 앱 프로세스를 종료하는 로직 대신에 로그를 출력하도록 스크립트를 인젝션해보자. onClick() 을 재작성하기 위해 이 메소드를 구현하고 있는 클래스를 가지고 와야하는데, 이 메소드가 OnClickListener 생성자가 호출될 때 구현하고 있으므로 이 생성자가 속한 android.content.DialogInterface 라는 것을 알 수 있다.

// Uncrackable1.js
setImmediate(function() {
  Java.perform(function() {
    var exit_bypass = Java.use("android.content.DialogInterface.OnClickListener");
    exit_bypass.onClick.implementation = function(arg1, arg2) {
      console.log("[*] Exit Bypass");
    }
  })
})

스크립트 작성 후, 프리다를 통해 앱 프로세스에 스크립트를 인젝션 해야 하는데, 루팅 탐지의 경우 앱을 실행하자 마자 로직이 실행되므로, 인젝션 후에 앱 프로세스를 실행하기 위해 인젝션 후 Spawn 하는 방식을 사용할 것이다.

보면 에러가 나오는데, 이 android.content.DialogInterface.OnClickListener 의 경우 클래스가 아닌 인터페이스라서 클래스를 가져오는 프리다 API를 적용하지 못하는 것 같다.
=> 여기에 메소드 후킹하는 방법 나와있음.

따라서, onClick() 메소드를 재작성하는 방법 대신에 System.exit(0) 메소드를 재작성하여 앱 프로세스가 종료되지 않도록 해보자. 이 메소드 이름을 구글에 검색해보면 java.lang.System 클래스에 속하는 메소드임을 알 수 있다.

// Uncrackable1.js
setImmediate(function() {
  Java.perform(function() {
    var exit_bypass = Java.use("java.lang.System");
    exit_bypass.exit.implementation = function(arg) {
      console.log("[*] Exit Bypass");
    }
  })
})

스크립트 수정 후, 다시 프리다 명령어를 실행하면, 앱이 실행되고 똑같이 루팅 탐지 알림창이 표시되지만 이전과 달리 OK 버튼을 클릭하면 캡처 화면처럼 종료되지 않는다.

암호 복호화

위 루팅 탐지 우회에 사용했던 앱을 계속 사용하겠다. 성공적으로 루팅 탐지 우회에 성공했다면, Secret String 입력창에 아무 문자열을 입력하고 VERIFY 버튼을 클릭해보자.

test 라는 문자열 입력 후 VERIFY 버튼을 클릭해보니 위 캡처화면과 같은 알림창이 표시되었다. 루팅 탐지 우회 때 했던 방법처럼 알림창 메세지를 검색하여 어떤 로직이 구현되어 있는지 확인해보자.

Nope 이라는 메세지를 검색해보니 MainActivity 클래스 내에 verify() 메소드에 해당 로직이 구현되어 있음을 알 수 있다. 입력한 문자열은 obj 라는 변수 안에 저장되고, 이 값을 a.a() 메소드의 인자 값으로 전달했을 때 참을 리턴하면 성공 메세지를 출력하므로 이 a.a() 메소드의 내용을 확인해 보자.

일단 복잡해 보이는 로직은 무시하고, 가장 아래 리턴문을 확인해 보면 bArr 이라는 바이트 배열을 문자열로 변환하여 a() 메소드 호출할 때 인자로 전달받은 문자열(입력한 Secret String)과 비교하여 같으면 참을 반환한다. 즉, 위에 있는 try 문을 봤을 때 sg.vantagepoint.a.a.a() 메소드 호출하고 반환된 바이트 배열이 복호화된 Secret String 임을 알 수 있다. 이 메소드의 내용도 확인해 보자.

암호화에 대해 정확히는 모르지만 AES 라는 암호화 알고리즘을 통해 복호화한 값을 리턴하고 있음을 알 수 있다.

여기서 사용할 수 있는 방법은 두 가지 인데, 하나는 애초에 verify() 메소드에 있는 조건문인 a.a(obj) 가 무조건 참을 반환하도록 재작성하는 방법이고, 다른 하나는 복호화 된 bArr 바이트 배열에 있는 값을 알아내서 Secret String 에 정확한 정답을 입력하는 방법이 있다. 일단 전자의 방법을 사용해 보자.

// Uncrackable1.js
setImmediate(function() {
  Java.perform(function() {
  	// 루팅 탐지 우회 로직 생략
  
    var trueClass = Java.use("sg.vantagepoint.uncrackable1.a");
    trueClass.a.implementation = function(arg) {
      console.log("\n[*] Return True");
      return true;
    }
  })
})

이전처럼 test를 입력해보니 이번에는 Success 라는 성공 메세지가 표시되었다. 사실 이런 방법을 사용해서 해결하는 게 목적은 아니고, 복호화 하는 메소드인 sg.vantagepoint.a.a.a() 메소드를 후킹하여 복호화 된 바이트 배열 값을 알아내는 것이 이 문제의 목적이다.

// Uncrackable1.js
setImmediate(function() {
  Java.perform(function() {
  	// 루팅 탐지 우회 로직 생략
  
    var decrypteClass = Java.use("sg.vantagepoint.a.a");
    decrypteClass.a.implementation = function(arg1, arg2) {
      var secret_string = this.a(arg1, arg2);
      console.log(secret_string);
      return secret_string;
    }
  })
})

해당 메소드가 정의 된 sg.vantagepoint.a.a 클래스를 가지고 와서 a() 메소드를 재작성하는데, 메소드를 그대로 호출하여 반환 되는 복호화된 바이트 배열을 secret_string 변수에 저장한다. 그 후 그 값을 로그로 출력하고, 정상적으로 로직을 진행하기 위해 마지막에 반환까지 해주어야 한다. 한 번 이 스크립트를 프로세스에 인젝션 해보자.

실행 된 앱에서 Secret String 입력 값으로 아무 문자열을 입력 후 VERIFY 버튼을 클릭하면 위와 같은 [Object Object 라는 로그가 출력된다. 이는 호출한 a() 메소드의 반환 값이 객체(바이트 배열)임을 나타낸다. 바이트 배열의 내용을 하나씩 출력하도록 수정하자.

// Uncrackable1.js
setImmediate(function() {
  Java.perform(function() {
  	// 루팅 탐지 우회 로직 생략
  
    var decrypteClass = Java.use("sg.vantagepoint.a.a");
    decrypteClass.a.implementation = function(arg1, arg2) {
      var secret_string = this.a(arg1, arg2);
      for(var i=0;i<secret_string.length;i++) {
        console.log(secret_string[i]);
      }
      return secret_string;
    }
  })
})

이번에는 정수형 값들이 출력되고 있는데, 이는 아스키 코드 값이다. 따라서 이 아스키 코드 값에 해당하는 문자로 변환하도록 수정하자. 자바스크립트에서 아스키 코드 값을 문자로 변환해주는 함수는 String.fromCharCode() 이다.

// Uncrackable1.js
setImmediate(function() {
  Java.perform(function() {
  	// 루팅 탐지 우회 로직 생략
  
    var decrypteClass = Java.use("sg.vantagepoint.a.a");
    decrypteClass.a.implementation = function(arg1, arg2) {
      var secret_string = this.a(arg1, arg2);
      for(var i=0;i<secret_string.length;i++) {
        console.log(String.fromCharCode(secret_string[i]));
      }
      return secret_string;
    }
  })
})

출력된 로그를 확인해보니 복호화 된 문자열이 I want to believe 임을 알 수 있다. 한 번 이 값을 Secret String 값으로 입력해보자.

성공적으로 암호화 된 문자열을 찾았음을 알 수 있다.

profile
안녕하세요 함께 공부합시다
post-custom-banner

0개의 댓글