R0 CREW

Gray Hat Python: Глава 12 - PyEmu (Перевод: Prosper-H)

Перейти к содержанию

Intro

PyEmu был за релизен на конференции BlackHat Глава 11). Класс PyDbgPyEmu позволяет использовать эмулятор во время динамического анализа, что дает возможность использовать реальные значения памяти и регистров в нутрии скриптов. Класс PEPyEmu является отдельной библиотекой статического анализа, которая не требует наличия IDA Pro для дизассемблирования. В этой главе мы рассмотрим использование IDAPyEmu и PEPyEmu, а исследование PyDbgPyEmu оставим читателю в качестве упражнения. Давайте установим PyEmu, а затем перейдем на рассмотрение базовой архитектуры эмулятора.

12.1 Установка PyEmu

Установка PyEmu довольна простая; просто загрузите zip-архив по следующей ссылке:

[INDENT]http://www.nostarch.com/ghpython.htm[/INDENT]

Как только загрузите файл – распакуйте его в “Gray Hat Python: Глава 2 - Отладчики и устройство отладчика (Перевод: M.Chumichev)”]Главы 2[/URL]. Они работают похожим образом и в эмуляторе PyEmu; всякий раз, при срабатывании исключения, будет вызван установленный обработчик исключений. В настоящий момент, PyEmu поддерживает только general protection fault (прим. пер. не знаю, как лучше перевести), что позволяет обрабатывать любой неправильный доступ к памяти в нутрии эмулятора. Для установки обработчика исключений, используется следующий прототип:

set_exception_handler( "https://forum.reverse4you.org/t/gray-hat-python-11-idapython-prosper-h/190"]главу по IDAPython[/URL]) для загрузки секций двоичного файла в эмулятор. Давайте продолжим писать наш скрипт <B>[I]addnum_function_call.py[/I]</B>:

<B>addnum_function_call.py</B>

```lang-auto
...
(#1): emu = IDAPyEmu()

# Load the binary's code segment
code_start = SegByName("https://forum.reverse4you.org/t/gray-hat-python-11-idapython-prosper-h/190"]Глава 11[/URL]). После запуска вы должны видеть следующий вывод, как показано в <B>Листинге 12-2</B>.

<B>Listing 12-2</B>: Output from our IDAPyEmu function emulator

```lang-auto
[*] 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.

Довольно просто! Можно видеть, что скрипт успешно перехватывает аргументы стека и извлекает значение регистра EAX (сумма двух аргументов). Попрактикуйтесь, загружайте различные двоичные файлы в IDA Pro, выбирайте случайную функцию и пытайтесь эмулировать ее вызов. Вы будете поражены тем, насколько мощной может быть эта техника, когда функция состоит из сотен или тысяч инструкций с множеством ветвлений, циклов и точек возвратов (return points). Использование этого метода в реверсинге функции может сохранить часы ручного исследования. Теперь давайте воспользуемся библиотекой PEPyEmu для распаковки сжатого двоичного файла.

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 по следующей ссылке:

[INDENT]http://upx.sourceforge.net[/INDENT]

После загрузки файла, распакуйте zip-архив на диск “C:”. Для работы с UPX следует использовать командную строку, потому что он поставляется без графической оболочки (GUI). В командной строке, перейдите в директорию, куда был установлен UPX (в моем случае это “C:\upx303w”) и введите следующую команду:

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>

Это создаст сжатую версию Windows калькулятора и сохранит его в корне диска “C:”. Флаг –o определяет имя файла, под которым должен быть сохранен упакованный исполняемый файл; в нашем случае мы сохраняем его как calc_upx.exe. Теперь у нас есть полностью упакованный файл, с которым можно проводить тесты в PyEmu, так что давайте приступим к кодингу!

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.

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

Можно видеть, что секция UPX1 содержит код. Это место, где упаковщик UPX создает основную процедуру распаковки. Упаковщик выполняет процедуру распаковки в этой секции, и когда распаковка закончена – передает (JMP) управление на реальный код двоичного файла, находящийся за пределами секции UPX1. Все, что мы должны сделать, это позволить эмулятору выполнить процедуру распаковки и обнаружить инструкцию JMP, которая получает EIP, выходящий за пределы секции UPX1, после чего мы должны оказаться в OEP.

Теперь, когда у нас есть двоичный файл, упакованный с помощью UPX, давайте воспользуемся PyEmu, чтобы распаковать и сдампить (dump) оригинальный бинарный файл на диск. На этот раз, мы будем использовать автономный модуль PEPyEmu, поэтому откройте новый Python файл, назовите его upx_unpacker.py и введите следующий код:

upx_unpacker.py

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 )

Вначале загружаем упакованный двоичный файл в PyEmu (#1). Затем устанавливаем library handlers (#2) на LoadLibraryA, GetProcAddress и VirtualProtect. Все эти функции будут вызваны во время процедуры распаковки, поэтому нам нужно перехватить и эмулировать их вызовы. Следующим шагом является обработка случая, когда процедура распаковки закончена и происходит прыжок на OEP. Обработка этого случая будет произведена с помощью установки mnemonic handler на инструкцию JMP (#3). В конце, говорим эмулятору, начать выполнение с точки входа (EP) исполняемого файла (#4). Теперь, давайте доработаем скрипт и добавим соответствующие обработчики:

upx_unpacker.py

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

Наш обработчик LoadLibrary (#1) захватывает имя DLL из стека перед использованием ctypes, чтобы сделать реальный вызов функции LoadLibraryA, которая экспортируется из kernel32.dll. После того, как был осуществлен реальный вызов, в регистр EAX устанавливается возвращаемое значение дескриптора, сбрасывается стек эмулятора и осуществляется выход из обработчика. Обработчик функции GetProcAddress (#2) работает похожим образом. Он из стека получает два параметра функции и делает реальный вызов GetProcAddress, которая экспортируется из kernel32.dll. Затем в регистре EAX возвращается адрес запрашиваемой процедуры. После чего сбрасывается стек эмулятора и осуществляется выход из обработчика. Обработчик функции VirtualProtect (#3) – возвращает значение TRUE, сбрасывает стек эмулятора и выходит из обработчика. Причина, по которой мы не делаем реальный вызов VirtualProtect заключается в том, что нам не нужно защищать страницы памяти; поэтому нам просто нужно, чтобы вызов VirtualProtect завершался успешно. Обработчик инструкции JMP (#4) делает простую проверку, чтобы понять выпрыгиваем ли мы из процедуры распаковки? Если да, то вызывает функцию dump_unpacked, чтобы сдампить распакованный двоичный файл на диск. После чего говорит эмулятору остановить выполнение, так как распаковка нашего файла подошла к своему концу.

Последним шагом будет добавление процедуры dump_unpacked; мы добавим ее после наших обработчиков.

upx_unpacker.py

...
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."

Тут мы просто дампим секции файла UPX0 и UPX1 и это является нашим последний шаг в распаковке запакованного исполняемого файла. После того, как файл сохранен на диск, мы можем загрузить его в IDA Pro и продолжить дальнейший анализ уже оригинального файла. Теперь давайте запустим наш скрипт распаковки из командной строки; вы должны видеть вывод похожий на тот, что показан в Листинге 12-4.

Listing 12-4: Command line usage of upx_unpacker.py

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:\>

Теперь у вас есть файл “C:\calc_clean.exe”, который содержит сырой код оригинального исполняемого файла calc.exe, который до этого был упакован. Теперь вы на пути к тому, чтобы начать использовать PyEmu для различного множества задач реверс-инженера!

© Translated by Prosper-H from r0 Crew

PS: Перевод местами кривоват, поэтому заранее извините (насколько сил хватило, так и перевел).

Спасибо.
Есть предложение, можно после полного перевода, выкатить куда то на паблик в pdf.:wink:

Будет время - выкатим =)

Книга (pdf) в вашем переводе очень понравилась, жалко опечаток много, явно нужна вторая вычитка.
Спасибо за работу!