View difference between Paste ID: nzF8PPLU and RhBmngSV
SHOW: | | - or go back to the newest paste.
1
import sys
2
from struct import pack
3
from io import BytesIO
4
5
from ctypes import *
6
from winappdbg import Debug, EventHandler
7
import pefile
8
9
from capstone import *
10
from capstone.x86 import *
11
12
from unicorn import *
13
from unicorn.x86_const import *
14
15
from keystone import *
16
17
SECTION_HEADER_SIZE = 0x28
18
IMAGE_DESCRIPTOR_HEADER_SIZE = 0x14
19
20
md = Cs(CS_ARCH_X86, CS_MODE_64)
21
md.detail = True
22
23
global target_pe
24
global mem_pages
25
26
# ----------------------------------------------------------------------------------------------------------------
27
def align_data(data, blocksize):
28
	r = data
29
	rm = len( r ) % blocksize
30
	if rm != 0:
31
		r += (blocksize - rm) * b'\x00'
32
	return r
33
34
# ----------------------------------------------------------------------------------------------------------------
35
def align_int(integer, blocksize):
36
	r = integer
37
	rm = r % blocksize
38
	if rm != 0:
39
		r += (blocksize - rm)
40
	return r
41
42
# ----------------------------------------------------------------------------------------------------------------
43
def image_import_descriptor(OriginalFirstThunk, Name, FirstThunk):
44
	"""
45
	+00		DWORD	OriginalFirstThunk
46
	+04 	DWORD 	TimeDateStamp
47
	+08 	DWORD 	ForwarderChain
48
	+12 	DWORD 	Name
49
	+16 	DWORD 	FirstThunk
50
	"""
51
	return pack('<LLLLL', OriginalFirstThunk, 0, 0, Name, FirstThunk)
52
53
# ----------------------------------------------------------------------------------------------------------------
54
def image_import_by_name(hint, name):
55
	"""
56
	+00		WORD 	Hint
57
	+02 	BYTE 	Name
58
	"""
59
	return pack('<H', hint) + name.encode() + b'\0'
60
61
62
# ----------------------------------------------------------------------------------------------------------------
63
def image_thunk_data(imageimportbyname_rva, x64, lastthunk=True):
64
	if x64:
65
		r  = pack('<Q', imageimportbyname_rva )
66
		if lastthunk:
67
			r += pack('<Q', 0 )
68
	else:
69
		r  = pack('<L', imageimportbyname_rva )
70
		if lastthunk:
71
			r += pack('<L', 0 )
72
	return r
73
74
# ----------------------------------------------------------------------------------------------------------------
75
def build_image_import_by_name(names, base=0):
76
	funcrva = []
77
	r = b''
78
	for i, name in enumerate(names):
79
		funcrva.append( len( r ) )
80
		r += image_import_by_name(i, name)
81
	return funcrva, r
82
83
# ----------------------------------------------------------------------------------------------------------------
84
def build_image_thunk_data(offsets, x64, base=0):
85
	r = b''
86
	offd = dict()
87
	for i in range(len(offsets)):
88
		r += image_thunk_data(base + offsets[i], x64, bool(i==len(offsets)-1))
89
	return r
90
91
# ----------------------------------------------------------------------------------------------------------------
92
def section_header(roff, rsize, voff, vsize, name=b'.h4x'):
93
	"""
94
	+00 	BYTE 	name[8]
95
	+08 	DWORD 	virtualsize
96
	+12 	DWORD 	virtualaddress
97
	+16 	DWORD 	rawsize
98
	+20 	DWORD 	rawaddress
99
	+24 	DWORD 	relocaddress
100
	+28 	DWORD 	linenumbers
101
	+32 	WORD 	nrofrelocs
102
	+34 	WORD 	nroflinenumbers
103
	+36 	DWORD 	characteristics
104
	"""
105
	return name.ljust(8, b'\x00') + pack('<LLLLLLHHL', vsize, voff, rsize, roff, 0, 0, 0, 0, 0xC0000040)
106
107
# ----------------------------------------------------------------------------------------------------------------
108
def rebuild_import_table(file_data, impt):
109
	# collect some pe info from the file for later	
110
	pe = pefile.PE(data=file_data, fast_load=True)
111
	is64bits = bool(pe.OPTIONAL_HEADER.Magic == 0x20b)
112
	nrofsections = pe.FILE_HEADER.NumberOfSections
113
	secalignment = pe.OPTIONAL_HEADER.SectionAlignment
114
	filealignment = pe.OPTIONAL_HEADER.FileAlignment
115
	sizeofheaders = pe.OPTIONAL_HEADER.SizeOfHeaders
116
	lastsecoffset = pe.sections[nrofsections-1].__file_offset__
117
	lastsection = pe.sections[nrofsections-1]
118
	lastviraddr = lastsection.VirtualAddress + lastsection.Misc_VirtualSize
119
	sizeofimage = pe.OPTIONAL_HEADER.SizeOfImage
120
	pe.close()
121
122
	imp_tbl = b''
123
	imp_disc = b''
124
	rvas = dict()
125
	for dllname in impt:
126
		importbyname_offsets, importbyname_data = build_image_import_by_name(impt[dllname])
127
		thunk_data = build_image_thunk_data(importbyname_offsets, is64bits, sizeofimage + len(imp_tbl) + len(dllname) + 1)
128
129
		name_rva = len(imp_tbl)
130
131
		imp_tbl += dllname.encode() + b'\x00'
132
		imp_tbl += importbyname_data
133
		imp_tbl = align_data(imp_tbl, 8)
134
		firstthunk_rva = sizeofimage + len(imp_tbl)
135
		imp_tbl += thunk_data
136
137
		rvas[dllname] = dict()
138
		for i, funcname in enumerate(impt[dllname]):
139
			rvas[dllname][funcname] = firstthunk_rva + (i * 8)
140
141
		imp_disc += image_import_descriptor( firstthunk_rva, sizeofimage + name_rva, firstthunk_rva )
142
	
143
	imp_disc += image_import_descriptor( 0, 0, 0 )
144
	imp_tbl = align_data(imp_tbl, 4)
145
146
	import_dir_rva = sizeofimage + len(imp_tbl)
147
	imp_tbl += imp_disc
148
	newsec_data = align_data(imp_tbl, filealignment)
149
150
	newsec_rawsize = len(newsec_data)
151
	# get the alignd virtual offset and size
152
	newsec_viraddr = align_int(lastviraddr, secalignment)
153
	newsec_virsize = align_int(newsec_rawsize, secalignment)
154
	newsec_rawaddr = len(file_data)
155
	
156
	# create a section header
157
	newsec_header = section_header(newsec_rawaddr, newsec_rawsize, newsec_viraddr, newsec_virsize)
158
159
	# contruct the new pe file
160
	new_pe  = file_data[:lastsecoffset + SECTION_HEADER_SIZE]
161
	new_pe += newsec_header
162
	new_pe += (sizeofheaders - len(new_pe)) * b'\x00'
163
	new_pe += file_data[sizeofheaders:]
164
	new_pe += newsec_data
165
166
	# parse the pe of the rebuild data
167
	pe = pefile.PE(data=new_pe, fast_load=True)
168
	# increase nr of sections
169
	pe.FILE_HEADER.NumberOfSections += 1
170
	# update the imagesize
171
	pe.OPTIONAL_HEADER.SizeOfImage = newsec_viraddr + newsec_virsize
172
	# update the data_dir[imports] rva and size
173
	pe.OPTIONAL_HEADER.DATA_DIRECTORY[ pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_IMPORT'] ].VirtualAddress = import_dir_rva
174
	pe.OPTIONAL_HEADER.DATA_DIRECTORY[ pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_IMPORT'] ].Size = newsec_rawsize + IMAGE_DESCRIPTOR_HEADER_SIZE
175
	# since this is removed from the header we reset its values if set
176
	bound_imorts_dir = pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT']
177
	if pe.OPTIONAL_HEADER.DATA_DIRECTORY[ bound_imorts_dir ].VirtualAddress != 0:
178
		pe.OPTIONAL_HEADER.DATA_DIRECTORY[ bound_imorts_dir ].VirtualAddress = 0
179
		pe.OPTIONAL_HEADER.DATA_DIRECTORY[ bound_imorts_dir ].Size = 0
180
	
181
	# write to new file
182
	return pe.write(), rvas
183
184
# ----------------------------------------------------------------------------------------------------------------
185
def hook_code64(uc, address, size, user_data):
186
	code = uc.mem_read(address, size)
187
	insn = disassemble(code, 0)
188
	print_insn(insn)
189
190
# ----------------------------------------------------------------------------------------------------------------
191
def emu(pe, offsets, imports, tracecode=False):
192
	imp = dict()
193
	imgbase = pe.OPTIONAL_HEADER.ImageBase
194
	imgsize = pe.OPTIONAL_HEADER.SizeOfImage
195
196
	esp = 0x1000000
197
198
	mu = Uc(UC_ARCH_X86, UC_MODE_64)
199
200
	mu.mem_map(imgbase, imgsize)
201
	mu.mem_map(esp, 2 * 1024 * 1024)
202
203
	mu.mem_write(imgbase, bytes(pe.__data__))
204
	mu.mem_write(esp, int(2 * 1024 * 1024) * b'\0')
205
206
	if tracecode: mu.hook_add(UC_HOOK_CODE, hook_code64)
207
208
	for offset, vm_eip  in offsets:
209
		mu.reg_write(UC_X86_REG_ESP, esp + int(1 * 1024 * 1024))
210
		try:
211
			mu.emu_start(imgbase + (vm_eip + 0x1000), imgbase + imgsize)
212
		except UcError as e:
213
			rip = mu.reg_read(UC_X86_REG_RIP)
214
			if rip in imports:
215
				print('0x%016x => %s' % (offset, imports[rip]))
216
				imp[offset] = imports[rip]
217
			else:
218
				print(f'0x{rip:016x} not found')
219
	return imp
220
221
# ----------------------------------------------------------------------------------------------------------------
222
def print_insn(insn):
223
	print("0x%016x: %s %s" % (insn.address, insn.mnemonic.ljust(5, ' '), insn.op_str))
224
225
# ----------------------------------------------------------------------------------------------------------------
226
def disassemble(code, ep, maxinslen=12):
227
	for insn in md.disasm(code[ep:ep+maxinslen], ep):
228
		return insn
229
230
# ----------------------------------------------------------------------------------------------------------------
231
def call_ins_in_range(code, minaddress, maxaddress):
232
	ep = 0
233
	codelen = len(code)
234
	addresses = list()
235
	while ep < codelen:
236
		insn = disassemble(code, ep)
237
		if insn and insn.size == 5 and insn.id == X86_INS_CALL \
238
		and insn.operands[0].type == X86_OP_IMM:
239
			addr = insn.operands[0].imm & 0xffffffff
240
			if addr >= minaddress and addr <= maxaddress:
241
				addresses.append( (ep, addr) )
242
		ep += 1
243
	return addresses
244
245
# ----------------------------------------------------------------------------------------------------------------
246
def dump_fix(img, oep=None):
247
	pe = pefile.PE(data=img, fast_load=True)
248
	sectionAlignment = pe.OPTIONAL_HEADER.SectionAlignment
249
	for section in pe.sections:
250
		section.PointerToRawData = section.VirtualAddress
251
		vsize = align_int(section.Misc_VirtualSize, sectionAlignment)
252
		# section.SizeOfRawData = section.Misc_VirtualSize
253
		section.SizeOfRawData = vsize
254
		section.Misc_VirtualSize = vsize
255
	
256
	if oep:
257
		pe.OPTIONAL_HEADER.AddressOfEntryPoint = oep
258
259
	# disable aslr for now, we need to do a reloc correction actualy
260
	pe.OPTIONAL_HEADER.DllCharacteristics ^= 0x40
261
262
	return pe.write()
263
264
# ----------------------------------------------------------------------------------------------------------------
265
def analyse_vmp_api_stub(code, address):
266
	vmp_api = list()
267
	while True:
268
		insn = disassemble(code, address)
269
		if insn.id in [X86_INS_PUSH, X86_INS_POP]:
270
			vmp_api.append(insn)
271
		elif insn.id in [X86_INS_LEA, X86_INS_MOV] and (insn.operands[0].type == X86_OP_MEM or insn.operands[1].type == X86_OP_MEM):
272
			vmp_api.append(insn)
273
		elif insn.id == X86_INS_XCHG and insn.operands[0].type != insn.operands[1].type:
274
			vmp_api.append(insn)
275
		elif insn.id == X86_INS_RET:
276
			vmp_api.append(insn)
277
			break
278
279
		if insn.id == X86_INS_JMP:
280
			address = insn.operands[0].imm
281
		else:
282
			address += insn.size
283
	return vmp_api
284
285
# ----------------------------------------------------------------------------------------------------------------
286
def action_callback_page_access( event ):
287
	global target_pe
288
289
	process = event.get_process()
290
	thread  = event.get_thread()
291
	context = thread.get_context()
292
293
	img_base = process.get_image_base()
294
295
	rip = context['Rip']
296
297
	# we need this rip range filter, because we keep hiting a vmp section first...?!
298
	if rip >= img_base + target_pe.sections[0].VirtualAddress \
299
	and rip <= img_base + target_pe.sections[0].VirtualAddress + target_pe.sections[0].Misc_VirtualSize:
300
		print(f'page_access: OEP => 0x{rip:016x} - 0x{rip-img_base:016x}')
301
		event.debug.erase_all_breakpoints()
302
303
		img_pe = dump_fix(process.read( img_base, target_pe.OPTIONAL_HEADER.SizeOfImage ), rip-img_base)
304
305
		pe = pefile.PE(data=img_pe, fast_load=True)
306
		pe.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_IMPORT']])
307
308
		# grab the imported dll names from the import table
309
		implookup = dict()
310
		for entry in pe.DIRECTORY_ENTRY_IMPORT:
311
			dllname = entry.dll.decode()
312
			mod = process.get_module_by_name(dllname)
313
			
314
			pedll = pefile.PE('c:\\windows\\system32\\' + dllname, fast_load=True)
315
			pedll.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_EXPORT']])
316
			
317
			print(f'--> {dllname}')
318
			for exp in pedll.DIRECTORY_ENTRY_EXPORT.symbols:
319
				if exp.name == None:
320
					continue
321
				addr = mod.resolve( exp.name )
322
				if addr not in implookup:
323
					implookup[addr] = (dllname, exp.name.decode())
324
			pedll.close()
325
		
326
		# locate call's into the vmp section
327
		code_section_data = pe.sections[0].get_data()
328
		vm_section = pe.sections[4]
329
330
		calltos = call_ins_in_range(
331
			code_section_data, 
332
			vm_section.VirtualAddress, 
333
			vm_section.VirtualAddress + vm_section.Misc_VirtualSize
334
			)
335
336
		print('got %d vmp_api call(s)' % len(calltos))
337
338
		# emulate the vmp api stubs
339
		call2imp = emu(pe, calltos, implookup)
340
		
341
		pe.close()
342
343
		# filter imports for iat rebuilding
344
		imptbl = dict()
345
		for callfrom in call2imp:
346
			dll, func = call2imp[callfrom]
347
			if dll not in imptbl:
348
				imptbl[dll] = list()
349
			
350
			if func not in imptbl[dll]:
351
				imptbl[dll].append( func )
352
353
		# rebuild import table
354
		img_pe, iat_rvas = rebuild_import_table(img_pe, imptbl)
355
356
		# patch vmp call instructions
357
		fio = BytesIO(img_pe)
358
		ks = Ks(KS_ARCH_X86, KS_MODE_64)
359
		for cfrom, cto in calltos:
360
			vmpapi = analyse_vmp_api_stub(img_pe, cto + 0x1000)
361
			if vmpapi[2].id == X86_INS_LEA and vmpapi[2].operands[1].type == X86_OP_MEM and vmpapi[2].operands[1].mem.disp == 1:
362
				"""
363
				0x00000000000fd9eb: push  rax
364
				0x00000000000fd9f7: mov   rax, qword ptr [rsp + 8]
365
				0x0000000000242ad2: lea   rax, [rax + 1]
366
				0x00000000001b7986: mov   qword ptr [rsp + 8], rax
367
				0x00000000001b7992: lea   rax, [rip - 0x1af489]
368
				0x0000000000173257: mov   rax, qword ptr [rax + 0xd5f69]
369
				0x000000000017325e: lea   rax, [rax + 0x7fde623c]
370
				0x000000000023dff5: xchg  qword ptr [rsp], rax
371
				0x0000000000256c24: ret  
372
				"""
373
				dllname, funcname = call2imp[cfrom]
374
				encoding, count = ks.asm('call qword ptr[0x%016x]' % (iat_rvas[dllname][funcname] - 0x1000 - cfrom))
375
				fio.seek( cfrom + 0x1000 )
376
				fio.write( bytes(encoding) )
377
			elif vmpapi[0].id == X86_INS_POP and vmpapi[-1].size == 1:
378
				"""
379
				0x000000000020c562: pop   rbx
380
				0x000000000020bcda: xchg  qword ptr [rsp], rbx
381
				0x00000000000fcc27: push  rbx
382
				0x00000000000fcc28: lea   rbx, [rip - 0xf30ea]
383
				0x0000000000162a3c: mov   rbx, qword ptr [rbx + 0x261566]
384
				0x00000000000e2e6f: lea   rbx, [rbx + 0x19bd2ee5]
385
				0x00000000000e2e76: xchg  qword ptr [rsp], rbx
386
				0x0000000000259b32: ret			
387
				"""				
388
				dllname, funcname = call2imp[cfrom]
389
				encoding, count = ks.asm('call qword ptr[0x%016x]' % (iat_rvas[dllname][funcname] - 0x1000 - (cfrom - 1)))
390
				fio.seek( (cfrom - 1) + 0x1000 )
391
				fio.write( bytes(encoding) )
392
			elif vmpapi[0].id == X86_INS_POP and vmpapi[-1].size == 3:
393
				"""
394
				0x0000000000199b18: pop   rbp
395
				0x0000000000287e2e: xchg  qword ptr [rsp], rbp
396
				0x000000000016eeff: push  rbp
397
				0x000000000016ef04: lea   rbp, [rip - 0x167a8b]
398
				0x00000000002735e8: mov   rbp, qword ptr [rbp + 0x10440d]
399
				0x00000000000fcc15: lea   rbp, [rbp + 0x1c97310c]
400
				0x00000000001885a6: xchg  qword ptr [rsp], rbp
401
				0x0000000000150cb8: ret   8
402
				"""
403
				dllname, funcname = call2imp[cfrom]
404
				# print(f'pop_call_ret8: 0x{img_base+0x1000+cfrom:016x} -> {funcname}')
405
				encoding, count = ks.asm('jmp qword ptr[0x%016x]' % (iat_rvas[dllname][funcname] - 0x1000 - (cfrom - 2)))
406
				fio.seek( (cfrom - 2) + 0x1000 )
407
				fio.write( bytes(encoding) )
408
			elif vmpapi[0].id == X86_INS_PUSH and vmpapi[-1].size == 3:
409
				"""
410
				0x0000000000149386: push  rsi
411
				0x000000000014938a: lea   rsi, [rip - 0x144344]
412
				0x00000000001bfe00: mov   rsi, qword ptr [rsi + 0x12b866]
413
				0x0000000000224a7c: lea   rsi, [rsi + 0x95a4b96]
414
				0x00000000001f6ad4: xchg  qword ptr [rsp], rsi
415
				0x00000000001f6ad8: ret   8
416
				"""
417
				dllname, funcname = call2imp[cfrom]
418
				# print(f'push_call_ret8: 0x{img_base+0x1000+cfrom:016x} -> {funcname}')
419
				dist = 1 if img_pe[(cfrom-1)+0x1000] == 0x48 else 0
420
				encoding, count = ks.asm('jmp qword ptr[0x%016x]' % (iat_rvas[dllname][funcname] - 0x1000 - (cfrom - dist)))
421
				fio.seek( (cfrom - dist) + 0x1000 )
422
				fio.write( bytes(encoding) )
423
424
			else:
425
				print(f'unknown!!!! 0x{img_base+0x1000+cfrom:016x}')
426
427
		fio.seek(0)
428
		with open('dump.exe', 'wb') as fout:
429
			fout.write( fio.read() )
430
431
		print('PE image dumped')
432
		print('Done')
433
		exit(1)
434
435
# ----------------------------------------------------------------------------------------------------------------
436
def action_callback_NtProtectVirtualMemory( event ):
437
	global target_pe
438
	global mem_pages
439
440
	process = event.get_process()
441
	thread  = event.get_thread()
442
	context = thread.get_context()
443
444
	img_base = process.get_image_base()
445
	img_size = target_pe.OPTIONAL_HEADER.SizeOfImage
446
447
	address = process.read_qword(context['Rdx'])
448
	size = process.read_qword(context['R8'])
449
	mode = context['R9']
450
451
	if address >= img_base and address <= img_base + img_size:
452
		print(f'NtProtectVirtualMemory: address=0x{address:016x} size=0x{size:016x} prot={mode:x}')
453
		if address not in mem_pages:
454
			mem_pages.append( address )
455
		elif len(mem_pages) > 1 and address == mem_pages[-1]:
456
			# memory bp on the .text(0) section
457
			event.debug.erase_all_breakpoints()
458
			pid = process.get_pid()
459
			pages = (align_int(target_pe.sections[0].Misc_VirtualSize, 4096) // 4096)
460
			print(f'page_breakpoint: address=0x{img_base + target_pe.sections[0].VirtualAddress:016x} pages={pages} size=0x{pages*4096:x}')
461
			event.debug.define_page_breakpoint(
462
				pid, 
463
				img_base + target_pe.sections[0].VirtualAddress, 
464
				pages=pages, 
465
				action=action_callback_page_access)
466
			event.debug.enable_page_breakpoint(pid, img_base + target_pe.sections[0].VirtualAddress)
467
468
# ----------------------------------------------------------------------------------------------------------------
469
class MyEventHandler( EventHandler ):
470
	def load_dll( self, event ):
471
		module = event.get_module()
472
		if module.match_name('ntdll.dll'):
473
			# set HWBP on ntdll.NtProtectVirtualMemory
474
			address = module.resolve( 'NtProtectVirtualMemory' )
475
			tid = event.get_thread().get_tid()
476
			event.debug.define_hardware_breakpoint(
477
				tid, 
478
				address, 
479
				triggerFlag=Debug.BP_BREAK_ON_EXECUTION, 
480
				sizeFlag=Debug.BP_WATCH_BYTE, 
481
				action=action_callback_NtProtectVirtualMemory)
482
			event.debug.enable_hardware_breakpoint(tid, address)
483
484
# ----------------------------------------------------------------------------------------------------------------
485
def debugger( argv ):
486
	global target_pe
487
	global mem_pages
488
489
	target_pe = pefile.PE(argv[0], fast_load=True)
490
	mem_pages = list()
491
492
	with Debug( MyEventHandler(), bKillOnExit = True ) as debug:
493
		debug.execv( argv )
494
		debug.loop()
495
496
# ----------------------------------------------------------------------------------------------------------------
497
if __name__ == "__main__":
498
	debugger( sys.argv[1:] )