R0 CREW

Starting to write Immunity Debugger PyCommands: my cheatsheet (Перевод: dahaka)

Перейти к списку статей от Corelan Team

Много лет назад, когда я только начинал заниматься разработкой эксплоитов для windows, в качестве отладчика я использовал WinDbg и немного Olly. Но несмотря на все преимущества Windbg, было очевидно, что в дальнейшем мне понадобятся какие-либо дополнительные инструменты.

У консольного интерфейса windbg есть много преимуществ, но все же это не лучший инструменты для поиска “хороших” переходов, модулей, скомпилированных без safeseh, не защищенных aslr и.т.д. Ладно, с простым «jmp esp» проблем не возникнет, но вот поиск всех pop pop ret комбинаций в non-safeseh модулях…не слишком простая задача.

Собрать плагины для windbg вполне возможно, но те, что я находил(MSEC, byakugan (Metasploit)), не всегда работали так, как мне нужно, и некоторые проблемы, которые возникали во время создания эксплоитов так и не были решены.

В то же время, OllyDbg и Immunity Debugger существенно отличаются от windbg. Не столько за счет GUI: количество плагинов для этих отладчиков существенно выше. Попробовав оба отладчика — они очень похожи как внешне, так и в использовании — и возможности создания плагинов для них, я остановился на Immunity Debugger.

Это вовсе не значит, что OllyDbg хуже или ограниченнее в плане расширения…В ней сложнее отредактировать плагин «на коленке» во время создания эксплоита. Плагины для OllyDbg компилируются в dll’ки, так что каждое изменение в плагине требует перекомпиляции и тестирования. А в Immunity Debugger используются скрипт на python’e. Я могу открыть скрипт, сделать небольшую правку и тут же увидеть результат, всё просто.

Как для OllyDbg, так и для Immunity Debugger есть много плагинов, которые либо включены в дистрибутив, либо распространяются сообществом. В этих плагинах нет ничего плохого( или в PyCommands в случае Immunity Dbg), мне хотелось иметь отдельный плагин, который помогал бы мне в создании эксплоитов(stack based) от А до Я. Эта идея была реализована в виде моего pvefindaddr PyCommand.

Очевидным выбором был Immunity и Python. Я не великий python-разработчик, но мне удалось создать мой собственный PyCommand за относительно короткий промежуток времени. Это лишь подтверждает, что разработать PyCommands для Immunity, даже если ты не ас в программировании. Если я смог, то и вы сможете наверняка.

Единственная проблема, с которой я столкнулся - помимо изучения синтаксиса python’a и привыкания к раздражающим отступам — был поиск описания работы специфичных для Immunity API/методов/атрибутов. Если честно, справка по API, поставляемая вместе с Immunity, не слишком помогает. В ней содержится лишь список доступных методов/атрибутов…и всё. Никакого объяснения что эти методы и атрибуты делают, для чего они нужны и как их использовать. Когда ты понимаешь как что-то работает, то это может служить какой-то основой, но когда ты изучаешь что-то с нуля, то немного настойчивости явно не помешает.

К счатстью, в дистрибутиве Immunity есть много готовых PyCommands, так что они могут являться основой.

В любом случае, будет полезно иногда обратиться и к справке по ImmDbg Python API. Ее можно найти через пункт меню “Help” → “Select API help file” и выбрать IMMLIB.HLP из папки «Documentation».

В дальшейшем, ее можно окрыть через “Help” → “Open API help file”

Также существует онлайн версия документации по API http://debugger.immunityinc.com/update/Documentation/ref/ . (онлайн версия может содержать больше информации)

Моя основная задача сегодня — создать шпаргалку, которая станет основой для тех, кто хочет писать плагины, так что вы можете создать их быстрее, чем я. Это не будет «полным руководством», но явно поможет начать создавать свои плагины.

В расширениях Immunity используется синтаксис python версии 2.(для тех, кто не знаком с питоном, версии 2. и 3.* значительно отличаются в некоторых моментах, так что при выборе литературы\примеров, обратите внимание, чтобы они были основаны на ветке 2.*).

Создание PyCommand с нуля

Начните с создания файла в директории PyCommands: .py

Имя файла следует запомнить, так как в дальнейшем запуск скрипта будет производиться по имени файла.

Запуск PyCommand

Запустить PyCommand предельно просто: введите имя фалйа (без расширения .py, но с восклицательным знаком вначале) в консоль в нижней части Immunity Debugger.

Так, если вы назвали плагин “plugin1.py”, то запустить его можно следующей командой:

!plugin1

Базовая структура

Базовая структура плагина выглядит следующим образом:

  • загрузите Immunity Libraries (и возможно другие библиотеки, в зависимости от того, что вы хотите сделать)

  • напишите функцию main() которая будет считывать параметры запуска и вызывать функции, опять же в зависимости от того, что вы хотите сделать

  • напишите функции, которые будут выполнять требуемые действия

#!/usr/bin/env python
"""
(c) Peter Van Eeckhoutte 2009
U{Peter Van Eeckhoutte - corelan.<http://www.corelan.be:8800>}

peter.ve@corelan.be
corelanc0d3r

"""
__VERSION__ = '1.0'
import immlib
import getopt
import immutils
from immutils import *

""""""""""""
 Functions
""""""""""""



""""""""""""""""""
 Main application
""""""""""""""""""
def main(args):

Дальше, вам нужно указать, что вы хотите использовать библиотеки Immunity Debugger в своем скрипте. Лучший способ сделать это — объявить переменную, которая создает экземпляр класса Immunity:

imm = immlib.Debugger()

Обычно я объявляю одну глобальную переменную за пределами функции main() (например, вы можете разметить объявление сразу же после блока импорта)

Список доступных классов:

Обработка аргументов

Аргументы\параметры, определяемые при запуске плагина помещаются в массив “args”(на самом деле это не массив, в классическом его представлении, а «список» прим.пер.)

len(args)

Доступ к аргументам есть ни что иное, как проход через массив и получение значение элементов массива.

def main(args):
    if not args:
        usage()
    else:
        print "Number of arguments : " + str(len(args))
        cnt=0
        while (cnt < len(args)):
            print " Argument " + str(cnt+1)+" : " + args[cnt]
            cnt=cnt+1

Вывод в лог, таблицу, файл

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

У вас есть несколько вариантов: вы можете писать в окно Immunity Log (самая распространенная техника), новую/отдельную таблицу (которая есть нечто более, чем новое окно в котором информация представляется в виде таблицы) или в файл (что может быть хорошей идеей, если объем генерируемых данных может переполнить буфер окна «Log»).

Вывод данных в Log Window

Log Window – это часть отладчика, так что нам необходимо использовать экземпляр объекта, который мы объявили ранее, чтобы писать в Log.

Метод, используемый для этого, довольно прост: imm.Log()

def main(args):
	print "Number of arguments : " + str(len(args))
	imm.Log("Number of arguments : %d " % len(args))
	cnt=0
	while (cnt < len(args)):
		imm.Log(" Argument %d : %s" % (cnt+1,args[cnt]))
		if (args[cnt] == "world"):
			imm.Log("  You said %s !" % (args[cnt]),focus=1, highlight=1)
		cnt=cnt+1

Вы также можете указать собственный адрес памяти в левой колонке окна Log Window(значение по умолчанию: 0BADF00D). Все, что для этого нужно — указать адрес памяти(целочисленное значение!) в качестве второго аргумента при вызове метода Log():

def main(args):
	print "Number of arguments : " + str(len(args))
	imm.Log("Number of arguments : %d " % len(args))
	cnt=0
	while (cnt < len(args)):
		imm.Log(" Argument %d : %s" % (cnt+1,args[cnt]),12345678)
		if (args[cnt] == "world"):
			imm.Log("  You said %s !" % (args[cnt]),focus=1, highlight=1)
		cnt=cnt+1

Хорошим тоном будет выводить инструкцию по использованию, если не задан ни один аргумент. Как-то вроде этого:

__VERSION__ = '1.0'
import immlib
import getopt
import immutils
from immutils import *
imm = immlib.Debugger()

"""
Functions
"""

def usage():
    imm.Log("  ** No arguments specified ** ")
    imm.Log("  Usage : ")
    imm.Log("       blah blah")

    
"""
Main application
"""
def main(args):
    if not args:
        usage()
    else:
        imm.Log("Number of arguments : %d " % len(args))
        cnt=0
        while (cnt < len(args)):
            imm.Log(" Argument %d : %s" % (cnt+1,args[cnt]))
            if (args[cnt] == "world"):
                imm.Log("  You said %s !" % (args[cnt]),focus=1, highlight=1)
            cnt=cnt+1

Обновление log/table

По ходу разработки плагина вы можете заметить, что в процессе поиска в памяти(или выполнения какой-либо ресурсоемкой задачи) окно лога может обновляться не сразу. Immunity может немного подвисать, но когда все задачи будут завершены, то все отобразится в окне лога(или таблице, раз уж на то пошло).

Есть один способ решить эту проблему. После каждого вызова imm.Log() вы можете принудительно обновлять окно лога. Это можно сделать путем вставки такой конструкции в ваш код:

imm.updateLog()

Единственным(большим) неудобством является то, что эта процедура в то же время будет замедлять скорость выполнения «тяжелых» задач. Что выбирать: быстродействие или возможность наблюдать в реальном времени что происходит, решать только вас.

Вывод в таблицу

Вывод данных в Log позволяет отображать как структурированную, так и не структурированную информацию. Сначала вы должны определить таблицу (имена таблицы и колонок), затем вы можете использовать метод .add() для добавления данных в таблицу.

def main(args):
    if not args:
        usage()
    else:
        #create table
        table=imm.createTable('Argument table',['Number','Argument'])
        imm.Log("Number of arguments : %d " % len(args))
        cnt=0
        while (cnt < len(args)):
            table.add(0,["%d"%(cnt+1),"%s"%(args[cnt])])
            cnt=cnt+1

Вывод в файл

Вывод данных в лог или таблицу работает хорошо, если объем выводимых данных не превышает доступный буфер окна Log. Если же переполнение имеет место быть, то можно сохранять лог в файл, помимо вывода в окно.

Для этого в Immunity нет специальных средств — все делается на чистом питоне. Стандартный путь сохранения файлов — корневая папка Immunity.
Код довольно прост:

filename="myfile.txt"
FILE=open(filename,"a")  #this will append to the file
FILE.write("Blah blah" + "\n")    
FILE.close()

Что я обычно делаю: сначала очищаю содержимое файл, затем добавляю данные в файл использую функцию tofile()

Очистить содержимое файла можно как-нибудь так:

def resetfile(file1):
    FILE=open(file1,"w")
    FILE.write("")
    FILE.close()
    return ""   

В начале функции я очищаю файл, а затем я использую tofile() для вывода данных в файл

def tofile(info,filename):
    info=info.replace('\n',' - ')
    FILE=open(filename,"a")
    FILE.write(info+"\n")    
    FILE.close()    
    return ""

Работа с адресами

Когда вам нужно отобразить адрес памяти или использовать его в функции, вам нужно определить в каком формате адрес должен быть представлен: в строковом(если вам нужно отобразить адрес) или в целочисленном(если адрес используется в методе)

Когда Immunity возвращает адрес памяти или ожидает его получения, он будет целочисленным. Если вы хотите отобразить адрес на экране, вывести его в лог и.т.д., вам следует использовать строковое значение для приведения адреса в удобночитаемый формат:

def usage():
   imm.Log("  ** No arguments specified ** ")
   imm.Log("  Usage : ")
   imm.Log("       blah blah")

def tohex(intAddress):
   return "%08X" % intAddress
    
"""
Main application
"""
def main(args):
   if not args:
      usage()
   else:
      myAddress=1234567  #integer address
      imm.Log(" Integer         : %d " % myAddress,address=myAddress)
      imm.Log(" Readable hex    : 0x%08X" % myAddress,address=myAddress)
      hexAddress = tohex(myAddress)
      imm.Log(" Readable string : 0x%s" % hexAddress,address=myAddress)
      imm.Log(" Back to integer : %d" % int(hexAddress,16),address=int(hexAddress,16))

Это всё, что нам нужно знать на данный момент — теперь мы можем начать писать код, который будет делать что-то полезное и выводить информацию в лог, таблицу или файл…

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

Одна из основных возможностей, которой вы возможно захотите воспользоваться будет поиск в памяти, например:

  • поиск адресов переходов (jump, call, push+ret, pop pop ret, и.т.д)
  • сравнение байтов в памяти с байтами в файле
  • и.т.д.

В некоторых случаях вы также захотите преобразовать опкоды в инструкции и наоборот.

Рассмотрим это шаг за шагом. Допустим, вы хотите найти все «jmp esp» инструкции, доступные в памяти. Первое, нам нужен процесс, к которому приаттачен отладчик. Нет процесса — нет результата.

Пример 1: поиск “jmp esp”

def main(args):
   imm.Log("Started search for jmp esp...")
   imm.updateLog()
   searchFor="jmp esp"
   results=imm.Search( imm.Assemble (searchFor) )
   for result in results:
      imm.Log("Found %s at 0x%08x " % (searchFor, result), address = result)

Уже неплохо, не так ли?

Пример 2: если вы хотите найти последовательность инструкций(например, “push esp + ret”), то инструкции должны быть разделены «\n»:

def main(args):
   imm.Log("Started search for push esp / ret...")
   imm.updateLog()
   searchFor="push esp\nret"
   results=imm.Search( imm.Assemble (searchFor) )
   for result in results:
      imm.Log("Found %s at 0x%08x " % (searchFor.replace('\n',' - '), result), address = result)

Пример 3:

Если вы хотите использовать опкод напрямую, то:

  • поиск будет немного отличаться (не нужно сначала преобразовывать в инструкцию)
  • вы можете дизассемблировать инструкцию по найденному адресу, используя функции Disasm() + getDisasm()
def main(args):
   imm.Log("Started search for mov ebp,esp ")
   imm.updateLog()
   searchFor="\x8b\xec" #mov ebp,esp / ret
   results=imm.Search( searchFor )
   for result in results:
      opc = imm.Disasm( result )
      opstring=opc.getDisasm()   
      imm.Log("Found %s at 0x%08x " % (opstring, result), address = result)

Пометка: когда вы производите поиск в памяти, он затрагивает память всего процесса(загруженные модули и пространство вне модулей, но более количества памяти, используемого процессом). Как говорилось ранее, поиск в памяти может загружать процессор вплоть до 100%.

Создание своего ассемблера…

…это довольно просто:

__VERSION__ = '1.0'
import immlib
import getopt
import immutils
from immutils import *
imm = immlib.Debugger()
import re

    
"""
Main application
"""
def main(args):
   if (args[0]=="assemble"):
      if (len(args) < 2):
         imm.Log("  Usage : !plugin1 compare instructions")
         imm.Log("           separate multiple instructions with #")
      else:
         cnt=1
         cmdInput=""
         while (cnt < len(args)):
            cmdInput=cmdInput+args[cnt]+" "
            cnt=cnt+1
         cmdInput=cmdInput.replace("'","")
         cmdInput=cmdInput.replace('"','')
         splitter=re.compile('#')
         instructions=splitter.split(cmdInput)
         for instruct in instructions:
            try:
               assembled=imm.Assemble( instruct )
               strAssembled=""
               for assemOpc in assembled:
                  strAssembled =  strAssembled+hex(ord(assemOpc)).replace('0x', '\\x')
               imm.Log(" %s = %s" % (instruct,strAssembled))
            except:
               imm.Log("   Could not assemble %s " % instruct)
               continue

(я добавил “import re’” в начале скрипта, чтобы использовать разделитель (re.compile()))
(на самом деле странное решение, можно было обойтись и без этого(прим. Пер.))

Обход памяти

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

if (args[0]=="readmem"):
   if (len(args) > 1):
      imm.Log("Reading 8 bytes of memory at %s " % args[1])
      cnt=0
      memloc=int(args[1],16)
      while (cnt < 8):
         memchar = imm.readMemory(memloc+cnt,1)
         memchar2 = hex(ord(memchar)).replace('0x','')
         imm.Log("Byte %d : %s" % (cnt+1,memchar2))
         cnt=cnt+1

Метод readMemory() принимает два аргумента: позицию, откуда нужно начать чтение и количество байтов, которые нужно прочесть.

Регистры

Получить доступ к регистрам тоже довольно просто:

regs = imm.getRegs()
for reg in regs:
   imm.Log("Register %s : 0x%08X " % (reg,regs[reg]))

Цепочка SEH

def main(args):
   if (args[0]=="sehchain"):         
      thissehchain=imm.getSehChain()
      sehtable=imm.createTable('SEH Chain',['Address','Value'])
      for chainentry in thissehchain:
         sehtable.add(0,("0x%08x"%(chainentry[0]),("%08x"%(chainentry[1]))))

Параметры адресов и модулей

Когда ваш скрипт производит поиск в памяти, он возвращает адреса. Вот основные вещи, которые вы хотите знать об этих адресах:

  • принадлежит ли он модулю? Если да, то какому?
  • какой у модуля базовый адрес и размер?
  • скомпилирован ли модуль с использованием safeseh или нет?
  • защищен ли модуль aslr или нет?
  • какой уровень доступа в заданной области памяти?

Очень хорошие вопросы, и на все из них можно получить ответы с помощью скрипта.

Мы сделаем вид, что вы произвели поиск и «результат» - элемент в массиве резульатов поиска.

Посмотрим, принадлежит ли адрес модулю

module = imm.findModule(result)
if not module:
   module="none"
else:
   module=module[0].lower()

Базовый адрес и размер модуля

modbase=module.getBaseAddress()
modsize=module.getSize()
modtop=modbase+modsize   

Скомпилирован ли модуль с использованием safeseh?

(эта часть кода потребует импортировать модель «struct»)

module = imm.findModule(result)
#
mod=imm.getModule(module[0])
mzbase=mod.getBaseAddress()
peoffset=struct.unpack('<L',imm.readMemory(mzbase+0x3c,4))[0]
pebase=mzbase+peoffset
flags=struct.unpack('<H',imm.readMemory(pebase+0x5e,2))[0]
numberofentries=struct.unpack('<L',imm.readMemory(pebase+0x74,4))[0]
if numberofentries>10:
   sectionaddress,sectionsize=struct.unpack('<LL',imm.readMemory(pebase+0x78+8*10,8))
   sectionaddress+=mzbase
   data=struct.unpack('<L',imm.readMemory(sectionaddress,4))[0]
   condition=(sectionsize!=0) and ((sectionsize==0x40) or (sectionsize==data))
   if condition==False:
      imm.Log("Module %s is not safeseh protected" % module[0])
      continue

Защищен ли модуль aslr или нет?

module = imm.findModule(result)
mod=imm.getModule(module[0])
mzbase=mod.getBaseAddress()
peoffset=struct.unpack('<L',imm.readMemory(mzbase+0x3c,4))[0]
pebase=mzbase+peoffset
flags=struct.unpack('<H',imm.readMemory(pebase+0x5e,2))[0]
if (flags&0x0040)==0:
    imm.Log("Module %s is not aslr aware" % module[0])

Уровень доступа

page = imm.getMemoryPagebyAddress( result )
access = page.getAccess( human = True )
imm.Log("Access : %s" % access)

Всё в одном скрипте:

if (args[0]=="test"):
   imm.Log("Started search for mov ebp,esp ")
   imm.updateLog()
   searchFor="\x8b\xec" #mov ebp,esp / ret
   results=imm.Search( searchFor )
   for result in results:
      opc = imm.Disasm( result )
      opstring=opc.getDisasm()
      module = imm.findModule(result)
      if not module:
         module="none"
      else:
         page = imm.getMemoryPagebyAddress( result )
         access = page.getAccess( human = True )
         mod=imm.getModule(module[0])
         mzbase=mod.getBaseAddress()
         peoffset=struct.unpack('<L',imm.readMemory(mzbase+0x3c,4))[0]
         pebase=mzbase+peoffset
         flags=struct.unpack('<H',imm.readMemory(pebase+0x5e,2))[0]
         numberofentries=struct.unpack('<L',imm.readMemory(pebase+0x74,4))[0]
         if numberofentries>10:
            sectionaddress,sectionsize=struct.unpack('<LL',imm.readMemory(pebase+0x78+8*10,8))
            sectionaddress+=mzbase
            data=struct.unpack('<L',imm.readMemory(sectionaddress,4))[0]
            condition=(sectionsize!=0) and ((sectionsize==0x40) or (sectionsize==data))
            if condition==False:
              imm.Log("Module %s is not safeseh protected" % module[0],highlight=1)
              continue         
         if (flags&0x0040)==0:
            extrastring="not ASLR aware"
         else:
            extrastring="ASLR protected"
         imm.Log("Found %s at 0x%08x - [module %s] - access %s - ASLR : %s " % (opstring, result,module[0],access,extrastring), address = result)

Отлаживаемое приложение: имя и путь

Если вы хотите (динамически) отфильтровать имя загруженного экзешника (и получить путь к нему), используйте следующие методы:

name=imm.getDebuggedName()
imm.Log("Name : %s" % name)
me=imm.getModule(name)
path=me.getPath()
imm.Log("Path : %s" % path)

Мнение о переходе на Immunity Debugger v1.74 и выше

Immunity планируют исправить несоответствия некоторых методов\атрибутов (особенно регистр символов в именах методов\функций) в новых версиях отладчика и библиотек. В результате, вам придется исправить ваш PyCommand, чтобы заставить его работать в более новых весиях отладчика.

Вот обзор того, что вам возможно придется исправить:

Текущая v1.73

новые версии

.Log
.log

.Assemble
.assemble

.Disassemble
.disassemble

.Search
.search

.getMemoryPagebyAddress
.getMemoryPageByAddress

В заключении

Руководство является далеко не полным и есть еще много штук, которые вы можете сделать с помощью Immunity API. Я всего лишь хотел дать вам начальный толчок в создании ваших собственных плагинов.

Если вы создаете свои плагины, поделитесь ими с остальными — я уверен, другие люди отблагодарят вас за старания.

Примечания переводчика

Как любитель питона могу сказать, что код в данной статье стоит рассматривать скорее как “proof of concept”, потому что от питона в нем разве что отступы. Заметно, что автор разбирается в создании эксплоитов гораздо лучше, чем в питоне.

Помимо этого, статья могла несколько потерять актуальность за счет изменений в API, это предстоить проверить уже вам самостоятельно.

Обо всех опечатках и неточностях прошу сообщать в ЛС.

© Translated by dahaka from r0 Crew