Салют, Хабр! Я думаю, каждый из вас знаком или, по крайней мере, слышал о такой прекрасной утилите как NoDPI написанной на питоне (большое спасибо @Lord_of_Rings!). Сегодня я хочу представить вам (почти) свою разработку, не требующую ни питона ни прокси. Мы будем патчить прямо на диске библиотеку chrome.dll - входяющую в пакет Chrome на Windows и лежащую в директории "C:\Program Files\Google\Chrome\Application\140.0.7339.208\chrome.dll". Цифры могут меняться в зависимости от версии. Данный патч занимает всего 8 байт и после него у нас появится YouTube.
Сначала скачаем https://boringssl.googlesource.com/boringssl это библиотека TLS/SSL, на которой построен Chrome/Chrominium.
Нас будет интересовать файл s3_both.cc вот в этом месте:
bool tls_add_message(SSL *ssl, Array<uint8_t> msg) {
// Pack handshake data into the minimal number of records. This avoids
// unnecessary encryption overhead, notably in TLS 1.3 where we send several
// encrypted messages in a row. For now, we do not do this for the null
// cipher. The benefit is smaller and there is a risk of breaking buggy
// implementations.
//
// TODO(crbug.com/374991962): See if we can do this uniformly.
Span<const uint8_t> rest = msg;
if (!SSL_is_quic(ssl) && ssl->s3->aead_write_ctx->is_null_cipher()) {
while (!rest.empty()) {
Span<const uint8_t> chunk = rest.subspan(0, ssl->max_send_fragment);
rest = rest.subspan(chunk.size());
if (!add_record_to_flight(ssl, SSL3_RT_HANDSHAKE, chunk)) {
return false;
}
}
} else {
Можно легко увидеть, что здесь пакет нарезается на chunk размером max_send_fragment и далее из этого формируются пакеты с заголовком SSL3_RT_HANDSHAKE (равно 22). Кто следил как устроен NoDPI на питоне, то там всё тоже самое, но нарезка происходит не фиксированной длины, а случайным образом. Примерно так:
while data:
chunk_len = random.randint(1, len(data))
parts.append(
bytes.fromhex("160304")
+ chunk_len.to_bytes(2, "big")
+ data[:chunk_len]
)
data = data[chunk_len:]
Прекрасно, модифицируем код s3_both.cc, чтобы уменьшить размер max_send_fragment (по умолчанию он равен 16384). Таким образом пакеты начнут дробиться на кусочки. Случайность делать не будем, как выяснилось, она не нужна.
См код ниже. Новыми являются первые две строчки, остальной код не трогаем.
ssl->max_send_fragment=0x3e8;
if (1) {
while (!rest.empty()) {
Span<const uint8_t> chunk = rest.subspan(0, ssl->max_send_fragment);
rest = rest.subspan(chunk.size());
if (!add_record_to_flight(ssl, SSL3_RT_HANDSHAKE, chunk)) {
return false;
}
}
} else {
А вот как это выглядит на языке Ассемблера.
Было:
00D02D21: 4883B99800000000 cmp q,[rcx][000000098],0 ;!SSL_is_quic(ssl)
00D02D29: 0F85B9000000 jnz 000D02DE8
00D02D2F: 488B4730 mov rax,[rdi][030] ;ssl->s3->aead_write_ctx->is_null_cipher()
00D02D33: 488B8010010000 mov rax,[rax][000000110]
00D02D3A: 48833800 cmp q,[rax],0
00D02D3E: 0F85A4000000 jnz 000D02DE8
00D02D44: 4D85E4 test r12,r12
00D02D47: 0F8439010000 jz 000D02E86
00D02D4D: 440FB74F10 movzx r9d,w,[rdi][010] ;ssl->max_send_fragment
Стало:
00D02D21: 66C74710E803 mov w,[rdi][010],003E8 ;max_send_fragment=0x3e8
00D02D27: EB1B jmps 000D02D44 ; if (1) --->
00D02D29: 0F85B9000000 jnz 000D02DE8 ; далее ничего не трогаем
00D02D2F: 488B4730 mov rax,[rdi][030]
00D02D33: 488B8010010000 mov rax,[rax][000000110]
00D02D3A: 48833800 cmp q,[rax],0
00D02D3E: 0F85A4000000 jnz 000D02DE8
00D02D44: 4D85E4 test r12,r12 ;приходим сразу сюда <---
00D02D47: 0F8439010000 jz 000D02E86
00D02D4D: 440FB74F10 movzx r9d,w,[rdi][010]
В итоге мы поменяли всего 8 байт в файле chrome.dll по оффсету с 2D21 по 2D28
Создаём маску поиска 4883B998000000000F85B9000000488B4730 это байты с 2D21 по 2D32
Теперь маска замены: 66C74710E803EB1B0F85B9000000488B4730
Маска немного больше размером, чем 8 байт, так как иначе мы можем найти сторонний код, так как последовательность cmp q,[rcx][000000098],0 и jnz 000D02DE8 часто встречается, а если добавить еще одну команду mov rax,[rdi][030] то такая последовательность встречается в chrome.dll только один раз, что и требуется для патчинга.
Далее качаем патч-утилиту, воспользуемся этим проектом https://github.com/pbatard/winpatch, здесь кроме патча бинарного кода, также производится модификация контрольной суммы PE-заголовка и подпись кода самоподписанным SSL-сертификатом, что скорее всего излишне, но и не помешает.
Пишем bat-файл, производящий поиск chrome.dll в каталоге "C:\Program Files\Google\Chrome\Application", код более-менее универсальный, чтобы обработать все подкаталоги с разными версиями.
@echo off
taskkill /F /IM chrome.exe
timeout 2
for /r "C:\Program Files\Google\Chrome\Application" %%f in (chrome.dll) do IF EXIST "%%f" ( %~dp0winpatch.exe "%%f" 4883B998000000000F85B9000000488B4730 66C74710E803EB1B0F85B9000000488B4730 )
echo Patch done
echo Press ENTER to close window
pause
Здесь дополнительно удаляется процесс сhrome.exe (вкладки можно будет восстановить после перезапуска), иначе chrome.dll будет залочен и недоступен для патча. Далее ждём 2 секунды и скармливаем утилите winpatch.exe полный путь chrome.dll а также маску поиска и маску замены.
Пишем эти 8 строк в runme.bat и запускаем bat из-под администратора. Патч произведён!

Запускаем патченный Chrome - Youtube работает отлично! Проверено на 3х компьютерах, 2х провайдерах и 5ти версиях Chrome.
В заключение создадим самораспаковыващийся WinRar архив (это полноценный exe-файл), в который поместим утилиту winpatch.exe и runme.bat, назначим ему через диалог конфигурации привилегии администратора на запуск, это чтобы мы смогли модифицировать chrome.dll, лежащий в системной директории, а также ставим в автостарт runme.bat и контрольный вопрос - патчим Chrome? Назовём утилиту youtube_patch_v1.0.exe
Конечно, для полноценной работы нужны списки blacklist.txt - но 8 байт, это 8 байт!
Если что-то пошло не так, восстановите chrome.dll из бэкапа chrome.dll.bak
Если Chrome обновится на новую версию, конечно же, файл chrome.dll будет заменён на оригинальный и патч нужно будет повторить.
Самораспаковывающийся и самозапускающийся архив, полностью готовый для патчинга, выложен на гугл-диске: youtube_patch_v1.0.exe
UPD: Версия 1.0 для Linux
Было:
041C4DCF: 4883BF9800000000 cmp q,[rdi][000000098],0
041C4DD7: 0F85BF000000 jnz 0041C4E9C
041C4DDD: 498B4630 mov rax,[r14][030]
041C4DE1: 488B8010010000 mov rax,[rax][000000110]
041C4DE8: 48833800 cmp q,[rax],0
041C4DEC: 0F85AA000000 jnz 0041C4E9C
041C4DF2: 4D85ED test r13,r13
041C4DF5: 0F84A6000000 jz 0041C4EA1
041C4DFB: 410FB74E10 movzx ecx,w,[r14][010]
Стало:
041C4DCF: 6641C74610E803 mov w,[r14][010],003E8
041C4DD6: EB1A jmps 0041C4DF2
041C4DD8: 85BF00000049 test [rdi][049000000],edi
041C4DDE: 8B4630 mov eax,[rsi][030]
041C4DE1: 488B8010010000 mov rax,[rax][000000110]
041C4DE8: 48833800 cmp q,[rax],0
041C4DEC: 0F85AA000000 jnz 0041C4E9C
041C4DF2: 4D85ED test r13,r13
041C4DF5: 0F84A6000000 jz 0041C4EA1
041C4DFB: 410FB74E10 movzx ecx,w,[r14][010]
Строчки для winpatch.exe для патчинга /opt/google/chrome/chrome под Windows
4883BF98000000000F85BF000000
6641C74610E803EB1A85BF000000
Для Linux сделаем нативный скрипт на bash с использованием bbe
(это аналог всем известного sed но применим для бинарных строк)
Сначала установите пакет bbe
sudo apt install bbe
Выполните следующий скрипт с правами администратора, оригинальный chrome будет сохранен в /opt/google/chrome/chrome.bak если что-то пойдет не так, можно будет его восстановить.
#!/bin/bash
FILE=/opt/google/chrome/chrome
FILE_BAK=/opt/google/chrome/chrome.bak
FILE_TMP=/tmp/chrome.$$$
if [ "$USER" != "root" ]; then
echo "Для выполнения скрипта нужны права sudo, запустите скрипт с правами суперпользователя"
exit
fi
read -r -p "Do you agree to patch v1.0 Chrome for YouTube ? [y/N] " response
if [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]
then
if [ ! -f $FILE_BAK ]; then
cp $FILE $FILE_BAK
fi
bbe -e 's@\x48\x83\xBF\x98\x00\x00\x00\x00\x0F\x85\xBF\x00\x00\x00@\x66\x41\xC7\x46\x10\xE8\x03\xEB\x1A\x85\xBF\x00\x00\x00@' $FILE_BAK >$FILE_TMP
cmp $FILE_TMP $FILE_BAK >/dev/null
if [ $? -eq 0 ]
then
echo "Patch failed!"
exit
fi
chmod +x $FILE_TMP
cp $FILE_TMP $FILE
echo "Patch done!"
fi
Данный скрипт выложен также на гугл-диске youtube_patch_v1.0
Перед запуском сделать +x:
chmod +x youtube_patch_v1.0
sudo ./youtube_patch_v1.0
UPD: Обновление алгоритма (версия 1.1)
Всё-таки нужно пускать к сайтам, которые не поддерживают данный режим даже если нет blacklist.txt.
Поэтому в версии 1.1 сделаем так:
00D02D21: 0F31 rdtsc
00D02D23: A808 test al,8
00D02D25: 66B80040 mov ax,04000
00D02D29: 7404 jz 000D02D2F
00D02D2B: 66B8E803 mov ax,003E8
00D02D2F: 66894710 mov [rdi][010],ax
00D02D33: EB0F jmps 000D02D44
Это означает, что с вероятностью 1/2 будем устанавливать режим разделения пакетов на кусочки (размер 3e8) или же старый режим (по умолчанию 4000). В итоге, теперь все сайты должны заработать, но будут наблюдаться некоторые тормоза при первой загрузке, так как если мы не попали в нужный режим - то потребуется некоторое время на переповторы. Ну и если что-то не открывается, можете пару раз нажать перезагрузить - страница войдет в кеш браузера и всё начнёт летать.
Выглядит так, что первый раз youtube несколько тупит, но потом ситуация проясняется, так как накладные расходы у нас только на момент установления соединения, когда тело ролика качается - ничего лишнего нет.
В итоге новые строки для патча такие:
4883B998000000000F85B9000000488B4730488B
0F31A80866B80040740466B8E80366894710EB0F
Новый bat-файл v1.1
@echo off
taskkill /F /IM chrome.exe
timeout 2
for /r "C:\Program Files\Google\Chrome\Application" %%f in (chrome.dll) do IF EXIST "%%f" ( %~dp0winpatch.exe "%%f" 4883B998000000000F85B9000000488B4730488B 0F31A80866B80040740466B8E80366894710EB0F )
echo Patch done!
echo Press ENTER to close window
pause
Собранную с помощью WinRar версию v1.1 выложил на гугл-диск youtube_patch_v1.1.exe
UPD: Обновление алгоритма (версия 1.2)
Предыдущая версия (1.1) работает для всех сайтов, но с тормозами. Поэтому в версии 1.2 сделаем выбор сайтов по ScrollLock, выглядит это так:
012FF271: 48C7C191000000 mov rcx,000000091
012FF278: FF155262EE0D call GetKeyState
012FF27E: A801 test al,1
012FF280: 66B8E803 mov ax,003E8
012FF284: 7504 jnz 0012FF28A
012FF286: 66B80040 mov ax,04000
012FF28A: 66894710 mov [rdi][010],ax
012FF28E: EB04 jmps 0012FF294
Это означает, что если клавиша ScrollLock (rcx = VK_SCROLL 0x91) нажата и горит на клавиатуре соответствующий значок - то будем работать с YouTube, если нажать ScrollLock ещё раз и лампочка погаснет - тогда Chrome работает по-старому, без патчей.
Выглядит очень даже хорошо, работаешь-работаешь сайт login.microsoft.com открывается нормально, вдруг захотелось в YouTube глянуть одним глазком, жмешь ScrollLock и смотришь без тормозов. Обратно - опять ScrollLock.
Единственный недостаток патча - так как вызывается системная функция GetKeyState, то патч применим только для конкретной версии, а именно 141.0.7390.66 - эта сейчас последняя. Для других адрес GetKeyState поедет и будет мусор, приводящий к GPF (падению Chrome).
Для других версий нужно будет настроить релокейшены для GetKeyState, например вот как это сделать с помощью hiew.exe. Сначала патчим, потом запускаем
dumpbin.exe /IMPORTS chrome.dll | findstr GetKeyState
получаем на экране адрес функции
000000018D5CE3E0 0 GetKeyState
Ищем эти байты в chrome.dll - жмем F4,F7 и вбиваем E0 E3 5C 8D 01
находим их в файле по смещению (это которая с точкой)
00000001`8F1DF6B0: E0 E3 5C 8D.01 00 00 00
и возвращаемся на наш патч (начало оффсет 12ff271) и делаем этот адрес вместо
call q,[00DEE6252] -> call q,[.18F1DF6B0] (не забываем про точку)
и мусор превращается в call GetKeyState прямо в коде.
В итоге новые строки для Chrome 141.0.7390.66 такие:
4883B998000000000F85B9000000488B4730488B8010010000488338000F85 48C7C191000000FF155262EE0DA80166B8E803750466B8004066894710EB04
Новый bat-файл v1.2
@echo off
taskkill /F /IM chrome.exe
timeout 2
for /r "C:\Program Files\Google\Chrome\Application\141.0.7390.66" %%f in (chrome.dll) do IF EXIST "%%f" ( %~dp0winpatch.exe "%%f" 4883B998000000000F85B9000000488B4730488B8010010000488338000F85 48C7C191000000FF155262EE0DA80166B8E803750466B8004066894710EB04 )
)
echo Patch done!
echo Press ENTER to close window
pause
Собранную с помощью WinRar версию v1.2 (сработает только для Chrome 141.0.7390.66) выложил на гугл-диск youtube_patch_v1.2.exe