Introduction to OSX Auditor

1. 소개

최근에 Mac OS X 사용자가 많아지기도 했고, 윈도우 포렌식을 연구하는 사람이 많아져서인지 1~2년 사이에 OS X에 대한 포렌식 도구가 속속 등장하고 있다.

대략 2008년까지만 해도 Mac OS X 침해대응 시 라이브 정보를 수집하려면 유닉스에서 사용하던 쉘스크립트를 약간 수정하여 정보 수집을 해야했다. 뭐 사실 그 때만해도 메모리 포렌식 도구는 있지도 않았으니, 다른 고급 기술을 Mac OS X에서 사용한다는건 꿈같은 일이였다. 뭐 국내에선 크게 수요도 있지 않았지만 말이다.

하지만 아이폰의 영향인지 윈도우에서 해먹을 건 다 해먹어서인지 2010년부터 슬슬 Mac OS X에 대한 포렌식 분석 기술이 소개되기 시작했다. 내가 만든 volafox도 윈도우에선 할게없다보니 Mac OS X로 만들어본 메모리 포렌식 도구였고, 2011년초에 라이브 정보를 수집하는MacResponseLE도 등장하였다. 사실 내가 보았을 때 MacResponse는 잘나가는 오픈소스 프로젝트가 될 줄 알았는데, 사람들의 무관심으로 인해 라이언 이상은 제대로 지원을 못한다-_-;; 사실 개발회사에서 추진하는 오픈소스 프로젝트 치고는 정말 빠르게 버려진 프로젝트였다. (이 프로젝트와 관련된 글은 여기를 참조하기 바람)

최근에 몸이 안좋다보니, 공부를 멀리하고 유흥(?)을 즐기다가 이제 안되겠다 싶어서 인터넷을 좀 보다가 OSX Auditor라는 오픈소스 도구를 발견했다. 대충 보름 전에 최종 커밋을 한 것을 보아 나름 개발자가 애착을 가지고 진행하는 프로젝트인 것 같았다. (제발 오래가길..)

일단 언어 자체는 파이썬으로 개발되어 있어서 모든 맥에서 별도의 라이브러리 설치없이 잘 작동하는 점은 매우 마음에 들었다. 사실 개인적으로 포렌식 도구가 대상 시스템에서 동작한다면, 최대한 대상 시스템에 아무것도 설치하지 않고 동작해야 한다고 생각하기 때문에 파이썬 언어를 사용하여 구현하는 것을 선호하는 편이다. 하지만 이 도구는 파이썬을 사용했음에도 pyobjc라는 파이썬 라이브러리를 설치해서 돌리라고 하고 있다.

뭐 사실 라이브 수집 자체가 시스템 무결성을 손상시키니 이정도 설치하는 것이 뭐가 문제가 있겠냐 싶겠지만, 추가적인 라이브러리를 설치하는 것과 그냥 돌리는 것은 생각보다 무결성 침해의 차이가 크다. (이건 운영체제를 좀 아는 분들은 동의할 것이다. 왜냐고 댓글로 물어봐도 귀찮아서 안써줌.)

2. 수집 정보

어쨌든, 일단 도구가 나왔으니, 어떠한 정보를 수집하는지 확인해보기로 했다. GitHub에 나온 정보에 따르면, 다음과 같은 정보를 수집한다.

OS X Auditor parses and hashes the following artifacts on the running system or a copy of a system you want to analyze:
  • the kernel extensions
  • the system agents and daemons
  • the third party's agents and daemons
  • the old and deprecated system and third party's startup items
  • the users' agents
  • the users' downloaded files
  • the installed applications
It extracts:
  • the users' quarantined files
  • the users' Safari history, downloads, topsites, HTML5 databases and localstore
  • the users' Firefox cookies, downloads, formhistory, permissions, places and signons
  • the users' Chrome history and archives history, cookies, login data, top sites, web data, HTML5 databases and local storage
  • the users' social and email accounts
  • the WiFi access points the audited system has been connected to (and tries to geolocate them)

크게 "KEXT", "자동실행", "설치된 소프트웨어", 다운로드한 파일"을 분석한다고 한다. 그리고 다운로드하여 실행이 제한된 파일과, 웹브라우저 내역, 소셜 및 이메일 정보, 와이파이 연결 정보를 추출한다고 한다. 사실 다른 정보야 MacResponse에서도 잘 진행했던 부분이고, 이미 많이 알려진 정보를 수집하는거라 크게 감흥은 없었는데, 다음 두가지 기능이 매우 마음에 들었다.
  • 각 애플리케이션의 무결성을 입증하기 위해 설치한 애플리케이션의 md5 해시를 산출하고, 이 해시를 이용하여 VirusTotal의 링크를 제공.
  • 현재까지 알려진 자동실행 내역을 수집하고, 해당 plist를 분석하여 자동실행 요소를 추출, 각 파일의 해시 값을 산출하여 VirusTotal의 링크를 제공.
기존 도구는 그냥 단순히 데이터를 수집하고 목록을 보여주는데 그쳤는데, 이러한 기능은 분석가가 초동분석을 좀 더 쉽게 할 수 있도록 한다.

3. 설치하기

이 도구는 파이썬 기반으로 동작하기 때문에 맥 사용자는 손쉽게 사용할 수 있다. Mac OS X에서 사용한다면 다음과 같은 순서로 구동하면 된다.

1. GitHub에서 최신버전 클론 (git 명령어는 github for Mac 설치 시 사용 가능, MacPort도 됨.)
victims-MacBook-Pro:MacMemoryReader victim$ git clone https://github.com/jipegit/OSXAuditor.git


2. pyobjc 설치 (easy_install로 설치함.)
victims-MacBook-Pro:MacMemoryReader victim$ sudo easy_install pyobjc

Password:
Searching for pyobjc
Reading http://pypi.python.org/simple/pyobjc/
Best match: pyobjc 2.5.1
Downloading https://pypi.python.org/packages/source/p/pyobjc/pyobjc-2.5.1.tar.gz#md5=f242cff4a25ce397bb381c21a35db885
Processing pyobjc-2.5.1.tar.gz
Running pyobjc-2.5.1/setup.py -q bdist_egg --dist-dir /tmp/easy_install-Qopl7q/pyobjc-2.5.1/egg-dist-tmp-7JX7TK
warning: install_lib: 'build/lib' does not exist -- no Python modules to install

Adding pyobjc 2.5.1 to easy-install.pth file

Installed /Library/Python/2.7/site-packages/pyobjc-2.5.1-py2.7.egg
Processing dependencies for pyobjc
Searching for pyobjc-framework-ServiceManagement==2.5.1
Reading http://pypi.python.org/simple/pyobjc-framework-ServiceManagement/
Best match: pyobjc-framework-ServiceManagement 2.5.1
Downloading https://pypi.python.org/packages/source/p/pyobjc-framework-ServiceManagement/pyobjc-framework-ServiceManagement-2.5.1.tar.gz#md5=0ec67fb8fae22104643d423a2b66ca17
Processing pyobjc-framework-ServiceManagement-2.5.1.tar.gz
Running pyobjc-framework-ServiceManagement-2.5.1/setup.py -q bdist_egg --dist-dir /tmp/easy_install-xz0b4q/pyobjc-framework-ServiceManagement-2.5.1/egg-dist-tmp-UvaErB
error: Installed distribution pyobjc-core 2.3.2a0 conflicts with requirement pyobjc-core>=2.5.1

이렇게만 하면 구동할 준비가 완료된다.

4. 돌려보기

이제 대충 기능은 알았으니, 테스트 겸 도구를 돌려보았다. 도구는 위에서 설명한 pyobjc만 easy_install로 설치해놓으면 잘 돌아간다... 는 훼이크고 다음과 같은 에러를 발생한다.

??

이 에러는 외국 친구가 개발할 때 많이 실수하는 부분으로 동아시아권의 URL정보가 있는 경우, 유니코드 인코딩을 제대로 해석 못해서 발생하는 문제이다. 전에 volafox 개발자 중 한명이 lsof를 플러그인을 init commit했을 때도 간헐적으로 저 에러를 목격한 적이 있었다. 에러를 알려주긴 해야하는데.. 일단은 영어로 쓰기가 귀찮아서 안알려주고 있다. 나중에 생각나면 보내야겠다.

(여담이지만 예전에 웹크롤러를 만들 때도 저놈의 인코딩 때문에 짜증나 죽는줄 알았음.)

5. 가상머신에서 돌려보기

여튼 이 문제 때문에 도구 결과를 제대로 볼 수도 없어서 가상머신인 라이언으로 돌려봤다. 가상머신에는 들은게 없기 때문에 에러가 날리가 없어서 기쁜 마음으로 도구를 돌려보았다.

자동 실행 내역

HTML로 결과를 뽑아봤는데, 생각보다 깔끔한 UI를 제공한다. 최근에 volafox도 모든 정보를 한번에 분석해서 HTML로 뽑아주는걸 생각 중인데, 요런 output으로 출력하는 것도 나쁘진 않은 것 같다.

위 결과는 자동실행(Startup) 항목에 대한 분석 결과이다. 자동실행의 각 항목에 대해서는 다음 포스팅으로 다룰 예정이니 지금은 그냥 윈도우 자동 실행과 똑같이 생각하면 된다. 사실 포렌식 분석 보고서에도 '자동 실행' 정도도 과한 정보이기 때문에 이정도 정보만 제공해도 충분하다고 생각한다. 나처럼 덕스럽게 파고들 것 아니면 말이다.

여하튼 자동 실행이 가능한 10군데에 있는 항목을 추출하고, 각 항목에서 로드하는 파일의 경로와 해시값을 기반으로 하는 Virus Total 링크 정보, 파일 생성 및 수정시간을 제공한다. 글을 쓰다보니, 저걸 테이블로 보여줘서 시간 순이나 파일 명 순 정렬을 해주면 더 좋을 것 같다는 생각이 들었으나 내가하긴 귀찮으니 생략한다.

5. 결론

이 도구는 가뜩이나 척박한 Mac OS X 포렌식 시장에서 한줄기 단비같은 도구인줄 알았으나, 아직은 갈길이 멀은 포렌식 도구이다. 사실 최근에는 메모리 분석 도구의 기능이 막강해져서 대부분의 라이브 데이터 분석이 가능하다보니 이러한 도구의 의미가 많이 퇴색되었다. 하지만, 디스크 이미지를 분석하기엔 시간이 부족한 상황에서 빠르게 알려진 악성코드가 존재했는지를 확인하기에는 여러모로 유용한 도구로 판단된다.
디지털 포렌식 분야 또는 악성코드 분석 분야에 종사자라면 한번 정도 고려할만한 포렌식 도구라 생각한다 :-)

OSXPMem의 물리 메모리 이미징 방법

2012년까지만해도 OSX이 메모리 이미징 방법에 Closed-Source 프로젝트인 MacMemoryReader에 의존하다보니, 개발자가 업데이트하지 않는 이상 최신 버전의 OS X의 메모리 이미징을 하지 못하는 문제가 발생하였다. OSXPMem는 2012년 말에 volatility framework를 개발 중인 구글 코드 프로젝트에서 오픈소스로 개발된 OSX용 물리 메모리 이미징 도구이다. 이 도구는 Mac OS X Leopard부터 최신 버전인 Mountain Lion까지 지원했기 때문에 새로운 기술을 이용한 메모리 이미징 기법을 사용했을 거라 생각했다. 하지만, 실제 코드 분석 결과 물리 메모리를 읽는 루틴이 MacResponseLE와 동일하였다. 이 포스팅에서는 OSXPMem의 메모리 이미징 절차를 알아보겠다. 이 도구도 크게 세 절차를 통해 물리 메모리를 이미징하며, 크게 보면 MacResponseLE와 동일한 방법을 사용한다.


1. Platform Export를 이용한 PE_state 구조체 정보 획득
OSXPMem은 MacResponseLE와 다르게 DTrace를 이용하지 않는다. 대신에 KEXT의 platform export를 이용한다. Platform export는 운영체제 플랫폼인 커널에서 익스포트한 구조체나 함수를 의미하는 것으로, 필요 시 커널 구조체의 정보에 원할하게 접근할 수 있게 하기 위해 존재한다. KEXT는 로드될 때, start 핸들러인 'pmem_start'가 실행되며, 이 함수는 PE_state의 bootArgs 구조체에 접근한다.

  // Access the boot arguments through the platform export,
  // and parse the systems physical memory configuration.
  boot_args * ba = reinterpret_cast<boot_args *>(PE_state.bootArgs);
  pmem_physmem_size = ba->PhysicalMemorySize;
  pmem_mmap = reinterpret_cast<EfiMemoryRange *>(ba->MemoryMap +
                                                 pmem_kernel_voffset);
  pmem_mmap_desc_size = ba->MemoryMapDescriptorSize;
  pmem_mmap_size = ba->MemoryMapSize;
… <SNIP> ...

 KEXT가 물리 메모리 분석에 필요한 기본적인 정보를 저장해두고, 애플리케이션은 ioctl() 함수를 이용하여 자신이 로드한 pmem 디바이스에 bootArgs 구조체에서 물리 메모리 맵 정보를 가져온다.

// Send an ioctl to the driver to get the physical memory map.
// Will also retrieve the size of the map and its descriptors.
// This function will allocate memory for mmap, make sure you free it.
//
// args: mmap is a pointer to a pointer that will recieve the memory map.
//       mmap_size is a pointer that will recieve the size of the memory map.
//       mmap_desc_size is a pointer that will recieve the size of an individual
//       memory descriptor in the memory map.
//       device_file is an open file descriptor to the pmem device file.
//
// return: EXIT_SUCCESS and EXIT_FAILURE.
//
unsigned int get_mmap(uint8_t **mmap, unsigned int *mmap_size,
                      unsigned int *mmap_desc_size, int device_file) {
  int err;
  int status = EXIT_FAILURE;

  err = ioctl(device_file, PMEM_IOCTL_GET_MMAP_SIZE, mmap_size);
  if (err != 0) {
    PMEM_ERROR_LOG("Error getting size of memory map");
    goto error;
  }
… <SNIP>

이 결과를 통해 물리 메모리 맵을 확보한다.

2. EfiMemoryRange 구조체에 메모리 맵을 저장
앞서 획득한 bootArgs 구조체의 정보를 이용하여 EfiMemoryRange라는 이름의 버퍼에 물리 메모리 맵 정보를 만든다. 물리 메모리 맵핑 정보는 세그먼트 형태로 나뉘어져있기 때문에, 익스포트할 파일 포맷에 맞게 맵핑 데이터를 재구성한다.

// Parse the mmap and dump each section into a raw file. Memory holes or
// unreadable sections like MMIO are zero padded in the dump file.
//
// args: mem_dev is an open filehandle to the pmem device file (/dev/pmem).
//       dump_file is an open filehandle to which the image will be written.
//
// return: EXIT_SUCCESS or EXIT_FAILURE.
//
unsigned int dump_memory_raw(int mem_dev, int dump_file) {
  unsigned int status = EXIT_FAILURE;
  uint64_t section = 0;
  uint64_t phys_as_size = 0;
  uint64_t bytes_imaged = 0;
  uint8_t *mmap = NULL;
  unsigned int mmap_size = 0;
  unsigned int mmap_desc_size = 0;

  if (get_mmap(&mmap, &mmap_size, &mmap_desc_size, mem_dev) == EXIT_FAILURE) {
    print_msg(STD, "Failed to obtain memory map\n");
    goto error_malloc;
  }
  // Iterate over each section in the physical memory map and write it to disk.
  for (section = 0; section < mmap_size / mmap_desc_size; section++) {
    EfiMemoryRange *segment = (EfiMemoryRange *)(
        mmap + (section * mmap_desc_size));
    // dump the segment
    uint64_t start = segment->PhysicalStart;
    uint64_t size = segment->NumberOfPages * PAGE_SIZE;
    print_msg(STD, "[%016llx - %016llx] %s ", start, start + size,
              physmem_type_tostring(segment->Type));
    if (segment_accessible(segment)) {
      if (write_segment(segment, mem_dev, dump_file, start) == EXIT_FAILURE) {
        print_msg(STD, "Failed to dump segment %d\n", section);
        goto error;
      }
…<SNIP>


3. IOMemoryDescriptor 클래스의 메서드를 이용한 물리 메모리 추출
물리 메모리 맵을 확보하면, MacReponseLE와 같은 방법으로 물리 메모리를 덤프한다. OSXPMem이 등록한 'pmem' KEXT에 read 메시지를 전달하면, 내부적으로 'pmem_read'를 호출하며 이 함수는 결과적으로 다음 함수를 호출한다.

// Copy the requested amount to userspace if it doesn't cross page boundaries
// or memory mapped io. If it does, stop at the boundary. Will copy zeroes
// if the given physical address is not backed by physical memory.
//
// args: uio is the userspace io request object
// return: number of bytes copied successfully
//
static uint64_t pmem_partial_read(struct uio *uio, addr64_t start_addr,
                                  addr64_t end_addr) {
  // Separate page and offset
  uint64_t page_offset = start_addr & PAGE_MASK;
  addr64_t page = trunc_page_64(start_addr);
  // don't copy across page boundaries
  uint32_t chunk_len = (uint32_t)MIN(PAGE_SIZE - page_offset,
                                     end_addr - start_addr);
  // Prepare the page for IOKit
  IOMemoryDescriptor *page_desc = (
      IOMemoryDescriptor::withPhysicalAddress(page, PAGE_SIZE, kIODirectionIn));
  if (page_desc == NULL) {
    pmem_error("Can't read from %#016llx, address not in physical memory range",
               start_addr);
    // Skip this range as it is not even in the physical address space
    return chunk_len;
  } else {
    // Map the page containing address into kernel address space.
    IOMemoryMap *page_map = (
        page_desc->createMappingInTask(kernel_task, 0, kIODirectionIn, 0, 0));
    // Check if the mapping succeded.
    if (!page_map) {
      pmem_error("page %#016llx could not be mapped into the kernel, "
               "zero padding return buffer", page);
      // Zero pad this chunk, as it is not inside a valid page frame.
      uiomove64((addr64_t)pmem_zero_page + page_offset,
                (uint32_t)chunk_len, uio);
    } else {
      // Successfully mapped page, copy contents...
      uiomove64(page_map->getAddress() + page_offset, (uint32_t)chunk_len, uio);
      page_map->release();
    }
    page_desc->release();
  }
  return chunk_len;
}

이 함수는 인자로 받은 uio(User I/O) 구조체에 맵핑된 물리 메모리 정보를 복사한다. uio 구조체는 사용자가 애플리케이션의 버퍼이다. 단순 버퍼 복사로는 커널 메모리 영역의 데이터를 사용자 영역의 데이터로 전송할 수 없으므로, uiomove64()를 이용한다. 이 함수는 BSD에 있는 시스템 콜(system call)중 하나로, 커널 영역 버퍼의 정보를 사용자 영역의 버퍼로 복사한다.

결론적으로 OSXPMem과 MacResponseLE는 내부적으로 동일한 루틴을 가지지만, 기존 프로젝트와 다르게 DTrace 호출하지 않고 KEXT가 커널의 데이터에 접근 가능하다는 점을 이용하여 platform export를 활용하였다. OSXPMem은 앞으로도 지속적인 업데이트를 수행할 예정이니만큼, 기대해봐도 좋을 것 같다. ;-)