[Tech Report] 사라진 악성코드까지 추적한다
1부_카빙, 복구와 복원 : PE 카빙
|
카빙(carving)의 동사 원형인 ‘carve’의 사전적 정의는 ‘조각하다, 깎아서 만들다’로, 어떠한 덩어리를 조각하여 의미 있는 형태로 만드는 행위를 뜻한다. 컴퓨터 기술 용어인 ‘카빙’ 역시 어떤 덩어리, 즉 데이터에서 불필요한 부분을 제거하거나 삭제하고 필요한 부분을 합치는 등의 작업를 통해 애플리케이션이 읽을 수 있는 형태의 데이터를 만드는 작업이라 할 수 있다. 즉, 필요에 따라 데이터를 추출하고 재구성함으로써 파일을 복구하거나 시스템을 분석할 수 있어 복잡다단한 시스템과 나날이 고도화되는 공격을 대면하고 있는 분석가들에게 필수적인 기술이다.
|
< 연재 목차 >
1부_ 카빙, 복구와 복원 : PE 카빙(이번 호)
2부_ 카빙, 복구와 복원 : 이미지 카빙
3부_ 카빙, 복구와 복원 : 음성 카빙
4부_ 카빙, 복구와 복원 : 로그 카빙
이번 월간 안에서 카빙 방법과 활용을 살펴볼 대상은 PE(Portable Executable)다. 윈도우 운영체제에서 실행 가능한 파일로, 윈도우에서 동작하면서 서비스 및 기타 유용한 기능을 제공한다. 일반적으로 많이 사용하는 계산기, 그림판, 메모장 등도 PE 구조체로 이루어져 있다.
|
PE 포맷
|
이처럼 PE는 편리한 기능을 제공해 다양한 프로그램에 이용되지만 악성코드 제작에 악용되는 경우도 빈번하다. PE가 악성코드로 동작할 경우, 자기 자신을 숨기거나 공격 후 자기 자신을 삭제하기도 한다. 이 때문에 분석가들은 악성코드를 찾는데 실패할 수도 있고, 특히 해당 악성코드가 삭제된 경우에는 분석할 대상을 확인할 수 없는 경우도 있다. 바로 이럴 때 이용할 수 있는 방법이 카빙이다.
삭제된 데이터를 되살리는 카빙
그렇다면 PE 파일이 삭제된 경우에는 어떨까? OS에 의해 생성된 데이터나 기타 데이터가 삭제된 데이터의 자리를 덮어쓰지 않았다면, PE 파일 역시 삭제됐더라도 하드디스크에 남아있다. 남아있는 데이터는 그 형태를 알고 있다면 완벽히 복원할 수 있다. 복원을 위해 PE의 생김새(?) 먼저 알아보도록 하자.
생김새(?)를 알아야 카빙도 한다
데이터를 카빙하려면 우선 원하는 데이터가 어떻게 생겼는지, 어디에 위치했는지를 알아야 한다. 즉, PE 데이터를 임의의 덩어리에서 뽑아내기 위해서는 PE가 어떻게 생긴 것인지, 그 구조부터 알아야 한다. [그림 1]과 같이 PE는 크게 헤더(Header)와 바디(Body)로 나뉜다.
[그림 1] PE 기본 구조
PE 헤더는 DOS Header + DOS Stub + NT Header(File Header + Optional Header) + Section Headers를 포함하고 있으며, PE 바디는 여러 섹션(Section)을 포함하고 있다. 대략 [그림 2]와 같이 구성되어 있는 것으로 이해할 수 있다.
[그림 2] PE 구조체의 구성
그럼 지금부터 실제 헥스 에디터를 이용해 PE의 구조를 살펴보도록 하자. PE 구조체를 가진 데이터를 헥스 에디터로 열어보면, 육안으로 식별이 불가능한 숫자들이 가득하다. 그러나 하나씩 천천히 살펴보면 어떤 것이 중요한 부분인지 파악할 수 있다.
[그림 3] 헥스 에디터로 확인한 PE 구조 예시
PE 상세 구조
1. DOS Header
[그림 3]의 PE 구조체를 살펴보면, 처음 위치에 0x4D 0x5A라는 값을 확인할 수 있다. 이 값을 매직(Magic)이라고 하며, 아스키(ASCII) 값으로 MZ라고 표현된다. 이 데이터는 MZ를 기준으로 0x40만큼의 크기를 갖고 있다. 또한 0x3C 위치에서 DWORD로 0xE0 0x00 0x00 0x00라는 값을 갖고 있다. 이 값을 e_lfanew라고 하며, 리틀 엔디안(Little Endian)으로 0x000000E0이라고 읽는다. 이 값은 다음 구조체인 NT 헤더(Header)가 시작되는 위치를 가리킨다.
[그림 4] DOS 헤더(Header) 예시
2. DOS Stub
DOS 스터브(Stub)는 DOS에서 사용하는 코드를 담고 있다. 이 데이터는 존재 자체도 옵션으로 부여되고 사이즈 또한 일정하지 않다. 게다가 해당 데이터가 PE에서 확인되지 않아도 동작에는 전혀 지장이 없다.
[그림 5] DOS 스터브(Stub) 예시
3. NT Header
DOS 스터브 부분을 지나면 [그림 6]과 같이 아스키 값으로 ‘PE’라고 표시된 글씨를 확인할 수 있다. 이 지점부터가 NT 헤더(Header)라고 볼 수 있다. PE는 헥스(Hex) 값으로 0x50 0x45 0x00 0x00라는 값으로 표현된다. NT 헤더의 크기는 0xF8이며, 해당 데이터의 크기는 파일 헤더(File Header)와 옵셔널 헤더(Optional Header)를 포함한 크기이다. 즉, 이 데이터의 크기는 NT 헤더 시그니처(PE) + 파일 헤더 + 옵셔널 헤더 = 0xF8인 것이다.
[그림 6] NT 헤더(Header) 예시
4. File Header
NT 헤더 중 파일 헤더(File Header)는 ‘PE’라는 아스키 값으로 시작한다. 이 값은 0x50 0x45 0x00 0x00이며, NT 헤더의 시작이자 파일 헤더의 시작이 된다. 파일 헤더는 PE가 갖고 있는 섹션(Section)의 수와 옵셔널 헤더(Optional Header)의 크기를 담고 있다. 섹션 개수는 오프셋(offset) 0x7에 위치하며, 크기는 WORD이다.
[그림 7]에서 확인할 수 있는 바와 같이, 이 데이터의 섹션 개수는 0x05이다. 옵셔널 헤더(Optional Header)의 크기는 NT 헤더가 시작되는 부분으로부터 0x14 위치에 WORD값으로 존재한다. [그림 7]에서는 0x00 0xE0이며, 해당 값은 224이다.
[그림 7] 파일 헤더(File Header) 예시
5. Optional Header
NT 헤더 중 옵셔널 헤더(Optional Header)는 파일 헤더가 끝나는 부분에서 바로 이어진다. 이 크기는 [그림 7]에서 확인한 바와 같이 E0h이다. 옵셔널 헤더의 매직 값(Magic Value)은 32bit 또는 64bit에 따라 각기 다르다. 32bit는 10Bh, 64bit는 20Bh이다. 이 값을 리틀 엔디안으로 보면 0x0B 0x01, 그리고 0x0B 0x02가 된다. [그림 8]에서 확인할 수 있는 PE파일은 32bit로, 0x0B 0x01의 값을 갖고 있다.
한편, 옵셔널 헤더는 PE 헤더의 전체 크기에 대한 정보를 담고 있다. 이 정보의 위치는 옵셔널 헤더가 시작하는 곳으로부터 0x3C 위치에 있다. [그림 8]에서는 400h라는 값으로 표현되고 있다. 이를 통해 확인할 수 있는 PE 헤더의 전체 크기는 400h, 즉 1024이다.
[그림 8] 옵셔널 헤더(Optional Header) 예시
현재까지 파악한 내용을 통해 알 수 있는 이 데이터의 PE 헤더 크기는 [그림 9]와 같다.
[그림 9] PE 헤더 크기
PE 카빙과 관련해 한 가지 더 고려해야 하는 부분이 있다. 바로 섹션 얼라인먼트(Section Alignment)와 파일 얼라인먼트(File Alignment)이다. 섹션 얼라인먼트는 메모리에서 섹션이 차지하는 크기를 뜻하며 파일 얼라인먼트는 메모리에 올라가기 전, 파일 시스템에 남아있는 파일 안의 섹션 크기를 말한다. 따라서 카빙하고자 하는 대상이 메모리 파일인 경우 섹션 얼라인먼트를, 파일 시스템인 경우 파일 얼라인먼트를 참고하면 섹션의 크기를 확인 할 수 있다.
또한 섹션 얼라인먼트는 옵셔널 헤더의 0x20 위치에 WORD 값으로 표시된다. [그림 10]의 경우, 이 값은 0x00 0x10이다.
파일 얼라인먼트는 옵셔널 헤더에서 0x24 위치에 WORD 값으로 존재하며 값은 0x00 0x02이다. 즉 메모리 상에서 섹션은 1000h의 크기를, 파일 시스템에서는 200h의 크기를 갖는다는 것을 알 수 있다.
[그림 10] 옵셔널 헤더에 표시되는 섹션 얼라인먼트 및 파일 얼라인먼트
6. Section Header
섹션은 1개 이상 존재하며, 섹션의 수는 NT 헤더의 파일 헤더에서 확인할 수 있다. 일반적으로 코드(CODE), 데이터(DATA), 리소스(RESOURCE) 등의 섹션으로 나뉘며 각 섹션의 사이즈는 0x28이다. 만약 섹션의 수가 3개일 경우, 섹션 헤더(Section Header)의 크기는 0x28x3이라고 생각하면 된다.
[그림 11] 섹션 헤더(Section Header) 예시
섹션 헤더 뒤에는 섹션들이 붙어 있다. 이 같은 섹션들을 PE 바디(PE Body)라고 부르며, 해당 데이터의 크기를 모두 구하려면 각 섹션 헤더의 처음으로부터 0x10만큼 이동한 곳의 DWORD 값을 확인해야 한다. PE 바디를 구성하고 있는 5개 섹션의 총 크기를 구하면 [그림 12]와 같다.
| PE Body text == E600h rdata == 2A00h data == 1200h rsrc == 200h reloc == 1200h |
[그림 12] PE 바디(PE Body)내 5개 섹션의 크기 총합
PE 카빙을 위한 툴 작성 방법
지금까지 카빙에 필요한 PE의 속성을 모두 살펴봤다. 이를 토대로 이제 임의의 더미 파일에서 PE만을 카빙하는 도구를 제작해 보도록 하자. 이를 위해 먼저 PE 속성 중 어떤 부분을 확인해야 하는지 간략하게 정리하면 다음과 같다.
① MZ로 PE 시작 부분 확인
더미 파일 안에서 PE의 시작인 MZ를 찾는다. 실제로 찾는 값은 헥스 값인 0x4D 0x5A이다.
② e_lfanew로 NT 헤더 확인
e_lfanew의 값(Offset 0x3C)을 확인한 후, 이 값이 아스키 값의 PE인지 확인한다. 아스키 값 PE는 헥사 값으로 0x50 0x45 0x00 0x00이다.
③ 섹션 수 확인
섹션 수는 파일 헤더의 첫 부분으로부터 오프셋(Offset) 0x06을 이동 후, WORD 크기로 확인할 수 있다. 위치 계산 방법은 다음과 같다.
④ 옵셔널 헤더 크기
옵셔널 헤더의 크기는 파일 헤더의 시작부터 Offset 0x14 이동 후 WORD 크기로 확인할 수 있다. 위치 계산 방법은 다음과 같다.
⑤ 옵셔널 헤더 증명
옵셔널 헤더의 시작이 0x0B 0x01이나 0x0B 0x02인지 확인한다. 오프셋의 위치 계산은 아래와 같다.
⑥ PE 헤더 크기
옵셔널 헤더의 시작부터 오프셋 0x3C만큼 이동하면 PE 헤더의 크기를 얻을 수 있다. 위치 계산 방법은 아래와 같다.
⑦ 섹션 헤더 위치
섹션의 크기를 알기 위해서는 섹션 헤더로 이동해야 한다. 섹션 헤더는 NT 헤더 다음에 위치한다. 위치 계산 방법은 아래와 같다.
⑧ 섹션 헤더의 크기
섹션 헤더의 크기는 각각 0x28이다. 섹션 수 x 0x28을 하면 섹션 테이블의 전체 크기를 알 수 있다.
⑨ 섹션의 크기
섹션의 크기는 섹션 헤더에 존재한다. 각각의 섹션 헤더의 시작으로부터 Offset 0x10을 이동 후 DWORD 크기로 확인 할 수 있다.
위의 항목들에 대한 확인을 마친 후 PE 카빙을 수행하기 위해 파이썬으로 간단한 프로그램을 작성했다. 관련 링크와 해당 코드는 아래와 같다.
*참고 링크 : https://dorumugs-tools.googlecode.com/files/PE_Carver.py
|
import optparse def Timestamp(epoch=None): def LogLine(line):
def Data2File(data, filename):
NT_Header = index + e_lfanew_value_little verifyOptional_Header = binascii.hexlify(data[NT_Header+24:NT_Header+26]) if '50450000' == binascii.hexlify(data[NT_Header:NT_Header+4]) and \ sizeOfOptional_Header = binascii.hexlify(data[NT_Header+20:NT_Header+22]) sizeOfPE_Header = binascii.hexlify(data[NT_Header+84:NT_Header+88]) if len(sizeOfOptional_Header) > 0: totalLength = sizeOfPE_Header_big + totalSizeOfSection filenameExists = os.path.exists(filename) resultDir = str(os.path.abspath(filename)) + "\\" + str(index) print "MZ Offset " + str(index) index = index + int(bSize)
return True
if options.input == False or options.bsize == False or options.output == False: #filenames = sum(map(glob.glob, filenames), []) ExtractPEFromFile(rData, bSize, folder, options.output) if __name__ == '__main__': |
위의 도구를 이용해 카빙된 데이터는 아웃풋 폴더(output folder)에 쌓인다. 이때 각 파일의 이름은 카빙된 시작 오프셋으로 부여된다.
|
사용법 : python PE_Carver.py -i input_dummy -b block_size -o output_folder |
*예제 : python PE_Carver.py -i ntfs_dd -b 1024 -o result
[그림 13] PE 카빙 툴에 의해 카빙된 데이터
카빙된 파일의 활용
대상 파일의 구조부터 구조별 항목 확인까지 녹록지 않은 과정을 거쳤다. 이제 카빙을 통해 얻은 결과물을 활용하는 방법에 대해 살펴 보자.
(1) 삭제된 파일 복구 및 악성 여부 점검
카빙을 수행하면 삭제된 파일도 복구가 가능하다. 이렇게 복구된 파일은 악성 여부의 판별을 위해 백신 프로그램을 이용해 점검한다.
[그림 14] 복구된 파일에 대한 악성 여부 검사
(2) APT 공격 등의 연관성 파악
PE 안에는 컴파일된 시간 정보가 남아 있다. 해당 파일이 언제 만들어졌는지 알 수 있는 것이다. 이를 통해 침해사고 발생 시, 공격과 관련한 파일의 추적이 가능하다. 예를 들어, 만약 PE를 통해서 APT 공격을 받았을 경우 최근 컴파일된 시간 정보를 이용해 해당 파일이 APT 공격에 이용된 악성코드인지를 판별할 수 있다.
[그림 15] PE에 존재하는 시간 정보
PE 카빙으로 공격의 실체를 파악하는 단서를 얻는다
지금까지 임의의 데이터를 깎아서 PE를 만들어내는, 즉 PE 카빙 방법을 살펴봤다. 파일 시스템에서 PE를 카빙할 경우, 삭제된 PE 및 존재하는 PE에 대해서도 악성 여부를 점검하고 판단할 수 있다. PE 카빙이 APT 공격으로 침해를 입은 시스템에서 흔적도 없이 사라진 악성코드를 발견할 수 있는 열쇠가 될 수도 있다는 의미다.
이처럼 하나의 파일이 전체적인 분석의 실마리를 제공하기도 하는 만큼 카빙을 통해 파일을 복구하고 분석하는 것은 포렌식에 있어 매우 중요하다. 특히 최근 자주 등장하는 공격 이후 자기 자신을 삭제하는 악성코드를 카빙을 이용해 복구할 수도 있으므로 카빙을 이용해 분석하는 것을 강력히 추천한다.@
- AhnLab클라우드분석팀 소재현 주임 연구원