WinDbg - 2가지 유형의 식 평가 방법(MASM, C++)
이번 글은 아래의 내용을 그대로 참조한 것입니다. ^^
WinDBG expression evaluation tutorial
; https://codemachine.com/articles/windbg_expressions.html
Evaluating Expressions
; https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/evaluating-expressions
WinDbg. From A to Z!
; http://windbg.info/download/doc/pdf/WinDbg_A_to_Z_color.pdf
별도 설정을 하지 않았다면, WinDbg는 기본적으로 MASM 방식으로 식을 평가합니다.
6: kd> .expr
Current expression evaluator: MASM - Microsoft Assembler expressions
물론 기본값을 /s 옵션을 통해 바꾸는 것도 가능합니다.
// C++ 방식으로 변경
6: kd> .expr /s c++
Current expression evaluator: C++ - C++ source expressions
// MASM 방식으로 변경
6: kd> .expr /s masm
Current expression evaluator: MASM - Microsoft Assembler expressions
또는, 명시적으로 방법을 지정할 수 있는데, "?" 물음표 1개 명령어는 기본 설정된 방식의 평가를 하는 반면 "??" 2개 명령어는 언제나 "C++" 방식으로 평가합니다. 이후의 모든 예제는 기본 방식이 MASM으로 설정된 것을 가정하기 때문에 "?" 물음표 한 개를 사용했을 시 MASM 방식의 평가가 됩니다.
두 가지 평가 방식의 주요 차이점을 살펴볼까요? ^^
1) 심벌의 주소 평가
우선, MASM에서는 모든 심벌에 대해 주솟값으로 평가를 하는 반면 C++은 그런 해석을 하지 않습니다.
// MASM 방식에서는 nt 모듈을 나타내는 심벌을 그것의 로딩 주소로 평가
6: kd> ? nt
Evaluate expression: -8786477580288 = fffff802`3d200000
// 반면 C++ 방식에서는 오류
6: kd> ?? nt
Couldn't resolve error at 'nt'
재미있는 건 표현식 내에서 특정 영역을 MASM 또는 C++ 방식으로 해석을 바꿀 수 있는 "@@" 기호가 제공되므로, 이것을 도입하면 혼용이 가능합니다.
// 괄호 안의 식을 MASM 방식으로 평가
@@masm( ...[식]...)
// 괄호 안의 식을 C++ 방식으로 평가
@@c++( ...[식]...)
가령, 위에서 nt 심벌을 C++ 평가식에서 주소 참조를 하고 싶다면 다음과 같이 할 수 있습니다.
6: kd> ?? @@masm(nt)
unsigned int64 0xfffff802`3d200000
참고로 masm, c++을 지정하지 않고 단독으로 "@@" 기호만을 사용하면 현재 표현식의 평가 방식에서 무조건 다른 평가 방식을 사용하는 것으로 간주합니다. 즉, MASM 방식이었다면 C++ 방식으로, C++ 방식이었다면 MASM 방식으로 평가를 합니다.
// C++ 평가 방식에서 "@@" 기호를 통해 MASM 방식으로 평가
6: kd> ?? @@(nt)
unsigned int64 0xfffff802`3d200000
// MASM 방식에서, "@@" 기호를 통해 C++ 방식으로, 다시 "@@" 기호를 통해 MASM 방식으로 평가
6: kd> ? @@( @@(nt) )
Evaluate expression: -8786477580288 = fffff802`3d200000
마지막으로, MASM 평가 방식에서만 사용할 수 있는
@!"...[심벌 문자열]..." 구문이 있는데요, 심벌 내에 "<", ">"이나 공백과 같은 특수 문자가 포함된 경우에 사용할 수 있습니다.
// 사용자 모드의 C++ 프로그램을 디버깅하는 상황으로 가정
0:000> bu @!"ExecutableName!std::pair<unsigned int,std::basic_string<unsigned short,std::char_traits<unsigned short>,std::allocator<unsigned short> > >::operator="
2) 기본 진수(radix)
MASM 방식의 경우 별다른 접두사가 없다면 숫자에 대해 16진수로 평가하지만, C++ 방식에서는 10진수로 평가합니다.
// MASM 방식에서는 16진수로 평가
6: kd> n
base is 16
6: kd> ? 10
Evaluate expression: 16 = 00000000`00000010
// 기본 진법을 바꾸고 싶다면 n 명령어 사용
6: kd> n 10
base is 10
6: kd> ? 10
Evaluate expression: 10 = 00000000`0000000a
// C++ 방식에서는 10진수로 평가
6: kd> ?? 10
int 0n10
// 따라서, C++ 방식의 경우 16진수로 평가하고 싶다면 명시적으로 0x 접두사를 붙여야 함
6: kd> ?? 0x10
int 0n16
3) 레지스터 접근
이름에서 이미 유추할 수 있듯이, MASM 방식은 어셈블리어의 문법을 기반으로 식 평가를 지원하기 때문에 레지스터의 경우 이름을 그대로 사용할 수 있습니다.
// MASM 방식에서 rbx 레지스터 값을 평가
6: kd> ? rbx
Evaluate expression: -87374944424720 = ffffb088`6f79acf0
// C++ 방식에서는 오류
6: kd> ?? rbx
Couldn't resolve error at 'rbx'
// 대신, pseudo 레지스터 문법으로 접근
6: kd> ?? @rbx
unsigned int64 0xffffb088`6f79acf0
// MASM 방식에서도 '@' 기호를 명시해 로컬/전역 변수와 이름이 같은 경우를 위해 구분 용도로 사용 가능
// 예를 들어 로컬 변수로 rbx가 있다면, 아래의 식은 명식적으로 레지스터 rbx를 평가
6: kd> ? @rbx
Evaluate expression: -87374944424720 = ffffb088`6f79acf0
기타 WinDbg가 지원하는 사용자 정의 가상 레지스터에 대해서도 MASM 방식이 더 편리합니다.
// 사용자 정의 레지스터인 $t0에 0x1064 값을 설정
6: kd> r $t0 = 0x1064
// MASM 방식에서는 바로 평가
6: kd> ? $t0
Evaluate expression: 4196 = 00000000`00001064
// C++ 방식에서는 "@" 기호를 붙여야 함
6: kd> ?? $t0
Unexpected character in '$t0'
6: kd> ?? @$t0
unsigned int64 0x1064
4) 연산자 및 고유 언어별 확장
기타 연산자의 경우에는 2가지 방식 모두 동일하게 사용할 수 있습니다.
- arithmetic (+ - * / %)
- shift (<< >>)
- bitwise ( & | ^)
- comparison (== < > <= >= !=)
6: kd> ? (efl & ( 1 << 6)) != 0
Evaluate expression: 0 = 00000000`00000000
6: kd> ?? (@efl & ( 1 << 6)) != 0
bool false
단지 각자의 이름이 의미하는 것처럼 언어 차원에서 지원하거나 별도로 확장한 연산자가 있는 정도인데요, 가령 signed right shift 연산자(>>>)는 MASM에서만 지원합니다.
6: kd> ? 0xfa >>> 4
Evaluate expression: 15 = 00000000`0000000f
6: kd> ?? 0xfa >>> 4
Syntax error at '> 4'
// 그 외에 $scmp(), $sicmp(), $spat(), $vvalid 등의 확장 연산자도 MASM 방식에서만 지원
반면, sizeof, 구조체 및 배열 접근 연산자(-> . [])와 형변환 연산자 등은 C++ 방식에서만 지원합니다.
6: kd> ? sizeof(nt!_EPROCESS)
Couldn't resolve error at 'sizeof(nt!_EPROCESS)'
6: kd> ?? sizeof(nt!_EPROCESS)
unsigned int64 0xb80
6: kd> !process 0 0 notepad.exe
PROCESS ffffb0888f7bd0c0
SessionId: 2 Cid: c508 Peb: 7f16d76000 ParentCid: 1b38
DirBase: a75e6000 ObjectTable: ffffa08bafb18540 HandleCount: 929.
Image: Notepad.exe
// MASM 방식에서는 구조체 멤버 접근을 위한 문법이 없으므로 dt 명령어 자체에서 제공하는 기능 사용
6: kd> dt _EPROCESS ffffb0888f7bd0c0 UniqueProcessId
nt!_EPROCESS
+0x440 UniqueProcessId : 0x00000000`0000c508 Void
// C++ 방식에서는 구조체로의 형변환이 가능하고, 그것의 멤버 접근 가능
6: kd> ?? ((nt!_EPROCESS *)0xffffb0888f7bd0c0)->UniqueProcessId
void * 0x00000000`0000c508
6: kd> ?? @$proc->UniqueProcessId
void * 0x00000000`0000c508
이 외에도 C++ 방식에선 그 나름대로 잘 알려진 매크로 함수에 준하는 #CONTAINING_RECORD(), #FIELD_OFFSET(), #RTL_CONTAINS_FIELD(), #RTL_FIELD_SIZE(), #RTL_NUMBER_OF(), #RTL_SIZEOF_THROUGH_FIELD() 등의 몇 가지 함수를 지원합니다.
// C++ 방식에서만 지원
6: kd> ?? #FIELD_OFFSET(nt!_EPROCESS, UniqueProcessId)
long 0n1088
자, 그럼 여기까지 잘 이해했다면
지난 글의 마지막에 "win32k 서비스 함수"를 WinDbg 수식 한 줄로 찾는 실습을 해보겠습니다. ^^
// 실습 환경: Windows 10 (버전 10.0.19045.5247)
//
// 함수표에서 NtGdiInvertRgn의 함수 번호는 Windows 10 환경에서 0x1064
// 1) 0x1064 함수 번호에서 0x1000을 뺀 오프셋을 구하고,
2: kd> dd /c1 win32k!W32pServiceTable + 4*(0x1064 - 0x1000) L1
ffffe8d1`72e59190 ff934140
// 2) 8바이트로 부호 확장
ff934140 ==> ffffffff`ff934140
// 3) 4비트 signed 우측 시프트
ffffffff`ff934140 ==> ffffffff`fff93414
// 4) 최종 주소 평가
2: kd> u win32k!W32pServiceTable + ffffffff`fff93414 L1
win32k!NtGdiInvertRgn:
ffffe8d1`72dec414 4883ec28 sub rsp,28h
우선, MASM 방식으로 평가를 해볼까요? ^^
1) 단계는 해당 포인터가 가리키는 값에서 4바이트를 취해야 하는데요, 이를 위해
dwo 명령을 사용해 다음과 같이 작성할 수 있습니다.
// W32pServiceTable 심벌 풀이를 위해 프로세스 문맥으로 변경
// 대상 주소에서 4바이트 값을 반환
6: kd> ? dwo(win32k!W32pServiceTable + 4*(0x1064 - 0x1000))
Evaluate expression: 4287840576 = 00000000`ff934140
// poi, qwo 명령어의 경우 8바이트를 읽어 반환
6: kd> ? poi(win32k!W32pServiceTable + 4*(0x1064 - 0x1000))
Evaluate expression: -30098576766713536 = ff951180`ff934140
6: kd> ? qwo(win32k!W32pServiceTable + 4*(0x1064 - 0x1000))
Evaluate expression: -30098576766713536 = ff951180`ff934140
위의 값을 이제 2) 단계로 8바이트 부호 확장을 해야 하는데, 이게 좀 어렵습니다. 왜냐하면, dwo는 4바이트를 값을 주소로부터 읽어내기는 하지만 값 자체는 8바이트로 반환하기 때문에 부호 확장을 하려고 해도 상위 4바이트가 0으로 채워져 있어 음수 값으로 인식되지 않습니다.
어쩔 수 없이, 이런 경우에는 좌측 shift 연산자 + signed 우측 shift 연산자를 혼합해 동일한 효과를 내야 합니다.
// 4바이트 값을 8바이트로 부호 확장하는 효과
6: kd> ? dwo(win32k!W32pServiceTable + 4*(0x1064 - 0x1000)) << 0n32 >>> 0n32
Evaluate expression: -7126720 = ffffffff`ff934140
그럼 자연스럽게 3) 단계도 shift 크기만 늘리는 것으로 해결됩니다.
6: kd> ? dwo(win32k!W32pServiceTable + 4*(0x1064 - 0x1000)) << 0n32 >>> 0n36
Evaluate expression: -445420 = ffffffff`fff93414
마지막으로 4) 단계에서는 위에서 구한 주솟값을 그대로 "u" 명령어에 적용해 완성할 수 있습니다.
// Windows 10의 경우 win32k의 0x1064 함수는 NtGdiInvertRgn
0: kd> u win32k!W32pServiceTable + (dwo(win32k!W32pServiceTable + 4*(0x1064 - 0x1000)) << 0n32 >>> 0n36) L1
win32k!NtGdiInvertRgn:
fffffb93`0e2ac414 4883ec28 sub rsp,28h
이제 동일한 과정을 C++ 방식으로 해볼까요? ^^
1) 단계는 심벌 해석을 MASM 방식으로 바꿔서 처리하는 것만 신경 쓰면 되는데요,
6: kd> ?? *(long *)(@@(win32k!W32pServiceTable) + 4*(0x1064 - 0x1000))
long 0n-7126720
6: kd> ? 0n-7126720
Evaluate expression: -7126720 = ffffffff`ff934140
재미있게도 MASM 방식과는 달리 4바이트 값을 읽기 위해 (long *) 형변환으로 처리했기 때문에 자연스럽게 8바이트로 부호 확장까지 이뤄져 2) 단계가 필요 없게 되었습니다. 따라서 곧바로 3) 단계 처리로 갈 수 있는데요, 문제는 C++ 방식의 경우 signed right shift 연산을 지원하지 않기 때문에 여기서 다시 MASM 방식과 혼용을 해야 합니다.
6: kd> ?? @@( @@( *(long *)(@@(win32k!W32pServiceTable) + 4*(0x1064 - 0x1000)) ) >>> 4 )
unsigned int64 0xffffffff`fff93414
==> ?? @@masm( @@c++( *(long *)(@@masm(win32k!W32pServiceTable) + 4*(0x1064 - 0x1000)) ) >>> 4 )
갑자기 복잡해졌죠? ^^ 어쨌든, 저렇게 평가한 주솟값을 4) 단계에서 u 명령어로 적용하면 되는데요, 이 과정에서 다시 식 평가 방식을 고려해야 합니다.
왜냐하면, "??" 명령어와 "u" 명령어는 각자 고유의 명령어이기 때문에 "?? u ..."와 같은 식으로 명령을 사용할 수는 없습니다. 즉, 독자적으로 "u" 명령어만 사용해야 하는데요, 그렇다면 이제 u 명령어 내부에서 식 평가를 하는 것이기 때문에 WinDbg의 기본 방식인 MASM 방식으로 평가가 됩니다.
따라서 최종 식은 다음과 같이 작성하면 됩니다.
6: kd> u win32k!W32pServiceTable + ( @@( *(long *)( @@(win32k!W32pServiceTable) + 4*(0x1064 - 0x1000) ) ) >>> 4 ) L1
win32k!NtGdiInvertRgn:
fffffb93`0e2ac414 4883ec28 sub rsp,28h
==> u win32k!W32pServiceTable + ( @@c++( *(long *)( @@masm(win32k!W32pServiceTable) + 4*(0x1064 - 0x1000) ) ) >>> 4 ) L1
물론, 기본 평가 방식이 C++로 설정됐다면 이렇게 바꿔서 처리할 수 있고!
6: kd> .expr /s c++
Current expression evaluator: C++ - C++ source expressions
6: kd> u @@(win32k!W32pServiceTable) + @@( @@( *(long *)(@@(win32k!W32pServiceTable) + 4*(0x1064 - 0x1000)) ) >>> 4 ) L1
win32k!NtGdiInvertRgn:
fffffb93`0e2ac414 4883ec28 sub rsp,28h
음... ^^; 의외로 복잡하죠? 왠지 저런 수식을 단 번에 만드는 것은 쉽지 않을 듯합니다. ^^ 어쨌든 저렇게 완성했으면, 마지막으로 사용자 정의 가상 레지스터를 곁들이면 그럴싸하게 공식처럼 사용하는 것도 가능합니다.
// 0x1064 함수 번호의 win32k 서비스 함수를 찾고 싶다면?
0: kd> r $t0 = 0x1059
6: kd> u win32k!W32pServiceTable + (dwo(win32k!W32pServiceTable + 4*($t0 - 0x1000)) << 0n32 >>> 0n36) L1
win32k!NtGdiPatBlt:
fffffb93`0e2ac67c 4883ec48 sub rsp,48h
6: kd> u win32k!W32pServiceTable + ( @@( *(long *)(@@(win32k!W32pServiceTable) + 4*(@@($t0) - 0x1000)) ) >>> 4 ) L1
win32k!NtGdiPatBlt:
fffffb93`0e2ac67c 4883ec48 sub rsp,48h
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]