Суть проблемы
МФУ Xerox 3220 поддерживает сетевое сканирование через приложение (Network Scan), установленное на один из компьютеров в сети, в котором это МФУ регистрируется по IP адресу. Но однажды производственная необходимость потребовала пользоваться на данном компьютере VPN подключениями (Cisco VPN Client и OpenVPN). И в момент подключения любого из соединений связь со сканером из приложения моментально терялась. О моем процессе исследования и решении этой проблемы и будет дальше идти речь.
Первый взгляд
Сразу же возникшую мысль о подменяемых шлюзах по-умолчанию, потенциальном заворачивании всего трафика в VPN отбрасываем как нерабочую, т.к. всего этого нет ни в одном из используемых подключений. Web-интерфейс сканера по-прежнему доступен, в конце-концов, он банально пингуется. Тогда мне, как сетевому инженеру в первую очередь, стало интересно как происходит обнаружение сканера в сети.
Для этого я воспользовался Wireshark, запустив его без VPN подключения на локальном сетевом интерфейсе. Обнаружил, что приложение отправляет широковещательный udp пакет на 1124 порт, на что сканер и откликается. Теперь подключаем VPN и на локальном интерфейсе полная тишина, никаких даже исходящих пакетов.
А что же там внутри?
Ну что ж, очевидно, что программа почему-то начинает слать пакеты не в тот интерфейс, и ей там, конечно, никто не откликается. Как это исправить, мне было совершенно непонятно, т.к. если бы приложение руководствовалось таблицей маршрутизации вместе с адресом сканера, или еще чем-то вменяемым, то все должно было быть нормально, однако это было не так. Значит, нужно хотя бы понять алгоритм выбора интерфейса, а осуществить я это решил с помощью OllyDbg.
Запустив под отладчиком NSCSysUI_XEROX.exe (собственно приложение сетевого сканирования, к которому прилагался ряд *.dll), я, ожидаемо, ничего не понял, т.к. это хоть и не первый мой опыт реверс-инжиниринга, но и предыдущие особенным успехом не отличались.
Обнаружение сканера явно происходило в том числе после нажатия кнопки «Обновить», и я обратил внимание, что в OllyDbg в этот момент происходит какой-то отладочный строковый вывод. Последним сообщением являлось как раз о вроде как выбранном IP адресе интерфейса:
За эту строку «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:
А если у вас, как и у меня, там даже меню нет, то вот как его включить:
Где, конечно же, я увидел, что все мои VPN-адаптеры стоят выше локального сетевого подключения, переместив которое на самый верх, я решил изначальную проблему.
P.S. Но все же разработчики из Xerox могли бы отправлять пробные запросы и во все интерфейсы, а не только в первый попавшийся.
Автор: v0rdych