SMB, 너로 정했다! PART 02

흰토끼

Research by BlackPerl Security

Writer Choirish

Choirish 의 다른 글 더 보기


뚜둔! SMB 너로 정했다 ㅇ<-< 2부가 시작되었습니다.

슬라이드2_1

지난 시간에 chapter 1,2를 다루었으니, 오늘은 chapter 3을 살펴보겠습니다.


슬라이드19_1

Chapter 3의 내용은 크게 5부분으로 이루어져 있는데..
조금 자세하게 얘기할 거라서 :)

오늘은 Background / Vulnerability 부분을 다루고,
다음 시간에, Exploitation Sequence / Patch 부분을 다루는 걸로 할게요! ㅎㅎㅎ
그럼 본격! Wannacry 랜섬웨어에 사용된 MS17-010 취약점을 분석해봅시다.


슬라이드20

참고 링크 : 


슬라이드21


 

본격 취약점을 분석하기에 앞서, SMB 프로토콜이 어떻게 통신하는지 간단히 살펴봅시다.

슬라이드22

SMB(Server Message Block)
Windows에서 파일이나 디렉터리 및 주변장치들을 공유하는 데 사용되는
메시지 형식입니다.


슬라이드23

SMB 메시지는
header
parameter
data
이렇게 세 부분으로 크게 나눌 수 있습니다.


슬라이드24

SMB header 부분을 보면, 8bit의 COMMAND 값을 지정할 수 있습니다.


슬라이드25

SMB 프로토콜 통신에 사용되는 Command들은 MSDN을 통해 살펴볼 수 있고,
기능별로 다양하게 구현되어 있습니다.


슬라이드26

실제 익스플로잇을 작성할 때도,
다음과 같이 Command와 그 Parameters, Data 값을 설정하여 SMB 메시지를 전송합니다.

 


 

그럼 이제 ㄹㅇ 본격 MS17-010 에 대해 자세히 알아봅시다.

슬라이드27

SMB의 주요 기능 중 하나가 “파일 공유”인 것은 알고 계셨지요?
그래서 파일 공유 시 파일의 EA(Extended Attribute) 정보도 함께 전달하는데,
이 때 EA 값을 인코딩하는 과정에서 취약점이 발생한다는 사실!

 

관련 용어 참고 링크 : 


슬라이드28

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 구조체를 그림으로 표현하면 다음과 같습니다.

슬라이드29


 

FEA와 관련된 주요 함수는 다음과 같습니다.

슬라이드30

+) 다음 함수들은 srv.sys 드라이버 안에 있는 함수입니다!


 

SrvOs2FeaListToNt()는 FEALIST를 NtFEA 포맷으로 변환하는 역할을 하는데,
이 함수 내부에서 SrvOs2FeaListSizeToNt(), SrvOs2FeaToNt()가 호출됩니다.

각 함수의 역할은 다음과 같습니다.

슬라이드31


 

SrvOs2FeaListToNt()는 다음 함수들을 거쳐 호출됩니다.

슬라이드32

SrvSmbNtTransaction()은
SMB 프로토콜을 통해 모든 Data를 받고나면 호출되는 함수
로,

Data를 모두 받고 나서
FEALIST를 NtFEA로 변환하는 과정(SrvOs2FeaListToNt()를 통해)
이루어지는 것을 알 수 있습니다.


 

이제부터 Assembly/C 코드를 통해 SrvOs2FeaListToNt() 함수 내부를 속속들이 살펴봅시다.
먼저 초반부에 SrvOs2FeaListSizeToNt()가 호출됩니다.

슬라이드33


 

SrvOs2FeaListSizeToNt()는 전송할 FEALIST의 크기를 구하고,
해당 FEALIST를 NtFEA 형태로 바꿨을 때의
NtFEA 크기를 계산하는 함수라고 미리 말씀드렸습니다.

FEALIST의 크기를 구하는 과정을 자세히 보면 다음과 같습니다.

슬라이드34

List_Pointer에 4byte(LISTSIZE 값 크기)를 더하여
첫 번째 FEA가 시작하는 부분을 찾습니다. 


슬라이드35

List의 끝을 가리키는 LIST_End_Pointer를 구합니다.


슬라이드36

FEA 하나의 크기에 대한 정보는 각 FEA의 앞부분(FEA_Header)에 담겨있습니다!

FEA_Size = FEA_Header(4) + EAName_Length + EAValue_Length


슬라이드38

같은 방법으로 다음 FEA의 시작주소를 하나씩 탐색하다보면…


LIST_END_Pointer를 만나게 되는데…

슬라이드39

다음 FEA_Pointer가 LIST_End_Pointer보다 크거나 같은 상태가 되면,
LIST의 끝에 도달했다는 뜻이므로
LIST_End_Pointer를 넘지 않는 FEA를 마지막 FEA로 인식하여
LISTSIZE를 계산하고 이 값을 갱신합니다.

이 때! LISTSIZE 값을 갱신하는 과정에서 문제가 발생한다고 한다!

FEALIST의 크기를 구하는 방법을 이해했으니,
진짜 취약점이 발생하는 부분을 살펴봅시다!


 

취약점이 발생하는 부분을 C코드로 보면…!

슬라이드40

원래 DWORD로 계산해야 하는 값을 WORD로 계산하여 문제가 생깁니다!!


 

Assembly 코드로 이 부분을 보면…!

슬라이드41

bx = bx (마지막 FEA의 끝) – si (FEALIST의 시작)

계산한 값(bx)을 [rsi](FEALIST의 가장 첫 번째에 저장된 LISTSIZE(4byte)) 에 옮깁니다.

이 때 계산한 값을 WORD로 옮기기 때문에…
원래는 [rsi] 값이 0x10000에서 0xff7e로 바뀌어야 하는데,
0x1ff7e라는 매우 큰 값으로 LISTSIZE가 설정되는 것을 알 수 있습니다!

 

…버그를 찾았네요!!!!

d5da9d2399284af93a205b726b1cce4e29a0a15ad4fa403ac04e55ba00c82516.jpg

씐나!!!!

사실… 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을 할당받게 됩니다.

슬라이드42


그런 후에, 분홍색 박스의 loop을 돌면서
SrvOs2FeaToNt()를 통해 FEA를 하나씩 memmove합니다.

슬라이드43

그런데 이 때.. FEALIST의 LISTSIZE가 0x1ff7e로 바뀐 탓
마지막 FEA의 시작 위치(r14)를 실제 FEALIST 크기보다 훨씬 뒤로 인식하게 되고..

loop를 더 많이 돌게 되니까 Malicious FEA 까지도 memmove하는 것입니다 뚜둔!


슬라이드44

IDA에서 C코드로 보면, SrvOs2FeaToNt()
두 번째 memmove에서 FEA의 Value 값을 옮기다가….. Pool Overflow가 발생합니다.

여기까지가 취약점이 발생하는 주요 함수에 대한 분석 내용이었고~ ‘ㅅ’/


아쉽지만… 이번 포스팅은 여기에서 끝! ㅠㅠ

오늘 설명한 이 취약점을 통해
어떻게 RIP를 컨트롤 할 수 있게 되는지!!!!
…에 대한 내용을 들고 다시 찾아 뵙겠습니다.

슬라이드45

See you soon!

-comments by Choirish

SMB, 너로 정했다! PART 02”의 2개의 생각

  1. 혹시 질문있는데요 smb 프로토콜 취약점 저거 공격할때 공유 폴더에 패스워드 걸려있어도 성공하나요 ?

    좋아요

댓글 남기기