Приключения «Электрона». Отлаживаем JSC-код любой версии Electron без декомпилятора

Ког­да‑то ком­пания Positive Technology раз­работа­ла собс­твен­ный пла­гин для деком­пилято­ра Ghidra, поз­воля­ющий иссле­довать веб‑при­ложе­ния на Electron, в которых исполь­зует­ся ском­пилиро­ван­ный бинар­ный код V8 JSC. C тех пор сме­нилось мно­го вер­сий Node.js, фор­мат кар­диналь­но поменял­ся, в резуль­тате сущес­тву­ющие перес­тали работать. В этой статье я покажу, как самос­тоятель­но без деком­пилято­ра отла­живать бинар­ный JS-байт‑код про­изволь­ной вер­сии.

Се­год­ня мы погово­рим о при­ложе­ниях, соз­данных при помощи сре­ды Electron. Этот фрей­мворк — клас­сичес­кий при­мер инс­тру­мен­та, с помощью которо­го любой прод­винутый вер­сталь­щик может почувс­тво­вать себя пол­ноцен­ным раз­работ­чиком кросс‑плат­формен­ных прог­рам­мных пакетов. То есть Electron может скон­верти­ровать сайт в самодос­таточ­ное (хотя и нес­коль­ко неук­люжее) при­ложе­ние. Надо ли говорить, что тех­нология получи­ла дос­таточ­но широкое рас­простра­нение. Если верить статье на Хаб­ре, на этой плат­форме реали­зова­но мно­жес­тво популяр­ных при­ложе­ний — мес­сен­дже­ры Skype и Discord, редак­торы для кода Visual Studio Code и Atom и дру­гие.

Про этот фрей­мворк написа­но мно­жес­тво ста­тей (жела­ющие могут изу­чить, нап­ример, ру­ководс­тво для начина­ющих или крат­кое руководс­тво по Electron). Одна­ко наш инте­рес к нему лежит нес­коль­ко в иной плос­кости. По тра­диции мы поп­робу­ем пре­пари­ровать готовое при­ложе­ние Electron на пред­мет иссле­дова­ния его внут­ренней струк­туры, отладки, ревер­са, воз­можной доработ­ки и исправ­ления багов.

Итак, у нас име­ется при­ложе­ние, при откры­тии исполня­емо­го модуля которо­го (надо ска­зать, весь­ма упи­тан­ного — боль­ше сот­ни мегабайт) Detect It Easy выда­ет сле­дующую информа­цию.

Ес­ли ты уже слег­ка зна­ком с пакетом Electron или успел бег­ло про­читать пред­ложен­ные выше статьи, то чудовищ­ный раз­мер исполня­емо­го фай­ла тебя не уди­вит — ведь, по сути, исполня­емый модуль пред­став­ляет собой слег­ка опти­мизи­рован­ную вер­сию бра­узе­ра Chromium + Node. В ней запус­кает­ся код JavaScript, на котором, собс­твен­но, и написа­на основная логика при­ложе­ния. Этот код и про­чие HTML-пот­роха могут лежать в откры­том и проз­рачном для иссле­дова­ния виде (этот слу­чай нам неин­тересен, как три­виаль­ный). Так­же они могут быть соб­раны или упа­кова­ны в отдель­ный ресурс спе­циаль­ного вида asar — тут воз­можны слож­ности, но мы пока не будем их касать­ся. Еще ресур­сы могут быть час­тично откомпи­лиро­ваны в натив в исполня­емом фай­ле (обыч­но так и есть). А иног­да они откомпи­лиро­ваны целиком, сей­час мы этот вари­ант так­же под­робно рас­смат­ривать не будем. Мы нач­нем с прос­того слу­чая: ядро Node ском­пилиро­вано в натив, а JS-скрип­ты интер­пре­тиру­ются ядром в виде байт‑кода.

Ос­тановим­ся попод­робнее на этом момен­те. Для тех, кто не читал мои пре­дыду­щие статьи (нап­ример, «Ре­вер­синг .NET. Как искать JIT-ком­пилятор в при­ложе­ниях» или «Без­защит­ная Java. Лома­ем Java bytecode encryption»), пояс­ню, что акту­аль­ные скрип­товые плат­формы (Java, .NET и дру­гие) работа­ют по такому основно­му алго­рит­му. Для удобс­тва и ско­рос­ти скрипт ком­пилиру­ется в про­межу­точ­ный кросс‑плат­формен­ный байт‑код, который уже при работе прог­раммы, по мере вызова методов и клас­сов, или интер­пре­тиру­ется интер­пре­тато­ром, или кон­верти­рует­ся в исполня­емый натив­ный код встро­енным JIT-ком­пилято­ром. Это спра­вед­ливо и для JavaScript, для которо­го при­дума­но мно­жес­тво интер­пре­тато­ров: V8, SpiderMonkey, Chakra, Rhino, KJS, Nashorn и дру­гие. Даже в Adobe соз­дали свой собс­твен­ный дви­жок, про который я тоже в свое вре­мя на­писал статью.

Нас же инте­ресу­ет встро­енный в Electron дви­жок Chrome V8. В нем име­ется пакет bytenote, поз­воля­ющий сох­ранить исходный скрипт в виде сери­али­зован­ного пред­став­ления байт‑кода V8, которое выпол­няет­ся так же, как и сам скрипт. Этот фор­мат име­ет рас­ширение .JSC и час­тень­ко исполь­зует­ся для обфуска­ции как в самом пакете Electron, так и в дру­гих сер­верных при­ложе­ниях, написан­ных на язы­ке JavaScript.

Упо­мяну­тый фор­мат мало­изу­чен: три года назад груп­па Positive Technologies оза­боти­лась его ревер­сом и про­вела иссле­дова­ние байт‑кода вер­сии 8.16.0, резуль­татом чего ста­ли нес­коль­ко ста­тей. Я рекомен­дую озна­комить­ся с ними самос­тоятель­но для пущего понима­ния пред­метной области.

Еще одним резуль­татом иссле­дова­ния ста­ло появ­ление пла­гина для деком­пилято­ра Ghidra, спо­соб­ного работать с JSC-фай­лами вер­сии 8.16.0. К сожале­нию, пос­ле это­го инте­рес к теме у иссле­дова­телей угас. Нес­мотря на то что с тех пор было выпуще­но еще нес­коль­ко про­ектов для ревер­са бинар­ного кода JS (нап­ример, jsc-decompile-mozjs-34 или cocos2d-jsc-decompiler), вер­сии Node.js меня­ются с такой быс­тро­той, что на текущий момент все эти про­екты без­надеж­но уста­рели. Поэто­му я на при­мере байт‑кода вер­сии 10.2.154.26 (JSC-сиг­натура A905 DEC0 866CEBA8) попыта­юсь рас­ска­зать, как самос­тоятель­но без деком­пилято­ра иссле­довать и отла­живать бинар­ный JS-байт‑код про­изволь­ной вер­сии при помощи отладчи­ка x64dbg и IDA.

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

Как мы вык­ручива­лись из подоб­ного зат­рудне­ния в прош­лые разы? Тупо атта­чились ко всем копи­ям прог­раммы (в при­ори­тете про­цес­сы с заголов­ком активно­го окна или Chrome_Widget) и про­веря­ли, какая копия исполня­ет нуж­ный нам код, оста­нав­лива­ясь на бря­ках. Или же вар­вар­ски оста­нав­ливали код в нуж­ной точ­ке, вби­вая туда корот­кий jmp self (0xEB,0xFE), тем самым зацик­ливая прог­рамму в дан­ном мес­те.

Вто­рой спо­соб под­ходит нам даже боль­ше, пос­коль­ку нуж­ное мес­то мгно­вен­но прос­какива­ет при заг­рузке и руками при­тор­мозить в нем прог­рамму мы никак не успе­ваем. А так при­ложе­ние прос­то зависа­ет в дан­ной точ­ке, и затор­можен­ный про­цесс дос­таточ­но отсле­дить, при­атта­чив­шись к нему отладчи­ком, или раз­бло­киро­вать, вер­нув исходный код на мес­то и про­дол­жив пос­ле это­го выпол­нение про­цес­са.

Я наз­вал опи­сан­ный спо­соб вар­вар­ским, потому что так делать мож­но далеко не всег­да. Запущен­ные про­цес­сы вза­имо­дей­ству­ют меж­ду собой: посыла­ют зап­росы, получа­ют отве­ты, и, если ответ не пос­тупа­ет через опре­делен­ное вре­мя, про­цесс впол­не может совер­шить сэп­пуку по тайм‑ауту. В час­тнос­ти, иссле­дуемый нами Electron отсле­жива­ет тай­минг про­хож­дения про­цес­сов (я так понимаю, не из каких‑то парано­идаль­ных побуж­дений, это обыч­ная фича любого бра­узе­ра). Но, по счастью, в нашем слу­чае ничего катас­тро­фичес­кого при­ложе­ние не дела­ет, огра­ничи­ваясь пре­дуп­режде­ниями о подоз­ритель­ной небезо­пас­ности про­исхо­дяще­го.

Итак, у нас получи­лось оста­новить­ся в нуж­ном мес­те прог­раммы. Открыв стек вызовов, мы обна­ружи­ваем, что отла­жива­емый нами EXE-модуль, нес­мотря на всю свою раз­дутость, явля­ется весь­ма неболь­шой частью прог­раммы (откомпи­лиро­ван­ным в натив ядром V8), которая вызыва­ется из JIT-ком­пилиро­ван­ного кода.

Ад­рес перехо­да меж­ду интер­пре­тато­ром (выделе­но крас­ным) и ском­пилиро­ван­ными в натив биб­лиоте­ками ядра Node (выделе­но синим) мы видим на сте­ке вызовов. Интер­пре­татор ском­пилиро­ван­ного байт‑кода выг­лядит при­мер­но так:

... 00007FF6BFE4BCDA | 4D:8BBD A8450000 | mov r15,qword ptr ds:[r13+45A8] // r12 — указатель на байт-код метода; r9 — программный счетчик // r9 — текущий опкод 00007FF6BFE4BCE1 | 47:0FB6140C | movzx r10d,byte ptr ds:[r12+r9] // Таблица обработчиков опкодов 00007FF6BFE4BCE6 | 4B:8B0CD7 | mov rcx,qword ptr ds:[r15+r10*8] // Вызов обработчика опкода 00007FF6BFE4BCEA | FFD1 | call rcx // Восстановить указатель на байт-код 00007FF6BFE4BCEC | 4C:8B65 E0 | mov r12,qword ptr ss:[rbp-20] // Восстановить zigzag-coded программный счетчик 00007FF6BFE4BCF0 | 44:8B4D D8 | mov r9d,dword ptr ss:[rbp-28] 00007FF6BFE4BCF4 | 41:D1E9 | shr r9d,1 // bl — текущий байт-код 00007FF6BFE4BCF7 | 43:0FB61C0C | movzx ebx,byte ptr ds:[r12+r9] 00007FF6BFE4BCFC | 4D:8BC1 | mov r8,r9 // Указатель на количество параметров 00007FF6BFE4BCFF | 49:8B8D 281B0000 | mov rcx,qword ptr ds:[r13+1B28] 00007FF6BFE4BD06 | 80FB 03 | cmp bl,3 // Если команда не префикс — переход 00007FF6BFE4BD09 | 77 1D | ja 7FF6BFE4BD28 // Следующий байт за префиксом 00007FF6BFE4BD0B | 41:FFC1 | inc r9d // Префикс Wide или ExtraWide? 00007FF6BFE4BD0E | F6C3 01 | test bl,1 // Получаем опкод, следующий за префиксом 00007FF6BFE4BD11 | 43:0FB61C0C | movzx ebx,byte ptr ds:[r12+r9] 00007FF6BFE4BD16 | 75 09 | jne 7FF6BFE4BD21 // Префикс Wide — смещение в таблице параметров +0xC6 00007FF6BFE4BD18 | 48:81C1 C6000000 | add rcx,C6 00007FF6BFE4BD1F | EB 07 | jmp 7FF6BFE4BD28 // Префикс ExtraWide — смещение в таблице параметров +0xC6 00007FF6BFE4BD21 | 48:81C1 8C010000 | add rcx,18C // Выход из метода, если опкод Return 00007FF6BFE4BD28 | 80FB A9 | cmp bl,A9 00007FF6BFE4BD2B | 0F84 1D000000 | je 7FF6BFE4BD4E // Выход из метода, если опкод SuspendGenerator 00007FF6BFE4BD31 | 80FB AF | cmp bl,AF 00007FF6BFE4BD34 | 0F84 14000000 | je 7FF6BFE4BD4E // Если опкод JumpLoop, то никуда не двигаться 00007FF6BFE4BD3A | 80FB 89 | cmp bl,89 00007FF6BFE4BD3D | 75 05 | jne 7FF6BFE4BD44 00007FF6BFE4BD3F | 4D:8BC8 | mov r9,r8 00007FF6BFE4BD42 | EB 08 | jmp 7FF6BFE4BD4C // r10 — количество параметров команды 00007FF6BFE4BD44 | 44:0FB61419 | movzx r10d,byte ptr ds:[rcx+rbx] // Сдвигаем программный счетчик на него и переходим к обработке следующего опкода 00007FF6BFE4BD49 | 45:03CA | add r9d,r10d 00007FF6BFE4BD4C | EB 8C | jmp 7FF6BFE4BCDA 00007FF6BFE4BD4E | 48:8B5D E0 | mov rbx,qword ptr ss:[rbp-20] ...

Как видишь, отсю­да мож­но получить дли­ны всех обра­баты­ваемых интер­пре­тато­ром опко­дов. Для начала чуть про­ясню, что такое пре­фик­сы Wide и ExtraWide и почему на опко­ды с ними положе­ны раз­ные таб­лицы. Как ска­зано в упо­мяну­той мной статье, в зависи­мос­ти от дли­ны опе­ран­дов каж­дая инс­трук­ция име­ет три вари­анта — обыч­ный (1-бай­товые опе­ран­ды), Wide (2-бай­товые опе­ран­ды) и ExtraWide (4-бай­товые опе­ран­ды). В новой вер­сии добави­лось еще два пре­фик­са — отла­доч­ные, соот­ветс­твен­но, тоже в вари­антах Wide и ExtraWide.

Поп­робу­ем вытащить мне­мони­ку новой сис­темы команд. Конеч­но же, интер­пре­татор содер­жит таб­лицу внут­ренних имен всех инс­трук­ций, ведь они нуж­ны для отла­доч­ной печати при обра­бот­ке оши­бок. Нем­ного покурив код обра­бот­чиков таких оши­бок в IDA, натыка­емся на такой слож­ный case, в котором каж­дому опко­ду соот­ветс­тву­ет имя инс­трук­ции:

... .text:00144A71AE0 cmp cl, 0C5h ; switch 198 cases .text:00144A71AE3 ja def_144A71B01 ; jumptable 0000000144A71B01 default case .text:00144A71AE9 lea rax, aWide_0 ; "Wide" .text:00144A71AF0 movzx ecx, cl .text:00144A71AF3 lea rdx, jpt_144A71B01 .text:00144A71AFA movsxd rcx, ds:(jpt_144A71B01 - 144A72130h)[rdx+rcx*4] .text:00144A71AFE add rcx, rdx .text:00144A71B01 jmp rcx ; switch jump .text:00144A71B03 loc_144A71B03: ; CODE XREF: sub_144A71AE0+21↑j .text:00144A71B03 lea rax, aExtrawide ; jumptable 0000000144A71B01 case 1 .text:00144A71B0A locret_144A71B0A: ; CODE XREF: sub_144A71AE0+21↑j .text:00144A71B0A retn ; jumptable 0000000144A71B01 case 0 .text:00144A71B0B loc_144A71B0B: ; CODE XREF: sub_144A71AE0+21↑j .text:00144A71B0B lea rax, aDebugbreak4 ; jumptable 0000000144A71B01 case 8 .text:00144A71B12 retn ...

Источник: xakep.ru

Ответить

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