[안드로이드 모의해킹] Dynamic DEX Loading 분석하기

1. 개요

악성앱(피싱앱) 분석을 하던 중 동적 덱스 로딩 기법이 적용된 앱을 분석하게 되어 정리해둔다.

 

2. DEX 파일 복호화

샘플 앱의 경우, '/assets' 경로 내 동적 로딩을 위한 .dex 파일이 위치하고 있으며, 내용은 암호화되어 있음을 확인한다. 앱 내부에서만 접근 가능한 저장소에 암호화한 dex 파일을 저장하고 배포하여 설치 시에 복호화 후 로드하는 "동적 DEX 로딩(Dynamic DEX Loading)" 방식이다. 

 

apk 파일 내 assets 경로에 위치한 dex 파일

 

암호화되어 있는 dex 파일 내용

 

코드에서 해당 경로(assets/)에 접근하여 파일을 복호화하고 복사하는 과정이 있을 것이므로 문자열로 검색하여 사용하고 있는 부분을 파악한다. assets 경로에는 앱 실행에 필요한 다른 리소스 또한 포함되어 있으므로 해당 기능을 제외하면 다음의 결과를 확인할 수 있다. 대상 앱의 경우 같은 클래스 내 다음 행에 dex 파일명으로 사용된 문자열도 확인할 수 있다.

 

assets/ 문자열 확인

 

 

자바 코드에는 파일을 열고 로드하는 과정은 없었으며 JNI를 이용하여 호출하고 있음을 확인하였다. 공유 라이브러리 파일(.so)을 분석하여 로직을 확인한다. 자바 디컴파일 코드에서 심볼 정보가 난독화되어 있어 일대일로 매핑하기는 어렵다. 일반적으로 다음 방법을 사용하여 함수를 찾을 수 있다.

  1. 같은 경로 문자열(/asset)로 검색하여 사용하고 있는 위치 파악
  2. 파일을 열고 쓰는 함수 심볼(open, fopen 등)로 검색하여 사용하고 있는 위치 파악
  3. 복호화를 위해서 파일의 내용에 접근해야 하므로 메모리 복사, 비교함수(memset, memcpy, memcmp 등) 추적
  4. JNI(JavaNativeInterface) 호출 패턴을 가진 함수 분석

위 방법으로 의심 함수를 식별하고 크로스 레퍼런스 함수를 찾아 호출과 참조를 추적한다. 그 결과 다음의 함수를 찾을 수 있었다.

 

bool __fastcall __IsStandardDex(void *a1)
{
  return j_memcmp(a1, "dex\n", 4u) == 0;
}

 

 

DEX 파일의 시그니처 값을 확인하여 올바른 파일인지 여부를 반환한다. 파일의 내용을 복호화한 후 올바른 DEX 파일로 복구가 되었는지 확인하는 의도로 사용할 것이다. 해당 함수를 호출하고 있는 함수를 분석하면 다음의 함수를 확인할 수 있다.

 

 

void __fastcall __UpdateDex__(int a1)
{
  unix_file::FdFile *v1; // r4
  size_t v2; // r0
  int v3; // r0
  int *v4; // r0
  const char *v5; // r4
  char *v6; // r0
  unsigned int v7; // r0
  void *v8; // r5
  unsigned int v9; // r0
  int *v10; // r0
  const char *v11; // r5
  char *v12; // r0
  unsigned int v13; // r0
  void (__fastcall *v14)(unix_file::FdFile *, void *, _DWORD, _DWORD, _DWORD, _DWORD); // r7
  __int64 v15; // r0
  int v17; // [sp+14h] [bp-4h]
  String v18[3]; // [sp+18h] [bp+0h] BYREF
  char v19[16]; // [sp+24h] [bp+Ch] BYREF
  char *v20; // [sp+34h] [bp+1Ch]
  char *v21; // [sp+38h] [bp+20h]

  String::String(v18, (const String *)&Globals::absoluteEncryptJar_);
  v1 = (unix_file::FdFile *)operator new(0x28u);
  unix_file::FdFile::FdFile(v1);
  v20 = v19;
  v21 = v19;
  v2 = j_strlen(v18[0]);
  std::string::_M_range_initialize((std::string *)v19, v18[0], &v18[0][v2]);
  v17 = unix_file::FdFile::Open(v1, (const std::string *)v19, 2);
  v3 = sub_7CEE(v19);
  if ( v17 )
  {
    v7 = (*(int (__fastcall **)(unix_file::FdFile *))(*(_DWORD *)v1 + 20))(v1);
    v8 = (void *)operator new[](v7);
    v9 = (*(int (__fastcall **)(unix_file::FdFile *))(*(_DWORD *)v1 + 20))(v1);
    if ( !unix_file::FdFile::ReadFully(v1, v8, v9) )
    {
      v10 = (int *)((int (*)(void))j___errno)();
      v11 = v18[0];
      v12 = j_strerror(*v10);
      j___android_log_print(2, "JDOG", "%s : Failed to read %s , error msg  '%s'.", "__UpdateDex__", v11, v12);
      (*(void (__fastcall **)(unix_file::FdFile *))(*(_DWORD *)v1 + 8))(v1);
      goto LABEL_11;
    }
    if ( a1 )
    {
      if ( a1 != 1 || !__IsStandardDex(v8) )
        goto LABEL_10;
    }
    else if ( __IsStandardDex(v8) )
    {
LABEL_10:
      (*(void (__fastcall **)(unix_file::FdFile *))(*(_DWORD *)v1 + 8))(v1);
      operator delete(v8);
      goto LABEL_11;
    }
    v13 = (*(int (__fastcall **)(unix_file::FdFile *))(*(_DWORD *)v1 + 20))(v1);
    XorArray(v8, v13, 0);
    v14 = *(void (__fastcall **)(unix_file::FdFile *, void *, _DWORD, _DWORD, _DWORD, _DWORD))(*(_DWORD *)v1 + 24);
    v15 = ((__int64 (__fastcall *)(unix_file::FdFile *))*(_DWORD *)(*(_DWORD *)v1 + 20))(v1);
    v14(v1, v8, v15, HIDWORD(v15), 0, 0);
    goto LABEL_10;
  }
  v4 = (int *)j___errno(v3);
  v5 = v18[0];
  v6 = j_strerror(*v4);
  j___android_log_print(2, "JDOG", "%s : Failed to open %s , error msg  '%s'.", "__UpdateDex__", v5, v6);
LABEL_11:
  String::~String(v18);
}

 

파일을 읽고 DEX 파일인지 여부를 확인하고, 아닌 경우 XOR 연산을 통해 파일을 생성하는 코드이다. 전부 분석하여 파일을 얻을 수 있지만, 후킹을 통해 생성 시점의 DEX 파일을 확인할 수 있다.

 

3. DEX 로딩

DEX 파일을 복호화하여 준비했다면, 앱이 인식할 수 있도록 로드한다. 로드하기 위한 방법은 로드하는 위치에 따라 세가지가 있다.

  • DexClassLoader
  • PathClassLoader
  • InMemoryDexClassLoader

분석 앱에서는 복호화한 DEX 파일로부터 메모리에 로드하는 방식을 사용한다. 기존 라이브러리가 로드될 때 실행되는 콜백 함수인 JNI_OnLoad() → Start_Load_jar → __LoadDexHigh (SDK 13 이상) 함수에 구현되어 있다.

 

jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
  _JNIEnv *v3; // r6
  const char *v4; // r1
  void *Class; // r1
  jint v6; // r4
  _JNIEnv *v8; // [sp+0h] [bp-38h] BYREF
  char v9[16]; // [sp+4h] [bp-34h] BYREF
  int v10[2]; // [sp+14h] [bp-24h] BYREF
  int v11[7]; // [sp+1Ch] [bp-1Ch] BYREF

  j_gettimeofday(v9, 0);
  v8 = 0;
  if ( (*vm)->GetEnv(vm, (void **)&v8, 65540) )
    return -1;
  _Init_(v8);
  v3 = v8;
  v11[0] = (int)"a";
  v11[1] = (int)"()Z";
  v11[2] = (int)__Start_Load_jar__;
  v4 = (const char *)Globals::java_library_;
  if ( !Globals::java_library_ )
    v4 = "com/jdog/JLibrary";
  Class = (void *)_JNIEnv::FindClass(v8, v4);
  v10[0] = (int)v3;
  v10[1] = (int)Class;
  if ( Class && v8->functions->RegisterNatives((JNIEnv *)v8, Class, (const JNINativeMethod *)v11, 1) >= 0 )
  {
    j_gettimeofday(&v9[8], 0);
    v6 = 65540;
  }
  else
  {
    v6 = -1;
  }
  ScopedLocalRef<_jclass *>::reset(v10, 0);
  return v6;
}

 

이후에 __LoadDexHigh() 함수호출하며 이것저것 하지만 포스팅 의도에 벗어나므로 생략한다.

 

4. 동적 라이브러리 링크 생성(Linking)

정적(static) 라이브러리의 경우 빌드하는 시점에 링크되어 바이너리에 포함되지만, 동적(dynamic)으로 로딩한 라이브러리 파일은 실행 시점에 링크를 걸어주어야 하기 때문에 /system/bin/linker 바이너리를 이용하여 링크를 걸어준다.

 

 

안드로이드 10 이상 변경사항으로는 내부 동적 네이티브 라이브러리 런타임 종속성 문제를 방지하기 위해 각각의 네임스페이스를 가진 런타임 모듈 라이브러리에 설정된다.

 

 

링커는 /system 내 바이너리의 경우 /system/etc/ld.config.txt에 선언되어 있다. 같은 바이너리를 사용하는 것이지만 심볼릭 링크로 전환되는 것이다. 앱 코드에서 /system/bin/linker를 호출하면 안드로이드 런타임에서 /apex/com.android.runtime/bin/linker 경로로 변경하어 호출한다. 단, 하위버전 호환성을 위해 /system/bin 경로에 심볼릭 링크를 유지한다.

(참고: https://source.android.com/docs/core/architecture/modular-system/runtime?hl=ko)

 

 

다음 코드를 보면 '/system/bin/linker'를 이용하여 동적 링크를 생성할 준비를 하고 있다.

 

 

이제 필요한 DEX 파일을 로드했으니 분석을 이어가면 된다.

 

마치며

메니페스트 파일에 선언된 액티비티가 디컴파일된 코드에 존재하지 않는 경우 의심해볼만한 기법이다. 시그니처 기반 파일 검사에서 탐지되지 않고 문자열 또한 난독화되어 있기 때문에 검색으로 찾기도 어렵다. 디컴파일의 종류에 따라 코드레벨까지만 검사하는 경우도 있어 식별하기 어려운 파일이다. 동적 로딩을 하는 위치의 경우 앱 내에서 접근 가능한 경로를 사용하는 경우도 있지만 서버에서 다운받아 로드하는 경우도 있으니 경우의 수를 따져봐야 한다.

 

 

루팅 탐지 로직이나 어려운 난독화가 적용된 것이 아니기 때문에 분석하는데 어렵진 않았다. 여러 기법이 같이 적용되었다면 시간이 오래걸리는 이유가 된다.

반응형