By choirish | July 24, 2017
Research by BlackPerl Security
Writer Choirish
뚜둔! 오랜만입니다!
“SMB 너로 정했다 ㅇ<-<” 시리즈의 마지막, Part 03을 시작하겠습니다!
지난 시간에 Chapter 3의 앞부분을 이야기했으니… 오늘은,Exploitation Sequence
와Patch
부분에 대해 알아봅시다.
사실.. 본 발표자료는 총 4개의 Chapter로 구성되어있는데
Chapter 4에는 사실 별 내용이 없고…
Code Auditing을 통해 새로운 SMB 취약점을 찾았다!는 내용이 포인트입니다!
그리하여, 추후 취약한 코드가 패치되고 나면!
해당 취약점에 대한 분석 내용으로 다시 돌아오겠습니다 :)
그럼 오늘은 Chapter 3을 끝마쳐 볼까요? ㄱㄱ!
지난 PART 02 포스팅에서 취약점이 발생하는 원리에 대해 이야기했다면!
오늘은 이 취약점을 이용해 RIP를 컨트롤하고, 쉘코드를 호출하기까지의 과정을 이야기 해 보려고 합니다!
그 과정을4단계
로 나누면 다음과 같습니다.
- 지난 시간에, Casting 취약점을 통해 할당 받은 메모리를 넘어서 다른 메모리 영역(nonpaged-pool)을 덮을 수 있다고 말씀드렸는데…
이 때! 원하는 값을 덮기 위해서는
★☆★덮고 싶은 영역을 메모리에 사전 배치하는 작업 ★☆★
이 필요합니다!!
그 덮고자 하는 영역은 바로
srvnet 드라이버가 사용하는 buffer
이고,
정확히 우리가 조작해야 할 값은해당 buffer의 pool header 구조체
입니다.
1번부터 자세히 봅시다!
How to control RIP (1)
- 메모리를 세팅하는 방법을 간단히 요약하면,
덮고 싶은
SRVNET buffer
사이에 구멍을 만들어 놓고,딱 그 구멍에
0x10fe8
만큼의 데이터를 넣을 메모리 공간을 할당받도록 하는 것
입니다!
그림으로 나타내면 다음 슬라이드와 같습니다.
HOLE의 아래위로 SRVNET buffer
를 쫙 깔아둔 후,
0x11000 크기의 HOLE buffer
를 비우면
Malicious FEA가 담긴 FEALIST
가 그 위치에 쏙 들어가게 되고,
SRVNET buffer의 앞부분(pool header)
을 덮게 되는 것입니다!
- SRVNET buffer를 할당 받는 부분을 Windbg로 확인해보았습니다.
왼쪽의 코드는 익스플로잇에서 SRVNET buffer를 세팅하기 위해 연결을 맺는 부분입니다.
연결이 맺어지고,
srvnet!SrvNetWskReceiveEvent
함수에서
srvnet!SrvNetAllocateNonPagedBufferInternel
함수를 호출합니다.
이 때,
할당받을 buffer의 size
(0xfff7 만큼 요청했더니 0x10000을 할당하는 것을 확인 가능)가
해당 함수의 인자(rdx)로 들어가고, 할당한 buffer(nonpaged-pool)의 주소를 리턴(rax)
합니다.
그 주소를
!pool @rax
명령으로 확인해보면,
실제 할당된 pool의 크기는 0x11000
임을 알 수 있습니다.
SRVNET buffer가 잘 세팅되었는지 Windbg로 확인해보았습니다.
공격자가 조작한 FEALIST를 NtFEA 형식으로 memmove
하기 위해,
할당 받은 영역(of srv.sys)을 기준으로 앞쪽 영역의 pool 정보를 검색하면
srvnet.sys의 buffer로 주르륵 세팅
되어 있는 것을 알 수 있습니다.물론! 여기에 담진 않았지만, srv.sys buffer의 뒤쪽 영역에도 잘 세팅되어 있습니다 :)
How to control RIP (2)
- SRVNET buffer를 할당받을 영역(만들어둔 구멍)의 앞뒤로 잘 배치해두었다면…
정상 FEALIST 끝의 Malicious FEA가 SRVNET 영역을 덮어쓰게 됩니다.
Malicious FEA
는SrvOs2FeaToNt()의 두 번째 memmove()를 통해 복사
되는데
복사할 값을 확인해보면,특정 주소가 세팅된 구조체 형태
처럼 보입니다!
How to control RIP (3)
- 앞서 살짝 말씀드렸듯이, Malicious FEA가 덮게 될 부분은
SRVNET buffer의 pool header에 위치한 구조체
입니다.
(임의로 SRVNET_POOLHDR 구조체라 부르겠습니다.)- 이 때, 구조체의 어떤 부분을 조작해야 하는지 알아봅시다.
먼저 아주 중요한 개념인
MDL
과HAL
이 무엇인지 짚고 갈게요!
MDL(Memory Descriptor List)
은 시스템에 정의된 구조체로서,특정 buffer에 대해 물리 주소를 매칭하는 역할
을 합니다.- 패킷과 같이 큰 데이터를 다루는 드라이버(ex : srvnet.sys)는 direct I/O 방식을 통해 메모리에 값을 읽고 쓰는데 direct I/O는 MDL을 이용해 다음 패킷을 메모리 어디에 올릴지 결정합니다.
즉, MDL에 쓰인 메모리 포인터를 덮으면 arbitrary write가 가능하다는 것입니다!
그리고 무려 우리가 덮으려고 하는 SRVNET_POOLHDR 구조체 내부에 MDL이 있다는 사실!!!
이제 왜SRVNET_POOLHDR
를 덮어야 하는지 이해가 되셨죠??
MDL
구조체 정보와,MdlFlag
값의 정보는 다음과 같습니다.참고로, 공격 payload에서는 MdlFlag 값을 0x1004로 설정하였습니다.
0x1004 = 0x0004 + 0x1000
→MDL_SOURCE_IS_NONPAGED_POOL + MDL_NETWORK_HEADER
HAL(Hardware Abstractions Layer)
은
컴퓨터의 물리적인 하드웨어와 컴퓨터에서 실행되는 소프트웨어 사이의 추상화 계층으로,
컴퓨터 운영체제와 하드웨어 서비스 간의 상호작용을 돕는 역할
을 합니다.
HAL은 Windows가 부팅될 때 가장 먼저 로드되는 모듈(HAL.dll) 중 하나로, 커널 모드로 실행됩니다.
여기서 우리가
주목해야 할 점
은 바로!
- HAL의 heap이 메모리에서
고정 주소
를 가진다는 사실과!!- HAL의 heap은
실행 가능한 영역
이라는 것!!!
너무나… 익스플로잇에 사용되기 최적화된 영역이라서 놀라웠답니다..
But, 이번 취약점에 대한 패치 이후로는 일부 OS 버전에 대해 HAL 영역에도ASLR
이 적용되었다고 하니 알아두시길!
이제 MDL과 HAL이 이번 익스플로잇에서 어떤 역할을 하게 될지, 얼마나 중요한지 아시겠죠?
정리해보면, 저희는
- AL의 heap 영역에다가 익스플로잇에 필요한 payload(또 다른 fake struct && 쉘코드)를 담아놓고
- MDL의 특정 포인터를, payload를 넣어둔 HAL 영역의 주소로 덮을 계획
입니다.
그렇다면, SRVNET_POOLHDR
를 덮을 Malicious FEA(= Fake Structure)
를 어떻게 구성했는지 자세히 살펴봅시다.
- 공개된 익스플로잇들마다 Malicious FEA 부분의 크기가 조금씩 다른데(0xa8, 0x8f 등) 저희가 참고한 익스플로잇에서는 0x8f 만큼 SRVNET_POOLHDR를 덮습니다.
- 슬라이드에 표시한 부분(SRVNET_POOLHDR의 시작으로부터 offset 0x58, 0x88)이 arbitrary write를 위해 조작한 가장 핵심적인 값이고, 나머지 값들 또한 필요에 의해 적절히 구성된 것입니다.
- 근데 SRVNET_POOLHDR의 구조가 어떻게 되어있는지 어떻게 알았냐? 하신다면… 저희 또한 Worawit이 공개한 익스플로잇에 적힌 매우 상세한 주석의 도움을 받았습니다!!
여담으로… 지금은 해당 취약점에 대한 상세 분석 보고서가 많이 나왔지만…
연구를 처음 시작할 때만 해도 막막하기 그지없었는데…
이 분의 넘나 친절한 주석 덕분에 그나마 원리를 이해할 수 있었다고 합니다…’ㅅ’/무튼 그래서 바로 이 익스플로잇에, 커널 속 중요한 구조체 정보가 다 담겨있는데! 기본적으로 커널에 대한 정보는 공개되지 않고 꽁꽁 숨겨진 게 많아서… 여기 익스플로잇에 담긴 이러한 구조체 정보는.. Worawit(혹은 그의 동료들)이 모두 리버싱하여 알아낸 것이라고….. 존경합니다…!!!
SRVNET_POOLHDR의 구조는 다음과 같습니다. Fake Structure의 값이 각각 무엇을 뜻하는지 주석을 통해 확인하실 수 있습니다.
// Reverse from srvnet.sys (Win7 x64)
// SrvNetAllocateNonPagedBufferInternal() and SrvNetWskReceiveComplete():
// for x64
struct SRVNET_POOLHDR {
DWORD size;
char unknown[12];
SRVNET_BUFFER hdr;
};
struct SRVNET_BUFFER {
// offset from POOLHDR: 0x10
USHORT flag;
char pad[2];
char unknown0[12];
// offset from SRVNET_POOLHDR: 0x20
LIST_ENTRY list;
// offset from SRVNET_POOLHDR: 0x30
char *pnetBuffer;
DWORD netbufSize; // size of netBuffer
DWORD ioStatusInfo; // copy value of IRP.IOStatus.Information
// offset from SRVNET_POOLHDR: 0x40
MDL *pMdl1; // at offset 0x70
DWORD nByteProcessed;
DWORD pad3;
// offset from SRVNET_POOLHDR: 0x50
DWORD nbssSize; // size of this smb packet (from user)
DWORD pad4;
QWORD pSrvNetWekStruct; // want to change to fake struct address
// offset from SRVNET_POOLHDR: 0x60
MDL *pMdl2;
QWORD unknown5;
// offset from SRVNET_POOLHDR: 0x70
// MDL mdl1; // for this srvnetBuffer (so its pointer is srvnetBuffer address)
// MDL mdl2;
// char transportHeader[0x50]; // 0x50 is TRANSPORT_HEADER_SIZE
// char netBuffer[0];
};
# Here is the important fields on x64
# - offset 0x58 (VOID*) : pointer to a struct contained pointer to function.
the pointer to function is called when done receiving SMB request.
# The value MUST point to valid (might be fake) struct.
# - offset 0x70 (MDL) : MDL for describe receiving SMB request buffer
# - 0x70 (VOID*) : MDL.Next should be NULL
# - 0x78 (USHORT) : MDL.Size should be some value that not too small
# - 0x7a (USHORT) : MDL.MdlFlags should be 0x1004 (MDL_NETWORK_HEADER|MDL_SOURCE_IS_NONPAGED_POOL)
# - 0x80 (VOID*) : MDL.Process should be NULL
# - 0x88 (VOID*) : MDL.MappedSystemVa MUST be a received network buffer address.
Controlling this value get arbitrary write.
# The address for arbitrary write MUST be subtracted by a number
of sent bytes (0x80 in this exploit).
그리하여… SRVNET_POOLHDR에 있는 MDL.MappedSystemVA 값을 조작함으로써 그 다음 받을 패킷을 저장할 메모리의 주소를(HAL 영역으로) 컨트롤할 수 있게 됩니다.
익스플로잇 코드로 봤을 때,
마지막 Malicious FEA(fake structure of SRVNET_POOLHDR)를 보낸 직후에
“또 다른 kernel fake struct && 쉘코드”를 뙇 전송하면??
그 값이 정확히 우리가 의도한 ffffffff` ffd00010(HAL’s heap!!!)에 들어갑니다 X)
씐나!!!!!!!!
How to control RIP (4)
이제 마지막으로 shellcode를 CALL!하는 단계입니다.
모든 패킷을 전송하고 나면, SMB 세션을 닫으면서 SrvNetWskReceiveComplete() 함수가 호출되고 내부에서 SrvNetCommonReceiveHandler() 함수가 다시 호출되는데…!이 때 SrvNetCommonReceiveHandler()가 또 다른 fake struct를 참조하는 과정에서 shellcode를 CALL합니다.
SrvNetCommonReceiveHandler()+0x54 부터 자세히 보면… HAL 영역(ffffffff` ffd00010)에 넣어둔 fake struct를 참조하는 부분이 딱 있습니다.
Worawit이 분석한 두 번째 fake struct의 주요 offset은 다음과 같습니다.# fake struct for SrvNetWskReceiveComplete() and SrvNetCommonReceiveHandler()
# x64: fake struct is at ffffffff ffd00010
# offset 0xa0: LIST_ENTRY must be valid address. cannot be NULL.
# offset 0x08: set to 3 (DWORD) for invoking ptr to function
# offset 0x1d0: KSPIN_LOCK
# offset 0x1d8: array of pointer to function
#
# code path to get code execution after this struct is controlled
# SrvNetWskReceiveComplete() -> SrvNetCommonReceiveHandler() -> call fn_ptr
rbx에 fake struct의 시작 주소가 들어있을 때, 구조체의 0x1d8 offset (ffffffff` ffd001e8)에 있는 값을 r10에 넣습니다.
SrvNetCommonReceiveHandler()+0xb7 을 보면…
[r10+8]에 들어있는 값을 CALL합니다! r10이 ffffffffffd001f0이므로, [r10+8](= r10+8 위치에 든 값)은 ffffffff
ffd00200 즉 shellcode를 바로 뙇! 호출하게 되는 것입니다! 유후!
- 그럼 우리가 공부한 내용을, 네트워크 패킷을 통해 확인해보겠습니다.
- FEALIST 중 정상적인 FEA 부분을 전송하는 패킷입니다.
- 여기서 정상적인
FEA
부분이라 함은,0x10fe8
만큼의srv.sys 버퍼
에 제대로 딱 들어갈 녀석입니다.- srvnet.sys 버퍼를 덮을 Malicious FEA는 왜인지 잘 모르겠지만 나중에 따로 보냅니다;;
※ SMB 연결을 맺은 상태에서, 데이터를 모두 받은 후에서야 메모리를 할당하므로 아직 FEALIST를 담을 메모리는 전혀 할당되지 않은 상태라는 걸 알아두시길 바랍니다!
- Kernel pool 영역에
SRNVET buffer
를 주르륵 배치시키는 패킷입니다.
- FEALIST의 마지막 부분인,
Malicious FEA
를 보내는 패킷입니다.
- MDL을 조작하여 다음 패킷을 받을 주소를 HAL 영역으로 지정한 후,
“두 번째 fake struct && shellcode”를 보내는 패킷
입니다.
- 마지막으로, 데이터를 모두 보낸 후 연결을
SrvNetCommonReceiveHandler()
를 호출하고,쉘코드를 실행
시킬 수 있게 됩니다.
- 이제 03 Wannacry – Exploitation Sequence 파트의 마지막 부분으로,
Wannacry
에 사용된DoublePulsar(feat. Kernel shellcode)
를 소개하겠습니다.
- 참고링크
DoublePulsar
의 동작을 전반적으로 정리하면 다음과 같습니다.
- 1번 과정에서 이용되는
KPCR
과IDT
이 무엇인지 먼저 알아보면…
KPCR
구조체의0x38 offset
에IDTBase
포인터가 존재합니다.
DoublePulsar
의 동작 과정을 1번부터 차례로 살펴보겠습니다!
먼저,
KPCR
에서IDTBase(KIDTENTRY64 pointer struct를 포함하고 있는)
주소를 찾은 후
KIDTENTRY64
구조체의offset 0x4
부분을 참조하여ntoskrnl.exe
내부에서 정의된interrupt handler
에 대한function pointer
를 가져옵니다.
그러고 나서
handler
로부터 거꾸로 검색하며DOS MZ header
를 찾으면ntoskrnl.exe
의 시작 주소를 알게 되는 것입니다.
ntoskrnl.exe
내부에서, 익스플로잇에 필요한 주요 세 함수 주소를 구합니다.
ntoskrnl.exe
에서 구한ZwQuerySystemInformamtion()
함수를 통해srv.sys
드라이버의 주소를 찾습니다.
srv.sys
드라이버의.data
영역을 찾고…
.data
영역의SrvTransaction2DispatchTable
의
SrvTransactionNotImplemented
포인터를
백도어 쉘코드 주소로 덮어씁니다!!
- 그 후, 잘못된
request
를 만들어 보내면SrvTransactionNotImplemented
가 호출되면서 쉘코드가 실행됩니다.
- 덧붙이자면…
DoublePulsar
와 완전히 같지는 않지만…
RiskSense
에서,본 SMB 취약점을 이용한 익스플로잇에 사용되는
커널 쉘코드의 원리를 잘 정리해 둔 보고서
가 있으니
더 자세히 공부해보고 싶으신 분은 이를 참고하시길 바랄게요! ㅎㅎ
- +) 덧 : RiskSense의 보고서에 소개된 커널 쉘코드에는 백도어 기능이 제거되어 있다고 합니다.
- 그럼!!!!!!!!! 이제 “SMB Exploitation Sequence”도 모두 끝냈고!
- 마무으리로 해당 취약점이 어떻게 패치되었는지 볼까요?
- 다시 봐도… 정말 취약점이 발생한 이유는 너무나도 간단했져…
DWORD
값을WORD
로 계산한 실수 하나로… 이 엄청난 일이 발생한 것이었습니다.
그리고, 다시 한번… 코딩을 할 때 (취약점을 찾는 입장에서도 ㅋㅋㅋ)
“사소한 것도 다시 살펴 보자”
는 다짐을 하게 되네요!! ‘ㅅ’a
크허어어어어어엉
다가오지 않았으면 하던 김치콘도, (ㅋㅋ)
김치콘 발표 마무리로서의 블로그 포스팅도 모두 끝이 났습니다!
하고 싶은 말이 많지만 모두 접어두고… ㅎㅎㅎ
미리 말씀 드렸듯이,
먼 훗날 저희가 새롭게 찾은 취약점이 패치가 되고 나면!
패치된 취약점 분석 후기로 다시 찾아 뵐게요!!
감쟈합니다(꾸벅)!
- comment by Choirish