Бы­тует мне­ние, что защит­ные механиз­мы для Android-при­ложе­ний сла­бы и лег­ко обхо­дят­ся, одна­ко реаль­ность ока­зыва­ется гораз­до слож­нее. На рын­ке сущес­тву­ет мно­жес­тво ком­мерчес­ких про­тек­торов, спо­соб­ных защитить код от взло­ма, и Baidu — один из них. Давай под­робно раз­берем внут­реннюю архи­тек­туру и устрой­ство это­го наворо­чен­ного мобиль­ного про­тек­тора.

warning

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

Ес­ли ты регуляр­но чита­ешь «Хакер», у тебя мог­ло сло­жить­ся впе­чат­ление, буд­то вся­кие хит­рые про­тек­торы, исполь­зующие обфуска­цию с шиф­ровани­ем кода, вир­туали­зацию и про­чие страш­ные анти­хакер­ские при­емы, сущес­тву­ют толь­ко для дес­ктоп­ных защит (типа Themida, WMProtect и им подоб­ных). А мобиль­ные, в час­тнос­ти андро­идов­ские, при­ложе­ния прак­тичес­ки без­защит­ны, в свя­зи с чем лома­ются лег­ко и при­ятно, даже без помощи отладчи­ка в ста­тике.

Ко­неч­но же, это не так: под Android сущес­тву­ет мно­жес­тво серь­езных ком­мерчес­ких решений, слож­ность и цена которых оправды­вают сох­ранение при­ват­ности кода. Разуме­ется, спрос рож­дает пред­ложение, и спе­циаль­но обу­чен­ные люди (боль­шей частью китай­цы) успе­ли нап­ридумы­вать мно­жес­тво про­тек­торов, защища­ющих код мобиль­ных при­ложе­ний от взло­ма и ревер­са. И конеч­но, тут же наш­лись дру­гие китай­цы, которые иссле­дуют подоб­ные про­тек­торы на пред­мет уяз­вимос­тей и кле­пают инс­тру­мен­ты для взло­ма.

Нап­ример, лет десять назад прос­тые сту­ден­ты Гон­конг­ско­го политех­ничес­кого инсти­тута про­вели собс­твен­ное иссле­дова­ние наибо­лее популяр­ных (китай­ских, как ты, навер­ное, догадал­ся) про­тек­торов Android-при­ложе­ний и даже соз­дали собс­твен­ный па­кет DexHunter, поз­воля­ющий более‑менее успешно их обхо­дить. Жела­ющие могут под­робно озна­комить­ся с общи­ми прин­ципами пос­тро­ения таких про­тек­торов и спо­соба­ми их обхо­да, так ска­зать, из пер­вых рук. Мы же, как обыч­но, перей­дем от теории к прак­тике и раз­берем взлом одной из опи­сан­ных авто­рами сис­тем защиты — Baidu.

Выб­ранный мною при­мер будет полезен еще и тем, что прог­ресс не сто­ит на мес­те и защиты совер­шенс­тву­ются. В час­тнос­ти, акту­аль­ные вер­сии Baidu уже не обой­ти при помощи DexHunter и про­чих инс­тру­мен­тов реверс‑инже­нера. А зна­чит, за неиме­нием однокно­поч­ного решения при­ходит­ся мас­терить его собс­твен­ной головой и руками, чем мы сегод­ня и зай­мем­ся.

Исследуем задачу

Итак, усло­вие задачи. У нас име­ется некое Android-при­ложе­ние, работа­ющее с экзо­тичес­ким железом. Под­дер­жка это­го при­ложе­ния прек­ращена, исходни­ки недос­тупны. Тре­бует­ся получить исходный код для пос­леду­юще­го ана­лиза. Если ты уже нем­ного зна­ком с отладкой и ревер­сом Android-при­ложе­ний, навер­няка уме­ешь поль­зовать­ся таким полез­ным инс­тру­мен­том, как JEB. Заг­ружа­ем в него наш .APK и для начала попыта­емся най­ти в коде ссыл­ку на тек­сто­вую стро­ку «Authentication Failed. Wrong password?», выс­какива­ющую при неп­равиль­ном вво­де пароля.

По­ка все нор­маль­но, стро­ка успешно най­дена в ресур­сах: она при­сутс­тву­ет там в явном виде, и даже иден­тифика­тор ее известен — authentication_failed_wrong_password. А вот даль­ше все ста­новит­ся грус­тно: поиск это­го иден­тифика­тора дает толь­ко вари­анты исходной стро­ки на язы­ках народов мира, а ссы­лок на код нет ни малей­ших.

Спа­сибо хотя бы на том, что нам теперь известен шес­тнад­цатерич­ный иден­тифика­тор иско­мой стро­ки — 0x7f0f009a, по которо­му мож­но поп­робовать попытать­ся най­ти ее в откомпи­лиро­ван­ном smali-коде. Одна­ко здесь нас тоже ждет разоча­рова­ние — ни в одном из рас­пакован­ных фай­лов про­екта подоб­ная ком­бинация не встре­чает­ся.

По­пут­но мы обна­ружи­ваем в разоб­ранном про­екте нес­коль­ко инте­рес­ных вещей. В нем, по сути, нет кода, как‑либо свя­зан­ного с фун­кци­ями при­ложе­ния, — толь­ко вспо­мога­тель­ные биб­лиотеч­ные пакеты и ресур­сы. Одна­ко в коде при­сутс­тву­ет стран­ный, час­тично обфусци­рован­ный класс com.sagittarius вот с таким содер­жимым.

Код явно ссы­лает­ся на натив­ные биб­лиоте­ки baiduprotect, вер­сии которых под раз­личные архи­тек­туры лежат в под­катало­гах Assets и Libraries. Открыв для при­мера в дизас­сем­бле­ре IDA вер­сию такой биб­лиоте­ки под архи­тек­туру x86, обна­ружи­ваем, что внут­ри она сос­тоит из крип­тован­ного и вир­туали­зован­ного кода чуть менее, чем пол­ностью, и вер­сии под осталь­ные архи­тек­туры выг­лядят не луч­ше.

Это озна­чает, что ста­тичес­ким ана­лизом деком­пилиро­ван­ного кода в нашем слу­чае не обой­тись, — при­дет­ся каким‑то обра­зом заг­ружать прог­рамму в отладчик и иссле­довать работу защиты в динами­ке. Поп­робу­ем сов­местить при­ятное с полез­ным и решить еще одну сопутс­тву­ющую задачу. Ты, веро­ятно, уже слы­шал, что серь­езные иссле­дова­ния мобиль­ных при­ложе­ний про­водят­ся на спе­циаль­но рутован­ных подопыт­ных Android-устрой­ствах, под­клю­чен­ных к компь­юте­ру через отла­доч­ные интерфей­сы. А мы люди жад­ные и небога­тые, покупать телефон‑жер­тву спе­циаль­но для этой цели не будем.

Поп­робу­ем решить нашу задачу, не выходя за рам­ки Windows-компь­юте­ра, — будем исполь­зовать для этой цели Android-эму­лятор. Для при­мера возь­мем популяр­ный условно‑бес­плат­ный эму­лятор BlueStacks. Я выб­рал имен­но его по ряду сооб­ражений: во‑пер­вых, как я ска­зал выше, он бес­плат­ный, во‑вто­рых, прек­расно работа­ет офлайн, а в‑треть­их, под­держи­вает отла­доч­ный интерфейс ADB (не буду отвле­кать­ся от повес­тво­вания, объ­ясняя, что это такое, — думаю, что ты все‑таки име­ешь неболь­шой опыт ревер­са и отладки под Android и читал соот­ветс­тву­ющие статьи). И что самое глав­ное, наше при­ложе­ние прек­расно уста­нав­лива­ется и запус­кает­ся на этом эму­лято­ре, хотя, как пишут в интерне­те, далеко не каж­дый эму­лятор вос­при­нима­ет Baidu, как род­ной. А раз так, то вклю­чаем в нашем BlueStacks дос­туп к отла­доч­ному интерфей­су ADB (напоми­наю, для это­го в нас­трой­ках прог­раммы нуж­но уста­новить фла­жок «Вклю­чить Android Debug Bridge (ADB)». Заод­но обра­щаем вни­мание на адрес под­клю­чения 127.0.0.1:5555.

Те­перь заг­ружа­ем и уста­нав­лива­ем наше при­ложе­ние в эму­лятор (сно­ва не буду под­робно оста­нав­ливать­ся на этом момен­те: все под­робно опи­сано в интерне­те). Пос­ле это­го уста­нав­лива­ем на свой компь­ютер Android Debug Bridge и про­буем по инс­трук­ции под­клю­чить­ся к эму­лято­ру. Если ты все сде­лал пра­виль­но, то коман­да adb devices покажет эму­лятор в спис­ке дос­тупных устрой­ств.

Те­перь мож­но под­клю­чить­ся к нему, выпол­нив коман­ду adb -s emulator-5554 shell. Это необя­затель­но получит­ся с пер­вого раза, но если ты акку­рат­но сле­довал инс­трук­ции, то рано или поз­дно под­клю­чение будет уста­нов­лено. Выпол­нив коман­ду ps, мож­но убе­дить­ся, что иссле­дуемое при­ложе­ние запуще­но и находит­ся в спис­ке активных про­цес­сов.

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

am dumpheap <process PID> /data/local/tmp/dump.bin

Од­нако в получен­ном фай­ле dump.bin ни малей­ших сле­дов рас­пакован­ного кода не обна­ружи­лось. Похоже, мы вер­нулись на исходную: сдам­пить рас­шифро­ван­ные дан­ные из памяти про­цес­са нель­зя, пос­коль­ку они там отсутс­тву­ют. Но что, если поис­кать эти дан­ные не в памяти про­цес­са, а в памяти эму­лято­ра? Ведь BlueStacks — это обыч­ное Windows-при­ложе­ние, мож­но открыть его отладчи­ком x64dbg, получить дос­туп к памяти всех заг­ружен­ных в эму­лятор про­цес­сов и даже отсле­живать отту­да их работу и заг­рузку.

Бук­валь­но на пер­вом шаге по это­му пути нас ожи­дает неболь­шая проб­лема: а к какому же про­цес­су атта­чить­ся? Запущен­ный эму­лятор порож­дает сра­зу нес­коль­ко активных про­цес­сов: BstkSVC, HD-Agent, HD-Player, BlueStacksServices (нес­коль­ко экзем­пля­ров) и собс­твен­но Bluestacks (их тоже, вне­зап­но, нес­коль­ко). Пос­коль­ку мы про­веря­ем гипоте­зу, что рас­шифро­ван­ный образ находит­ся в памяти про­цес­са, смот­рим, есть ли в памяти шес­тнад­цатерич­ные иден­тифика­торы исполь­зуемых строк, на которые нет пря­мых ссы­лок в рас­пакован­ном .APK (0x7f0f009a, 0x7F0F0693, 0x7F0F0694 и так далее). Про­цеду­ра дос­таточ­но мутор­ная, ведь нам надо убе­дить­ся, что это не слу­чай­ное вхож­дение 32-бит­ной пос­ледова­тель­нос­ти, а имен­но ссыл­ка на прав­доподоб­ный дал­виков­ский код, в котором исполь­зует­ся эта кон­стан­та. Но вот наше тер­пение воз­награж­дено — при­атта­чив­шись к про­цес­су HD-Player, мы видим вот такое вхож­дение кон­стан­ты 0x7F0F0694 (на скрин­шоте выделе­но крас­ным).

Ищем расшифрованный образ

По­чему нас заин­тересо­вало имен­но это вхож­дение кон­стан­ты? Если вни­матель­но пос­мотреть на сосед­ний код, то пред­шес­тву­ющие ей два бай­та (DW 0x314, выделе­но жел­тым) пред­став­ляют собой опкод Dalvik-коман­ды const p1(v3),0x7F0F0694, что намека­ет на осмыслен­ный код фун­кции, начало которой явно прос­лежива­ется нес­коль­кими стро­ками выше:

.short 4 # Выделено оранжевым: Number of registers : 4
.short 3 # Выделено светло-зеленым: Size of input args (in words) : 3
.short 2 # Выделено голубым: Size of output args (in words) : 2
.short 0 # Выделено черным: Number of try_items : 0
.int 0x23D82B # Выделено синим: Адрес Debug info
.int 0x41 # Выделено темно-зеленым: Size of bytecode (in 16-bit units): 0x41

Из этой информа­ции мы даже видим сме­щения до Debug info фун­кций, ана­лизи­руя которые и исполь­зуя до­кумен­тацию по фор­мату DEX мы можем при извес­тном ста­рании най­ти адре­са начала сек­ций Code 0x26F914 (на сле­дующем рисун­ке выделе­но крас­ным) и Debug Info 0x23A904 (на сле­дующем рисун­ке выделе­но синим). А по их вхож­дени­ям — най­ти сек­цию Map.

В этой сек­ции при­сутс­тву­ет ссыл­ка на саму себя (0x4CC338, выделе­но серым). По ней уже находит­ся заголо­вок все­го рас­шифро­ван­ного DEX-обра­за.

Ка­залось бы, теперь мы победи­ли хит­рых китай­цев, оста­ется толь­ко взять из заголов­ка обра­за его раз­мер (0x4CC414, выделе­но серым) и сдам­пить его на диск для пос­леду­юще­го пре­пари­рова­ния. Одна­ко и тут нас ждет страш­ный облом. Хоть рас­шифро­ван­ный образ и находит­ся, по всей видимос­ти, в памяти HD-Player, но не одним неп­рерыв­ным кус­ком, а фраг­менти­рован­но, раз­бро­сан­ными бло­ками раз­мером, крат­ным 0x1000 байт. Cоб­рать такую моза­ику руками чер­тов­ски слож­но.

Нет, не невоз­можно, нес­мотря на то что количес­тво этих бло­ков может быть свы­ше тысячи. Если хорошо под­напрячь­ся и покопать вир­туаль­ную машину BlueStacks (а она в основном содер­жится в модулях BstkVMM.dll, BstkRT.dll и BstkDD.dll), то мож­но раз­работать мас­су хит­рых вари­антов сле­жения за дис­ковыми опе­раци­ями вир­туаль­ного обра­за, в час­тнос­ти за выделе­нием и чте­нием вир­туаль­ной памяти. В кон­це кон­цов, мож­но даже кон­тро­лиро­вать исполне­ние кода в вир­туаль­ной песоч­нице. Воз­можно, я ког­да‑нибудь раз­беру этот тер­нистый путь на кон­крет­ном при­мере в сво­ей оче­ред­ной статье, но не в этот раз, пос­коль­ку он все‑таки чер­тов­ски сло­жен.

Призываем на помощь «Фриду»

Пе­рей­дем к сле­дующе­му еще не исполь­зован­ному нами инс­тру­мен­ту — Frida. Сно­ва наде­юсь на твою осве­дом­ленность, тем более про эту прог­рамму «Хакер» уже рас­ска­зывал. Поэто­му кос­нусь лишь слож­ностей и проб­лем, свя­зан­ных с работой Frida в сочета­нии с эму­лято­ром BlueStacks. А их немало, и они весь­ма мер­зкие.

Ска­жем пря­мо: в интерне­те быту­ет мне­ние, что Frida — не для эму­лято­ров, а BlueStacks ее вооб­ще не под­держи­вает. Но мы любоз­натель­ные и недовер­чивые, в свя­зи с чем поп­робу­ем разоб­рать­ся в этом сами. Глав­ное пре­пятс­твие уста­нов­ки сер­вера Frida на BlueStacks — отсутс­твие у пос­ледне­го рута «из короб­ки», а «Фри­де», как извес­тно, нужен имен­но рут или модифи­циро­ван­ное при­ложе­ние. Это вооб­ще не наш вари­ант, пос­коль­ку Baidu к модифи­кации отно­сит­ся болез­ненно и раз­бирать­ся еще и с этой проб­лемой уж точ­но не вхо­дит в наши пла­ны.

В интерне­те опи­сано мно­жес­тво кос­тыль­ных спо­собов получе­ния root на эму­лято­ре BlueStacks, боль­шая часть из которых не работа­ет из‑за зак­рытой для записи фай­ловой сис­темы (эта проб­лема нам еще попор­тит кро­ви в даль­нейшем). Поэто­му не буду оста­нав­ливать­ся на их обзо­ре, а при­веду наибо­лее прос­той метод, который лич­но у меня более‑менее зарабо­тал на BlueStacks 4. Речь идет о BlueStacks Tweaker. Решение исклю­читель­но для ленивых: ничего никуда копиро­вать и собирать не надо, прос­то заг­ружа­ешь прог­рамму с отно­ситель­но дру­жес­твен­ным интерфей­сом, жмешь нес­коль­ко кно­пок — и твой BlueStacks рутован.

Из плю­сов — в эму­лято­ре теперь дос­тупна коман­да su и работа­ет при­ложе­ние SuperSU, которым мож­но и нуж­но пре­дос­тавить пра­ва супер­поль­зовате­ля для при­ложе­ний ADB Shell. И если ты хочешь исполь­зовать эму­лятор тер­минала Termux для про­хож­дения даль­нейше­го квес­та уста­нов­ки Frida, то ему тоже нуж­но про­писать пра­ва по этой инс­трук­ции.

Итак, мы пре­одо­лели пер­вое пре­пятс­твие на пути уста­нов­ки Frida на наш эму­лятор, но цена это­го пре­одо­ления ока­залась дос­таточ­но высока: переза­пус­тив рутован­ный BlueStacs, мы с ужа­сом обна­ружи­ваем, что иссле­дуемое при­ложе­ние перес­тало запус­кать­ся, — оно сра­зу мол­ча зак­рыва­ется. Baidu наконец‑то понял, что сей­час его нач­нут ломать, и в ответ он при­нял­ся саботи­ровать работу прог­раммы.

Ка­залось бы, смысл даль­нейших телод­вижений пол­ностью отсутс­тву­ет, но на вся­кий слу­чай заг­ружа­ем эму­лятор в x64dbg и про­веря­ем наличие рас­шифро­ван­ного DEX в памяти эму­лято­ра. И он там есть! Перед тем как совер­шить сеп­пуку, Baidu успе­вает‑таки рас­шифро­вать наш DEX, а зна­чит, есть смысл тру­дить­ся даль­ше, даже с уче­том такой стран­ной работы при­ложе­ния.

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

Те­перь нам пред­сто­ит уста­новить сер­вер Frida на эму­лятор. C уста­нов­кой кли­ента на компь­ютер по инс­трук­ции никаких проб­лем не воз­ника­ет, чего не ска­жешь о сер­вере. Все при­веден­ные выше инс­трук­ции пред­полага­ют уста­нов­ку питона на Android-устрой­ство и сбор­ку сер­вера, но, к сожале­нию, у BlueStacks и с этим воз­ника­ют проб­лемы. Лич­но я сколь­ко ни ста­рал­ся, так и не смог прой­ти шаг pip install frida frida-tools.

В ито­ге я прос­то ска­чал уже ском­пилиро­ван­ную вер­сию Frida server под плат­форму x86 от­сюда. Нужен имен­но сер­вер под x86, пос­коль­ку для BlueStacks это род­ная плат­форма. Самое стран­ное, что ARM-сер­вер тоже запус­тится и даже будет работать, но чудить при этом ста­нет изрядно. В общем, далее дей­ству­ем по инс­трук­ции — закиды­ваем сер­вер в эму­лятор:

adb push frida-server-16.5.6-android-x86 /data/local/tmp/frida-server

Пос­ле это­го в Termux или ADB Shell даем пра­ва и запус­каем сер­вер su (пос­ле этой коман­ды кур­сор приг­лашения коман­дной стро­ки дол­жен сме­нить­ся на #):

cd /data/local/tmp
chmod 755 frida-server
./frida-server

Про­веря­ем работос­пособ­ность сер­вера, выпол­нив из коман­дной стро­ки Windows коман­ду frida-ps -U. Она тоже может сра­ботать не с пер­вого раза, но, если все‑таки выдаст спи­сок про­цес­сов, в котором при­сутс­тву­ет и наш иссле­дуемый, зна­чит, этот этап мы прош­ли.

Те­перь для начала поп­робу­ем сдам­пить память про­цес­са уже через Frida. Для это­го вос­поль­зуем­ся ути­литой fridump. К сожале­нию, и на этот раз чуда не про­исхо­дит — в дам­пе памяти про­цес­са никаких сле­дов рас­шифро­ван­ного DEX нет. Это озна­чает, что рас­шифро­ван­ный код при­сутс­тву­ет в памяти толь­ко на крат­кий миг меж­ду заг­рузкой с дис­ка с рас­шифров­кой и ком­пиляци­ей. К счастью, у нас теперь под рукой прек­расный инс­тру­мент, с исполь­зовани­ем которо­го мы можем внед­рять­ся в любые, даже самые интимные мес­та прог­раммы в любой момент ее исполне­ния. Давай подума­ем, где может быть сла­бое мес­то у китай­ской защиты, бОль­шая часть кода которой зашиф­рована? Разуме­ется, это импорти­руемые сим­волы. Ведь прог­рамма, по сути, рас­шифро­выва­ет код, может, она исполь­зует при этом какие‑то стан­дар­тные крип­тогра­фичес­кие биб­лиоте­ки? Сно­ва откры­ваем биб­лиоте­ку baiduprotect.so в IDA и смот­рим, что имен­но она импорти­рует из стан­дар­тных биб­лиотек.

К сожале­нию, никакой крип­тогра­фии в спис­ке импорти­руемых фун­кций с ходу не вид­но, одна­ко бро­сают­ся в гла­за сра­зу нес­коль­ко фун­кций работы с ком­прес­сией — inflate, uncompress и подоб­ные. На исполь­зование ком­прес­сии как бы намека­ет и тот факт, что нам точ­но известен раз­мер рас­шифро­ван­ного обра­за: 0x4CC414 — 5 030 932 бай­та. А бес­хозных фай­лов такого раз­мера в .APK нету, да и внут­ри сущес­тву­ющих DEX-обра­зов пакета такой раз­мер скрыть нег­де.

Са­мые подоз­ритель­ные фай­лы (явно пок­рипто­ван­ные или запако­ван­ные) под наз­вани­ем baiduprotect*.jar находят­ся в катало­ге Accets рядом с биб­лиоте­ками libbaiduprotect*.so, но они силь­но мень­ше иско­мого раз­мера. Поп­робу­ем про­верить эту гипоте­зу — трас­сиру­ем вызовы uncompress в запущен­ной прог­рамме. Как я говорил ранее, нес­мотря на зак­рытие прог­раммы при запус­ке на рутован­ном BlueStacks, она про­дол­жает висеть в активных про­цес­сах и ее вид­но при выпол­нении коман­ды frida-ps -U.

Сра­зу пре­дуп­реждаю, что Frida сама по себе очень кап­ризная шту­ка, а в сочета­нии с BlueStacks вооб­ще может взбры­кивать неп­ред­ска­зуемо, поэто­му все дей­ствия надо выпол­нять край­не акку­рат­но:

Frida-trace завис­нет в ожи­дании, одна­ко, если ткнуть в зна­чок нашего при­ложе­ния в BlueStacks, она выдаст три стро­ки "uncompress", пос­ле которых обна­ружи­вает­ся исклю­чение в libbaiduprotect.so. Выходит, что при запус­ке при­ложе­ния libbaiduprotect перед смертью триж­ды рас­паковы­вает некий код.

Пос­коль­ку в катало­ге Assert имен­но три зашиф­рован­ных фай­ла (точ­нее, три пары) — baiduprotect1.jar (baiduprotect1.i.dex), baiduprotect2.jar (baiduprotect2.i.dex) и baiduprotect3.jar (baiduprotect3.i.dex), то, похоже, мы на вер­ном пути. Что­бы окон­чатель­но убе­дить­ся в этом, напишем скрипт внед­рения и перех­вата вызова uncompress.

Пишем скрипт

Для это­го заг­лянем в маны и пос­мотрим на парамет­ры это­го вызова. Собс­твен­но, алго­ритм скрип­та будет сле­дующий: на вхо­де в uncompress мы печата­ем парамет­ры dest (адрес буфера рас­пакован­ных дан­ных), destLen (дли­ну рас­пакован­ных дан­ных), source (адрес буфера упа­кован­ных дан­ных), sourceLen (дли­ну упа­кован­ных дан­ных). Ну и заод­но адрес вызова, который нам может потом при­годить­ся для ана­лиза кода. Запоми­наем адре­са буфера и счет­чика рас­пакован­ных дан­ных, что­бы при выходе из фун­кции сдам­пить их. В ито­ге получа­ется такой скрипт:

const f = Module.getExportByName('libz.so',
// Отслеживаемая функция uncompress библиотеки libz.so
'uncompress');
// Указатель на буфер распаковки
var buf;
// Указатель на счетчик распакованных байтов
var buflen;
// Дамп первых строк распакованных данных
function outputArray(data,len)
{
var cnt=0;
for (var i=0;i<10;i++)
{
var st="";
if (cnt==len) break;
for(var j=0;j<16;j++) {
st+=('0' + (data[cnt] & 0xFF).toString(16)).slice(-2);
st+=" ";
cnt++;
if (cnt==len) break;
}
console.log(st);
}
}
Interceptor.attach(f, {
// Код, выполняемый на входе в uncompress
onEnter(args) {
console.log('uncompress called from:\n' +
// Откуда был вызов?
Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join('\n') + '\n');
// Адрес буфера распаковки
console.log('Bytef * dest: '+args[0]);
// Указатель на счетчик распакованных байтов
console.log('uLongf * destLen: '+args[1]);
// Указатель на упакованные байты
console.log('const Bytef * source: '+args[2]);
// Длина упакованных данных
console.log('uLong sourceLen: '+args[3]);
// Запоминаем адрес буфера распаковки
buf=args[0];
// Запоминаем указатель на счетчик распакованных байтов
buflen=args[1];
},
// Код, выполняемый на выходе в uncompress
onLeave(retval) {
console.log('uncompress exited\n');
// Адрес буфера распаковки
console.log('dest: '+buf);
// Печатаем количество распакованных байтов
console.log('destLen: '+buflen.readUInt());
const data = buf.readByteArray(buflen.readUInt());
// Дампим распакованные данные
outputArray(new Uint8Array(data), buflen.readUInt());
}
});

Сох­раня­ем этот код в файл uncompress.js, который запус­каем (акку­рат­но выпол­нив все ука­зан­ные выше шаги) из Windows-кон­соли сле­дующей коман­дой:

frida -U -f <классовое имя нашего приложения, полученное командой ps> -l uncompress.js

В эму­лято­ре мор­гает и гас­нет окош­ко запущен­ного при­ложе­ния, и в кон­соль вывали­вает­ся сле­дующая прос­тыня логиру­емых дан­ных:

uncompress called from:
0x899d80cc libbaiduprotect.so!0x130cc
0xbf6633d0
Bytef * dest: 0x891fb000
uLongf * destLen: 0xbf84fb3c
const Bytef * source: 0x88fc9010
uLong sourceLen: 0x20457f
uncompress exited
dest: 0x891fb000
destLen: 5030932
64 65 78 0a 30 33 35 00 ee ca ca fd a3 5a 33 c8
93 99 93 ac 92 3f be ac ef 15 ba f6 6f 07 58 cc
14 c4 4c 00 70 00 00 00 78 56 34 12 00 00 00 00
00 00 00 00 38 c3 4c 00 78 b1 00 00 70 00 00 00
ab 18 00 00 50 c6 02 00 fb 1e 00 00 fc 28 03 00
4e 3e 00 00 c0 9c 04 00 c0 78 00 00 30 8f 06 00
d8 11 00 00 30 55 0a 00 e4 33 40 00 30 90 0c 00
30 90 0c 00 32 90 0c 00 36 90 0c 00 39 90 0c 00
4b 90 0c 00 59 90 0c 00 5c 90 0c 00 60 90 0c 00
6b 90 0c 00 95 90 0c 00 9f 90 0c 00 06 91 0c 00
uncompress called from:
0x899d80cc libbaiduprotect.so!0x130cc
0xbf6633d0
Bytef * dest: 0x89158000
uLongf * destLen: 0xbf84fb3c
const Bytef * source: 0x89101010
uLong sourceLen: 0x29992
uncompress exited
dest: 0x89158000
destLen: 409340
64 65 78 0a 30 33 35 00 49 f9 37 58 e0 82 9b db
ac eb 66 8c 61 1c 3d 36 dc 7c f7 a6 6f e3 e4 59
fc 3e 06 00 70 00 00 00 78 56 34 12 00 00 00 00
00 00 00 00 2c 3e 06 00 5e 13 00 00 70 00 00 00
df 02 00 00 e8 4d 00 00 ed 04 00 00 64 59 00 00
1d 06 00 00 80 94 00 00 6d 0e 00 00 68 c5 00 00
76 01 00 00 d0 38 01 00 6c d7 04 00 90 67 01 00
90 67 01 00 92 67 01 00 c0 67 01 00 e0 67 01 00
48 68 01 00 4b 68 01 00 5d 68 01 00 63 68 01 00
6e 68 01 00 71 68 01 00 80 68 01 00 8f 68 01 00
uncompress called from:
0x899d80cc libbaiduprotect.so!0x130cc
0xbf6633d0
Bytef * dest: 0x89127000
uLongf * destLen: 0xbf84fb3c
const Bytef * source: 0x890e3010
uLong sourceLen: 0x165d4
uncompress exited
dest: 0x89127000
destLen: 196864
64 65 78 0a 30 33 35 00 60 cc 07 49 66 67 41 78
97 05 88 65 0e a4 0b ea 46 ae 87 88 dc 40 93 14
00 01 03 00 70 00 00 00 78 56 34 12 00 00 00 00
00 00 00 00 30 00 03 00 1b 07 00 00 70 00 00 00
d8 01 00 00 dc 1c 00 00 3b 02 00 00 3c 24 00 00
7c 02 00 00 00 3f 00 00 ac 06 00 00 e0 52 00 00
ce 00 00 00 40 88 00 00 00 5f 02 00 00 a2 00 00
3a 46 02 00 3c 46 02 00 40 46 02 00 43 46 02 00
46 46 02 00 49 46 02 00 4c 46 02 00 4f 46 02 00
52 46 02 00 55 46 02 00 58 46 02 00 5b 46 02 00

Мы видим, что дей­стви­тель­но при вызове uncompress из libbaiduprotect.so (код, из которо­го выпол­няет­ся вызов RVA=0x130cc, тоже на этот момент рас­шифро­ван) рас­паковы­вают­ся три зашиф­рован­ных DEX-модуля. Это понят­но по заголов­кам. Иско­мым модулем, судя по раз­меру, явля­ется самый боль­шой — пер­вый. Мож­но даже срав­нить заголо­вок с тем, который мы получи­ли при ана­лизе памяти BlueStacks в x64dbg.

Выводы

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

Нач­нем с пло­хих новос­тей: нес­мотря на то что мы получи­ли на блю­деч­ке пол­ный рас­шифро­ван­ный код модуля DEX одним кус­ком, что­бы вытащить его из Frida, тебе при­дет­ся еще повозить­ся. Потому как дамп в кон­соли — это, конеч­но, кра­сиво и хорошо, но пять мегабайт бинар­ного кода так не извле­чешь.

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

К при­меру, у меня не получи­лось записать из Frida файл даже в /data/local/tmp, куда мы перед этим бла­гопо­луч­но переса­дили саму «Фри­ду». Разуме­ется, твой пыт­ливый ум най­дет мас­су дру­гих обходных спо­собов извле­кать рас­пакован­ные дан­ные, пусть это будет домаш­ним задани­ем.

Хо­рошая же новость зак­люча­ется в том, что при исполь­зовании Frida сами биб­лиоте­ки перес­тают быть зашиф­рован­ным «чер­ным ящи­ком». К при­меру, ничего не меша­ет из вышеп­риведен­ного скрип­та сдам­пить код самого libbaiduprotect в рас­шифро­ван­ном виде, про­ана­лизи­ровать его и на его осно­ве написать, к при­меру, свой собс­твен­ный инс­тру­мент для рас­шифров­ки DEX безо вся­кого эму­лято­ра и Frida.