Персональный блог Игоря Антонова aka "spider_net"

Proxy сервер на Delphi и Winsock API


Рубрика: Delphi -> Программирование -> Журнал Хакер -> Статьи
Метки: | |
Просмотров: 93578
Proxy сервер на Delphi и Winsock API

Мы много раз рассказывали тебе про прокси-серверы: для чего они нужны, как они работают и чем полезны хакеру. Тем не менее знать и использовать – это одно, а вот создавать самому – совсем другое дело. Этот творческий труд полезен для души, тела и, конечно же, твоего WM-кошелька.

Немного теории

Итак, прокси-сервер – это прежде всего программа, выступающая посредником между клиентом и сервером. Все привыкли связывать понятие прокси только с протоколом HTTP. На самом деле существуют проксики и для других протоколов, о которых я расскажу чуть позже. Самый распространенный вид проксиков – HTTP. При работе через HTTP-прокси твой браузер не будет соединяться с сервером, на котором расположен запрашиваемый сайт, он соединится с прокси и передаст ему запрос. Получив от тебя все необходимые данные, проксик сам сконнектится с удаленным web-сервером и отправит твой запрос. После его обработки web-сервер вернет документ проксику, который затем отправит его тебе. Такие проксики полезны, когда нужна анонимность (поскольку они бывают прозрачными) или если твой провайдер ограничивает тебя и не разрешает посещать сайты, расположенные на забугорных серверах.

Еще одно место, где постоянно используются прокси-серверы, – корпоративные (домашние) локальные сети. Для предоставления сотрудникам компании доступа в инет админы устанавливают на шлюзе проксик, и вся контора ходит через него в Сеть. Плюсы такого способа очевидны: можно легко отслеживать маршруты пользователей, считать количество израсходованного трафика и быть уверенным в том, что юзеры не будут пользоваться лишним софтом, так как не каждая программка способна работать через HTTP-прокси.

Proxy сервер средствами Delphi и Winsock API

Я уже говорил, что HTTP-прокси не является единственным типом прокси-серверов. В природе также встречаются:

  • HTTPS proxy – один из самых универсальных типов прокси-серверов. В нем реализована поддержка спецификации протокола HTTP 1.1. Особенность этой версии протокола - поддержка метода CONNECT, благодаря которому становится возможным работать с HTTPS (безопасным HTTP), а также заставить работать через прокси-сервер программы вроде ICQ, функционирование которых через HTTP-прокси невозможно.
  • FTP proxy – довольно редкий вид, занесенный в Красную книгу. Как и следует из названия, эти прокси предназначены для работы с FTP-протоколом. Главная их особенность – возможность работы как в пассивном, так и в активном режимах.
  • Socks proxy – самый продвинутый тип проксиков. Такие прокси-серверы работают с любым TCP/IP-протоколом (ftp, pop3, smtp, nntp и т.д.).
  • Зачем писать свой прокси-сервер

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

    Другим интересным способом применения твоего шедевра может быть снифание хакером паролей, которые сосед вводит в своем браузере. В этом случае хакеру также нужно будет подкинуть несчастному соседу твою тулзу и убедить его запустить ее. После запуска ][-проксик автоматически сконфигурирует бродилку соседа на работу через самого себя. Тем самым чел будет спокойно бороздить инет, а все его запросы (отправка паролей и т.д.) будут записываться лог. Круто? Несомненно! Но мы-то с тобой знаем, что все эти бредовые идеи носят противозаконный характер, поэтому мы будем писать прокси-сервер лишь в образовательных целях, даже и не думая о получении выгоды.

    Используемые технологии

    При написании серверных сетевых приложений не рекомендуется использовать компонентную модель Delphi. Компоненты не обладают той гибкостью, которую можно получить, применяя API. Поэтому сегодня нам опять предстоит столкнуться со страшным WinSock API.

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

    После установки соединения с клиентом – браузером пользователя - первое, что нам необходимо будет сделать, – это получить запрос клиента. Получив запрос и вытащив из него адрес удаленного сервера, надо сразу попытаться соединиться с ним, передать полученный ранее запрос, дождаться ответа и переслать полученный ответ обратно клиенту. Если ты внимательно слушал теорию, то мог заметить, что в качестве удаленного сервера не обязательно должен выступать web-сервер. На его месте вполне может быть и другой проксик. Таким образом, можно создать настоящую цепочку проксиков, что, несомненно, повысит анонимность.

    Обсудим получение запроса от клиента. В запросе, который формирует браузер, содержится информация, на основании которой web-сервер может определить, какой именно web-документ мы от него хотим. Все нюансы запросов ты можешь узнать из RFC 2068. Рассмотрим пример. Когда ты набираешь в браузере www.xakep.ru, запрос имеет следующий вид (может отличаться, зависит от браузера):

    GET http://xakep.ru/ HTTP/1.0
    User-Agent: Opera/9.21 (Windows NT 5.1; U; ru)
    Host: xakep.ru
    Accept: text/html, application/xml;q=0.9, application/xhtml+xml,
    image/png, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1
    Accept-Language: ru-RU,ru;q=0.9,en;q=0.8
    Accept-Charset: iso-8859-1, utf-8, utf-16

    Как видишь, в запросе содержится много полезной и бесполезной информации, но самое главное - это первая и третья строчка. В первой определен адрес, который запросил пользователь, а во второй - хост. Получив этот запрос, наш проксик должен извлечь адрес хоста, определить его IP, соединиться и послать ему весь запрос. Наверняка у тебя возник вопрос: можно ли изменить этот запрос? Отвечаю. Конечно можно! Мы легко сможем поиздеваться над пользователем, и вместо www.yandex.ru он будет попадать на www.xxx-porno.com.

    Winsock API для Proxy

    Как и подобает в программировании, после обсуждения алгоритма решения поставленной задачи, нужно определиться с инструментами, которые будут необходимы для этого. В нашем случае главным молотком будет Delphi, а гвоздями с шурупами – WinSock API и классы TThread. Рассмотрим необходимые WinSock API функции.

    function WSAStartup (wVersionRequested:word; var WSAData:TWSAData):integer; stdcall;

    Эта функция, с вызова которой нужно начинать программирование любого сетевого приложения. Она предназначена для инициализации сетевой библиотеки Windows. Функции необходимо заслать два параметра:

  • wVersionRequested – версия инициализируемой библиотеки. Их всего две: 1.1 и 2.0. Например, для первой версии пишем: makeword(1,1).
  • Указатель на структуру WSAData. После выполнения функции в эту структуру запишется информация о сетевой библиотеке.
  • При успешном выполнении функция вернет 0. Для получения кодов ошибок в WinSock API служит функция WSAGetLastError(). Ей не нужно передавать какие-либо параметры, после вызова она возвращает код последней возникшей при работе с сетевыми функциями ошибки.

    function socket (af:integer; type:integer; protocol:integer):TSocket, stdcall;

    Перед тем как соединиться с удаленным узлом, нужно создать «розетку» - socket. Как раз за его создание и отвечает одноименная функция socket. Входных параметров три:

  • af – семейство протоколов. Нам потребуется лишь TCP, поэтому будем указывать AF_INET.
  • type – тип создаваемого сокета. Может быть sock_stream (для протокола TCP/IP) и sock_dgram (udp).
  • protocol – протокол. Для TCP нужно указать IPPROTO_TCP.
  • Результатом выполнения будет новый сокет. Создав сокет, можно пробовать подключаться. Для этого в библиотеке реализована функция Connect.

    function Connect (S:TSocket; var name:TSockAddr; namelen:integer):Integer:stdcall;

    Параметрами для функции служат:

  • s – socket, созданный функцией socket.
  • name – структура SockAddr, содержащая данные, необходимые для подключения (протокол, адрес удаленного компьютера, порт).
  • namelen – размер структуры типа TSockAddr.
  • Успешно выполнившись, а значит, и установив соединение, функция вернет 0, в противном случае - ошибку, которую можно получить с помощью WSAGetLastError().

    Структура TSockAddr выглядит так:

    TSockAddrIN = sockaddr_in;
     SockAddr_in = record
     sin_family: u_short; //семейство протоколов
     sin_port: u_short; //порт, с которым нужно будет установить соединение
     sin_addr: TInAddr; //структура, в которой записана информация об адресе удаленного компьютера
     sin_zero: array[0..7] of Char; //совмещение по длине структуры sockaddr_in с sockaddr и наоборот.
    end;

    Чтение и отправка данных удаленной стороне осуществляется с помощью функций send и recv. Они описаны следующим образом:

    function send (s:TSocket, var Buf; len:integer; flags:integer):Integer;stdcall;
    function recv (s:TSocket, var Buf; len:integer; flags:integer):Integer;stdcall;

    Параметры для обеих функций одинаковые:

  • s – сокет, на который нужно отправить (принять) данные.
  • buf – буфер с данными для отправки (приема).
  • len – размер передаваемых (принимаемых) данных.
  • flags – флаги, отвечающие за метод отправки (приема).
  • Выполнившись, функция вернет фактическое количество отправленных/принятых байт.

    function bind (S:TSocket; var addr:TSockAddr; namelen:Integer):integer; stdcall;

    Назначение функции – связывание структуры TSockAddr с созданным сокетом. Параметров три: сокет, структура, размер структуры.

    function listen (s:TSocket; backlog:Integer):Integer; stdcall;

    Фактическое прослушивание порта начинается после вызова этой функции. Для работы функции требуется всего два параметра: сокет и максимальное количество запросов на ожидания подключения.

    function CloseSocket(s:TSocket):integer;stdcall;

    Эта функция закрывает сокет. Параметр всего один – сокет, который нужно закрыть.

    function Select (nfds:Integer, readfds, writefds, exceptfds: PFDSet, timeout: PTimeVal):LingInt; stdcall;

    Цель функции – проверка готовности сокета (чтение, запись срочных данных). Select очень пригождается, когда требуется разрабатывать многопользовательские сетевые приложения подобно нашему, где использование событийной модели Windows не оправдывает себя. В качестве параметров функция принимает:

  • nfds – параметр игнорируется и присутствует лишь для совместимости с моделью сокетов Беркли.
  • readfds, writefds, exceptfds – они определяют возможность чтения, записи и факт прибытия срочных данных. Эти три параметра являются указателями на структуру FD_SET, которая представляет собой набор сокетов.
  • TimeOut – указатель на структуру timeval. В структуре определено максимальное время ожидания. Для установки бесконечного ожидания следует передать в этот параметр nil.
  • procedure FD_ZERO (var FDSet: TFDSet);

    Очистка и инициализация набора сокетов. Перед добавлением сокетов в набор необходимо его проинициализировать с помощью этой функции.

    procedure FD_SET(Socket: TSocket; var FDSet: TFDSet);

    Процедура предназначена для добавления сокета, переданного в первом параметре в набор, указанный во втором.

    function FD_ISSET(Socket: TSocket; var FDSet: TFDSet): Boolean;

    Функция позволяет проверить вхождение сокета (первый параметр) в набор (второй параметр).

    Кодим

    Вот и настала та заветная минута, когда мы заканчиваем разбираться с теорией и приступаем к реальному кодингу. Запускай Delphi, создавай новый проект и придавай форме вид, похожий на вид моей формы. Мы не будем ничего скрывать от пользователя, поскольку, если ты помнишь, мы пишем программу в образовательных целях. Ты там дальше сам разберешься. На форме у меня три кнопки:

  • «Запустить» – запускает проксик на порту 8080.
  • Configure IE – для автоматического конфигурирование браузера IE.
  • Configure Opera – то же самое конфигурирование, только для Opera.
  • Proxy сервер средствами Delphi и Winsock API

    В остальной части формы у меня располагается ListView с тремя колонками. В них мы будем отображать IP клиентов и адреса хостов, к которым они обратились. По событию OnClick для кнопки «Запустить» напиши следующий код:

    _listenThread := TListenThread.Create (false);

    Этой одной-единственной строчкой кода мы создаем новый поток типа TListenThread. Потоки можно создавать приостановленными. Именно поэтому в качестве параметра метода Create я передаю значение false, требующее немедленного запуска.

    Proxy сервер средствами Delphi и Winsock API

    Поток TListenThread подготовит сокет для прослушивания и будет ожидать подключений на порт 8080. Код создания приведен во врезке «Поток TListenThread».

    var
    _listenSocket, _clientSocket:TSocket;
    _listenAddr, _clientAddr: sockaddr_in;
    _clientThread:TClientThread;
    _size:integer;
    begin
     _listenSocket := socket (AF_INET, SOCK_STREAM, 0);
     
     if (_listenSocket = INVALID_SOCKET) then
     begin
      ShowMessage('Ошибка создания сокета!');
      Exit;
     end;
     _listenAddr.sin_family := AF_INET;
     _listenAddr.sin_port := htons(8080);
    _listenAddr.sin_addr.S_addr := htonl(INADDR_ANY);
     if (Bind(_listenSocket, _listenAddr, sizeof(_listenAddr)))=SOCKET_ERROR then
     begin
      ShowMessage('Ошибка связывания сокета с адресом!');
      Exit;
     end;
     if (Listen(_listenSocket, 4)) = SOCKET_ERROR then
     begin
      ShowMessage('Не могу начать прослушивание!');
      Exit;
     end;
     while true do
     begin
      _size := sizeof(_clientAddr);
      _clientSocket := accept(_listenSocket, @_clientAddr, @_size);
     
      if (_clientSocket = INVALID_SOCKET) then
       Continue;
     _clientThread := TClientThread.Create(true);
     _clientThread._Client := _ClientSocket;
     _clientThread._ip := inet_ntoa(_clientAddr.sin_addr);
     _clientThread.Resume;
    end;

    Давай подробнее рассмотрим содержимое приведенной выше врезки. Процедура Execute(), определенная у объекта TlistenThread, является основной для потоков. После запуска потока она выполняется самой первой, а раз так, то именно в ней нужно расположить код, отвечающий за начало прослушивания определенного порта.

    Чтобы начать слушать порт, нужно создать сокет с помощью одноименной функции socket(). Параметры, необходимые для работы функции, определяются исходя из того, какой протокол мы будем использовать. HTTP-проксик должен задействовать TCP/IP-протокол, обеспечивающий надежную передачу данных. Поэтому во втором параметре я указываю SOCK_STREAM.

    Proxy сервер средствами Delphi и Winsock API

    Создав сокет, нужно убедиться, что после выполнения функции Socket не произошла ошибка. Для проверки достаточно сравнить переменную сокета со значением константы INVALID_SOCKET. Если они окажутся равными, то произошла ошибка и дальнейшее выполнение программы бессмысленно. Предположим, что сокет успешно создался, а значит, следующим шагом будет заполнение структуры sockaddr_in, содержащей необходимые данные для начала прослушивания.

    Подробное описание всех свойств структуры я уже приводил, поэтому сейчас не буду заострять на этом внимание. Заполнив все свойства структуры, ее нужно связать с нашим сокетом с помощью функции BIND. Если функция BIND выполнилась без ошибок, то надо вызвать функцию для начала прослушивания - Listen. После ее выполнения запускается бесконечный цикл, в котором вызывается функция accept(). Успешное ее выполнения будет означать, что к нам подсоединился клиент, и для работы с ним необходимо создать новый поток. В потоке TClientThread будет происходить обмен данными между клиентом и нашим проксиком и, соответственно, между проксиком и удаленным сервером. Основной код потока TClientThread приведен во врезке, а полную версию ты всегда можешь посмотреть на нашем диске.

    Код потока TClientThread

    var
    _buff: array [0..1024] of char;
    _port: integer;
    _request:string;
    _srvAddr : sockaddr_in;
    _srvSocket : TSocket;
    _mode, _size : Integer;
    _fdset : TFDSET;
    begin
     Recv(_client, _buff, 1024, 0);
     _request:=string(_buff);
     
     if _request='' then
     begin
      CloseSocket(_client);
      exit;
     end;
    _host:=Copy(_request, Pos('Host: ', _request), 255);
     Delete(_host, Pos(#13, _host), 255);
     Delete(_host, 1, 6);
     _port:=StrToIntDef(Copy(_host, Pos(':', _host)+1, 255), 80);
     Delete(_host, Pos(':', _host), 255);
     if (_host='') then
     begin
      SendStr(_client, '<h1>Error 400: Invalid header</h2>');
      CloseSocket(_client);
      exit;
     end;
     Synchronize(addToLog);
     _srvSocket := socket(AF_INET, SOCK_STREAM, 0);
     _srvAddr.sin_addr.s_addr := htonl(INADDR_ANY);
     _srvAddr.sin_family := AF_INET;
     _srvAddr.sin_port := htons(_port);
     _srvAddr.sin_addr := LookupName(_host);
     if connect(_srvSocket, _srvAddr, sizeof(_srvAddr))=SOCKET_ERROR then
     begin
      SendStr(_Client, '<h1>Error 404: NOT FOUND</h1>');
      exit;
     end;
     _mode:=1;
     setsockopt(_srvSocket, IPPROTO_TCP, TCP_NODELAY, @_mode, sizeof(integer));
     send(_srvSocket, _buff, strlen(_buff), 0);
     while true do
     begin
      FD_ZERO(_fdset);
      FD_SET(_client, _fdset);
      FD_SET(_srvSocket, _fdset);
     
      if (select(0, @_fdset, nil, nil, nil) < 0) then
       exit;
      if (FD_ISSET(_client, _fdset)) then
      begin
       _size := recv(_Client, _buff, sizeof(_buff), 0);
       
     if _size=-1 then break;
     
     send(_srvSocket, _buff, _size, 0);
     continue;
    end;
    if(FD_ISSET(_srvSocket, _fdset)) then
    begin
     _size := recv(_srvSocket, _buff, sizeof(_buff), 0);
     
     if _size=0 then
      exit;
     Send(_client, _buff, _size, 0);
     continue;
    end;
    end;
    CloseSocket(_client);
    CloseSocket(_srvSocket);

    Этот код получился чуть больше по размеру, но сложного в нем ничего нет, в чем ты сейчас убедишься. Для того чтобы разобраться в этом листинге, придется вспомнить алгоритм работы нашей программы, который мы обсуждали в самом начале статьи. Установив соединение с клиентом, нужно сразу получить от него текст запроса, в котором определен адрес запрашиваемого документа. Чтение данных из сокета происходит функцией Recv(), описание которой я приводил.

    Получив текст запроса, нужно выдернуть из него значение атрибута «хост». По этому значению мы сможем получить адрес удаленного сервера, которому и будем отправлять запрос пользователя. Если из запроса удалось выделить адрес хоста, нужно начинать подготавливать сокет для установки соединения с web-сервером, в противном случае отправить пользователю сообщение типа «Ошибка в запросе». Для установки соединения нужно заполнить уже знакомую нам структуру типа sockaddr_in и выполнить функцию Connect().

    Proxy сервер средствами Delphi и Winsock API

    Как только соединение будет установлено, нужно перевести сокет в асинхронный режим. Смена режима происходит с помощью функции setsockopt(). Перевод в асинхронный режим необходим, поскольку в таком случае нехило повысится производительность нашего приложения. Это станет возможным из-за минимизации задержек перед пересылкой данных между нами, web-сервером и клиентом. Получив от сервера порцию данных, мы не будем ждать остальных, а будем сразу отправлять ее клиенту.

    Итак, переведя сокет в асинхронный режим, можно смело отправлять серверу запрос, ранее полученный от клиента и запускать бесконечный цикл, в котором будет реализован обмен данным. Перед тем как читать данные, нужно добавить сокеты в набор для ожидания. Как ты помнишь, после мы сможем проверять готовность сокета с помощью функции Select().

    Ну а дальше все просто. Остается только сделать проверки сокетов. Если запрос пришел от клиента, то перенаправляем его web-серверу; если от сервера, то, наоборот, отправляем его клиенту. Для проверки я запустил созданный проксик у себя на компе, сконфигурировал Opera и попробовал зайти на один из сайтов локальной сети, пользователем которой я явлюсь. После отправки запроса моя опера шустренько начала принимать данные от прокси-сервера. Тем временем ListView стал заполняться моим IP и адресом хоста, к которому я посылал запрос.

    Слуховое окно прорублено

    Надеюсь, сегодняшний пример получился достаточно полезным как для программиста, так и для хакера. В очередной раз ты убедился, что Delphi – это не только базы данных и отчеты, но и язык, с помощью которого можно решать как прикладные, так и хакерские задачи. Мне остается только пожелать тебе успешного применения полученных знаний в своих будущих проектах.

    Исходник примера Proxy сервер на Delphi

    Статья опубликована журнале "Хакер" (http://xakep.ru). Январь 2008 г.

    Ссылка на опубликованную статью сайта издания: http://goo.gl/juyZFN

    Ссылка на журнал: http://goo.gl/q7uKzg

    Оставьте комментарий!
    comments powered by HyperComments