constWORLDant

개인공부 _ 커널 조작에 관한 기본 개념 본문

0x03 Reversing Theory

개인공부 _ 커널 조작에 관한 기본 개념

data type ConS_tanT 2017.12.13 21:35

루트킷은 유저모드(user mode)와 커널모드(kernel mode) 요소를 모두 쉽게 포함할 수 있다. 


유저모드에서는 네트워킹, 원격제어와 같은 대부분의 기능을 담당하고, 커널모드에서는 은닉기능과 하드웨어 접근에 관련된 기능을 담당한다.


루트킷의 구조는 책을 참조하였다.



[ A rootkit that contains both user mode and kernel mode elements ]


유저 모드 프로그램은 커널 레벨 드라이버와 다양한 방법으로 서로 통신할 수 있다. 

그 중에서 가장 일반적인 방법이 I/O Control(IOCTL) 명령을 이용하는 것이다.


IOCTL 명령은 유저 애플리케이션과 커널 드라이버간의 통신을 위해서 프로그래머가 정의 하는 명령이다.


유저모드와 커널모드 요소를 모두 포함하는 루트킷을 만들기 위해서는 이후에 설명되는 디바이스 드라이버의 개념들을 모두 이해해야 한다.


디바이스 드라이버를 이해하기 위해서는 I/O Request Packet(IRP)을 이해해야 한다.

윈도우 디바이스 드라이버는 유저 모드 프로그램과 통신하기 위해서 IRP에 대한 처리가 필요하다.

IRP는 데이터 버퍼를 포함하는 데이터 구조체라고 할 수 있다.

유저 모드 프로그램이 파일을 열어서 그 파일에 데이터를 쓰면 커널 레벨에서는 이 작업을 IRP를 이용해 표현한다.


예를 들어 유저 모드 프로그램이 "HELLO DRIVER!" 라는 문자열을 파일에 쓰게 되면, 커널은 "HELLO DRIVER!"라는 문자열이 포함된

IRP를 생성한다. 


이처럼 IRP를 통해서 유저 모드와 커널 모드간의 통신이 이루어지게 된다.


커널 드라이버는 IRP를 처리하는 함수가 있어야만 IRP를 처리할 수 있다고 한다.

IRP를 처리하기 위해서는 드라이버 오브젝트에 해당 함수를 등록하면 된다.


책에 있는 예시를 참조하였다.


NTSTATUS OnStubDispatch(IN PDEVICE_OBJECT DeviceObject,

IN PIRP Irp)

{

Irp->IoStatus.Status = STATUS_SUCCESS;

IoCompleteRequest(Irp, IO_NO_INCREMENT);

return STATUS_SUCCESS;

}


VOID OnUnload( IN PDRIVER_OBJECT DriverObject )

{

DdbPrint("OnUnload called\n");

}


NTSTATUS DriverEntry( IN PDRIVER_OBJECT theDriverObject,

IN PUNICODE_STRING theRegistryPath)

{

int i;

theDriverObject -> DriverUnload = OnUnload;


for(i=0; i<IRP_MJ_MAXIMUM_FUNCTION ; i++)

theDriverObject -> MajorFunction[i] = OnStubDispatch;


return STATUS_SUCCESS;

}



The path from the user mode I/O call to the kernel driver's major function ]


예제코드와 그림에서 알 수 있는 점은 드라이버의 Major Function의 주소는 하나의 배열에 저장되고 read, write, ioctl에 해당하는 Major Function의 배열에서의 인덱스 값은 IRP_MJ_READ, IRP_MJ_WRITE, IRP_MJ_DEVICE_CONTROL로 정의된다.


예제에서는 모든 Major Function에 대한 함수가 OnStubDispatch로 세팅되었으며 OnStubDispatch 함수에서는 아무런 작업도 수행하지 않는다.


[ kernel driver's structure ]


파일 핸들 생성


유저 모드 프로그램이 커널 드라이버를 사용하려면 유저 모드 프로그램은 해당 드라이버의 핸들을 구해야 한다. 

이것은 드라이버가 이름 있는 디바이스로서 등록된 경우에만 가능하다.

일단 드라이버가 이름 있는 디바이스로 등록했다면 유저 모드 프로그램은 파일의 경우와 동일한 방법으로 디바이스 이름을 이용해 핸들을 구할 수 있다. 이것은 유닉스에서 모든 것이 파일로 취급되는 것과 유사하다.


다음은 커널 모드 드라이버가 디바이스를 등록하는 과정을 보여주고 있다.


const WCHAR deviceNameBuffer[] = L"\\Device\\MyDevice";

PDEVICE_OBJECT g_RootkitDevice; // global pointer value about device object

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)

{

NTSTATUS ntStatus;

UNICODE_STRING deviceNameUnicodeString;

// set to name and symbolic link

RtlInitUnicodeString (&deviceNameUnicodeString, deviceNameBuffer);

// generate device object


ntStatus = IoCreateDevice (DriverObject, 0, &deviceNameUnicodeString, 0x00001234, 0,TRUE, &g_RootkitDevice);


}

예제 코드에서 MyDevice라는 이름을 가진 디바이스가 DriverEntry 함수 내에서 생성됨을 알 수 있다. 


심볼링 링크 추가


드라이버는 유저 모드 프로그램이 드라이버의 핸들을 쉽게 구할 수 있도록 심볼릭 링크를 이용하게 된다.

유저 모드 입장에서는 단순히 심볼릭 이름을 기억하고 있으면 편리하기 때문에 많이 사용한다. 

드라이버가 디바이스를 생성한 이후에는 IoCreateSymbolicLink 함수를 호출해서 심볼릭 링크를 생성한다.

루트킷 중에는 심볼릭 링크를 이용하는 것들도 있고 그렇지 않은 것들도 있다.


const WCHAR deviceLinkBuffer[] = L"\\DosDevices\\vicesys2";

const WCHAR deviceNameBuffer[] = L"\\Device\\vicesys2"; 

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)

{

NTSTATUS ntStatus;

UNICODE_STRING deviceNameUnicodeString;

UNICODE_STRING deviceLinkUnicodeString;

// set to name and symbolic link

RtlInitUnicodeString (&deviceNameUnicodeString, deviceNameBuffer);

RtlInitUnicodeString (&deviceLinkUnicodeString, deviceLinkBuffer);

// generate device object


ntStatus = IoCreateDevice (DriverObject, 0, &deviceNameUnicodeString, 0x00001234, 0,TRUE, &g_RootkitDevice);

if(NT_SUCCESS(ntStatus))

{

ntStatus = IoCreateSymbolicLink(&deviceLinkUnicodeString, &deviceNameUnicodeString);

}

}



심볼링 링크가 생성되면 유저 모드 프로그램은 "\\.\MyDevice" 문자열을 이용해서 핸들을 구할 수 있다.

심볼릭 링크를 만드는 것은 전적으로 선택사항이다.

그것은 단지 유저 모드 프로그램이 드라이버를 쉽게 찾을 수 있는 편의를 제공할 뿐이다.

hDevice = CreateFile("\\\\.\\MyDevice", GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );


if( hdevice == ((HANDLE) -1 )) 

return FALSE;


루트킷 로딩

우리는 유저 모드 프로그램을 통해 드라이브를 로드할 필요성이 반드시 생기게 된다고 한다. 

이게 무슨말인지 잘 몰랐는데 책에는 다음과 같은 예시를 들어놓았다. 

'어떤 컴퓨터 시스템에 침투한 다음에는 루트킷을 커널 레벨로 로딩해 주는 프로그램이 필요하다' 


음.. 유저모드에서 원활한 활동을 못하니 커널 레벨로 상승시켜주는 역할을 하는 프로그램인 것 같다.


로딩 프로그램은 전형적으로 압축을 풀어서 .sys파일(드라이버 파일)을 하드디스크에 복사할 것이다.

그런 다음에는 복사 할 드라이버 파일을 로드하기 위한 명령을 실행할 것이다.


로딩을 수행하는 프로그램은 "관리자" 권한을 소지하고 실행 되어야 한다. 


오늘 재밌는 이론을 배워보았다. 

주제 : 드라이버를 커널 레벨로 로드하려면?


비 정상적인 방법 vs 정상적인 방법 


드라이버 로드를 위한 비 정상적인 방법부터 알아보았다.


알려 지지 않은 API를 이용하게 되면 레지스트리 키를 생성하지 않더라도 드라이버를 "커널 레벨"로 로드할 수 있다고 한다.

여기서 발생할 수 있는 문제점은 "로드한 드라이버가" 페이징 될 수 있다.


페이징 : 디스크로 메모리 내용이 스왑 되는 현상

메모리가 디스크로 스왑되게 되면 해당 메모리에 대한 접근이 불가능 해진다.

스왑되어 접근이 불가능한 메모리에 접근하려고 하면 블루 스크린이 발생하게 되는 것이다. 

음 그렇다면 블루 스크린이 트랩과도 연관성이 있지 않을까?

트랩의 원리 중에 올바르지 않은 주소 값을 가리킬 때 터지는 원리도 있으니까.

단지, 메모리 <---> 디스크일 뿐인데..??

시간 날 때 생각을 조금해보자.. skip


드라이버 로드를 위한 정상적인 방법


Service Control Manager(SCM)을 이용하는 것이라고 한다.

SCM을 이용하면 레지스트리 키가 생성되며 SCM에 의해 로드 되는 드라이버는 페이징이 되지 않는다고 한다.

이는 IRP를 처리하는 콜백 함수들뿐만 아니라 드라이버의 다른 중요한 코드가 메모리에서 사라지거나 페이징되지 않는다는 것을 의미하기도 한다. 



루트킷 드라이버의 핵심 "시스템이 부팅되는 시점에 로드"

실제로 시스템이 부팅되는 시점에 로드되는 소프트웨어는 상당하다. 


방법은 여러개가 있다. 하나하나 알아볼 것이다.



1. Run 키를 이용 (레지스트리)

RUN 키를 이용하게 되면 임의의 프로그램을 부팅 시점에 실행시킬 수 있다.

RUN 키에 의해 실행 된 프로그램은 자신에게 삽입 된 루트킷 파일을 생성해서 그것을 로드한다.

로드된 루트킷은 탐지되지 않기 위해 자신을 위한 run 키의 값을 감출 수 있다.

대부분의 바이러스 스캐너는 기본적으로 run 키를 검사한다. 하지만, 루트킷이 성공적으로 로드 되게 되면 키 값은 보이지 않기 때문에 

RUN 키를 이용하는 경우는 잦다.


2. 트로이 목마 혹은 감염된 파일을 이용

부팅 시점에 로드되는 .sys 파일이나 실행파일은 교체될 수가 있다. 또는 바이러스가 파일을 감염시키는 방법과 비슷하게 원하는 파일을 로딩시킬 수 있는 코드를 삽입할 수도 있다. 

책에 의하면 바이러스 스캐너나 보안 프로그램이 가장 코드를 유입하기 좋은 프로그램이라고 한다.

그 이유는 단순하다. 바이러스 스캐너나 보안 프로그램은 시스템이 부팅되면서 함께 실행되는 프로그램에 손꼽히기 때문이다. 

트로이목마 DLL을 PATH 경로에 삽입하거나 기존에 존재하는 DLL을 단순히 교체 하거나 감염시키는 방법을 택할 수 있다.


3. .ini 파일 이용

.ini 파일을 변경해서 프로그램이 실행되게 만들 수도 있다. 

많은 프로그램이 자신이 실행되면서 수행할 명령이나 로드할 DLL 목록을 포함하는 초기화 파일을 이용한다.


4. 드라이버로 등록

루트킷은 자기 자신을 부팅하면서 로드 되는 드라이버로 등록할 수 있다.

이를 위해서는 RUN 처럼 레지스트리 키를 만들어야 한다.


5. 기존 애플리케이션의 추가 된 기능으로 등록

스파이웨어에 의해서 자주 사용되는 방법은 자신을 웹 브라우저 애플리케이션의 확장 컴포넌트로 등록하는 것이다

(ex. 검색바로 가장한 스파이웨어)

그렇다면 iexplorer.가 실행 될 때 검색 바가 포함이 되게 되는데 이 검색 바에 루트킷을 심는 것이다.

하지만, 불행하게도 애드웨어 스캐너는 대부분 이를 탐지 한다.


6. 커널을 디스크상에서 변경

가장 강력하면서 어려워 보이는 기법이다.

커널을 하드디스크 상에서 직접 변경하는 것인데 커널에 대한 무결성 검사를 통과하려면 변경을 최소화 해야 한다.

커널의 변경사항은 일시적이지 않으며 어떤 드라이버도 등록할 필요가 없기 때문에 매우 강력하다.


7. 부트 로더 변경

커널보다 먼저 동작하는  것이 부트로더 인데 부트로더를 변경함으로써 루트킷 삽입도 가능하다.

이 방법의 장점은 시스템을 분석해도 커널이 변경된 것으로 보여지지 않는다.

여기까지의 스킬을 익히려면 많은 공부가 필요할 것 같다..


오늘 적은 내용은 틈틈히 복습해야 한다. !! 

0 Comments
댓글쓰기 폼