В сегод­няшней статье мы погово­рим о 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».

Присоединяйся к сообществу «Xakep.ru»!

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

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

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

    Подписаться

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