64비트 콜스택을 좀 보다보니 수동으로 따라갈 수 있는 방법을 하나 알게 되었다.
거의 포기하고 있었는데 보다보니 이런 것들이 보이기 시작하는구나... ㅋㅋ
이론을 먼저 설명하면 머리만 복잡해지니 예제부터 보기로 한다.
다음과 같은 콜스택이 있다고 해 보자.
kd> k
Child-SP RetAddr Call Site
fffff880`009adeb0 fffff800`019a6135 nt!ExAllocatePoolWithTag+0x326
fffff880`009adfa0 fffff800`019a3e3e nt!RtlpInheritAcl+0xc5
fffff880`009ae070 fffff800`0197f6c2 nt!RtlpNewSecurityObject+0x8ce
fffff880`009ae300 fffff800`0197e582 nt!ObpAssignSecurity+0x82
fffff880`009ae370 fffff800`01980db3 nt!ObInsertObjectEx+0x1e2
fffff880`009ae5c0 fffff800`019801be nt!PspInsertThread+0x2f3
fffff880`009ae740 fffff800`0192f679 nt!PspCreateThread+0x246
fffff880`009ae9c0 fffff800`01795352 nt!PsCreateSystemThread+0x125
fffff880`009aeab0 fffff800`01bc751d nt!DisplayBootBitmap+0x162
fffff880`009aeb00 fffff800`01b18e29 nt!Phase1InitializationDiscard+0x17d
fffff880`009aecd0 fffff800`0192f73a nt!Phase1Initialization+0x9
fffff880`009aed00 fffff800`016848e6 nt!PspSystemThreadStartup+0x5a
fffff880`009aed40 00000000`00000000 nt!KiStartSystemThread+0x16
ExAllocatePoolWithTag 중간쯤에서 멈춰있는 상태다.
ExAllocatePoolWithTag 함수에서 사용중인 스택 베이스 포인터가 Child-SP fffff880`009adeb0로 나타나고 있다.
콜스택을 수동으로 추적하려면 각 함수의 스택 프레임의 기준을 찾는 것이 중요한데 Child-SP가 기준이라고 보면 된다.
Child-SP를 따라갈 수 있으면 되는데... 어떻게 할 수 있는지 확인해 보자.
ExAllocatePoolWithTag 함수에서 멈춰 있는 상태에서 레지스터 상태를 보면 다음과 같다.
kd> r
rax=fffff8a000006000 rbx=fffffa8000c2d280 rcx=0000000000000001
rdx=0000000000000001 rsi=000000000000000e rdi=0000000000000001
rip=fffff800017c7f36 rsp=fffff880009adeb0 rbp=0000000000001000
r8=0000000000000000 r9=0000000000000000 r10=0000000000000000
r11=fffff880009ade80 r12=fffffa8000c2c140 r13=0000000000000000
r14=0000000000000002 r15=0000000063416553
iopl=0 nv up ei ng nz na po nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00000286
nt!ExAllocatePoolWithTag+0x326:
fffff800`017c7f36 448928 mov dword ptr [rax],r13d ds:002b:fffff8a0`00006000=fe05a30d
rsp가 Child-SP와 같은 값을 가지고 있다.
일단 이론을 보기 전에 하나는 그냥 외우고 시작하자.
64비트에서는 기본적으로 rsp가 32비트에서의 ebp와 비슷한 역할을 한다.
64비트 코드를 분석해 보면 알겠지만 대부분 rsp를 거의 변경하지 않고 사용한다.
따라서 ExAllocatePoolWithTag는 rsp를 기준으로 자신의 스택을 사용하고 있다.
여기서 상위 함수의 rsp 값을 찾으려면 ExAllocatePoolWithTag의 코드 앞부분(프롤로그)을 보면 된다.
kd> u nt!ExAllocatePoolWithTag
nt!ExAllocatePoolWithTag:
fffff800`017c7c10 fff5 push rbp
fffff800`017c7c12 57 push rdi
fffff800`017c7c13 4157 push r15
fffff800`017c7c15 4881ecd0000000 sub rsp,0D0h
fffff800`017c7c1c 8bc1 mov eax,ecx
fffff800`017c7c1e 458bf8 mov r15d,r8d
fffff800`017c7c21 448944245c mov dword ptr [rsp+5Ch],r8d
fffff800`017c7c26 83e044 and eax,44h
함수에 진입하자마자 push를 3번하면서 스택을 약간 사용하고 이 함수가 사용할 스택 크기를 확보하는 코드가 있다.
여기에 상위함수가 ExAllocatePoolWithTag를 호출하면서 자신의 리턴 주소를 push하는 것까지만 추가로 고려하면 함수 호출시에 사용된 스택 크기를 알 수 있다.
현재 rsp에서 이 크기를 계산해 주면 상위 함수의 rsp가 나온다.
따라서 상위 함수의 기준 rsp를 정확히 구하는 식은 다음과 같다.
stacksize = 8 (리턴주소 push) + 8*3 (push 3번) + 0D0h (rsp에서 빼준 값)
상위 함수 rsp = 현재 함수 rsp + stacksize
ExAllocatePoolWithTag의 stacksize 계산 결과는 0F0h다.
다음과 같이 계산할 수 있다.
kd> ? fffff880`009adeb0 + F0
Evaluate expression: -8246327058528 = fffff880`009adfa0
계산된 fffff880`009adfa0는 콜스택을 확인해 보면 두번째 함수인 RtlpInheritAcl의 Chile-SP와 일치한다.
계속해서 같은 방법으로 계속 따라갈 수 있다.
기계적으로 한번 진행해 보자.
함수 프롤로그 보고...
kd> u nt!RtlpInheritAcl
nt!RtlpInheritAcl:
fffff800`019a6070 fff3 push rbx
fffff800`019a6072 55 push rbp
fffff800`019a6073 56 push rsi
fffff800`019a6074 57 push rdi
fffff800`019a6075 4881eca8000000 sub rsp,0A8h
fffff800`019a607c 410fb6f1 movzx esi,r9b
fffff800`019a6080 418bd8 mov ebx,r8d
fffff800`019a6083 488bea mov rbp,rdx
stacksize를 계산한다.
stacksize = 8 + 8*4 + 0A8h = D0h
kd> ? fffff880`009adfa0 + D0
Evaluate expression: -8246327058320 = fffff880`009ae070
fffff880`009ae070은 RtlpNewSecurityObject의 Child-SP와 일치한다.
RtlpNewSecurityObject를 따라가 보자.
kd> u nt!RtlpNewSecurityObject
nt!RtlpNewSecurityObject:
fffff800`019a3570 fff3 push rbx
fffff800`019a3572 55 push rbp
fffff800`019a3573 57 push rdi
fffff800`019a3574 4881ec70020000 sub rsp,270h
fffff800`019a357b 488b055eb4e6ff mov rax,qword ptr [nt!_security_cookie (fffff800`0180e9e0)]
fffff800`019a3582 4833c4 xor rax,rsp
fffff800`019a3585 4889842450020000 mov qword ptr [rsp+250h],rax
fffff800`019a358d 488b8424d0020000 mov rax,qword ptr [rsp+2D0h]
stacksize = 8 + 8*3 + 270h = 290h
kd> ? fffff880`009ae070 + 290
Evaluate expression: -8246327057664 = fffff880`009ae300
fffff880`009ae300은 ObpAssignSecurity의 Child-SP와 일치한다.
ObpAssignSecurity를 확인한다.
kd> u nt!ObpAssignSecurity
nt!ObpAssignSecurity:
fffff800`0197f640 48895c2410 mov qword ptr [rsp+10h],rbx
fffff800`0197f645 48896c2418 mov qword ptr [rsp+18h],rbp
fffff800`0197f64a 56 push rsi
fffff800`0197f64b 57 push rdi
fffff800`0197f64c 4154 push r12
fffff800`0197f64e 4883ec50 sub rsp,50h
fffff800`0197f652 488364247000 and qword ptr [rsp+70h],0
fffff800`0197f658 4d8be0 mov r12,r8
stacksize = 8 + 8*3 + 50h = 70h
kd> ? fffff880`009ae300 + 70
Evaluate expression: -8246327057552 = fffff880`009ae370
fffff880`009ae370은 ObInsertObjectEx의 Child-SP와 일치한다.
계속해서 ObInsertObjectEx를 따라가 본다.
kd> u nt!ObInsertObjectEx
nt!ObInsertObjectEx:
fffff800`0197e3a0 fff3 push rbx
fffff800`0197e3a2 55 push rbp
fffff800`0197e3a3 56 push rsi
fffff800`0197e3a4 57 push rdi
fffff800`0197e3a5 4154 push r12
fffff800`0197e3a7 4155 push r13
fffff800`0197e3a9 4156 push r14
fffff800`0197e3ab 4157 push r15
kd> u
nt!ObInsertObjectEx+0xd:
fffff800`0197e3ad 4881ec08020000 sub rsp,208h
fffff800`0197e3b4 488b052506e9ff mov rax,qword ptr [nt!_security_cookie (fffff800`0180e9e0)]
fffff800`0197e3bb 4833c4 xor rax,rsp
fffff800`0197e3be 48898424f0010000 mov qword ptr [rsp+1F0h],rax
fffff800`0197e3c6 488bac2480020000 mov rbp,qword ptr [rsp+280h]
fffff800`0197e3ce 4c8bf9 mov r15,rcx
fffff800`0197e3d1 4533d2 xor r10d,r10d
fffff800`0197e3d4 410fb647e8 movzx eax,byte ptr [r15-18h]
이번엔 u 명령 한번으로는 sub rsp, 208h까지 보이지 않아서 u 명령을 한번 더 사용했다.
마찬가지로 모든 push와 sub rsp 명령에서 사용한 스택 사용량을 계산하면 된다.
stacksize = 8 + 8*8 + 208h = 250h
kd> ? fffff880`009ae370 + 250
Evaluate expression: -8246327056960 = fffff880`009ae5c0
fffff880`009ae5c0는 PspInsertThread의 Child-SP와 일치한다.
이 정도면 충분히 연습이 됐을 것 같다.
모두 같은 방법으로 따라간 것이고 계속해서 같은 방법으로 진행할 수 있다
너무 길어졌으므로 이에 대한 이론은 다음 포스팅에서 다루도록 하겠다.