Руби наотмашь. Исследуем архитектуру приложения на Ruby и учимся его реверсить

Содержание статьи

  • Задача
  • Исследуем программу
  • Генерируем ошибку
  • Ищем альтернативу
  • Выводы

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

Ког­да‑то о Ruby было написа­но мно­жес­тво рек­ламных ста­тей. Для тех, кто хочет более деталь­но озна­комить­ся с его кон­цепци­ей и внут­ренней струк­турой, есть кни­га Ruby under a microscope. В общем, как ты уже понял, цель сегод­няшней статьи — не пос­тижение дзе­на прог­рамми­рова­ния на Ruby, а сухой прак­тичес­кий раз­бор осо­бен­ностей ревер­са при­ложе­ний, реали­зован­ных на этой экзо­тике.

 

Задача

Итак, дано при­ложе­ние, чита­ющее и валиди­рующее некий бинар­ный файл. Нам нуж­но разоб­рать­ся с алго­рит­мом валида­ции. Пер­вичный авто­мати­чес­кий ана­лиз при­ложе­ния при помощи Detect It Easy на этот раз нам тол­ком ничего не дает — при­ложе­ние детек­тится как соб­ранное из C++ в баналь­ном Microsoft Visual Studio 2015. Одна­ко в ком­плект динами­чес­ких биб­лиотек прог­раммы вхо­дит инте­рес­ный файл x64-msvcr100-ruby200.dll, на который пол­но ссы­лок из исполня­емо­го модуля. Файл позици­они­рует себя как интер­пре­татор Ruby за авторс­твом самого отца‑осно­вате­ля Юки­хиро Мацумо­то со ссыл­кой на со­ответс­тву­ющий сайт.

Вдо­бавок в ком­плект прог­раммы вхо­дит око­ло двух с полови­ной тысяч фай­лов *.rb, при бли­жай­шем рас­смот­рении ока­зав­шихся обыч­ными тек­сто­выми ничем не защищен­ными Ruby-скрип­тами. Как ты уже догадал­ся, есть малень­кий под­вох, без которо­го эту статью писать было бы неин­терес­но: ни в одном из этих фай­лов обра­щений к нашему целево­му фай­лу не наш­лось, а зна­чит, при­дет­ся заг­ружать нашу прог­рамму в отладчик x64dbg и искать обра­щение к фай­лу в динами­ке интер­пре­тиро­ван­ного кода.

 

Исследуем программу

Дей­ству­ем по стан­дар­тной схе­ме, навер­няка уже зна­комой тебе по моим пре­дыду­щим стать­ям. Ста­вим точ­ку оста­нова на фун­кцию чте­ния фай­ла kernel32.dll.ReadFile. Конеч­но же, при заг­рузке прог­раммы будут тысячи подоб­ных обра­щений ко все­воз­можным фай­лам нас­тро­ек и биб­лиотек, вхо­дящих в пакет, но нам нем­ного облегчит задачу монито­ринг чте­ния фай­лов с помощью тоже извес­тной нам прог­раммы Process Monitor (ProcMon). Бла­года­ря ей мы замеча­ем, что чте­ние нашего иско­мого фай­ла, в отли­чие от всех осталь­ных, идет исклю­читель­но бло­ками по 0x10000 байт. Поэто­му, что­бы отсечь все ненуж­ные нам чте­ния, мож­но пос­тавить в качес­тве усло­вия для оста­нова Size==0x10000.

За­пус­каем нашу прог­рамму и ждем дос­тижения этой точ­ки. Про­ана­лизи­руем стек вызова получен­ного ReadFile.

Вер­хние пять вызовов пред­став­ляют собой натив­ную обвязку чте­ния фай­ла и нам неин­терес­ны. А вот ниже явно идут два вло­жен­ных вызова интер­пре­тато­ра, до боли зна­комых нам по раз­бору вир­туаль­ных машин дру­гих скрип­товых язы­ков. Что­бы не отвле­кать­ся на пов­торение того, что я мно­гок­ратно писал в сво­их пре­дыду­щих стать­ях, пос­тара­юсь ска­зать в двух сло­вах. Любой, даже самый упо­ротый (вро­де питона) интер­пре­татор скрип­тового язы­ка никог­да не раз­бира­ет тек­сто­вую семан­тику во вре­мя выпол­нения. Для опти­миза­ции работы интер­пре­тато­ра во вре­мя заг­рузки модуля (клас­са, объ­екта, метода и так далее) про­исхо­дит ком­пиляция тек­ста в натив или шитый байт‑код, сам про­цесс называ­ется JIT-ком­пиляци­ей (just-in-time, в нуж­ное вре­мя) или поп­росту динами­чес­кой ком­пиляци­ей.

Ра­зуме­ется, нес­мотря на заяв­ленную экс­тра­ваган­тность, в точ­ности так же пос­тупа­ет и Ruby. Про осо­бен­ности JIT-ком­пиляции для раз­ных реали­заций Ruby мож­но почитать на сай­те patshaughnessy.net, а мы поп­робу­ем разоб­рать­ся с нашей вир­туаль­ной машиной. Нем­ного поизу­чав вло­жен­ные вызовы сте­ка с пре­дыду­щего скрин­шота, находим основной цикл выбор­ки команд шитого кода (выделе­ны стрел­ками). В IDA код этой про­цеду­ры (sub_18001B6E0, экспор­тиру­емая фун­кция — rb_vm_get_insns_address_table) выг­лядит так.

Как видим, опкод занима­ет 8 байт, вир­туаль­ная машина содер­жит 83 коман­ды ассем­бле­ра, реали­зация каж­дой из которых пред­став­лена в этой фун­кции. Ока­зыва­ется, биб­лиоте­ка интер­пре­тато­ра даже содер­жит в себе дизас­сем­блер для ком­пилиро­ван­ного байт‑кода (экспор­ты rb_iseq_disasm_insn, rb_iseq_disasm). Ана­лизи­руя эти фун­кции, мож­но най­ти таб­лицу мне­моник команд, находя­щуюся по адре­су 180200500:

00 nop 01 getlocal 02 setlocal 03 getspecial 04 setspecial 05 getinstancevariable 06 setinstancevariable 07 getclassvariable 08 setclassvariable 09 getconstant 0A setconstant 0B getglobal 0C setglobal 0D putnil 0E putself 0F putobject 10 putspecialobject 11 putiseq 12 putstring 13 concatstrings 14 tostring 15 toregexp 16 newarray 17 duparray 18 expandarray 19 concatarray 1A splatarray 1B newhash 1C newrange 1D pop 1E dup 1F dupn 20 swap 21 reput 22 topn 23 setn 24 adjuststack 25 defined 26 checkmatch 27 trace 28 defineclass 29 send 2A opt_send_simple 2B invokesuper 2C invokeblock 2D leave 2E throw 2F jump 30 branchif 31 branchunless 32 getinlinecache 33 onceinlinecache 34 setinlinecache 35 opt_case_dispatch 36 opt_plus 37 opt_minus 38 opt_mult 39 opt_div 3A opt_mod 3B opt_eq 3C opt_neq 3D opt_lt 3E opt_le 3F opt_gt 40 opt_ge 41 opt_ltlt 42 opt_aref 43 opt_aset 44 opt_length 45 opt_size 46 opt_empty_p 47 opt_succ 48 opt_not 49 opt_regexpmatch1 4A opt_regexpmatch2 4B opt_call_c_function 4C bitblt 4D answer 4E getlocal_OP__WC__0 4F getlocal_OP__WC__1 50 setlocal_OP__WC__0 51 setlocal_OP__WC__1 52 putobject_OP_INT2FIX_O_0_C_ 53 putobject_OP_INT2FIX_O_1_C_

При вни­матель­ном рас­смот­рении иден­тифици­рует­ся и сам тип интер­пре­тато­ра байт‑кода, это раз­новид­ность YARV.

 

Генерируем ошибку

Ин­тер­пре­татор мы опре­дели­ли, байт‑код, из которо­го вызыва­ется чте­ние нашего фай­ла, наш­ли, но что даль­ше? Ревер­сить работу с фай­лом, ана­лизи­руя ском­пилиро­ван­ный байт‑код, даже при наличии встро­енно­го дизас­сем­бле­ра как‑то грус­тно. Хочет­ся най­ти тек­сто­вый исходник Ruby-скрип­та, хотя, конеч­но, не исклю­чена воз­можность, что интер­пре­тато­ру под­сунули уже искусс­твен­но ском­пилиро­ван­ный байт‑код. Но пока что не будем рас­смат­ривать столь край­ние вари­анты. Поп­робу­ем хотя бы опре­делить имя метода, вызыва­юще­го чте­ние фай­ла.

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

Ответить

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