1. 程式人生 > >PRX 通過LSP實現瀏覽器Socks5/Tcp代理(從傳送資料上著手)

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<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