0xFF tl;dr

This article document what I learned from @zolutal’s blog Understanding x86_64 Paging

0x00 Prologue

This article will go through paging and understand the weird values in memory.

0x01 Enviroment

KZong: git@github.com:n132/KZone.git

0x02 Paging

We need paging since we want to provide virtual memory space for each process so all processes can assume they own the whole memory space. However, we can’t satisfy the huge amount of memory requests: a pointer in x64 has 64 bits so we can logically visit 2^64(more than 16777216 TB) memory but as we know, our little laptops don’t have so much RAM. Therefore, we should have a way to solve the problem and let the processes feel like they have 2^64 bytes of memory space. So a method to map the physical memory address to the virtual memory address is designed.

The smallest element of mapping for x86 linux is page, which is 0x1000 bytes for x86-64. We’ll introduce the method to transform a virtual memory address into a physical memory address.

0x02 Virtual Address

You may feel confused if you debugged user space programs with GDB when you find that all addresses didn’t use the high 2 bytes. For example, if you turn off aslr, 0x7ffffffde000 could be a stack address. If we counter it meticulously, we’ll find there are 12 hex characters, which means 6 bytes are used but a pointer has 8 bytes why do we only use low 48 bits(6 bytes)?

The virtual address is a set of indexes. It includes page offset and the indexes for physically addressing paging tables(directories). The paging table is also some page in on your RAM so we have at most 0x1000 bytes to store the values(512 entries), which is not enough to accommodate all page addresses so we need multi-level level tables.

In the implementation, there are 4 levels of paging tables: Page Global Directory(PGD), Page Upper Directory(PUD), Page Mid-level Directory(PMD), and Page Table Entry(PTE). Each directory requires one index to represent the entries to the next level paging directory. Knowing that each directory has 0x1000 bytes, we can easily compute that we at most have 0x200 entries, which need 9 bits to represent. Plug the 12 bits to represent the in-page offset, we need at least 48(9*4+12) bits. This is the reason why you see so many 6-byte addresses in user space.

There is a little function to get each level’s index from a virtual address:


def page_dir_index(addr):
    offset = addr&0xfff
    PET    = (addr>>12)&0x1ff
    PMD    = (addr>>12>>9)&0x1ff
    PUD    = (addr>>12>>9>>9)&0x1ff
    PGD    = (addr>>12>>9>>9>>9)&0x1ff
    return PGD, PUD, PMD, PET, offset

Assume we have a virtual memory address 0x7f9e8f25b001:

pwndbg> x/8gx 0x7f9e8f25b000
0x7f9e8f25b000: 0x00010102464c457f  0x0000000000000000
0x7f9e8f25b010: 0x00000001003e0003  0x00000000000020e0
0x7f9e8f25b020: 0x0000000000000040  0x000000000000b2c8
0x7f9e8f25b030: 0x0038004000000000  0x001d001e00400007

The corresponding indexes are: (255, 122, 121, 91, 1)

$CR3

We have all the indexes but where should we start? Also, for different processes, we may use the same virtual address. For example, we fork from one process. The child process has the same memory as the parent. However, what they do to their memory space should not influence each other. If we only use the knowledge we talked about in the previous section, we can’t make it. We have to have something special for each process.

The answer is $CR3 register. $CR3 stores the PGD value for each process. The child process has a different $CR3 value than the parent so we solve the problem!

While debugging, you can print the value of $CR3: (If you are not debugging the kernel, you can’t check $cr3)

pwndbg> p/x $cr3
$1 = 0x138138000

Also, you can find the value of PGD by checking the value of task_struct->mm->pgd for a specific task/process.

So we have all the information we need to convert a virtual memory address to a physical memory address!

0x03 Multi-level Page Directory

The more detailed structure of the Multi-level Paging Directory is demonstrated at Zolutal’s blog. I’ll give a lite and the not precise version here for later reference.

PGD

Assume our $cr3 value is 0x138138000 (if it doesn’t end with 000, please zero the last 12 bits to get PGD) and our address’s page directory indexes are (255, 122, 121, 91) so we need to visit the 255th slot of physical memory 0x138138000 to find the entry of next level page directory.

We have two methods to check the physical memory in GDB: monitor xp/gx <physical address> or x/gx physmap+<physical address>. Physmap is an area directory mapped from physical memory and it’s stored in the symbol page_offset_base and its default value is 0xffff888000000000 if there is no kaslr.

So we can use the above two commands to check the entry of next level page directory:

pwndbg> x/gx 0xffff888000000000+0x138138000+255*8
0xffff8881381387f8: 0x000000013836e067
pwndbg> monitor xp/gx 0x138138000+255*8
00000001381387f8: 0x000000013836e067

PUD

We’ll find another physical address: 0x000000013836e067. So we can simply zero the last 12 bits and the first 13 bits to get the PUD Physical Address:

# Since for most cases the high 2 bytes are zero, which means the first 13 bits should be zero I just simply zero the last 12 bits.
pwndbg> p/x (0x000000013836e067>>12<<12)
$1 = 0x13836e000
pwndbg> x/8gx 0xffff888000000000+0x13836e000+8*122
0xffff88813836e3d0: 0x000000013824c067  0x0000000000000000
0xffff88813836e3e0: 0x0000000000000000  0x0000000000000000
0xffff88813836e3f0: 0x0000000000000000  0x0000000000000000
0xffff88813836e400: 0x0000000000000000  0x0000000000000000
pwndbg> 

Another thing we should be careful of is the 7th bit for PUD and PWD. If it’s set, it’s hugfied, which means the physical address we got is the physical address of the final physical page page. But, for our example, it’s not hugified.

PMD

We use the same method to process the entry we got from PUD.

pwndbg> p/x (0x000000013824c067>>12<<12)
$2 = 0x13824c000
pwndbg> x/8gx 0xffff888000000000+0x13824c000+121*8
0xffff88813824c3c8: 0x00000001382fc067  0x00000001382fb067
0xffff88813824c3d8: 0x00000001382fa067  0x0000000138251067
0xffff88813824c3e8: 0x000000013701a067  0x0000000137017067
0xffff88813824c3f8: 0x0000000137016067  0x0000000137015067

So we got the next level entry: 0x00000001382fc067

PET

Apply the same method again, we’ll get the physical address.

pwndbg> p/x (0x00000001382fc067>>12<<12)
$3 = 0x1382fc000
pwndbg> monitor xp/8gx 0x1382fc000+0x8*91
00000001382fc2d8: 0x0000000139523025 0x000000013bb39025
00000001382fc2e8: 0x000000013bab9025 0x000000013958b025
00000001382fc2f8: 0x0000000138f60025 0x0000000138f61025
00000001382fc308: 0x0000000138f7d025 0x0000000138f5e025

pwndbg> monitor xp/8gx 0x0000000139523000+1
0000000139523001: 0x0000010102464c45 0x0300000000000000
0000000139523011: 0xe000000001003e00 0x4000000000000020
0000000139523021: 0xc800000000000000 0x00000000000000b2
0000000139523031: 0x0700380040000000 0x01001d001e004000

pwndbg> x/8gx 0x7f9e8f25b001
0x7f9e8f25b001: 0x0000010102464c45  0x0300000000000000
0x7f9e8f25b011: 0xe000000001003e00  0x4000000000000020
0x7f9e8f25b021: 0xc800000000000000  0x00000000000000b2
0x7f9e8f25b031: 0x0700380040000000  0x01001d001e004000

0x04 Epilogue

In short, all virtual memory addresses can be transformed into physical memory addresses through paging.