By choirish | July 17, 2017
Research by BlackPerl Security
Writer Choirish
뚜둔! 오랜만입니다!
뚜둔! SMB 너로 정했다 ㅇ<-< 2부가 시작되었습니다.
지난 시간에 chapter 1,2를 다루었으니, 오늘은 chapter 3을 살펴보겠습니다.
Chapter 3의 내용은 크게 5부분으로 이루어져 있는데..
조금 자세하게 얘기할 거라서 :)
오늘은Background / Vulnerability
부분을 다루고,
다음 시간에,Exploitation Sequence / Patch
부분을 다루는 걸로 할게요! ㅎㅎㅎ
그럼 본격! Wannacry 랜섬웨어에 사용된MS17-010
취약점을 분석해봅시다.
- ※ 참고 링크
본격 취약점을 분석하기에 앞서, SMB 프로토콜이 어떻게 통신하는지 간단히 살펴봅시다.
SMB(Server Message Block)
란Windows
에서 파일이나 디렉터리 및 주변장치들을 공유하는 데 사용되는 메시지 형식입니다.
- SMB 메시지는
header
,parameter
,data
이렇게 세 부분으로 크게 나눌 수 있습니다.
SMB header
부분을 보면,8bit의 COMMAND
값을 지정할 수 있습니다.
- SMB 프로토콜 통신에 사용되는
Command
들은MSDN
을 통해 살펴볼 수 있고, 기능별로 다양하게 구현되어 있습니다.
- 실제 익스플로잇을 작성할 때도, 다음과 같이
Command
와 그Parameters
,Data
값을 설정하여 SMB 메시지를 전송합니다.
그럼 이제 ㄹㅇ 본격 MS17-010 에 대해 자세히 알아봅시다.
- SMB의 주요 기능 중 하나가 “파일 공유”인 것은 알고 계셨지요?
- 그래서 파일 공유 시
파일의 EA(Extended Attribute) 정보
도 함께 전달하는데, 이 때EA
값을 인코딩하는 과정에서 취약점이 발생한다는 사실!
EA
는 파일이나 디렉터리의 속성 정보를 담고 있는 구조체 형태로,SMB
에서는Full Extended Attribute(FEA)라는 구조체
를 사용해 이를 표현합니다.
- SMB_FEA_LIST
- 가장 첫 번째 값으로 FEA_LIST의 크기를 저장한다.
- (4bytes) : 나중에 중요한 역할을 함!
- 그 아래에는 여러 개의 FEA 배열을 갖고 있다.
- SMB_FEA
- EA flag (1byte)
- EA Name Length (1byte)
- EA Value Length (2bytes)
- EA Name[EA_Name_Length + 1(NULL)] / EA Value[EA_Value_Length]
- : EA는 Name/Value 값을 쌍으로 갖고 있으며, 각 값의 길이를 FEA 구조체의 앞부분에 저장하고 있다.
- FEALIST 구조체를 그림으로 표현하면 다음과 같습니다.
- FEA와 관련된 주요 함수는 다음과 같습니다.
+) 다음 함수들은srv.sys
드라이버 안에 있는 함수입니다!
SrvOs2FeaListToNt()
는 FEALIST를 NtFEA 포맷으로 변환하는 역할을 하는데,
이 함수 내부에서SrvOs2FeaListSizeToNt()
,SrvOs2FeaToNt()
가 호출됩니다.- 각 함수의 역할은 다음과 같습니다.
SrvOs2FeaListToNt()
는 다음 함수들을 거쳐 호출됩니다.
- SrvSmbNtTransaction()은 SMB 프로토콜을 통해 모든 Data를 받고나면 호출되는 함수로,
Data를 모두 받고 나서FEALIST
를NtFEA로 변환하는 과정
(SrvOs2FeaListToNt()를 통해)이 이루어지는 것을 알 수 있습니다.
- 이제부터
Assembly/C
코드를 통해SrvOs2FeaListToNt()
함수 내부를 속속들이 살펴봅시다.- 먼저 초반부에
SrvOs2FeaListSizeToNt()
가 호출됩니다.
SrvOs2FeaListSizeToNt()
는 전송할FEALIST
의 크기를 구하고,
해당 FEALIST를 NtFEA 형태로 바꿨을 때의
NtFEA 크기를 계산하는 함수
라고 미리 말씀드렸습니다.- FEALIST의 크기를 구하는 과정을 자세히 보면 다음과 같습니다.
List_Pointer
에 4byte(LISTSIZE
값 크기)를 더하여 첫 번째FEA가 시작하는 부분
을 찾습니다.
- List의 끝을 가리키는
LIST_End_Pointer
를 구합니다.
- FEA 하나의 크기에 대한 정보는 각 FEA의 앞부분(FEA_Header)에 담겨있습니다!
FEA_Size = FEA_Header(4) + EAName_Length + EAValue_Length
같은 방법으로
다음 FEA의 시작주소
를 하나씩 탐색하다보면…
LIST_END_Pointer
를 만나게 되는데…
- 다음
FEA_Pointer
가LIST_End_Pointer
보다 크거나 같은 상태가 되면LIST의 끝
에 도달했다는 뜻이므로,
LIST_End_Pointer를 넘지 않는 FEA를 마지막 FEA로 인식
하여LISTSIZE를 계산하고 이 값을 갱신
합니다.
이 때! LISTSIZE 값을 갱신하는 과정에서 문제가 발생한다고 한다!
FEALIST의 크기를 구하는 방법을 이해했으니,진짜 취약점이 발생하는 부분
을 살펴봅시다!
취약점이 발생하는 부분을
C코드
로 보면…!
- 원래
DWORD
로 계산해야 하는 값을WORD
로 계산하여 문제가 생깁니다!!
Assembly
코드로 이 부분을 보면…!
bx = bx (마지막 FEA의 끝) – si (FEALIST의 시작)
- 계산한 값(bx)을
[rsi]
(FEALIST의 가장 첫 번째에 저장된 LISTSIZE(4byte)) 에 옮깁니다.이 때 계산한 값을
WORD
로 옮기기 때문에…원래는 [rsi] 값이
0x10000
에서0xff7e
로 바뀌어야 하는데,
0x1ff7e
라는 매우 큰 값으로 LISTSIZE가 설정되는 것을 알 수 있습니다!
…버그를 찾았네요!!!!
씐나!!!!
사실… LISTSIZE 값은 잘못 갱신되었지만..실제 LIST의 크기(0xff7e)는 잘 구했고,
이에 대한 NtFEA 크기(0x10fe8)도 잘 계산했으므로
SrvOs2FeaListSizeToNt()의 리턴값은 0x10fe8으로,
본 함수의 역할은 잘 수행한 셈입니다…
But… 0xff7e 크기만큼의 FEALIST를 옮겨 담기 위해서 0x10fe8
크기의 메모리 공간을 준비했는데…
0xff7e 보다 더 큰 FEALIST라고 착각하여(실제 값을 옮길 때는 LISTSIZE 값을 읽어와 FEALIST의 끝을 인식하기 때문!)
더 많은 양의 데이터를 옮기려고 하니까!
할당 받은 공간 너머 다른 영역을 덮을 수 있게 되는 것입니다.
하핳…ㅇㅅㅇa
그나저나,
‘왜 0xff7e byte의 FEALIST를 NtFEA 형태로 바꾸면 왜 0x10fe8 byte가 되는지??’
‘왜 0x10fe8 만큼 메모리를 할당시키도록 했을까????’
궁금하지 않으신가요?? ㅎㅎㅎ
왜냐면… 저희는 그게 너무 궁금했었거든여…
이거 이해하는 데만 시간 엄청 썼답니다 또륵..
그럼 잠시만 짚고 가볼게요~
1. FEALIST → NtFEA
앞서도 설명 드렸지만… SMB_FEALIST
는 다음과 같이 구성되어 있습니다.
//FEALIST = LISTSIZE(4byte) + FEAs
SMB_FEA_LIST
{
ULONG SizeOfListInBytes;
UCHAR FEAList[];
}
//각 FEA = Header(4byte) + Body(Name + NULL + Value)
// Name/Value 값이 비어있다면 FEA 1개의 최소 길이는 5byte
SMB_FEA
{
UCHAR ExtendedAttributeFlag;
UCHAR AttributeNameLengthInBytes;
USHORT AttributeValueLengthInBytes;
UCHAR AttributeName[AttributeNameLengthInBytes + 1];
UCHAR AttributeValue[AttributeValueLengthInBytes];
}
NtFEA
는 대략 다음과 같이 구성되어있습니다.
// Name/Value 값이 비어있다면 NtFEA 1개의 최소 길이는 12byte
typedef struct _FILE_FULL_EA_INFORMATION
{
ULONG NextEntryOffset;
UCHAR Flags;
UCHAR EaNameLength;
USHORT EaValueLength;
UCHAR EaName[1];
} FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION;
사실 아래 구조를 봤을 때 Header라고 말할 수 있는 값(except EaName[1])은 8byte인데… 디버깅을 해서 직접 알아본 결과… 각 FEA에 대한 NtFEA 크기를 구할 때 12byte씩 더하는 것을 확인함! 후엥..;;
여하튼! 최소 5byte의 FEA도 NtFEA 형태로 바뀌면서 최소 12byte로 늘어난다는 것!
실제 공격 payload
로 쓰인 FEALIST(0Xff7e byte)
를 통해 설명해 드릴게요ㅎㅎ
-------------------------------------- ---------------------
LISTSIZE(4)
내용없는 FEA 600개(5*600) --→ (12*600)
0xf3bd byte의 내용이 든 FEA 1개(5+0xf3bd) --→ (12+0xf3bc)
-------------------------------------- ---------------------
= 4 + 3000 + 5 + 0xf3bd = 7200 + 12 + 0xf3bc
= 0xff7e ----→ = 0x10fe8
2. Why 0x10fe8 ???
SrvOs2FeaListSizeToNt()가 인식한 정상 FEALIST의 크기는 [0xff7e]byte 였지만…
실제로 공격 페이로드에 담긴 FEALIST의 크기는 [0xff7e + (5 + 0x8f) + 4]byte 입니다.
앞서 간단히 말씀드렸듯이… FEALIST의 크기는 0xff7e로 구했습니다!
왜냐면 FEALIST 크기를 구할 당시에는, FEALIST의 첫 4byte(LISTSIZE) 값이 0x10000이므로, 0x10000 크기를 넘어가는 FEA는 인식하지 못하였던 것이죠.
LISTSIZE가 0x1ff7e로 잘못 바뀐 것은 FEALIST에 대한 NtFEA 크기를 모두 구한 이후 시점!이라는 것도 헷갈리지 않으시길!
(5 + 0x8f) 크기의 마지막 FEA가 바로,
할당 받은 메모리를 넘어 다른 영역을 덮게 되는 Malicious한 FEA
입니다! (5byte는 알다시피 헤더값)
4byte는 더미값인데.. FEA의 헤더값에 맞지 않는 더미를 넣어서
더 이상 FEA를 복사하지 않도록 하는 장치라고만 알아두시면 될 듯 해여! ㅎㅎ
즉, 중요한 건 우리가 덧붙여 보낸 FEA의 Body 부분이(0x8f!!!!!!)
딱 다른 영역에 덮이도록 하려면????
0x10fe8 byte의 메모리를 할당받아야 한다는 것입니다!!
WHY???
- 우리가 분석한 시스템(Windows 7 64bit)에서는, 큰 nonpaged-pool의 크기가 대부분 0x11000 byte였다.
그리고 0x10fe8 만큼 메모리를 할당해달라고 했을 때, SrvAllocateNonPagedPool에서 할당한 주소는[ pool의 시작주소 + 0x10 ]
위치였다.- ex) nonpaged-pool의 시작 주소 : [ 0xfffffa80′ 32e3b000 ]
- ex) 할당받은 주소 : [ 0xfffffa80′ 32e3b010 ]
- 이 때 할당받은 pool 주위로 큰 nonpaged-pool이 쭉 배치되어있다고 해보자..
그렇다면 할당받은 pool의 크기가 0x11000이니까 이로부터 0x11000 만큼 떨어진 곳에 다음 pool이 위치할 것이다.- ex) 할당받은 pool의 바로 다음 pool 시작 주소 : [ 0xfffffa80′ 32e4c000 ]
- 이제 할당받은 [ ~32e3b010 ] 메모리에 0x10fe8 만큼의 정상 FEA가 복사된다고 하자.
그럼 다음 Malicious FEA가 복사되야할 주소는 [ ~32e4bff8 ]이다…
이 때 Malicious FEA의 헤더부분(5byte)가 NtFEA의 헤더(8byte)로 매칭되야하므로, 헤더(8byte)를 먼저 복사하고 나면?Malicious FEA의 Body(0x8f)가 복사될 주소 : [ ~32e4c000 ]
뚠따리린뚱뚱뚱!
아까 은근슬쩍 말해 둔 다음 pool의 시작 주소부터 뙇! 덮게 된다….
사실 그래서…! 덮고 싶은, 조작하고 싶은 영역을 미리!
할당받을 곳 주변에 쫙 깔아두는 작업이 필요하게 됩니다.
익스플로잇에서 매우 중요한 작업이죠!
하지만.. 아쉽게도 이건 다음 포스팅에서 다룰 내용이라 ㅎㅎ
자세히 말하면 더 아쉬우니 기대감을 안겨두고 넘어가도록 할게요! X)
이제 좀 궁금증이 풀리셨나요??
사실 관심 없는 사람이야.. 넘기고 싶은 부분이겠지만..
진정 관심을 갖고 알고 싶어하는 사람에게는 중요한 point이리라 생각합니다!
그럼 얼른 정리 겸, 이번 포스팅의 끝으로 달려봅시다(3장 남음) ㄱㄱ!
- 자, 이렇게
SrvOsFeaListSizeToNt()
에서 엄청난 오류가 일어난 후…
해당 함수의 리턴값이 SrvAllocateNonPagedPool()의 인자로 들어가서
0x10fe8 만큼의 nonpaged-pool을 할당받게 됩니다.
- 그런 후에, 분홍색 박스의 loop을 돌면서
SrvOs2FeaToNt()를 통해 FEA를 하나씩 memmove
합니다.
그런데 이 때.. FEALIST의 LISTSIZE가 0x1ff7e로 바뀐 탓에
마지막 FEA의 시작 위치(r14)를 실제 FEALIST 크기보다 훨씬 뒤로 인식하게 되고..
loop를 더 많이 돌게 되니까 Malicious FEA 까지도 memmove하는 것입니다
뚜둔!
- IDA에서 C코드로 보면,
SrvOs2FeaToNt()
의 두 번째 memmove에서 FEA의 Value 값을 옮기다가…..Pool Overflow
가 발생합니다.
여기까지가 취약점이 발생하는 주요 함수에 대한 분석 내용이었고~ ‘ㅅ’/
아쉽지만… 이번 포스팅은 여기에서 끝! ㅠㅠ
오늘 설명한 이 취약점을 통해
어떻게 RIP
를 컨트롤 할 수 있게 되는지!!!!
…에 대한 내용을 들고 다시 찾아 뵙겠습니다.
see you soon!
– comments by Choirish