Мы живем в то самое замечательное время, когда космические корабли бороздят просторы Большого театра, а каждая домохозяйка в курсе последних трендов из области связи. И даже если технические аспекты ей вряд ли интересны, то как посмотреть котиков через Wi-Fi - она уж точно знает. Сегодня мы сотворим небольшое приложение, которое будет полезно как увлеченным хакерам, занимающимся вардрайвингом, так и суровым любителям домашних животных.

 

Тактико-технические характеристики

Темой нашего сегодняшнего изыскания будет приложение-детектор точек доступа Wi-Fi, работающее в среде Android. Так как беспроводное оборудование, работающее на частоте 5 ГГц, не сильно распространено, ограничимся диапазоном 2 ГГц. Кроме того, будем особо отмечать, во-первых - открытые (незапароленные) точки доступа, во-вторых - имеющие устаревшее, а значит слабое шифрование WEP, и в-третьих — точки, поддерживающие технологию WPS. Об уязвимостях последней можно почитать в мартовском номере «Хакера» за 2012 год. Наконец, чтобы сделать задачу еще более интересной, нарисуем в качестве GUI экран «настоящего» радара. Одним словом, смотри рис. 1. В качестве среды разработки будем использовать Android Studio.

Глоссарий

  • WAP (беспроводная точка доступа) — устройство для объединения компьютеров в единую беспроводную сеть (обычно встраивается в роутер).
  • WPS (Wi-Fi Protected Setup) — стандарт полуавтоматического создания беспроводной сети Wi-Fi (посредством нажатия кнопки)
  • WEP (Wired Equivalent Privacy) — алгоритм для обеспечения безопасности сетей Wi-Fi, основан на поточном шифровании RC4.
  • WPA (Wi-Fi Protected Access) — замена WPA c усовершенствованной схемой шифрования RC4, обязательной аутентификацией с EAP.
  • WPA2 — замена WPA, усилено шифрование за счет ССMP и AES.
  • TKIP — протокол целостности временного ключа, в отличие от WEP использует более эффективный механизм управления ключами, но тот же самый алгоритм RC4 для шифрования данных.
  • CCMP — протокол блочного шифрования с кодом аутентичности, созданный для замены TKIP.
  • AES — симметричный алгоритм блочного шифрования, принятый в качестве стандарта шифрования правительством США.
  • EAP — расширяемый протокол аутентификации клиентов.

 

Слушаем Wi-Fi-радио

Рис. 1. Порыбачим
Рис. 1. Порыбачим

Использование технологии Wi-Fi предполагает наличие беспроводной точки доступа (WAP) и не менее одного клиента. Каждая WAP должна иметь два идентификатора — SSID и BSSID. Первый определяет имя точки и, соответственно, сети (например, home, AKADO_1742, FREE_WIFI) и задается непосредственно пользователем (последний тренд — именовать сеть иероглифами), второй, как правило, содержит MAC-адрес точки.

Каждые 100 мс WAP передает в эфир (широковещательно) свой идентификатор SSID, с помощью которого клиент может узнать о существовании этой точки доступа, используемых технологиях шифрования, ну и, конечно, подключиться к ней. Если SSID не рассылается, такая точка доступа называется скрытой и, чтобы подключиться к ней, клиент должен знать ее имя изначально. Нужно сказать, что «скрытность» не служит сколько-нибудь значимой защитой от злоумышленника, так как любой снифер трафика без труда покажет SSID.

Для защиты от несанкционированного использования точки доступа применяются разные виды аутентификации и шифрования: WEP, WPA, WPA2, TKIP/AES и тому подобные. Самый первый из них, WEP, в настоящее время считается технически устаревшим и не рекомендован к использованию, хотя иногда применяется для объединения в сеть совсем уж бородатых устройств. Взлом WEP можно осуществить всего за несколько минут. WPA, пришедший на смену WEP, сломать уже сложнее, хотя в 2008 году на конференции PacSec был представлен способ компрометации ключа TKIP в WPA (за разъяснением терминов отсылаю тебя к врезке) всего за четверть часа. Наконец, самой защищенной (на сегодняшний день, разумеется) технологией является WPA2 с алгоритмом шифрования AES.


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

Когда приходят гости — это хорошо, когда приходят гости со смартфонами — это тоже ничего так, а вот когда приходят гости со смартфонами и просят Wi-Fi — это уже не круто. Давать им пароль от точки доступа как-то не хочется, ведь это вторжение в личную сетевую жизнь, а отказывать — совсем уж не комильфо. Специально для этих целей в некоторых маршрутизаторах (aka роутерах) предусмотрено создание гостевой сети. Такая сеть существенно ограничена в скорости доступа в интернет, но открыта для подключения всем желающим. Единственным сдерживающим фактором служит белый (или черный ;)) список MAC-адресов смартфонов гостей, но собирать его — то еще занятие, да и смена MAC не такая уж экзотика. В итоге, как ни прискорбно для одних, найти открытую сеть к радости других можно.

Бойся желаний своих

Для работы с модулем Wi-Fi необходимо запросить у пользователя разрешение для чтения и изменения подключений:

<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>

Кроме того, Android позволяет указать конкретную аппаратную особенность, которая необходима для установки приложения. В нашем проекте уместно требовать наличие аппаратного модуля Wi-Fi:

<uses-feature android:name="android.hardware.wifi"/>

Если Wi-Fi-модуль на устройстве выключен, следующий код его оживит:

if (!wifi.isWifiEnabled())
 if (wifi.getWifiState() != WifiManager.WIFI_STATE_ENABLING)
  wifi.setWifiEnabled(true);

Тем не менее негласный кодекс хорошего программиста, создающего хорошие программы, рекомендует предварительно спрашивать у пользователя разрешения включить Wi-Fi.

 

Wi-Fi-класс

Рис. 2. Проект в Android Studio
Рис. 2. Проект в Android Studio

Для хранения информации обо всех найденных беспроводных точках доступа напишем специальный класс:

public class WiFiAP {
    private String SSID;
    private String BSSID;
    private int Channel;
    private int Level;
    private int Security; // 0 — открытая сеть; 1 — WEP; 2 — WPA/WPA2
    private boolean WPS;  // WPS?
    public WiFiAP(String SSID, String BSSID, int Channel, int Level, int Security, boolean WPS) {
        this.SSID = SSID;
        this.BSSID = BSSID;
        this.Channel = Channel;
        this.Level = Level;
        this.Security = Security;
        this.WPS = WPS;
    }
    ...
    // Геттеры
}

где Channel — канал, на котором вещает точка, а Level — уровень сигнала (0 — минимальный, 100 — максимальный). Кстати, допустимый частотный диапазон, который определяет номер канала, в разных странах неодинаков. Например, в России на сертифицированном оборудовании разрешены к использованию тринадцать каналов, тогда как во Франции только четыре, в США — одиннадцать. Для преобразования частоты в номер канала воспользуемся функцией

private final static ArrayList<Integer> channelsFrequency = new ArrayList<Integer>(
        Arrays.asList(0, 2412, 2417, 2422, 2427, 2432, 2437, 2442, 2447,
                      2452, 2457, 2462, 2467, 2472, 2484));
public static int getChannelFromFrequency(int frequency) {
    return channelsFrequency.indexOf(Integer.valueOf(frequency));
}
 

Wi-Fi-приемник

За работу с Wi-Fi в Android отвечает класс WifiManager, представляющий собой системный сервис. Для доступа к нему необходимо использовать константу Context.WIFI_SERVICE:

wifi = (WifiManager)getSystemService(Context.WIFI_SERVICE);

Для запуска процесса сканирования точек доступа используется метод wifi.startScan(). Результаты сканирования будут транслироваться в асинхронном режиме, поэтому для их получения и обработки нужно предусмотреть специальный широковещательный приемник, обрабатывающий системное намерение со стандартным действием SCAN_RESULTS_AVAILABLE_ACTION.

 ArrayList<WiFiAP> WiFiAPs = new ArrayList<WiFiAP>();
 ...
 BroadcastReceiver scanReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        List<ScanResult> results = wifi.getScanResults();
        if (results == null) return;
        WiFiAPs.clear();
        for (ScanResult result : results) {
         WiFiAP AP = new WiFiAP(result.SSID,
                                result.BSSID,
                                getChannelFromFrequency(result.frequency),
                                WifiManager.calculateSignalLevel(result.level, maxSignalLevel),
                                getSecurity(result.capabilities),
                                getWPS(result.capabilities));
         WiFiAPs.add(AP);
        }
        ...
    }
};

В методе onReceive работает цикл, добавляющий в массив WiFiAPs все найденные точки: параметры result.SSID и result.BSSID содержат уже готовые к употреблению строки, а вот с остальными нужно поработать. Так, result.frequency возвращает частоту, на которой работает точка доступа, но мы, как радиолюбители без стажа, привыкли работать с номером канала, поэтому вычисляем соответствующее значение — getChannelFromFrequency(result.frequency). Уровень сигнала, содержащийся в result.level, определен как уровень затухания сигнала (в децибелах) между клиентом и точкой доступа, и для нормализации этого значения в классе WifiManager предусмотрен метод calculateSignalLevel, принимающий в качестве второго параметра условный максимальный уровень (в нашем случае — 100).

Параметр result.capabilities содержит строку с поддерживаемыми методами аутентификации и шифрования. В качестве примера она может иметь вид:

[WPA2-PSK-CCMP][ESS]
[WPA-PSK-TKIP][WPS]
[WEP]
[ESS]

Нас интересуют «хеш-теги»: WPA, WPA2, WEP, WPS — именно их пытаются обнаружить методы getSecurity и getWPS и возвращают результаты в поля Security и WPS соответственно.

Для работы широковещательный приемник должен быть зарегистрирован в системе. Существует два метода регистрации — в файле-манифесте проекта AndroidManifest.xml (этот вариант мы рассматривали несколько номеров назад) и непосредственно в коде. Обычно приемники, влияющие на пользовательский интерфейс конкретной активности, регистрируются внутри обработчика onResume и отменяются во время срабатывания onPause:

@Override
public void onResume() {
    super.onResume();
    IntentFilter filter = new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
    registerReceiver(scanReceiver, filter);
}
@Override
public void onPause() {
    unregisterReceiver(scanReceiver);
    super.onPause();
}

Итак, у нас есть список доступных (если повезет — очень доступных) беспроводных точек доступа. Осталось только все это красиво упаковать, чем мы и займемся далее.

 

Творим GUI

Так как Google в ближайшее время вряд ли реализует компонент типа «радар» в своем SDK, создадим его сами.

В Андроиде все визуальные компоненты называются представлениями (View). Чтобы соорудить новый элемент с чистого листа, необходимо наследовать один из двух классов — либо View, либо SurfaceView. Их принципиальные отличия приведены во врезке, а для нашей задачи вполне сгодится View.

View vs. SurfaceView

Для рисования в Андроиде предусмотрены два класса поверхностей — View и SurfaceView. В большинстве приложений представления (например, нестандартные компоненты) рисуются в главном графическом потоке. Но в этом же потоке работают активности, приемники и даже сервисы. Вынести код отрисовки в отдельный поток при использовании View принципиально невозможно, так как любое изменение элементов графического интерфейса из фонового потока в Андроиде явно запрещено.

Выйти из сложившейся ситуации поможет применение объекта SurfaceView, который поддерживает рисование из фоновых потоков, что делает SurfaceView идеальным решением для ресурсоемких задач с быстрым обновлением экрана (например, игр). Кроме того, SurfaceView предоставляет поверхность (Surface), где можно отображать сложные сцены, используя весь арсенал пакета OpenGL ES 1 и 2/3.

INFO


Доработанное приложение Wi-Fi Radar доступно для тестирования в Google Play.

Класс View представляет собой пустую область экрана (канву) размером 100 × 100 пикселей. Для корректировки размера необходимо переопределить метод onMesure, а для рисования на канве — onDraw. Внутри первого вычисляется высота и ширина канвы, которая в нашем случае будет соответствовать размеру экрана Android-устройства; внутри второго мы будем рисовать экран нашего детектора и выводить точки доступа. Поскольку физический размер экрана может быть любым, все геометрические параметры должны не привязываться к абсолютным значениям, а вычисляться относительно какой-либо величины (например, радиуса радара). Каркас нашего класса представлен в листинге (как всегда, полный исходник ищи на dvd.xakep.ru):

public class RadarView extends View {
    ...
    public RadarView(Context context) {
        super(context);
        setupView(); // Инициализируем объекты для рисования
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int measuredWidth = measure(widthMeasureSpec);
        int measuredHeight = measure(heightMeasureSpec);
        int d = Math.min(measuredWidth, measuredHeight);
        setMeasuredDimension(d, d);
    }
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // Определяем шейдеры
        ...
    }
    @Override
    protected void onDraw(Canvas canvas) {
       // Рисуем
       ...
    }
}

Метод onMeasure вызывается в момент, когда родительское представление (разметка активности) размещает внутри себя дочерний элемент (наш радар). Параметры widthMeasureSpec и heightMeasureSpec определяют доступное пространство, которое мы и займем, вызвав метод setMeasuredDimension. Так как наше представление — идеальный круг, который должен вписаться по ширине или высоте в экран (в зависимости от ориентации устройства), мы с помощью Math.min вычисляем длину короткой грани и передаем ее в качестве обоих параметров.

Для рисования на канве используются кисти (Paint), позволяющие как задавать свойства графических примитивов (цвет, узор, толщину линий, размер текста и прочее), так и указывать порядок отрисовки: шейдерный градиент, цветовые фильтры, контурные эффекты и так далее. Метод onDraw вызывается всякий раз при обновлении экрана, поэтому в нем нежелательно создание каких-либо объектов. Обычно все используемые объекты инициализируются в конструкторе представления, в нашем случае — в методе setupView (дублирующий код опущен):

protected void setupView() {
    // Сетка
    axisPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    axisPaint.setColor(Color.argb(0x60, 0xFF, 0xFF, 0xFF));
    axisPaint.setStyle(Paint.Style.STROKE);
    axisPaint.setStrokeWidth(1);
    // Текст и маркеры
    textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    textPaint.setFakeBoldText(true);
    textPaint.setSubpixelText(true);
    textPaint.setTextAlign(Align.LEFT);
    ...
{

Флаг Paint.ANTI_ALIAS_FLAG включает режим сглаживания при рисовании, а метод Color.argb, помимо цветовых составляющих (Red, Green, Blue), первым параметром позволяет указать степень прозрачности примитива. Возможности рисования на канве в Андроиде очень похожи на те, что предоставляет подсистема GDI в Windows.

Для придания реалистичности экрану нашего прибора будем использовать радиальный градиентный шейдер. Основная идея — цвет вблизи центра радара должен быть более светлым, чем по краям. В то же время он должен значительно темнеть при приближении к внешнему кольцу. Градиентный шейдер позволяет закрасить любой контур с помощью интерполированной цветовой гаммы, задавать которую удобно в виде массива:

mainGradientColors = new int[4];
mainGradientColors[0] = Color.rgb(0xB8, 0xE0, 0xFF);
mainGradientColors[1] = Color.rgb(0xA1, 0xCF, 0xFF);
mainGradientColors[2] = Color.rgb(0x62, 0xAA, 0xFF);
mainGradientColors[3] = Color.BLACK;

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

mainGradientPositions = new float[4];
mainGradientPositions[0] = 0.0f;
mainGradientPositions[1] = 0.2f;
mainGradientPositions[2] = 0.9f;
mainGradientPositions[3] = 1.0f;

где первый элемент массива соответствует центру радара, а последний — внешнему радиусу. Обрати внимание, все приведенные свойства относительны и никак не зависят от физического размера экрана, поэтому они вынесены в отделенный от onDraw метод.

Рис. 3. Шейдер фона радара
Рис. 3. Шейдер фона радара

Непосредственное создание шейдера с указанными выше параметрами и привязку к экранным координатам в нашем случае удобнее всего производить в методе onSizeChanged:

private RadialGradient mainGradient;
...
int px = w / 2;
int py = h / 2;
center = new Point(px, py);
radius = Math.min(px, py) - 20; // Отступ от краев канвы
mainGradient = new RadialGradient(center.x, center.y, radius, mainGradientColors, mainGradientPositions, Shader.TileMode.CLAMP);

Этот метод вызывается при любом изменении размеров представления. Объект сenter (центр радара) и переменная radius (внешний радиус) вычисляются исходя из физического размера представления (то есть экрана) с помощью переданной в onSizeChanged ширины (w) и высоты (h). Константа CLAMP определяет использование цвета по краям шейдера в случае, если область, заданная кистью шейдера, меньше области заполнения.

Рис. 4. Добавление сетки
Рис. 4. Добавление сетки

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

int glassColor = 0xF5;
glassGradientColors[0] = Color.argb(0, glassColor, glassColor, glassColor);
glassGradientColors[1] = Color.argb(50, glassColor, glassColor, glassColor);
// и так далее
Рис. 5. Шейдер стеклянного купола
Рис. 5. Шейдер стеклянного купола

Полный листинг функции отрисовки onDraw приведен ниже:

@Override
protected void onDraw(Canvas canvas) {
    // Радиальный градиентный шейдер радара
    mainGradientPaint.setShader(mainGradient);
    canvas.drawOval(outerBox, mainGradientPaint);
    // Сетка
    canvas.drawCircle(center.x, center.y, radius*0.10f, axisPaint);
    canvas.drawCircle(center.x, center.y, radius*0.40f, axisPaint);
    canvas.drawCircle(center.x, center.y, radius*0.70f, axisPaint);
    canvas.save();
    canvas.rotate(15, center.x, center.y);
    for (int i = 0; i < 12; i++) {
        canvas.drawLine(center.x, center.y, center.x + radius * 0.75f, center.y, axisPaint);
        canvas.rotate(30, center.x, center.y);
    }
    canvas.restore();
    // Данные
    drawData(canvas);
    // Шейдер стекла
    glassPaint.setShader(glassShader);
    canvas.drawOval(innerBox, glassPaint);
    // Внешняя окружность кольца
    circlePaint.setStrokeWidth(1);
    canvas.drawOval(outerBox, circlePaint);
    // Внутренняя окружность кольца
    circlePaint.setStrokeWidth(2);
    canvas.drawOval(innerBox, circlePaint);
}

Метод drawCircle рисует окружность на канве, а drawOval — эллипс, ограниченный прямоугольником. Для привязки созданного ранее шейдера к кисти рисования используется метод setShader. При нанесении сетки канва сначала сохраняет систему координат — canvas.save, затем поворачивается в цикле на 30 градусов относительно центра canvas.rotate(30, center.x, center.y), образуя тем самым двенадцать секторов окружности. Каждый сектор соответствует номеру канала работы точек доступа (канал №13 мы не рассматриваем). Для возвращения к исходной системе координат вызывается метод canvas.restore.

Метод drawData отображает маркеры точек доступа и их названия (SSID): зеленым цветом — если точка открытая или используется WEP, синим — если доступен WPS и белым во всех остальных случаях (но без отображения SSID). При этом чем выше уровень сигнала точки, тем ближе она к центру радара и ярче. Полный код ради экономии места (а также из-за его очевидности) я приводить не буду — готовый исходник всегда ждет тебя с распростертыми объятиями.

 

Встраиваемая техника

Сотворенный класс RadarView пока не привязан к какой-либо активности или фрагменту. Исправим это, дополнив разметку единственной активности нашего приложения (main.xml):

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
  <example.com.wifiradar.RadarView
     android:id="@+id/radarView"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
  />
</FrameLayout>

С точки зрения разметки класс RadarView представляет собой обычный компонент, но с тем отличием, что при задании необходимо в обязательном порядке указывать полное наименование класса — example.com.wifiradar.RadarView. В поле android:id задается уникальный идентификатор, с помощью которого можно получить ссылку на наш радар в коде активности:

RadarView rv = (RadarView)this.findViewById(R.id.radarView);
rv.setData(WiFiAPs);

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

rv.invalidate();
 

Outro

Все любят Wi-Fi! Нет, правда, все любят Wi-Fi! Это одна из немногих технологий беспроводной связи, которая доступна абсолютно всем и проста в использовании. Сегодня мы ненадолго заглянули за кулисы радиоэфира и даже создали симпатичный компонент для Андроида. Надеюсь, ты знаешь, как правильно использовать полученный скилл.

5 комментариев

  1. Аватар

    akroba4o

    21.07.2015 в 07:42

    Что за прога у кого подписка скинте апк ну или название хотя бы

  2. Аватар

    shkurinskiy

    21.07.2015 в 13:15

  3. Аватар

    therealman

    17.08.2015 в 00:17

    Сергей, скажите, для чего приложению (не обязательно этому) нужны «Данные о Wi-Fi-подключении»? И как это разрешение влияет на безопасность и анонимность?

  4. Аватар

    sher-machine

    14.10.2019 в 19:30

    Есть ли полный исходный код проекта? не отрывками?
    Спасибо

  5. Аватар

    sher-machine

    14.10.2019 в 19:52

    Очень нужен, помогите)

Оставить мнение