
- TDL4 부트킷 상세 분석 과정과 기늠
연재 목차
1. TDL4 부트킷의 유래 및 배포 과정, 특징(2011년 10월호)
2. TDL4 부트킷 상세 분석 과정과 기능(2011년 11월호)
3. 감염된 MBR 분석과 수행 기능(2011년 12월호)
4. TDL4 부트킷 치료와 예방 방법(2012년 1월호)
지난호에서는 TDL4 부트킷의 유래와 배포 과정, 그 특징에 대해 알아봤다. 이번호에서는 TDL4 코드 감염에 따른 부팅 시 재감염에 대해 상세히 살펴본다. 지난호에서 살펴봤듯이, TDL4는 윈도우 비스타 이후 버전과 윈도우 XP, x64, x86에 따라 감염 루틴이 달라진다. 하지만 실제 동작하는 드라이버의 코드는 사실상 같다(32비트 코드를 64비트 코드로 컴파일한 것처럼 보인다). 따라서 악성 TDL4 드라이버의 기능은 같은 것으로 보인다. 드라이버를 드롭하는 악성코드와 은폐 및 자기 보호, 감염된 MBR이 수행하는 기능에 대해 상세히 알아본다.
1. TDL4 드로퍼
TDL4 드로퍼(Dropper)는 각각 전혀 다른 바이너리를 가진 Custom Packer로 돼 있다. 해당 Packer를 분석하면 실제 코드가 나오는데, 감염 시스템의 OS 버전을 체크한다. 윈도우 비스타나 윈도우 7에서 관리자 권한을 얻기 위해 MS10-092 취약점을 이용한다.
드로퍼는 [그림 1]과 같이 동작한다.

[그림 1] TDL4 드로퍼 동작
MS10-092는 Task Scheduler 취약점을 이용하는데, 윈도우 비스타 이후 버전의 운영체제에서 Task Scheduler는 환경 설정 정보가 있는 XML 파일 형태로 Job을 등록한다. Job 환경 설정 파일은 일반적으로 %SystemRoot%\system32\TaskFolder\에 생성되며, Job Type, 실행 파일 Path, 실행 파일의 아규먼트, 실행 권한 등이 포함된다.
위와 같이 Job 환경 파일을 보호하기 위해 윈도우에서는 CRC32 CheckSum을 계산하여 비교한다. 비교된 값이 동일해야만 실행이 가능하다.
해당 취약점을 실행시키는 시나리오는 아래와 같다.

[그림 2] MS10-092 취약점 시나리오
Job 환경 파일을 아래와 같이 변경한다.
[그림 3] 변경된 Job 파일
LocalSystem의 권한으로 등록된 실행 파일이 실행된다. 변경된 Job 환경 파일에 아래와 같은 이상한 문자열이 등록되는데, 이것은 reversing한 CRC 값이 추가된 것이다.

[그림 4] reversing CRC
휴리스틱 진단을 우회하기 위해, ZwconnectPort() API를 아래와 같이 후킹한다.

[그림 5] ZwconnectPort() API 후킹
후킹 코드는 아래와 같다.

[그림 6] ZwconnectPort() API 후킹 코드
TDL4 드로퍼는 메모리를 할당하고 자신의 DLL 이미지를 복사한다. 해당 DLL를 로드하기 위해 AddPrintProvidor()를 실행해 Sploove.exe에 인젝션한다. 해당 API는 아래와 같다.

[그림 7] AddPrintProvidor API
각 파라미터의 의미는 아래와 같다.

TDL4는 Level 1을 사용하고 PROVIDOR_INFO_1.pDLLName에 인젝션할 DLL 이름을 넣으면 된다. 해당 기법은 TDL3에서도 사용됐다.

[그림 8] AddPrintProvidor() 호출
Sploove.exe에 인젝션된 코드는 실제 TDL4 감염 기능을 수행한다. 일단 %TEMP% 디렉터리에 TDL4 드라이버를 드롭한 후, NtLoadDriver() API를 이용해 해당 드라이버를 로드한다.

[그림 9] TDL4 드라이버 로드
참고로 TDL4가 64비트 시스템에서 아래와 같이 동작한다.

[그림 10] TDL4 64비트 드로퍼
2. TDL4 드라이버
로드된 드라이버는 메모리를 할당하여 실제 TDL4 기능을 수행하는 드라이버를 복호화한다. 또한 로더 역할을 하여 복호화된 드라이버를 로드한다.
로드 역할을 수행하기 위해 ntoskrnl.exe의 Image Address를 얻어오는데, LDR_DATA_TABLE_ENTRY의 링크드 리스트를 이용해 BaseDllName과 이름을 비교한다.

[그림 11] Get ntoskrnl.exe Image Address
얻어온 ntoskrnl.exe의 Image Address를 이용해, 필요한 API의 주소를 얻는다.
이후 할당한 메모리 영역에 PE 파일 형태로 TDL4 드라이버 파일을 생성한다.

[그림 12] 복호화된 드라이버 이미지
[그림 13]과 같이 복호화한 드라이버를 로드한다.

[그림 13] 실제 기능을 수행하는 TDL4 드라이버 엔트리 진입
TDL4 Real Driver Entry
진입한 드라이버 엔트리는 아래와 같은 코드를 갖고 있다.

[그림 14] 실제 기능을 수행하는 TDL4 드라이버 코드
디스크 넘버 얻기
코드에서는 해당 C 드라이버의 물리 드라이버 정보를 이용해 해당 디바이스 이름과 핸들을 얻어온다.
먼저 "\\??\\c:" 파일 핸들을 얻어온다.

[그림 15] "\\??\\c:" 파일 핸들 얻기
얻어온 파일 핸들을 가진 볼륨 정보를 얻기 위해 ZwDeviceIoControlFile()을 호출한다. IOCTL_CODE가 0x560000으로, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS로 정의돼 있다.

[그림 16] IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS
아래와 같이 구조체에 대한 정보를 얻을 수 있다.

[그림 17] _VOLUME_DISK_EXTENTS 구조체
아래와 같이, _DISK_EXTENT 구조체의 디스크 넘버를 얻을 수 있다.
![]()
[그림 18] 디스크 넘버 얻기
위의 문자열을 통해 "\??\physicaldrive0"에 대한 핸들을 얻어온다.
언파티션드 영역 물리적 주소 얻기
TDL3와 TDL4 모두 언파티션드(Unpartitioned) 영역에 자신의 모듈을 저장한다. TDL3에서 언파티션드 영역의 위치를 구하기 위해, MBR 정보를 활용했다.
하지만 TDL4는 아주 간단하게 ZwDeviceIoControlFile()를 호출해 언파티션드 영역의 위치를 얻어온다.
IOCTL_CODE 0x4D014 IOCTL_SCSI_PASS_THROUGH_DIRECT란 IOCTL_CODE를 이용하면, 아래와 같은 구조체를 통해 디스크에 읽고 쓰기가 가능해진다.
TDL3에서 _SCSI_REQUEST_BLOCK 구조체를 이용하여, 언파티션드 영역에 자신의 모듈을 저장했다.

[그림 19] _SCSI_PASS_THROUGH_DIRECT 구조체
CDB(command descriptor block)를 통해 디스크 관련 명령을 수행할 수 있다. _SCSI_REQUEST_BLOCK 구조체에 관련해서는 나중에 설명하겠지만, 역시 이 CDB를 통해 명령을 수행할 수 있다.
아래는 TDL4에서 위와 같은 기능을 수행하는 코드이다.

[그림 20] DirectDiskAccessLBA 수행 코드
DirectDiskAccessLBA는 개인적으로 명명한 함수이며, 아래와 같은 파라미터를 가진다.

[그림 20]에서 첫번째 DirectDiskAccessLBA 함수 중 SCSI_Command가 0x25인 걸 알 수 있다. SCSI Read Capacity Command(0x25)를 이용해, 하드 드라이브의 마지막 크기를 알아내고, 다음 섹터부터 언파티션드 영역이라 생각하고, 한 섹터씩 반복해서 읽어본다. 그러다 에러 코드를 보내주면 언파티션드의 가장 마지막이라 생각한다.
위키피디아를 참조하면, 0x25는 SCSI Read Capacity Command로 설명돼 있다. 타깃 디바이스의 데이터 용량의 정보를 알 수 있다. 아래는 CDB(Command Descriptor Block) 구조이다.

[그림 21] SCSI Read Capacity Command
아래와 같이 채워진 구조체를 ZwDeviceIoControlFile() 호출한다.

DataBuffer(0xf89aba78)에 해당 디스크 섹터의 크기가 구해진다.

[그림 22] SCSI Read Capacity Command
이렇게 구해진 디스크의 마지막 섹터부터 1섹터씩 읽어가며, 에러를 리턴할 때까지 지속해서 읽어본다. 에러 리턴하는 섹터의 바로 전 섹터가 언파티션드 영역의 마지막 섹터인 걸 알 수 있다.
[그림 20]에서 두번째 DirectDiskAccessLBA 함수의 파라미터 중 Mode가 0x28이다. 0x28은 SCSI Read Commands로 LBA 위치를 크기만큼 읽어 버퍼에 저장할 수 있다.
이렇게 언파티션드 영역의 마지막 섹터 위치가 확인되면 TDL4는 해당 영역에 읽기/쓰기 테스트를 한다.
SRB를 이용한 물리적 주소로 데이터 바로 쓰기
읽기는 SCSI Read Commands(0x28)를 이용하며, 쓰기는 TDL3에서 사용했던 기능과 같이 _SCSI_REQUEST_BLOCK 구조체와 CDB 구조체를 구성한 후 Atapi.sys(또는 FileSystem Mini Filter Driver)의 INTERNAL_DEVICE_CONTROL을 호출한다. 물론 IoCaller()는 직접 구현했다.
아래는 TDL4에서 구현된 쓰기 전용 CallDriver Function이다.

해당 기능을 수행하는 코드는 아래와 같다.

[그림 23] SCSI 쓰기 기능 코드
해당 코드는 일단 IRP 구조체를 구성한다. 이후 IO_STACK_LOCATION 구조체를 구성하는데 파라미터 부분에 Scsi – srb에 아래와 같이 _SCSI_REQUEST_BLOCK 구조체를 설정한다.
![]()
[그림 24] IO_STACK_LOCATION 구조체 파라미터
구성된 _SCSI_REQUEST_BLOCK 구조체는 아래와 같다.

[그림 25] _SCSI_REQUEST_BLOCK 구조체
[그림 25]와 같이 구성된 구조체들을 Atapi.sys의 INTERNAL_DEVICE_CONTROL로 호출하면, 설정된 LBA(Logical Block Address) 위치로 데이터가 쓰여진다.

[그림 26] TDL4 언파티션드 영역 테스트
[그림 26]과 같이 임의의 데이터를 언파티션드 영역에 쓰고, 읽어오는 테스트 작업을 수행한다. 이와 같은 방법으로 정상 MBR을 백업하고, 악성 MBR로 덮어쓴다.

[그림 27] 감염된 MBR과 정상 MBR 비교
PnpManager를 이용한 디스크 IO 구성
MBR을 감염시킨 TDL4는 PnpManager의 드라이버에 자신의 디바이스를 생성하고 VPB(Volume Parameter Block)에 TDL4 디바이스를 설정하는데, 이는 디스크 IO의 역할을 수행하기 위해서다.
먼저 TDL4는 “\driver\pnpmanager” DriverObject를 구한 후, 새로운 디바이스를 생성한다. 생성된 디바이스에 VBP를 구성한 후 NextDevice의 링크드 리스트를 수정하여 은폐한다.
![]()
[그림 28] TDL4 VBP 구성 코드
아래는 PnpManager에 생성된 디바이스와 구성된 VPB이다.

[그림 29] PnpManager에 생성된 디바이스와 VBP 설정
[그림 29]에서 보듯이, DriverObject의 주소는 PnpManager이며, VPB 구조체의 주소가 설정돼 있으며, Device Object list에서 해당 Device Object를 제거하여 은폐한다.
아래는 생성된 VBP 구조체이다.

[그림 30] VPB 구조체
VPB 구조체의 Flags 필드의 의미는 아래와 같다.

VPB 구조체 중 Device Object 필드가 TDL4 드라이버가 가지고 있는 Device Object를 가리킨다. VPB(Volume Parameter Block)는 마운트된 볼륨을 나타내는 파일 시스템 드라이버의 Device Object로 파일 시스템의 데이터 구조를 포함하는 물리 디스크나 가상 디스크를 나타내는 디바이스 오브젝트 사이의 연결을 나타낸 것이다.
즉, 물리/가상 파일 시스템 볼륨 객체를 Device Object로 표현하기 위해 매핑되는 구조체이다.

[그림 31] TDL4 Disk IO 구성
위에서 설명한 내용을 [그림 31]과 같이 표현할 수 있다. TDL4는 언파티션드 영역에 자신이 필요로 하는 모듈을 저장하고, 일부 모듈은 유저 모드에서 읽기/쓰기 권한이 있다.
이때, 위와 같이 생성된 가상 볼륨을 통해 접근이 가능하다. 아래서도 설명하겠지만, 유저 모드에서 ZwOpenSymblicLinkObject로 접근할 수 있다.

[그림 32] 유저 모드에서 ZwOpenSymblicLinkObject 호출
TDL4 Driver Object 생성 및 MajorFunction 등록
TDL4는 IoCreateDriver()라는 Undocument Function을 이용하여 TDL4 악성 기능을 수행하는 Driver Object를 생성한다.

[그림 33] IoCreateDriver로 TDL4 Driver Object 생성
생성된 TDL4 Driver Object 아래와 같은 MajorFunction을 등록한다.

[그림 34] TDL4 Driver Object 구성
TDL4 Driver Object는 로드된 Atapi.sys(File System Minifilter Driver)의 Driver Object를 그대로 복사한다. 따라서 TDL4의 DriverName이 Atapi.sys가 된다. 하지만 MajorFunction은 자신의 Function 주소로 등록한다.
위와 같은 설정된 TDL4 Driver Object는 유저 모드에서 언파티션 영역으로 접근할 때 사용된다. 또한 Atapi.sys의 StartIo를 자신의 코드로 변경하여 후킹한다. 각 MajorFunction은 아래와 같이 분기된다.

[그림 35] TDL4 Driver Object Major Function 분기
위의 MajorFunction 중 DeviceIoControl 및 INTERNAL_DEVICE_CONTROL일 경우에 IRP와 관련된 구조체를 조작 후 실제 Atapi.sys의 INTERNAL_DEVICE_CONTROL로 보낸다

[그림 36] TDL4 Driver INTERNAL_DEVICE_CONTROL 호출
Work Thread 및 LoadImage CallBack Function 등록
TDL4에서 PsSetLoadImageNotifyRoutine() 함수를 이용하여, 프로세스가 호출될 때마다 등록된 CallBack 함수를 호출한다. 호출된 CallBack 함수는 프로세스가 로드될 때마다, APC를 이용하여 Cmd.dll(or x64 cmd64.dll)를 인젝션한다.
ExQueueWorkItem()을 이용하여 Work Thread를 생성하고 Atapi.sys의 StartIo() MajorFunction을 후킹하는데, 해당 Work Thread가 후킹을 보호하는 역할을 한다.

[그림 37] TDL4 Work Thread 및 CallBack 함수 등록
PsSetLoadImageNotifyRoutine()으로 등록된 CallBack 함수는 위에서 설명하였듯이, [그림 37] 코드로 각 프로세스에 DLL 인젝션을 한다.

[그림 38] TDL4 프로세스 인젝션 코드
ExQueueWorkItem()에 의해 실행되는 WorkItem 루틴은 System Thread의 문맥에서 호출되는 콜백 함수이다. IO 관리자는 부팅 과정 중에 몇 개의 System Thread를 생성한다. 이것들은 평상시 아무런 작업을 하지 않고 대기하며, 드라이버들은 WorkItem 루틴을 등록하는 시기에만 해당 WorkItem 루틴을 스레드 문맥에서 호출한다.
실행된 WorkItem 루틴은 cfg.ini를 수정한 후, 해당 프로세스에 메모리를 할당하고 cmd.dll(TDL4 악성 DLL)에 PE 이미지를 복사한다. 복사할 때 URL 같은 몇 가지 정보를 추가한다.
또한 하나의 다른 메모리 영역을 생성하는데, 이것은 LoadLibraryExA(), GetProcAddress(), VirtualFree() API 주소를 저장하며, 해당 API를 호출하는 코드를 복사한다.
해당 코드는 나중에 설명하겠지만, 인젝션하는 프로세스의 대상 리스트가 존재하며 해당 리스트의 프로세스가 아닐 경우, 인젝션 코드를 실행하지 않게 하려는 목적을 가진다.

[그림 39] 악성 DLL(Cmd.dll)을 메모리 복사
Atapi.sys(FileSystem Mini filter Driver) Device Object 후킹
또한 TDL4 드라이버에 의해 생성된 Device Object의 AttachedDevice는 Disk.sys의 Device Object를 가리킨다.

[그림 40] Atapi.sys DeviceObject 중 TDL4로 교체된 상황
위와 같은 형태를 원상복구시켜야 실제 언파티션드 영역에 암호화된 TDL 모듈과 악성 MBR를 확인할 수 있다.

[그림 41] Disk.sys, Atapi.sys, TDL4의 연결 현황
정상 MBR 백업 및 SymbolicLink 설정

[그림 42] 정상 MBR 백업
또한 SymbolicLink를 생성하는데, 유저 모드에서 언파티션드 영역으로 접근하기 위해서이다.

[그림 43] SymbolicLink 생성
ZwCreateSymbolicLinkObject() 관련 파라미터는 아래와 같다.

[그림 44] ObjectName과 TargetName
전체적으로 Disk, Atapi, PnpManager, TDL4의 관계는 아래 그림과 같다.
3. Spoolsv.exe 인젝션
NtLoadDriver()를 이용하여 TDL4 드라이버를 설치 및 후킹 등의 작업을 마친 뒤, 다시 Spoolsv.exe에서 인젝션된 코드로 돌아온다.
SymbolicLink를 이용한 언파티션드 영역 접근
유저 모드로 돌아온 TDL4은 ZwOpenSymbolicLinkObject()라는 API를 실행하는데, 앞서 드라이버에서 생성한 SymbolicLink를 이용한다.

[그림 45] ZwOpenSymbolicLinkObject
TDL4 모듈 저장
TDL4는 이제 언파티션드 영역에 자신의 모듈을 저장한다. 앞서 드라이버에서는 악성 MBR 감염, 정상 MBR 백업, cfg.ini을 저장했고, 나머지 모듈을 유저 모드에서 저장한다.

[그림 46] TDL4 모듈 저장
아래는 TDL4에서 언파티션드 영역에 저장하는 TDL4 모듈이다.

위와 같은 작업이 완료되면, 실질적인 TDL4 설치 작업은 마무리된다. 다음호에서는 후킹한 Atapi.sys의 StartIo 기능 및 자체 보호 기능 등에 대해 설명하겠다.@