본문 바로가기

My Study/Programming&Theory

Driver에서 ZwReadVirtualMemory 사용하기


X64 환경에서 진행되었습니다.

드라이버에서 특정 프로세스의 유저모드 메모리를 읽어오려는 작업을 하고 있었습니다.

컨텍스트를 타겟 프로세스 컨텍스트로 바꿔주고 유저메모리에서 값을 읽어오기 위해 ZwReadVirtualMemory를 코드에 썼습니다. 하지만 해당 함수는 nt 에서 export 하고 있지않아 컴파일 에러가 뜨더군요.

할수 없이 커널의 SDT에서 NtReadVirtualMemory 주소 값을 얻어와 사용했습니다. 하지만! 접근 위반이라는 에러를 내뿜더군요. ㅠㅠ


커널 상에서 강제로 ZwReadVirtualMemory로 주소 값을 바꿔서 진행해보니 잘 얻어오는 것이었습니다. 으잉?!

Nt와 Zw 차이점을 아시는 분이라면 왜 이러한 결과가 발생하는지 대충 짐작하실 수 있겠습니다 :D

여기선 자세한 설명은 생략~


중요한건 그러면 우리 드라이버에선 어떻게 NT모듈의 ZwReadVirtualMemory를 사용할 수 있느냐입니다..

구글링을 해도 딱히 나오는게 없더군요. 하지만 이를 구현한 프로그램이 있습니다. ExploitShield 프로그램 입니다.

해당 프로그램의 커널 모듈은 내부적으로 ZwReadVirtualMemory 함수를 쓰고 있기 때문에 어쩔수 없이 리버싱을해 분석을 해보았습니다. 조금 시간이 걸렸지만 어떻게 얻어오는지 알아냈습니다. 리버싱 과정은 생략하고 방법만 간단히 적겠습니다.


모든 코드는 x64 driver에서 작동 중입니다.

1. ZwQuerySystemInformation 함수를 사용해 시스템의 ntdll.dll의 모듈 주소를 얻어옵니다.

   - 얻기 실패한 경우 직접 "\SystemRoot\System32\ntdll.dll" 파일을 MapView를 사용해 메모리에 올려주면 됩니다.

      ( ZwCreateFile, ZwCreateSection, ZwMapViewOfSection 조합 )

2. 얻어온 ntdll.dll 모듈의 export table을 따라가 ntdll ! ZwReadVirtualMemory 의 주소를 알아냅니다.

3. 얻어온 ntdll ! ZwReadVirtualMemory 주소에서 +4를해 해당 함수의 SystemCall Number를 가져옵니다. - A (0x3C)

4. 2,3 과정과 마찬가지로 ntdll ! ZwAllocateVirtualMemory 의 SystemCall Number를 가져옵니다. - B (0x15)


여기서 잠시 nt 모듈의 ZwAllocateVirtualMemory의 주소 부분의 코드를 올리겠습니다.

( ZwAllocateVirtualMemory 함수는 nt 모듈에서 export 하고 있기 때문에 그냥 주소를 가져올 수 있습니다 )

nt!ZwAllocateVirtualMemory:

fffff800`02e8cee0 488bc4 mov rax,rsp fffff800`02e8cee3 fa cli fffff800`02e8cee4 4883ec10 sub rsp,10h fffff800`02e8cee8 50 push rax fffff800`02e8cee9 9c pushfq fffff800`02e8ceea 6a10 push 10h fffff800`02e8ceec 488d057d2f0000 lea rax,[nt!KiServiceLinkage (fffff800`02e8fe70)] fffff800`02e8cef3 50 push rax fffff800`02e8cef4 b815000000 mov eax,15h fffff800`02e8cef9 e9c2660000 jmp nt!KiServiceInternal (fffff800`02e935c0) fffff800`02e8cefe 6690 xchg ax,ax

nt!ZwQueryInformationProcess: fffff800`02e8cf00 488bc4 mov rax,rsp fffff800`02e8cf03 fa cli fffff800`02e8cf04 4883ec10 sub rsp,10h fffff800`02e8cf08 50 push rax fffff800`02e8cf09 9c pushfq fffff800`02e8cf0a 6a10 push 10h fffff800`02e8cf0c 488d055d2f0000 lea rax,[nt!KiServiceLinkage (fffff800`02e8fe70)] fffff800`02e8cf13 50 push rax fffff800`02e8cf14 b816000000 mov eax,16h fffff800`02e8cf19 e9a2660000 jmp nt!KiServiceInternal (fffff800`02e935c0) fffff800`02e8cf1e 6690 xchg ax,ax


5. nt ! ZwAllocateVirtualMemory 코드를 보면 내부적으로 SystemCall Number를 eax에 설정하는 부분이 있습니다. 함수의 Base 주소부터 저 위치 까지 Offset을 구해줍니다. 구하는 방법은 아래와 같습니다.

 - for문을 돌리면서 +1씩 메모리를 탐색해 나갑니다. 이 때 0xB8를 만나면 그 다음 메모리에 있는 값이 ZwAllocateVirtualMemory 의 SystemCall Number 값인지 확인합니다. 3번에서 구했었습니다. 

맞다면 그 offset을 저장 - C (22)


6. for문을 멈추지 말고 계속 탐색해 나가면서 nt ! ZwAllocateVirtualMemory 아래 있는 함수의 SystemCall Number 까지의 Offset을 구합니다. 즉, nt ! ZwAllocateVirtualMemory 주소부터 nt ! ZwQueryInformationProcess 의 SystemCall Number가 있는 Offset을 구하는 것입니다. - D (54)

ZwQueryInformationProcess의 SystemCall Number는 따로 구할필요 없이 ZwAllocateVirtualMemory 의 SystemCall Number에서 1만 더해주면 되겠지요? ;D


7. D에서 C를 뺍니다.

54 - 22 = 32 Byte => E

32 Byte라는 값은 위 코드에서 보이는 nt ! ZwAllocateVirtualMemory 사이즈입니다. 하지만 모든 Zw 함수들은 SystemCall Number 순서대로 쭉 메모리에 나열되어 있습니다. 똑같은 32Byte 크기로 말이죠.


8. 이제 다음과 같은 계산을 통해 nt ! ZwReadVirtualMemory의 주소를 구할 수 있습니다.


ZwAllocateVirtualMemory + (A-B)*E => ZwReadVirtualMemory


실제 계산을 해보면

fffff800`02e8aee0 + ( 0x3C - 0x15 ) * 32 = fffff800`02e8b3c0


kd> ln fffff800`02e8aee0

(fffff800`02e8aee0)   nt!ZwAllocateVirtualMemory


kd> ln fffff800`02e8b3c0

(fffff800`02e8b3c0)   nt!ZwReadVirtualMemory



결론은 nt 모듈에서 export하고 있는 Zw 함수를 사용해 

export 되고 있지 않은 Zw 함수 주소를 구한다는 것입니다.


실제로 ExploitShield 프로그램에서 이와같이 ZwReadVirtualMemory 주소를 구해서 Driver에서 사용하고 있습니다.

이런 내용은 구글에 찾아봐도 딱히 없더군요. 필요하신 분은 도움이 되셨으면 좋겠습니다.