Pull to refresh

Написание простейшего SOCKS4 сервера на языке Assembler

Reading time8 min
Views10K
  Какое то время тому назад захотелось мне попробовать реализовать прокси сервер для собственных нужд, да такой, который можно было бы в дальнейшем использовать, а также, чтобы размер его был минимален. Естественным вариантом для меня стала реализация с использованием ассемблера. Программка получилась небольшая, удобная и в дальнейшем я очень часто ей пользовался. А вот теперь, по прошествии лет, хотелось бы показать простейшую реализацию одного протокола, SOCKS4. Данный протокол был создан для того, чтобы клиенты, находящиеся в локальной сети за межсетевым экраном могли обращаться во внешнюю сеть. В то же время запросы клиентов в таком случае есть возможность контролировать :) Самым первым, что нужно, при реализации – прочитать документацию с описанием данного протокола, так как мы хотим, чтобы наш протокол понимался стандартными программами, без “подтачивания напильником”. Итак, документация :

Описание протокола SOCKS
SOCKS: A protocol for TCP proxy across firewalls

  Теперь, вооружившись описанием, приступим. Работа прокси сервера состоит в том, чтобы принять от клиента запрос в определенном формате, сформировать сокет и подключить его по адресу запрошенному клиентом, после чего обеспечить обмен данными между двумя сокетами, до их закрытия со стороны сервера, либо клиента. Приступим к реализации.



Макросы и структуры данных, используемые в программе

  Сформируем включаемый файл, includes.inc. В данный файл поместим стандартные, при написании Windows программ макросы + структуры для работы с SOCKS4. Здесь я не буду приводить все макросы, я приведу лишь описание и функционал, необходимый для решения основной задачи, все остальное вы найдете в приложенном файле с исходными кодами.
; 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


  По большому счету, структуры CONNECT_SOCK4 и RESPONSE_SOCK4 ничем не отличаются, так как мы реализуем протокол без авторизации. Но я решил все таки оставить их отдельно, чтобы в дальнейшем можно было легко изменить их, для доработки. В самих структурах, в переменной VN – указывается версия протокола, в нашем случае, здесь всегда должно быть 4, в случае с SOCKS5 в данной переменной находится 5 (протокол в принципе похож). Переменная CD используется для возврата клиенту результата запроса прокси сервера к запрошенному клиентом адресу (90 – соединение успешно / 91 – соединение не удалось).
У нас в программе по факту три этапа.
 * Первый, инициализируем сокет, слушаем сокет на предмет наличия клиентских запросов, создаем поток обработки.
 * Второй этап – анализ запроса клиента, попытка создания и соединения сокета с запрошенным клиентом сервером.
 * И окончательный, третий этап – пересылка данных между сокетом клиента и сокетом созданным и соединенным нами с запрошенным адресом.

Реализация первого этапа, инициализация программы :

; Основная процедура, является стартовой для программы
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

  Это первая наша процедура, я постарался максимально прокомментировать код, чтобы вы могли разобраться, но если все же что-то не понятно – обращайтесь либо ко мне, либо к MSDN. В принципе весь код написан с использованием синтаксиса MASM и WinAPI. Итогом работы приведенной функции должен быть работающий сокет на одном из сетевых адресов вашей машины (локальный адрес, либо внешний адрес если у вас реальный IP) + по соединению клиента функция создает отдельный поток, используемый для работы с пришедшим клиентом. Теперь пойдем дальше ...

Второй этап, анализ запроса клиента

  На втором этапе все, что необходимо сделать, это принять структуру CONNECT_SOCK4, создать сокет, попытаться соединить его и отправить клиенту ответ. Реализация :

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

  Результат выполнения данной процедуры – соединенный сокет, а также созданный поток, реализующий обмен данными между двумя сокетами. Все просто. Стоит только уточнить, здесь используется несколько моментов по адресации внутри структур, которые введены в MASM для облегчения жизни программиста. Первый момент, макрос “Assume”.
  Строчка Assume Esi: Ptr CONNECT_SOCK4 говорит компилятору о том, что в данном регистре(Esi) находится адрес структуры CONNECT_SOCK4, что в дальнейшем упрощает обращение к переменным внутри данной структуры. Assume Esi:Nothing отменяет привязку. Чтобы лучше понять, возможно будет проще, если я укажу несколько вариантов адресации :
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

либо
mov   al, [esi][CONNEСT_SOCK4.VN]      ; Помещаем в AL значение байта из переменной VN
mov   al, [esi][CONNEСT_SOCK4.CD]      ; Помещение в AL переменной CD
mov   ax, [esi][CONNEСT_SOCK4.DSTPORT] ; Помещение в AX переменной DSTPORT

либо
mov  al, byte ptr [esi]   ; Помещение в AL переменной VN
mov  al, byte ptr [esi+1] ; Помещение в AL переменной CD
mov  ax, word ptr [esi+2] ; Помещение в AX переменной DSTPORT


  Я думаю, вам очевидно так же как и мне, что быстрее, удобней и наглядней использовать первый вариант. Хотя если необходимо обратиться к одной переменной структуры – имеет право на существование и второй вариант. Третий же вариант думаю лучше использовать в случаях, когда данные по адресу не структурированы. Но, как известно, на вкус и цвет, каждый сам себе тамбовский волк. Пользуйтесь тем методом, который вам удобней.
  Еще один момент, который стоит уточнить. Макрос Result. Данный макрос написан, чтобы можно было одной строкой вызвать функцию WinAPI и занести результат исполнения в регистр или память. Так строка :
mov lpMem, @Result(LocalAlloc, LMEM_FIXED or LMEM_ZEROINIT, dAmount)

Выполняет сначала вызов такого вида :
invoke LocalAlloc, LMEM_FIXED or LMEM_ZEROINIT, dAmount

  а уже после выполнения данного вызова результат исполнения (Eax) заносит в переменную lpMem. В данном конкретном случае, будет выделена память, а в переменную будет записан адрес, по которому находится выделенный для нас участок.

Этап третий, передача данных

  Итак, выполнены два самых сложных этапа. Клиент пришел, мы соединили его с удаленным сервером и настал черед простейшей “обезьяньей” работы. Передачи данных между двумя сокетами. Сделаем это просто, по-быстрому :
; Поток читающий из клиентского и передающий в серверный сокет....
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

  Первоначально в данной процедуре производится инициализация внутренних переменных из переданной в поток структуры, чтобы их было удобнее использовать. Затем, в цикле производится проверка, есть ли данные на чтение из сокетов, затем двумя кусками кода (фактически копипаст, здесь я не заморачивался выносом функции и оптимизацией ибо так наглядней) делается чтение из одного сокета и отправка во второй.
  Все, ура! Компилируем и пробуем. В принципе самый хороший вариант – FireFox. В настройках подключения указываем, что нужно использовать прокси сервер SOCKS4. Указываем его адрес и порт, на котором он у нас находится. После этого, сохраняем настройки и наслаждаемся интернетом, прошедшим, через наш прокси, размером 3,5 кбайт ))) Да, уточню. Для компиляции жлательно наличие установленного пакета MASM32, компиляция производится пакетными файлами bldall.bat либо bldallc.bat(консольный вариант приложения).

Исходные тексты приложения, как и несколько старых проектов на ассемблере можно найти тут:

Socks4
Tags:
Hubs:
Total votes 28: ↑26 and ↓2+24
Comments11

Articles