* 모든 내용은 Windows 7 x64 를 기반으로 진행되었음 *  


짬을 내서, 64비트 시스템의 시스템콜 메커니즘에 관해서 한번 살펴보았다. x86에 관한 내용은 인터넷에 매우 많은데 비해, x64에 관한 내용은 그다지 많지가 않아서, 나름대로 정리를 해보려고 한다. 일단 시스템콜의 순서를 서술해보면 다음과 같다.


Native Service를 요청할 경우
kernel32!CreateFileA -> kernel32!CreateFileW -> ntdll!NtCreateFile -> nt!KiSystemCall64 -> nt!NtCreateFile

GUI Service를 요청할 경우
USER32!NtUserCallNoParam -> nt!KiSystemCall64 -> win32k!NtUserCallNoParam

 
  공통적인 점은, Native건 GUI건 모두 KiSystemCall64를 통과한다는 것이다. 관심 있는사람이라면 다 알다시피, x86 Windows에서는 SYSENTER를 사용하였고, 이는 곧 MSR의 0x176번지인 IA32_SYSENTER_EIP를 참조하여, KiFastCallEntry 함수의 주소를 EIP에 대입하여 실행되는 형태였다. 그러나 64비트에서는 좀 다른데, 이는 유저모드의 NtXXXXXX 함수의 코드를 살펴보면 금방 확인할 수 있다.

00000000`76fcd190 4c8bd1          mov     r10,rcx
00000000`76fcd193 b805100000      mov     eax,1005h
00000000`76fcd198 0f05            syscall
00000000`76fcd19a c3              ret

위 코드는  USER32!NtUserCallNoParam 함수의 코드를 따온것이다. (Export 된 함수는 아님) 살펴보면, syscall 명령어가 있는것을 확인할 수 있는데, SYSCALL과 SYSENTER는 엄연히 다르다. 인텔IA32 문서를 참조해보면 다음과 같이 나와있다.

 SYSCALL saves the RIP of the instruction following SYSCALL to RCX and loads a new RIP from the IA32_LSTAR (64-bit mode). Upon return, SYSRET copies the value saved in RCX to the RIP.

IA32_LSTAR로부터 64비트만큼 값을 읽어와서 RIP에 대입한다고 되어져 있는데, IA32_LSTAR에 관해서도 알아봐야 될것 같다.

Register Address는 0xC0000082 이며, 이름은 IA32_LSTAR 이다.
따라서 rdmsr 0xC0000082 를 쳐보면, KiSystemCall64의 주소가 나올것이다.

1: kd> rdmsr C0000082
msr[c0000082] = fffff800`02cd8ec0
1: kd> r $t0 = fffff800`02cd8ec0
1: kd> u @$t0
nt!KiSystemCall64:
fffff800`02cd8ec0 0f01f8          swapgs
fffff800`02cd8ec3 654889242510000000 mov   qword ptr gs:[10h],rsp
fffff800`02cd8ecc 65488b2425a8010000 mov   rsp,qword ptr gs:[1A8h]
fffff800`02cd8ed5 6a2b            push    2Bh
fffff800`02cd8ed7 65ff342510000000 push    qword ptr gs:[10h]
fffff800`02cd8edf 4153            push    r11
fffff800`02cd8ee1 6a33            push    33h
fffff800`02cd8ee3 51              push    rcx



  이제 KiSystemCall64까지 오는건 알았는데, 그다음부터 실제 Service Function이 어떻게 호출되는지를 살펴봐야 될것이다. 32비트 시스템에서는 SSDT를 살펴보면 , 그냥 Direct로 Service Routine의 주소가 때려박혀져 있었다. 그러나 x64에서는 일부는 그대로 대입되어져 있는 반면, 대부분은 이상한 QWORD 값으로 저장되어져 있다. 아래와 같이 말이다.

1: kd> dqs nt!KeServiceDescriptorTable
fffff800`02f10840  fffff800`02cdab00 nt!KiServiceTable
fffff800`02f10848  00000000`00000000
fffff800`02f10850  00000000`00000191
fffff800`02f10858  fffff800`02cdb78c nt!KiArgumentTable
fffff800`02f10860  00000000`00000000
fffff800`02f10868  00000000`00000000
fffff800`02f10870  00000000`00000000
fffff800`02f10878  00000000`00000000
fffff800`02f10880  fffff800`02cdab00 nt!KiServiceTable
fffff800`02f10888  00000000`00000000
fffff800`02f10890  00000000`00000191
fffff800`02f10898  fffff800`02cdb78c nt!KiArgumentTable
fffff800`02f108a0  fffff960`001c1c00 win32k!W32pServiceTable
fffff800`02f108a8  00000000`00000000
fffff800`02f108b0  00000000`0000033b
fffff800`02f108b8  fffff960`001c391c win32k!W32pArgumentTable
1: kd> dqs poi(nt!KeServiceDescriptorTable) L191

..............................
..............................
fffff800`02cdb0c8  03216b01`0456b900
fffff800`02cdb0d0  02b88980`04809300
fffff800`02cdb0d8  0450ca00`01151200
fffff800`02cdb0e0  042b9500`045f7400
fffff800`02cdb0e8  02518280`044e9800
fffff800`02cdb0f0  0261f900`028f83c2
fffff800`02cdb0f8  00fdc700`02ca6b40
fffff800`02cdb100  02a7b402`03d60700
fffff800`02cdb108  0386dd00`04259c00
fffff800`02cdb110  028af440`03e82d00
fffff800`02cdb118  04504800`028a0c00
fffff800`02cdb120  01146500`02726801
fffff800`02cdb128  04381b00`04649600
fffff800`02cdb130  000f5100`02e8e2c0
fffff800`02cdb138  041f0900`041f0000
fffff800`02cdb140  fffff800`ffe74c40
fffff800`02cdb148  fffff800`03103090 nt!NtFreezeTransactions
fffff800`02cdb150  fffff800`02f2c280 nt!NtGetContextThread
fffff800`02cdb158  fffff800`02f68610 nt!NtGetCurrentProcessorNumber
fffff800`02cdb160  fffff800`030fe2b0 nt!NtGetDevicePowerState
fffff800`02cdb168  fffff800`02f90788 nt!NtGetMUIRegistryInfo
fffff800`02cdb170  fffff800`0310bb80 nt!NtGetNextProcess
fffff800`02cdb178  fffff800`0310b890 nt!NtGetNextThread
..................................................
..................................................


가만히 생각해보면 이상하지 않은가? 분명히 System Service Routine의 주소를 얻어와서 CALL  하는 과정은 KiSystemCall64에서 이루어지고,  그 속에서 Service Routine의 주소를 얻어오는 일련의 과정은  동일할텐데, 어떤 서비스는 주소값이 제대로 들어가있고 , 어떤 서비스는 정체불명의 QWORD값의 형태로 들어있다니?  

결론부터 말하면 이리저리 살펴본 결과, 위 의문점에 관해서 의문을 가질 필요가 없다는것을 알게되었다.
그러기 위해선 KiSystemServiceStart 주변부의 어셈블리 코드를 살펴볼 필요가 있다.
KiSystemServiceStart는 KiSystemCall64에서 조금만 밑으로 디스어셈블 하다보면 나오는 부분이다.
이제  실행순서 및 코드를 살펴보자.



USER32!NtUserCallNoParam (사실, 이 함수는 Export 되지 않은 함수)

00000000`76fcd190 4c8bd1          mov     r10,rcx
00000000`76fcd193 b805100000      mov     eax,1005h
00000000`76fcd198 0f05            syscall
00000000`76fcd19a c3              ret



1: kd> u @$t0 L 100
nt!KiSystemCall64:
fffff800`02cd8ec0 0f01f8          swapgs
fffff800`02cd8ec3 654889242510000000 mov   qword ptr gs:[10h],rsp
fffff800`02cd8ecc 65488b2425a8010000 mov   rsp,qword ptr gs:[1A8h]
fffff800`02cd8ed5 6a2b            push    2Bh
fffff800`02cd8ed7 65ff342510000000 push    qword ptr gs:[10h]
.....................
.....................
//현재 쓰레드의 PKTHREAD를 얻어옴.
fffff800`02cd8f14 65488b1c2588010000 mov   rbx,qword ptr gs:[188h]
.....................
.....................
fffff800`02cd8fce 6690            xchg    ax,ax
fffff800`02cd8fd0 fb              sti
//서비스의 첫번째 파라메터를 PKTHREAD->FirstArgument 에 저장
fffff800`02cd8fd1 48898be0010000  mov     qword ptr [rbx+1E0h],rcx
//시스템 서비스 번호를 PKTHREAD->SystemCallNumber 에 저장
fffff800`02cd8fd8 8983f8010000    mov     dword ptr [rbx+1F8h],eax
nt!KiSystemServiceStart:
//커널 트랩 프레임을  PKTHREAD->TrapFrame 에 저장
fffff800`02cd8fde 4889a3d8010000  mov     qword ptr [rbx+1D8h],rsp
//시스템 서비스 넘버를 EDI에 넣음. EDI는 GUI서비스인지 판별하려는 의도 (EAX에는 0x1005 가 있음) 

fffff800`02cd8fe5 8bf8            mov     edi,eax            //Rax = 0x1005 , Rdi = 0x1005
fffff800`02cd8fe7 c1ef07          shr     edi,7               //Right Shift 하여 하위 7비트 모두 잘라냄. (RDI = 0x20)
fffff800`02cd8fea 83e720          and     edi,20h           //GUI서비스인지 AND를 이용해서 판별. ( RDI = 0x20 )
fffff800`02cd8fed 25ff0f0000      and     eax,0FFFh       //테이블 비트를 잘라내고 서비스 넘버만을 검출 ( RAX = 0x0005 )
nt!KiSystemServiceRepeat:
// DescriptorTable과 , ShadowTable의 주소를 각각 r10과 r11에 대입
fffff800`02cd8ff2 4c8d1547782300  lea     r10,[nt!KeServiceDescriptorTable (fffff800`02f10840)]
fffff800`02cd8ff9 4c8d1d80782300  lea     r11,[nt!KeServiceDescriptorTableShadow (fffff800`02f10880)]
// if ( PKTHREAD->GuiThread ) 가 유효하다면, 즉 GuiThread라면
fffff800`02cd9000 f7830001000080000000 test dword ptr [rbx+100h],80h
// Conditional MOVe if Not Equal  즉 , KeServiceDescriptorTableShadow을 r10에 대입.
// GuiThread가 아니라면 , KeServiceDescriptorTable 의 값이 r10에 그대로 보존 됨. 
fffff800`02cd900a 4d0f45d3        cmovne  r10,r11
fffff800`02cd900e 423b441710      cmp     eax,dword ptr [rdi+r10+10h]
fffff800`02cd9013 0f83e9020000    jae     nt!KiSystemServiceExit+0x1a7 (fffff800`02cd9302)
// r10 = * (KeServiceDescriptorTableShadow + 0x20) 즉,  win32k!w32pServiceTable을 r10에 대입
fffff800`02cd9019 4e8b1417        mov     r10,qword ptr [rdi+r10]
// w32pServiceTable + Service Number * 4DWORD만큼 R11에 저장한다. QWORD만큼 읽는게 아니다!!
fffff800`02cd901d 4d631c82        movsxd  r11,dword ptr [r10+rax*4] 

참고로 이때 w32pServiceTable + 0x05 * 4 의 값은 아래와 같았다.
1: kd> dds win32k!w32pServiceTable + 0x05 * 4
fffff960`001c1c14  00022700
 
fffff800`02cd9021 498bc3          mov     rax,r11               // r11 = 0x022700
// 서비스 테이블에서 DWORD만큼 읽었던것을 4만큼 Arithmetic Shift Right 함. 
fffff800`02cd9024 49c1fb04        sar     r11,4               // r11 = 0x2270
// R10  실제 Service Routine = w32pServiceTable + 0x2270 
fffff800`02cd9028 4d03d3          add     r10,r11          // r10 = 실제서비스루틴   
fffff800`02cd902b 83ff20          cmp     edi,20h
...............................
...............................
 nt!KiSystemServiceCopyEnd:
fffff800`02cd9140 f705fee4180040000000 test dword ptr [nt!PerfGlobalGroupMask+0x8 (fffff800`02e67648)],40h
fffff800`02cd914a 0f8550020000    jne     nt!KiSystemServiceExit+0x245 (fffff800`02cd93a0)
fffff800`02cd9150 41ffd2          call    r10        //서비스 호출
fffff800`02cd9153 65ff042538220000 inc     dword ptr gs:[2238h]
 

자, 이제 서비스 호출과정이 대략적으로 다 밝혀진것 같다. 
이제 이걸 WinDBG 스크립트로 한번 만들어보자.
그럼 모든 서비스의 실제 주소를 추출해낼 수 있을것 같다.
(사실, WinDBG 스크립트에 익숙치 않아서 , 이 스크립트를 짜기까지 많은 삽질이 있었따 ㅠㅠ)



// sdt.txt    -> for KeServiceDescriptorTable  x64 system
r $t0 = nt!KeServiceDescriptorTable

.for (r $t2 = 0; (@$t2 < poi(@$t0 + 0x10) * 0x04); r $t2 = @$t2 + 0x04)
{
r $t3 = poi(poi(@$t0) + @$t2)
r? $t4 = (long*) @$t3
.if ( (@$t4 & 00000000`f0000000) == 0 ) { r $t4 = @$t4 & 00000000`ffffffff }
.else { r $t4 = @$t4 | ffffffff`00000000 }
r $t4 = @$t4 >>> 4
r $t5 = poi(@$t0) + @$t4
u @$t5 L1
}
//////////////////// end of sdt.txt ////////////////////





// sdtshadow.txt   -> for KeServiceDescriptorTableShadow   x64 system
r $t1 = nt!KeServiceDescriptorTableShadow + 0x20

.for (r $t2 = 0; (@$t2 < poi(@$t1 + 0x10) * 0x04); r $t2 = @$t2 + 0x04)
{
r $t3 = poi(poi(@$t1) + @$t2)
r? $t4 = (long*) @$t3
.if ( (@$t4 & 00000000`f0000000) == 0 ) { r $t4 = @$t4 & 00000000`ffffffff }
.else { r $t4 = @$t4 | ffffffff`00000000 }
r $t4 = @$t4 >>> 4
r $t5 = poi(@$t1) + @$t4
u @$t5 L1
}
//////////////////////// end of sdtshadow.txt ////////////////////////



결과는 아래와 같다.

Result of SDT
nt!NtMapUserPhysicalPagesScatter:
fffff800`030eb190 48895c2408      mov     qword ptr [rsp+8],rbx
nt!NtWaitForSingleObject:
fffff800`02fd1a00 4c8bdc          mov     r11,rsp
nt!NtCallbackReturn:
fffff800`02cd1dd0 4883ec18        sub     rsp,18h
nt!NtReadFile:
fffff800`02ff4b10 4c8bdc          mov     r11,rsp
nt!NtDeviceIoControlFile:
fffff800`02ff2bb0 4883ec68        sub     rsp,68h
...........................
nt!NtAddBootEntry:
fffff800`0311d7d0 ??              ???

이상하게 nt!NtAddBootEntry 에서 메모리를 읽을수가 없는데, 왜 그런지 이유는 나중에 살펴봐야될것 같다.
일단 , 이 에러를 해결하기 위해서 부트타임에 실행을 중단시키고 , 서비스를 읽으니 잘 읽혔다.
아마 패치가드때문인것으로 생각된다.


 
Result of SDTSHADOW
1: kd> $$><myscript\sdtshadow.txt
win32k!NtUserGetThreadState:
fffff960`001b5580 48895c2408      mov     qword ptr [rsp+8],rbx
win32k!NtUserPeekMessage:
fffff960`001b2630 48895c2408      mov     qword ptr [rsp+8],rbx
win32k!NtUserCallOneParam:
fffff960`001c3c6c 48895c2408      mov     qword ptr [rsp+8],rbx
win32k!NtUserGetKeyState:
fffff960`001d1dd0 48895c2408      mov     qword ptr [rsp+8],rbx
......................... 
.........................
win32k!NtUserYieldTask:
fffff960`001ce06c 48895c2408      mov     qword ptr [rsp+8],rbx
win32k!NtUserSetClassLongPtr:
fffff960`001c6b84 48895c2408      mov     qword ptr [rsp+8],rbx
win32k!NtUserSetWindowLongPtr:
fffff960`001a1ca0 48895c2408      mov     qword ptr [rsp+8],rbx


win32k는 정상적으로 모두 출력된다.
 결과 파일은 따로 파일로 첨부한다.
 



그리고 스크립트 파일도 따로 첨부한다.




이제 이러한 내용을 바탕으로 패치가드를 무력화시키고, 후킹을 하기만 한다면 64비트에서도 후킹은 성공할 수 있을것 같다.
패치가드 무력화에 관한 방법들은 이미 몇몇 문서가 공개된것으로 알고있다.


ps. NtAddBootEntry 가 왜 안읽히는지 아시는분?
저작자 표시 비영리 변경 금지
신고
by Sone 2011.06.14 06:51

디버깅이라함은 , 이제 막 프로그램 개발에 입문하였거나, 현직 개발자 직업으로 일하고 계신분이나,
혹은  의도치않게 상용바이너리의 버그를 발견하고 이에 대해 분석할때,  자주 이용되는 기술이라고 생각된다.

디버깅툴은 이미 많이들 알고계신대로 , 가장 많이 대중적으로 쓰여지는 대표적인 것들을 나열해보자면, 

유저모드 디버깅/
MS Visual Studio , Olly Debugger, IDA  , WinDBG

커널모드 디버깅/
WinDBG , SoftIce (최근의 운영체제에서는 동작하지 않는다는것이 아쉬울뿐이다.)

요즘 대부분의 디버거들은 개발자에게 보다 친숙하게 느껴질수 있도록 하기위하여 , 
GUI 기반의 인터페이스를 지원하고 , 접근이 쉽게쉽게 이루어질수 있도록 구성되어져 있다.

WinDBG는  GUI 환경으로 개발되어졌다고는 하지만,  90%는 콘솔 인터페이스 기반이라고 봐도 무관한데,
이는 대부분 개발자들에게 접근성이 떨어지는 결과를 낳게되었고, 개발자들 사이에서 기피하는 디버거가 되버렸다.
위에서 확인할 수 있다시피 , WinDBG는  유저와 커널 모두 접근할 수 있고 ,  다양한 기능들을 제공해주지만
사용법이 어렵다는 이유만으로 기피되고 있는것이다.

그러던 와중에!  국내 개발자분들께서 WinDBG 사용법을 널리 전파하고자, WinDBG 사용법을 초보자부터 봐도 자세하고 쉽게 알수있게끔 아주 좋은 책을 내주셨다.

SAMSUNG TECHWIN | NV24HD, VLUU NV24HD, LANDIAO NV106HD | Normal program | Pattern | 1/30sec | F/2.8 | 0.00 EV | 4.3mm | ISO-120 | Off Compulsory | 2009:09:21 18:10:56
SAMSUNG TECHWIN | NV24HD, VLUU NV24HD, LANDIAO NV106HD | Normal program | Pattern | 1/30sec | F/2.8 | 0.00 EV | 4.3mm | ISO-120 | Off Compulsory | 2009:09:21 18:11:26



개인적으로 WinDBG 사용법을 너무도 모르고 있던터라 바로 질렀다!
(가끔 드라이버 엔트리포인트 앞단에 __asm int 3h(0xCC)  를 걸고  디버깅을 하곤하는데, 기본적인 bp  ba  bl bc  kb  uf  dt  process 등 이정도밖에 모르고 있던터라...)

이번에도 에이콘 출판사인데 , 에이콘 출판사 책이 지금 몇개나 있는지 궁금하구랴..
어쨌든  이게 책 홍보하는 글인가,  개인적인 생각을 쓰는건가는 잘 모르겠는데 

초보자도 매우 쉽게 접근할수있도록,  툴의 설치법 , 세팅법,  크래시파일 확보방법부터 시작해서
고급적인 내용까지  순차적으로  자세하게 다루고있는것 같다.(WinDBG의 또다른 장점이라함은 , 크래시파일 분석!)

블루스크린 케이스별  디버깅방법도 다루고있는것도 흥미로운 부분이라고 생각된다.

구매전에 주의할점은 ,  이 책의 저자는  C/C++ 언어와  기초적인 디버깅 지식(Visual Studio로 한번쯤 해봤다! 하는정도)
,  운영체제의 지식을 알고있다고 가정하고 설명을 진행한다는 것이다.
운영체제를 알아야 하는 이유는 , 아무래도 커널 디버깅또한 다루기때문에 , 프로세스 구조체 , 쓰레드 구조체  , 핸들 , Usage Count  등등등  윈도우 운영체제에 대한 용어와 개념들이 많이 등장하기때문에 요구되는것으로 보인다.

시간날때마다 틈틈히 책을 보면서 즐거운 삽질(?) 을 해봐야겠다.
(사실 디버깅도 개발경험에서 중요한 부분이기 때문에 삽질이라는 용어는 적절치 않은듯
근데 어찌보면 삽질이라는 의미가 맞는것 같기도??  뭥미 )
저작자 표시 비영리 변경 금지
신고
by Sone 2009.09.21 18:33
| 1 |