Windows DeviceIoControl 분석하기 with Windbg

wangki·4일 전

개요

Windbg로 DeviceIoControl 호출 시, ioctl이 어떻게 IO 매니저를 통해서 irp로 래핑 되어 넘겨지는지 확인할 예정이다.
또한 syscall이 호출되어 ring3 -> ring0로 변경되는 것을 눈으로 직접 확인한다.


DebugBreak

#include <Windows.h>

DebugBreak()

sxe eb // set exception enable exception breakpoint
Windbg 실행 시, 실행하도록 세팅해놓음

break가 걸린 뒤,
.reload /user을 통해 symbol을 load 해준다.

// symbol settings
srv*C:\Symbols*https://msdl.microsoft.com/download/symbols;

k 명령어로 콜스택을 확인한 결과 사진

.process /i /p
.reload /user

syscall 추적하기

syscall을 추적하기 위해서는 ntdll.dll의 api에 break point를 설정해줘야 한다.
유저 어플리케이션 쪽에서 ioctl명령을 내리기 때문에 DeviceIoControl을 호출 시, 내부에서 호출되는 api인 NtDeviceIoControlFilebp를 걸어 준다.

bp ntdll!NtDeviceIoControlFile

ring 상태 확인

x64 아키텍처에서는 CS (Code Segment) 레지스터의 하위 2비트(CPL: Current Privilege Level)로 Ring 0과 Ring 3을 구분한다.

0: kd> r cs
cs=0033

아직 syscall이 호출되지 않았기에 ring 3인 것을 확인할 수 있다.

RIP란 ?

RIP는 Register Instruction Pointer의 약자로 CPU가 다음에 실행할 어셈블리 명령어의 메모리 주소를 저장하는 64비트 레지스터이다. (다른 아키텍처나 일반적인 컴파일러에서는 PC(Program Counter)라고 보면 된다.

ntdll!NtDeviceIoControlFile+0x12:
00007ffe`c219eee2 0f05            syscall
00007ffe`c219eee4 c3              ret
00007ffe`c219eee5 cd2e            int     2Eh
00007ffe`c219eee7 c3              ret
00007ffe`c219eee8 0f1f840000000000 nop     dword ptr [rax+rax]
ntdll!NtWriteFile:
00007ffe`c219eef0 4c8bd1          mov     r10,rcx

명령어 p를 통해서 syscall 라인까지 내려왔고 명령어 r rip를 통해 실제로 rip 레지스터 값을 확인해 보겠다.

2: kd> r rip
rip=00007ffec219eee2

syscall 라인 주소와 일치하는 것을 확인할 수 있다.

syscall 호출 전 레지스터

2: kd> r
rax=0000000000000007 rbx=0000000000000000 rcx=00000000000001c8
rdx=0000000000000000 rsi=00000077d24ff6c0 rdi=00000000000001c8
rip=00007ffec219eee2 rsp=00000077d24ff578 rbp=00000000000001c8
 r8=0000000000000000  r9=0000000000000000 r10=00000000000001c8
r11=00000077d24ff748 r12=000001ba2a1a8408 r13=00007ff65b674b68
r14=0000000000000000 r15=0000000000000002
iopl=0         nv up ei pl zr na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246

이슈 발생

t를 눌러서 스텝 인투를 했지만, 커널 내부로 들어가지지 않았다. 스킵이 되어버린다는 것 같다. 그래서 커널 최전방 코드인 KiSystemCall64에 breakpoint를 걸어보겠다.

bp nt!KiSystemCall64

그런데도 여전히 잘되지 않는다. 커널 함수에 bp를 걸게되면 너무 자주 hit가 되어서 정상적으로 디버깅이 힘들다.

그래서 프로세스의 EPROCESS 커널 구조체 주소를 얻어서 해당 주소에서 호출된 함수인 경우에는 hit가 되도록 할 수 있다.

!process 0 0 TestApp.exe

-> 

1: kd> !process 0 0 TestApp.exe
PROCESS ffff830a3cea3080
    SessionId: none  Cid: 0a48    Peb: a79cf81000  ParentCid: 0318
    DirBase: 20500c000  ObjectTable: ffffe682497ca7c0  HandleCount:  48.
    Image: TestApp.exe
1: kd> bp /p ffff830a3cea3080 nt!NtDeviceIoControlFile
1: kd> bl
     0 e Disable Clear  fffff800`c1ec0200     0001 (0001) nt!NtDeviceIoControlFile
     Match process data ffff830a`3cea3080

레지스터를 확인해보면 된다.

3: kd> r
rax=fffff800c1ec0200 rbx=ffff830a3d7d1080 rcx=00000000000000b4
rdx=0000000000000000 rsi=000000a79d0ff898 rdi=ffffd186712bca88
rip=fffff800c1ec0200 rsp=ffffd186712bca68 rbp=ffffd186712bcb60
 r8=0000000000000000  r9=0000000000000000 r10=fffff800c1ec0200
r11=fffff800c1cbfb00 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0         nv up ei pl zr na pe nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246

rip의 값이 fffff800c1ec0200으로 커널 주소를 가르키는 것을 확인할 수 있고 cs 레지스터가 0010으로 ring 0 모드인 것을 확인했다. 콜스택을 확인해보자

3: kd> k
 # Child-SP          RetAddr               Call Site
00 ffffd186`712bca68 fffff800`c1cbfb55     nt!NtDeviceIoControlFile
01 ffffd186`712bca70 00007ff9`691c0464     nt!KiSystemServiceCopyEnd+0x25
02 000000a7`9d0ff878 00007ff9`662c3953     0x00007ff9`691c0464
03 000000a7`9d0ff880 000000a7`9d0ff888     0x00007ff9`662c3953
04 000000a7`9d0ff888 00000002`0000000c     0x000000a7`9d0ff888
05 000000a7`9d0ff890 00007ff6`00000101     0x00000002`0000000c
06 000000a7`9d0ff898 0000e66b`d45a5c3c     0x00007ff6`00000101
07 000000a7`9d0ff8a0 000000a7`9d0ff8d0     0x0000e66b`d45a5c3c
08 000000a7`9d0ff8a8 00000000`00222000     0x000000a7`9d0ff8d0
09 000000a7`9d0ff8b0 00000000`00000000     0x222000

추가로 IO 매니저가 Irp를 만드는 부분을 따라가보겠다. 꿀팁으로 @$proc를 통해 현재 컨텍스트의 EPROCESS의 주소를 얻어올 수 있다.

3: kd> bp /p @$proc nt!IoAllocateIrp
3: kd> bl
     0 e Disable Clear  fffff800`c1ec0200     0001 (0001) nt!NtDeviceIoControlFile
     Match process data ffff830a`3cea3080
     1 e Disable Clear  fffff800`c185a570     0001 (0001) nt!IoAllocateIrp
     Match process data ffff830a`3cea3080

bl로 결과를 보면 process가 동일한 것을 확인할 수 있다.

추가로 IofCallDriver에도 bp를 걸어두고 g로 실행 후, hit되어서 분석하였다.

1: kd> k
 # Child-SP          RetAddr               Call Site
00 ffffd186`72661678 fffff800`c185c036     nt!IofCallDriver
01 ffffd186`72661680 fffff800`c1ec1f0b     nt!IopCallDriverReference+0xe6
02 ffffd186`72661700 fffff800`c1ec0c0c     nt!IopSynchronousServiceTail+0x30b
03 ffffd186`72661790 fffff800`c1ec025e     nt!IopXxxControlFile+0x99c
04 ffffd186`72661a00 fffff800`c1cbfb55     nt!NtDeviceIoControlFile+0x5e
05 ffffd186`72661a70 00007ff9`691c0464     nt!KiSystemServiceCopyEnd+0x25
06 00000022`aa8ff4b8 00007ff9`662c3953     ntdll!NtDeviceIoControlFile+0x14
07 00000022`aa8ff4c0 00007ff9`67a219f5     KERNELBASE!DeviceIoControl+0x73
08 00000022`aa8ff530 00007ff7`19627e67     KERNEL32!DeviceIoControlImplementation+0x75
09 00000022`aa8ff580 00000000`00000000     TestApp!main+0xc7 [C:\work\TestDriver\TestApp\TestApp.cpp @ 31] 

IofCallDriver의 함수 시그니처가 아래와 같다.

NTSTATUS IofCallDriver(
  PDEVICE_OBJECT        DeviceObject,
  __drv_aliasesMem PIRP Irp
);

두 번째 매개변수를 담는 레지스터인 rdx를 통해서 !irp @rdx 1 명령어를 주어서 확인할 수 있다.

Irp is active with 2 stacks 3 is current (= 0xffff830a3e6d67c0)
 No Mdl: No System Buffer: Thread ffff830a4239b0c0:  Irp is completed.  
Flags = 00060000
ThreadListEntry.Flink = ffff830a4239b600
ThreadListEntry.Blink = ffff830a4239b600
IoStatus.Status = 00000000
IoStatus.Information = 00000000
RequestorMode = 00000001
Cancel = 00
CancelIrql = 0
ApcEnvironment = 00
UserIosb = 22aa8ff510
UserEvent = 00000000
Overlay.AsynchronousParameters.UserApcRoutine = 00000000
Overlay.AsynchronousParameters.UserApcContext = 00000000
Overlay.AllocationSize = 00000000 - 00000000
CancelRoutine = 00000000   
UserBuffer = 00000000
&Tail.Overlay.DeviceQueueEntry = ffff830a3e6d66d8
Tail.Overlay.Thread = ffff830a4239b0c0
Tail.Overlay.AuxiliaryBuffer = 00000000
Tail.Overlay.ListEntry.Flink = 00000000
Tail.Overlay.ListEntry.Blink = 00000000
Tail.Overlay.CurrentStackLocation = ffff830a3e6d67c0
Tail.Overlay.OriginalFileObject = ffff830a43ba9360
Tail.Apc = 00000000
Tail.CompletionKey = 00000000
     cmd  flg cl Device   File     Completion-Context
 [N/A(0), N/A(0)]
            0  0 00000000 00000000 00000000-00000000    

			Args: 00000000 00000000 00000000 00000000
 [N/A(e), N/A(0)]
            5  0 00000000 ffff830a43ba9360 00000000-00000000    

			Args: 00000000 00000000 0x222000 00000000

AI의 도움을 받아 [N/A(e), N/A(0)]eIRP_MJ_DEVICE_CONTROL라고 한다.
그리고 Args의 0x222000은 실제로 우리가 넘긴 ioctl인 것을 확인할 수 있다.

#define IOCTL_COMMAND_TEST CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
(((0x00000022) << 16) | ((0) << 14) | ((0x800) << 2) | (0))	// 계산하면 0x222000 나온다. 

그리고 fileobj 주소인 ffff830a43ba9360을 통해 어느 오브젝트로 요청하는지 확인할 수 있다.

1: kd> !fileobj ffff830a43ba9360



Device Object: 0xffff830a36657d80   \Driver\TestDriver
Vpb is NULL

Flags:  0x40002
	Synchronous IO
	Handle Created

File Object is currently busy and has 0 waiters.

CurrentByteOffset: 0

결론

정말 궁금했던 부분을 Windbg로 확인했다. 개발을 처음 시작하면서 가졌던 의문들에 대해서 조금은 이해를 할 수 있는 시간이었다.
그렇지만 완벽하게 이해된 것은 아니기 때문에 계속 연습을 할 필요가 있다. 또한 커널이라는 방대한 개념을 이해하기 위해서 공부를 더 열심히 해야겠다.

0개의 댓글