Ques/Help/Req Змеиная анатомия. Вскрываем и потрошим PyInstall

XakeR

Member
Регистрация
13.05.2006
Сообщения
1 912
Реакции
0
Баллы
16
Местоположение
Ukraine
Человечество породило целый зоопарк скриптовых языков с низким порогом вхождения в попытке облегчить всем желающим «вкатывание в айти» сразу после окончания месячных курсов. Есть мнение, что в этом зоопарке царем зверей сейчас работает 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»!​


Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

-60%

1 year​


9990 рублей 4000 р.


[TD]

1 month_r​


920 р.
[/TD]

Я уже участник «Xakep.ru»
 
198 237Темы
635 209Сообщения
3 618 425Пользователи
Pandar96Новый пользователь
Верх