本文阐述针对市面上主流的浏览器 实现基于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); // PORT server.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 else binder->Nonblock = nonblock; Debugger::Write(L"%s 设置阻塞模式成功 ...", processname); char message[272]; message[0] = 0x05; // VER message[1] = 0x01; // NMETHODS message[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; // IPv4 memcpy(&message[4], &sin->sin_addr.s_addr, 4); // ADDR memcpy(&message[8], &sin->sin_port, 2); // PORT if (!SocketExtension::Send(s, message, 0, 10)) return ECONNREFUSED; } else { message[3] = 0x03; // hostname int offset = (int)strlen(hostname); if (offset <= 0) return ECONNREFUSED; message[4] = (char)offset; memcpy(&message[5], hostname, offset); // ADDR offset += 5; memcpy(&message[offset], &sin->sin_port, 2); // PORT offset += 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 OBJECT if (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