동기화에 대해서 windbg를 사용해 분석하고자 한다. 이번 포스팅은 좀 많이 복잡하다.
커널에서 생성한 세마포어 커널 오브젝트를 얻어와서 어떻게 동작을 하는지 살펴볼 예정이다.
DISPATCHER_HEADER 구조체를 가진다. 카운트가 0일 때 획득을 시도하면 쓰레드는 Waiting 상태로 변경되고, 해당 세마포어의 WaitListHead에 쓰레드의 KWAIT_BLOCK이 등록된 후 컨텍스트 스위칭이 발생한다. 메모리에 물리적 장벽을 쳐서 접근을 막는 것이 아니라, "코드의 실행 흐름"을 막는다.
물리적으로 막는 것은 아니다.
int g_Data = 0;
KSPIN_LOCK g_Lock;
// Thread A (정상적인 락 사용)
KeAcquireSpinLock(&g_Lock, &oldIrql);
g_Data = 100; // 임계 구역 내부
KeReleaseSpinLock(&g_Lock, oldIrql);
// Thread B (락을 안 거르고 직접 접근하는 악성/실수 코드)
g_Data = 999; // 락을 확인 안 함!
실제 코드 레벨에서는 모든 스레드가 임계구역에 들어가기 전에 반드시 락을 거친다라는 가정을 바탕으로 동작한다.
객체(EPROCESS)를 찾는다.Tips:
!process 0 0 SyncTest.exe
!process <Process Flag> <Detail Level> <Image Name>
- Process Flag로 0을 지정하면 모든 프로세스를 대상으로 검색
- Detail Level을 0으로 지정하면 최소한의 정보만 출력
- !process만 입력해도 현재 브레이크 걸린 프로세스 정보 찾기 가능
0: kd> !process 0 0 SyncTest.exe
PROCESS ffff8383a61f0080
SessionId: none Cid: 02d0 Peb: 8979d6a000 ParentCid: 28a8
DirBase: 205dd0000 ObjectTable: ffffe586ed008a00 HandleCount: 48.
Image: SyncTest.exe
3: kd> !handle 0 3 -1 Semaphore
Tips: !handle <HandleValue> <Flags> <Process> <Type\n>
- HandleValue: 0 (전체검색)
- Flags: 3 (상세 정보 레벨)
- Process: -1 (현재 멈춰있는 프로세스 대상)
3: kd> !handle 0 1 -1 Semaphore
Searching for handles of type Semaphore
PROCESS ffff8383a29d4080
SessionId: none Cid: 24b8 Peb: 16aa66d000 ParentCid: 28a8
DirBase: 194e3d000 ObjectTable: ffffe586ed009e40 HandleCount: 49.
Image: SyncTest.exe
Handle table at ffffe586ed009e40 with 49 entries in use
0084: Object: ffff83839c0982a0 GrantedAccess: 001f0003 (Protected) (Inherit) (Audit)
0088: Object: ffff83839c097ee0 GrantedAccess: 001f0003 (Protected) (Inherit)
00b4: Object: ffff8383a03a9b60 GrantedAccess: 001f0003 (Protected) (Inherit)
어느 세마포어가 우리가 직접 만든 세마포어인지 찾아야한다.
2-1. Watch 창에서 global 변수 g_sema 조회

2-2. 핸들 값으로 커널 세마포어 객체 주소 직접 뽑기
0: kd> !handle 0xb4 3 -1
PROCESS ffff8383a29d4080
SessionId: none Cid: 24b8 Peb: 16aa66d000 ParentCid: 28a8
DirBase: 194e3d000 ObjectTable: ffffe586ed009e40 HandleCount: 49.
Image: SyncTest.exe
Handle table at ffffe586ed009e40 with 49 entries in use
00b4: Object: ffff8383a03a9b60 GrantedAccess: 001f0003 (Protected) (Inherit) Entry: ffffe586e44ff2d0
Object: ffff8383a03a9b60 Type: (ffff83839379ed60) Semaphore
ObjectHeader: ffff8383a03a9b30 (new version)
HandleCount: 1 PointerCount: 1
2-3. 커널 세마포어 구조체 구하기
0: kd> dt nt!_KSEMAPHORE ffff8383a03a9b60
+0x000 Header : _DISPATCHER_HEADER
+0x018 Limit : 0n1
2-4. _DISPATCHER_HEADER의 내부필드 보기
-r 옵션을 주어서 다시 검색
kd> dt nt!_KSEMAPHORE -r ffff8383a03a9b60
+0x000 Header : _DISPATCHER_HEADER
+0x000 Lock : 0n524293
+0x000 LockNV : 0n524293
+0x000 Type : 0x5 ''
+0x001 Signalling : 0 ''
+0x002 Size : 0x8 ''
+0x003 Reserved1 : 0 ''
+0x000 TimerType : 0x5 ''
+0x001 TimerControlFlags : 0 ''
+0x001 Absolute : 0y0
+0x001 Wake : 0y0
+0x001 EncodedTolerableDelay : 0y000000 (0)
+0x002 Hand : 0x8 ''
+0x003 TimerMiscFlags : 0 ''
+0x003 Index : 0y000000 (0)
+0x003 Inserted : 0y0
+0x003 Expired : 0y0
+0x000 Timer2Type : 0x5 ''
+0x001 Timer2Flags : 0 ''
+0x001 Timer2Inserted : 0y0
+0x001 Timer2Expiring : 0y0
+0x001 Timer2CancelPending : 0y0
+0x001 Timer2SetPending : 0y0
+0x001 Timer2Running : 0y0
+0x001 Timer2Disabled : 0y0
+0x001 Timer2ReservedFlags : 0y00
+0x002 Timer2ComponentId : 0x8 ''
+0x003 Timer2RelativeId : 0 ''
+0x000 QueueType : 0x5 ''
+0x001 QueueControlFlags : 0 ''
+0x001 Abandoned : 0y0
+0x001 DisableIncrement : 0y0
+0x001 QueueReservedControlFlags : 0y000000 (0)
+0x002 QueueSize : 0x8 ''
+0x003 QueueReserved : 0 ''
+0x000 ThreadType : 0x5 ''
+0x001 ThreadReserved : 0 ''
+0x002 ThreadControlFlags : 0x8 ''
+0x002 CycleProfiling : 0y0
+0x002 CounterProfiling : 0y0
+0x002 GroupScheduling : 0y0
+0x002 AffinitySet : 0y1
+0x002 Tagged : 0y0
+0x002 EnergyProfiling : 0y0
+0x002 SchedulerAssist : 0y0
+0x002 ThreadReservedControlFlags : 0y0
+0x003 DebugActive : 0 ''
+0x003 ActiveDR7 : 0y0
+0x003 Instrumented : 0y0
+0x003 Minimal : 0y0
+0x003 Reserved4 : 0y00
+0x003 AltSyscall : 0y0
+0x003 Emulation : 0y0
+0x003 Reserved5 : 0y0
+0x000 MutantType : 0x5 ''
+0x001 MutantSize : 0 ''
+0x002 DpcActive : 0x8 ''
+0x003 MutantReserved : 0 ''
+0x004 SignalState : 0n1
+0x008 WaitListHead : _LIST_ENTRY [ 0xffff8383`a03a9b68 - 0xffff8383`a03a9b68 ]
+0x000 Flink : 0xffff8383`a03a9b68 _LIST_ENTRY [ 0xffff8383`a03a9b68 - 0xffff8383`a03a9b68 ]
+0x008 Blink : 0xffff8383`a03a9b68 _LIST_ENTRY [ 0xffff8383`a03a9b68 - 0xffff8383`a03a9b68 ]
+0x018 Limit : 0n1
중요한 필드로 Limit, SignalState를 확인하면 된다.
Limit값이 0n1로 최대 카운트 1개짜리 세마포어를 만든 것이다. 현재 SignalState값은 0n1으로 어떤 스레드도 락을 쥐고 있지 않은 상태로 볼 수 있다. 또한 WaitListHead를 관찰하면
+0x008 WaitListHead : _LIST_ENTRY [ 0xffff8383`a03a9b68 - 0xffff8383`a03a9b68 ]
+0x000 Flink : 0xffff8383`a03a9b68 _LIST_ENTRY [ 0xffff8383`a03a9b68 - 0xffff8383`a03a9b68 ]
+0x008 Blink : 0xffff8383`a03a9b68 _LIST_ENTRY [ 0xffff8383`a03a9b68 - 0xffff8383`a03a9b68 ]
0xffff8383a03a9b68으로 자기자신을 가리키고 있는 것을 확인할 수 있다.

세마포어가 lock을 획득한 뒤, 상태 값을 확인했다.

SignalState값이 0n0으로 바뀐 것을 확인할 수 있다. 자원 1개를 가져갔기때문에 남은 자원이 0개가 되어 0n0으로 변경된 것이다.
ThreadB를 실행 후, WaitForSingleObject에서 lock을 획득하지 못하고 대기한 후, 상태 값을 확인했다.

WaitList값이 변경된 것을 확인할 수 있다.
_KWAIT_BLOCK을 확인해 상태 값을 확인한다.
0: kd> dt nt!_KWAIT_BLOCK 0xffff8383a5ff31c0
+0x000 WaitListEntry : _LIST_ENTRY [ 0xffff8383`a03a9b68 - 0xffff8383`a03a9b68 ]
+0x010 WaitType : 0x1 ''
+0x011 BlockState : 0x4 ''
+0x012 WaitKey : 0
+0x014 SpareLong : 0n8
+0x018 Thread : 0xffff8383`a5ff3080 _KTHREAD
+0x018 NotificationQueue : 0xffff8383`a5ff3080 _KQUEUE
+0x018 Dpc : 0xffff8383`a5ff3080 _KDPC
+0x020 Object : 0xffff8383`a03a9b60 Void
+0x028 SparePtr : (null)
추가로 ThreadB의 콜스택을 확인할 수 있다.
0: kd> !thread 0xffff8383`a5ff3080
THREAD ffff8383a5ff3080 Cid 24b8.0714 Teb: 00000016aa672000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
ffff8383a03a9b60 Semaphore Limit 0x1
Not impersonating
DeviceMap ffffe586d19ee0d0
Owning Process ffff8383a29d4080 Image: SyncTest.exe
Attached Process N/A Image: N/A
Wait Start TickCount 98912 Ticks: 579 (0:00:00:09.046)
Context Switch Count 8 IdealProcessor: 3
UserTime 00:00:00.000
KernelTime 00:00:00.000
Win32 Start Address SyncTest!ILT+16265(?ThreadBYAKPEAXZ) (0x00007ff6a0345f8e)
Stack Init ffffcc002bf73c70 Current ffffcc002bf736c0
Base ffffcc002bf74000 Limit ffffcc002bf6e000 Call 0000000000000000
Priority 8 BasePriority 8 IoPriority 2 PagePriority 5
Child-SP RetAddr : Args to Child : Call Site
ffffcc00`2bf73700 fffff801`bb6cbf64 : 00000000`00000047 00000000`00000000 00000000`00000000 ffff9501`9219f2a8 : nt!KiSwapContext+0x76
ffffcc00`2bf73840 fffff801`bb715c9d : 00000000`00000000 00000000`00000000 ffffcc00`2bf739d0 00000000`00000000 : nt!KiSwapThread+0x6d4
ffffcc00`2bf738d0 fffff801`bb713e99 : ffffcc00`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiCommitThreadWait+0x39d
ffffcc00`2bf73960 fffff801`bbc4809f : ffff8383`a03a9b60 00000000`00000006 00000000`00000001 00000000`00000000 : nt!KeWaitForSingleObject+0x859
ffffcc00`2bf73a40 fffff801`bbc47fca : ffff8383`a5ff3080 00000000`ffffffff 00000000`00000000 ffffcc00`2bf73ae8 : nt!ObWaitForSingleObject+0xbf
ffffcc00`2bf73aa0 fffff801`bbabfb55 : 00000000`00000000 00000000`000000b4 00000000`00000000 00000000`00000000 : nt!NtWaitForSingleObject+0x6a
ffffcc00`2bf73ae0 00007fff`6bf80404 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x25 (TrapFrame @ ffffcc00`2bf73ae0)
00000016`aaaff898 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!NtWaitForSingleObject+0x14
ntdll!NtWaitForSingleObject+0x14을 호출 후 멈춰있는 것을 확인할 수 있다.
동일한 시점에 작성하는 게 아니라서 위와 아래의 주소값이 다를 수 있다.
Tips:
.hh [명령어]를 입력하면 옵션과 사용법을 바로 확인할 수 있다.

Thread A에서 ReleaseSemaphore 직전에 브레이크를 잡은 뒤, SignalState값이 어떻게 변경되는지 살펴보겠다. f10을 눌러서 g_sema을 unlock하겠다.
0: kd> dt nt!_DISPATCHER_HEADER 0xffffa884577c5460 SignalState
+0x004 SignalState : 0n0
SignalState가 0이다. 엥? 할 수 있지만 ReleaseSemaphore가 호출되는 순간에 다른 쪽에서 lock을 했을 가능성이 있는 것 같다. 따라서 현재 쓰레드 상태를 확인했다.
THREAD ffffa88452eac0c0 Cid 1600.1b5c Teb: 0000003e4d130000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
ffffa884531e70c0 Thread
THREAD ffffa884531e70c0 Cid 1600.0bc4 Teb: 0000003e4d132000 Win32Thread: 0000000000000000 RUNNING on processor 0
THREAD ffffa88452f9b0c0 Cid 1600.1f7c Teb: 0000003e4d134000 Win32Thread: 0000000000000000 RUNNING on processor 3
대기 중이던 스레드가 변경된 것을 확인할 수 있다. 그리고 WaitListHead가 자기참조를 하는 것을 확인할 수 있다.
+0x008 WaitListHead : _LIST_ENTRY [ 0xffffa884`577c5468 - 0xffffa884`577c5468 ]
0: kd> dx -id 0,0,ffffa88452f240c0 -r1 (*((ntkrnlmp!_LIST_ENTRY *)0xffffa884577c5468))
(*((ntkrnlmp!_LIST_ENTRY *)0xffffa884577c5468)) [Type: _LIST_ENTRY]
[+0x000] Flink : 0xffffa884577c5468 [Type: _LIST_ENTRY *]
[+0x008] Blink : 0xffffa884577c5468 [Type: _LIST_ENTRY *]

정확하게 의도한대로 동작하고 어플리케이션이 종료되는 것을 확인했다.
windbg 명령어를 전부 외우지 못하지만, 차근차근 물어가며 디버깅했고 결국 Semaphore의 동작을 눈으로 확인했다. 비록 바로 SignalState 값이 바뀌면서 다시 lock이 걸려버렸지만, WaitListHead가 자기 참조로 바뀌면서 Wait 중인 스레드가 없는 것을 확인했다.
동기화에 대해서 좀 더 자세히 알 수 있는 기회였다.