Реверсинг протокола Network Assistant 3.x

Дата публикации 5 окт 2006

Реверсинг протокола Network Assistant 3.x — Архив WASM.RU

Содержание

Введение

В статье рассматривается реверсинг протокола чата Network Assistant 3.x. Это первая моя статья, поэтому можно считать, что она написана новичком для новичков, тем более что разработчики никакой защиты на программу не навешивали, код достаточно прост и ясен, а единственная проблема - его объём.

Идея поковыряться в чём-либо возникла, когда мне надоело писать свою собственную программу, поэтому выбор продукта по большому счёту случаен.

Network Assistant (далее сокращённо - Nassi) от Gracebyte Software представляет собой чат для локальной сети под Windows. Программа шароварная, однако в сети легко можно найти ключ. На настоящий момент актуальной является четвёртая версия, однако я буду рассматривать третью, т. к. именно этой версией пользуются в моей локалке

Версия 3.x поддерживает общие, приватные и защищённые паролем каналы, личные сообщения, доску рисования, смену состояний пользователей, передачу информации о версии чата, ОС, заголовке активного окна, просмотр и управление процессами, копии экрана, буфера обмена, статистики, сигнализаторы и т. д. К счастью, большую часть этих "возможностей" можно отключить. Чат не имеет сервера и работает через UDP-броадкаст (по умолчанию порты 50138/50139) и IP-мультикаст. Далее я буду ориентироваться на броадкаст т. к. его по историческим причинам используют в моей сети, хотя в общем-то формат пакетов Nassi от типа транспорта не зависит.

Прежде чем самому пытаться вскрыть протокол имеет смысл поискать в интернете открытую информацию. Побродив по сети можно выяснить следующее:

Во-первых. Nassi-пакет устроен следующим образом. Первый байт обозначает версию протокола: 0 для 2.x, 1 для 3.x. Далее идут два байта с длиной пакета. Это справедливо для 2.x и 3.x. Формат следующих далее данных зависит от версии чата. Структура пакета 2.x хорошо описывается в статье Эксплоит для сетевого чата в 48-м спецвыпуске Хакера (советую почитать, как очень неплохой туториал по реверсингу и написанию эксплоитов для начинающих). Для 3.x же все данные со смещения +3 зашифрованы.

Во-вторых. Для Nassi существует два бота: Vsbot и Krbot. Первый написан на Делфи (как, к слову, и сам чат) второй на Борланд С++, оба запакованы, однако распаковываются легко (самая длительная фаза - набор в адресной строке браузера http://cracklab.ru/download.php :smile3: ). Боты работают только с запущенным чатом и используют сырые сокеты для перехвата его пакетов. Очевидно, что функционально они победнее чата, по объёму кода - всего на один десятичный порядок меньше, поэтому реверсить их особого смысла нет.

Из открытых источников известно, что разработчики чата помогли авторам ботов, а значит у последних должно быть точное описание формата пакетов. Мне удалось связаться с одним из них, однако поделиться информацией он отказался, сославшись на договорённости с Gracebyte. Сами же разработчики меня проигнорировали.

Итак. Для реверсинга нам потребуется собственно сам чат - я исследовал Network Assistant 3.2.5 build 2260, отладчик и дизассемблер - я использовал Ida Pro, что и вам советую

Первым делом скармливаем главный модуль Nassi.exe Иде (не забываем применить сигнатуры - Delphi, Borland VCL, CBuilder). Пока идёт анализ программы подумаем, откуда в листинге в несколько сот тысяч строк начинать поиск интересующего нас кода по формированию, шифровке, расшифровке и анализу пакета.

Во-первых, нам известен криптоалгоритм используемый чатом - Blowfish (об этом можно прочитать на SecurityLab и на форуме самой Gracebyte) и можно попытаться найти код, отвечающий за шифровку/дешифровку, если была использована более или менее стандартная реализация. Во-вторых, можно начинать с анализа вызовов Winsock API

Blowfish

Начнём с Blowfish. Я использовал вот это описание алгоритма: достаточно подробное, но без излишеств с примером реализации на Яве. Из него мы можем выделить следующие, предположительно полезные фрагменты:

  1. Blowfish — 64-битный блочный шифр с ключом переменной длины. Алгоритм включает в себя 2 части: часть расширения ключей и часть шифрования данных. Расширение ключа преобразует ключ, в большинстве 448-битный, в несколько суммированных массивов подключей в 4168 байт. Шифрование данных происходит через 16-итерационную сеть Feistel
  2. Blowfish использует большое количество подключей. Эти ключи должны быть предварительно вычислены перед любым шифрованием данных или расшифровкой
    1. P-массив включает 18 32-битных подключей
    2. Имеются четыре 32-битных S-блока с 256 входами каждый
  3. Подключи вычисляются, путем использования алгоритма Blowfish. Данный метод состоит в следующем:
    1. Сначала инициализируется P-массив и затем четыре S-блока, с фиксированной строкой. Эта строка состоит из шестнадцатеричных цифр pi (меньше начальной тройки).
    2. Произведите XOR P1 с первыми 32 битами ключа, XOR P2 со вторым 32-битами ключа, и так далее для всех битов ключа (возможно до P14). Циклически пройдите биты ключа, пока весь P-массив не будет "поXORен" с битами ключа. (Для каждой короткого ключа, имеется, по крайней мере, один эквивалентный более длинный ключ; например, если ключ 64-битный, тогда AA, AAA, и т.д., являются эквивалентными ключами.)
    3. Необходимо шифровать все пустые строки с помощью алгоритма Blowfish, используя подключи, описанные на шагах (1) и (2).
    4. Заменить P1 и P2 с использованием (3).
    5. Шифровать с изменяемым подключом, используя шаг (3).
    6. Заменить P3 и P4 с использованием шага (5).
    7. Продолжить процесс, заменяя все входы P- массива, и затем все четыре S-блока.

И так, попробуем найти Blowfish в Nassi. Для начала попробуем на удачу поискать в листинге дизассемблера строки типа blow, blowfish и т. п. К счастью, нам везёт и мы сразу находим приведённый ниже фрагмент кода (Примечание: его наличие показывает, что Gracebyte использовали при написании чата сторонний криптокомпонент, т. к. вывод такого сообщения Nassi не нужен в принципе - как мы увидим далее чат использует два типа ключей, длины которых - константы. Некоторое время, я искал его в сети и нашёл один фриварный, похожий на используемый в Nassi, однако т. к. он достаточно большой и поставляется без исходников, анализировать его не имеет смысла):

Код (Text):
  1.  
  2. CODE:004B476D                 jle     short loc_4B4774
  3. CODE:004B476F                 cmp     ebx, 38h
  4. CODE:004B4772                 jle     short loc_4B478A
  5. CODE:004B4774
  6. CODE:004B4774 loc_4B4774:                             ; CODE XREF: sub_4B4754+19.j
  7. CODE:004B4774                 mov     ecx, offset aBlowfishKeyMus ; "Blowfish: Key must be between 1 and 56 "...
  8. CODE:004B4779                 mov     dl, 1
  9. CODE:004B477B                 mov     eax, off_40860C
  10. CODE:004B4780                 call    unknown_libname_40 ; Delphi 5 Visual Component Library
  11. CODE:004B4785                 call    @System@@RaiseExcept$qqrv ; System::__linkproc__ RaiseExcept(void)
  12. CODE:004B478A
  13. CODE:004B478A loc_4B478A:                             ; CODE XREF: sub_4B4754+1E.j
  14.  

Очевидно, что перед нами проверка корректности Blowfish-ключа: если его длина (в ebx) ненулевая и меньше либо равна 56, исполнение продолжается, если нет - возбуждается исключение. Эта проверка - часть довольно большой функции sub_4B4754 (назовём её BlowInit), которая предположительно вычисляет подключи. Чтобы убедиться в этом проанализируем код в районе выше и ниже по листингу. При этом обнаруживаются следующие факты:

Во-первых, функция BlowInit вызывается согласно конвенкции thiscall и принимает четыре параметра (это видно по прологу и сгенерированному Идой заголовку функции и использованию регистров):

Код (Text):
  1.  
  2. CODE:004B4754 var_20          = dword ptr -20h
  3. CODE:004B4754 var_1C          = dword ptr -1Ch
  4. CODE:004B4754 var_18          = dword ptr -18h
  5. CODE:004B4754 var_10          = dword ptr -10h
  6. CODE:004B4754 var_C           = dword ptr -0Ch
  7. CODE:004B4754 var_8           = dword ptr -8
  8. CODE:004B4754 var_4           = dword ptr -4
  9. CODE:004B4754 arg_0           = dword ptr  8
  10. CODE:004B4754
  11. CODE:004B4754                 push    ebp
  12. CODE:004B4755                 mov     ebp, esp
  13. CODE:004B4757                 add     esp, 0FFFFFFE0h
  14. CODE:004B475A                 push    ebx
  15. CODE:004B475B                 push    esi
  16. CODE:004B475C                 push    edi
  17. CODE:004B475D                 mov     ebx, ecx
  18. CODE:004B475F                 mov     [ebp+var_8], edx
  19. CODE:004B4762                 mov     [ebp+var_4], eax
  20. CODE:004B4765                 mov     esi, [ebp+arg_0]
  21. CODE:004B4768                 lea     edi, [ebp+var_18]
  22.  
Аргументы функции:
  1. eax - указатель на объект, назовём его TBlow
  2. edx - какой-то указатель
  3. ecx - длина ключа
  4. один аргумент в стеке - указатель

Во-вторых, функция BlowInit вызывается из двух мест в программе


  1. С адреса 004E19EF:
    Код (Text):
    1.  
    2. CODE:004E19D9                 mov     eax, ds:off_52AC8C
    3. CODE:004E19DE                 push    eax
    4. CODE:004E19DF                 mov     edx, ds:off_52ADBC
    5. CODE:004E19E5                 mov     eax, ds:off_52AF7C
    6. CODE:004E19EA                 mov     ecx, 0Ah
    7. CODE:004E19EF                 call    BlowInit
    8.  
    С, как мы видим, следующми аргументами:
    1. eax = off_52AF7C = offset dword_52D9A4
    2. edx = off_52ADBC
    3. ecx = 10
    4. off_52AC8C - через стек

  2. С адреса 00504EDB:
    Код (Text):
    1.  
    2. CODE:00504EBD                 call    GetTickCount_0
    3. CODE:00504EC2                 mov     ds:dword_526644, eax
    4. CODE:00504EC7                 push    offset unk_526620
    5. CODE:00504ECC                 mov     edx, offset dword_526644
    6. CODE:00504ED1                 mov     eax, offset unk_52D9A4
    7. CODE:00504ED6                 mov     ecx, 4
    8. CODE:00504EDB                 call    BlowInit
    9.  
    С аргументами:
    1. eax = dword unk_52D9A4
    2. edx = указатель на переменную хранящую текущий тик таймера
    3. ecx = 4
    4. offset unk_526620 в стеке

В-третьих, в районе адресов 004B478A - 004B4817 путем последовательных вызовов функции sub_4029C4 (копирует ecx байт с eax в edx) и sub_403324, которую у меня Ида распознаёт как @@FillChar (заполняет строку длиной edx по адресу eax байтом cl) инициализируются поля объекта TBlow. В итоге TBlow содержит следующие поля:

  1. Два блока по 8 байт, копируемых с буфера, на который указывает 4-й аргумент BlowInit
  2. 4096 байт, копируемых с адреса 0052472C
  3. 72 байта, копируемых с адреса 005246E4

Теперь самое время вспомнить, что для генерации подключей Blowfish использует P-массив размером в 72 байта и S-массив размером 4096 байт. В стандартной реализации для их инициализации используется число пи в двоичном виде без начальной тройки. Оно (точнее, первые 4096+72 байт) приведено в в этой статье. Как видим, реализация в Nassi не стала исключением. (К слову, если скормить Nassi.exe PEiD, он найдёт в ней несколько криптокомпонентов, причём в качестве сигнатур используются как раз эти четыре с небольшим килобайта - число пи)

И так, код инициализации Blowfish нами найден. Самое время найти функции шифровки и расшифровки. Очевидно, что они обращаются к полям объекта TBlow. Ставим бряк на 52D9A4 и аттачимся к уже запущенному процессу (чтоб не ловить инициализацию). После минутного эксперимента обнаруживается, что бряки часто срабатывают, когда программа обращается к TBlow с адреса 004B4A4E и иногда с 004029А3

Посмотри на код в районе 004B4A4E. Этот адрес находится в теле функции sub_4B4A30, которая вызывается по 16 раз из тел функций sub_4B4A78 и sub_4B4C58. В свою очередь sub_4B4A78 вызывается дважды с BlowInit и один раз с sub_4B4E38, которая в свою очередь вызывается в цикле практически сразу перед отправкой пакета функцией sendto:

Код (Text):
  1.  
  2. CODE:0050444B                 xor     ebx, ebx
  3. CODE:0050444D
  4. CODE:0050444D loc_50444D:                             ; CODE XREF: sub_5042D4+199.j
  5. CODE:0050444D                 mov     eax, ebx
  6. CODE:0050444F                 shl     eax, 3
  7. CODE:00504452                 add     eax, 3
  8. CODE:00504455                 lea     ecx, buf[eax]
  9. CODE:0050445B                 lea     edx, buf[eax]
  10. CODE:00504461                 mov     eax, offset dword_52D9A4
  11. CODE:00504466                 call    sub_4B4E38
  12. CODE:0050446B                 inc     ebx
  13. CODE:0050446C                 dec     esi
  14. CODE:0050446D                 jnz     short loc_50444D
  15. CODE:0050446F
  16. CODE:0050446F loc_50446F:                             ; CODE XREF: sub_5042D4+174.j
  17. CODE:0050446F                 mov     eax, offset dword_52D9A4
  18. CODE:00504474                 call    sub_4B4EBC
  19. CODE:00504479                 mov     [ebp+to.sa_family], 2
  20. CODE:0050447F                 mov     eax, ds:off_52B3B8
  21. CODE:00504484                 mov     ax, [eax]
  22. CODE:00504487                 push    eax             ; hostshort
  23. CODE:00504488                 call    htons
  24. CODE:0050448D                 mov     word ptr [ebp+to.sa_data], ax
  25. CODE:00504491                 mov     eax, [ebp+hostlong]
  26. CODE:00504494                 push    eax             ; hostlong
  27. CODE:00504495                 call    htonl
  28. CODE:0050449A                 mov     dword ptr [ebp+to.sa_data+2], eax
  29. CODE:0050449D                 push    10h             ; tolen
  30. CODE:0050449F                 lea     eax, [ebp+to]
  31. CODE:005044A2                 push    eax             ; to
  32. CODE:005044A3                 push    0               ; flags
  33. CODE:005044A5                 mov     eax, [ebp+len]
  34. CODE:005044A8                 push    eax             ; len
  35. CODE:005044A9                 push    offset buf      ; buf
  36. CODE:005044AE                 mov     eax, ds:dword_526634
  37. CODE:005044B3                 push    eax             ; s
  38. CODE:005044B4                 call    sendto
  39.  

Функция sub_4B4C58 вызывается из тела sub_4B4E6C которая вызывается в очень похожем цикле в теле sub_518534:

Код (Text):
  1.  
  2. CODE:005186A0                 xor     ebx, ebx
  3. CODE:005186A2
  4. CODE:005186A2 loc_5186A2:                             ; CODE XREF: sub_518534+193.j
  5. CODE:005186A2                 mov     eax, ebx
  6. CODE:005186A4                 shl     eax, 3
  7. CODE:005186A7                 add     eax, 3
  8. CODE:005186AA                 mov     edx, [ebp+var_11C]
  9. CODE:005186B0                 lea     ecx, [edx+eax]
  10. CODE:005186B3                 mov     edx, [ebp+var_11C]
  11. CODE:005186B9                 add     edx, eax
  12. CODE:005186BB                 mov     eax, ds:off_52AF7C
  13. CODE:005186C0                 call    sub_4B4E6C
  14. CODE:005186C5                 inc     ebx
  15. CODE:005186C6                 dec     esi
  16. CODE:005186C7                 jnz     short loc_5186A2
  17.  

Данные циклы так и просятся, чтобы их назвали циклами кодирования и декодирования пакетов блоками по 8 байт со смещения +3. Однако в отличии от цикла loc_50446F, который находится практически сразу перед вызовом sendto, что выдаёт его "вину", никаких winsock выше loc_5186A2 не видно. Вместо них мы находим заголовок функции sub_518534 вызываемой из sub_512E44, которая в свою очередь вызывается по таблице. Однако, если посмотреть на код следующий за вызовом recvfrom по адресу 0050384B, обнаруживается, что в случае удачного вызова какому-то окну (с классом TfrmNassiMain, что, впрочем, не важно) отсылается сообщение 40Ah = WM_USER + 0Ah, wParam которого указывает на буфер содержащий в том числе и принятый пакет

Код (Text):
  1.  
  2. CODE:005038DB                 mov     eax, [ebp+lParam]
  3. CODE:005038DE                 call    sub_40278C
  4. CODE:005038E3                 mov     ebx, eax
  5. CODE:005038E5                 mov     edx, ebx
  6. CODE:005038E7                 lea     eax, [ebp+var_8]
  7. CODE:005038EA                 mov     ecx, 4
  8. CODE:005038EF                 call    sub_4029C4
  9. CODE:005038F4                 lea     edx, [ebx+4]
  10. CODE:005038F7                 lea     eax, [ebp+PerformanceCount]
  11. CODE:005038FA                 mov     ecx, 8
  12. CODE:005038FF                 call    sub_4029C4
  13. CODE:00503904                 lea     edx, [ebx+0Ch]
  14. CODE:00503907                 lea     eax, [ebp+buf]
  15. CODE:0050390D                 mov     ecx, [ebp+wParam]
  16. CODE:00503910                 call    sub_4029C4
  17. CODE:00503915                 mov     eax, [ebp+lParam]
  18. CODE:00503918                 push    eax             ; lParam
  19. CODE:00503919                 push    ebx             ; wParam
  20. CODE:0050391A                 push    40Ah            ; Msg
  21. CODE:0050391F                 mov     eax, ds:off_52B35C
  22. CODE:00503924                 mov     eax, [eax]
  23. CODE:00503926                 call    @Controls@TWinControl@GetHandle$qqrv ; Controls::TWinControl::GetHandle(void)
  24. CODE:0050392B                 push    eax             ; hWnd
  25. CODE:0050392C                 call    PostMessageA
  26.  

Если поставить бряки на 0050392C и 00512E44 и посмотреть на значения в регистрах и памяти, на которую они ссылаются, то хорошо видно, что sub_512E44 не что иное как процедура обработки сообщения 40Ah

Теперь у нас есть практически все знания необходимые для создания собственного кодера/декодера пакетов в виде отдельной dll-ки. Осталось только узнать, каким ключем осуществляется шифрование. Для этого ставим бряк на функцию BlowInit и (на всякий случай) на объект TBlow. До отправки кодирования первого пакета бряк на BlowInit срабатывает дважды. Первый раз Nassi использует 4-байтный ключ - тик таймера, второй раз 10-байтный ключ с адреса 00526614. При этом параметр передающийся через стек указывает на 8-байтный блок по адресу 00526620. По-видимому, этот блок Очевидно, что подключи генерируемые первым вызовом игнорируются и шифровка/расшифровка использует только результат второго вызова.

Стартуя с BlowInit, loc_5186A2 и loc_50444D проходим по вложенным вызовам и выдираем из листинга данные и код (благо их немного), подчищаем, редактируем, создаём удобные нам входы для BlowInit, CryptPacket и DecryptPacket и компилируем. Подробнее я описывать не буду, т. к. это быстрее сделать, чем рассказать. В файлах идущих со статьёй есть dll-ка с "исходниками". В комментариях указаны адреса, с которых выдрана та или иная функция, так что разобраться думаю будет не сложно.

Заголовок

Начинать исследовать поля пакета можно тремя методами:

  1. Анализировать конструктор
  2. Анализировать получателя
  3. Связать вместе снифер и декодер и анализировать сами пакеты

Начнём с конструктора. Беглым просмотром от начала функции sub_5042D4 до цикла loc_50444D можно восстановить приблизительную структуру пакета:

СмещениеРазмерНазначение
01Версия протокола; в нашем случае всегда 1
12Размер пакета
Далее все данные кодируются
31Неизвестный байт
44Неизвестный dword
81Неизвестный байт
94Неизвестный dword
13...Неизвестный блок
......Неизвестный блок
...8Время отправки пакета в формате TDateTime
Далее предположительно идут собственно данные пакета

Неизвестный байт по смещению +8 передается функции-сендеру (sub_5042D4), как первый параметр (в dl). Сама же функция вызывается с нескольких десятков мест с разными константными значениями dl. Предположительно этот байт определяет тип пакета

Неизвестные блоки, как это видно под отладчиком, - имя хоста и ник-нейм в виде pascal-строк, а двойное слово по +4 - инкрементальный счётчик.

Больше на поверхности здесь ничего вроде бы не лежит, поэтому можно перейти к анализатору пакетов - функции sub_518534. В ней мы найдём, что байт +3 должен быть меньше 2 (на практике - всегда 1), а байт +8 действительно определяет тип пакета (по адресу 005188CC и ниже распологается ветвление). Также легко обнаружить, что двойное слово +9 есть цвет пользователя в формате RGB

Итого заголовок Nassi-пакета имеет следующий вид:

СмещениеРазмерНазначение
01Версия протокола; в нашем случае всегда 1
12Размер пакета
Далее все данные кодируются
31Всегда 1
44Счётчик
81Тип пакета, Nassi-команда
94Цвет пользователя
13...Pascal-строка, хост-нейм
......Pascal-строка, ник-нейм
...8Время отправки пакета в формате TDateTime
Далее идут собственно данные пакета

Тело

Чтоб выяснить ещё что-либо я написал снифер и прикрутил к нему декодер пакетов. Снифер вместе с исходниками приведён во вложении. Подробно описывать его не буду, скажу лишь, что он использует сырые сокеты для перехвата (информации об этом в сети достаточно), APC для корректного прерывания перехвата (см. статью Проблемы реализации многопоточных WinSockets-приложений в 11-м номере PC Magazine за 2005 год) и алгоритм приведённый в Зубкове для печати чисел в hex-формате. Остальное вроде бы вопросов вызывать не должно. Снифер игнорирует фрагментированные IP-пакеты, однако для исследования протокола это не важно

Написав снифер мы получаем относительно удобный инструмент для анализа пакетов и в короткое время можем узнать формат наиболее важных из них.
Например:

СмещениеРазмерНазначение
Тип 36: Смена состояния
01Номер состояние:
0.. 5 - встроенное (Свободен, Играю, Работаю, Не беспокоить, Секретное, Отошёл),
6.. 15 - определяемое пользователем
11Тип состояния: 1 для встроенного
2...Pascal-строка - название состояния
......Pascal-строка - описание состояния
Тип 10: Сообщение в канал
01Тип канала:
0 - обычный
1 - общий для всех
2 - запароленный
3 - приватный
11Pascal-строка, название канала
......Pascal-строка, пароль (только для 2-го типа)
......ASCIIZ-строка, сообщение в канал
и т. д.

Всего типов сообщений - несколько десятков, для практических целей могут понадобиться:
2 - Запрос информации о пользователе
3 - Оповещение об активности окна
10 - Сообщение в канал
13 - Оповещение о присоединении к каналу
14 - Оповещение о выходе из канала
30 - Личное сообщение
35 - Оповещение о смене ника
36 - Оповещение о смене состояния
37 - Запрос состояния
130 - Сообщение информации о себе
и, возможно, некоторые другие

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

  1. Написание собственного клиента или бота (Существующие боты работают через raw-сокеты только параллельно чату, а сам чат - лучший детектор ОС в сетях, где им активно пользуются :smile3: )
  2. Написание флудилок
  3. Чтение сообщений из запароленных каналов
  4. Вместе с ARP-спуфером - перехват личных сообщений и сообщений из приватных каналов
  5. Написание nassi-шлюзов для общения в чате пользователей из различных сегментов
  6. Написание шлюзов а-ля nassi2irc
  7. Совместо с известным нюкером nassidos.pl написание полезных утилит (например: автокикалка любителей создавать общие для всех каналы, рассылать всплывающие сообщения, употребляющих "плохие слова", флудящих, имеющих ненравящийся вам цвет :smile3: и т. п.)
    (Примечание: В отличии от версии 2.х, насколько мне известно, серьёзной уязвимости, позволяющей написание эксплоита, в 3.х найдено не было. Однако 3.х подвержена DoS-атаке: программа не всегда корректно обрабатывает "мусорные пакеты" и может аварийно завершить работу. Подробнее об этом можно прочитать в частности на SecurityLab. Gracebyte выпустили два патча для устранения проблемы. При установке они обновляют версию Nassi до build 2261 (первый глючный патч) и 2262 соответственно, однако в настоящее время скачать их с сайта разработчиков нельзя - очевидно таким образом они пытаются убедить юзеров перейти на версию 4.х)

Файлы к статье находятся здесь.

© Mescalito

0 1.214
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532