Proxy сервер на Delphi и Winsock API
Рубрика: Delphi -> Программирование -> Журнал Хакер -> Статьи
Метки: delphi | sockets | программирование
Просмотров: 94107
Мы много раз рассказывали тебе про прокси-серверы: для чего они нужны, как они работают и чем полезны хакеру. Тем не менее знать и использовать – это одно, а вот создавать самому – совсем другое дело. Этот творческий труд полезен для души, тела и, конечно же, твоего WM-кошелька.
Немного теории
Итак, прокси-сервер – это прежде всего программа, выступающая посредником между клиентом и сервером. Все привыкли связывать понятие прокси только с протоколом HTTP. На самом деле существуют проксики и для других протоколов, о которых я расскажу чуть позже. Самый распространенный вид проксиков – HTTP. При работе через HTTP-прокси твой браузер не будет соединяться с сервером, на котором расположен запрашиваемый сайт, он соединится с прокси и передаст ему запрос. Получив от тебя все необходимые данные, проксик сам сконнектится с удаленным web-сервером и отправит твой запрос. После его обработки web-сервер вернет документ проксику, который затем отправит его тебе. Такие проксики полезны, когда нужна анонимность (поскольку они бывают прозрачными) или если твой провайдер ограничивает тебя и не разрешает посещать сайты, расположенные на забугорных серверах.
Еще одно место, где постоянно используются прокси-серверы, – корпоративные (домашние) локальные сети. Для предоставления сотрудникам компании доступа в инет админы устанавливают на шлюзе проксик, и вся контора ходит через него в Сеть. Плюсы такого способа очевидны: можно легко отслеживать маршруты пользователей, считать количество израсходованного трафика и быть уверенным в том, что юзеры не будут пользоваться лишним софтом, так как не каждая программка способна работать через HTTP-прокси.
Я уже говорил, что HTTP-прокси не является единственным типом прокси-серверов. В природе также встречаются:
Зачем писать свой прокси-сервер
Разобравшись на практике с основами написания прокси-серверов, ты сможешь пополнить коллекцию ][-тулз собственного изготовления. Например, можно без труда сделать прокси абсолютно невидимым в системе. Подсунув такую штуку соседу, в случае если он не думает о 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. Функции необходимо заслать два параметра:
При успешном выполнении функция вернет 0. Для получения кодов ошибок в WinSock API служит функция WSAGetLastError(). Ей не нужно передавать какие-либо параметры, после вызова она возвращает код последней возникшей при работе с сетевыми функциями ошибки.
function socket (af:integer; type:integer; protocol:integer):TSocket, stdcall;
Перед тем как соединиться с удаленным узлом, нужно создать «розетку» - socket. Как раз за его создание и отвечает одноименная функция socket. Входных параметров три:
Результатом выполнения будет новый сокет. Создав сокет, можно пробовать подключаться. Для этого в библиотеке реализована функция Connect.
function Connect (S:TSocket; var name:TSockAddr; namelen:integer):Integer:stdcall;
Параметрами для функции служат:
Успешно выполнившись, а значит, и установив соединение, функция вернет 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;
Параметры для обеих функций одинаковые:
Выполнившись, функция вернет фактическое количество отправленных/принятых байт.
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 не оправдывает себя. В качестве параметров функция принимает:
procedure FD_ZERO (var FDSet: TFDSet);
Очистка и инициализация набора сокетов. Перед добавлением сокетов в набор необходимо его проинициализировать с помощью этой функции.
procedure FD_SET(Socket: TSocket; var FDSet: TFDSet);
Процедура предназначена для добавления сокета, переданного в первом параметре в набор, указанный во втором.
function FD_ISSET(Socket: TSocket; var FDSet: TFDSet): Boolean;
Функция позволяет проверить вхождение сокета (первый параметр) в набор (второй параметр).
Кодим
Вот и настала та заветная минута, когда мы заканчиваем разбираться с теорией и приступаем к реальному кодингу. Запускай Delphi, создавай новый проект и придавай форме вид, похожий на вид моей формы. Мы не будем ничего скрывать от пользователя, поскольку, если ты помнишь, мы пишем программу в образовательных целях. Ты там дальше сам разберешься. На форме у меня три кнопки:
В остальной части формы у меня располагается ListView с тремя колонками. В них мы будем отображать IP клиентов и адреса хостов, к которым они обратились. По событию OnClick для кнопки «Запустить» напиши следующий код:
_listenThread := TListenThread.Create (false);
Этой одной-единственной строчкой кода мы создаем новый поток типа TListenThread. Потоки можно создавать приостановленными. Именно поэтому в качестве параметра метода Create я передаю значение false, требующее немедленного запуска.
Поток 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.
Создав сокет, нужно убедиться, что после выполнения функции 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().
Как только соединение будет установлено, нужно перевести сокет в асинхронный режим. Смена режима происходит с помощью функции setsockopt(). Перевод в асинхронный режим необходим, поскольку в таком случае нехило повысится производительность нашего приложения. Это станет возможным из-за минимизации задержек перед пересылкой данных между нами, web-сервером и клиентом. Получив от сервера порцию данных, мы не будем ждать остальных, а будем сразу отправлять ее клиенту.
Итак, переведя сокет в асинхронный режим, можно смело отправлять серверу запрос, ранее полученный от клиента и запускать бесконечный цикл, в котором будет реализован обмен данным. Перед тем как читать данные, нужно добавить сокеты в набор для ожидания. Как ты помнишь, после мы сможем проверять готовность сокета с помощью функции Select().
Ну а дальше все просто. Остается только сделать проверки сокетов. Если запрос пришел от клиента, то перенаправляем его web-серверу; если от сервера, то, наоборот, отправляем его клиенту. Для проверки я запустил созданный проксик у себя на компе, сконфигурировал Opera и попробовал зайти на один из сайтов локальной сети, пользователем которой я явлюсь. После отправки запроса моя опера шустренько начала принимать данные от прокси-сервера. Тем временем ListView стал заполняться моим IP и адресом хоста, к которому я посылал запрос.
Слуховое окно прорублено
Надеюсь, сегодняшний пример получился достаточно полезным как для программиста, так и для хакера. В очередной раз ты убедился, что Delphi – это не только базы данных и отчеты, но и язык, с помощью которого можно решать как прикладные, так и хакерские задачи. Мне остается только пожелать тебе успешного применения полученных знаний в своих будущих проектах.
Исходник примера Proxy сервер на Delphi
Статья опубликована журнале "Хакер" (http://xakep.ru). Январь 2008 г.
Ссылка на опубликованную статью сайта издания: http://goo.gl/juyZFN
Ссылка на журнал: http://goo.gl/q7uKzg