$30 off During Our Annual Pro Sale. View Details »

Obfuscation 101 @ Naver Tech Concert

Obfuscation 101 @ Naver Tech Concert

네이버 테크 콘서트에서 발표한 바이트코드, 난독화 관련 발표입니다.

Leonardo YongUk Kim

November 01, 2018
Tweet

More Decks by Leonardo YongUk Kim

Other Decks in Programming

Transcript

  1. Obfuscation 101 KakaoBank | Leonardo YongUk Kim ZFMMPX'"&3(# CMBDL&&&3(# ஠஠য়ߛ௼জই੉௑@ചݶ

    ୭ࣗࢎ੉ૉ٣߄੉झചݶ೧࢚بীٮۄ౸ױೞݴ оةࢿਸ೧஖஖ঋח੸੺ೠ௼ӝܳҊ۰ೞৈࢎਊ
  2. 이 발표에서 다루지 않는 것. 1. ProGuard 사용법 2. 난독화

    도구 벤치마크 3. 앱을 해킹하는 방법 4. ProGuard 유즈 케이스 5. 소스 코드에 기반한 분석 6. 복잡한 난독화 이론 7. 안드로이드 10 이름 8. 구글, 네이버, 라인, 카카오, 카뱅 입사
  3. 이 발표에서 다루는 것. 1. 안드로이드 코드의 특징 2. 바이트

    코드에 대한 간략한 이해 3. Transform API 4. 난독화와 앱 보호에 대한 간략한 이론 5. ProGuard와 R8의 역할
  4. 안드로이드 코드의 특징 1. 이식에 좋은 바이트 코드를 사용. 2.

    동적 컴파일에 기반한 성능 향상. 3. 의존성도 동적으로 로드. 4. 암호화되지 않은 클래스와 리소스로 구성. 5. 공간 효율적인 데이터 포맷.
  5. 안드로이드 코드의 특징 1. 이식에 좋은 바이트 코드를 사용. 2.

    동적 컴파일에 기반한 성능 향상. 3. 의존성도 동적으로 로드. 4. 암호화되지 않은 클래스와 리소스로 구성. 5. 공간 효율적인 데이터 포맷.
  6. 이식에 좋은 바이트 코드 자바 Bytecode 1. 스택 기반의 VM을

    사용. 스택에서 입력 받고 결과도 스택으로. 2. 32비트 스택 하나의 요소가 대부분의 타입을 커버. 3. char을 사용해도 기본적으로는 메모리에 이점이 없다. 4. 64비트가 필요한 자료형(double, long)을 사용할 경우 스택 두 요소. 5. 256개의 연산 (실제로는 예약어가 많음.) 6. 스택 기반은 검증하기 쉬움. 7. 레지스터 기반의 실제 기기와 차이가 있어 성능상 단점.
  7. 스택 기반 VM 스택 변수 Frame 상수 풀 상수의 종류를

    구분하지 않는 것이 특징. (안드로이드는 분리함.)
  8. 스택 기반 VM 1 + 2 = ? 1 스택에

    1과 2를 넣는다. istore_1 // 1을 스택에 넣는다. int 상수 -1(m1)부터 5(0)까지는 별도의 istore 명령을 제공한다.
  9. 스택 기반 VM 1 + 2 = ? 1 2

    스택에 1과 2를 넣는다. istore_1 // 1을 스택에 넣는다. istore_2 // 2를 스택에 넣는다. int를 위한 연산
  10. 스택 기반 VM 1 + 2 = ? 1 2

    스택에서 값을 빼고 합산한다. istore_1 // 1을 스택에 넣는다. istore_2 // 2를 스택에 넣는다. iadd // 두 개의 수를 더해 스택에 넣는다. 사칙연산도 Java는 타입을 따진다. (닷넷은 따지지 않음.)
  11. 스택 기반 VM 1 + 2 = ? 3 합산된

    3을 스택에 넣는다. istore_1 // 1을 스택에 넣는다. istore_2 // 2를 스택에 넣는다. iadd // 두 개의 수를 더해 스택에 넣는다.
  12. 스택 기반 VM 1 + 2 = ? istore_1 명령을

    나열해보자. istore_2 iadd
  13. 스택 기반 VM 1 + 2 = ? 0x04 실제

    opcodes로 바꾸어 보자. 0x05 0x60 낯설어도 x86, ARM 기계어보다 단순. https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html istore_1 istore_2 iadd
  14. 이식에 좋은 바이트 코드 달빅 Bytecode 1. 레지스터 기반의 VM을

    사용. 2. 64K 개의 레지스터 하지만 주로 앞의 256개, 가끔은 16개만 사용. 3. 32비트 레지스터 하나의 요소가 대부분의 타입을 커버. 4. char을 사용해도 기본적으로는 메모리에 이점이 없다. 5. 64비트가 필요한 자료형(double, long)을 사용할 경우 레지스터 2개. 6. 200여개의 연산. 7. 실제 하드웨어와 매핑에 이점. 성능 상의 이점. 메모리를 덜 씀. 8. 상대적으로 검증이 어려움.
  15. 달빅 bytecode 1+2 = ? const/4 v2, 0x1 // 레지스터

    v1에 4비트 상수 0x1을 넣습니다. const/4 v3, 0x2 // 레지스터 v2에 4비트 상수 0x2를 넣습니다. add-int v0, v2, v3 // 레지스터 v1과 v2의 합을 v0에 저장한다. const/4 0x1, v2 0x12 0x12 http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html
  16. 달빅 bytecode 1+2 = ? const/4 v2, 0x1 // 레지스터

    v1에 4비트 상수 0x1을 넣습니다. const/4 v3, 0x2 // 레지스터 v2에 4비트 상수 0x2를 넣습니다. add-int v0, v2, v3 // 레지스터 v1과 v2의 합을 v0에 저장한다. add-int v0 0x90 0x00 v2 v3 0x02 0x03
  17. 달빅 bytecode 조금 더 작은 바이너리 const/4 v2, 0x1 //

    레지스터 v1에 4비트 상수 0x1을 넣습니다. const/4 v3, 0x2 // 레지스터 v2에 4비트 상수 0x2를 넣습니다. add-int/2addr v2, v3 // 레지스터 v2에 v3를 더한다. add- int/2addr v3, v2 0xB0 0x32
  18. 자바와 달빅 bytecode가 다른데? javac desugar dex .java .class .class

    .dex JAVA 8 기능을 사용하는 코드를 JAVA7 바이트 코드로 변형.
  19. 자바와 달빅 bytecode가 다른데? javac desugar dex .java .class .class

    .dex .class 파일을 dex(dx, d8)을 통해 .dex 파일로 변환
  20. DX와 D8은 무엇인가요? dex .class .dex .class 파일을 dex(dx, d8)을

    통해 .dex 파일로 변환 DX - 안드로이드 스튜디오 3.0 전. D8 - 안드로이드 스튜디오 3.0 이후. gradle.properties에서 android.enableD8 = true | false D8은 써드파티 툴에서 문제가 될 수 있다. - 새로운 DEX를 지원하지 못하는 문제.
  21. 한번에 달빅 바이너리로 빌드하면 안되요? Jack & Jill .dex 구글은

    Jack & Jill을 통해 한번에 빌드하는 것을 시도 .java
  22. 구글은 결국 써드파티를 위한 API을 열었습니다. javac desugar 3rd party

    transformers dex 바이트 코드를 써드파티도 조작할 수 있다. Transform API
  23. 구글은 결국 써드파티를 위한 API을 열었습니다. Transformer .class .class 안드로이드

    빌드 툴에서 제공하는 클래스 파일 변조 표준. 프로가드 등의 도구들도 Transform API에 의존적. 다만 여전히 Transform API를 사용하지 못하거나 이해하지 못하는 도구들이 존재.
  24. 구글은 결국 써드파티를 위한 API을 열었습니다. Transform을 상속받고 transform을 구현합니다.

    입력으로 클래스 파일을 받아 출력으로 클래스를 내보내면 됩니다. (실제로는 다른 파일에 접근 가능하지만 대부분은 클래스에)
  25. 구글은 결국 써드파티를 위한 API을 열었습니다. ProGuard .class .class Realm

    Transformer .class .class Desugar .class .class 구글 코드들 조차도 표준화된 API를 따릅니다.
  26. 구글도 Transform API를 사용한다. 1. FixStackFramesTransform – 스택을 검증하기 위해

    바이트 코드 재계산. 2. DesugarTransform – Java 8 코드의 기능을 Java 7으로 변경. (람다!) 3. StripDebugSymbolTransform – 네이티브 라이브러리 디버그 심볼 제거. 4. ProGuardTransform – 프로가드 적용 트랜스포머. 5. R8Transform – D8에 난독화를 추가한 R8을 적용. (신기능) 6. JarMergingTransform – 여러 Jar를 합쳐줌. 7. DexMergerTransform - 여러 Dex를 합침. ... https://android.googlesource.com/platform/tools/base/+/studio-master- dev/build-system/gradle- core/src/main/java/com/android/build/gradle/internal/transforms/
  27. 여러분도 Transformer를 만들고 싶다면 ASM BCEL Javassist AspectJ Low Level

    High Level 바이트 코드를 손으로 한땀 한땀 조작 도구를 사용합시다. 구글은 ASM을 좋아하는 것 같으며, Realm은 Javassist를 선택했고, Jake Wharton이 AspectJ를 쓴 걸 봤어요.
  28. 안드로이드 코드의 특징 1. 이식에 좋은 바이트 코드를 사용. 2.

    동적 컴파일에 기반한 성능 향상. 3. 의존성도 동적으로 로드. 4. 암호화되지 않은 클래스와 리소스로 구성. 5. 공간 효율적인 데이터 포맷.
  29. 동적 컴파일에 기반한 성능 향상 인터프리터 vs JIT vs AOT

    1. 아무런 정보가 없었을 때 인터프리터로 해석. (느림) 2. 일정 횟수 이상 수행된 메서드만 컴파일해 JIT 코드 캐쉬에 저장. 3. 메서드 수행 시 JIT 코드 캐쉬에 있다면 인터프리터 대신 캐쉬 사용. 4. 프로파일링 정보에 기반해서 JIT 코드 캐쉬를 업데이트. 5. 주기적으로 dex2aot 데몬이 코드를 빌드해서 .oat 파일을 생성. 6. 다른 앱에 의해 사용되면 전체 빌드하고 아니면 프로파일에 기반해 빌드. 7. JIT 컴파일과 AOT 컴파일이 있다면 JIT 컴파일을 사용. (동적 최적화)
  30. 안드로이드 코드의 특징 1. 이식에 좋은 바이트 코드를 사용. 2.

    동적 컴파일에 기반한 성능 향상. 3. 의존성도 동적으로 로드. 4. 암호화되지 않은 클래스와 리소스로 구성. 5. 공간 효율적인 데이터 포맷.
  31. 의존성도 동적으로 로드. 1. import / export 해야하는 부분은 숨길

    수 없다. 2. 메서드 명, 클래스 명, 필드 명이 공개된다. 3. 안드로이드 앱의 경우 Activity, Fragment, Service 등을 외부로 공개해야 한다. 4. 난독화 도구에 exclude/include 항목을 설정해야 함.
  32. 안드로이드 코드의 특징 1. 이식에 좋은 바이트 코드를 사용. 2.

    동적 컴파일에 기반한 성능 향상. 3. 의존성도 동적으로 로드. 4. 암호화되지 않은 클래스와 리소스로 구성. 5. 공간 효율적인 데이터 포맷.
  33. 암호화되지 않은 클래스와 리소스로 구성 1. 클래스와 리소스는 암호화되지 않음.

    2. 기본 클래스 로더가 지원하지 않아 한계가 존재. 기본 클래스 로더가 호출되기 전에 암호화가 풀린 클래스를 가로챌 수 있음. 3. 클래스 암호화는 선호되는 보호 방법이 아님.
  34. 안드로이드 코드의 특징 1. 이식에 좋은 바이트 코드를 사용. 2.

    동적 컴파일에 기반한 성능 향상. 3. 의존성도 동적으로 로드. 4. 암호화되지 않은 클래스와 리소스로 구성. 5. 공간 효율적인 데이터 포맷.
  35. 공간 효율적인 데이터 포맷 1. LEB-128을 사용하여 가변 바이트(1-5바이트)로 32비트

    잘 저장. (최악의 경우에는 더 나빠질 수 있음.) 2. 부호가 있는 수를 위한 SLEB128, 부호없는 수를 위한 ULEB128과 -1만 지원하는 ULEB128p1이 있음. 3. 여러 클래스에서 상수를 공유. (Shared Constant Pool) 4. 상대 주소 사용. 5. 하지만 메서드 갯수까지 효율적으로 64K T.T
  36. LEB-128 (Little Endian Base-128) 10011000011101100101 제일 앞 그룹은 0, 그

    외는 앞에 1을 붙인다. 0100110 0001110 1100101 00100110 10001110 11100101
  37. LEB-128 (Little Endian Base-128) 10011000011101100101 1. 가변 바이트를 사용해 작은

    수는 더 적은 비트로 저장. (1바이트) 2. 최악의 경우 32비트 자료형을 5바이트로 저장할 수 있다. 3. SLEB128P는 전체 값이 -1이면 0이 된다. 음수 중 -1만 지원. 0100110 0001110 1100101 00100110 10001110 11100101
  38. 안드로이드 코드를 어떻게 숨길까? 1. 클래스/리소스에 암호화를 적용한다. 2. 리소스

    사이에 클래스를 매복시킨다. 3. 네이티브 코드로 핵심 코드를 숨긴다. 4. 서버에 핵심 코드를 숨긴다. 5. 템퍼 감지를 추가한다. 6. 클래스, 필드, 메서드 명을 변경한다. 7. 리플렉션 호출을 추가한다. 8. 노이즈 추가. 9. 제어 흐름.
  39. 안드로이드 코드를 어떻게 숨길까? 1. 클래스/리소스에 암호화를 적용한다. 2. 리소스

    사이에 클래스를 매복시킨다. 3. 네이티브 코드로 핵심 코드를 숨긴다. 4. 서버에 핵심 코드를 숨긴다. 5. 템퍼 감지를 추가한다. 6. 클래스, 필드, 메서드 명을 변경한다. 7. 리플렉션 호출을 추가한다. 8. 노이즈 추가. 9. 제어 흐름.
  40. 클래스/리소스에 암호화를 적용한다. 1. 클래스를 암호화 하고 커스텀 로더를 사용하는

    방법. 2. 암호화된 클래스와 로더를 가지는 패커(Packer)와 깨어진 클래스와 복구 코드를 가지는 프로텍터(Protector)로 나누어짐. 3. VM이 암호화된 클래스를 지원하지 않기 때문에 결국 해독을 하여 클래스를 전달해야 한다. 결국, 커스텀 로더가 공격의 취약점이 된다. 4. 많은 패커와 프로텍터는 ODEX 복사나 심지어 Dex2jar툴에 의해 쉽게 풀리기도 한다.
  41. 안드로이드 코드를 어떻게 숨길까? 1. 클래스/리소스에 암호화를 적용한다. 2. 리소스

    사이에 클래스를 매복시킨다. 3. 네이티브 코드로 핵심 코드를 숨긴다. 4. 서버에 핵심 코드를 숨긴다. 5. 템퍼 감지를 추가한다. 6. 클래스, 필드, 메서드 명을 변경한다. 7. 리플렉션 호출을 추가한다. 8. 노이즈 추가. 9. 제어 흐름.
  42. 리소스 사이에 클래스를 매복시킨다. 1. 이미지 파일로 dex 파일을 숨기는

    방법 등을 사용한다. 2. 수상하게 큰 파일을 찾거나 파일 포맷이 올바른지 검증하는 것만으로 혐의가 좁혀진다.
  43. 안드로이드 코드를 어떻게 숨길까? 1. 클래스/리소스에 암호화를 적용한다. 2. 리소스

    사이에 클래스를 매복시킨다. 3. 네이티브 코드로 핵심 코드를 숨긴다. 4. 서버에 핵심 코드를 숨긴다. 5. 템퍼 감지를 추가한다. 6. 클래스, 필드, 메서드 명을 변경한다. 7. 리플렉션 호출을 추가한다. 8. 노이즈 추가. 9. 제어 흐름.
  44. 네이티브 코드로 핵심 코드를 숨긴다. 1. 성능 상의 이점이 있으며

    기계어 코드가 자바 / 달빅의 바이트 코드보다 해석하기 어렵다는 장점이 있음. 하지만 인내가 있다면 결국 기계어 코드도 해석할 수 있다. 2. VM 밖은 위험하다. (온갖 하드웨어 이슈를 경험할 수 있다.) .so 파일이 간혈적으로 설치되지 않는 문제 발생. POSIX 함수가 목업이었던 B사. OpenSSL이 목업이었던 L사. 메모리 할당량을 잘못 알려주었던 S사. 재현 불가능한 이슈도 동작한다. 3. ABI 세트를 맞추기 어렵다.
  45. 안드로이드 코드를 어떻게 숨길까? 1. 클래스/리소스에 암호화를 적용한다. 2. 리소스

    사이에 클래스를 매복시킨다. 3. 네이티브 코드로 핵심 코드를 숨긴다. 4. 서버에 핵심 코드를 숨긴다. 5. 템퍼 감지를 추가한다. 6. 클래스, 필드, 메서드 명을 변경한다. 7. 리플렉션 호출을 추가한다. 8. 노이즈 추가. 9. 제어 흐름.
  46. 서버에 핵심 코드를 숨긴다. 1. 리얼 타임 응답성이 떨어진다. 웹

    소켓 등의 상시 연결을 하더라도 로컬만큼 쾌적하기 어렵다. 응답성을 올리기 위해 로컬 데이터베이스등이 필요할 수 있다. 2. 서버가 다운된 경우 어플리케이션을 사용할 수 없다. 로컬 캐쉬를 이용해서 폴백을 구현하기 쉽지 않다.
  47. 안드로이드 코드를 어떻게 숨길까? 1. 클래스/리소스에 암호화를 적용한다. 2. 리소스

    사이에 클래스를 매복시킨다. 3. 네이티브 코드로 핵심 코드를 숨긴다. 4. 서버에 핵심 코드를 숨긴다. 5. 템퍼 감지를 추가한다. 6. 클래스, 필드, 메서드 명을 변경한다. 7. 리플렉션 호출을 추가한다. 8. 노이즈 추가. 9. 제어 흐름.
  48. 템퍼 감지를 추가한다. 1. 디버거, ptrace를 발견하는 코드를 삽입한다. 2.

    에뮬레이터 등에 감지한다. 3. 해당 부분을 우회하면 대부분 앱에 진입할 수 있다.
  49. 안드로이드 코드를 어떻게 숨길까? 1. 클래스/리소스에 암호화를 적용한다. 2. 리소스

    사이에 클래스를 매복시킨다. 3. 네이티브 코드로 핵심 코드를 숨긴다. 4. 서버에 핵심 코드를 숨긴다. 5. 템퍼 감지를 추가한다. 6. 클래스, 필드, 메서드 명을 변경한다. 7. 리플렉션 호출을 추가한다. 8. 노이즈 추가. 9. 제어 흐름.
  50. 클래스, 필드, 메서드 등의 이름을 변경한다. 1. 무료 도구인 ProGuard

    부터 대부분의 도구가 지원. 2. 짧고 간결한 이름으로 클래스, 필드, 메서드의 이름을 변경. 단 외부에서 import되거나 export되는 명칭들은 변경될 수 없음. 3. 짧고 간결한 이름의 경우 공간 복잡도도 줄이고 실행 시간에도 긍정적인 영향을 줄 수 있음. 4. 명칭이 바뀌는 것이기 때문에 공격자의 인내로 해결 가능. 5. 도구마다 고유의 네이밍 패턴이 있음. (APKiD에서 검출)
  51. 안드로이드 코드를 어떻게 숨길까? 1. 클래스/리소스에 암호화를 적용한다. 2. 리소스

    사이에 클래스를 매복시킨다. 3. 네이티브 코드로 핵심 코드를 숨긴다. 4. 서버에 핵심 코드를 숨긴다. 5. 템퍼 감지를 추가한다. 6. 클래스, 필드, 메서드 명을 변경한다. 7. 리플렉션 호출을 추가한다. 8. 노이즈 추가. 9. 제어 흐름.
  52. 리플렉션 호출을 추가 1. 메서드 호출에 단계를 추가하여 해석하기 어렵게

    만듬. 2. ProGuard는 지원하지 못하지만 DexGuard나 Arxan 등의 유료 도구에서 지원. 3. 리플렉션이 반복적인 패턴을 가지기 때문에 기계적으로 풀 수 있음. (예: Dex-Oracle에서 DexGuard 패턴 해제.) 4. 많은 단말에서 리플렉션 크래시가 보고됨. 리플렉션 실행이 랜덤하게 실패하는 것으로 보임. (혼탁한 안드로이드의 세계)
  53. 안드로이드 코드를 어떻게 숨길까? 1. 클래스/리소스에 암호화를 적용한다. 2. 리소스

    사이에 클래스를 매복시킨다. 3. 네이티브 코드로 핵심 코드를 숨긴다. 4. 서버에 핵심 코드를 숨긴다. 5. 템퍼 감지를 추가한다. 6. 클래스, 필드, 메서드 명을 변경한다. 7. 리플렉션 호출을 추가한다. 8. 노이즈 추가. 9. 제어 흐름.
  54. 노이즈 추가 1. 메서드 외부에 아무런 영향이 없는 코드를 추가.

    2. 클래스 상태 변경, I/O, 반환 값 등이 없음. 3. 디버거나 디스컴파일러가 깨지는 코드를 추가하기도 한다. 하지만 몇 줄로 망가트린 문제는 그만큼 쉽게 고쳐진다.
  55. 안드로이드 코드를 어떻게 숨길까? 1. 클래스/리소스에 암호화를 적용한다. 2. 리소스

    사이에 클래스를 매복시킨다. 3. 네이티브 코드로 핵심 코드를 숨긴다. 4. 서버에 핵심 코드를 숨긴다. 5. 템퍼 감지를 추가한다. 6. 클래스, 필드, 메서드 명을 변경한다. 7. 리플렉션 호출을 추가한다. 8. 노이즈 추가. 9. 제어 흐름.
  56. 제어 흐름 1. 반복문이나 분기문을 불 필요하게 추가. 2. If문

    대신 try-catch 블록을 사용하기도 함. (Replacing if null instructiions with try-catch blocks) 3. Switch 문을 중첩적인 레이블로 변경하기도 함. (Partially trapping switch statements) 4. 브랜치를 jsr 명령 (goto)로 변환하기도 함. (Converting branches to jsr instructions) 일반적으로 속도를 저하시키는 요인.
  57. 프로가드는 무엇을 지원할까? 1. 클래스/리소스에 암호화를 적용한다. 2. 리소스 사이에

    클래스를 매복시킨다. 3. 네이티브 코드로 핵심 코드를 숨긴다. 4. 서버에 핵심 코드를 숨긴다. 5. 템퍼 감지를 추가한다. 6. 클래스, 필드, 메서드 명을 변경한다. 7. 리플렉션 호출을 추가한다. 8. 노이즈 추가. 9. 제어 흐름.
  58. 프로가드는 무엇을 지원할까? 1. 클래스/리소스에 암호화를 적용한다. 2. 리소스 사이에

    클래스를 매복시킨다. 3. 네이티브 코드로 핵심 코드를 숨긴다. 4. 서버에 핵심 코드를 숨긴다. 5. 템퍼 감지를 추가한다. 6. 클래스, 필드, 메서드 명을 변경한다. 7. 리플렉션 호출을 추가한다. 8. 노이즈 추가. 9. 제어 흐름. 10. 사용하지 않은 클래스, 필드, 메서드 제거.
  59. 안드로이드 빌드에 통합된 프로가드 ProGuard Transform javac Desugar Transform ProGuard

    .java .class .class .class Dex .dex 제대로 통합되지 않은 툴이 많은 것을 감안하면 엄청난 장점.
  60. 통합되지 않은 난독화 도구들의 흐름 javac Desugar Transform .java .class

    .class Dex .dex Dex2jar Obfuscator .class .class .class Dex .dex
  61. 구글의 신 병기 R8 R8 javac Desugar Transform .java .class

    .class .dex R8 = D8 + Shrinker + Name Obfuscator https://android.googlesource.com/platform/external/r8/ ProGuard를 버리겠다는 계획. 갑자기 괜히 렌더스크립트, Jack & Jill을 불러보고 싶다.