LINE Corporation이 2023년 10월 1일부로 LY Corporation이 되었습니다. LY Corporation의 새로운 기술 블로그를 소개합니다. LY Corporation Tech Blog

Blog


AIR GO를 소개합니다

AIR GO 소개

안녕하세요. LINE에서 AIR GO를 개발하고 있는 김태우입니다. 이번 블로그에서는 AIR GO에 대해 말씀드리겠습니다.

AIR GO는 Android와 iOS의 패키지 파일(APK 파일과 IPA 파일)을 스캔하여 취약점을 찾아주는 서비스입니다. 오프소스 툴인 SandDroid를 사용해보신 분이라면 어떤 서비스인지 쉽게 떠올릴 수 있을 것 같습니다. 최근 LINE에서는 앱을 배포하기 전에 AIR GO를 활용하여 취약점을 발견하고 통지함으로써 앱에 포함된 위험을 미리 제거하고 있습니다. 또한 AIR GO는 누구나 간단한 이메일 계정 등록만으로 사용 가능한 서비스입니다. 보안 서비스 'AIR'는 이미 한 번 소개된 적이 있는데요. 이번에는 엔지니어의 관점에서 AIR를 구성하는 도구 중 하나인 AIR GO에 대해서 소개드리려고 합니다.

AIR GO 진단 서비스는 아래 두 가지 방식으로 이용할 수 있습니다.

  • APK, IPA 파일 입력 방식: 패키징된 APK, IPA 파일을 입력하면 압축 해제 후 필요한 정보를 추출하여 보안 취약점, 난독화 적용 여부, 사용된 오픈소스 라이선스, 악성코드 등을 진단 및 리포팅
  • URL 입력 방식: 입력된 웹사이트의 악성여부 리포팅

이번 글에서는 APK 파일을 입력받았을 때의 진단과정과 AIR GO 서비스 이용방법, AIR GO 진단 결과 확인하는 방법에 대해 설명 드리고자 합니다.

AIR GO APK 파일 진단 과정

APK파일의 취약점 진단은 크게 아래와 같은 순서로 진행됩니다.

위 과정을 거치면 AIR GO화면에서 진단 결과를 확인할 수 있습니다.

코드 디컴파일 후 파싱

코드를 디컴파일하고, 디컴파일된 코드를 다시 파싱하는 과정은 빌드된 바이너리를 읽기 쉬운 형태로 디컴파일하고 취약점 진단에 필요한 데이터를 추출, 사용할 수 있는 형태의 정보로 변환하는 과정입니다. Android 앱 파일(APK파일)의 압축을 풀면 다양한 포맷의 파일이 존재하는데요. AIR GO에서는 DEX, SO, DLL, XML 등의 파일에서 진단에 필요한 주요 정보를 추출합니다. 이 가운데 classes.dex 파일은 Dalvik VM에서 동작하는, 사람이 이해하기 힘든 바이트코드를 포함하는데요. 이를 사람이 이해할 수 있는 smali 코드 형태로 변환하는 게 가능합니다. 이 과정을 디스어셈블리(bytecode에서 instruction list로 변환)라고 하고, AIR GO에선 이 과정의 결과물인 smali 코드를 기반으로 취약점을 분석합니다.

smali 코드의 이해를 돕기 위해 간단한 샘플 코드를 보겠습니다. 아래는 Android Toast를 이용하여 "Toast Hello"라는 간단한 메세지를 화면에 출력하는 코드입니다.

  • Java 코드
String message = "Toast Hello";
Toast toast = Toast.makeText(this, message, Toast.LENGTH_LONG);
toast.show();
  • smali 코드
const-string v2, "Toast Hello"
const/4 v3, 0x1
invoke-static {p0, v2, v3}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
move-result-object v1
invoke-virtual {v1}, Landroid/widget/Toast;->show()V

위 코드에서 Toast 클래스의 show 메소드 호출 부분을 보면, smali 코드가 어떤 형태로 메소드를 호출하는지 알 수 있습니다. 좀 더 명확한 설명을 위해 smali 코드에서 show 메소드를 호출하고 있는 마지막 줄만 다시 보겠습니다.

airgo_smali

제가 따로 확대한 위 코드를 보시면 invoke-virtual 명령어(opcode)를 사용하여 virtual 메소드인 show 메소드를 호출하고 있는데요. 코드 앞부분에서 makeText 메소드를 통해 만든 Toast 클래스의 인스턴스인 v1 레지스터를 이용하여 show 메소드를 호출하고 있는 것을 알 수 있습니다. 위 코드를 통해 show 메소드가 Landroid/widget/Toast 클래스에 존재한다는 것과, 인자를 필요로 하지 않고, V 타입인 void를 리턴한다는 것을 알 수 있습니다.

취약점과 패턴 비교

앞에서 보신 것처럼, smali 코드에서 메소드를 호출하려면 클래스 경로와 메소드 이름, 인자, 리턴 타입 등을 나열해야 합니다. AIR GO에서는 이 부분을 이용해 취약한 메소드의 호출 명령어를 패턴화하여 저장하고, 입력된 APK 파일을 저장된 패턴과 비교하여 진단합니다. AIR GO에서는 Google의 App security improvement programCVE같은 경로를 통해 새로운 취약점을 확인하고 검증하여 모바일 앱 진단에 사용할 취약점 패턴으로 등록하고 있습니다.

AIR GO에 등록되어 있는 'Insecure Hostname Verification' 취약점을 예시로 자세하게 설명드리겠습니다.

'Insecure Hostname Verification' 취약점

대부분의 앱들은 클라이언트 만으로 동작하지 않고 서버와 통신을 합니다. 통신하면서 데이터를 주고 받고 정보도 업데이트합니다. 이러한 서버-클라이언트 통신 과정 중에 노출되는 여러가지 취약점들이 있습니다. 그 중에서 'Insecure Hostname Verification'이라는 취약점은 코드에서 서버 주소를 제대로 검증하지 않아서, 중간자 공격(Man-in-the-middle attack)에 의해 의도하지 않은 서버 주소로 접속이 가능하고, 공격의 대상이 될 수 있다는 내용의 취약점입니다. 대부분의 앱에서 클라이언트는 개발자가 의도한, 제한된 호스트 주소로만 접속하게 됩니다. 이 과정에서 최소한 호스트 주소만 체크해도 의도하지 않는 서버로 접속하는 것은 차단할 수 있습니다.

'Insecure Hostname Verification' 취약점이 내재된 예시

Java에서 서버-클라이언트 간에 통신할 땐 HttpsURLConnection 클래스의 setHostnameVerifier 메소드 인자로 HostnameVerifier 클래스를 구현한 클래스의 인스턴스가 입력됩니다. HostnameVerifier를 구현하기 위해서는 추상 메소드인 verify를 오버라이드(override)해야 하는데요. 개발자는 이 메소드를 오버라이드하면서 호스트 주소를 검증할 수 있습니다. 검증한 뒤 의도한 호스트 주소라면 true, 의도하지 않은 서버 주소라면 false를 리턴하면 됩니다. 그러나 적절한 검증없이 무조건 true를 리턴한다면, 의도하지 않은 호스트로도 접속할 수 있게 됩니다. 이렇게 되면 서버-클라이언트 접속 과정에서 공격자가 의도한 중간자 공격(Man-in-the-middle attack)의 대상이 될 수 있습니다. 다음은 호스트 주소를 검증하지 않은 Java 코드 예시입니다.

  • 안전하지 않은 코드(Java)
URL url = new URL("https://example.org/");
HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection();
// Set Hostname verification
urlConnection.setHostnameVerifier(new HostnameVerifier() {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        // Ignore host name verification. It always returns true.
        return true;
    }
});

위 코드의 verify메소드는 hostname 인자로 넘어온 호스트명을 받아서 원래 의도한 호스트명인지 검증하지 않고, 무조건 true를 리턴해버리기 때문에 취약한 코드라 할 수 있습니다.

AIR GO에서 진단 시 사용하는 smali 코드 형태로 위 Java 코드의 verify 메소드를 변환하면 아래 코드와 같습니다.

  • 안전하지 않은 코드 (smali)
# virtual methods
.method public verify(Ljava/lang/String;Ljavax/net/ssl/SSLSession;)Z
    .locals 1
    .param p1, "hostname"    # Ljava/lang/String;
    .param p2, "session"    # Ljavax/net/ssl/SSLSession;

    .prologue
    .line 62
    const/4 v0, 0x1

    return v0
.end method

위 코드를 보시면 p1, p2 값으로 인자를 전달받고 있습니다. p1 값이 hostname, p2 값이 session인 것을 알 수 있는데요. 현재 p1 값으로 호스트명을 검증하지 않은 채, v0 레지스터에 true를 의미하는 0x1 값을 저장하고, v0 레지스터를 그대로 리턴하고 있습니다. 위와 같은 코드가 존재하면 AIR GO에서 취약한 코드로 탐지합니다.

'Insecure Hostname Verification' 취약점이 해결된 예시

구글에서는 아래와 같이 적절한 검증을 구현할 것을 권장하고 있습니다.

  • 적절한 검증 코드 (Java)
URL url = new URL("https://example.org/");
HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection();
// Set Hostname verification
urlConnection.setHostnameVerifier(new HostnameVerifier() {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        HostnameVerifier hv =
            HttpsURLConnection.getDefaultHostnameVerifier();
        return hv.verify("example.com", session);
    }
});

(출처: https://developer.android.com/training/articles/security-ssl.html)

  • 적절한 검증 코드 (smali)
# virtual methods
.method public verify(Ljava/lang/String;Ljavax/net/ssl/SSLSession;)Z
    .locals 2
    .param p1, "hostname"    # Ljava/lang/String;
    .param p2, "session"    # Ljavax/net/ssl/SSLSession;
    .prologue
    .line 21
    invoke-static {}, Ljavax/net/ssl/HttpsURLConnection;->getDefaultHostnameVerifier()Ljavax/net/ssl/HostnameVerifier;
    move-result-object v0

    .local v0, "hv":Ljavax/net/ssl/HostnameVerifier;
    const-string/jumbo v1, "example.com"

    invoke-interface {v0, v1, p2}, Ljavax/net/ssl/HostnameVerifier;->verify(Ljava/lang/String;Ljavax/net/ssl/SSLSession;)Z
    move-result v1

    return v1
.end method

위 코드에서는 검증없이 바로 true를 리턴하지 않고, 호스트명 검증 절차를 거쳐 적절한 값이 리턴되도록 구현해 놨습니다. AIR GO에서는 setHostnameVerifier() 메소드 호출 시 인자로 입력되는 클래스의 verify 메소드가 적절한 검증없이 return true로만 구현된 메소드인지 확인하여 진단합니다.

AIR GO 사용방법

계정 등록 및 분석 요청

AIR GO는 이메일 인증을 통해 간단하게 계정 등록하고 사용할 수 있습니다. 가입 후 로그인하면 아래와 같은 화면이 나타납니다.

airgo_main

위 화면에서 APK 파일 혹은 IPA을 입력하거나 URL을 입력하여 진단을 시작할 수 있습니다. 입력 파일의 크기는 최대 512MB까지 가능합니다. 입력된 파일은 서버로 전송되어 분석되는데요. 전송 후 수 분 이내에 파싱, 스캔, 리포팅 과정을 거친 결과를 화면에서 확인할 수 있습니다. 결과 이력은 서버에 저장되어 추후에 다시 참고할 수 있습니다.

진단 결과 확인

진단 결과에 대한 설명은 아래 Android 앱을 진단한 결과 화면을 예시로 말씀드리겠습니다.

airgo_report

진단 등급

AIR GO에서는 진단 결과를 심각도 순으로 CRITICAL, WARNING, NORMAL, SAFE의 4개 등급으로 구분하고 있습니다.

  • CRITICAL: 아래에 해당하는 취약점이 하나 이상 존재하거나, 악성코드가 발견된 경우
  • WARNING: 아래에 해당하는 취약점이 하나 이상 존재하는 경우
    • OpenSSL 중간 등급(Moderate severity)의 취약점
    • CVSS v3.0 Ratings에서 4.0-6.9의 취약점
    • 구글 플레이 리젝 사유 리스트 중 수정 기한이 정해지지 않은 취약점
  • NORMAL: 아래에 해당하는 취약점이 하나 이상 존재하는 경우
    • OpenSSL 낮은 등급(Low severity)의 취약점
    • CVSS v3.0 Ratings에서 0-3.9의 취약점
    • 기타 권고 사항
  • SAFE : 위 등급에 해당하는 취약점이 하나도 발견되지 않은 경우

진단 상세 결과

탐지 결과에 대한 상세 정보는 요약, 난독화, 취약점, 오픈소스 라이선스, 악성코드, 인증서, 패키지 구조 등으로 분류되어 있는 탭을 클릭하여 확인할 수 있습니다.

  • 요약(summary) 탭: 전체 탐지 결과를 등급별로 분류한 카운트 정보 표시
  • 난독화(obfuscation) 탭: 난독화 적용 여부와 주요 파일 여부에 따라 세 가지 색상으로 구분하여 패키지 내 각 파일 별로 표시
    • 빨간색: 난독화 미적용된 주요 파일
    • 주황색: 난독화 미적용된 일반 파일
    • 초록색: 난독화 적용된 파일
  • 취약점(vulnerability) 탭: 취약점의 위험도에 따라 등급 표시. 발견된 취약점에 대한 설명과 수정 가이드 제공. 취약점이 발견된 상세 위치 표시
  • 오픈소스 라이선스(license) 탭: 앱에서 사용된 외부 라이브러리와 라이브러리가 가지는 라이선스 정보 나열 및 해당 라이선스의 소스코드 공개 의무(reciprocal)여부에 따라 '반환 의무', '반환 의무 불필요' 구분 표시
  • 악성코드(malware) 탭: 악성코드가 발견된 위치와 탐지에 사용된 정보 표시
  • 인증서(certification) 탭: APK 파일의 디지털 서명에 사용된 인증서 정보-발행자와 해쉬값 정보 표시
  • 구조(structure) 탭: APK, IPA 파일의 패키지 디렉토리 구조와 간단한 파일 내용 표시

AIR GO 활용사례 - LINE MUSIC Android 앱

이미 아시는 분들도 계시겠지만, LINE은 자사앱에서 유저의 업데이트가 필요한 취약점이 발견되면 유저가 최신 버전으로 업데이트하도록 jvn자사 홈페이지에 공표하고 있습니다. 지난 7월에는 LINE MUSIC Android 앱에서 'SSL 서버 증명서 미검증'이 취약점으로 공표된 적 있습니다.

'SSL 서버 증명서 미검증' 취약점

LINE MUSIC Android 앱에서 발견된 'SSL 서버 증명서 미검증(https 인증서 검증)' 취약점은, 원격 호스트에 https 연결 시도할 때 발생하는 에러를 무시하거나, 잘못된 SSL 인증서 검증 구현으로 외부 중간자 공격(Man-in-the-middle attack)에 노출될 수 있을 때 탐지됩니다. 이런 경우 공격자가 전송된 데이터를 읽는 것은 물론 변경하는 것도 가능합니다.

이런 취약점을 방지하기 위해서는 자바에서 https 통신을 구현 할 때 SSLContext 클래스를 이용하여 안전한 소켓 프로토콜을 구현해야 합니다. SSLContextinit 메소드를 호출할 때 두 번째 인자로 TrustManager를 구현한 클래스의 배열을 입력하게 되는데요. 이 때 TrustManagercheckServerTrusted 메소드를 오버라이드하여 직접 서버의 인증서 검증 코드를 구현할 수 있습니다.

여기에서 SSL 인증서 검증을 하지 않거나, 올바른 인증서가 아님에도 예외 처리하지 않고 무시하는 경우 AIR GO에서는 취약점이라고 판단합니다. 앱을 개발하는 과정에서 개발 환경, 마감 시간의 이유로 테스트 편의를 위해 인증서 검증 결과를 일시적으로 무시할 수는 있으나, 배포하기 전에 반드시 취약한 코드를 제거하고 올바른 SSL 인증서 검증 절차를 넣어야 합니다.

AIR GO 탐지 상세 내용

아래 화면에서 과거 AIR GO에서 탐지되었던 LINE MUSIC의 'TrustManager Verification' 취약점에 대한 내용을 볼 수 있습니다. detect_count 값이 2인 것으로 보아 두 곳에서 탐지되었으며, 그중 한 곳의 경로를 확인할 수 있습니다. GoogleHttpClient 클래스(https 연결을 위해 MUSIC팀에서 만든 클래스)의 a 메소드에서 SSLContext.init 메소드를 호출하고 있는 것을 알 수 있는데요. 메소드의 두 번째 인자인, GoogleHttpClient의 내부 클래스 GoogleHttpClient$a에서 구현된 checkServerTrusted 메소드에서 취약점을 발견했다는 내용입니다. 실제 checkServerTrusted 메소드 확인 결과 인증서 확인을 하지 않았습니다.

airgo_detection

이 취약점은 AIR GO에서 쉽게 탐지할 수 있는 부분이기 때문에, 재발 방지를 위해 개발자가 앱을 빌드한 뒤 반드시 AIR GO를 사용해서 취약점이 없는지를 체크하도록 가이드하였습니다.

맺음말

AIR GO에서는 취약한 라이브러리 사용, 안전하지 않은 https 인증서 검증, 의도하지 않은 호스트 접속 확인, 안전하지 않은 암호화 알고리즘 사용, Android Activity, Receiver, Provider의 export 값 등을 진단하여 앱에 내재된 취약한 코드를 알려줍니다. 또한, URL 스캔을 통해 위험한 웹사이트도 알려 줍니다.

이제까지 AIR GO의 동작 원리와 간단한 사용방법을 알려 드렸습니다. AIR GO의 큰 특징들을 정리해 보면 아래와 같습니다.

  • 빌드된 바이너리를 입력으로 받습니다. 기업 입장에서 제품의 소스코드 유출은 금전적 피해와 연결될 수 있습니다. 그렇기 때문에 소스코드를 제3자에게 전달하는 것은 부담될 수 밖에 없는데요. AIR GO는 바이너리 파일을 입력으로 받기 때문에 진단 대상의 소스코드 자체를 전달할 필요가 없습니다. 따라서 코드 전달 시 발생할 수 있는 코드 유출에 대한 개발자의 부담을 덜 수 있습니다.
  • DEX, DLL, SO 등 파일 별로 난독화 적용여부를 진단합니다. 난독화는 코드의 흐름을 바꾸거나 코드를 읽기 어려운 형태로 바꿔 앱 분석을 어렵게 하는 방식으로 코드를 보호하는 방법입니다. 앱에 난독화가 적용된 것과 그렇지 않은 것은 분석 난이도에 큰 차이가 있는데요. 아직 난독화를 적용하지 않았다면 AIR ARMOR를 적용할 수 있도록 가이드도 하고 있습니다.
  • 배포 전 구글 플레이 보안 기준을 미리 파악할 수 있습니다. 구글 플레이에서는 배포되는 앱에 대한 보안 기준을 정해 두고 이 기준을 만족하지 못한 앱은 배포를 거절합니다. 사전에 구글 플레이에서 위험하다고 판단한 코드를 확인하여 배포 전에 미리 대처할 수 있습니다.

이 외에도 사용한 오픈소스들의 라이선스 점검, URL 스캔, IPA 파일에 대한 점검을 지원하는데요. 이번 기회에 AIR GO를 사용하여 배포 전 앱 보안 상태를 점검해보는 건 어떨까요?