본문 바로가기

Android

[OWASP-MSTG] UnCrackable-Level1.apk

반응형

모바일 앱 보안을 강화하려면 어떻게 해야 할까요
공격자의 입장에서 안드로이드 앱이 얼마나 취약한지 확인해보기 위해 CrackMe를 시도해보았습니다

시작하기에 앞서, 글쓴이의 배경 지식은 다음과 같습니다:

배경 지식이 기초적인 수준이기 때문에 내용에 오류가 있을 수 있습니다

CrackMe 프로그램은 리버스 엔지니어링 실력을 시험해 볼 수 있는 프로그램으로, 보통 다양한 탐지 및 디버깅 방지 기법을 우회하여 키 값을 찾아내는 것을 목적으로 합니다

OWASP(The Open Web Application Security Project)의 MASTG(Mobile Application Security Testing Guide)에서 제공하는 CrackMe를 풀어보려 합니다
https://github.com/OWASP/owasp-mastg

총 다섯 개의 안드로이드 CrackMe 앱을 제공하고 있습니다
https://mas.owasp.org/crackmes/

UnCrackable-Level1을 에뮬레이터에 설치하고 실행해보니, 루팅된 디바이스로 분류되어 앱이 종료됩니다

먼저 루팅 탐지부터 우회해야 하겠네요 jadx를 사용하여 정적 분석을 시도합니다
jadx 기본 설정의 경우 너무 긴 함수의 경우 디컴파일하지 않지만, --show-bad-code 플래그를 붙이는 경우 부정확하게나마 보여줍니다
함수를 길게 작성하는 것도 도움이 되려나요

jadx-gui UnCrackable-Level1.apk --show-bad-code

네비게이션 > 클래스 검색(Cmd-N) 메뉴에서 Activity를 검색합니다
간단한 앱의 경우 소스코드 트리를 뒤져봐도 좋지만 복잡도가 높은 앱의 경우 검색을 하는 편이 원하는 결과를 빠르게 얻을 수 있습니다

현재 앱에는 MainActivity 밖에 없네요
코드를 확인해봅니다

onCreate()를 보면 다양한 함수로 루팅 여부를 탐지하고 이후 디버깅 가능 여부를 탐지하고 있네요
루팅 체크 부분을 자세히 확인해보겠습니다

a() 메소드는 환경변수에 su 파일이 있는지 확인하는 메소드입니다
b() 메소드는 os의 빌드 태그에 "test-keys"가 포함되어 있는지 확인하는 메소드이며,
c() 메소드는 시스템에 루팅과 관련된 파일이 존재하는지 확인하는 메소드네요
파일 체크의 경우 해당하는 파일의 이름만 변경해도 무력화되니, 의미있는 방식인지는 잘 모르겠습니다

루팅 탐지를 우회하는 frida 스크립트를 작성합니다

// unCrackable1.js
Java.perform(() => {
  const c = Java.use("sg.vantagepoint.a.c");
  c["a"].implementation = function () {
    console.log(`c.a(): ${this.a()}`);
    return false;
  };

  c["b"].implementation = function () {
    console.log(`c.b(): ${this.b()}`);
    return false;
  };

  c["c"].implementation = function () {
    console.log(`c.c(): ${this.c()}`);
    return false;
  };
});

스크립트를 실행합니다

frida-ps -Uai # 설치된 앱 Identifier 확인
frida -U -f owasp.mstg.uncrackable1 -l unCrackable1.js # USB 연결된 디바이스에서 UnCrackable 1 앱 실행 및 스크립트 로딩

앱이 종료되지 않습니다

이상하게도 디버깅 가능 여부는 탐지되지 않네요
코드를 확인해보니 ApplicationInfo의 플래그에 FLAG_DEBUGGABLE이 포함되어 있는지 확인하는 코드였습니다

 

EditText에 텍스트를 입력하고 VERIFY를 눌러보면 verify() 메소드로 입력 값이 키 값과 동일한지 체크하는 모습을 볼 수 있습니다
우리가 관심있는 부분은 정확한 키 값이니 검증 과정을 우회하는 건 의미가 없겠네요

 

검증 함수(a.a(obj))를 따라가봅니다

b("8d127684cbc37c17616d806cf50473cc")를 시크릿 키로 Base64.decode("5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", 0) AES 복호화를 적용하네요

복호화 후 결과 값을 String으로 출력하면 키 값을 알 수 있겠죠?
이전에 작성한 루팅 탐지 우회 스크립트를 수정해봅니다

Java.perform(function () { 
  passRootDetection(); 
  logVerify(); 
});

function passRootDetection() {
  const c = Java.use("sg.vantagepoint.a.c");
  c["a"].implementation = function () {
    return false;
  };
  c["b"].implementation = function () {
    return false;
  };
  c["c"].implementation = function () {
    return false;
  };    
}

function logVerify() {
  const stringClass = Java.use("java.lang.String");
  const a = Java.use("sg.vantagepoint.a.a");
  a["a"].implementation = function (bArr, bArr2) {
    const result = this["a"](bArr, bArr2);
    console.log(`a.a result: ${stringClass.$new(result)}`);
    return result;
  };
}
[Android Emulator 5554::owasp.mstg.uncrackable1 ]-> a.a result: I want to believe

키 값은 I want to believe였네요
동일한 값을 입력 후 VERIFY 버튼을 누르니 성공 팝업이 표시됩니다

공격자의 입장을 체험해보고 나니 jvm 환경에서 작성된 그 어떤 것도 믿을 수 없을 것 같습니다..
너무 쉽게 분석되고 우회가 가능하네요..
난독화, 패킹 등의 방식을 더 알아보아야 하겠습니다

반응형