Че­лове­чес­тво породи­ло целый зоопарк скрип­товых язы­ков с низ­ким порогом вхож­дения в попыт­ке облегчить всем жела­ющим «вка­тыва­ние в айти» сра­зу пос­ле окон­чания месяч­ных кур­сов. Есть мне­ние, что в этом зоопар­ке царем зве­рей сей­час работа­ет Python. Эта пол­зучая реп­тилия так силь­но опу­тала сво­ими коль­цами IT, что даже ней­росеть без ее учас­тия теперь ничему не обу­чить. А раз так, нас­тало вре­мя пре­пари­ровать это­го аспи­да и пос­мотреть, что у него внут­ри. Нач­нем с тех­нологии под наз­вани­ем PyInstaller.

warning

Статья написа­на в иссле­дова­тель­ских целях, име­ет озна­коми­тель­ный харак­тер и пред­назна­чена для спе­циалис­тов по безопас­ности. Автор и редак­ция не несут ответс­твен­ности за любой вред, при­чинен­ный с при­мене­нием изло­жен­ной информа­ции. Исполь­зование или рас­простра­нение ПО без лицен­зии про­изво­дите­ля может прес­ледовать­ся по закону.

В качес­тве при­мера возь­мем некое гра­фичес­кое при­ложе­ние, для регис­тра­ции которо­го нуж­но ввес­ти пра­виль­ный серий­ник в ответ на пред­ложен­ный прог­раммой код обо­рудо­вания. При неп­равиль­ном вво­де при­ложе­ние отве­чает ругатель­ным сооб­щени­ем «No valid license code». Detect It Easy уве­рен­но под­ска­зыва­ет, что это наш паци­ент.

Ис­сле­дова­ние при­ложе­ния мы начина­ем по стан­дар­тной схе­ме. Поиск сопутс­тву­ющих тек­сто­вых строк в exe-фай­ле не дает резуль­тата: исполня­емый код и дан­ные явно упа­кова­ны или зак­рипто­ваны. Заг­рузка при­ложе­ния в IDA кос­венно это под­твержда­ет, exe-файл пред­став­ляет собой заг­рузчик для обширно­го саморас­паковы­вающе­гося фай­лового пакета.

Поп­робу­ем заг­рузить прог­рамму в наш любимый отладчик x64dbg. По счастью, при­ложе­ние совер­шенно не соп­ротив­ляет­ся это­му, нор­маль­но заг­ружа­ется и пре­рыва­ется по пер­вому тре­бова­нию. Слег­ка поп­рыгав по коду трас­сиров­щиком, сра­зу натыка­емся на учас­ток, силь­но сма­хива­ющий на интер­пре­татор шитого пи‑кода:

07FF9AE401274 | 49:8BC7 | mov rax,r15
07FF9AE401277 | 49:2BC1 | sub rax,r9
07FF9AE40127A | 48:D1F8 | sar rax,1
07FF9AE40127D | 03C0 | add eax,eax
07FF9AE40127F | 41:8945 68 | mov dword ptr ds:[r13+68],eax
07FF9AE401283 | 837A 44 00 | cmp dword ptr ds:[rdx+44],0
07FF9AE401287 | 0F85 85A71200 | jne python39.7FF9AE52BA12
07FF9AE40128D ; edi — байт-код текущей команды
07FF9AE40128D | 41:0FB73F | movzx edi,word ptr ds:[r15]
07FF9AE401291 | 4D:8BF4 | mov r14,r12
07FF9AE401294 | 40:0FB6F7 | movzx esi,dil
07FF9AE401298 | C1EF 08 | shr edi,8
07FF9AE40129B | 49:83C7 02 | add r15,2
07FF9AE40129F | 4C:8965 C8 | mov qword ptr ss:[rbp-38],r12
07FF9AE4012A3 | 4C:897D B0 | mov qword ptr ss:[rbp-50],r15
07FF9AE4012A7 | 66:0F1F8400 00000000| nop word ptr ds:[rax+rax],ax
07FF9AE4012B0 | 8D46 FF | lea eax,qword ptr ds:[rsi-1]
07FF9AE4012B3 | 3D A4000000 | cmp eax,A4
07FF9AE4012B8 | 0F87 85E21200 | ja python39.7FF9AE52F543
07FF9AE4012BE | 48:98 | cdqe
07FF9AE4012C0 ; В rcx — относительный адрес обработчика
07FF9AE4012C0 ; текущей команды
07FF9AE4012C0 | 41:8B8C83 D8C80600 | mov ecx,dword ptr ds:[r11+rax*4+6C8D8]
07FF9AE4012C8 | 49:03CB | add rcx,r11
07FF9AE4012CB ; Переход на обработчик текущей команды
07FF9AE4012CB | FFE1 | jmp rcx
07FF9AE4012CD | 48:63D7 | movsxd rdx,edi
07FF9AE4012D0 | 49:8B84D5 68010000 | mov rax,qword ptr ds:[r13+rdx*8+168]
07FF9AE4012D8 | 48:85C0 | test rax,rax
07FF9AE4012DB | 0F84 B3E01200 | je python39.7FF9AE52F394
07FF9AE4012E1 | 48:FF00 | inc qword ptr ds:[rax]
07FF9AE4012E4 | 48:8B55 90 | mov rdx,qword ptr ss:[rbp-70]
07FF9AE4012E8 | 49:890424 | mov qword ptr ds:[r12],rax
07FF9AE4012EC | 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, types
def 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])

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

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

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    2 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии