作者:凯米宝贝青岛Pacific早教中心 | 来源:互联网 | 2023-10-15 18:03
by fanxiushu 2022-10-17 转载或引用请注明原始作者。
xFsRedir软件其中之一的功能就是实现了全方位的网络代理,从主机代理,到本地代理。
可以灵活的设置代理规则。代理规则可以包括本地程序名(主机代理对应内网的源ip),目标ip,目标port等规则。
可以说是提供了一个全面的网络代理功能。
(比如我最近比较热衷的,购买的云服务器只有内网IP,然后通过其他手段映射出公网IP。
其实绝大部分云服务器都是这种模式。我现在想直接访问云服务器的内网IP,
当然可以通过xFsRedir的虚拟局域网功能组成一个虚拟局域网,但是这样也会把自己的电脑暴露出去,毕竟是虚拟局域网。
使用xFsRedir的代理功能,把代理服务器设置成此云服务器公网地址,把代理规则的目标ip范围设置成云服务器的内网ip,
这样就能成功访问云服务器的内网ip,而且也不会暴露自己电脑的内网地址。)
但是之前实现的版本,也有个缺憾,就是对域名的代理处理得并不成功。
之前的xFsRedir版本为了拦截域名的解析,往往需要全部拦截,如下图所示
(至于为何要拦截域名解析,下面会有详细的解释):
如上图所示, 把目标端口设置为 53, 设置代理服务器信息,其他都不设置,这样目标端口为53的数据都会朝代理服务器发送。
53目标端口,就是固定的dns域名解析服务器端口。
这么做会造成整台电脑的域名解析都通过代理来处理,
这样的结果不言而喻,会影响整个系统访问网络。
在网络代理中,我们为何需要处理域名解析呢?
举个简单例子:
比如我们访问www.baidu.com网站,
我们的电脑在上海,而代理服务器在深圳。
如果不做域名解析代理的话,我们的电脑系统默认解析www.baidu.com域名,
根据就近原则,解析www.baidu.com实际服务器地址会跟我们的电脑在同一个城市,都在上海。
于是接下来,我们的电脑通过代理来访问baidu网站,实际过程是:
我们的电脑连接到深圳的代理服务器,深圳的代理服务器然后回过来连接上海的baidu服务器地址。
等于是绕了一圈,这种代理还不如不用代理呢?
还有大家应该都听过的域名污染问题,就是为了阻止某些网站访问,
某些域名解析服务器要么返回一个空结果,要么返回一个乱七八糟的地址。
这是一种简单而经济的阻断手段。
但并不是全世界的域名解析服务器都会阻断这些网站,除非是真的是那种全世界都在抵制的网站。
只需手动设置能解析这些网站的域名解析服务器,但是这样跟上面xFsRedir设置53端口的代理一样,都是全局性的。
都会影响整个系统访问其他网络。
我们来看看xFsRedir对域名解析的拦截处理过程。
xFsRedir是使用wfp驱动实现网络数据包的拦截处理。
xFsRedir中,所有端口是53的udp数据包都会被拦截到,这个就是dns域名解析的请求包和应答包。
然后解析里边的dns数据包格式,找到域名和ip对应关系。如下图才会尽最大可能找到ip和域名的对应关系:
同时在解析dns域名的应答包,找到对应域名和ip地址关系之后,根据xFsRedir配置中设置的目标域名规则,
把解析出的ip地址动态添加到xFsRedir的目标代理ip中,这样对这个域名的对应域名的ip地址访问就会走代理了。
举个例子:
比如代理规则中的目标ip范围里设置了:*github*; *google*;
在解析dns应答包中,如果域名匹配到了github和google这些字符串,则把这个域名解析出的对应ip地址添加到代理的目标ip地址中。
这样对这些域名的ip地址访问,就直接走代理了。
不过之前的xFsRedir版本只解析了dns应答包,并没解析和处理dns请求包。
也就意味着,之前的xFsRedir版本的域名解析还是使用系统默认解析服务器。
在即将升级的新版本中,添加了这个功能。
但是windows系统对域名的解析过程复杂,主要是里边的缓存机制复杂,
使得仅仅依靠wfp网络驱动中拦截53端口来解析dns数据包,还不能完全实现代理规则中的代理需求。
主要是无法实现具体进程解析域名的绑定需求。
这也是写这篇文章的主要目标。
我们在wfp网络驱动中拦截端口是53的udp数据包,并且在确认是dns请求包的时候,
根据代理规则(需要代理的本地进程,目标域名等),
一旦匹配到这个dns请求包属于代理规则内,则把这个数据包转发到代理服务器上去。
我们举例来分析这个过程。
假设我们要代理 github.com 这个域名。
这里又可以分成两种情况:
1,电脑上的任何程序访问github.com都会走代理。
2,只允许某个程序(比 chrome.exe 程序)访问github.com走代理,其他程序访问github不走代理。
对于第一种情况,不论什么程序,在解析github.com域名的时候,
我们都会在wfp驱动中通过拦截53端口,拦截到github.com域名解析请求,从而把这个dns解析请求转发到代理服务器。
于是第一种情况,我们通过wfp驱动,无论如何都能正确路由域名解析。
对于第二种情况,就挺复杂了。
这种情况我们是不可避免要遇到的,
比如有些网站我们可能只知道主要域名,它可能还有许多其他域名,我们不大可能都全部都找到并且填写到代理规则中。
这个时候,专门使用一个浏览器程序比如chrome.exe,让这个程序全部走代理。
也就是对应规则中的:本地程序规则是 chrome.exe, 域名规则是 *
总结起来说,第二种情况,我们要实现的目标是:找到发起域名请求的进程和域名的关联关系,并且自己处理dns请求。
windows中对域名的解析是有缓存机制的。
其实任何现代系统,不管是linux,macos,Android,ios等,都有dns解析缓存机制。
不过windows的dns缓存机制似乎有点复杂,如果真要详细去说,多篇文章都说不完。
现在只是简单解释windows的dns解析缓存机制。
我们在windows系统中,调用 gethostbyname,getaddrinfo这些通用解析dns域名的接口函数的时候,
并不是在这些接口函数内部创建udp 的socket套接字,然后构建dns请求包,然后通过网络发送给域名解析服务器。
如果真是这样,那我们在wfp网络驱动中,就能直接拦截这个dns请求,并且还能知道是那个进程发起的dns请求。
而现实的情况是:
这些函数会首先查询本地缓存,而这个本地缓存不是调用这些接口函数的某个程序的本地缓存,而是windows系统全局的本地缓存。
而代理这个缓存的windows服务就是一个叫 Dnscache的服务,也叫做 DNS Client。
dnscache服务是以系统 svchost.exe 作为宿主进程运行的。
其他进程的接口函数,通过RPC等进程之间的通信机制,在dnscache服务器查询某个域名是否有缓存,
如果有缓存,直接从缓存取。
如果没有找到,则从网络获取,从网络获取也是dnscache这个服务发起的请求,
而不是某个具体进程请求发起dns网络请求。
这样造成的结果就是:我们在wfp驱动拦截的53端口的dns请求包,都是清一色 svchost.exe 这个程序发起的请求。
于是在dnscache缓存开启的情况下,我们是不可能从wfp网络驱动中定位哪个进程发起的dns请求的。
得想其他办法来获取dns请求和具体进程的关系。
其中一个简单粗暴的办法,就是禁用Dnscache服务。
禁用dns缓存服务之后,dns查询接口函数无法从缓存获取,它就只能老老实实的发起网络请求。
于是wfp网络驱动中就能拦截到dns请求和具体进程关系。
而这个办法总体来说并不可取。
因为这些操作都是全局性的,会影响电脑整体访问网站的性能。
可以想象一下,禁用dns缓存服务之后。之后所有的域名解析请求都会发起网络请求,
有些网站可能几百个子域名,每次都要发起网络解析请求,整个网站的浏览速度估计也快不到哪去。
也许我们可以禁用dnscache之后,在wfp驱动中再次对这些域名做缓存。
但是这个做法工作量大不说,还无法与dnscache功能完整融合,也就是可能会影响其他基于dnscache服务的程序。
dnscache开启情况下,网络驱动无法解决。
因为其他进程和dnscache是通过RPC进程间通信机制来交换dns信息,所以其他驱动其实也插不上手。
也许我们可以考虑拦截rpc通信,但是这个做法也难,因为别说拦截难,rpc通信协议内容我们也不清楚,也没公开。
于是我们干脆在具体的dns查询接口函数上打主意。
挂钩(hook)具体的接口函数,比如挂钩(hook)gethostbyname接口等函数。
这些接口函数可不止gethostbyname,还包括 getaddrinfo,
WSALookupServiceBegin,WSALookupServiceNext,
DnsQuery_Xxx
至少我知道的起码有以上这些接口函数,应该还有更多,不过到时可以根据具体程序来查漏补缺。
至于如何hook具体程序的具体这些接口函数,这里不再赘述,反正是能实现的。
有兴趣可以去查阅我讲述的远程桌面系列文章中关于dxhook的相关文章,里边有阐述如何hook接口函数的内容。
除了hook具体的接口函数之外,难道没有别的办法了吗?
自然是有的,windows系统提供了NSP (Name Space Provider) 机制,类似于winsock中的LSP机制。
其实性质也是做具体接口函数的hook。
NSP的大致实现办法:
我们需要开发一个dll动态库,这个动态库必须导出 NSPStartup 接口函数,
而在这个接口函数中,会提供 NSP_ROUTINE 结构的参数,
里边我们必须填写正确对应的回调函数,其中包括三个核心函数:
NSPLookupServiceBegin
NSPLookupServiceNext
NSPLookupServiceEnd
只要dns查询接口函数发起调用,这三个核心函数就会被调用。
于是我们在此回调函数中就能找到dns域名和具体进程的对应关系。
并且把查询dns的请求正确路由到我们的代理中。
但是这里的回调函数调用与我们通常理解的hook又有些不同,
具体下面会讲述,这个也是把dns请求正确路由到我们的代理中的必须考虑的。
而且细心就会发现,系统导出的win32 api函数
WSALookupServiceBegin, WSALookupServiceNext, WSALookupServiceEnd
与上面的三个核心回调函数很像。
确实如此,比如 WSALookupServiceBegin调用的时候,就是查询nsp服务列表,然后调用这些nsp服务的回调函数,
也就是如果我们注册了自己的nsp服务,最终WSALookupServiceBegin 也会调用我们自己NSPLookupServiceBegin回调函数。
而系统的gethotbyname,getaddrinfo等其他函数,内部是通过调用 WSALookupServiceBegin 接口函数实现的。
这样就等于是一环扣一环,最终都会正确路由到nsp的回调函数中。
接着我们在程序中,调用 WSCInstallNameSpace 函数,安装我们的这个dll动态库,
这样我们nsp就能正常工作了。
是不是很简单!
不过也别高兴的太早,让nsp老老实实的为我们服务,也不大容易。
系统中存在一个系统默认的nsp服务,这个服务就是沟通dnscache,发起真正的dns请求nsp服务。
也就是如果不出意外的话,系统默认的nsp都会被调用。
举个例子,有三个NSP服务:
myself(M)我们自己的nsp; other(O)其他NSP; System(S)系统默认的NSP。
现在假设这三个服务的安装顺序是:M->O->S
按照我们通常hook的理解,当某个函数返回成功,就不再继续调用下一链条的函数。
比如 如果M.NSPLookupServiceBegin成功之后,
就不调用 O.NSPLookupServiceBegin 函数和S.NSPLookupServiceBegin 函数。
但实际情况是: 不管成功与否,都会按如下顺序,所有nsp的函数都会调用一遍:
M.NSPLookupServiceBegin -> O.NSPLookupServiceBegin ->S.NSPLookupServiceBegin
同样的,
M.NSPLookupServiceNext -> O.NSPLookupServiceNext -> S.NSPLookupServiceNext
也会跟着这样顺序调用一遍,并且最要命的是,会把 *Next函数的调用结果进行合并:
比如 M查询到地址是 1.1.1.1, S查询到的地址是 2.2.2.2,于是返回的DNS结果就有 1.1.1.1和2.2.2.2两个。
我们实现NSP的目标是完全拦截对dns请求,也就是只要处于设置的规则范围内的域名解析,
都交给我们自己处理,也不能再次通过dnscache缓存,再去查询一遍。
其他nsp我们暂时可以不必去关心,最讨厌的是如何让系统的nsp别调用,或者调用了也让它返回空值。
好在还是有办法解决这个问题:
在NSPLookupServiceBegin 函数中第2个参数,是 WSAQUERYSETW 结构的参数。
WSAQUERYSETW 里边有个参数 dwNameSpace, 这个就是关键所在。
这个dwNameSpace 有多个选项,比如NS_DNS就是dns查询,具体含义可以去msdn查询。
系统的nsp在调用NSPLookupServiceBegin 时候是会检测这个参数的。
如果我们把这个参数改成别的值,那系统nsp就会错误的调用。
最通用的就是设置 dwNameSpace = NS_TCPIP_HOSTS ,
这样让系统默认的nsp只查询 本地 /etc/hosts文件中域名对应关系,自然大部分情况下都查不到。
光这样设置还不够,因为上面说了,NSPLookupServiceBegin 是按照上面的安装顺序来调用的。
如果系统默认nsp在我们的nsp之前调用,其实是没效果的。
而WSCInstallNameSpace 函数安装后,我们的nsp是排到最后的,也就是 O->S->M 这样的顺序。
因此我们还得排个序,调用WSCWriteNameSpaceOrder来排序,排到最前面,变成 M->O->S 这样的顺序。
这样,我们的nsp才算基本上为我们服务。
至于如何截取dns域名和具体进程关系,我们在xFsRedir中如何再次处理dns请求,
这里不再赘述。
有兴趣可以下载 xfsredir使用,最新版本以后会更新上去。
GitHub - fanxiushu/xFsRedir