В этой статье я расскажу о своем опыте реверса приватного API одного из популярных впн клиентов - ExpressVPN. Мы рассмотри как находить и обходить методы обнаружения MITM(обход ssl pinning), научимся работать с фридой в iOS реверсинге, поколупаемся в клиенте используя иду, и после этого разберем необычную шифровку запроса, при помощи которого происходит авторизация в аккаунт. Вот, что нам будет нужно при выполнение этого задания Джейлбрейкнутый айфон Любой снифер трафика(я использую Burp Suite) IDA Pro Frida Surge 4 (это vpn-based снифер с большим количество плюшек, таких как поддержка проксей и захвата сокетов tcp/udp) Попытки "просто" отснифать трафик Первое что приходит в голову - это просто отснифать трафик приложения любым снифером, и нужно признать, что для большинства приложений этого хватает. Как вы могли понять ExpressVPN не тот случай. Прописав проксю бурпа в настройках WI-FI на айфоне, и установив сертификат для дешифровки https соединений я стал пытаться отснифать запрос при попытке авторизации в клиенте. И... Ничего не получилось, как и всегда вылезло окошко о том, что пароль неверный, в бурпе вообще ничего не изменилось.  После этого я пришел к такому выводу: либо же при авторизации не используется http, либо же приложение просто игнорирует системные настройки прокси(приложениям на flutter привет). Почему я откинул идею о том, что в приложении сработал ssl pinning? Так как в этом случае в бурпе во вкладке Event log появилась бы ошибка Received fatal alert: certificate unknown Пытаемся отснифать трафик с помощью vpn-based снифера В этом пункте мы убьем сразу двух зайцев: проверим правда ли что приложение использует чистые сокеты для отправки запросов, и если нет - то сможем отснифать запросы, так как сниферы на базе впн действуют на все устройство, независимо от того, игнорирует ли приложение настройки прокси или нет. Еще пару месяцев назад я купил полную версию Surge 4, которой сейчас и воспользуюсь. Настроем его: Во-первых, нам нужно включить дешифровку https трафика. После того как мы открыли Surge 4 нужно зайти во вкладку mitm, и установить сертификат(так же его нужно будет сделать доверенным. Для этого зайдите в настройки телефона - General - About - Certificate Trust Settings, там поставьте галочку напротив вашего сертификата). Так же важно поставить галочку напротив Skip server certificte verification В итоге должно получится так Теперь в этой же вкладке MITM заходим в Hostnames и добавляем такой: *.* Настроем прокси, это для того, чтобы было удобно работать с запросами прямо в бурпе. Во вкладке Outbound Mode выберите Global Proxy и откройте вкладку Proxy Servers, там нужно добавить новую проксю - назовем её burp Должно получится так: Теперь осталось включить еще и снифер tcp/udp трафика, это так, на всякий случай Отлично, включаем снифер. В статус баре должен появится значок VPN Заходим в приложение, пытаемся войти, и... Снова неуспех, приложение советует нам перепроверить интернет, вот и ssl pinning подъехал Возвращаемся в снифер, заходим в HTTP Capture, и находим вот такой интересный запрос И это с учетом того, что у меня стоит твик ssl kill switch 2, означать это может только одно: наше приложение не использует стандартные методы обнаружения mitm. На этом этапе мы переходим к IDA Pro и Frida Ищем кастомный метод обнаружения mitm Во-первых нам нужно получить ipa файл приложения, для этого воспользуемся замечательной тулзой bagbak. Переходим в папку с приложением и воспользуемся командой grep -rins "www.expressapisv2.net" . Таким образом мы нашли где используется этот текст Binary file ./Frameworks/xvclient.framework/xvclient matches. Вы конечно спросите, почему именно этот текст, а не, например,check your connection- потому-что pinning обычно проходит в том же бинарнике, где и запросы Открываем его в иде, ждем окончания анализа. Первое что приходит в голову это поискать части ssl или pinning в функциях, это и сделаем Бегло просмотрев эти функции я смог предположить, что разработчики просто запихнули openssl в свой клиент, вместо того, чтобы использовать системные библиотеки(наше счастье что не обфусицировали) Мы могли-бы прибегнуть к статичному анализу, но куда быстрее будет оттрейсить все вызовы функций начинающиеся на SSL_. Frida как раз предоставляет такую возможность - будем использовать frida-trace. Перезапускаем приложение и выполняем такую команду: frida-trace -H 192.168.1.183 -i "xvclient!SSL_*" ExpressVPN (примечание: вы можете использовать аргумент -U, если айфон подключен к вашему устройству по usb, если же нет - узнайте адрес айфона в вашей сети и используйте его). Ждем пока оно найдет все функции и входим(на этом этапе снифер уже нам ни к чему). Мы увидели все вызовы нужных нам функций Какую же функцию нам нужно хукнуть? SSL_set_verify, документацию на которую можно найти здесь /docs/man1.0.2/man3/SSL_set_verify.html (openssl.org). Нас интересует аргумент mode и verify_callback, первый нужно заменить на 0, второй нужно заменить на свою функцию, которая будет возвращать SSL_VERIFY_NONE, то есть опять же 0. Отлично, осталось написать скрипт для фриды, который будет этот делать. Во-первых, нужно найти адрес этой функции, для этого сделаем такое: var func = Module.findExportByName("xvclient", "SSL_set_verify") , далее создадим новую функцию на основе этой, чтобы в дальнейшем передать туда наш callback, в скобках прописываем типы аргументов, void - возвращаемое значение Теперь создадим наш callback, который будет возвращать нужное нам значение И наконец-то заменим функцию SSL_set_verify на нашу, которую мы вызовем, передав нужные нам аргументы Давайте проверять правильно ли мы обнаружили функцию, которая отвечает за ssl pinning. Перезапускаем приложение, запускаем скрипт командой frida -H 192.168.1.183 --no-paus -l script.js -f com.expressvpn.iosvpn, пытаемся войти. Да! Никакой таблички об интернет соединении не вылезло, будем смотреть что в бурпе Разбираем шифровку тела запроса Вот это уже интересно, мы видим 2 сигнатуры, которые похожи на хэши sha1, но в base64, а так же само тело запроса. На gzip оно не похоже, хотя в запросе, напротив, об этот сказано. Структура тела тоже не понятна, в начале что-то похожее на сертификат, больше сказать ничего нельзя. Возвращаемся в иду Попробуем найти что-то связанное с шифрованием по ключевому слову encrypt Нас интересует xc::Crypto, это мы и будет трейсить, frida-trace -H 192.168.1.183 -i "xvclient!*xc6Crypto*" ExpressVPN Первые две функции похоже для генерации ключей шифрования, которые энкодируются с помощью base64, далее само шифрование - некая pkcs7 encryption, далее мы видим генерацию подписи - это hmac sha1. Прекрасно, приблизительный алгоритм мы узнали, осталось найти ключи и исходные данные Давайте взглянем на метод Encrypt поближе Функция PKCS7_encrypt принимает такое: STACK *certs, BIO *in, const EVP_CIPHER *cipher, int flags. Ключ выступает в роли сертификатов. Теперь снова вооружаемся фридой и начинаем дебажить. Примечу, что нам не нужно хукать именно эту функцию, будем искать те места, где данные передаются в виде байтов. Как раз таки исходные данные для шифровки мы можем очень легко получить хукнув функцию Encrypt, в которой мы сейчас находимся. Взглянем на эту строку *(const unsigned __int8 **)v3 - это адрес, где лежат данные, чтобы его получить, нам просто нужно прочитать указатель, то есть args[1].readPointer(). Теперь рассмотрим такое *(_QWORD *)(v3 + 8) - *(_QWORD *)v3 - это длина данных, читаем это так - добавляем к v3 8 байтов, и читаем указатель по этому адресу, получаем адрес, которые переводим в 10-ю систему, вычитаем адрес, где лежат данные. Получилось такое: args[1].add(8).readPointer().toInt32()-args[1].readPointer().toInt32() Скомпонуем это все для хука функции Делаем вход - получаем такое в консоли Не очень то похоже на авторизационные данные, но не забываем, что мы видели заголовок, которые говорит нам, что данные сжаты с помощью gzip. Идем на CyberChef и делаем gunzip  Неплохо, осталось найти ключ. Key и iv указывают нам на AES шифрование, они генерируются случайно, это что-то типа ключа сессии Помните это строку? v5 = (STACK *)xc::Crypto::CertificateStack::Get((xc::Crypto::CertificateStack *)(a1 + 8)); если сертификат откуда-то получается, значит его туда загружают из байтов, будем искать это место Я пришел к такому методу xc::Crypto::CertificateStack::Push аргументов к нему идет объект такого типа xc::Crypto::Certificate, логично, что он где-то инициализируется. Искать долго не пришлось, xc::Crypto::Certificate::Certificate(xc::Crypto::Certificate *this, const unsigned __int8 *a2, __int64 a3). a2 - указатель на адрес, куда записан сертификат, a3 - длинна сертификата. Пишем хук Но так как скорее всего сертификат подгружается либо при запуске приложения, либо при первой авторизации, закрываем его. Запускаем, но уже с параметром -f com.expressvpn.iosvpn и получаем кучу логов, а в начале такую красоту -  Some-State, как раз это и было в начале той пост даты. Итого мы получили: генерируем 2 ключа длинной 16 байтов; делаем обычную пост дату в формате json; сжимаем это дело с помощью gzip; шифруем, генерируя enveloped data с одним единственным сертификатом; отправляем. Мы у финиша, осталась 2 сигнатуры Разбираем 2 сигнатуры Еще когда мы трейсили крипто функции мы заметили что-то связанное с hmac, давайте хукним и это. xc::Crypto::Hmac::Sha1(xc::Crypto::Hmac *this@, const unsigned __int8 *a2@, __int64 a3@, const unsigned __int8 *a4@, __int64 *a5@) this - данные(почему данные в this я так и не понял), a2 - длинна данных, a3 - ключ, a4 - длинна ключа Итого: ключ - @y{T4]wfJMA},qG}06rDO{f0<kYEwYWX'K)-GOyB^exg;K_k-J7j%$)L@[2me3 , данные для X-Signature - POST /apis/v2/credentials?client_version=11.5.2&installation_id=b183025c9148241dcdb3289275b00b5cb0fae3b5c3c0896451f6aaa0605c5c85&os_name=ios&os_version=14.4 , данные для X-Body-Signature - финальное тело запроса.  Полный скрипт для фриды А так же мой скрипт, для тестирования API: Numenorean/ExpressVPN-Auth: This is reverse engineered ExpressVPN authentification in iOS client (github.com). На этом все, всем спасибо, кто дочитал до этого момента, пишите замечания по поводу статьи, это мой первый опыт.