Сетевое сканирование на Xerox 3220 при подключенном VPN

в 15:14, , рубрики: GetAdaptersInfo(), vpn, системное администрирование, системное программирование, метки:

Суть проблемы

МФУ Xerox 3220 поддерживает сетевое сканирование через приложение (Network Scan), установленное на один из компьютеров в сети, в котором это МФУ регистрируется по IP адресу. Но однажды производственная необходимость потребовала пользоваться на данном компьютере VPN подключениями (Cisco VPN Client и OpenVPN). И в момент подключения любого из соединений связь со сканером из приложения моментально терялась. О моем процессе исследования и решении этой проблемы и будет дальше идти речь.

Первый взгляд

Сразу же возникшую мысль о подменяемых шлюзах по-умолчанию, потенциальном заворачивании всего трафика в VPN отбрасываем как нерабочую, т.к. всего этого нет ни в одном из используемых подключений. Web-интерфейс сканера по-прежнему доступен, в конце-концов, он банально пингуется. Тогда мне, как сетевому инженеру в первую очередь, стало интересно как происходит обнаружение сканера в сети.
Для этого я воспользовался Wireshark, запустив его без VPN подключения на локальном сетевом интерфейсе. Обнаружил, что приложение отправляет широковещательный udp пакет на 1124 порт, на что сканер и откликается. Теперь подключаем VPN и на локальном интерфейсе полная тишина, никаких даже исходящих пакетов.

А что же там внутри?

Ну что ж, очевидно, что программа почему-то начинает слать пакеты не в тот интерфейс, и ей там, конечно, никто не откликается. Как это исправить, мне было совершенно непонятно, т.к. если бы приложение руководствовалось таблицей маршрутизации вместе с адресом сканера, или еще чем-то вменяемым, то все должно было быть нормально, однако это было не так. Значит, нужно хотя бы понять алгоритм выбора интерфейса, а осуществить я это решил с помощью OllyDbg.
Запустив под отладчиком NSCSysUI_XEROX.exe (собственно приложение сетевого сканирования, к которому прилагался ряд *.dll), я, ожидаемо, ничего не понял, т.к. это хоть и не первый мой опыт реверс-инжиниринга, но и предыдущие особенным успехом не отличались.
Сетевое сканирование на Xerox 3220 при подключенном VPN
Обнаружение сканера явно происходило в том числе после нажатия кнопки «Обновить», и я обратил внимание, что в OllyDbg в этот момент происходит какой-то отладочный строковый вывод. Последним сообщением являлось как раз о вроде как выбранном IP адресе интерфейса:
Сетевое сканирование на Xerox 3220 при подключенном VPN
За эту строку «Selected NIC IP» я и решил зацепиться. Попытался ее сразу же поискать через Search For — All referenced strings, но там ее не оказалось, что повергло меня в некоторое уныние и долгое F7 от точки входа в .exe файл. В результате выяснилось, что совершенно неудивительно отсутствие строки там, где я ее искал, т.к. этот отладочный вывод принадлежит вызову из NSCProtocol_XEROX.dll, в котором то уже строка успешно находится, и, ура-ура, вот эта процедура с выводом сообщения:

003B737D  |.  FF75 DC       PUSH DWORD PTR SS:[EBP-24]               ; /<%s>
003B7380  |.  8D45 F0       LEA EAX,[EBP-10]                         ; |
003B7383  |.  C645 FC 12    MOV BYTE PTR SS:[EBP-4],12               ; |
003B7387  |.  68 04A13D00   PUSH OFFSET 003DA104                     ; |Format = "Selected NIC IP : %s"
003B738C  |.  50            PUSH EAX                                 ; |Arg1
003B738D  |.  E8 D6F70000   CALL 003C6B68                            ; NSCProtocol_XEROX.003C6B68

Но это пока неинтересно, ищем, что же там вообще в этой процедуре. Первым делом в ней глаз зацепился за вызов функции gethostbyname(), которая, как говорит нам MSDN, в том числе возвращает весь список IP адресов по указанному символьному имени.

003B6FCA  |.  50            PUSH EAX                                 ; /Arg1, в EAX имя хоста
003B6FCB  |.  E8 1EF70000   CALL <JMP.&WSOCK32.#52>                  ; WS2_32.gethostbyname
003B6FD0  |.  8BF8          MOV EDI,EAX                              ; поместили в EDI ответ от hostbyname (в EAX)
003B6FD2  |.  3BFE          CMP EDI,ESI                              ; проверка на ноль ответа от hostbyname
003B6FD4  |.- 74 34         JE SHORT 003B700A
003B6FD6  |.  8B47 0C       MOV EAX,DWORD PTR DS:[EDI+0C]            ; взяли ссылку на первый IP адрес из полученных
003B6FD9  |.  8B00          MOV EAX,DWORD PTR DS:[EAX]               ; взяли сам IP адрес
003B6FDB  |.  FF30          PUSH DWORD PTR DS:[EAX]                  ; /Arg1
003B6FDD  |.  E8 06F70000   CALL <JMP.&WSOCK32.#11>                  ; WS2_32.inet_ntoa
003B6FE2  |.  50            PUSH EAX                                 ; /Arg1, что-то с полученным символьным представлением сделали
003B6FE3  |.  8D4D D4       LEA ECX,[EBP-2C]                         ; |
003B6FE6  |.  E8 FD050100   CALL 003C75E8                            ; NSCProtocol_XEROX.003C75E8
003B6FEB  |.  8B47 0C       MOV EAX,DWORD PTR DS:[EDI+0C]            ; снова взяли ссылку на первый IP
003B6FEE  |.  8B00          MOV EAX,DWORD PTR DS:[EAX]               ; снова взяли сам первый IP
003B6FF0  |.  3BC6          CMP EAX,ESI
003B6FF2  |.- 74 16         JE SHORT 003B700A
003B6FF4  |.  33DB          XOR EBX,EBX                              ; обнуляем EBX перед проходом по массиву с IP адресами
003B6FF6  |>  FF30          /PUSH DWORD PTR DS:[EAX]                 ; /Arg1, каждый из IP адресов преобразовываем в текст
003B6FF8  |.  E8 EBF60000   |CALL <JMP.&WSOCK32.#11>                 ; WS2_32.inet_ntoa
003B6FFD  |.  8B47 0C       |MOV EAX,DWORD PTR DS:[EDI+0C]
003B7000  |.  83C3 04       |ADD EBX,4
003B7003  |.  8B0418        |MOV EAX,DWORD PTR DS:[EBX+EAX]
003B7006  |.  3BC6          |CMP EAX,ESI
003B7008  |.- 75 EC         JNE SHORT 003B6FF6                      ; и ничего не делаем! просто берем следующий адрес и снова по циклу
003B700A  |>  E8 D3F60000   CALL <JMP.&WSOCK32.#116>                 ; [WS2_32.WSACleanup, к этому моменту мы пробежались по всем IP адресам, но что-то полезное сделали только с первым

Комментарии говорят сами за себя. Увидев такую странную логику приложения, я подумал, что вот он ключ — программа просто использует первый из массива IP адресов, а по остальным просто впустую пробегает. Эту уверенность подкрепил тестовый вызов gethostbyname(), в возвращаемой структуре которого в массиве адресов на первом месте всегда появлялся IP адрес VPN подключения. И тут у меня возникло два решения. Первое — написать загрузчик, который перехватывал бы gethostbyname() для указанного процесса и возвращал, либо ставил на первое место нужный IP адрес. Но т.к. я такое уже когда-то делал, то это было не спортивно, и я решил попробовать второе — пропатчить dll хотя бы на примитивном уровне.
Я заменил весь указанный кусок кода на то, что по-моему более соответствовало моему случаю (хотя надо признаться, ничем не отличается от подхода приложения) и прогонял единственный call (считая, что он какой-то крайне полезный), участвующий в вышеуказанном коде не только по первому IP адресу, но по всем встретившимся:

003B6FD6      8B47 0C       MOV EAX,DWORD PTR DS:[EDI+0C]            ; взяли ссылку на первый IP адрес из полученных
003B6FD9      8B00          MOV EAX,DWORD PTR DS:[EAX]               ; взяли сам IP адрес
003B6FDB      33DB          XOR EBX,EBX
003B6FDD      FF30          PUSH DWORD PTR DS:[EAX]
003B6FDF      E8 04F70000   CALL <JMP.&WSOCK32.#11>                  ; Jump to WS2_32.inet_ntoa
003B6FE4      50            PUSH EAX
003B6FE5      8D4D D4       LEA ECX,[EBP-2C]
003B6FE8      E8 FB050100   CALL 003C75E8
003B6FED      8B47 0C       MOV EAX,DWORD PTR DS:[EDI+0C]
003B6FF0      83C3 04       ADD EBX,4
003B6FF3      8B0418        MOV EAX,DWORD PTR DS:[EBX+EAX]
003B6FF6      3BC6          CMP EAX,ESI                              ; каждый из IP адресов преобразовываем в текст
003B6FF8    ^ 75 E3         JNE SHORT 003B6FDD
003B6FFA      90            NOP
...                               
003B7009      90            NOP
003B700A  |>  E8 D3F60000   CALL <JMP.&WSOCK32.#116>                 

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

Момент истины

Тогда я спустился чуть ниже по листингу, и увидел вызов GetAdaptersInfo(), а за ним цикл на немало строк, который вроде бы пытается пройтись по каждой записи из возвращаемого функцией списка. На этот код я медитировал 2 дня. И я до сих пор до конца так и не понял логику, но моя версия такова: берется первая запись, для нее проверяется на тип интерфейса, ненулевой IP адрес, и длину символьного представления (? вот эта проверка для меня самая загадочная) шлюза по-умолчанию для этого интерфейса. Если тип подходит, адрес есть и символьное представление шлюза не нулевое, то выходим из цикла и, получается, этот адаптер и выбираем. А вот тут нюанс. Для моего VPN соединения шлюза по-умолчанию нет вообще, но тестовый вызов GetAdaptersInfo() на C++ с кодом из MSDN возвращает его не как отсутствующее значение, а как «0.0.0.0», соответственно и длина не нулевая, и этот интерфейс по всем параметрам подходит. А т.к. он первый в списке, то он и выбирается.
Тут я совсем загрустил, и как поправить этот код для корректной работы я не понимал. Но нужно просто внимательнее читать документацию. В описании функции сказано:

The GetAdaptersInfo function can retrieve information only for IPv4 addresses.
The order in which adapters appear in the list returned by this function can be controlled from the Network Connections folder: select the Advanced Settings menu item from the Advanced menu.

Если вы, как и я (а с администрированием windows я сталкиваюсь, начиная с win2000 server), раньше никогда не видели этого меню, то вот где оно в Win7:
Сетевое сканирование на Xerox 3220 при подключенном VPN

А если у вас, как и у меня, там даже меню нет, то вот как его включить:
Сетевое сканирование на Xerox 3220 при подключенном VPN

Где, конечно же, я увидел, что все мои VPN-адаптеры стоят выше локального сетевого подключения, переместив которое на самый верх, я решил изначальную проблему.

P.S. Но все же разработчики из Xerox могли бы отправлять пробные запросы и во все интерфейсы, а не только в первый попавшийся.

Автор: v0rdych

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js