Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Человечество породило целый зоопарк скриптовых языков с низким порогом вхождения в попытке облегчить всем желающим «вкатывание в айти» сразу после окончания месячных курсов. Есть мнение, что в этом зоопарке царем зверей сейчас работает Python. Эта ползучая рептилия так сильно опутала своими кольцами IT, что даже нейросеть без ее участия теперь ничему не обучить. А раз так, настало время препарировать этого аспида и посмотреть, что у него внутри. Начнем с технологии под названием PyInstaller.
warning
Статья написана в исследовательских целях, имеет ознакомительный характер и предназначена для специалистов по безопасности. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Использование или распространение ПО без лицензии производителя может преследоваться по закону.
В качестве примера возьмем некое графическое приложение, для регистрации которого нужно ввести правильный серийник в ответ на предложенный программой код оборудования. При неправильном вводе приложение отвечает ругательным сообщением «No valid license code». Detect It Easy уверенно подсказывает, что это наш пациент.
Исследование приложения мы начинаем по стандартной схеме. Поиск сопутствующих текстовых строк в exe-файле не дает результата: исполняемый код и данные явно упакованы или закриптованы. Загрузка приложения в IDA косвенно это подтверждает, exe-файл представляет собой загрузчик для обширного самораспаковывающегося файлового пакета.
Попробуем загрузить программу в наш любимый отладчик x64dbg. По счастью, приложение совершенно не сопротивляется этому, нормально загружается и прерывается по первому требованию. Слегка попрыгав по коду трассировщиком, сразу натыкаемся на участок, сильно смахивающий на интерпретатор шитого пи‑кода:
00007FF9AE401274 | 49:8BC7 | mov rax,r15
00007FF9AE401277 | 49:2BC1 | sub rax,r9
00007FF9AE40127A | 48:D1F8 | sar rax,1
00007FF9AE40127D | 03C0 | add eax,eax
00007FF9AE40127F | 41:8945 68 | mov dword ptr ds:[r13+68],eax
00007FF9AE401283 | 837A 44 00 | cmp dword ptr ds:[rdx+44],0
00007FF9AE401287 | 0F85 85A71200 | jne python39.7FF9AE52BA12
00007FF9AE40128D | 41:0FB73F | movzx edi,word ptr ds:[r15] ; edi <- байт-код текущей команды
00007FF9AE401291 | 4D:8BF4 | mov r14,r12
00007FF9AE401294 | 40:0FB6F7 | movzx esi,dil
00007FF9AE401298 | C1EF 08 | shr edi,8
00007FF9AE40129B | 49:83C7 02 | add r15,2
00007FF9AE40129F | 4C:8965 C8 | mov qword ptr ss:[rbp-38],r12
00007FF9AE4012A3 | 4C:897D B0 | mov qword ptr ss:[rbp-50],r15
00007FF9AE4012A7 | 66:0F1F8400 00000000 | nop word ptr ds:[rax+rax],ax
00007FF9AE4012B0 | 8D46 FF | lea eax,qword ptr ds:[rsi-1]
00007FF9AE4012B3 | 3D A4000000 | cmp eax,A4
00007FF9AE4012B8 | 0F87 85E21200 | ja python39.7FF9AE52F543
00007FF9AE4012BE | 48:98 | cdqe
00007FF9AE4012C0 | 41:8B8C83 D8C80600 | mov ecx,dword ptr ds:[r11+rax*4+6C8D8] ; В rcx <- относительный адрес обработчика текущей команды
00007FF9AE4012C8 | 49:03CB | add rcx,r11
00007FF9AE4012CB | FFE1 | jmp rcx ; Переход на обработчик текущей команды
00007FF9AE4012CD | 48:63D7 | movsxd rdx,edi
00007FF9AE4012D0 | 49:8B84D5 68010000 | mov rax,qword ptr ds:[r13+rdx*8+168]
00007FF9AE4012D8 | 48:85C0 | test rax,rax
00007FF9AE4012DB | 0F84 B3E01200 | je python39.7FF9AE52F394
00007FF9AE4012E1 | 48:FF00 | inc qword ptr ds:[rax]
00007FF9AE4012E4 | 48:8B55 90 | mov rdx,qword ptr ss:[rbp-70]
00007FF9AE4012E8 | 49:890424 | mov qword ptr ds:[r12],rax
00007FF9AE4012EC | 49:83C4 08 | add r12,8
Как видим, таблица обработчиков команд находится по адресу 6C8D8
, а указатель на PC текущей команды — в регистре R15
.
На этом месте отложим пока отладчик в сторону и вспомним теорию. Но сначала, чтобы не забыть, зафиксируем один интересный момент: большинство динамических библиотек, на которые имеются ссылки на вкладке «Отладочные модули», физически находятся в подпапке _MEI100722
системной папки для временных файлов. Судя по всему, это и есть каталог (или один из каталогов), в который сборка распаковывается на время работы приложения.
Чтобы лучше понимать вопрос, давай для начала вспомним, что это за зверь такой — Python. Думаю, не ошибусь, если предположу, что многие знают его как язык для написания простеньких сценариев, вроде JavaScript, отличающийся несколько экстравагантной концепцией выделения блоков кода отступами. Проект создан и развивался в лучших традициях черного английского юмора (как известно, само название — это отсылка к сатирическому британскому телешоу). В ходе этой эволюции узкоспециализированный скриптовый язык получил множество разнообразных библиотек, как в свое время это произошло с фортраном.
Как известно, спрос рождает предложение, поэтому, чтобы разработчикам было легче создавать полноценные коммерческие приложения в рамках привычной концепции Python, были придуманы компиляторы самых разнообразных реализаций. Кто‑то попытался сделать нативный компилятор, другие прикрутили к Python JIT (компиляцию времени исполнения, я рассказывал про эту концепцию в своих предыдущих статьях).
Соответственно, были созданы проекты Jython (трансляция в байт‑код JVM) и IronPython (трансляция в .NET IL). Но, к сожалению, как ты мог убедиться из приведенного выше фрагмента кода интерпретатора, эталонная реализация лишена полезных свойств — перед нами обычная интерпретация py-кода, не отличающаяся высокой оптимизацией.
Подробнее про различные методы компиляции питоновского кода в исполняемые приложения можно почитать, например, на «Хабре». В этой статье упомянута сборка приложения с помощью исследуемого нами PyInstaller и разборка его на составляющие файлы проекта с использованием PyInstaller Extractor.
Хотя лично я для извлечения файлов из проекта посоветовал бы более продвинутый инструмент — pydumpck. Разумеется, он тоже не всемогущ и ему присущи определенные недостатки. К примеру, у меня он нормально запускается только на версии питона 3.9, но вообще, надо сказать, проблема совместимости кода даже между соседними подверсиями — обычная и даже не самая главная проблема этого языка. В общем, достаточно лирики, вернемся к суровым техническим подробностям эталонной реализации.
Минимальной единицей скомпилированного питоновского байт‑кода является файл .pyc
(есть еще файлы .pyo
, скомпилированные с оптимизацией, но их мы трогать не будем). Этот файл генерируется из текстового скриптового кода вызовом метода py_compile.compile
или просто при вызове директивы import
во время исполнения скрипта, чтобы не компилировать импортируемый модуль лишний раз. Подобным образом разработчики попытались компенсировать отсутствующий в эталонной реализации JIT. Этот файл содержит в себе байт‑код скомпилированного модуля, константы, ссылки и так далее. Формат его зависит от версии Python, официально не документирован, однако хорошо описан в интернете, например на сайте Nedbatchelder. В этой же статье приведен и текст простейшего дизассемблера pyc
, написанного на питоне:
import dis, marshal, struct, sys, time, typesdef show_file(fname): f = open(fname, "rb") magic = f.read(4) moddate = f.read(4) modtime = time.asctime(time.localtime(struct.unpack('L', moddate)[0])) print "magic %s" % (magic.encode('hex')) print "moddate %s (%s)" % (moddate.encode('hex'), modtime) code = marshal.load(f) show_code(code)def show_code(code, indent=''): print "%scode" % indent indent += ' ' print "%sargcount %d" % (indent, code.co_argcount) print "%snlocals %d" % (indent, code.co_nlocals) print "%sstacksize %d" % (indent, code.co_stacksize) print "%sflags %04x" % (indent, code.co_flags) show_hex("code", code.co_code, indent=indent) dis.disassemble(code) print "%sconsts" % indent for const in code.co_consts: if type(const) == types.CodeType: show_code(const, indent+' ') else: print " %s%r" % (indent, const) print "%snames %r" % (indent, code.co_names) print "%svarnames %r" % (indent, code.co_varnames) print "%sfreevars %r" % (indent, code.co_freevars) print "%scellvars %r" % (indent, code.co_cellvars) print "%sfilename %r" % (indent, code.co_filename) print "%sname %r" % (indent, code.co_name) print "%sfirstlineno %d" % (indent, code.co_firstlineno) show_hex("lnotab", code.co_lnotab, indent=indent)def show_hex(label, h, indent): h = h.encode('hex') if len(h) < 60: print "%s%s %s" % (indent, label, h) else: print "%s%s" % (indent, label) for i in range(0, len(h), 60): print "%s %s" % (indent, h[i:i+60])show_file(sys.argv[1])
Источник: xakep.ru