macOS의 Fileless형 코드 로딩 방법

소개

공격자가 자기자신의 의도를 숨기기 위한 파일리스(Fileless, 파일을 디스크에 두지 않는)형 악성코드 로드 방법이 많이 발견되고 있다. 보통 파일리스형 악성코드는 두가지 형태로 나눠 생각할 수 있다.

  1. wscript, powershell 등 스크립트 형태 실행 : 스크립트 파일로 악성 행위를 수행하는 방법으로, 사실 과거에도 공격자가 많이 사용한 방법이다. 윈도우의 파워쉘의 기능이 막강해짐에 따라 공격자들이 스크립트 형태로 악성행위를 수행하여 널리 알려졌다. 스크립트 파일을 획득하여 분석하면 악성 행위를 찾을 수 있다. 과거에도 많이 쓰였던 방법으로 사실 파일리스라는 생각이 들진 않는다.
  2. 레지스트리, 암호화된 파일 : 코드가 실행 시점에만 특정 위치에 난독화 또는 암호화된 파일을 메모리에 로드하여 복호화하여 실행하는 방법을 말한다. 분석가는 로더를 분석하여 암호화된 파일을 찾고 이를 다시 복호화하여 분석해야 한다.
  3. 외부 서버를 통한 동적 페이로드 로딩 : 로더는 디스크에 존재하고 C2 서버에 연결하여 악성행위를 하는 코드를 동적으로 받아 메모리에서 실행하는 방법을 말한다. 이 방법은 실행 파일을 디스크에 보존하지 않으므로 공격자 입장에서 자신의 목적을 숨기고 악성 행위를 하기 유용한 방법이다. 단, 로더를 활용하지 않으므로 API가 제공되지 않는다면 가장 리스크가 큰 방법이다.

2,3과 같은 파일리스형 코드 실행 방법은 공격자가 자신의 의도를 숨길 수 있는 강력한 방법이지만, 메모리에 맵핑된 실행 파일을 충돌없이 실행하게 만들기 위한 추가적인 작업이 요구된다.

19년 12월경, Lazarus 그룹이 배포한 것으로 알려진 악성코드인 AppleJeus는 macOS에서 파일리스형 코드 실행 기법을 적용한 최초의 사례이다. 과거에 macOS의 악성코드는 독립적으로 실행하거나 드롭퍼 형태로 다른 실행 파일을 생성하고 이를 실행하는 방식을 따랐으나, 이 악성코드는 C2 서버에서 동적으로 코드를 메모리에 받아 이를 실행하는 방법을 사용하였다. 오늘 글은 그 방법을 정리하고자 한다.

macOS의 파일리스 로딩 방법

애플은 애플리케이션을 사용할 때 외부 기능을 플러그인 형태로 로드하여 사용하고 싶어 했다. 이에, 이러한 파일을 로드하기 위한 포맷으로 Bundle이라는 파일 포맷을 만들었다. 사실 말이 만들었다이지, 이 파일 포맷은 기존의 Mach-O 파일과 동일한 파일 포맷으로 헤더의 파일 타입에 MH_BUNDLE이라고 설정되어 있다. 애플의 아이무비와 같은 애플리케이션의 플러그인은 다 이 파일 타입을 가지고 있다.

Computer:macos_execute_from_memory n0fate$ otool -h test.bundle 
Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
 0xfeedfacf 16777223          3  0x00           8    13       1240 0x00000085
Computer:macos_execute_from_memory n0fate$

파일 타입은 애플의 오픈소스 코드에서 확인할 수 있다.

#define	MH_OBJECT	0x1		/* relocatable object file */
#define	MH_EXECUTE	0x2		/* demand paged executable file */
#define	MH_FVMLIB	0x3		/* fixed VM shared library file */
#define	MH_CORE		0x4		/* core file */
#define	MH_PRELOAD	0x5		/* preloaded executable file */
#define	MH_DYLIB	0x6		/* dynamically bound shared library */
#define	MH_DYLINKER	0x7		/* dynamic link editor */
#define	MH_BUNDLE	0x8		/* dynamically bound bundle file */
#define	MH_DYLIB_STUB	0x9		/* shared library stub for static */
					/*  linking only, no section contents */
#define	MH_DSYM		0xa		/* companion file with only debug */
					/*  sections */
#define	MH_KEXT_BUNDLE	0xb		/* x86_64 kexts */

위와 같이 0x08은 동적 바운드된 번들 파일이라고 정의되어 있다. 번들 파일이 기존 실행 파일과 다른 점은 아래와 같다.

  1. 독립적인 실행 불가능 : 파일 타입이 번들로 되어있는 경우, 실행 시 독립적으로 실행할 수 없다는 메시지가 출력된다.
  2. PAGEZERO 세그먼트가 없음 : NULL Dereference 방어 및 PIE를 위한 PAGEZERO 세그먼트가 없다. 플러그인으로 다른 프로세스에 의해 로드되기 때문으로 생각된다.

2015년 블랙햇에 @Wardle은 이러한 번들 파일을 이용하여 파일리스 형태로 로드하는 방법을 발표하였다발표자료. 파일리스 형태로 실행하는 절차는 다음과 같다.

  1. Bundle 파일을 메모리에 그대로 로드한다. (mmap)
  2. 메모리에 로드된 파일을 이미지 파일로 인식시킨다. (NSCreateObjectFileImageFromMemory)
  3. 이미지 파일을 모듈로 인식시킨다. (NSLinkModule)
  4. 심볼을 찾고 해당 주소를 받아서 실행한다. (NSLookupSymbolInModule → NSAddressOfSymbol)

NSCreateObjectFileImageFromMemory 라는 API는 mmap을 통해 맵핑된 실행 파일의 이미지 레퍼런스를 생성(Creates an image reference for a Mach-O file currently in memory.)한다. 즉, 해당 파일을 마치 실행파일 로더가 로딩한 것과 같이 만들어 준다. (링크)

NSObjectFileImageReturnCode NSCreateObjectFileImageFromFile(
   void* address,
   unsigned long size,
   NSObjectFileImage* objectFileImage);

이렇게, 로드된 파일은 NSLinkModule을 통해 현재 프로그램의 모듈처럼 인식시켜준다. 이 과정을 통해 커널은 해당 파일을 모듈로 인식(Links the given object file image as a module into the current program.)한다. 이 의미는 해당 함수 호출 시점에 lsof를 통해 파일이 로드되었음을 확인할 수 있다는 의미이다.

NSModule NSLinkModule(
   NSObjectFileImage objectFileImage,
   const char* moduleName,
   unsigned long options);

이렇게 로딩된 파일은 ‘NSLookupSymbolInModule’을 통해 모듈 내에 특정 심볼(실행하고자하는 함수의 이름)객체를 획득하고 이 객체를 ‘NSAddressOfSymbol’ 입력으로 심볼이 로드된 가상 메모리 주소를 얻을 수 있다.

// Given a module reference, returns a reference to the symbol with the given name.

NSSymbol NSLookupSymbolInModule(
   NSModule module,
   const char* symbolName);

// Returns the address in the program’s address space of the data represented by the given symbol. The data may be a variable, a constant, or the first instruction of a function.

void* NSAddressOfSymbol(
   NSSymbol symbol);

공격자는 이러한 API를 이용하여 효과적으로 악성코드를 파일리스 형태로 실행할 수 있다. 그러면 실제 케이스를 확인해보자.

AppleJeus(2019)의 파일리스 로딩 확인

AppleJeus는 UnionCrypto 라 불리는 암호화폐 거래 프로그램으로 위장하여 악성코드를 설치하였다. 실제 악성행위는 ‘/Library/UnionCrypto/unioncryptoupdater’ 이며, 이 악성코드의 ‘0x10000699F’ 주소에 파일리스 로딩 코드가 담겨져 있다.

__int64 __fastcall sub_10000699F(_DWORD *address, size_t size, __int64 a3)
{
  ...

  v3 = a3;
  v4 = a1[3];
  if ( v4 != 8 )
    a1[3] = 8;
  if ( NSCreateObjectFileImageFromMemory(address, size, &objectFileImage) == 1 )
  {
    module = NSLinkModule(objectFileImage, "core", 3u);
    if ( module )
    {
      result = 0xFFFFFFF5LL;
      if ( v4 == 2 )
      {
				sub_1000067D7(v6, (const char **)&baseAddress, 4u, 1);
        NumberofLoadCmd = *(_DWORD *)(baseAddress + 16);
        if ( NumberofLoadCmd )
        {
          loadcmd = baseAddress + 32;           // Load Commands
          Counter = 0;
          while ( *(_DWORD *)loadcmd != 0x80000028 )// lookup an address of LC_MAIN
          {
            loadcmd += *(unsigned int *)(loadcmd + 4);// add loadcmd size
            if ( ++Counter >= NumberofLoadCmd )
              goto LABEL_10;
          }
          LC_MAIN = (__int64 (__fastcall *)(signed __int64))(*(_QWORD *)(loadcmd + 8) + baseAddress);// lcmain.vmaddress + baseAddress
          v16 = "";
          v17 = v3;
          v18 = 0LL;
          v15 = 0LL;
          v14 = 0LL;
          result = LC_MAIN(2LL);
        }
        else
        {
LABEL_10:
          fwrite("Could not find ec.\n", 0x13uLL, 1uLL, __stderrp);
          result = 0xFFFFFFF6LL;
        }
      }
    }
    else
    {
      fwrite("Could not link image.\n", 0x16uLL, 1uLL, __stderrp);
      result = 0xFFFFFFF8LL;
    }
  }
  else
  {
    fwrite("Could not create image.\n", 0x18uLL, 1uLL, __stderrp);
    result = 0xFFFFFFF9LL;
  }
  return result;

위 코드를 보면 메모리에 올라가있는 페이로드를 ‘core’라는 이름으로 모듈화까지 작업하고 sub_1000067D7 함수를 호출하였다. sub_1000067D7함수는 페이로드에서 MH_MAGIC_64, 즉 64비트 실행 파일의 시그니처를 찾는다.

__int64 __fastcall sub_1000067D7(char *a1, const char **a2, unsigned int a3, int a4)
{
  int v4; // er15
  char *v5; // r12
  __int64 i; // r13
  const char *v7; // rbx

  v4 = a4;
  v5 = a1;
  *a2 = 0LL;
  for ( i = a3; ; v5 += i )
  {
    v7 = v5;
    if ( v4 )
      v7 = *(const char **)v5;
    chmod(v7, 511u);
    if ( *__error() == 2 && *(_DWORD *)v7 == 0xFEEDFACF )
      break;
  }
  *a2 = v7;
  return 0LL;
}

그 후 앞선 예제와 다르게 메모리에 로드된 파일의 Load Commands를 직접 뒤져서 LC_MAIN의 위치를 찾는다. LC_MAIN은 OS X 10.7 이전에 엔트리 포인트 역할을 하는 Command로 현재는 Deprecated된 녀석이다. 아예 사용을 막은건 아니므로 현재 운영체제에서도 사용은 가능하다.

앞 섹션에서 언급한 방법은 심볼이 있어야 가능하며, 심볼이 있으면 분석이 용이하므로 위와 같은 방법을 통해 공격자의 의도를 숨기고자 했던 것으로 판단된다. 즉 AppleJeus는 다음 순서대로 파일리스 형태의 코드를 실행한다.

  1. C2 서버에서 페이로드를 메모리로 다운로드한다.
  2. NSCreateObjectFileImageFromMemory 로 이미지화 한다.
  3. NSLinkModule 로 모듈로 등록한다.
  4. LoadCommand의 타입을 확인하여 LC_MAIN을 찾는다.
  5. LC_MAIN을 실행한다.

결론

과거부터 현재까지 윈도우에서 새로운 악성코드 기술의 트랜드(?)가 생기면 이 기술을 macOS에 적용한 사례가 발견되고 있다. 파일리스형도 마찬가지이다. 애플도 과거 macOS의 주요 API를 바로 제거하지 않고 Deprecated 상태로 두고 오래동안 사용 가능한 형태를 유지하기 때문에 옛날 API 중에서 공격에 효과적인 API를 찾아서 공격에 활용할 가능성이 존재한다. 이번 파일리스 로딩 건도 마찬가지로 과거의 API로 최신 트랜드의 기술을 구현한 것을 볼 수 있었다.

비록 최신 기술이다라고 언급하였지만 현재 macOS의 파일리스 로딩 방식은 lsof와 같은 프로그램으로 로딩 여부를 쉽게 확인할 수 있는 한계점(?)을 가지고 있으므로 분석가가 macOS에 대한 기본적인 분석 능력을 가진다면 쉽게 찾아낼 수 있었을 것이다. ;-)