Перейти к содержанию
Intro
PyEmu был за релизен на конференции BlackHat [1] в 2007 Коди Пирсом (Cody Pierce), одним из талантливых мемберов TippingPoint DVLabs. PyEmu является чистым Python эмулятором AI32, который позволяет разработчику использовать Python для эмуляции процессора. Использование эмулятора может быть очень полезным, при реверсе вредоносного программного обеспечения, когда вы не хотите запускать вредоносный код на реальной системе. Его использование также будет полезным и в ряде других задач реверс инженера. PyEmu имеет три метода включающих возможность эмуляции: IDAPyEmu, PyDbgPyEmu и PEPyEmu. Класс IDAPyEmu позволяет запускать эмуляцию задач внутри IDA Pro, используя IDAPython (см. Глава 11). Класс PyDbgPyEmu позволяет использовать эмулятор во время динамического анализа, что дает возможность использовать реальные значения памяти и регистров в нутрии скриптов. Класс PEPyEmu является отдельной библиотекой статического анализа, которая не требует наличия IDA Pro для дизассемблирования. В этой главе мы рассмотрим использование IDAPyEmu и PEPyEmu, а исследование PyDbgPyEmu оставим читателю в качестве упражнения. Давайте установим PyEmu, а затем перейдем на рассмотрение базовой архитектуры эмулятора.
12.1 Установка PyEmu
Установка PyEmu довольна простая; просто загрузите zip-архив по следующей ссылке:
http://www.nostarch.com/ghpython.htmКак только загрузите файл – распакуйте его в "C:\PyEmu". Каждый раз, создавая скрипт PyEmu, нужно указывать путь к коду PyEmu. Делается это с помощью следующих двух Python строк:
Вот и все! Теперь давайте углубимся в архитектуру системы PyEmu, а затем перейдем к созданию простых скриптов.Code:sys.path.append("C:\PyEmu\") sys.path.append("C:\PyEmu\lib")
12.2 Обзор PyEmu
PyEmu разделяется на три основные системы: PyCPU, PyMemory и PyEmu. По большей части вы будете взаимодействовать только с родительским классом PyEmu, который затем взаимодействует с классами PyCPU и PyMemory, чтобы выполнить все низкоуровневые задачи эмуляции. Когда вы просите PyEmu выполнить инструкции, он вызывает PyCPU для их фактического исполнения. Затем PyCPU обращается (calls back) к PyEmu, чтобы запросить необходимую память от PyMemory, которая требуется для выполнения поставленной задачи. Когда инструкция закончила выполнение и нужно вернуть память – происходит обратная операция.
Мы кратко рассмотрим каждую из подсистем и их различные методы для лучшего понимания того, как PyEmu делает свою грязную работу.
12.2.1 PyCPU
Класс PyCPU это сердце и душа PyEmu, поскольку ведет себя так же, как физический процессор на компьютере который вы сейчас используете. Его работа заключается в выполнении инструкций во время эмуляции. Когда PyCPU нужно выполнить инструкцию, он получает инструкцию из текущего указателя инструкций (который определяется либо статически от IDA Pro/PEPyEmu, либо динамически от PyDbg). Получив инструкцию, он внутренне передает ее в pydasm, который декодирует их в опкоды и операнды. Возможность самостоятельного декодирования инструкций позволяет PyEmu спокойно работать в различных средах, которые он поддерживает.
В PyEmu для каждой инструкции программы есть соответствующая инструкция эмулятора. Например, если бы в PyCPU была передана инструкция CMP EAX,1, он бы вызвал функцию CMP() из класса PyCPU, после чего, получив необходимые значения, установил бы соответствующие флаги процессора, чтобы указать прошло ли сравнение успешно (прим. пер. т.е. EAX = = 1) или нет. Не стесняйтесь исследовать файл PyCPU.py, который содержит все поддерживаемые PyEmu инструкции. Коди Пирс пошел на многое, чтобы гарантировать, что код эмулятора читабелен и понятен; исследование PyCPU является отличным способом понять, как задачи процессора выполняются на низком уровне.
12.2.2 PyMemory
Класс PyMemory является средством для класса PyCPU позволяющего загружать и сохранять необходимые данные во время выполнения инструкций. Он также отвечает за отображение секций кода и данных (выполняемой программы) таким образом, чтобы к ним можно было обращаться из эмулятора. Теперь, когда у вас есть некоторые знания о двух важных подсистемах PyEmu, давайте посмотрим на основной класс PyEmu и некоторые из поддерживаемых им методов.
12.2.3 PyEmu
Родительский класс PyEmu является основной движущей силой для всего процесса эмуляции. PyEmu был разработан для того, чтобы быть очень легким и гибким, и чтобы можно было быстро разрабатывать мощные скрипты без необходимости управления какими-либо низкоуровневыми процедурами. Это достигается с помощью воздействия вспомогательных функций, которые позволяют: контролировать выполнение потока, контролировать изменения значений регистров, изменять содержимое памяти и многое другое. Давайте углубимся в рассмотрение некоторых из этих вспомогательных функций до разработки нашего первого скрипта для PyEmu.
12.2.4 Execution
В PyEmu выполнение контролируется с помощью одной функции, с соответствующим название execute(). Она имеет следующий прототип:
Метод execute() принимает три необязательных параметра и если аргументы не будут подставлены, то он начнет выполнение с текущего адреса PyEmu. Им может быть: либо значение из EIP, во время динамического выполнения в PyDbg; либо точкой входа исполняемой программы, в случае с PEPyEmu; либо эффективным адресом курсора, установленным вами в нутрии IDA Pro. Параметр steps определяет, сколько инструкций должен выполнить PyEmu, прежде чем остановиться. При использовании параметра start, вы устанавливаете начало выполнения инструкций. Он также может быть использован с параметром steps или параметром end, чтобы определить, когда PyEmu должен остановить выполнение.Code:execute( steps=1, start=0x0, end=0x0 )
Чрезвычайно важно иметь возможность устанавливать или получать значения регистров и памяти во время выполнения скриптов эмуляции. PyEmu разбивает модифицирующиеся данные на четыре отдельных категории: memory, stack variables, stack arguments и registers. Чтобы установить или получить значение памяти (memory) используйте функции set_memory() и get_memory(), которые имеют следующие прототипы:Code:12.2.5 Memory and Register Modifiers
Функция get_memory() принимает два параметра: параметр address, говорит PyEmu какой адрес памяти запросить, а параметр size определяет размер полученных данных. Функция set_memory() принимает address памяти для записи, параметр value, определяет значение, которое будет записано (по указанному адресу), а необязательный параметр size говорит PyEmu размер записываемых данных.Code:get_memory( address, size ) set_memory( address, value, size=0 )
Две стековых категории (stack variables, stack arguments) ведут себя аналогично и используются для изменения функций аргументов и локальных переменных в кадре стека. Они имеют следующие прототипы:
Для функции set_stack_argument(), в параметре offset указывается смещение от переменной ESP, а в параметр value используется для установки аргумента стека (stack argument). При желании для аргумента стека можно указать имя в параметре name. При использовании функции get_stack_argument(), для получения значения аргумента стека, можно использовать либо параметр offset, либо параметр name, если ему до этого было установлено пользовательское имя. Пример такого использования показан ниже:Code:set_stack_argument( offset, value, name="" ) get_stack_argument( offset=0x0, name="" ) set_stack_variable( offset, value, name="" ) get_stack_variable( offset=0x0, name="" )
Функции set_stack_variable() и get_stack_variable() работают тем же самым образом, за исключением того, что для установки значения локальным переменным в области функций, в параметре offset, указывается смещение относительно регистра EBP (при наличии).Code:set_stack_argument( 0x8, 0x12345678, name="arg_0" ) get_stack_argument( 0x8 ) get_stack_argument( "arg_0" )
12.2.6 Handlers
Обработчики обеспечивают очень гибкий и мощный механизм обратного вызова (callback), позволяющего реверсеру наблюдать, модифицировать или изменять определенные моменты исполнения. PyEmu предоставляет восемь основных обработчиков: register handlers, library handlers, exception handlers, instruction handlers, opcode handlers, memory handlers, high-level memory handlers и program counter handler. Давайте быстро рассмотрим каждый из них, а затем перейдем к реальным случаям использования.
12.2.6.1 Register Handlers
Register handlers используются для того, чтобы наблюдать за изменениями в определенном регистре. Каждый раз, когда выбранный регистр изменится, будет вызван ваш обработчик. Для установки register handler используется следующий прототип:
После того как вы установили обработчик, нужно определить функцию обработчик, используя следующий прототип:Code:set_register_handler( register, register_handler_function ) set_register_handler( "eax ", eax_register_handler )
Когда вызывается обработчик процедуры, в первом параметре emu передается текущий экземпляр PyEmu, в следующем параметре register и последующем value, передается регистр, за которым вы наблюдаете, и его значение соответственно. В параметре type передается строка, указывающая на чтение (read) или запись (write) в регистр (прим. пре. т.е. дает понять, «пишут» в регистр или «читают» из него). Это невероятно мощный способ, позволяющий наблюдать за изменениями регистра в течении долгого времени, и помимо этого, позволяет изменять регистры в нутрии обработчика процедуры, если потребуется.Code:def register_handler_function( emu, register, value, type ):
12.2.6.2 Library Handlers
Library handlers позволяют PyEmu перехватывать любые функций, во внешних модулях (libraries), до их непосредственного вызова. Это позволяет эмулятору изменять как вызовы функций, так и возвращаемый ими результат. Для установки library handler, используется следующий прототип:
После того как library handler установлен, нужно определить функцию обработчик, используя следующий прототип:Code:set_library_handler( function, library_handler_function ) set_library_handler( "CreateProcessA", create_process_handler )
В первом параметре передается текущий экземпляр PyEmu. В параметре library передается имя вызванной функции. В параметре address передается адрес в памяти, где располагается импортируемая функция.Code:def library_handler_function( emu, library, address ):
12.2.6.3 Exception Handlers
Вы должны быть хорошо знакомы с обработчиками исключений (exception handlers) из Главы 2. Они работают похожим образом и в эмуляторе PyEmu; всякий раз, при срабатывании исключения, будет вызван установленный обработчик исключений. В настоящий момент, PyEmu поддерживает только general protection fault (прим. пер. не знаю, как лучше перевести), что позволяет обрабатывать любой неправильный доступ к памяти в нутрии эмулятора. Для установки обработчика исключений, используется следующий прототип:
Процедура обработчика, для обработки любых поступивших исключений, должна следовать следующему прототипу:Code:set_exception_handler( "GP", gp_exception_handler )
Как обычно, в первый параметр передается текущий экземпляр PyEmu, в параметр exeption передается код сгенерированного исключения, а в параметр address передается адрес, где произошло исключение.Code:def gp_exception_handler( emu, exception, address ):
12.2.6.4 Instruction Handlers
Instruction handlers являются очень мощным средством для перехвата определенных инструкций после того как те были выполнены. Это может пригодиться в самых разных случаях. Например, в своей статье, на конференции BlackHat, Коди Пирс говорит, что вы можете установить обработчик для инструкции CMP, чтобы следить за выбором ветвлений, производимым в зависимости от результата выполнения инструкции CMP. Для установки instruction handler, используется следующий прототип:
Функция обработчик должна соблюдать следующий прототип:Code:set_instruction_handler( instruction, instruction_handler ) set_instruction_handler( "cmp", cmp_instruction_handler )
В первом параметре передается экземпляр PyEmu, в параметре instruction передается выполненная инструкция, а в оставшихся трех параметрах передаются значения всех возможных операндов, которые были использованы.Code:def cmp_instruction_handler( emu, instruction, op1, op2, op3 ):
12.2.6.5 Opcode Handlers
Opcode handlers очень похожи на instruction handlers в том, что они вызываются при выполнении определенного опкода (opcode). Это дает вам более высокий уровень контроля, поскольку, каждая инструкция может иметь несколько опкодов, в зависимости от операндов, которые она использует. Например, инструкции “PUSH EAX” принадлежит опкод 0x50, в то время как инструкции “PUSH 0x70” принадлежит опкод 0x6A, а полный опкод операции будет 0x6A70. Для установки opcode handler, используется следующий прототип:
В первый параметр opcode нужно передать код операции, который нужно перехватить, а во второй параметр opcode_handler передать функцию обработчик. Вы не ограничены однобайтовыми опкодами: если опкод состоит из нескольких байт, можете передать весь набор, как показано во втором примере.Code:set_opcode_handler( opcode, opcode_handler ) set_opcode_handler( 0x50, my_push_eax_handler ) set_opcode_handler( 0x6A70, my_push_70_handler )
Функция обработчик должна соблюдать следующий прототип:
В первом параметре передается текущий экземпляр PyEmu, в параметре opcode передается выполненный опкод, а в оставшихся трех параметрах передаются значения всех возможных операндов, которые были использованы.Code:def opcode_handler( emu, opcode, op1, op2, op3 ):
12.2.6.6 Memory Handlers
Memory handlers могут использоваться, чтобы отследить доступ к определенным данным по конкретному адресу в памяти. Это может быть очень важно при отслеживании глобальной переменной или интересной части данных в буфере, и, наблюдая за тем, как это значение меняется с течением времени. Для установки memory handler, используется следующий прототип:
В первый параметр address нужно передать адрес в памяти, за которой вы хотите наблюдать, а во второй параметр memory_handler передать функцию обработчик.Code:set_memory_handler( address, memory_handler ) set_memory_handler( 0x12345678, my_memory_handler )
Функция обработчик должна соблюдать следующий прототип:
В первом параметре передается текущий экземпляр PyEmu, в параметре address передается адрес в памяти, по которому происходит доступ, в параметре value передаются данные, которые будут считаны или записаны, в параметре size передается размер данных, которые будут считаны или записаны, а в параметре type передается строковое значение, которое указывает на чтение или запись.Code:def memory_handler( emu, address, value, size, type ):
12.2.6.7 High-Level Memory Handlers
High-level memory handlers позволяют перехватить доступ к памяти вне определенного адреса. При установке high-level memory handler, можно контролировать все операции чтения/записи к любой памяти, стеку или куче, тем самым позволяя глобально контролировать доступ к памяти всех типов. Для установки различных high-level memory handlers, используются следующие прототипы:
Для всех этих обработчиков нужно просто передать функцию обработчик, которая будет вызываться в момент наступления одного из указанных событий доступа к памяти.Code:set_memory_write_handler( memory_write_handler ) set_memory_read_handler( memory_read_handler ) set_memory_access_handler( memory_access_handler ) set_stack_write_handler( stack_write_handler ) set_stack_read_handler( stack_read_handler ) set_stack_access_handler( stack_access_handler ) set_heap_write_handler( heap_write_handler ) set_heap_read_handler( heap_read_handler ) set_heap_access_handler( heap_access_handler )
Функция обработчик должна соблюдать следующий из прототипов:
Функции memory_write_handler и memore_read_handler просто принимают текущий экземпляр PyEmu и адрес, где происходит чтение или запись. Обработчик доступа (access handler) имеет немного отличающийся прототип, потому что получает третий параметр, который указывает на тип доступа к памяти. Параметр type это просто строка, указывающая на чтение (read) или запись (write).Code:def memory_write_handler( emu, address ): def memory_read_handler( emu, address ): def memory_access_handler( emu, address, type ):
12.2.6.8 Program Counter Handler
Program counter handler позволяет вызвать обработчик, когда выполнение достигает определенного адреса в эмуляторе. Так же, как и другие обработчики, он позволяет перехватывать определенные полезные места во время выполнения эмулятора. Для установки program counter handler, используется следующий прототип:
В параметре address нужно передать адрес места, где должна быть вызвана функция обратного вызова (callback), а в параметре pc_handler передать функцию, которая будет вызвана, в момент, когда переданный адрес достигает эмулятор, во время своего выполнения.Code:set_pc_handler( address, pc_handler ) set_pc_handler( 0x12345678, 12345678_pc_handler )
Функция обработчик должна соблюдать следующий прототип:
Как обычно, в первом параметре передается текущий экземпляр PyEmu, а в параметр address передается адрес, где произошел перехват.Code:def pc_handler( emu, address ):
Теперь, когда мы рассмотрели основы использования эмулятора PyEmu и некоторые его методы, давайте перейдем к использованию эмулятора в боевых условиях реверс-инженера. Для начала мы будем использовать IDAPyEmu, чтобы эмулировать простой вызов функции в двоичном файле, который мы загрузим в IDA Pro. Во втором упражнении мы будем использовать PEPyEmu для того, чтобы распаковать двоичный файл, который был упакован с помощью UPX.
12.3 IDAPyEmu
В нашем первом примере нужно загрузить двоичный файл в IDA Pro и используя PyEmu эмулировать вызов функции. Двоичный файл является простым приложением на C++, которое было названо addnum.exe. Оно доступно, вместе с остальными файлами для этой книги, по следующей ссылке:
http://www.nostarch.com/ghpython.htmЭтот двоичный файл просто принимает два числа в качестве параметров командной строки, затем складывает их между собой и выводит результат сложения на экран. Давайте быстро просмотрим исходный код, прежде чем смотреть на его дизассемблированное представление.
addnum.cpp
Это простая программа принимает два аргумента командной строки, преобразует их в целые числа (#1), а затем вызывает функцию add_number (#2), чтобы сложить их вместе. Мы будем использовать функцию add_number в качестве нашей цели для эмуляции, потому что ее довольно просто понять и проверить результат. Это будет хорошей отправной точкой для изучения того, как эффективно использовать систему PyEmu.Code:#include <stdlib.h> #include <stdio.h> #include <windows.h> int add_number( int num1, int num2 ) { int sum; sum = num1 + num2; return sum; } int main(int argc, char* argv[]) { int num1, num2; int return_value; if( argc < 2 ) { printf("You need to enter two numbers to add.\n"); printf("addnum.exe num1 num2\n"); return 0; } (#1): num1 = atoi(argv[1]); num2 = atoi(argv[2]); (#2): return_value = add_number( num1, num2 ); printf("Sum of %d + %d = %d",num1, num2, return_value ); return 0; }
Теперь, прежде чем погрузиться в код PyEmu, давайте посмотрим на дизассемблерное представление функции add_number. В Листинге 12-1 показан ее ассемблерный код:
Listing 12-1: Assembly code for the add_number function
Мы видим, как исходный код C++ транслируется в ассемблерный код после того, как он был скомпилирован. Мы будем использовать PyEmu, чтобы установить две переменные стека arg_0 и arg_4 в любое целое число, которое мы выберем. Затем перехватим значение в регистре EAX, когда функция будет выполнять инструкцию RETN. Регистр EAX будет содержать сумм двух чисел, которые мы передали в функцию. Хотя это и упрощенный вызов функции, он предоставляет отличную отправную точку для последующих эмуляций более сложных функций и перехвата их возвращаемых значений.Code:var_4= dword ptr -4 # sum variable arg_0= dword ptr 8 # int num1 arg_4= dword ptr 0Ch # int num2 push ebp mov ebp, esp push ecx mov eax, [ebp+arg_0] add eax, [ebp+arg_4] mov [ebp+var_4], eax mov eax, [ebp+var_4] mov esp, ebp pop ebp retn
12.3.1 Function Emulation
Первый шаг, при создании нового сценария PyEmu, убедиться в том, что path к PyEmu установлен правильно. Откройте новый Python-скрипт, назовите его addnum_function_call.py и введите следующий код.
addnum_function_call.py
Теперь, когда path правильно установлен, мы можем начать писать скрипт. Сначала нам нужно сопоставить секции кода и данных с двоичным файлом, таким образом, чтобы эмулятор имел реальный код для выполнения. Поскольку нами используется IDAPython, мы будем использовать похожие функции (см. предыдущую главу по IDAPython) для загрузки секций двоичного файла в эмулятор. Давайте продолжим писать наш скрипт addnum_function_call.py:Code:import sys sys.path.append("C:\\PyEmu") sys.path.append("C:\\PyEmu\\lib") from PyEmu import *
addnum_function_call.py
Вначале присваиваем значение объекта IDAPyEmu (#1), который необходим для использования в любом из методов эмулятора. Затем загружаем секции кода (#2) и данных (#3) из двоичного файла в память PyEmu. Мы используем функцию IDAPython SegByName(), чтобы найти начало секций, а функцию SegEnd() для определения конца секций. Затем просто перебираем секции по байтово, чтобы сохранить их в памяти PyEmu. Теперь, когда у нас имеются секции кода и данных, загруженные в память, нам нужно установить: параметры стека для вызова функции; обработчик инструкции (instruction handler), который будет вызван, когда будет выполнена инструкция RETN; и начать выполнение. Добавьте следующий код в ваш скрипт:Code:... (#1): emu = IDAPyEmu() # Load the binary's code segment code_start = SegByName(".text") code_end = SegEnd( code_start ) (#2): while code_start <= code_end: emu.set_memory( code_start, GetOriginalByte(code_start), size=1 ) code_start += 1 print "[*] Finished loading code section into memory." # Load the binary's data segment data_start = SegByName(".data") data_end = SegEnd( data_start ) (#3): while data_start <= data_end: emu.set_memory( data_start, GetOriginalByte(data_start), size=1 ) data_start += 1 print "[*] Finished loading data section into memory."
addnum_function_call.py
Сначала устанавливаем EIP в начало функции, которая размещена по 0x00401000 (#1); это место, откуда PyEmu начнет свое выполнение инструкций. Затем устанавливаем mnemonic или instruction handler, который будет вызван, когда функция выполнит инструкцию RETN (#2). Третьим шагом является установка параметров стека (#3) для вызова функции. Эти два числа будут сложены между собой; в нашем случае мы будем использовать 0x00000001 и 0x00000002. Затем просим PyEmu выполнить все 10 инструкций (#4), содержащиеся в этой функции. На последнем шаге напишем обработчик инструкции RETN, таким образом, заключительный вид скрипта должен выглядеть следующим образом:Code:... # Set EIP to start executing at the function head (#1): emu.set_register("EIP", 0x00401000) # Set up the ret handler (#2): emu.set_mnemonic_handler("ret", ret_handler) # Set the function parameters for the call (#3): emu.set_stack_argument(0x8, 0x00000001, name="arg_0") emu.set_stack_argument(0xc, 0x00000002, name="arg_4") # There are 10 instructions in this function (#4): emu.execute( steps = 10 ) print "[*] Finished function emulation run."
addnum_function_call.py
Code:import sys sys.path.append("C:\\PyEmu") sys.path.append("C:\\PyEmu\\lib") from PyEmu import * def ret_handler(emu, address): (#1): num1 = emu.get_stack_argument("arg_0") num2 = emu.get_stack_argument("arg_4") sum = emu.get_register("EAX") print "[*] Function took: %d, %d and the result is %d." % (num1, num2, sum) return True emu = IDAPyEmu() # Load the binary's code segment code_start = SegByName(".text") code_end = SegEnd( code_start ) while code_start <= code_end: emu.set_memory( code_start, GetOriginalByte(code_start), size=1 ) code_start += 1 print "[*] Finished loading code section into memory." # Load the binary's data segment data_start = SegByName(".data") data_end = SegEnd( data_start ) while data_start <= data_end: emu.set_memory( data_start, GetOriginalByte(data_start), size=1 ) data_start += 1 print "[*] Finished loading data section into memory." # Set EIP to start executing at the function head emu.set_register("EIP", 0x00401000) # Set up the ret handler emu.set_mnemonic_handler("ret", ret_handler) # Set the function parameters for the call emu.set_stack_argument(0x8, 0x00000001, name="arg_0") emu.set_stack_argument(0xc, 0x00000002, name="arg_4") # There are 10 instructions in this function emu.execute( steps = 10 ) print "[*] Finished function emulation run."
Обработчик инструкции RET (#1) просто извлекает аргументы стека и значение регистра EAX, а затем выводит результат вызова функции. Загрузите двоичный файл addnum.exe в IDA Pro и запустите скрипт PyEmu, также как вы запускали обычный файл IDAPython (см. Глава 11). После запуска вы должны видеть следующий вывод, как показано в Листинге 12-2.
Listing 12-2: Output from our IDAPyEmu function emulator
Довольно просто! Можно видеть, что скрипт успешно перехватывает аргументы стека и извлекает значение регистра EAX (сумма двух аргументов). Попрактикуйтесь, загружайте различные двоичные файлы в IDA Pro, выбирайте случайную функцию и пытайтесь эмулировать ее вызов. Вы будете поражены тем, насколько мощной может быть эта техника, когда функция состоит из сотен или тысяч инструкций с множеством ветвлений, циклов и точек возвратов (return points). Использование этого метода в реверсинге функции может сохранить часы ручного исследования. Теперь давайте воспользуемся библиотекой PEPyEmu для распаковки сжатого двоичного файла.Code:[*] Finished loading code section into memory.[*] Finished loading data section into memory.[*] Function took 1, 2 and the result is 3.[*] Finished function emulation run.
12.3.2 PEPyEmu
Класс PEPyEmu предоставляет реверсеру возможность, использовать PyEmu в средах статического анализа без использования IDA Pro. Он берет двоичный файл, отображает необходимые секции в памяти и затем использует pydasm для декодирования инструкций. Мы будем использовать PEPyEmu в реальной задаче реверсера, в которой будем брать упакованный двоичный файл, который будем запускать в эмуляторе, чтобы сдампить после того, как он будет распакован. Упаковщиком, который мы планируем использовать, будет Ultimate Packer for Executables (UPX) [2]. Это Open Source упаковщик, который используется множеством вариантов вредоносного ПО, в попытках сохранить размер исполняемого файла маленьким и затруднить статический анализ. В начале, давайте разберемся, что такое упаковщик и как он работает. Затем мы упакуем двоичный файл с помощью UPX. Нашим последним шагом будет использование специального PyEmu скрипта, который Коди Пирсон предоставил для распаковки исполняемых файлов и сброса (dump) результирующего двоичного файла на диск. После того, как файл распакован и сброшен (dumped) на диск, можно применять обычные методы статического анализа.
12.3.3 Executable Packers
Упакощики или компрессоры исполняемых файлов существуют уже довольно давно. Первоначально они использовались, чтобы уменьшить размер исполняемого файла, таким образом, чтобы тот смог поместиться на дискету 1.44 MB. C тех пор они выросли и стали использоваться авторами зловредного программного обеспечения для запутывания (обфускации, obfuscation) кода зловредов. Типичный упаковщик сжимает сегменты кода и данных в целевом двоичном файле и заменяет точку входа (Entry Point, EP) декомпрессора. Когда двоичный файл начинает свое выполнение, запускается декомпрессор, который распаковывает оригинальный двоичный файл в память, и затем передает управление на оригинальную точку входу (Original Entry Point, OEP). После того, как OEP достигнуто, двоичный файл начинает свое нормальное выполнение. Когда реверсер сталкивается с упакованным файлом, он должен сначала избавиться от упаковщика для того, чтобы спокойно анализировать истинный двоичный файл содержащийся в нем. Обычно можно использовать отладчик для выполнения подобного рода задач, но авторы вредоносов, в последние годы, стали более бдительными и встраивают анти-отладочные методы, внутри упаковщиков, таким образом, чтобы использование отладчика против упакованной программы стало очень сложным. Это то место, где использование эмулятора может быть выгодным, поскольку никакой отладчик не присоединен к исполняемой программе; мы просто запускаем код внутри эмулятора и ждем пока процедура распаковки будет окончена. После того, как упаковщик закончил распаковку оригинального файла, нужно сдампить распакованный бинарный файл на диск таким образом, чтобы мы могли загрузить его либо в отладчик, либо в инструмент статического анализа, например, IDA Pro.
Мы будем использовать UPX для сжатия файла calc.exe, который поставляется со всеми версиями Windows. Затем мы будем использовать скрипт PyEmu, чтобы распаковать исполняемый файл и сдампить его на диск. Этот прием может использоваться и для других упаковщиков. Его рассмотрение будет служить отличной отправной точкой для разработки более сложных скриптов, позволяющих противостоять различным упаковщикам, обнаруженным в дикой природе (In the Wild, ITW).
12.3.4 UPX Packer
UPX – это безплатный упаковщик c открытым исходным кодом, который работает на Linux, Windows и других операционных системах. Он предлагает различные уровни сжатия и множество дополнительных опций. Мы будем применять только основное сжатие, но вы не стесняйтесь исследовать все возможные опции, которые поддерживает UPX.
Для начала, загрузите UPX по следующей ссылке:
http://upx.sourceforge.netПосле загрузки файла, распакуйте zip-архив на диск "C:\". Для работы с UPX следует использовать командную строку, потому что он поставляется без графической оболочки (GUI). В командной строке, перейдите в директорию, куда был установлен UPX (в моем случае это "C:\upx303w\") и введите следующую команду:
Это создаст сжатую версию Windows калькулятора и сохранит его в корне диска "C:\". Флаг –o определяет имя файла, под которым должен быть сохранен упакованный исполняемый файл; в нашем случае мы сохраняем его как calc_upx.exe. Теперь у нас есть полностью упакованный файл, с которым можно проводить тесты в PyEmu, так что давайте приступим к кодингу!Code:C:\upx303w>upx -o c:\calc_upx.exe C:\Windows\system32\calc.exe Ultimate Packer for eXecutables Copyright (C) 1996 - 2008 UPX 3.03w Markus Oberhumer, Laszlo Molnar & John Reiser Apr 27th 2008 File size Ratio Format Name -------------------- -------- ----------- ------------ 114688 -> 56832 49.55% win32/pe calc_upx.exe Packed 1 file. C:\upx303w>
12.3.5 Unpacking UPX with PEPyEmu
Упаковщик UPX использует довольно простой метод сжатия исполняемых файлов: он добавляет две секции (UPX0 и UPX1) к двоичному файлу и изменяет точку входа (EP) так, чтобы она указывала на процедуру распаковки. Если вы загрузите упакованный файл в Immunity Debugger и исследуете расположение памяти (ALT+M), то вы увидите, что карта памяти в исполняемом файле аналогична той, что показана в Листинге 12-3:
Listing 12-3: Memory layout of a UPX compressed executable.
Можно видеть, что секция UPX1 содержит код. Это место, где упаковщик UPX создает основную процедуру распаковки. Упаковщик выполняет процедуру распаковки в этой секции, и когда распаковка закончена – передает (JMP) управление на реальный код двоичного файла, находящийся за пределами секции UPX1. Все, что мы должны сделать, это позволить эмулятору выполнить процедуру распаковки и обнаружить инструкцию JMP, которая получает EIP, выходящий за пределы секции UPX1, после чего мы должны оказаться в OEP.Code:Address Size Owner Section Contains Access Initial Access 00100000 00001000 calc_upx PE Header R RWE 01001000 00019000 calc_upx UPX0 RWE RWE 0101A000 00007000 calc_upx UPX1 code RWE RWE 01021000 00007000 calc_upx .rsrc data,imports RW RWE Resources
Теперь, когда у нас есть двоичный файл, упакованный с помощью UPX, давайте воспользуемся PyEmu, чтобы распаковать и сдампить (dump) оригинальный бинарный файл на диск. На этот раз, мы будем использовать автономный модуль PEPyEmu, поэтому откройте новый Python файл, назовите его upx_unpacker.py и введите следующий код:
upx_unpacker.py
Вначале загружаем упакованный двоичный файл в PyEmu (#1). Затем устанавливаем library handlers (#2) на LoadLibraryA, GetProcAddress и VirtualProtect. Все эти функции будут вызваны во время процедуры распаковки, поэтому нам нужно перехватить и эмулировать их вызовы. Следующим шагом является обработка случая, когда процедура распаковки закончена и происходит прыжок на OEP. Обработка этого случая будет произведена с помощью установки mnemonic handler на инструкцию JMP (#3). В конце, говорим эмулятору, начать выполнение с точки входа (EP) исполняемого файла (#4). Теперь, давайте доработаем скрипт и добавим соответствующие обработчики:Code:from ctypes import * # You must set your path to pyemu sys.path.append("C:\\PyEmu") sys.path.append("C:\\PyEmu\\lib") from PyEmu import PEPyEmu # Commandline arguments exename = sys.argv[1] outputfile = sys.argv[2] # Instantiate our emulator object emu = PEPyEmu() if exename: # Load the binary into PyEmu (#1): if not emu.load(exename): print "[!] Problem loading %s" % exename sys.exit(2) else: print "[!] Blank filename specified" sys.exit(3) (#2): # Set our library handlers emu.set_library_handler("LoadLibraryA", loadlibrary) emu.set_library_handler("GetProcAddress", getprocaddress) emu.set_library_handler("VirtualProtect", virtualprotect) # Set a breakpoint at the real entry point to dump binary (#3): emu.set_mnemonic_handler( "jmp", jmp_handler ) # Execute starting from the header entry point (#4): emu.execute( start=emu.entry_point )
upx_unpacker.py
Наш обработчик LoadLibrary (#1) захватывает имя DLL из стека перед использованием ctypes, чтобы сделать реальный вызов функции LoadLibraryA, которая экспортируется из kernel32.dll. После того, как был осуществлен реальный вызов, в регистр EAX устанавливается возвращаемое значение дескриптора, сбрасывается стек эмулятора и осуществляется выход из обработчика. Обработчик функции GetProcAddress (#2) работает похожим образом. Он из стека получает два параметра функции и делает реальный вызов GetProcAddress, которая экспортируется из kernel32.dll. Затем в регистре EAX возвращается адрес запрашиваемой процедуры. После чего сбрасывается стек эмулятора и осуществляется выход из обработчика. Обработчик функции VirtualProtect (#3) – возвращает значение TRUE, сбрасывает стек эмулятора и выходит из обработчика. Причина, по которой мы не делаем реальный вызов VirtualProtect заключается в том, что нам не нужно защищать страницы памяти; поэтому нам просто нужно, чтобы вызов VirtualProtect завершался успешно. Обработчик инструкции JMP (#4) делает простую проверку, чтобы понять выпрыгиваем ли мы из процедуры распаковки? Если да, то вызывает функцию dump_unpacked, чтобы сдампить распакованный двоичный файл на диск. После чего говорит эмулятору остановить выполнение, так как распаковка нашего файла подошла к своему концу.Code:from ctypes import * # You must set your path to pyemu sys.path.append("C:\\PyEmu") sys.path.append("C:\\PyEmu\\lib") from PyEmu import PEPyEmu ''' HMODULE WINAPI LoadLibrary( __in LPCTSTR lpFileName ); ''' (#1): def loadlibrary(name, address): # Retrieve the DLL name dllname = emu.get_memory_string(emu.get_memory(emu.get_register("ESP") + 4)) # Make a real call to LoadLibrary and return the handle dllhandle = windll.kernel32.LoadLibraryA(dllname) emu.set_register("EAX", dllhandle) # Reset the stack and return from the handler return_address = emu.get_memory(emu.get_register("ESP")) emu.set_register("ESP", emu.get_register("ESP") + 8) emu.set_register("EIP", return_address) return True ''' FARPROC WINAPI GetProcAddress( __in HMODULE hModule, __in LPCSTR lpProcName ); ''' (#2): def getprocaddress(name, address): # Get both arguments, which are a handle and the procedure name handle = emu.get_memory(emu.get_register("ESP") + 4) proc_name = emu.get_memory(emu.get_register("ESP") + 8) # lpProcName can be a name or ordinal, if top word is null it's an ordinal if (proc_name >> 16): procname = emu.get_memory_string(emu.get_memory(emu.get_register("ESP") + 8)) else: procname = arg2 # Add the procedure to the emulator emu.os.add_library(handle, procname) import_address = emu.os.get_library_address(procname) # Return the import address emu.set_register("EAX", import_address) # Reset the stack and return from our handler return_address = emu.get_memory(emu.get_register("ESP")) emu.set_register("ESP", emu.get_register("ESP") + 8) emu.set_register("EIP", return_address) return True ''' BOOL WINAPI VirtualProtect( __in LPVOID lpAddress, __in SIZE_T dwSize, __in DWORD flNewProtect, __out PDWORD lpflOldProtect ); ''' (#3): def virtualprotect(name, address): # Just return TRUE emu.set_register("EAX", 1) # Reset the stack and return from our handler return_address = emu.get_memory(emu.get_register("ESP")) emu.set_register("ESP", emu.get_register("ESP") + 16) emu.set_register("EIP", return_address) return True # When the unpacking routine is finished, handle the JMP to the OEP (#4): def jmp_handler(emu, mnemonic, eip, op1, op2, op3): # The UPX1 section if eip < emu.sections["UPX1"]["base"]: print "[*] We are jumping out of the unpacking routine." print "[*] OEP = 0x%08x" % eip # Dump the unpacked binary to disk dump_unpacked(emu) # We can stop emulating now emu.emulating = False return True
Последним шагом будет добавление процедуры dump_unpacked; мы добавим ее после наших обработчиков.
upx_unpacker.py
Тут мы просто дампим секции файла UPX0 и UPX1 и это является нашим последний шаг в распаковке запакованного исполняемого файла. После того, как файл сохранен на диск, мы можем загрузить его в IDA Pro и продолжить дальнейший анализ уже оригинального файла. Теперь давайте запустим наш скрипт распаковки из командной строки; вы должны видеть вывод похожий на тот, что показан в Листинге 12-4.Code:... def dump_unpacked(emu): global outputfile fh = open(outputfile, 'wb') print "[*] Dumping UPX0 Section" base = emu.sections["UPX0"]["base"] length = emu.sections["UPX0"]["vsize"] print "[*] Base: 0x%08x Vsize: %08x"% (base, length) for x in range(length): fh.write("%c" % emu.get_memory(base + x, 1)) print "[*] Dumping UPX1 Section" base = emu.sections["UPX1"]["base"] length = emu.sections["UPX1"]["vsize"] print "[*] Base: 0x%08x Vsize: %08x" % (base, length) for x in range(length): fh.write("%c" % emu.get_memory(base + x, 1)) print "[*] Finished."
Listing 12-4: Command line usage of upx_unpacker.py
Теперь у вас есть файл "C:\calc_clean.exe", который содержит сырой код оригинального исполняемого файла calc.exe, который до этого был упакован. Теперь вы на пути к тому, чтобы начать использовать PyEmu для различного множества задач реверс-инженера!Code:C:\>C:\Python25\python.exe upx_unpacker.py C:\calc_upx.exe calc_clean.exe[*] We are jumping out of the unpacking routine.[*] OEP = 0x01012475[*] Dumping UPX0 Section[*] Base: 0x01001000 Vsize: 00019000[*] Dumping UPX1 Section[*] Base: 0x0101a000 Vsize: 00007000[*] Finished. C:\>
© Translated by Prosper-H from r0 Crew
PS: Перевод местами кривоват, поэтому заранее извините (насколько сил хватило, так и перевел).



Reply With Quote
Thanks
