SMB, 너로 정했다! ch.02

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를 모두 받고 나서 FEALISTNtFEA로 변환하는 과정(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_PointerLIST_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

comments powered by Disqus