on
Mach-O binary dump on memory image for malware analysis
시작
악성코드 분석 기술 수준이 높아지는 만큼, 악성코드의 기능도 고도화되고 있다. 특히, 새로운 기능을 개발하기 보다는 자신의 흔적을 지우거나, 코드 분석을 까다롭게 만드는데 초점을 두고 있다. 이는 Mac OS X라고 예외가 아니다. Mac OS X에서도 자기 자신을 디스크에 존재하지 않게 하거나, 명령어 기반의 분석을 수행하는 라이브 포렌식의 결과를 위변조하여 자기 자신을 숨기기, 접속 도메인과 같은 주요 설정 정보를 암호화하는 등 다양한 방법을 이용한다. 기존 침해대응에서는 악성코드를 숨기지 못하는 영역 중 하나인 디스크를 분석하여 악성코드를 추출하였다. 이 경우 루트킷의 파일 은닉 기법으로 숨겨진 악성코드를 쉽게 찾아낼 수 있으나, 다양한 코드 분석 방해 기법이 적용된 악성코드는 분석이 까다로운 문제점을 가지고 있다. 수업 시간에 컴퓨터 아키텍처 수업을 들은 적이 있다면, 어떠한 데이터가 CPU에 의해 처리되려면 해당 정보가 메모리에 로드되야 한다는 점을 알고 있을 것이다. 즉, 구동 중인 코드는 항상 메모리에 존재하므로 악성코드도 이 점을 피해갈 순 없다. 메모리 포렌식은 이러한 점을 이용하여 메모리에서 악성코드를 추출한다.
Mac OS X의 프로세스 메모리 관리
메모리에서 악성코드를 추출하려면, Mac OS X 커널이 각 프로세스의 가상메모리 영역을 어떻게 관리하는지 알아야 한다. Mac OS X는 프로세스 정보는 BSD 컴포넌트의 proc 구조체로 관리 하지만, 이에 대한 가상 메모리 맵은 Mach 커널에서 관리한다. Proc 구조체와 가상 메모리 맵이 바로 연결되지 않고, proc 구조체 당 mach 커널의 task 구조체가 1:1로 맵핑(mapping)되어 있고, 이 구조체와 가상 메모리 맵이 연결되어 있다.
커널 구조체를 보면, 가상 메모리 맵(Virtual Memory Map) 구조체에는 가상 메모리 맵 헤더(vm_map_header)와 가상 주소를 물리 주소로 맵핑하기 위한 CR3 값을 가지는 pmap 구조체, 가상 메모리의 총 크기 정보 등을 가지고 있다. 여기에서 다루는 모든 커널 소스는 Mac OS X 매버릭스(Mavericks)를 기준으로 하지만, 이전 버전도 크게 다르지 않다.
struct _vm_map { lock_t lock; /* uni- and smp-lock */ struct vm_map_header hdr; /* Map entry header */ #define min_offset hdr.links.start /* start of range */ #define max_offset hdr.links.end /* end of range */ #define highest_entry_end hdr.highest_entry_end_addr pmap_t pmap; /* Physical map */ vm_map_size_t size; /* virtual size */ vm_map_size_t user_wire_limit;/* rlimit on user locked memory */ vm_map_size_t user_wire_size; /* current size of user locked memory in this map */ int ref_count; /* Reference count */ #if TASK_SWAPPER int res_count; /* Residence count (swap) */ int sw_state; /* Swap state */ #endif /* TASK_SWAPPER */ decl_lck_mtx_data(, s_lock) /* Lock ref, res fields */ lck_mtx_ext_t s_lock_ext; vm_map_entry_t hint; /* hint for quick lookups */ vm_map_entry_t first_free; /* First free space hint */ unsigned int /* boolean_t */ wait_for_space:1, /* Should callers wait for space? */ /* boolean_t */ wiring_required:1, /* All memory wired? */ /* boolean_t */ no_zero_fill:1, /*No zero fill absent pages */ /* boolean_t */ mapped_in_other_pmaps:1, /*has this submap been mapped in maps that use a different pmap */ /* boolean_t */ switch_protect:1, /* Protect map from write faults while switched */ /* boolean_t */ disable_vmentry_reuse:1, /* All vm entries should keep using newer and higher addresses in the map */ /* boolean_t */ map_disallow_data_exec:1, /* Disallow execution from data pages on exec-permissive architectures */ /* reserved */ pad:25; unsigned int timestamp; /* Version number */ unsigned int color_rr; /* next color (not protected by a lock) */ #if CONFIG_FREEZE void *default_freezer_handle; #endif boolean_t jit_entry_exists; } ;
가상 메모리 맵 헤더에는 가상 메모리 맵 링크(vm_map_links)가 있으며, 가상 메모리 맵 엔트리의 갯수에 대한 정보를 가지고 있다. 가상 메모리 맵 링크는 가상 메모리 맵 링크에 각 가상 메모리 맵 엔트리(vm_map_entry)를 이중 연결 리스트(Double Linked List) 형태로 관리하고 있으며, 총 가상 메모리의 시작 주소와 끝주소를 저장한다. 각 가상 메모리 맵 엔트리에는 사용하는 가상 메모리의 시작 주소와 끝 주소, 엔트리의 속성(공유 메모리, 접근 권한(rwx) 등) 정보를 담고 있다. 커널 구조체를 분석하여 가상 메모리 관리 방법을 파악할 수 있다. 아래 그림은 앞 서 설명한 가상 메모리 관리 방식을 그림으로 표현한 것이다.
volafox에서는 ps 명령어로 프로세스의 가상 메모리 영역을 추출할 수 있다.
$ python vol.py -i ../test.mem -o ps -x 2850 [fusion_builder_container hundred_percent="yes" overflow="visible"][fusion_builder_row][fusion_builder_column type="1_1" background_position="left top" background_color="" border_size="" border_color="" border_style="solid" spacing="yes" background_image="" background_repeat="no-repeat" padding="" margin_top="0px" margin_bottom="0px" class="" id="" animation_type="" animation_speed="0.3" animation_direction="left" hide_on_mobile="no" center_content="no" min_height="none"][+] Process List OFFSET(P) PID PPID PRIORITY NICE PROCESS_NAME USERNAME(UID,GID) CRED(UID,GID) CREATE_TIME (UTC+0) 0x161C0C540 2850 534 255 0 PluginProcess chainbreaker(501,20) (501,20) Thu Oct 24 08:24:18 2013 [+] Resetting the Page Mapping Table: 0x25402d000 [+] Process Dump Start [-] [DUMP] Image Name: 2850-PluginProcess-10f58d000-10f58e000 [-] [DUMP] Image NaPluginProcess-10f58e000-10f58f000 [-] [DUMP] Image Name: 2850-PluginProcess-10f58f000-10f591000 [-] [DUMP] Image Name: 2850-PluginProcess-10f592000-10f593000 [-] [DUMP] Image Name: 2850-PluginProcess-10f593000-10f594000 [-] [DUMP] Image Name: 2850-PluginProcess-10f594000-10f595000 [-] [DUMP] Image Name: 2850-PluginProcess-10f595000-10f596000 [-] [DUMP] Image Name: 2850-PluginProcess-10f596000-10f597000 [-] [DUMP] Image Name: 2850-PluginProcess-10f597000-10f598000 [-] [DUMP] Image Name: 2850-PluginProcess-10f598000-10f59a000 [-] [DUMP] Image Name: 2850-PluginProcess-10f59c000-10f5b1000 [-] [DUMP] Image Name: 2850-PluginProcess-10f5b3000-10f5c8000 [-] [DUMP] Image Name: 2850-PluginProcess-10f5c9000-10f5ca000 [-] [DUMP] Image Name: 2850-PluginProcess-10f5ca000-10f5cb000 [-] [DUMP] Image Name: 2850-PluginProcess-10f5cc000-10f5e1000 [-] [DUMP] Image Name: 2850-PluginProcess-10f5e3000-10f5f8000 [-] [DUMP] Image Name: 2850-PluginProcess-10f5f9000-10f5fc000 [-] [DUMP] Image Name: 2850-PluginProcess-10f5fc000-10f5fd000 [-] [DUMP] Image Name: 2850-PluginProcess-10f5fd000-10f5fe000 [-] [DUMP] Image Name: 2850-PluginProcess-10f5fe000-10f5ff000 [-] [DUMP] Image Name: 2850-PluginProcess-10f5ff000-10f600000 [-] [DUMP] Image Name: 2850-PluginProcess-10f600000-110600000 [+] Process Dump End $
프로세스 덤프
사실 프로세스 메모리 영역 전체를 덤프하는 것이 프로세스의 스택이나 힙영역의 데이터도 분석이 가능하므로 더 유용할 수 있다. 하지만 악성코드 분석 좀 해본 사람이라면, 이렇게 추출한 프로세스 메모리를 IDA와 같은 디스어셈블러에서 제대로 해석하지 못한다는 걸 알 수 있을 것이다. 디스어셈블러는 바이너리 파일을 분석하는 도구이지, 프로세스 메모리 덤프를 분석하는 도구가 아니기 때문이다.
지금 이 말이 무슨 말인지 이해를 못하는 분들을 위해 간단히 설명하면, 윈도우 뿐만 아니라 대부분의 운영체제에서 실행 파일 포맷을 보면, 실제 파일의 각 섹션의 주소와 메모리에 로드될 때 할당되는 주소가 다르다. 이렇게 주소가 다른 이유는 메모리 맵핑을 최적화하기 위함이다. 그로인해 가상 메모리 맵 엔트리를 그냥 주소 순서대로 붙이게 되면, 뒤에 붙는 섹션일 수록 데이터의 오프셋이 틀어지게 되어, 데이터를 올바르게 해석하지 못하게 된다. 특히 PE의 IAT나 mach-o의 symbol 섹션은 바이너리 뒷 부분에 붙기 때문에 메모리에서 덤프한 바이너리의 함수를 맵핑하지 못하는 문제가 발생한다. (물론 심볼 정보가 필요 없다고 판단하여 페이지가 덮어씌워지는 경우도 있다.) 이 작업을 하려면 Mach-O 파일 포맷에 대한 이해가 필요하다. 하지만 그 내용까지 이 포스팅에서 다루기엔 분량이 너무 많으므로, 추 후 별도의 페이지를 통해 Mach-O 포맷 분석 방법을 정리하겠다.
분석이 용이한 바이너리 형태로 재구성하기 위해선 각각의 섹션의 가상 주소(virtual address)와 가상 크기(virtual size)를 섹션 오프셋(file offset)과 섹션 크기(size)에 맞춰줘야 한다. volafox에서는 이 과정을 machdump로 수행할 수 있다.
$ python vol.py -i ../test.mem -o machdump -x 2850 [+] Process Dump Start [-] Find 64 bit Mach-O signature at 10f58d000 [-] from 10f58d000 to 10f58e000 [-] from 10f58e000 to 10f58f000 [-] from 10f58f000 to 10f590be0 [-] [DUMP] Image Name: 2850-PluginProcess-10f58d000 [+] Process Dump End $
덤프한 바이너리를 디스어셈블러(IDA)로 확인하면 정상적인 분석이 가능함을 알 수 있다.
유용한 점
메모리 분석을 통해 악성코드를 덤프하는 방법은 다음과 같은 상황에 유용하다.
1. 팩킹(packing)된 악성코드의 언패킹된 코드 획득
패킹된 악성코드는 메모리에 로드하면서 원본 코드를 언패킹하여 원본 코드를 실행한다. 문제는 이러한 패킹 기술에 안티 디버깅이나 디스어셈블러 기술이 적용되어 있다보니, 악성코드 분석가들이 기능 분석 보다는 패커와 씨름에 기력을 다 소진하게 된다. 메모리 분석을 통해 악성코드를 덤프할 경우, 이미 언팩되어 원본코드가 드러난 악성코드의 메모리 영역을 덤프하기 때문에 분석가들의 분석 시간이 많이 절약될 수 있다. 단, 최근에는 원본 코드 전체를 언패킹하는 것이 아니라, 일부를 필요할 때 언패킹하여 활용하는 경우도 있다고 하니 상황에 맞게 활용하는 것이 좋다.
2. 암호화된 악성코드의 복호화된 코드 획득
Mac OS X는 패커가 거의 전무하기 때문에 도메인, 아이피, 포트와 같은 정보를 숨기기 위해 암호화 기능을 사용한다. 보통 설정 정보는 코드 실행 초기에 복호화 과정을 거치기 때문에 메모리에서 악성코드를 덤프하여 복호화된 정보를 획득할 수 있다.
3. 메모리에만 존재하는 악성코드 획득
자기 자신을 실행하고 디스크 상의 파일을 삭제하는 경우, 이를 메모리에서 획득할 수 있다. 물론 아직까지 Mac OS X에서 메모리에만 존재하는 악성코드가 발견된 사례는 없는 걸로 알려져 있다.
한계점
현재 프로세스 추출을 위한 machdump 기능은 메모리에 로드된 악성코드의 특정 영역이 페이지 아웃 된 경우에는 물리 메모리에서 추출할 수 없는 문제를 가지고 있다. 이 경우에 스왑 파일(swapfile)이나 해당 바이너리에서 페이지를 불러와야 한다. 아직 스왑 데이터 분석 기능이 추가되지 않았기 때문에 페이지 아웃된 영역을 추출할 수 없는 문제점을 가지고 있다. 특히, 심볼 정보가 날라간 경우에는 프로세스를 완벽하게 덤프하더라도 디스어셈블러에서 함수 분석에 애로사항이 꽃필 수 있다.
결론
메모리 포렌식 기술이 발전하는만큼, 예전처럼 단순히 데이터 수집만 하는 것이 아니라 어떻게 악성코드를 효과적으로 추출하고 분석할 수 있는지에 대한 연구가 진행되고 있다. 메모리에서 덤프한 악성코드는 언팩된 코드나 복호화된 정보를 손쉽게 획득할 수 있어서 악성코드 분석가에게 여러모로 도움이 될 것이라 생각한다. 단, 아직 페이지 아웃된 데이터로 인한 바이너리 분석 이슈나, 몇몇 예외 상황이 발생함으로 분석에 문제가 생기는 케이스가 있기 때문에 좀 더 많은 연구가 필요하다.[/fusion_builder_column][/fusion_builder_row][/fusion_builder_container]