Эксперимент: ищем int i = 0xDEADBEEF в дампе физической памяти

в 10:17, , рубрики: kd, windbg, адресация, виртуальная память, отладка, разработка под windows, системное программирование, метки:

КДПВ

Изучение виртуального адресного пространства и алгоритма преобразования адресов заметно упростится, если начать с несложного практического примера. Для этого напишем простую программу, выводящую адрес локальной переменной:

int main()
{
    unsigned i = 0xDEADBEEF;
    std::cout << "address of i is " << std::hex << &i;
    std::cin.get(); //Чтобы процесс не завершился
    return 0;
}

Затем попробуем найти физический адрес и просмотреть значение по этому адресу.

Будем рассматривать 32-битную Windows (без всяких Physical Address Extension), потому что 64-битная сложнее. Описание преобразования упрощено, но достаточно для нашего эксперимента. Рекомендую проверять в виртуалке. Неважно в какой, но в конце я покажу как выгрузить дамп памяти в VirtualBox.

Основы

В моем случае адрес получился равным 0x22FF2C. Вообще, он может различаться при каждом запуске программы (см. ASLR). У других процессов по этому адресу могут находиться какие-то свои значения, потому что это не физический, а виртуальный адрес. Пожалуй, основное предназначение виртуального адресного пространства – возможность предоставлять каждому процессу собственное адресное пространство, в котором он бы не мешал другим. Размер виртуального адресного пространства зависит от платформы. Для x86 теоретический максимальный размер составляет 4 Гб. По умолчанию, первая половина (0 – 0x7FFFFFFF) является пространством процессов пользователя, в котором располагается образ исполняемого файла текущего процесса, его стек, куча и прочее. Вторая половина (0x80000000 – 0xFFFFFFFF) – системным. С некоторыми оговорками можно считать, что пространство процессов пользователя уникально для каждого процесса, а системное только одно. Адрес 0x22FF2C, очевидно, попал в первую половину.

Виртуальное адресное пространство разбито на 0x100000 (1048576) страниц размером 4096 байт каждая. Физическая память также разбита на страницы такого же размера, называемых страничными блоками. Страницы (не все, конечно) отображаются на страничные блоки, поэтому для каждой страницы нужна информация о ее расположении в физической памяти. Всем 0x100000 страницам соответствует столько же 4-байтовых записей, называемых PTE (page table entry – запись таблицы страниц). В виртуальном пространстве они расположены в диапазоне адресов 0xC0000000 — 0xC03FFFFF, и занимают 1024 страницы, называемых таблицами страниц. Получить запись просто: k-й странице соответствует k-я запись.

Виртуальная адресация
Оранжевым отмечены таблицы страниц.

virtual_address = 0x22FF2C
page_index = virtual_address / 4096
pte_addr = 0xC0000000 + page_index * 4

Умножение на 4 потому что PTE – 4-байтовые. Получаем, что в нашем случае pte_addr = 0xC00008BC

Наивная попытка

Адрес PTE есть, пробуем узнать что там:

std::cout << "PTE is " << std::hex << *(unsigned*)0xC00008BC;

Ну, ой. Аппаратное исключение. А все потому, что мы пытались читать из системного пространства. ReadProcessMemory тоже не поможет. Вызов VirtualQuery скажет нам PAGE_NOACCESS. Получить доступ можно только получив привилегии режима ядра. Пожалуй, самый простой способ для нашей исследовательской задачи — использовать отладчик ядра.

Использование отладчика ядра

Ставим KD и LiveKd. LiveKD позволяет запускать отладчики ядра Microsoft Kd и Windbg, входящие в пакет инструментов отладки для Windows, в действующей системе в локальном режиме. По последней ссылке также небольшая справка по установке и справке.

Запускаем наш пример (пусть он называется main.exe). Запускаем LiveKd. Пишем "!process 0 0", чтобы вывести список всех работающих процессов, или сразу "!process 0 0 main.exe"

0: kd> !process 0 0 main.exe
PROCESS 86530118  SessionId: 1  Cid: 0dcc    Peb: 7ffdd000  ParentCid: 0428
    DirBase: 2402e000  ObjectTable: 8879f430  HandleCount:  16.
    Image: main.exe

Нас интересует адрес после слова PROCESS (это адрес на структуру EPROCESS, содержащей атрибуты процесса). Подключаемся к процессу:

0: kd> .process 86530118
Implicit process is now 86530118

Проверяем содержимое по адресу 0x22FF2C, чтобы убедиться, что все сделали правильно:

0: kd> dd 22FF2C L1
0022ff2c  deadbeef

По умолчанию используются шестнадцатеричные числа. Команда dd выводит несколько 4-байтовых значений, начиная с указанного виртуального адреса. L1 – вывод только одного значения.

Чтение PTE

0: kd> dd C00008BC L1
c00008bc  6612f847

Можно было самим не считать:

dd C0000000 + (22FF2C >> 0xC) * 4 L1
c00008bc  6612f847

В значении PTE записи 6612f847 первые 20 бит (5 hex-цифр) – индекс страничного блока, остальное – различные флаги. Чтобы получить адрес страничного блока, нужно индекс умножить на размер блока – 4096 байт.

page_block_index = 0x6612F
page_block_address = page_block_index << 12 = 0x6612F000 //Умножение на 4096

Порядок байтов внутри страницы и страничного блока совпадает, поэтому необходимо рассчитать смещение внутри страницы и прибавить к адресу страничного блока:

virtual_adress = 0x22FF2C
offset = virtual_adress & 0xFFF = 0xF2C //Последние три hex-цифры
phisycal_address = page_block_address + offset = 0x6612FF2C

Проверяем:

0: kd> !dd 6612FF2C L1
#6612ff2c deadbeef

Команда !dd аналогична dd, только принимает физические адреса.

Мы выяснили, что наш адрес можно представить так:

0x22FF2C = b 00000000001000101111 111100101100
             20 бит               12 бит
             page_index           byte_offset

Но еще заметьте, что найденная PTE находится в 0-й таблице страниц с индексом 0x22F внутри нее. И наш адрес может быть представлен так:

0x22FF2C = b 0000000000 1000101111 111100101100
             10 бит     10 бит     12 бит
             table_idx  PTE_index  byte_offset

We need to go deeper (PDE)

Пользоваться виртуальными адресами PTE неспортивно. Ведь они тоже являются обычными страницами которым нужно найти страничные блоки. А раз так, то просто найдем свои PTE для этих страниц. Всего у нас 1024 таких страниц (называемых таблицами страниц) и все PTE для них помещаются в одной странице. Эту страницу называют каталогом страниц и она содержит 1024 записи (называемых PDE – page directory entry, запись каталога страниц) с адресами на таблицы страниц.

Виртуальная адресация
Синим отмечен каталог таблиц, оранжевым – таблицы страниц.

Поступаем точно так же, как уже делали:

pte_addr = 0xC00008BC
page_index = pte_addr / 4096 = 0xC0000
pde_addr = 0xC0000000 + page_index * 4 = 0xC0300000

Получили адрес PDE = 0xC0300000 (все PDE хранятся в странице по адресу 0xC0300000, мы попали в нулевую PDE). Проверяем содержимое:

0: kd> dd C0300000 L1
c0300000  0b21d867

Полностью аналогично: PDE, содержащая 0b21d867, дает нам адрес 0x0B21D000 страничного блока с таблицей страниц. Осталось найти в ней нужную PTE. Вспомним, что адресу 0x22FF2C соответствует PTE с индексом 0x22F в 0-й таблице (со смещением 0x22F * 4). Значит, PTE находится по адресу 0x0B21D000 + 0x22F * 4.

0: kd> !dd 0b21d000 + 0x22f * 4
# b21d8bc 6612f847

С адресом 6612f847 мы уже работали.

Осталось выяснить, где в физической памяти находится каталог (так как мы получали PDE с помощью виртуальной адресации). Адрес был указан в DirBase, когда мы просмотрели информацию о процессе командой "!process 0 0 main.exe". В нашем случае DirBase = 2402e000

0: kd> !dd 2402e000
#2402e000 0b21d867

Итоговая формула

0x22FF2C = b 0000000000 1000101111 111100101100
             10 бит     10 бит     12 бит
             PDE_index  PTE_index  byte_offset

pde_addr   = DirBase + PDE_index * 4
pte_addr   = ((*pde_addr) & 0xFFFFF000) | (PTE_index * 4)
value_addr = ((*pte_addr) & 0xFFFFF000) | byte_offset

Ищем в дампе

Думаю, снять дамп с работающей системы несколько проблематично, поэтому снимем с VirtualBox. Для этого нужно запустить в режиме отладки:

VirtualBox.exe --dbg --startvm VM_name

Выбрать в меню "Отладка" -> "Командная строка..." и набрать:

.pgmphystofile "path_to_dump_file"

Открываем файл (я пользуюсь HxD), переходим на 6612ff2c:

Просмотр в дампе

Зная DirBase и виртуальный адрес можно искать значение сразу в дампе, без отладчика. Вообще, в дампе можно найти значение DirBase по имени процесса, но это уже другая история.

Автор: Fil

Источник

Поделиться

* - обязательные к заполнению поля