Хакер - Реверсинг .NET. Как искать JIT-компилятор в приложениях

Хакер - Реверсинг .NET. Как искать JIT-компилятор в приложениях

hacker_frei

https://t.me/hacker_frei

МВК

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

  • Немного теории
  • Ищем JIT-компилятор
  • Проверка в бою
  • Заключение

Во вре­мя деком­пиляции и ана­лиза .NET-при­ложе­ний ревер­серы стал­кива­ются с раз­личны­ми метода­ми анти­отладки. Один из них — сок­рытие метадан­ных и IL-кода, которые вос­ста­нав­лива­ются толь­ко при JIT-ком­пиляции прог­раммы. Для борь­бы с ними хакеры и реверс‑инже­неры при­дума­ли целый ком­плекс спе­циаль­ных инс­тру­мен­тов и методов, которые мы рас­смот­рим в сегод­няшней статье.

INFO

О прин­ципах взло­ма при­ложе­ний, защищен­ных про­тек­тором Enigma, читай в стать­ях «Боль­ше не эниг­ма. Лома­ем защиту при­ложе­ний Enigma x64 акту­аль­ных вер­сий» и «Три­аль­ный конь. Как сло­мать trial, защищен­ный Enigma Protector».

C лег­кой руки Microsoft одной из самых популяр­ных плат­форм для прог­рамми­рова­ния в нас­тоящее вре­мя ста­ла .NET. Огромное количес­тво инс­тру­мен­тов, биб­лиотек и докумен­тации обес­печива­ют прос­тоту вхож­дения даже для самых начина­ющих кодеров, а кросс‑плат­формен­ность и все более совер­шенная опти­миза­ция кода дела­ют ее одним из основных стан­дартов написа­ния ком­мерчес­кого соф­та. Как следс­твие, инс­тру­мен­тов для взло­ма и реверс‑инжи­нирин­га под эту плат­форму тоже успе­ли соз­дать немало. Сре­ди них dnSpy, ILspy, ILdasm, Dile, SAE и мно­гие дру­гие, имя им — леги­он!

За­дача для ревер­серов упро­щает­ся тем, что по умол­чанию ском­пилиро­ван­ная прог­рамма фак­тичес­ки содер­жит свой исходник: име­на сим­волов хра­нят­ся в явном виде, а кросс‑плат­формен­ный IL-псев­докод лег­ко вос­ста­нав­лива­ется до исходных син­такси­чес­ких конс­трук­ций C# или VB, из которых он был получен при ком­пиляции. Соот­ветс­твен­но, взлом такой прог­раммы для начина­юще­го хакера — одно удо­воль­ствие: дос­таточ­но заг­рузить ее в dnSpy, и вот она, на блю­деч­ке в сво­их исходни­ках, для удобс­тва даже окра­шен­ных в при­ятные цве­та. Отла­живай и правь как хочешь, как буд­то сам эту прог­рамму и написал!

НЕМНОГО ТЕОРИИ

Ра­зуме­ется, про­изво­дите­ли соф­та мирить­ся с подоб­ным положе­нием дел не могут, и на оче­ред­ном вит­ке кон­фрон­тации меж­ду хакера­ми и про­тек­торами было раз­работа­но мно­го инс­тру­мен­тов, пре­пятс­тву­ющих вос­ста­нов­лению исходно­го кода из IL-сбор­ки. Гру­бо говоря, все подоб­ные инс­тру­мен­ты исполь­зуют три основных прин­ципа:

  • сок­рытие (шиф­рование, ком­прес­сия и так далее) .NET-метадан­ных и IL-кода с вос­ста­нов­лени­ем толь­ко в крат­кий миг JIT-ком­пиляции;
  • об­фуска­ция IL-кода, то есть пред­намерен­ное запуты­вание его логики, борь­ба с чита­емостью тек­сто­вых строк и имен сим­волов, что­бы понять логику работы вос­ста­нов­ленно­го IL-кода было слож­нее;
  • ком­бинация двух пре­дыду­щих катего­рий.

Се­год­ня мы погово­рим о методах из пер­вой катего­рии. В прин­ципе, наибо­лее прос­той и дубовый спо­соб огра­дить прог­рамму от ILDasm — ском­пилиро­вать ее с атри­бутом SupressIldasmAttribute. Понят­ное дело, это защита от чес­тных людей, пос­коль­ку такая сбор­ка пре­вос­ходно детек­тиру­ется как .NET-при­ложе­ние, деком­пилиру­ется дру­гими инс­тру­мен­тами, а дан­ный атри­бут с пол­пинка сни­мает­ся в CFFexplorer или, при изрядной сно­ров­ке, в прос­том HEX-редак­торе. Более инте­рес­но «завер­нуть» метадан­ные в обыч­ное натив­ное при­ложе­ние, фор­миру­ющее и запус­кающее .NET-сбор­ку на лету.

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

Са­мый прос­той спо­соб — вос­поль­зовать­ся ути­литой MegaDumper (или даже ее более прод­винутой вер­сией ExtremeDumper). Если .NET сфор­мирован и запущен по всем пра­вилам, то он кор­рек­тно рас­позна­ется упо­мяну­тыми ути­лита­ми имен­но как .NET-про­цесс, и при нажатии кно­поч­ки .NET dump дам­пится как стан­дар­тное .NET-при­ложе­ние. Прав­да, вов­се не факт, что оно будет запус­кать­ся. Что­бы при­вес­ти его в запус­каемый вид, при­дет­ся про­делать опре­делен­ные телод­вижения, в зависи­мос­ти от прод­винутос­ти про­тек­тора. Тем не менее метадан­ные .NET и IL в такой сдам­плен­ной сбор­ке будут дос­тупны для деком­пиляции и ана­лиза. Мож­но убе­дить­ся в этом, открыв сбор­ку, нап­ример, в CFFexplorer. Одна­ко я спе­циаль­но сде­лал ого­вор­ку «если». Поп­робу­ем разоб­рать­ся, почему подоб­ное может не сра­ботать.

Для это­го пос­тара­юсь корот­ко в двух сло­вах напом­нить прин­цип фун­кци­они­рова­ния .NET-при­ложе­ния для тех, кто забыл мат­часть. Нес­мотря на то что сбор­ка сос­тоит из метадан­ных и кросс‑плат­формен­ного IL-кода, при выпол­нении при­ложе­ния он не интер­пре­тиру­ется, а ком­пилиру­ется в весь­ма опти­мизи­рован­ный натив­ный код целево­го про­цес­сора и целевой опе­раци­онной сис­темы. Дела­ется это непос­редс­твен­но при заг­рузке бло­ка кода один раз, впос­ледс­твии будет выпол­нять­ся уже ском­пилиро­ван­ный натив­ный код метода. Сам про­цесс называ­ется JIT-ком­пиляция (Just In Time, «вре­мен­ная ком­пиляция на лету»). То есть если прер­вать прог­рамму в про­изволь­ный момент в отладчи­ке типа x64dbg, то про­цесс будет оста­нов­лен имен­но во вре­мя исполне­ния такого вре­мен­но ском­пилиро­ван­ного натив­ного кода.

Трас­сировать, отла­живать и ревер­сировать его, конеч­но, мож­но, но целесо­образность это­го сом­нитель­на. Нас инте­ресу­ет дру­гой под­ход — пой­мать и сдам­пить уже вос­ста­нов­ленный фраг­мент IL-кода перед его JIT-ком­пиляци­ей. Логика под­ска­зыва­ет, что, если мы хотим сде­лать это вруч­ную, нам надо най­ти в отладчи­ке изна­чаль­ную точ­ку вхо­да в JIT-ком­пилятор. Самое прос­тое — отыс­кать метод SystemDomain::Execute в биб­лиоте­ке clr.dll (или mscorwks.dll для более ста­рых вер­сий .NET). Обыч­но для подоб­ных вещей рекомен­дуют исполь­зовать WinDbg и его рас­ширение SOS, но я для при­мера покажу, как это делать в x64dbg.

ИЩЕМ JIT-КОМПИЛЯТОР

Итак, заг­рузив нуж­ное при­ложе­ние в отладчик, мы с неп­рият­ным удив­лени­ем обна­ружи­ваем, что биб­лиоте­ка clr.dll отсутс­тву­ет в спис­ке отла­доч­ных сим­волов. Зна­чит, ее при­дет­ся заг­рузить допол­нитель­но, пред­варитель­но отыс­кав глу­боко в нед­рах под­катало­гов сис­темной пап­ки Windows. Най­дя и заг­рузив clr.dll (попут­но заг­рузит­ся нес­коль­ко биб­лиотек), мы сно­ва с раз­дра­жени­ем обна­ружим, что метод SystemDomain::Execute отсутс­тву­ет в пра­вом спис­ке экспор­та. Ну что ж, по счастью, x64dbg пре­дос­тавля­ет прек­расную воз­можность заг­рузить отла­доч­ные сим­волы пря­мо с май­кро­соф­тов­ско­го сер­вера — для это­го нуж­но щел­кнуть пра­вой кла­вишей мыши на clr.dll и выб­рать соот­ветс­тву­ющий пункт в кон­текс­тном меню.

По­дож­дав некото­рое вре­мя, мы уви­дим, что спи­сок в пра­вой час­ти окна отладчи­ка изрядно уве­личил­ся и иско­мый метод SystemDomain::Execute в нем уже при­сутс­тву­ет. Ста­вим на него точ­ку оста­нова и запус­каем прог­рамму. В момент оста­нова на этом методе дот­нетов­ские метадан­ные чаще все­го уже рас­шифро­ваны, рас­пакова­ны и их мож­но дам­пить в файл хоть MegaDumper’ом, хоть Scylla из самого дебаг­гера. Одна­ко это­го тоже может ока­зать­ся недос­таточ­но. Поп­робу­ем коп­нуть чуть глуб­же и вый­ти на исходный JIT-ком­пилятор.

Для это­го най­дем и заг­рузим выше­опи­сан­ным спо­собом биб­лиоте­ку clrjit.dll, а так­же отла­доч­ные сим­волы к ней. Находим в них сле­дующий метод:

private: virtual enum CorJitResult __stdcall CILJit::compileMethod(class ICorJitInfo *,struct CORINFO_METHOD_INFO *,unsigned int,unsigned char * *,unsigned long *)

Это и есть иско­мая точ­ка вхо­да JIT-ком­пилято­ра, тран­сли­рующе­го IL-код в натив­ный машино­зави­симый. К сожале­нию (или к счастью), дан­ный метод может быть под­менен через фун­кцию GetJit того же самого модуля clrjit.dll, чем и поль­зуют­ся про­тек­торы, инжекти­руя в ком­пилятор собс­твен­ный модуль рас­шифров­ки IL-кода. К нашей радос­ти, сов­сем под­менить ком­пилятор на свой собс­твен­ный они не могут, ибо тог­да им при­дет­ся фак­тичес­ки с нуля перепи­сывать всю плат­форму .NET, с пол­ной под­дер­жкой раз­ных опе­раци­онных сис­тем и про­цес­соров. То есть в какой‑то момент рас­шифро­ван­ный код будет передан в най­ден­ный нами род­ной ком­пилятор. Там мы его бла­гопо­луч­но при­мем. Ста­вим точ­ку оста­нова на дан­ный метод и запус­каем прог­рамму.

Пос­ле того как прог­рамма оста­новит­ся, поп­робу­ем про­ана­лизи­ровать парамет­ры на сте­ке. Для это­го сно­ва вспом­ним теорию. В тер­минах язы­ка С опи­сание дан­ного метода выг­лядит вот так:

CorJitResult (__stdcall * compileMethod) {

struct ICorJitCompiler *pThis, /* IN */

struct ICorJitInfo *comp, /* IN */

struct CORINFO_METHOD_INFO *info, /* IN */

unsigned /* CorJitFlag */ flags, /* IN */

BYTE **nativeEntry, /* OUT */

ULONG *nativeSizeOfCode /* OUT */

Тре­тий свер­ху сте­ка адрес (акку­рат над двой­ным сло­вом flags, которые обыч­но рав­ны FFFFFFFF) — ука­затель на струк­туру CORINFO_METHOD_INFO. Эта струк­тура содер­жит дан­ные о бло­ке IL-кода, которым опи­сыва­ется ком­пилиру­емый метод. Сно­ва покурив ману­алы, находим опи­сание этой струк­туры:

struct CORINFO_METHOD_INFO {

CORINFO_METHOD_HANDLE ftn;

CORINFO_MODULE_HANDLE scope;

BYTE * ILCode;

unsigned ILCodeSize;

unsigned short maxStack;

unsigned short EHcount;

CorInfoOptions options;

CORINFO_SIG_INFO args;

CORINFO_SIG_INFO locals;

};

Пе­рей­дя в дам­пе по ссыл­ке, мы обна­ружим, что третье двой­ное сло­во с начала струк­туры и вправ­ду ука­затель на IL-код метода, а чет­вертое — раз­мер бло­ка. Конеч­но, доволь­но нап­ряжно вот так вот руками в отладчи­ке рас­шифро­вывать каж­дый метод по одно­му. Одна­ко мы теперь зна­ем, как это дела­ется, и при желании можем ревер­сировать всю пред­шес­тву­ющую пос­ледова­тель­ность дей­ствий, которую про­изво­дит с бло­ком кода инжекти­рован­ный в него про­тек­тор. В кон­це кон­цов, мож­но инжекти­ровать свой код меж­ду про­тек­тором и род­ным ком­пилято­ром и реали­зовы­вать собс­твен­ный дам­пер для каж­дой новой защиты.

ПРОВЕРКА В БОЮ

Поп­робу­ем исполь­зовать дан­ный метод на слу­чай­ном при­ложе­нии, которое защище­но рас­простра­нен­ным и доволь­но мер­зким про­тек­тором‑обфуска­тором Agile.Net. При­ложе­ние при заг­рузке в отладчи­ки или деком­пилято­ры выда­ет прак­тичес­ки у всех методов пус­тое тело, сос­тоящее из одной коман­ды ret или ldnull / ret. То же самое тво­рит­ся в сек­ции Main, одна­ко .cctor ссы­лает­ся на вызов внеш­ней DLL-ки, внут­ри которой при бег­лом осмотре обна­ружи­вает­ся упо­мина­ние AgileDotNetRT.dll. Собс­твен­но, die так и иден­тифици­рует защиту, сом­нений быть не может. Начина­ем копать прог­рамму все­ми дос­тупны­ми нам инс­тру­мен­тами.

Де­обфуска­торы (вклю­чая род­ной Agile.NET-Deobfuscator-DNLIB) с ходу спра­вить­ся с прог­раммой не могут, дамп с помощью MegaDumper и ExtreamDumper не добав­ляет дан­ных, отоб­ража­ющих­ся в телах методов. Не помога­ет так­же ManagedJitter — сло­вом, все инс­тру­мен­ты, име­ющиеся у нас под рукой, ока­зались бес­силь­ны. Забегая впе­ред, отме­чу, что сущес­тву­ет вер­сия дам­пера спе­циаль­но для Agile: SimpleMSILDecryptorForAgile, которая осно­вана имен­но на упо­мяну­том выше прин­ципе инъ­екции сво­его кода в clrjit, но мы поп­робу­ем дой­ти до это­го сво­им путем.

Итак, испы­тав все спо­собы, заг­ружа­ем нашу прог­рамму в x64dbg и, как было опи­сано выше, ста­вим бряк на CILJit::compileMethod. Нес­коль­ко раз бряк сра­баты­вает нор­маль­но, прав­да подава­емый на вход ком­пилиру­емый код методов не отли­чает­ся от исходно­го, который мы видели в деком­пилято­ре. И вдруг вне­зап­но счастье закан­чива­ется, прог­рамма мол­ча завер­шает­ся. Похоже, Agile оправды­вает свою репута­цию, активно борясь с отладчи­ком.

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

Смот­рим более вни­матель­но, где имен­но мы оста­нови­лись. Ага, вызов clrjit::CompileMethod из той самой кас­томной DLL-ки. Смот­рим по сте­ку вызовов, отку­да мы сюда попали. На наше счастье, все­го в одном вло­жении выше идет вызов инжекти­рован­ной фун­кции про­тек­тора из clr.dll. Мы наш­ли вход и выход рас­шифров­щика IL-кода. Cта­вим на них две точ­ки оста­нова, бла­го анти­отладчик борет­ся толь­ко с бря­ками на ори­гиналь­ный clrjit::CompileMethod, пос­ле чего переза­пус­каем прог­рамму. Попут­но ста­вим сох­ранение в лог зна­чений CORINFO_METHOD_INFO*info и BYTE*ILCode на вхо­де и выходе из инъ­екции.

Пос­коль­ку все точ­ки оста­нова рас­положе­ны вне clrjit, анти­отладчик нам уже не меша­ет. С момен­та запус­ка инжекти­рован­ного ком­пилято­ра два раза он сра­баты­вает вхо­лос­тую — исходный IL-код переда­ется на ори­гиналь­ную ком­пиляцию без изме­нений. А вот на тре­тий уже инте­рес­но: ука­затель на ILCode в струк­туре info под­менен на новый блок памяти раз­мером 0x100000, который, в прин­ципе, уже мож­но дам­пить для иссле­дова­ния. Оста­вим его за рам­ками нашей статьи, оста­новим­ся толь­ко на паре момен­тов.

Преж­де все­го про­верим, какой имен­но метод под­менен про­тек­тором. На самом деле задача не такая прос­тая, как может показать­ся. В струк­турах, подава­емых на вход CompileMethod, име­ются толь­ко парамет­ры ком­пилиру­емо­го бло­ка кода, но нет ни ука­зате­ля на имя метода, ни даже его индекса в таб­лице методов. А ведь это при­дет­ся делать, если мы хотим написать свой собс­твен­ный дам­пер. Пря­мой спо­соб — исполь­зовать сле­дующий метод интерфей­са ICorStaticInfo:

virtual const char* getMethodName(

CORINFO_METHOD_HANDLE ftn, /* IN */

const char **moduleName /* OUT */

) = 0;

Здесь задей­ство­ван параметр comp и хендл метода ftn, одна­ко из отладчи­ка это сде­лать зат­рудни­тель­но, поэто­му мы слег­ка схал­турим. Дело в том, что хендл ftn (пер­вое двой­ное сло­во в струк­туре CORINFO_METHOD_INFO), если его исполь­зовать как ука­затель, ука­зыва­ет на оди­нар­ное сло­во — индекс метода в .NET-метада­те EXE-модуля. В нашем при­мере это 0x23=35. Откры­ваем CFFExplorer и находим метод — Main. В ори­гина­ле он занима­ет 1 байт ret, одна­ко на выходе он поп­равил­ся до 0x1A байт. Попут­но мы наш­ли и раз­вилку в коде, филь­тру­ющую внеш­ние методы, которые тран­зитом переда­ются в ори­гиналь­ный ком­пилятор без изме­нений, а так­же сам код пре­обра­зова­ния и замены:

730B902E | mov ecx,dword ptr ds:[edx+C]

730B9031 | mov dword ptr ss:[ebp-190],ecx

730B9037 | cmp dword ptr ss:[ebp-19C],0

730B903E | je 730B90BF <-----------------------

730B9040 | mov eax,dword ptr ss:[ebp-18C]

730B9046 | push eax

730B9047 | mov ecx,dword ptr ss:[ebp-48]

730B904A | push ecx

730B904B | lea edx,dword ptr ss:[ebp-3C]

730B904E | push edx

730B904F | mov ecx,dword ptr ss:[ebp-4]

730B9052 | call 730B1361

730B9057 | push 1C

730B9059 | push 0

730B905B | call dword ptr ds:[<&GetProcessHeap>]

730B9061 | push eax

730B9062 | call dword ptr ds:[<&RtlAllocateHeap>]

730B9068 | mov dword ptr ss:[ebp-38],eax

730B906B | mov eax,dword ptr ss:[ebp-3C]

730B906E | push eax

730B906F | mov ecx,dword ptr ss:[ebp-38]

730B9072 | push ecx

730B9073 | call 730B1104

730B9078 | mov eax,dword ptr ss:[ebp+10]

730B907B | mov dword ptr ss:[ebp-1A0],eax

730B9081 | mov eax,dword ptr ss:[ebp+10]

730B9084 | mov ecx,dword ptr ss:[ebp-38]

735D9087 | mov edx,dword ptr ds:[ecx+C]

735D908A | mov dword ptr ds:[eax+8],edx <-- edx-указатель на новый IL-код метода Main

ЗАКЛЮЧЕНИЕ

И на закус­ку по тра­диции вспом­ним о нашей «Эниг­ме», которой были пос­вящены две мои пре­дыду­щие статьи («Ло­маем защиту при­ложе­ний Enigma x64 акту­аль­ных вер­сий» и «Как сло­мать trial, защищен­ный Enigma Protector»). Как я уже говорил, с ней сов­сем все пло­хо: дот­нетов­ские метадан­ные упря­таны глу­боко в упа­кован­ный и зашиф­рован­ный код. Соот­ветс­твен­но, при­ложе­ние не рас­позна­ется как дот­нетов­ское, не гру­зит­ся нор­маль­но в отладчи­ки и, понят­ное дело, не дам­пится. При­чем анти­отладчик там нас­толь­ко серь­езный, что не поз­воля­ет прос­то так взять и уста­новить бряк на CompileMethod (да и вооб­ще никуда не поз­воля­ет), а ведь нам для реали­зации опи­сан­ного метода нуж­на точ­ка оста­нова не прос­то в этом мес­те, а имен­но в момент JIT-ком­пиляции основно­го кода при заг­рузке при­ложе­ния.

Все выг­лядит доволь­но‑таки страш­но, одна­ко поп­робу­ем зай­ти с дру­гой сто­роны. В статье «Лома­ем защиту при­ложе­ний Enigma» я упо­минал, что во вре­мя работы при­ложе­ния рас­шифро­ван­ные сек­ции при­сутс­тву­ют в памяти про­цес­са. В дот­нетов­ских прог­раммах для ана­лиза обыч­но необ­ходимы две сек­ции: .text, где содер­жатся метадан­ные и IL-код, а так­же .rsrc с ресур­сами. Поп­робу­ем поис­кать эти сек­ции в памяти про­цес­са.

За мас­ку поис­ка сек­ции .text возь­мем, к при­меру, имя потока "#Strings", содер­жащего в себе спи­сок строк со слу­жеб­ной информа­цией: наз­вания клас­сов, методов и атри­бутов.

Та­ких вхож­дений обна­ружи­вает­ся немало (по чис­лу заг­ружен­ных .NET-биб­лиотек). Филь­тру­ем их по заголов­кам .NET-метадан­ных и по Assembly.Name опре­деля­ем имя модуля. Для поис­ка сек­ции ресур­сов мож­но исполь­зовать какую‑нибудь стро­ку из манифес­та, нап­ример <assembly xmlns=. При­над­лежность най­ден­ной сек­ции иден­тифици­руем по ProductName.

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

Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei



Report Page