本文阐述针对市面上主流的浏览器 实现基于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以下是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* 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