Змеиная анатомия. Вскрываем и потрошим PyInstaller

Че­лове­чес­тво породи­ло целый зоопарк скрип­товых язы­ков с низ­ким порогом вхож­дения в попыт­ке облегчить всем жела­ющим «вка­тыва­ние в айти» сра­зу пос­ле окон­чания месяч­ных кур­сов. Есть мне­ние, что в этом зоопар­ке царем зве­рей сей­час работа­ет 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

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *