Monday, September 13, 2010

Создание патчей для исполняемых файлов

Задача: пропатчить конкретный исполняемый файл некоторым кодом. Для примера, код будет выполняться до оригинальной точки входа, и вызывать LoadLibrary для всех *.plgn файлов в текущей папке, для добавления в программу пользовательских плагинов.

Инструменты:
  • FASM (или любой ассемблер с возможностью создания .bin файлов - fasm, nasm, yasm, etc).
  • Python, версии 2.5-2.7, в т.ч. IronPython.
  • Библиотека pefile. (работает только с Python'ом 2.5-2.7).
  • Любое средство позволяющее убедиться что в файле есть место под патч и нужные импорты.
Патч дописывается в конец секции кода. Для полной интеграции с файлом используются его же импорты. Для парсинга и модификации файла используем библиотеку pefile. Код ассемблера встроим в скрипт питона, чтобы компилировать патч и патчить файл "нажатием одной кнопки".
""" EXE file path example,
    using pefile library by Ero Carrera (http://code.google.com/p/pefile/)
    and FASM compiler (http://flatassembler.net/)
"""

import os
import tempfile
import pefile

# file to patch
FILE_NAME = 'some.exe'

# we can't know patch size before we compile it, so we should define it before
MAX_PATCH_SIZE = 0x80

# path to FASM without backslash, like 'c:\fasm'
FASM = '%FASM%'

TEMP_FILENAME = tempfile.gettempdir() + '\\patch'

pe = pefile.PE(FILE_NAME)
text = pe.sections[0]
textFileSize = text.SizeOfRawData + (
    0x1FF - (text.SizeOfRawData + 0x1FF) & 0x1FF) # aligned by 0x200
patchRVA = text.VirtualAddress + textFileSize - MAX_PATCH_SIZE

k32imp = (entry.imports for entry in pe.DIRECTORY_ENTRY_IMPORT
    if entry.dll.lower().startswith('kernel32')).next()

def get_imp_va(funcName):
    return hex((imp.address for imp in k32imp if imp.name == funcName).next())

asmFile = open(TEMP_FILENAME + '.asm ', 'w')
asmFile.write('''; THIS CODE WAS GENERATED BY SCRIPT

use32

label _LoadLibrary dword at ''' + get_imp_va('LoadLibraryA') + '''
label _FindFirstFile dword at ''' + get_imp_va('FindFirstFileA') + '''
label _FindNextFile dword at ''' + get_imp_va('FindNextFileA') + '''
label _OriginalEntryPoint at ''' + hex(
    pe.OPTIONAL_HEADER.AddressOfEntryPoint +pe.OPTIONAL_HEADER.ImageBase) + '''

include "''' + FASM + '''\include\win32a.inc"

org ''' + hex(pe.OPTIONAL_HEADER.ImageBase + patchRVA) + '''
align 16

; NewEntryPoint
call LoadPlugins
call _OriginalEntryPoint
ret

mask db "*.plgn", 0
align 16

proc LoadPlugins uses edi
    local findData:WIN32_FIND_DATA
    lea eax, [findData]
    invoke _FindFirstFile, mask, eax
    mov edi, eax
    test eax, eax
    jnz load_loo
    ret

load_loo:
    lea eax, [findData.cFileName]
    invoke _LoadLibrary, eax
    
    lea eax, [findData]
    invoke _FindNextFile, edi, eax
    test eax, eax
    jnz load_loo
    ret
endp
''')
asmFile.close()

err = os.system(FASM + '\\fasm.exe ' +
    TEMP_FILENAME + '.asm ' + TEMP_FILENAME + '.bin')
os.remove(TEMP_FILENAME + '.asm')
if err == 0:
    print "\nCompiled OK. Patching."
    binfile = open(TEMP_FILENAME + '.bin', 'rb')
    pe.set_bytes_at_rva(patchRVA, binfile.read())
    pe.OPTIONAL_HEADER.AddressOfEntryPoint = patchRVA
    pe.write(FILE_NAME)
    
    binfile.close()
    os.remove(TEMP_FILENAME + '.bin')
    print "All done."

os.system("pause")

Замечания по коду:
  • Код совместим с Python 2.5 по этому там нет with.
  • Код совместим с IronPython по этому там явно вызываются close() для файлов. В обычном пайтоне можно писать
    pe.set_bytes_at_rva(patchRVA, open(TEMP_FILENAME + '.bin', 'rb').read()) 
    хендлы закроются сами.
  • Разумеется это простейший пример, для .exe файла без релоков, у которого уже импортированы нужные функции, впрочем с помощью pefile можно решать и более сложные задачи.
  • Запись в конец секции плохо выглядит с точки зрения антивирусов. По хорошему надо как минимум патчить сразу за концом оригинального кода, чтобы не было большого разрыва.

No comments:

Post a Comment