[Перевод] Аппаратный взлом жёсткого диска

[Перевод] Аппаратный взлом жёсткого диска

Жёсткие диски: если вы читаете эту статью, то с большой вероятностью у вас есть одно или несколько таких устройств. Они довольно просты и, по сути, представляют собой набор 512-байтных секторов, пронумерованных возрастающими адресами, также называемыми LBA (Logical Block Address). Компьютер, к которому подключен жёсткий диск (hard drive, HD), может считывать и записывать данные в эти сектора. Обычно используется файловая система, абстрагирующая все эти сектора до файлов и папок.

Неспециалисту может показаться, что оборудование HD должно быть довольно простым: достаточно всего лишь устройства, подключаемого к порту SATA, которое может позиционировать свои головки чтения/записи и считывать или записывать данные на пластины. Однако их работа намного сложнее: разве жёсткие диски не занимаются обработкой сбойных блоки и атрибутов SMART, и не имеют кэша, с которым тоже каким-то образом нужно работать?

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

Элементы печатной платы

Чтобы разобраться, возможно ли взломать жёсткие диски, мне сначала нужно было лучше их узнать. К счастью, как и у большинства из вас, у меня накопилась целая куча старых и/или сломанных жёстких дисков для изучения:

Разумеется, все мы знаем, как должны работать механические детали жёстких дисков, но меня интересовали не они. Мне важна была небольшая печатная плата, находящаяся на обратной стороне HD, где расположены разъёмы SATA и питания. Вот как выглядят подобные печатные платы:

Видно, что на плате есть примерно четыре чипа. Вот что мне удалось о них узнать:

Это память DRAM. С ней всё проще всего, спецификации находятся легко. Эти чипы могут иметь объём от 8 МБ до 64 МБ, и этот размер связан с размером кэша, который должен иметь жёсткий диск.

Это контроллер двигателя привода шпинделя. Это нестандартная деталь, поэтому спецификации найти сложно, однако у некоторых контроллеров, похоже, существуют братья и сёстры, информация о которых чуть более доступна. Кажется, наиболее распространёнными являются контроллеры ST Smooth; кроме управления двигателем шпинделя они занимаются регулированием мощности и имеют несколько аналоговых/цифровых каналов.

Это флэш-память с последовательным интерфейсом. С ней всё тоже легко, она может иметь размер от 64 КБ до 256 КБ. Похоже, она используется для хранения программы, с которой запускается контроллер жёсткого диска. У некоторых жёстких дисков этого чипа нет, однако вместо него имеется флэш-память внутри чипа контроллера диска.

Эти небольшие устройства — не чипы, а пьезоэлектронные вибродатчики. Их можно использовать для перемещения головок в безопасное место, когда HD подвергается механическим колебаниям или ударам, но с наибольшей вероятностью они просто записывают куда-нибудь бит, обозначающий, что пользователь уронил HD и гарантия на него больше не действует.

А в этом элементе происходит всё самое интересное: это контроллер жёсткого диска. Они изготовляются Marvell, ST и некоторыми другими производителями больших интегральных схем. Некоторые компании-производители жёстких дисков изготавливают и собственные контроллеры: я замечал, что этим занимаются Samsung и Western Digital. Почти всё остальное не представляет никакой сложности, поэтому меня интересовало это устройство.

К сожалению, эти детали почти не имеют документации. Если бы я сказал, что компании-изготовители контроллеров не особо охотно делятся информацией о них, это было бы преуменьшением: они даже не упоминают существование этих деталей на своих сайтах! К сожалению, остальная часть Интернета тоже оказалась не очень полезной: поиск спецификаций приводит только на сайты спецификаций, где нет самих PDF, а также к загадочным китайским продавцам, утверждающим, что у них есть эти интегральные схемы.

Итак, у нас нет спецификаций на самые важные интегральные схемы, а значит, мы зашли в тупик… или нет?

Подключаем JTAG

К счастью, помимо спецификаций, существуют и другие способы поиска информации об этих ИС. В результате одного веб-запроса я нашёл нечто полезное.

На форумах HDDGuru я обнаружил тему пользователя Dejan. Ему каким-то образом удалось повредить внутреннюю флэш-память его жёсткого диска, и он захотел понять, существует ли способ загрузки контроллера с внешней флэш-памяти или способ перезаписи флэш-памяти. Ему не удавалось найти ответа в течение пяти дней, но парень оказался изобретательным: следующим постом он опубликовал сообщение, что он смог найти схему расположения выводов порта JTAG. Это важная находка: порт JTAG можно использовать для управления контроллером как марионеткой. Его можно останавливать, перезапускать, модифицировать память, устанавливать контрольные точки, и т.д. Затем Dejan разобрался, как создать дамп загрузочной ПЗУ контроллера, понял, что на одном из разъёмов жёсткого диска есть последовательный порт, и смог восстановить флэш-ПЗУ своего диска. Затем он выполнил дамп ещё нескольких битов и указателей о процессе обновления флэш, после чего окончательно пропал во мгле Интернета.

Всё это оказалось довольно полезной информацией: по крайней мере, это дало мне понять, что все контроллеры Western Digital, похоже, имеют ядро ARM, к которому можно получить доступ через порт JTAG. Из этой информации я также понял, что у жёстких дисков обычно есть последовательный порт, который чаще всего не используется, но может быть полезен для отладки моего хака. Теперь у меня должно быть достаточно информации, чтобы приступить к взлому.

Моя система выглядит так:

[Перевод] Аппаратный взлом жёсткого диска

Красная штука — это FT2232H, дешёвая плата, которую можно купить примерно за 30 долларов, на ней есть JTAG и последовательный порт, а также интерфейс SPI. Она подключена к интерфейсу JTAG жёсткого диска, а также как к разъёму, на котором у жёсткого диска есть последовательный порт. Жёсткий диск напрямую подключен к порту SATA на материнской плате моих компьютеров, а также к внешнему блоку питания ATX. Для управления JTAG я использовал ПО OpenOCD.

Теперь вопрос заключается в следующем: будет ли это работать на самом деле? Dejan проделал это с 2,5-дюймовым жёстким диском на 250 ГБ с контроллером 88i6745 и выяснил, что используется ядро arm9. Я взял 3,5-дюймовый диск на 2 ТБ с контроллером 88i9146, он имеет другой форм-фактор и чуть новее. К счастью, OpenOCD имеет возможность самостоятельно распознавать, что находится в цепочке JTAG. Вот что я обнаружил:

Это немного сбило меня с толку… Я ожидал одного tap, соответствующего одному ядру ARM… однако обнаружил три tap. Значит ли это, что у этого чипа три ядра ARM?

Проведя исследование, я выяснил, что это так, похоже, у чипа три ядра. Два Feroceon — довольно мощных ядра наподобие arm9, и ядро Cortex-M3 — ядро послабже, больше напоминающее микроконтроллер. Немного поэкспериментировав (и проведя дальнейшие исследования) я выяснил, что все контроллеры имеют собственные функции:

  • Feroceon 1 обрабатывает физическое чтение и запись с/на пластины жёсткого диска
  • Feroceon 2 управляет интерфейсом SATA
  • Feroceon 2 также работает с кэшем и занимается преобразованием из LBA в CHS
  • Cortex-M3 занимается… ничем? Мне удалось остановить его с сохранением всех функций жёсткого диска.

С какого же ядра начинать взлом? Моя задача заключалась в компрометации защиты системы с помощью модификаций прошивки жёсткого диска. Простейший способ реализации этого, который при этом, вероятно, сложнее всего обнаружить — изменение данных на лету. При этом данные на диске не нужно будет менять, а прошивка может просто сделать себя невидимой. Для этого мне нужно найти подходящее для такого перехвата ядро: мне требуется ядро, имеющее доступ к данным, находящимся в процессе переноса с диска на кабель SATA, которое при этом можно сфальсифицировать для модификации данных, пока они находятся между этими двумя точками.

Как эти данные попадают с пластин HD в интерфейс SATA? Здесь я воспользовался своей интуицией. Рассуждал я примерно так:

Если процессоры используют стандартное копирование памяти при частоте 150 МГц, то они бы могли достичь скорости в 150*23/2=2,4 Гбит/с, а на практике, скорее всего, гораздо меньше. По спецификации жёсткий диск имеет скорость 6 Гбит/с, поэтому, вероятно, используется какое-то аппаратное ускорение. Наиболее вероятным аппаратным ускорением будет применение DMA. Это означает, что данные копируются напрямую из логики чтения головок в память без активного участия процессора. То же самое относится и к порту SATA: процессору нужно просто указывать, где находятся данные, а логика DMA занимается считыванием данных напрямую из памяти.

Если это было так, то где бы располагалась память, на которую бы указывал движок DMA? Неплохим кандидатом является кэш жёсткого диска: считываемые с диска данные всё равно попадают в кэш, поэтому логично будет копировать их туда сразу после чтения с диска. Ранее я выяснил, что за работу с кэшем отвечает Feroceon 2, поэтому он стал основной целью моей попытки взлома.

Итак, я пришёл к выводу, что данные считываются и записываются с помощью DMA, без участия процессора. Теперь следующий вопрос: даже если процессоры не должны касаться данных при обычной работе, могут ли они получать к ним доступ? Для ответа на этот вопрос я сначала воспользовался подключением JTAG и дизассемблированием, чтобы разобраться с привязкой к памяти второго Feroceon:

Как видите, привязка к памяти немного фрагментирована. Разбросаны небольшие части ОЗУ, есть пространство IO (ввода-вывода) и IRQ (прерываний), а также немного внутреннего загрузочного ПЗУ. Здесь есть также большой сегмент на 64 МБ того, что, по моему мнению, является чипом DRAM с кэшем на нём. Давайте выясним, так ли это. Сначала я смонтировал диск на свою машину и записал в файл на нём «Hello world!». Смогу ли я найти эту строку в области памяти 64 МБ?

Да, вот и она. Похоже, процессоры Feroceon имеют доступ к кэшу, а сам он привязан к области DRAM 64 МБ.

Инъекция кода

Разумеется, если бы я хотел что-то менять в кэше, то не сканировал бы каждый раз целиком 64 МБ ОЗУ: мне нужно было понять, как работает кэш. Для этого мне нужно было создать дамп прошивки жёсткого диска, дизассемблировать его и понять, чтобы разобраться в функциях кэширования.

Дизассемблирование этой прошивки — нетривиальная задача. Во-первых, в коде перемешаны инструкции ARM и инструкции в стиле thumb. Это раздражает, а у меня нет дизассемблера, способного автоматически переключаться между двумя этими наборами. Кроме того, в коде отсутствует то, что очень упрощает дизассемблирование ПО: обычно процедуры закодированы так, чтобы выводить сообщения типа «Couldn’t open logfile!», когда что-то идёт не так. Эти сообщения оказывают огромную помощь, когда разбираешься в том, для чего нужна процедура. Однако в этой прошивке не было таких строк: приходилось разбираться в назначении процедуры только по её коду. Однако кодовая база кажется немного устаревшей, и иногда дизассемблированный код выглядит так, как будто часть функций к нему «прикрутили» позже, что чуть усложнило анализ.

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

После длительного изучения кода и запуска отладчика для проверки моих догадок мне удалось добраться до ядра системы кэширования: таблицы в ОЗУ, которую я назвал «таблицей дескрипторов кэша»:

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

Теперь, когда я раскрыл тайну таблицы дескрипторов кэша, смогу ли я перехватывать запись на диск до того, как она отправится из порта SATA к компьютеру? Чтобы сделать это, мне понадобится возможность исполнения собственного кода в контроллере жёсткого диска. Более того, мне нужно обеспечить запуск кода в нужное время: если я изменю кэш слишком рано, то данные в него ещё не попадут; если я изменю его слишком поздно, то данные уже отправятся на компьютер.

Реализовал я это подключением к уже имеющейся процедуре. Мой хак будет выполняться на Feroceon 2, а этот процессор занимается всей передачей по SATA, поэтому в нём должна быть какая-то процедура, отвечающая за настройку оборудования SATA для получения данных из кэша. Если я найду эту процедуру, то, вероятно, смогу выполнять перед ней собственный код.

После долгих исследований, установки контрольных точек, проб и ошибок я наконец нашёл соответствующую моим требованиям процедуру. Я модифицировал её, чтобы она сначала выполняла мой код. Вот оригинальный код:

000167BE ; r0 - slot in sata_req
000167BE sub_0_167BE:
000167BE                 PUSH    {R4-R7,LR}
000167C0                 MOVS    R7, R0
000167C2                 LSLS    R1, R0, #4
000167C4                 LDR     R0, =sata_req
000167C6                 SUB     SP, SP, #0x14
000167C8                 ADDS    R6, R1, R0
000167CA                 LDRB    R1, [R6,#0xD]
000167CC                 LDR     R2, =stru_0_40028DC
000167CE                 STR     R1, [SP,#0x28+var_1C]
000167D0                 LDRB    R0, [R6,#(off_0_FFE3F108+2 - 0xFFE3F0FC)]
000167D2                 LDRB    R5, [R6,#(off_0_FFE3F108 - 0xFFE3F0FC)]
000167D4                 LSLS    R0, R0, #4

А вот что происходит, когда в код добавляется вызов моего кода:

000167BE ; r0 - slot in sata_req
000167BE sub_0_167BE:
000167BE                 PUSH    {R4-R7,LR}
000167C0                 MOVS    R7, R0
000167C2                 LD      R6, =hookedAddr
000167C4                 BX      R6
000167C6                 .dw     checksumFix
000167C8                 .dd     hookedAddr
000167CC                 LDR     R2, =stru_0_40028DC
000167CE                 STR     R1, [SP,#0x28+var_1C]
000167D0                 LDRB    R0, [R6,#(off_0_FFE3F108+2 - 0xFFE3F0FC)]
000167D2                 LDRB    R5, [R6,#(off_0_FFE3F108 - 0xFFE3F0FC)]
000167D4                 LSLS    R0, R0, #4
...
FFE3F000                 PUSH    {R0-R12, LR}
FFE3F004                 BX      changeThingsInCache
FFE3F008                 POP     {R0-R12, LR}
FFE3F00C                 LSLS    R1, R0, #4
FFE3F010                 LDR     R0, =sata_req
FFE3F014                 SUB     SP, SP, #0x14
FFE3F018                 ADDS    R6, R1, R0
FFE3F01C                 LDRB    R1, [R6,#0xD]
FFE3F020                 BX      0x167CC

Как видите, некоторые исходные инструкции были заменены на переход к новому коду в ранее неиспользовавшейся области ОЗУ по адресу 0xFFE3F000 и дополнительное слово, чтобы гарантировать правильность контрольной суммы области кода. Если бы я этого не сделал, то HD попытался бы загрузить резервную копию со своих пластин, а нам этого не нужно. Код, к которому выполняется переход, исполняет процедуру под названием changeThingsInCache, а затем делает то, что бы сделал заменённый код. Затем он продолжает исполнение в исходной процедуре, как будто ничего не произошло.

Теперь мне достаточно написать процедуру для изменения кэшированных данных. Для первого теста я решил написать процедуру, которая в псевдокоде выполняла бы нечто подобное:

void hook() {
  foreach (cache_struct in cache_struct_table) {
    if (is_valid(cache_struct)) {
      foreach (sector in cache_struct.sectors) {
        sector[0]=0x12345678;
      }
    }
  }
}

Этот небольшой фрагмент кода при каждом его вызове заменяет первые 4 байта каждого сектора на 0x12345678, поэтому если бы я загрузил это всё на жёсткий диск, то должен был бы увидеть это число в начале каждого считываемого сектора. Я загрузил фрагменты кода по JTAG…

И вот результат:

Сохранность кода

Разумеется, я мог превратить это в завершённый хак, но из-за необходимости подключения JTAG для записи в ОЗУ при каждом запуске жёсткого диска он становится довольно бесполезным. Мне нужно было сделать его сохраняемым, то есть хранить мои модификации в каком-то месте, откуда их можно было бы брать при каждом включении жёсткого диска.

Для этого я выбрал флэш-ПЗУ. Вероятно, я бы мог поместить его куда-нибудь в зарезервированные сектора самого жёсткого диска, но если бы я что-то перепутал, то не смог бы восстановить диск. Чип флэш-памяти — это просто стандартная деталь с восемью выводами, поэтому я могу запросто извлечь её, прошить и вставить снова. Для этого я отпаял её и подключил к монтажной плате, чтобы можно было легко переключаться между программатором и жёстким диском:

Что же нам записать во флэш-память? К счастью, хранящийся в чипе формат уже был разобран: он состоит из множества блоков данных, а в самом начале находится описывающая их таблица. Эта таблица описывает местоположение блока во флэш-памяти, способ его сжатия (если он сжат), место, в которое блок должен помещаться в ОЗУ, а для последнего адреса — точка исполнения, в которую загрузчик должен перейти для исполнения программы.

К сожалению, я не мог модифицировать находящийся на флэш-памяти код; биты, содержавшие части, в которые я хотел записать мои хуки, были сжаты неизвестным алгоритмом сжатия, поэтому изменять их было нельзя. Однако я мог добавить дополнительный блок и изменить адрес исполнения, чтобы этот блок мог исполняться перед остальными. Это сильно упростило работу: когда начинал исполняться «мой» блок, я мог просто закодировать его, чтобы он вставил хуки в уже распакованные биты кода.

Разумеется, для этого мне нужно было дизассемблировать и снова собрать двоичный файл флэш-памяти. Я создал для этого инструмент и дал ему скучное название «fwtool». Этот инструмент может дампить разные блоки флэш-памяти, а также транслировать заголовок в текстовый файл для простоты редактирования. Затем можно модифицировать, удалить или добавить блок, а затем собрать всё снова в единый файл прошивки, готовый к записи во флэш-память. Я использовал его для добавления в образ собственного фрагмента кода, прошил всё обратно в чип, припаял чип к жёсткому диску, снова всё запустил и в результате получил вот это:

Это неудивительно: именно такой результат я и получал раньше. Хитрость заключается в том, что теперь для этого не требуется модификация через JTAG.

Прошиваем ПО

Хотя модификация флэш-памяти стала хорошим шагом вперёд, я по-прежнему не мог реализовать свой воображаемый хакерский сценарий: не думаю, что какая-то серверная компания принимает «пожертвования» в виде жёстких дисков с отпаянными и заново припаянными флэш-чипами. Мне нужно было найти способ перепрошивки чипа, когда он припаян к жёсткому диску, желательно с компьютера, к которому подключен диск.

Инструменты обновления прошивок Western Digital доказывают, что это возможно: по сути, это инструмент, запускаемый под DOS для записи новой прошивки во флэш-память и в сервисную область, то есть на зарезервированные сектора жёсткого диска. По информации из Интернета, для этого в инструменте используются так называемые Vendor Specific Commands. Существуют и другие инструменты, способные модифицировать прошивки: например, есть экспериментальный код, способный использовать незанятые зарезервированные сектора для сокрытия данных. Кроме того, есть набор инструментов под названием idle3-tools, которые можно использовать для модификации байта в прошивке, чтобы изменить поведение жёсткого диска в режиме ожидания. В этом коде тоже используются VSC при помощи «официального» способа с применением ioctls scsi-драйвера Linux. Я решил «позаимствовать» этот код, немного его изменить и интегрировать в fwtool. После экспериментов с кодом и подбора параметров VSC моя программа fwtool внезапно научилась считывать и записывать флэш-память жёсткого диска, подключенного к компьютеру, на котором она запущена.

На этом моя атака была завершена. Если бы blackhat-хакер каким-то образом получил бы root-доступ к серверу с этим диском, то он бы смог использовать fwtool для удалённого создания дампа флэш-памяти на диск, модифицировать его и снова прошить в память. Рано или поздно владелец компьютера выяснил бы, что я использую его машину с вредоносными целями, и, вероятно, переустановил бы систему, закрыв лазейку, через которую хакер проник изначально.

Однако после установки прошивки нападающий смог бы приказать жёсткому диску сделать что-нибудь вредоносное с новой установленной системой. Сначала бы ему нужно было запустить подобное поведение, и это можно было бы реализовать определённой магической строкой, которую бы хак прошивки искал на диске. Магическая строка может быть любым файлом; например, нападающий мог бы загрузить на сервер файл .jpeg со строкой в нём. Также он мог бы запросить файл с веб-сервера через магическую строку, присоединённую к URL. Затем бы она появилась в логах машин, что привело бы к срабатыванию эксплойта.

Затем хак прошивки жёсткого диска сделал бы нечто вредоносное. Например, он мог бы подождать, пока машина не начнёт считывать файл /etc/shadow, где в системе Unix/Linux хранятся все пароли, и модифицировать его содержимое на лету, заменив его чем-то, что хакер прописал заранее. Затем нападающий мог бы попробовать войти в систему с собственным паролем, а машина бы сравнила этот пароль с модифицированным /etc/shadow, позволив нападающему снова войти в систему.

Вот демонстрация, которую я сделал для презентации. Вы видите, как я безуспешно пытаюсь войти в аккаунт root компьютера. Затем я включаю хак и передаю ему хэш нового пароля, а именно пароля «test123». Так как Linux кэширует файл shadow (как и все файлы, к которым недавно выполнялся доступ), мне придётся генерировать сильную дисковую активность, чтобы «вытеснить» файл из кэша; благодаря этому, когда я снова попытаюсь выполнить вход, Linux будет вынужден снова получить файл shadow с диска. Наконец, благодаря очистке кэша я смогу просто выполнить вход в аккаунт root с фальшивым паролем test123.

Другие способы применения

Разумеется, восстановление доступа к серверам, у которых были закрыты тайные способы входа — не единственный полезный способ применения моей работы по реверс-инжинирингу. Её можно использовать и в целях защиты.

Например, можно создать неклонируемый жёсткий диск: жёсткий диск будет функционировать обычным образом, если паттерн доступа к секторам является случайным, аналогично тому, как к файловой системе обращается ОС. Если доступ к диску выполняется только последовательно, как это обычно делает утилита клонирования дисков, то жёсткий диск может искажать данные, делая клон отличным от оригинала.

Контроллер диска интересен и просто как плата контроллера. У него есть три довольно мощных процессорных ядра, к которым подключён достаточно большой объём ОЗУ. Также у него есть uart для последовательного порта и не менее двух интерфейсов SPI; один для флэш-ПЗУ, второй — для контроллеров двигателя шпинделя. Можно загружать код для процессора, обновляя данные во внешнем флэш-чипе или даже при помощи последовательного в загрузчике. Чтобы продемонстрировать мощь чипа, я портировал на свой жёсткий диск довольно популярное ПО. Демо является только проверкой концепции, единственное работающее периферийное устройство — это последовательный порт, а пользовательского пространства пока нет. Тем не менее, я всё равно немного горд тем, что установил на свой жёсткий диск Linux. Сверху расположена стандартная командная строка (жёсткий диск смонтирован в /mnt), снизу — вывод моей работы с последовательным портом жёсткого диска:

Расскажу немного подробнее о том, что здесь происходит: ядро и init каждый упакованы во фрагменты размером ровно в один сектор, а к их началу добавлены магическая строка и порядковый номер. При считывании файла с диска он оказывается в кэше диска. Запись магической строки «HD, lnx!» заставляет модифицированную прошивку искать в кэше все сектора, пересобрать образ ядра и загрузить его. Ядро собрано для процессора без MMU (у контроллера диска его нет) и оно имеет только драйвер для последовательного порта. К сожалению, для ядра без MMU требуется ещё и специально отформатированное пользовательское пространство. Мне не удалось его скомпилировать, поэтому ядро в конце концов выдаёт kernel panic, потому что не может найти init, который оно смогло бы исполнить.

 

Источник

JTAG, SATA, взлом жесткого диска, жёсткие диски, обратная разработка, перехват данных, прошивки

Читайте также