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

本文阐述针对市面上主流的浏览器 实现基于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的效果截图:

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

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


你可能感兴趣的:(C/C++,PROXY)