Pull to refresh

7 ножей в спину web-отладки

Level of difficultyMedium
Reading time10 min
Views1.6K

Преамбула

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

О процессе отладки я буду рассказывать в хронологической последовательности, как она происходила в жизни, а не с "точки зрения вечности". Надеюсь, это поможет читателю лучше понять историю моих озарений и провалов. Заранее предупрежу, что я пытался двигаться к цели не углубляясь в причины неудачи тех или иных шагов. Если подход "в лоб" в каком либо варианте не срабатывал, я откладывал этот вариант, и переходил к другому. Название каждого раздела соответствует трудности, с которой я столкнулся. Итак, начнем.

1. Проблема воспроизводится не всегда.

Первым делом скачиваю приложение с Play Store и используя предоставленные пользователем параметры регистрируюсь в библиотеке. И, о чудо, login проходит без сучка без задоринки. Иду разбираться с поддержкой. Оказывается, радоваться рано. Надо пару тройку раз войти и выйти и... при очередной попытке входа получаю ошибку. М-да. Придется залезть глубже. Для начала посмотреть, происходит ли вообще взаимодействие с сайтом. Тут нет ничего сложного. Заходим на телефон через adb shell и запускаем tcpdump.

2. Для доступа к сайту используется SSL

>tcpdump

Нажимаем на кнопку "login" и...

IP 192.168.2.254.46904 > ds-195-123.dri-services.net.https: Flags [S], seq 600777635, win 65535, options [mss 1460,sackOK,TS val 525800818 ecr 0,nop,wscale 8], length 0
IP ds-195-123.dri-services.net.https > 192.168.2.254.46904: Flags [S.], seq 1294337794, ack 600777636, win 28960, options [mss 1460,sackOK,TS val 2084490719 ecr 525800818,nop,wscale 7], length 0
IP 192.168.2.254.46904 > ds-195-123.dri-services.net.https: Flags [.], ack 1, win 343, options [nop,nop,TS val 525800853 ecr 2084490719], length 0
IP 192.168.2.254.46904 > ds-195-123.dri-services.net.https: Flags [P.], seq 1:518, ack 1, win 343, options [nop,nop,TS val 525800856 ecr 2084490719], length 517
IP ds-195-123.dri-services.net.https > 192.168.2.254.46904: Flags [.], ack 518, win 235, options [nop,nop,TS val 2084490752 ecr 525800856], length 0

Увы, видим протокол https. Что ж, в современном WEB-е трудно ожидать иного. Попробуем разобраться с этой проблемой. Разобраться раз и навсегда.

3. Стандартный код BoringSSL не вызывается

Изначальный план такой - чтобы не мучиться каждый раз с внешними инструментами которые проксируют https-трафик параллельно расшифровывая его, добавим эту возможность прямо в код OpenSSL, на самом что ни на есть нижнем уровне. Благо это наш смартфон. Тогда всё будет отлично работать независимо от приложения и помогать нам в отладке без установки third-party программ.

Итак, посмотрим где там собирается OpenSSL. Наша версия Android - 11, и Google подсказывает, что в нем используется не оригинальный OpenSSL а его fork от Google - BoringSSL (https://github.com/google/boringssl). Смотрим в репозиторий - да, так и есть. BoringSSL находится в каталоге ./external/boringssl. Не будем мелочиться а добавим логирование прямо в сердце библиотеки - ./external/boringssl/src/crypto/bio/bio.c
Напрямую log Android там не доступен, но нам поможет то, что стандартный вызов syslog() из UNIX перенаправляет сообщения прямо в logcat. Итак, вставляем контрольную печать, пересобираем Android, перепрошиваем смартфон и запускаем для начала собственный небольшой тест, чтобы убедиться, что наш код отрабатывает.

Код программы на С
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

#define HOST "www.example.com"
#define PORT "443"
#define REQUEST "GET / HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"

void handle_error(const char* msg) {
    fprintf(stderr, "%s\n", msg);
    ERR_print_errors_fp(stderr);
    exit(1);
}

void https_request(const char* hostname) {
    SSL_CTX* ctx;
    SSL* ssl;
    int sockfd;
    struct sockaddr_in server_addr;
    struct hostent* server;
    
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        handle_error("socket error");
    }
    
    server = gethostbyname(hostname);
    if (server == NULL) {
        handle_error("gethostbyname error");
    }
    
    // Заполнение структуры адреса сервера
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(443);
    memcpy(&server_addr.sin_addr.s_addr, server->h_addr, server->h_length);
    
    // Установка соединения с сервером
    if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        handle_error("connect error");
    }
    
    // Инициализация библиотеки OpenSSL
    SSL_library_init();
    SSL_load_error_strings();
    
    // Создание контекста SSL
    ctx = SSL_CTX_new(TLS_client_method());
    if (ctx == NULL) {
        handle_error("SSL_CTX_new error");
    }
    
    // Создание объекта SSL
    ssl = SSL_new(ctx);
    if (ssl == NULL) {
        handle_error("SSL_new error");
    }
    
    // Привязка объекта SSL к сокету
    SSL_set_fd(ssl, sockfd);
    
    // Установка соединения по SSL
    if (SSL_connect(ssl) <= 0) {
        handle_error("SSL_connect error");
    }
    
    // Отправка HTTP-запроса
    SSL_write(ssl, REQUEST, strlen(REQUEST));
    
    // Чтение и обработка ответа сервера
    char response[4096];
    int response_length = 0;
    int bytes_read;
    while ((bytes_read = SSL_read(ssl, response + response_length, sizeof(response) - response_length - 1)) > 0) {
        response_length += bytes_read;
    }
    response[response_length] = '\0';
    printf("Response:\n%s\n", response);
    
    // Завершение соединения
    SSL_shutdown(ssl);
    SSL_free(ssl);
    SSL_CTX_free(ctx);
    
    close(sockfd);
}

int main() {
    const char* hostname = HOST;

    // Пробуем соединиться
    https_request(hostname);
    
    return 0;
}

Запускаем скомпилированный код из adb shell. И... видим в логе запрос к серверу.
Ура! Неужели заветная цель близка?

Теперь запустим приложение, с которым нужно разобраться. Снова жмем на кнопку login, но... Никакой реакции в логе. Наш код не вызван. Как это возможно? Видимо придется сходить в отладчик.

4. Conscrypt вызывается, но только для части приложения

Быть может, в Java-приложениях под android используется какая-то другая библиотека для работы с HTTPS? Для проверки набросаем простое приложение и попробуем пройтись по нему в отладчике.

Код функции на Java
private String performHttpsRequest(String hostname, String path) {
		String response = "";
		try {
			// Создание SSL контекста
			SSLContext sslContext = SSLContext.getInstance("TLS");
			sslContext.init(null, null, null);

			// Создание SSL сокета
			SSLSocketFactory socketFactory = sslContext.getSocketFactory();
			SSLSocket socket = (SSLSocket) socketFactory.createSocket();
			SSLParameters sslParameters = socket.getSSLParameters();
			socket.setSSLParameters(sslParameters);

			// Установка соединения с сервером
				InetAddress address = InetAddress.getByName(hostname);
				socket.connect(new java.net.InetSocketAddress(address, 443));

				// Отправка HTTP-запроса
				String request = "GET " + path + " HTTP/1.1\r\n" +
						"Host: " + hostname + "\r\n" +
						"Connection: close\r\n\r\n";
				OutputStream outputStream = socket.getOutputStream();
				outputStream.write(request.getBytes());
				outputStream.flush();

				// Чтение и обработка ответа сервера
				BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
				StringBuilder responseBuilder = new StringBuilder();
				String line;
				while ((line = reader.readLine()) != null) {
					responseBuilder.append(line).append("\n");
				}
				response = responseBuilder.toString();

				// Закрытие соединения
				socket.close();
		}
		catch (Exception e) {
			Log.e(TAG, "Error: " + e.getMessage());
		}
		return response;

Входим внутрь метода на несколько уровней и попадаем в библиотеку Conscrypt (https://github.com/google/conscrypt). Эта библиотека позиционируется как Java Security Provider, и имплементирует Java Cryptography Extension (JCE) и Java Secure Socket Extension (JSSE). К счастью для нас тут уже есть специальный файл (./external/conscrypt/common/src/jni/main/cpp/conscrypt/trace.cc) в котором можно выставить флаги для отладки различных функций. Собственно, содержимое его невелико:

#include <conscrypt/trace.h>

namespace conscrypt {
namespace trace {

const bool kWithJniTrace = false;
const bool kWithJniTraceMd = false;
const bool kWithJniTraceData = false;
const bool kWithJniTracePackets = false;
const bool kWithJniTraceKeys = true;  // <- то, что нужно!
const std::size_t kWithJniTraceDataChunkSize = 512;

}  // namespace trace
}  // namespace conscrypt

Для наших целей вполне достаточно выставить kWithJniTraceKeys в true, пересобрать Android и снова перепрошить телефон.
Запускаем наш пример на Java и радостно наблюдаем в логе записи типа:

NativeCrypto-jni: ssl=0x6f9c6f8918 KEY_LINE: CLIENT_RANDOM d4cd8dbd2d740c2fb34e0f592023ae73e934969ce9806757a338c1102e7665fe dd9952f4577370a605dd5abe449bfba2e641890252641ce08789d3ec0792889558cadc70fdc36fe73b063ca345783733

NativeCrypto-jni: ssl=0xb400006f9c6f20d8 KEY_LINE: CLIENT_TRAFFIC_SECRET_0 d9d7cb07389159bf03604756ce27f308add1af9e3dd84a928b7e13cfbc9e9716 3691d1c3ebff780091812e404a0daeff6262f02e4bfcb778a5300f4ab85ae20c

Т.е. логирование ключей работает. Потенциально, используя эти ключи мы можем расшифровать SSL-сессию используя обычный WireShark. Теперь запускаем целевое приложение и тоже видим в логе обмен ключами. Однако сама попытка соединения с online-библиотекой никак в логе не отражается. Снова засада. Видимо (как это часто бывает ныне) в приложении для одних и тех же действий, но в разных частях программы используются разные библиотеки. (Как шутят в GameDev, степень зрелости игры определяется числом различных версий Boost присутствующих в репозитории). Значит настала пора посмотреть на содержимое APK-файла. Похоже, оно преподнесет нам сюрпризы. Распаковываем apk, и в каталоге ./lib/arm64-v8a/ обнаруживаем такое:

libbookshelfCoreJNI.so
libbookshelfCore.so
libcrashlytics-common.so
libcrashlytics-handler.so
libcrashlytics.so
libcrashlytics-trampoline.so
libcrypto.so
libgnustl_shared.so
libnative-lib.so
libNuanceVocalizer.so
libpdfium.so
libplugins_bearer_libqandroidbearer.so
libplugins_platforms_android_libqtforandroid.so
libplugins_platforms_libqminimalegl.so
libplugins_platforms_libqminimal.so
libplugins_sqldrivers_libqsqlite.so
libQt5Concurrent.so
libQt5Core.so
libQt5Gui.so
libQt5Network.so
libQt5Sql.so
libQt5Xml.so
libssl.so

Т.е. высокоодарённые программисты этого приложения решили заменить в том числе и системные библиотеки (libssl.so и libcrypto.so) своими собственными. Теперь понятно, почему в логах BoringSSL было пусто.

5. Замена openssl.so на системную роняет приложение

Однако, это не повод опускать руки! Для начала попробуем тривиальный ход - заменим копии библиотек libssl.so и libcrypto.so на системные и снова запустим приложение. Итак, копируем библиотеки в apk, и запаковываем его. Понятно, что после этого слетит подпись приложения, но в нашем случае это не проблема. Просто переподпишем его системным ключом, и заново проинсталлируем. Сказано - сделано. Запускаем новый apk, и в целом он работает, но к сожалению при попытке connect-а к библиотеке - падает. Значит версии библиотек таки не совпали. Жаль, но ничего не поделаешь. Искать именно ту версию, под которую всё слинковано и пересобирать мне сильно лень, так что будем пробовать иные способы.

6. Настройки HTTP-proxy игнорируются (не работает charles proxy)

Каноничным программами для отладки web-приложений (да еще и популярными на Хабре судя по картинке) являются Fiddler и Charles Proxy.

Битва титанов (картинка не моя)
Битва титанов (картинка не моя)

Я выбрал последний из-за наличия бесплатной версии, поставил его и приступил к отладке. Для перехвата HTTPS-трафика нужно установить на телефон сертификат Charles Proxy в качестве доверенного, но в нашем случае это не проблема. Плюс, выставить системное HTTP-proxy, чтобы перенаправить запросы с реального сервера на свой компьютер, где работает Charles. Выполняем всё это и... запросы к proxy не идут. Tcpdump сообщает нам, что приложение по-прежнему общается с сервером напрямую. При этом многие "нормальные" программы начинают действительно работать через прокси, но не наша. Очевидно, настройки HTTP-proxy тупо игнорируются. Что ж. Этот исход был вполне ожидаем.

7. Заворот трафика через iptables не помогает (mitmproxy глючит?)

Посмотрим, что еще есть в мире MITM. Первое, что попадается на глаза - программа с говорящим названием MitmProxy (https://mitmproxy.org). Она бесплатна, код есть на github и обещает работать как transparent proxy. Нужно завернуть трафик через iptables на целевой компьютер а там уж оно разберется. Документация есть на странице https://docs.mitmproxy.org/stable/howto-transparent/, там предлагается сделать что-то типа:

iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 443 -j REDIRECT --to-port 8080

Ок. Дело это не хитрое. С поправкой на специфику Android и наши адреса успешно заворачиваем трафик на mitmproxy, попутно закинув еще один доверенный сертификат на устройство. Запускаем приложение, трафик идет куда нужно, но в логах mitmproxy мы видим неудачу соединения. Это может быть вызвано двумя причинами: либо что-то написано не так в самом proxy, либо приложение использует еще одну неприятную технику, известную как Certificate Pinning, когда клиент проверяет не только валидность сертификата, но и его строгое соответствие своим представлениям о прекрасном. Второй случай довольно неприятен, хотя и с ним пытаются бороться (плюс, на сегодня он считается устаревшим и в целом используется редко). В поисках нужного средства натыкаюсь в сети еще на один проект - HTTP Toolkit который обещает помочь.

8. HTTP Toolkit свое дело делает

Изначально мое внимание привлекает статься - Defeating Android Certificate Pinning with Frida и я решаю поставить HTTP Toolkit. На компьютере он ставится довольно легко, плюс имеет специальный клиент под Android. Для перехвата трафика он использует забавный метод - вместо настройки HTTP-proxy или манипуляций с iptables, клиент изображает из себя VPN. И когда мы его запускаем, весь трафик с устройства автоматически перенаправляется в VPN-туннель, который и образован клиентом и HTTP toolkit-ом на компьютере.

Для активации туннеля есть несколько вариантов. Самый простой - если смартфон уже подключен к вашем компьютеру через adb - просто кликаем в него. Если же нет, но телефон доступен по сети - можно выбрать сканирование QR-кода, и телефон подключится к VPN-серверу.

Варианты подключения
Варианты подключения

Разумеется, на устройство, как и всегда, надо добавить доверенный сертификат SSL. Но для собственного телефона это не проблема.

Выполняем эти нехитрые действия, и вуаля! Сразу же видим запросы в окне HTTP Toolkit безо всяких плясок с бубном. Пробуем подключиться к online-библиотеке и снова успех! Расшифрованный запрос у нас перед глазами. Значит версия о certificate pinning была ложной, и создатели mitmproxy просто что-то намудрили (хотя, возможно ошибся и я при переназначении трафика).

Наконец-то мы видим трафик
Наконец-то мы видим трафик

9. Но исправить баг мы не в состоянии

Анализ трафика однако показал, что проблема не в клиенте, и тем паче не в наших правках кода Android Framework. Сервер просто иногда ни с того ни с сего отказывает в соединении. Причем, мне удалось воспроизвести проблему и на устройствах других брендов, типа Xiaomi. Так что решить проблему клиента мы оказались не в состоянии, но хотя бы сняли с себя подозрения.

10. Заключение

Вот и подошел к концу мой рассказ об увлекательном мире отладки web-приложений посредством MITM. Хоть и не удалось достигнуть всех целей, но в целом я доволен. Во-первых, удалось окунуться в незнакомый доселе мир программ для перехвата HTTP[S] трафика, во-вторых - лучше понять структуру HTTPS-стека в Android. Плюс, найти весьма полезную программу для дальнейшего использования, попутно реализовав простой способ для расшифровки трафика без внешних proxy, только силами tcpdump и wireshark (более подробно можно посмотреть тут https://habr.com/ru/articles/253521/), который будет работать для большинства Android-приложений, не учитывая самых извращенных. Что уже немало.

Tags:
Hubs:
Total votes 5: ↑4 and ↓1+5
Comments0

Articles