📜 ⬆️ ⬇️

Writing a simple SOCKS4 server in Assembler language

Some time ago, I wanted to try to implement a proxy server for my own needs, and one that could be used in the future, as well as its size was minimal. A natural option for me was an implementation using assembler. The program turned out to be small, convenient, and later I used it very often. But now, after years, I would like to show the simplest implementation of one protocol, SOCKS4. This protocol was created so that clients located on the local network behind a firewall could access the external network. At the same time, customer requests in this case have the opportunity to control :) The very first thing that is needed during implementation is to read the documentation describing this protocol, since we want our protocol to be understood by standard programs, without “undermining the file”. So, the documentation:

SOCKS protocol description
SOCKS: A protocol for TCP proxy across firewalls

Now, armed with a description, let's get started. The work of the proxy server consists in accepting a request from a client in a specific format, forming a socket and connecting it to the address requested by the client, and then providing data exchange between two sockets before they are closed by the server or the client. Let's start the implementation.
')


Macros and data structures used in the program

Form the include file, includes.inc. In this file we will place the standard macros + structures for working with SOCKS4 when writing Windows programs. Here I will not give all the macros, I will give only the description and functionality necessary to solve the main problem, everything else you will find in the attached file with the source codes.
; SOCKS4 – ,  ,    ;   (DSTIP)/(DSTPORT) CONNECT_SOCK4 Struc VN Db ? CD Db ? DSTPORT Dw ? DSTIP Dd ? NULL Db ? CONNECT_SOCK4 Ends ; SOCKS4 -  -  . RESPONSE_SOCK4 Struc VN Db ? CD Db ? DSTPORT Dw ? DSTIP Dd ? RESPONSE_SOCK4 Ends 


By and large, the CONNECT_SOCK4 and RESPONSE_SOCK4 structures are no different, since we implement the protocol without authorization. But I decided to leave them all the same so that in the future it was easy to change them, for refinement. In the structures themselves, in the variable VN, the protocol version is indicated, in our case, there should always be 4 here, in the case of SOCKS5 there is 5 in this variable (the protocol is basically similar). The variable CD is used to return the result of the proxy request to the client to the address requested by the client (90 - connection successful / 91 - connection failed).
In fact, we have three stages in the program.
* First, we initialize the socket, listen to the socket for client requests, create a processing thread.
* The second stage is the analysis of the client's request, an attempt to create and connect the socket with the server requested by the client.
* And the final, third stage is the transfer of data between the client's socket and the socket created and connected to us with the requested address.

Implementation of the first stage, initialization of the program:

 ;  ,     WinMain Proc LOCAL ThreadId, hServSock :DWORD LOCAL hostname[256] :BYTE LOCAL _wsa :WSADATA LOCAL _our :sockaddr_in ;     ,     1.1, ;     invoke WSAStartup, 0101h, ADDR _wsa .if eax == 0 ;   ,  ,     invoke gethostname, ADDR hostname, 256 invoke gethostbyname, ADDR hostname .if eax == 0 invoke inet_addr, ADDR hostname .else mov eax, [eax + 12] mov eax, [eax] mov eax, [eax] .endif mov _our.sin_addr, eax invoke inet_ntoa, eax mov _our.sin_family, AF_INET mov _our.sin_addr.S_un.S_addr, INADDR_ANY xor eax, eax ;  ,       mov ax, SOCKS_PORT invoke htons, eax mov _our.sin_port, ax invoke socket, AF_INET, SOCK_STREAM, 0 .if eax != INVALID_SOCKET ;     mov hServSock, eax ;          invoke bind, hServSock, ADDR _our, SIZEOF sockaddr_in .if eax != SOCKET_ERROR @@: ;     invoke listen, hServSock, SOMAXCONN .repeat ;  ,      invoke accept, hServSock, NULL, NULL .until eax != INVALID_SOCKET ;  ,       xchg eax, ebx invoke CreateThread, NULL, NULL, ADDR socketThread, ebx, NULL, ADDR ThreadId ;     jmp @B .endif .endif invoke closesocket, hServSock .endif invoke ExitProcess, 0 WinMain Endp 

This is our first procedure, I tried to comment on the code as much as possible so that you could figure it out, but if something is not clear, please contact me or MSDN. In principle, all code is written using MASM and WinAPI syntax. The result of the work of the above function should be a working socket on one of the network addresses of your machine (local address, or external address if you have a real IP) + the function creates a separate stream used by the client to work with the incoming client. Now let's go further ...

The second stage, the analysis of the client's request

At the second stage, all that needs to be done is to accept the CONNECT_SOCK4 structure, create a socket, try to connect it and send a response to the client. Implementation:

 socketThread Proc sock:DWORD LOCAL lpMem, _csock, ThreadId, dAmount :DWORD LOCAL Remote :sockaddr_in LOCAL wrFds, rdFds :fd_set LOCAL hResp :RESPONSE_SOCK4 ;       invoke FdZero, ADDR rdFds invoke FdSet, sock, ADDR rdFds invoke select, NULL, ADDR rdFds, NULL, NULL, NULL ;      invoke ioctlsocket, sock, FIONREAD, ADDR dAmount ;     mov lpMem, @Result(LocalAlloc, LMEM_FIXED or LMEM_ZEROINIT, dAmount) ;      invoke recv, sock, lpMem, dAmount, 0 ;   lea edi, hResp mov esi, lpMem ;  Esi   .   ()   SOCKS4, ; SOCKS5      ,    ... Assume Esi : Ptr CONNECT_SOCK4 Assume Edi : Ptr RESPONSE_SOCK4 .if [esi].VN == 4 ;    4 .if [esi].CD == 1 invoke socket, AF_INET, SOCK_STREAM, 0 .if eax != INVALID_SOCKET mov _csock, eax ;    ,      mov Remote.sin_family, AF_INET mov ax, [esi].DSTPORT mov Remote.sin_port, ax mov eax, [esi].DSTIP mov Remote.sin_addr, eax mov cx, [esi].DSTPORT mov edx, [esi].DSTIP ;  Edi    mov [edi].VN, 0 mov [edi].DSTPORT, cx mov [edi].DSTIP, edx ;      invoke connect, _csock, ADDR Remote, SIZEOF Remote .if !eax ;  ,    mov [edi].CD, 90 ;   ,     invoke send, sock, ADDR hResp, SIZEOF RESPONSE_SOCK4, 0 ;        ;    ; -        , ;   ; -       , ;     mov ebx, @Result(LocalAlloc, LMEM_FIXED or LMEM_ZEROINIT, SIZEOF THREAD_DATA) Assume Ebx : Ptr THREAD_DATA mov eax, _csock mov [ebx].Server, eax mov eax, sock mov [ebx].Client, eax Assume Ebx : Nothing ;     (    ;    ) invoke CreateThread, NULL, NULL, ADDR ClientSock, ebx, NULL, ADDR ThreadId .else ;     -    invoke closesocket, _csock ; ,     mov [edi][RESPONSE_SOCK4.CD], 91 ;   ,     invoke send, sock, ADDR hResp, SIZEOF RESPONSE_SOCK4, 0 .endif .endif .endif .endif Assume Edi: Nothing Assume Esi: Nothing ;  ,    invoke LocalFree, lpMem ret socketThread Endp 

The result of this procedure is a connected socket, as well as a stream created that implements the exchange of data between two sockets. It's simple. It is only necessary to clarify, here are used a few points on addressing within the structures that are entered into MASM to facilitate the life of the programmer. The first moment, the macro “Assume”.
The Assume Esi: Ptr CONNECT_SOCK4 line tells the compiler that this register (Esi) contains the address of the CONNECT_SOCK4 structure, which further simplifies the access to variables inside this structure. Assume Esi: Nothing unbinds. To better understand, it will probably be easier if I specify several addressing options:
 Assume Esi:Ptr CONNECT_SOCK4 mov al, [esi].VN ;   AL     VN  mov al, [esi].CD ;   AL  CD mov ax. [esi].DSTPORT ;   AX  DSTPORT Assume Esi:Nothing 

or
 mov al, [esi][CONNET_SOCK4.VN] ;   AL     VN mov al, [esi][CONNET_SOCK4.CD] ;   AL  CD mov ax, [esi][CONNET_SOCK4.DSTPORT] ;   AX  DSTPORT 

or
 mov al, byte ptr [esi] ;   AL  VN mov al, byte ptr [esi+1] ;   AL  CD mov ax, word ptr [esi+2] ;   AX  DSTPORT 


I think you obviously, just like me, that it is faster, more convenient and clearer to use the first option. Although if you need to refer to one variable structure - has the right to exist and the second option. The third option I think is better to use in cases where the data at the address are not structured. But, as you know, the taste and color, each a Tambov wolf to himself. Use the method you prefer.
Another point worth clarifying. Macro Result . This macro is written so that you can call the WinAPI function in one line and put the result of the execution in a register or memory. So the string:
 mov lpMem, @Result(LocalAlloc, LMEM_FIXED or LMEM_ZEROINIT, dAmount) 

It first performs the following call:
 invoke LocalAlloc, LMEM_FIXED or LMEM_ZEROINIT, dAmount 

and after the execution of this call, the result of execution (Eax) is entered into the lpMem variable. In this particular case, the memory will be allocated, and the address will be written into the variable at which the area allocated for us is located.

Stage Three, Data Transfer

So, the two most difficult stages are completed. The client came, we connected it to the remote server and the turn of the simplest “monkey” work came. Data transfer between two sockets. Let's make it simple, in a quick way:
 ;         .... ClientSock Proc Param:DWORD LOCAL sserver, sclient:DWORD LOCAL rdFds :fd_set LOCAL dAmount, lpBuf: DWORD ;  Param         , ;     mov ebx, Param Assume Ebx: Ptr THREAD_DATA mov eax, [ebx].Server mov sserver, eax mov eax, [ebx].Client mov sclient, eax Assume Ebx : Nothing ;     invoke LocalFree, Param @@: invoke FdZero, ADDR rdFds invoke FdSet, sserver, ADDR rdFds invoke FdSet, sclient, ADDR rdFds invoke select, NULL, ADDR rdFds, NULL, NULL, NULL ; ,      .if eax == SOCKET_ERROR || eax == 0 ;   -  jmp @F .endif ;     ,    ? invoke FdIsSet, sserver, ADDR rdFds .if eax ;      invoke ioctlsocket, sserver, FIONREAD, ADDR dAmount ;     mov lpBuf, @Result(LocalAlloc, LMEM_FIXED or LMEM_ZEROINIT, dAmount) invoke recv, sserver, lpBuf, dAmount, 0 .if eax == SOCKET_ERROR || eax == 0 jmp @F .endif invoke send, sclient, lpBuf, eax, 0 invoke LocalFree, lpBuf .endif ;         ? invoke FdIsSet, sclient, ADDR rdFds .if eax ;      invoke ioctlsocket, sclient, FIONREAD, ADDR dAmount ;     mov lpBuf, @Result(LocalAlloc, LMEM_FIXED or LMEM_ZEROINIT, dAmount) invoke recv, sclient, lpBuf, dAmount, 0 .if eax == SOCKET_ERROR || eax == 0 jmp @F .endif invoke send, sserver, lpBuf, eax, 0 invoke LocalFree, lpBuf .endif ;     jmp @B @@: ;   invoke closesocket, sserver invoke closesocket, sclient ;    invoke ExitThread, 0 ClientSock Endp 

Initially, in this procedure, internal variables are initialized from the structure passed to the stream so that they are more convenient to use. Then, the loop checks whether there is data to read from the sockets, then two pieces of code (in fact, copy-paste, I didn’t bother taking out the function and optimizing because it’s so clearer) reading from one socket and sending it to the second.
Everything, hooray! Compile and try. In principle, the best option is FireFox. In the connection settings, we indicate that you need to use a SOCKS4 proxy server. We indicate its address and the port on which it is located with us. After that, we save the settings and enjoy the Internet, passed through our proxy, 3.5 KB in size))) Yes, I will clarify. To compile, it is advisable to have the MASM32 package installed; compilation is done using the bldall.bat bldallc.bat batch files (the console version of the application).

The source code of the application, as well as several old projects in assembly language, can be found here:

Socks4

Source: https://habr.com/ru/post/80409/


All Articles