PRX 通过LSP实现浏览器Socks5/Tcp代理(从发送数据上着手)

来源:互联网 发布:山下智久长泽雅美 知乎 编辑:程序博客网 时间:2024/06/06 00:33

本文阐述针对市面上主流的浏览器 实现基于Socks5协议Tcp代理部分原理 它是浏览器翻墙的一种方法 这只是在LSP实现方式中一种类别 它具备很多不同方式 但在本文中不在累赘;此方法适应“Chrome、Firebox、IE、OperaWeb”浏览器

本文中给出的代码思路是利用C/C++实现的 并且不会提供完整可运行的代码 只会给出一些程式关键代码 具体实现需要各位有兴趣的boys 你可以自行利用本文中提出的思路实现一次。

那么从发送数据上实现浏览器的socks5代理 有什么好处?有哪些缺点。本质上此方法用于实现浏览器代理是不错的 如果你需要全局代理却不是那么好的,lsp本身在设计上不是那么完善 如果希望利用它实现完美的全局代理是不可能的 它不止是ws_32.dll对于socket函数处理并路由lsp的问题 有些函数lsp是无法得知的。一些网路应用程序可能会使用这些函数绕过ws_32.dll调用lsp,它只可以实现相对的应用层全局代理 却不是真正意义上的系统全局代理 但这些实际上已经足够了 但是你可以考虑NDIS开发~

在lsp中你可以在无hook的前提下 劫持到两个可以支持tcp协议发送数据报的下层套接字函数,一个是WSASendMsg(sendmsg)、WSPSend 实际上在不同的系统中 send函数执行可以调用WSPSend 但有些却是不可以的;这会造成一个bug则是 如果应用程序先执行send、后执行WSASend那么 本文中提出的方式就会出现问题。

本文思路:当应用程序调用connect、WSAConnect、ConnectEx时 修改链接的服务器地址 然后保存服务器的地址 然后在应用程序第一次对此SOCKET调用WSASend、WSASendMsg时 开始socks5协议handshake过程 然后正常发送应用程序之间的数据包

以下是WSPConnect一个轻量级实现,而ConnectEx与此实现是类似的

int WSPAPI WSPConnect(SOCKET s,const struct sockaddr* name,int namelen,LPWSABUF lpCallerData,LPWSABUF lpCalleeData,LPQOS lpSQOS,LPQOS lpGQOS,LPINT lpErrno){TCHAR processname[MAX_PATH];GetModuleFileName(NULL, processname, MAX_PATH);Debugger::Write(L"%s WSPConnect ...", processname);if (s == INVALID_SOCKET || !Socks5ProxyFilter::Effective((struct sockaddr_in *)name)){WSPClenupContext(s); // 清理与此套接字连接绑定的的上下文return LayeredServiceProvider_Current.NextProcTable.lpWSPConnect(s, name, namelen, lpCallerData, lpCalleeData, lpSQOS, lpGQOS, lpErrno);}struct sockaddr_in server;memset(&server, 0, sizeof(struct sockaddr_in));server.sin_family = AF_INET;server.sin_port = htons(1080); // PORTserver.sin_addr.s_addr = htonl(INADDR_LOOPBACK);int error = LayeredServiceProvider_Current.NextProcTable.lpWSPConnect(s, (struct sockaddr*)&server, sizeof(struct sockaddr_in),lpCallerData, lpCalleeData, lpSQOS, lpGQOS, lpErrno);SocketBinderContext* binder = SocketMappingPool_Current.Get(s);if (binder != NULL){binder->EnterLook();{binder->AddressFrmily = name->sa_family;binder->PeerNameLen = namelen;binder->PeerName = new BYTE[namelen];memcpy(binder->PeerName, name, namelen);}binder->LeaveLook();}return error;}
那么为什么在链接时不与socks5服务器之间handshake呢?这是由于此方式 它实际上在规避异步链接处理的问题 如果需要在connect时就进行handshake那么必须要对SOCKET 进行一系列的操作 最明显的一个例子就是需要剔除与此SOCKET的所有异步事件 如AsyncEvent、EventSelect等 同时需要将其转成同步的方式进行处理  还有一点如果是ConnectEx中处理在无hook的情况下会更不可实现 因为它可能会使用“完成端口” 而如果仅仅只是“完成例程”到是无妨;对于Chrome是可以直接返回NO_ERROR 但Firebox是不吃这一套的。

因为对于Firebox而言 异步链接是不可能立即返回的 它一定在链接时返回SOCKET_ERROR、WSAGetLastError()等于“WSA_IO_PENDING” 然后它才会监视SOCKET是不是链接成功 而Chrome却不是,而它监视SOCKET是否链接成功 是需要相对应的HEvent发出FD_CONNECT信号de(WSASetEvent)通知Firebox正在被WSAEnumNetworkEvents 阻塞的工作线程 否则Firebox是不会认为SOCKET已经链接成功的。而且如果错过此次机会 那么机会将不可再来 这个SOCKET将因此而报销。

以下是WSPSend一个轻量级实现,而WSASendMsg实现与此类似

int WSPAPI WSPSend(SOCKET s,LPWSABUF lpBuffers,DWORD dwBufferCount,LPDWORD lpNumberOfBytesSent,DWORD dwFlags,LPWSAOVERLAPPED lpOverlapped,LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine,LPWSATHREADID lpThreadId,LPINT lpErrno){int error = Handshake(s, lpErrno);if (error == NO_ERROR){return LayeredServiceProvider_Current.NextProcTable.lpWSPSend(s, lpBuffers, dwBufferCount, lpNumberOfBytesSent, dwFlags,lpOverlapped, lpCompletionRoutine, lpThreadId, lpErrno);}return SOCKET_ERROR; }
从上述代码中可以得出,它从一进到WSPSend时就开始进行handshake的行为 但这个handshake函数不会每次都去进行握手 它只会握手一次;否则以后它都会返回NO_ERROR 但需要 付出一些代价即每次应用在调用WSPSend发出数据包时 都会让其检索一次std::hash_map<TKey,TValue> 从发送效率上会有一些损失 但损失不是很大。

以下是socks5协议handshake过程的一个实现 它是兼容远程域名解析的;它意味着它是可以令浏览器翻墙离开大陆局域网的;关于远程域名解析的方法在本文中不会提供 但你可以从下述代码中推断出一些实现思路

int Handshake(SOCKET s, const sockaddr * name, int namelen, SocketBinderContext* binder){TCHAR processname[MAX_PATH];GetModuleFileName(NULL, processname, MAX_PATH);if (WSPPauseEvent(s, binder) != 0){Debugger::Write(L"%s 暂停套接字事件失败 ...", processname);return ECONNRESET;}Debugger::Write(L"%s 暂停套接字事件成功 ...", processname);BOOL nonblock = binder->Nonblock;if (nonblock && !SocketExtension::SetSocketNonblock(s, FALSE))return ECONNRESET; // REAL-BLOCKING elsebinder->Nonblock = nonblock;Debugger::Write(L"%s 设置阻塞模式成功 ...", processname);char message[272];message[0] = 0x05;    // VER message[1] = 0x01;    // NMETHODSmessage[2] = 0x00;    // METHODS if (!SocketExtension::Send(s, message, 0, 3)){Debugger::Write(L"%s 发送第一次,错误 ...", processname);return ECONNREFUSED;}if (!SocketExtension::Receive(s, message, 0, 2)){Debugger::Write(L"%s 第一次收到,错误 ...", processname);return ECONNABORTED;}if (message[1] != 0x00){Debugger::Write(L"%s 被本地代理服务积极拒绝,错误 ...", processname);return ECONNABORTED;}Debugger::Write(L"%s --开始获取域名了哈?", processname);const struct sockaddr_in* sin = (struct sockaddr_in *)name;LPCSTR hostname = NamespaceMappingTable_Current.Get(sin->sin_addr.s_addr); // 逆向解析被污染以前的域名Debugger::Write(L"%s OK ----------%d 成功的获取到了域名?", processname, hostname != NULL);if (hostname != NULL && !Socks5ProxyFilter::Effective(hostname)){Debugger::Write(L"%s 这杯获取出一个域名,然而这是虚拟错误的,so ...", processname);return ECONNABORTED;}BYTE* remoteaddr = (BYTE*)&sin->sin_addr.s_addr;struct sockaddr_in proxyin4;INT err;INT proxyaddrlen = sizeof(struct sockaddr_in);LayeredServiceProvider_Current.NextProcTable.lpWSPGetPeerName(s, (struct sockaddr*)&proxyin4, &proxyaddrlen, &err);BYTE* proxyaddr = (BYTE*)&proxyin4.sin_addr.s_addr;Debugger::Write(L"%s 开始握手 ...host---- %d.%d.%d.%d:%d :: proxy--- %d.%d.%d.%d:%d", processname, remoteaddr[0], remoteaddr[1], remoteaddr[2], remoteaddr[3], ntohs(sin->sin_port),proxyaddr[0], proxyaddr[1], proxyaddr[2], proxyaddr[3], ntohs(proxyin4.sin_port));message[0] = 0x05; // VAR message[1] = 0x01; // CMD message[2] = 0x00; // RSV message[3] = 0x00; // ATYPE if (hostname == NULL){message[3] = 0x01; // IPv4memcpy(&message[4], &sin->sin_addr.s_addr, 4); // ADDRmemcpy(&message[8], &sin->sin_port, 2); // PORTif (!SocketExtension::Send(s, message, 0, 10))return ECONNREFUSED;}else{message[3] = 0x03; // hostnameint offset = (int)strlen(hostname);if (offset <= 0)return ECONNREFUSED;message[4] = (char)offset;memcpy(&message[5], hostname, offset); // ADDRoffset += 5;memcpy(&message[offset], &sin->sin_port, 2); // PORToffset += 2;if (!SocketExtension::Send(s, message, 0, offset))return ECONNREFUSED;}if (!SocketExtension::Receive(s, message, 0, 10))return ECONNREFUSED;if (message[1] != 0x00)return ECONNREFUSED;Debugger::Write(L"%s 成功鉴权 ...", processname);if (WSPResumeEvent(s, binder) != 0){Debugger::Write(L"%s 无法恢复事件 ...", processname);return ECONNRESET;}if (nonblock && !SocketExtension::SetSocketNonblock(s, TRUE)){Debugger::Write(L"%s 无法设置成异步 ...", processname);return ECONNABORTED;}Debugger::Write(L"%s 成功的完成握手 ...", processname);return 0;}int Handshake(SOCKET s, INT* lpErrno){SupersocksRConfiguration* conf = SupersocksRInteractive_Current.Configuration();SocketBinderContext* binder = NULL;int error = NO_ERROR;if (conf->EnableProxyClient && (binder = SocketMappingPool_Current.Get(s))){binder->EnterLook();if (binder->RequireHandshake){binder->RequireHandshake = FALSE;error = Handshake(s, (sockaddr*)binder->PeerName, binder->PeerNameLen, binder);if (error != NO_ERROR){*lpErrno = error;error = SOCKET_ERROR;WSPShutdown(s, SD_BOTH, lpErrno);WSPCloseSocket(s, lpErrno);}}binder->LeaveLook();}return error;}

在真正handshake过程中 它必须先暂停应用程序与SOCKET关联的全部异步通知事件 然后在将其设置成阻塞模式 才开始进行鉴权 这是没有办法的你只有一次机会没有第二次

;你必须阻塞完成鉴权才可以允许发出数据 否则可能会出现与S5代理服务器鉴权出现故障。然后在完成鉴权完成时恢复它的异步SOCKET设置包括与此相关的事件绑定。

下面给出WSPPauseEvent实现的一个代码:

int WSPPauseEvent(SOCKET s, SocketBinderContext* binder){if (binder == NULL || s == INVALID_SOCKET){return SOCKET_ERROR;}int err = 0; // THE CLEANUP BIND EVENT OBJECTif (LayeredServiceProvider_Current.NextProcTable.lpWSPEventSelect(s, 0, NULL, &err)){return ECONNRESET;}binder->EnterLook();{for each(HWND hWnd in binder->HWndList){err = 0;if (LayeredServiceProvider_Current.NextProcTable.lpWSPAsyncSelect(s, hWnd, 0, 0, &err)){binder->LeaveLook();return ECONNRESET; // WSAAsyncSelect(s, hWnd, 0, 0);}}}binder->LeaveLook();return 0;}
WSPPauseEvent与它所做行为定义是相同的,与其说它是暂停事件 倒不如说它是清楚与SOCKET相关联的异步通知事件 它可能是WSAEvent 或许也可能是AsyncEvent

但你需要从这个SOCKET上将其移除掉 否则你将无法设置SOCKET为阻塞模式(willblock mode)

下面给出WSPResumeEvent实现的一个代码:

int WSPResumeEvent(SOCKET s, SocketBinderContext* binder){if (binder == NULL || s == INVALID_SOCKET){return SOCKET_ERROR;}binder->EnterLook();{vector<SocketEventContext*>* events = &binder->Events;for (size_t i = 0, len = events->size(); i < len; i++){SocketEventContext* evt = (*events)[i];int err = 0;if (evt->hWnd == NULL){if (LayeredServiceProvider_Current.NextProcTable.lpWSPEventSelect(s, (HANDLE)evt->hEvent, evt->lNetworkEvents, &err)){return ECONNRESET;}}else{if (LayeredServiceProvider_Current.NextProcTable.lpWSPAsyncSelect(s, evt->hWnd, evt->wMsg, (long)evt->hEvent, &err)){return ECONNRESET;}}delete evt;}events->clear();}binder->LeaveLook();return 0;}

WSPResumeEvent用于恢复与SOCKET的事件绑定 这是必须的 否则会影响应用程序无法得知是否已经收取到数据包 顺带一提Chrome是不惧的 

它是通过“完成端口”的模型工作的 同时它也不停的重新binder异步通知事件到SOCKET;

上述需要恢复异步通知事件绑定的问题是针对Firebox的 如果LSP在暂停(清楚)与SOCKET的异步通知事件

不恢复它的异步事件绑定那么Firebox会提示连接已被终止的页面,但如果需要恢复异步事件绑定 那么就必须要将SOCKET从阻塞模式

重新修改成非阻塞模式、

附一个利用此方式FanWa11的效果截图:


注:本文内探讨关于Wa11的内容 请各位忽视、合法好公民 这只是技术研究T……T


原创粉丝点击