Оглавление

1. Введение

1.1 Общие сведения

Проект pykd стартовал в 2010 году. Основным мотивом для разрботки было неудобство встроенных средств для написания отладочных скриптов для windbg. Язык python был выбран в качестве альтернативного скриптового двжика по многим причинам: легкость изучения самого языка, наличие большой стандартной библиотеки, наличие мощного и удобного фреймворка для создания модулей расширения. Pykd представляет собой модуль для интерпретатора CPython. Сам pykd написан на C++ и использует Boost.Python для экспорта функция и классов в Python. Pykd предоставляет доступ к управлению отладкой на платформе Windows через библиотеку Debug Engine и получению символьной информации через библиотеку MS DIA. Отметим, что pykd не дает прямого доступа к COM интерфейсам Debug Engine и MS DIA. Вместо этого он реализует собственный интерфейс, делающий процесс разработки более быстрым и удобным ( мы на это надеемся ).

Отметим, что pykd может работать в двух режимах: как плагин для windbg, в этом случае он предоставляет команды для запуска скриптов в контексте отладочной сесии; как отдельный модуль для интерпретатора python. Последний режим может быть полезен для создания автоматических средств разбора креш-дампов например.
Содержание

1.2 Быстрый старт

Содержание

1.3 Сборка из исходников

Содержание

1.4 Ручная установка

1.5 Изменения в API

2. Команды для windbg

2.1 Загрузка плагина

Для загрузки плагина в windbg необходимо выполнить команду:
kd>.load pykd_path/pykd.pyd
 

Если pykd.pyd находится в каталоге winext ( подкаталог в Debugging Tools for Windows ), то путь к pykd можно не указывать:
kd>.load pykd.pyd

Если pykd.pyd переименовать в pykd.dll, то расширение можно не указывать:
kd>.load pykd

Просмотреть загруженные расширения windbg можно с помощью команды .chain
kd>.chain

Выгрузить плагин:
kd>.unload pykd_path/pykd.pyd
kd>.unload pykd.pyd
kd>.unload pykd

Чтобы не загружать pykd каждый раз, можно после загрузки плагина выполнить команду "Save Workspace". После этого pykd будет загружаться автоматически для данного воркспейса.
Содержание

2.2 Запуск скрипта

Запуск скрипта осуществляется с помощью команды !py
kd>!py script_path/script_name.py  param1 param2 ... 

Расширение .py можно опустить. Чтобы не указывать полный путь к скрипту, нужно прописать его в переменную окружения PYTHONPATH или добавить ключ в раздел реестра
HKEY_LOCAL_MACHINE\SOFTWARE\Python\PythonCore\2.6\PythonPath
В этом случае пути к скриптам задаются в Default значении созданного ключа.

К параметрам в скрипте можно получить доступ через список sys.argv:
import sys
print "script path: " + sys.argv[0]
print "param1: " + sys.argv[1]
print "param2: " + sys.argv[2]

Содержание

2.3 режим консоли

Запуск консоли python осуществляется командой !py без параметров
1: kd> !py
Python 2.6.5 (r265:79096, Mar 19 2010, 18:02:59) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> 

Перед запуском будет автоматически выполнен импорт pykd, так что сразу можно вызывать функции pykd. Напомним, что выход из консольного режима осуществляется через функцию quit().
>>> quit(0)

2.4 локальный и глобальный режим

Запуск скриптов в может осуществляться в различных окружениях: в глобальном, при этом время жизни всех глобальных объектов совпадает со временем загрузки pykd в памяти windbg; и в локальном, при этом все глобальное пространство переменных будет очищено при окончании работы скрипта. По умолчанию, все скрипты выполняются в локальном окружении, а консоль запускается в глобальном. Можно задать явно режим работы с помощью параметров --global ( -g ) и --local ( -l ).
Параметры вызова Описание
!py script_name выполняется скрипт в локальном окружении
!py python консоль в глобальном окружении
!py -g script_name выполняется скрипт в глобальном окружении
!py -l python консоль в локальном окружении

Содержание

3. Управление отладкой

3.1 Остановка и возобновление процесса отладки

В windbg для этого служат команды Break ( Ctrl+Break ) и Go ( F5 ).
Их аналоги в pykd:
go()
breakin()

go возобновляет процесс отладки и вернет управление только когда отладчик будет снова остановлен - сработает точка останова или отладка будет остановлена вручную через Ctrl+Break.
Это нужно учитывать при написании скриптов. Функция может вернуть исключение DbgException. Обычно это происходит если отлаживаемый процесс завершается.
try:
     while True:
         go()
         print "break"
 except:
     print "process terminted"

Данный скрипт будет обрабатывать любые остановки отладчика и автоматически возобновлять исполнение.

Использовать функцию breakin при обычной работе врядли понадобится. Дело в том, что скрипты как правило выполняются только во время остановки отладчика. А в этот момент вызов функции breakin не имеет смысла. Для того, чтобы можно было останавливать процесс отладки из скрипта, придется создать отдельный поток в котором и вызывать эту функцию.
Внимание! Не пытайтесь использовать функции breakin, go, trace внутри обработчиков отладочных событий ( например, в условных точках останова ).
Содержание

3.2 Пошаговое выполнение

Для пошаговой отладки ( трассировки ) служат две функции:
step()
trace()
Из действие аналогично командам отладки "trace into" и "trace over". Обе функции могут вернуть исключение DbgException, если отлаживаемый процесс уже завершился.
Содержание

3.3 Управление отладкой из python приложений.

Если вы хотите исполнять свои скрипты вне windbg, то первым шагом, который вам необходимо сделать, будет создание отладочной сессии. Более подробно управление сессиями будет рассмотрено в соответствующем разделе. Если ваше приложение не планрует использовать несколько сессий отладки, то заботится об этом не надо - первая сессия будет создана автоматически при следующих вызовах:
loadDump( dumpName ) - загружает креш-дамп
id startProcess( imageName) - запускает в режиме отладки новый процесс
id attachProcess( processId) - присоединяет отладчик к существующему процессу
attachKernel( parameterStr) - присоединяет отладчик к ядру отлаживаемой системы

Для отсоединения отладчика от отлаживаемого процесса служит вызов detachProcess(id)

Для остановки отладки и удаления отлаживаемого процесса служит
функция killProcess(id)

для закрытия креш-дампа, открытого ранее через ф. loadDump, служит функция closeDump(id).

Узнать, режим отладчика можно с помощью вызовов:
  • bool isDumpAnalyzing()
  • bool isKernelDebugging()
Первая функция позволяет определить, находится ли отладчик в состоянии "живой" отладки или анализируется дамп памяти. Вторая функция позволяет различать отладку режима ядра или пользовательского режима. Если скрипт использует специфичные системные символы ( к примеру, символы ядра Windows ), то будет полезно вставить такую проверку в начале скрипта: это позволит сообщить пользователю, что он пытается запустить скрипт в неподходящей ситуации.

Содержание

3.4 Печать отладочной информации

Для вывода информации на экран можно воспользоваться стандартным оператором print. Но рекомендуется использовать специальные функции:
dprint( message, dml = False )
dprintln( message, dml = False )
Вторая функция отличается от первой тем, что автоматически добавляет символ перевода строки. Опциональный параметр dml включает вывод с DML разметкой ( работает только в windbg ). Разметка DML похожа на очень-очень упрощенный HTML. Форматирование текста осуществляется с помощью специальных тегов:
  • <b></b> - выделенный шрифт
  • <i></i> - курсив
  • <u></u> - шрифт с подчеркиванием
  • <link cmd = "cmdStr">command</link> - выполнение команды ( похоже на тег <a> в HTML )
dprinln("<b><u>This command reload all symbols</b></u>", True)
dprinln("<link cmd=\".reload /f\">reload</link>", True)

Содержание

3.5 Выполнение команд отладчика

Для выполнения команды отладчика служит функция:
str dbgCommand( commandStr, suppressOutput = True )
Это функция в зависимости выполняет команду windbg ( в том числе и вызов расширений, и за исключением команд специфичных для windbg, например, .cls). В зависимости от опционального параметра suppressOutput вывод будет перехвачен ( и возвращен как результат функции ) или обработан обычным порядком.
s = dbgCommand("!analyze -v")
dprint(s)


Для вычисления выражения ( аналог команды отладчика "?" ) служит функция
str expr( expressionStr )
expr("@rax + 10")

При выполнении python программы вам могут понадобиться команды из стандартных расширений windbg ( например, !analyze ). Их придется загрузить вручную. Для этого служит функция:
long loadExt( extensionPath )
Эта функция возвращает дескриптор расширения, который нужен для вызова функция расширения:
  • str callExt( extHandle, command, params )
И, если необходимо, для выгрузки расширения:
  • removeExt( extHandle )
Содержание

3.6 Создание креш-дампа

Сохранить состояние системы в виде креш-дампа можно с помощью ф. writeDump. Функция доступна для режима ядра и пользовательского режима. Второй параметр задает тип дампа ( False - полный дамп, True - минидамп )
writeDump( r"C:\dump\fulldump.dmp", False )
writeDump( r"C:\dump\minidump.dmp", True )

Содержание

4. Работа с памятью и регистрами

4.1 Доступ к регистрам общего назначения.

Доступ к регистрам общего назначения ( GPR ) осуществляется с помощью ф. reg:
  • reg( regName )
  • reg( regIndex )
Первый варинат принимает символьное имя регистра, второй - целочисленных индекс. Вторую форму можно использовать для перечисления регистров:
import pykd

try:
    i = 0
    while True:
        r = pykd.reg(i)
        pykd.dprintln( "%s    %x ( %d )" % ( r.name(), r, r )
        i += 1
except pykd.BaseException, IndexError
       pass

Если информация о регистре не может быть получена, будет возбужено исключение BaseException
Содержание

4.2 Доступ к модельно-специфичным регистрам ( MSR )

Доступ к модельно-специфичный регистрам осуществляется через ф. rdmsr( msrNumber ):
>>> print findSymbol( rdmsr( 0x176 ) )
nt!KiFastCallEntry

Содержание

4.3 Управление режимами процессора

Режим процессора определяется константами:
  • CPUType.I386
  • CPUType.AMD64
Другие типы процессоров в данный момент не поддерживаются.
Для определения типа процессора служит функция getCPUType. Данная функция не имеет никакого отношения к получению информации о физическом процессоре. Например, если процессор имеет расширение команд AMD64 но работает под управлением 32 битной операционной системы, функция getCPUType вернет CPUType.I386. Таким образом, данную функцию можно рассматривать как аналог функции is64bitSystem.

При работе на 64 битной операционной системе можно столкнуться с отладкой WOW64 процессов. При этом процессор находясь в 64 битном режиме выполняет 32 битный код. Для определения данной ситуации может помочь функция getProcessorMode. Если процессор выполняет 32 битный код WOW64 процесса, эта функция вернет значение CPUType.I386 ( при этом getCPUType будет возвращать "настоящий" режим процессора - CPUType.AMD64)
При отладке WOW64 процессов часто бывает нужно обращаться и к "32 битной части" процесса и к "64 битной". Сменить режим виртуализации можно с помощью функции setCPUMode(mode). В некоторых случаях может быть полезна альтернативная функция switchCPUMode(). Последняя функция не требует параметров и работает по приницпу триггера. При пытке сменить режим процессора не в контексте WOW64 потока будет возбуждено исключение DbgException.
Содержание

4.4 Нормализация виртуальных адресов

Все функции pykd возвращают виртуальные адреса в т.н нормализованном виде. Он представляет собой 64 битное целое число. Для 32 битных платформ адрес раширяется с учетом знака до 64 битного. Эта операция на С выглядит так:
ULONG64   addr64 = (ULONG64)(LONG)addr;

Таким образом адреса будет преобразовываться так:
0x00100000 -> 0x00000000 00100000
0x80100000 -> 0xFFFFFFFF 80100000
Это нужно учитывать, если адреса, которые возвращают функции pykd, участвуют в арифметических операциях. Для исключения возможных ошибок сравнения рекомендуется использовать ф. addr64():
import pykd
nt = pykd.module("nt")
if nt > addr64( 0x80000000 ):
    print "nt module is in highest address space"

Содержание

4.5 Прямой доступ к памяти

Для доступа к памяти отлаживаемой системы pykd представляет большой набор функций.
Для чтения целых беззанковых чисел служат следующие функции:
  • ptrByte( va )
  • ptrWord( va )
  • ptrDWord( va )
  • ptrQWord( va )
Для получения результата в виде целых чисео со знаком служат аналогичные функции:
  • ptrSignByte( va )
  • ptrSignWord( va )
  • ptrSignDWord( va )
  • ptrSignQWord( va )
Для удобства разработки кроссплатформенных скриптов служат функции:
  • ptrMWord(va)
  • ptrSignMWord(va)
  • ptrPtr(va)
Они возвращают результат в зависимости от битности платформы - 32 или 64 бита
Часто требуется прочесть блок памяти. Для этого служат функции:
  • loadBytes( va, count )
  • loadWords( va, count )
  • loadDWords( va, count )
  • loadQWords( va, count )
  • loadSignBytes( va, count )
  • loadSignWords( va, count )
  • loadSignDWords( va, count )
  • loadSignQWords( va, count )
  • loadPtrs( va, count )
Все функции возвращают объект list.
Содержание

4.6 Ошибки доступа к памяти

Все функции работы с памятью при невозможности прочесть данные по указанному адресу возвращают исключение MemoryException.
try:
    a = ptrByte( 0 )
except MemoryException:
    print "memory exception ocurred"

Проверить валидность виртуально адреса можно с помощью ф. isValid(va)
Содержание

4.7 Чтение строк из памяти

Часто приходится читать из памяти строковые данные. Конечно, для этого можно было бы использовать ф. loadBytes, но это не всегда удобно. Поэтому в pykd добавлен набор функций, возвращающих данные в виде строки.
В первую очередь это:
  • loadChars( va, count )
  • loadWChars( va, count )
Они работают совершенно также как loadBytes и loadWords. Отличие только в возвращаемом значении ( string вместо list ). Это позволяет использовать их, например, совместно с модулем struct:
from struct import unpack
shortField1, shortField2, longField = unpack('hhl', loadChars( addr, 8 ) )

Для чтения 0-терминированных строк из памяти служат функции:
  • loadСStr( va )
  • loadWStr( va )
Обе возвращают строки ( loadWStr - UNICODE ). Отметим небезопасность использования данных функций - ведь наличие терминирующего нуля никто не гарантирует! Внимание! Максимальная длина строки ограничена 64K. При попытке прочесть строку длинее, будет возвращено исключение MemoryException.

В ядре Windows для представления строк используются структуры UNICODE_STRING и ANSI_STRING. Для работы с ними существуют соответствующие функции:
  • loadAnsiString
  • loadUnicodeString
Содержание

5. Модули

5.1 Класс module

Модуль - это исполняемый файл, отображенный на память. Обычная программа состоит из главного модуля ( как правило, с расширением .exe ) и набора библиотек. Работа с модулями осуществляется с помощью класса module.

5.1.1 Создание экземпляра класса module:

Класс module имеет две формы конструктора:
  • module( moduleName )
  • module( va )
Первая форма создает модуль по его имени, второя - по виртуальному адресу, принадлежащему модулю. Если модуль не найден, конструктор возбудит исключение BaseException.
Пример:
from pykd import *
try
    ntdll = module( "ntdll" )
    print ntdll.name(), hex(ntdll.begin()), hex(ntdll.size()) 
except BaseException:
    print "module not found"

5.1.2 Получение информации о модуле

Для этого служат слежующие методы класса module:
  • name() - возвращает имя модуля
  • image() - возвращает имя исполняемого файла
  • pdb() - возвращает имя и полный путь к файлу с символьной информацией
  • begin() - возвращает виртуальный адрес, по которому загржен модуль
  • end() - возвращает виртуальный адрес конца модуля
  • checksum() - возвращает контрольную сумму
  • timestamp() - возвращает временную метку
  • getVersion() - возвращает кортеж, предсталяющий версию модуля. Например: ( 1, 0, 6452, 0 )
  • queryVersion( valueName ) - возвращает значение из ресурсов моудля

5.1.3 Загрузка и доступ к символам.

Для загрузки символьной информации служит метод reload()
Для поиска виртуального адреса, соответствующего нужному символу служит метод offset( symName ). Если указаный символ не найден, будет возбужедено исключение BaseException. Вместо явного вызова ф. offset можно получить адрес, соответствующий символу, обратившись к нему как свойству класса module
>>> nt = module("nt")
>>> print hex( nt.offset("PsLoadedModuleList") )
0xfffff801acb5ae80L
>>> print hex( nt.__getattr__("PsLoadedModuleList") )
0xfffff801acb5ae80L
>>> print hex( nt.PsLoadedModuleList )
0xfffff801acb5ae80L

Иногда может понадобится RVA символа, получить его можно с помощью ф. rva( symbolName ).

5.1.4 Приведения к другим типам

Экземпляр класса module имеет операторы приведения к строке ( str ) и целому числу:
>>> nt = module("nt")
>>> print nt
Module: nt
Start: fffff801ac882000 End: fffff801acfc8000 Size: 746000
Image: ntkrnlmp.exe
Pdb: c:\sym\ntkrnlmp.pdb\569F266AE67D457D969D92298F8F98082\ntkrnlmp.pdb
Timestamp: 4f7118bb
Check Sum: 6b3b15

>>> print hex(nt)
fffff801ac882000

Кроме того, экземпляр класса module может участвовать в арифметических операциях:
>>> print hex( nt + 10 )
0xfffff801ac88200aL

5.1.5 Получение информации о типе

Кроме символов, описывающих переменные и функции ( сущности, которые имеют RVA ), могут быть символы, описывающие типы. Для них, естественно, RVA не задан.
Если для модуля есть информация о типах, то ее можно получить через функцию type( typeName ). Эта функция возвращает экземпляр класса typeInfo, работа с которым будет рассмотрена позже.
>>> nt = module("nt")
>>> print nt.type("_MDL")
struct/class: _MDL Size: 0x1c (28)
   +0000 Next                : _MDL*
   +0004 Size                : Int2B
   +0006 MdlFlags            : Int2B
   +0008 Process             : _EPROCESS*
   +000c MappedSystemVa      : Void*
   +0010 StartVa             : Void*
   +0014 ByteCount           : ULong
   +0018 ByteOffset          : ULong

5.1.6 Типизированные переменные

pykd позволяет упростить работу с сложными типами, такими как классы и структуры. За это отвечает специальный класс typedVar. Получить экземпляр класса typedVar можно через методы класса module:
  • typedVar( va )
  • typedVar( symbolName )
  • typedVar( typeName, va )
>>> nt = module("nt")
>>> print nt.typedVar( "_LIST_ENTRY", nt.PsLoadedModuleList )
struct/class: _LIST_ENTRY at 0xfffff8000369c650
   +0000 Flink               : _LIST_ENTRY*   0xfffffa8003c64890
   +0008 Blink               : _LIST_ENTRY*   0xfffffa80092f8f30

Содержание

5.2 Обработка событий загрузки и выгрузки модуля

Для обработки событий загрузки и выгрузки модуля надо создать наследника класса eventHandler.
Обработка события загрузки модуля осуществляется методом onLoadModule. Обработка события выгрузки модуля - onUnloadModule
Содержание

6. Получение символьной информации

6.1 Символьные ( pdb ) файлы

При сборке модуля создается файл с символьной ( отладочной ) информацией ( обычно, с раширением pdb ). В зависимости от настроек компилятора он может содержать полную или обрезанную информацию ( т.н "публичные символы" ). Символьные файлы могут содержать следующую информацию:
  • Имена, типы и относительные смещения глобальных переменных и констант
  • Имена, параметры и относительные смещения функций и методов классов
  • Имена типов, определенных пользователем ( структур, классов, перечислений )
  • Значения констант
  • Информацию о локальных переменных функций и методов классов.
Для работы с символьными файлами Microsoft предоставляет специальную библиотеку MS DIA. Pykd использует ее для работы с символами. Для непосредственного доступа к символьной инфорамции pykd реализует свой собственный интерфейс.
Содержание

6.2 Информация о типах

6.2.1 Класс представления типа

Для представления информации о типе в питон экспортируется класс typeInfo. Этим классом описываются структуры, классы, объединения, перечисления, битовые поля, указатели и базовые типы.
Класс представляет следующие методы:
  • name - получение имени типа
  • size - получение полного размера типа
  • staticOffset - получение смещения статического поля
  • fieldOffset - получение смещения поля
  • bitOffset - получение смещения битового поля
  • bitWidth - получение размера битового поля
  • field - получение поля ( по имени или по номеру )
  • fieldName - имя поля ( по номеру )
  • fields - возвращает список кортежей: ( fieldName, fieldType )
  • deref - разыменование указателя
  • ptrTo - формирование указателя на тип
  • arrayOf - формирование массива, элементами которого является тип
  • append - добавление поля (метод для структур и перечислени, созданных через вызов функции createStruct или createUnion

Методы для интроспекции:
  • isArray - тип является массивом
  • isPointer - тип является указаетлем
  • isVoid - void тип
  • isBase - тип является базовым
  • isUserDefined- тип определен пользователем ( структура, класс, объеденение и.т.д.)
  • isEnum - тип является перечислением
  • isBitField - тип является битовым полем
  • isFunction - тип является функцией
  • isConstatnt - тип является константой

Специальные методы python:
  • str - позволяет получать строковое представление объекта класса typeInfo и, например, использовать его с оператором print.
  • getattr - позволяет использовать оператор .. для доступа к полям структуры, вместо метода field( name ).
  • len - позволяет использовать встроенную функцию len. Возвращает количество полей у структуры или элементов массива.
  • getitem - позволяет использовать оператор for ... in для перечисления полей структур.
Содержание

6.2.2 Получение объекта типа

Объект типа можно получить вызовом конструктора, передав в него имя типа. Передаваемая строка может содержать как полную спецификацию типа ("имямодуля!имятипа"), так и просто "имя_типа".
Объект типа можно получить косвенным образом:
  • метод type у объекта типа module - формирование объекта типа по имени
  • метод type у объекта типа typedVar - формирование объекта типа, который имеет переменная

Пример (печать структуры _UNICODE_STRING из ntdll):
>>> us = module("ntdll").type("_UNICODE_STRING")
>>> print us
class/struct : _UNICODE_STRING Size: 0x10 (16)
   +0000 Length                  : UInt2B
   +0002 MaximumLength           : UInt2B
   +0008 Buffer                  : UInt2B*

Для получения всех типов модуля можно использовать enumTypes у объекта типа module, который возвращает список имен типов, информация о которых представлена в отладочных символах модуля.
Содержание

6.3 Динамическое определение типа

Часто при отладке информация о типе переменных отсутствует в отладочных символах. Pykd позволяет определить информацию о типе динамически. Для этого служат функции:
  • createStruct( name, align )
  • createUnion( name, align )
Первая создает объект класса typeInfo, представляющий C тип "структура" с именем name. Опционально можно задать вывравнивание полей структуры через параметр align. Вторая функция возвращает объект, представляющий C тип "объеденение".
Чтобы добавить к структуре новое поле, класс typeInfo предоставляет метод append(fielName. fieldType). Все сложные типы состоят в конечном итоге из базовых. Чтобы добавить поле базового типа в структуру, нам необходимо получить объект класса typeInfo для базового типа. Для этого в pykd есть тип baseTypes со следующими полями:
  • UInt1B
  • UInt2B
  • UInt4B
  • UInt8B
  • Int1B
  • Int2B
  • Int4B
  • Int8B
  • Long
  • ULong
  • Bool
  • Char
  • WChar
  • VoidPtr
  • Float
  • Double
Задание новой структуры может выгядеть так:
myStructType = createStruct("MyStruct")
myStructType.append( "field1", baseTypes.UInt4B )
myStructType.append( "field2", baseTypes.UInt2B )
myStructType.append( "field4", baseTypes.Float )

Определение этой структуры можно использовать для типизированного доступа к памяти:
myStructVar = typedVar( myStructType, offset )
print myStructVar.field4


Если нужно определить новый тип, являющися производным от имеющегося ( указатель на перемнную известного типа или массив элементов известного типа), нужно воспользоваться следующими методами класса typeInfo:
  • arrayOf(size)
  • ptrTo(size)
Параметр size метода ptrTo - опциональный. Если его не задавать - размер указателя будет выбран в соответствии с текущей платоформой ( обычно, это и нужно ).
Добавим еще два поля в структуру из нашего предыдущего примера:
myStructType = createStruct("MyStruct")
myStructType.append( "field1", baseTypes.UInt4B )
myStructType.append( "field2", baseTypes.UInt2B )
myStructType.append( "field4", baseTypes.Float )
myStructType.append( "field5", baseTypes.Char.arrayOf(5) )
myStructType.append( "field6", exsistType.ptrTo() )

Содержание

7. Типизированные переменные

7.1 Класс typedVar

Ранее мы рассмотрели пример, как можно прочесть из памяти структурированные данные:
from struct import unpack
shortField1, shortField2, longField = unpack('hhl', loadChars( addr, 8 ) )

Очевидно, что работать с большими структрами, содержащими сотни полей, так не очень удобно. Поэтому в pykd реализован специальный класс: typedVar, позволяющий работать со сложными структурами данных. Информацию о типе данных typedVar получает из символьной информации.
Содержание

7.2 Создание экземпляра класса typedVar

Существует несколько перегруженных конструкторов класса typedVar:
  • typedVar( symbolName )
  • typedVar( typeName, va )
  • typedVar( typeInfo, va )
t1 = typedVar( "MyModule!MyVar" )
t2 = typedVar( "MyModule!MyType", addr )
ti = typeInfo( "MyModule!MyType" )
t3 = typedVar( ti, addr )

Все три способа приведут к одинаковому результату, если addr - адрес переменной MyVar. Отметим, что все эти способы ( и в особенности первый ) не являются оптимальными в плане производительности, так как существенное время может уходить на поиск символьной информации. Если есть возможность, лучше воспользоваться методами класса module:
  • module.typedVar( va )
  • module.typedVar( symbolName )
  • module.typedVar( typeName, va )
mod = module("MyModule")
t4 = mod.typedVar( addr )
t5 = mod.typedVar( "MyVar" )
t6 = mod.typedVar( "MyType", addr )

Результат будет аналогичный прямому вызову конструктора. Однако экземпляр класса module оптимизирует доступ к символьной информации.

В случае ошибки в задании имени переменной или типа будет возбуждено исключение SymbolException.
try:
    typedVar( "MyModule!NotExistVar")
except SymbolException:
    print "The var does not exist"

Содержание

7.3 Методы класса typedVar

  • getAddress() - возвращает адрес переменной
  • sizeof() - возвращает размер переменной
  • field(name) - возвращает значение поля стуркутры ( также объект класса typedVar)
  • fields() - возвращает набор полей структуры в виде списка кортежей ( имя, смещение, значение )
  • deref() - для указателей выполняет т.н. разыменование и возвращает результат в виде экземпляра класса typedVar
  • type() - возвращает тип переменной в виде экземпляра класса typeInfo
Также класс typedVar содержит специальные методы python:
  • __str__ позволяет преобразовывать переменную к печатному виду
  • __len__ позволяет использовать функицю len и возвращает количество элементов ( полей, элементов массива и.т.д )
  • __getitem__ позволяет использовать оператор in
  • __getattr__ позволяет использовать оператор . для доступа к полям.
Методы для работы с функциями:
  • getDebugStart - возвращает адрес внутри тела функции после пролога. Это позволяет ставить точки останова после формирования фрейма, что позволяет получать корректные значения параметров функции
  • getDebugEnd - возвращает адрес внутри тела функции, сразу перед эпилогом.
Содержание

7.4 Классы и структуры

Получить доступ к полям структуры можно с помощью метода field. Для удобства использования добавлен метод для доступ к полям как к аттрибутам класса:
>>>tv = typedVar( "structVar")
>>>tv.field("m_field) == tv.m_field
True

Кроме того, можно получить доступ к полям структуры по индексу:
tv = typedVar( "structVar")
for i in range(0,len(tv) )
    fieldName, fieldValue = tv[i]  
    print fieldName, fieldValue

Как видно из примера, в этом случае возвращается кортеж ( tuple ) из имени поля и его значения. Тот же пример можно записать короче:
tv = typedVar( "structVar")
for fieldName, fieldValue in tv:
    print fieldName, fieldValue


Переменные типа typedVar могут участвовать в арифметических операцих. В качестве значения берется адрес переменной. Внимание: при арифметических операциях не действуют правила адресной аримфетики Си. Адрес будет трактоваться просто как число и , соответственно, var+1 просто инкрементирует знаечние адреса.

Содержание

Массивы и указатели

Класс typedVar позволяет работать с массивами, в том числе многомерными. Для доступа к элементам массива нужно использовать оператор индекса []:
>>> tv = typedVar( "intMatrix" )
>>> print tv
Int4B[2][3] at 0x13f159150
>>> print tv[1]
Int4B[3] at 0x13f15915c
>>> print tv[1][2]
Int4B at 0x13f159164 Value: 0x5 (5)

Класс typedVar может работать так же и с указателями. Для "разыменования" указателя служит функция deref():
>>> tv = typedVar("ptrIntMatrix")
>>> print tv
Ptr Int4B(*)[2][3] at 0x13f1591c0 Value: 0x13f159150
>>> print tv.deref()
Int4B[2][3] at 0x13f159150
>>> print tv.deref()[1][2]
Int4B at 0x13f159164 Value: 0x5 (5)


Переменные typedVar могут участвовать в арифметический выражениях. Для массивов в качестве значения берется его адрес, для указателя - значение указателя ( т.е куда он указывает ). Внимание: при арифметических операциях не действуют правила адресной аримфетики Си.

Содержание

7.6 Энумераторы

Для работы с энумераторами будет полезно получить доуступ к информации о типе энумератора, так как именно через нее можно получить соответствие численных констант и символьных имен. Сделать это можно через метод type, который возвращает ссылку на переменную типа typeInfo:
var = typedVar( "myStruct" )
if  var.structType == var.structType.type().TYPE_ONE:
    print "TYPE_ONE"
else:
    print "ANOTHER_TYPE"

Класс typeInfo имеет метод asMap(), который для энумераторов возвращает объект типа dict, в котором ключами являются числовые константы, а значениями - их символьное представление:
var = typedVar( "myStruct" )
{
"TYPE_ONE" :  lambda var.field_one
"TYPE_TWO" : lambda var.field_two 
}[ var.type().asMap[ var.structType ] ]()

Данный пример во-первых демонстрирует как сделать на python логическую структуру, аналогичную оператору switch в Cи: для этого можно использовать тип dict, значения полей которого являются лямбда-выражениями. Во-вторых, он показывает как для энумератора получить соответствие численной константы и символьного имени.

Экземпляры typedVar, содержащие энумераторы, можно использовать в арифметических операциях:
>>>var = typedVar( "myStruct" )
>>>print var.structType *2 + 10

Содержание

7.7 Функции

Функции также могут быть представлены в виде экземпляра класса typedVar
Для функций можно вызывать следующие методы:
  • getAddress - адрес начала кода функции
  • getNumberField - вернет количество параметров функции
  • getDebugStart - адрес внутри тела функции, сразу после пролога ( формирования фрейма )
  • getDebugEnd - адрес внрути тела перед эпилогом ( разрушением фрейма )
  • type - вернет тип функции
Содержание

7.8 Приведениe к другим типам

Класс typedVar имеет операторы приведения к строке (str) и к целому числу ( long ).
>>> print str( typedVar("g_struct") )
struct/class: struct3 at 0x13f4391f8
   +0000 m_arrayField            : Int4B[2]   
   +0008 m_noArrayField       : Int4B   0x3 (3)

Целочисленное значение, возвращаемое функций long(), зависит от типа данных, хранящизся в typedVar:
  • Базовые типы - возвращается непосредственное значение
  • Структуры, классы и объединения - возвращается значение указателя на начало данных
  • Энумераторы - возвращается непосредственное занчение
  • Указатели - возвращается непосредственное значение
  • Массивы - возвращается значение указателя на начало данных
>>> long( typedVar("g_struct").m_noArrayField )
3L
>>> hex( long( typedVar("g_struct").m_arrayField ) )
'0x13f4391f8L'
>>> 
>>> long( typedVar("g_struct").m_arrayField[1] )
2L

Содержание

8. Процессы и потоки

8.1 Потоки в пользовательском режиме

В пользовательском режиме отладчик работает в контексте отлаживаемого процесса. Если процесс имеет несколько потоков, в режиме отладки можно переключить контекст на другой поток. Нужно различать текущий поток и поток, на который переключился отладчик. В оригинале они называются: "current thread" - поток который продолжит выполнение после возобновления отладки, "implicit thread" - поток, в контексте которого находится отладчик. Контекстом потока мы называем совокупность регистров процессора, в том числе и указатель текущей инструкции и стека.
Для смены контекста потока служит функция setImplicitThread. В качестве параметра она принимает указатель на TEB (thread enviroment block). Получить указатель на TEB можно с помощью:
  • getImplicitThread - TEB текущего потока
  • getProcessThreads - список TEB-ов всех потоков процесса
Содержание

8.2 Потоки в режиме ядра

В режиме ядра есть некоторые особенности.
Во-первых, функции setImplicitThread и getImplicitThread работают с указателями на ETHREAD, а не с TEB, как в пользовательском режиме. Во-вторых, ф. getProcessThreads для режима ядра не доступна. Если нужно получить список потоков какого либо процесса придется делать это вручную разбирая структуру EPROCESS. К счастью, это не сложно:
nt = module("nt")
process = nt.typedVar( "_EPROCESS", processAddr )
threadLst = nt.typedVarList(process.ThreadListHead, "_ETHREAD", "ThreadListEntry")

Остается добавить, что переключение контекста отлаживаемого потока не приводит к переключению контекста процесса. Подробнее об этом в следующем разделе.
Содержание

8.3 Процессы в режиме ядра

Содержание

9. Локальные переменные

9.1 Текущие локальные переменные

При отладке приложения, ядра системы или анализе аварийного дампа, всегда присутствует текущий поток, а в этом потоке есть текущий фрейм. Если у нас есть отладочная информация о модуле, котрому принадлежит данный фрейм и в отладочной информации присутствует информация о локальных переменных, то мы можем получить доступ к ним в удобной форме, без явных операций с регистрами и стеком. Для этого служит ф. getLocals(). Она возвращает объект типа dict, ключом является имя переменной, а значением - экземпляр класса typedVar:
# print local variable "argc"
print getLocals()["argc"]

# print all local vairables in the current frame
for varName, varValue in  getLocals().items():
    print varName, varValue

Содержание

10. Точки останова

10.1 Задание точек останова

Для задания точки останова служит функция setBp. Она позволяет устанавливать как программные точки останова, так и аппаратные. Функция возвращает числовой идентификатор, который можно в последствии использовать для удаления точки останова через ф. removeBp.
Установка программной точки останова:
nt = module("nt")
bpid = setBp( nt.NtCreateFile )

Установка аппаратной точки останова:
nt = module("nt")
bpid = setBp( nt.NtCreateFile, 1, 4 )

Второй параметр - размер памяти, к которой осуществляется доступ, Третий параметр - тип доступа ( 1 - чтение, 2- запись, 4 - исполнение, типы доступа работают как флаги и могут быть объединены. Например, 3 - чтение + запись ).

Содержание

10.2 Условные точки останова

Для организации точек останова с условием используется функция обратного вызова, которая передается в качестве параметра в вызов setBp. Эта функция обязана принимать один параметр ( туда передается идентификатор сработавшей точки останова ). Чтобы точка останова сработала, функция обратного вызова должна вернуть True
import fnmatch
from pykd import *

nt = module('nt')
objAttrType = nt.type(  "_OBJECT_ATTRIBUTES" )

def onCreateFile( id ):
   objattr = typedVar( objAttrType, ptrPtr( reg('esp') + 0xC ) )
   return fnmatch.fnmatch(  loadUnicodeString( objattr.ObjectName ), '*.exe' )

setBp( nt.NtCreateFile, onCreateFile )

Нужно обратить внимание, что в качестве функции обратного вызова может выступать лямбда-функция:
setBp( myAddr, lambda id: reg('rax') > 0x1000 )

Помните о времени жизни объектов, создаваемых в скриптах! Функции - в python такие же объекты и при завершении виртуальной машины python они будут удалены. И тут возможна следующая ловушка: выполнив предыдущий скрипт с помощью команды "!py setmybreak.py" мы не получим ожидаемого срабатывания точки останова, она будет удалена во время завершения работы скрипта. Что же делать? Есть два варианта:
1. Использовать в скрипте управление отладкой, примерно так:
setBp( nt.NtCreateFile, onCreateFile )
go() 

В таком случае, мы поймаем ровно одно срабатывание точки останова, далее скрипт завершится.
2. Использовать для установки точки останова команду !pycmd.
Напомним данная команда создает глобальный интерпретатор python и все объекты python продолжают быть доступными даже после выполнения команды quit():
>!pycmd
>>>import  setmybreak
>>>quit()
>g

При импортировании модуля будут выполнены все действия по установке точке останова и даже после выхода из консоли функции обратного вызова на python будут работать!

Внимание!
Функции обратного вызова имеют некоторые ограничения на использование API pykd:
  • Нельзя вызывать функции, которые могут изменить состояние отладчика: go, breakin, trace
  • Нельзя вызывать функции, которые могут привести к появлению или уничтожению отладочных сессий: startProcess, killProcess, openDump и.т.д
  • Нельзя манипулировать контекстами потоков и процессов ( setCurrentProcess, setImplicitThread )

Содержание

11. Отладочные события

Содержание

11.1 Обработка точек останова (метод onBreakpoint)

Содержание

11.2 Обработка исключительных ситуаций (метод onException)

Содержание

11.3 Обработка события загрузки исполняемого модуля (метод onLoadModule)

Содержание

11.4 Отработка события выгрузки исполняемого модуля (метод onUnloadModule)

Содержание

12. Класс disasm

disasm reference
Класс disasm является оболочкой над дизассемблером из Debug Engine. Соответственно, результаты его работы такие же, как и у команды u в kd/cdb/windbg.
Класс disasm имеет следующие методы:
  • __init__() - создает дизассемблер, который начнет работу с текущей инструкции
  • __init__( offset ) - создает дизассемблер, который начнет работу с указанного смещения
  • disasm() - возвращает дизассемблированное представление инструкции CPU с текущего смещения и переходит к следующей инструкции
  • disasm( offest ) - возвращает дизассмблированное представление инструкции CPU с текущего смещения и переходит к следующей инструкции
  • asm( code ) - ассемблирует указанную инструкциб и меняет машинный код по указанному смещению
  • begin() - возвращает смещение, заданное при созданни экземпляра класса disasm
  • current() - возвращает текущее смещение
  • length() - возвращает длину текущей инструкции CPU
  • instruction() - возвращает дизассемблированное представление инструкции CPU по текущему смещению
  • ea() - возвращает эффективный адрес последней дизассемблированной инструкции или 0
  • reset() - аналог вызова self.disasm( self.begin() )

Эффективный адрес - это адрес операнда находящегося в памяти. Например, для инструкции
mov ecx, [esi+0x10]
Эффективным адресом будет занчение esi + 0x10. Очевидно, что это значение имеет смысл при дизассемблировании текущей инструкции.
Содержание

Last edited Jan 20, 2015 at 2:18 PM by kernelnet, version 15

Comments

No comments yet.