Анонимный RAT при глобальном наблюдателе
Предисловие
Удалённый доступ может быть как очень опасной программной функцией, так и очень полезной — всё зависит от контекста, намерений, задач и целей с которыми подобные программы будут применяться. Такая же ситуация с анонимностью и анонимными коммуникациями в общем. Они могут как скрывать злонамеренную активность, так и скрывать законную активность от посторонних, которым её выявление может быть выгодно как по финансовым, так и по политическим причинам. Вследствие этого, технология остаётся нейтральной, ровно, как и любая полезная, и в это же самое время потенциально опасная вещь.
Под сокращением RAT будет пониматься далее Remote Access Tool, а не Remote Access Trojan, т.к. целью данной статьи не ставится написание какого-либо зловредного ПО, хоть и отличий в плане функциональности для этих двух обозначений имеется не так-то много.
Введение
При наличии глобального наблюдателя анонимные коммуникации очень ограничены в своих возможностях. Во-первых, сами коммуникации становятся очень медленными, отправление и получение информации могут занимать секунды и минуты времени. Во-вторых, таковые коммуникации плохо масштабируются и будут сводиться лишь к малым группам. Превышение группы будет приводить к увеличению процессорной нагрузки, что приведёт к отключению наиболее слабых участников. Понижение процессорной нагрузки приведёт к увеличению спама и перегрузке сетевого канала. Вследствие подобных функциональных ограничений возникает также и ряд прикладных ограничений в лице невозможности или сложности написания стриминговых сервисов, передачи больших файлов, реализации систем синхронизации и т.п. Благо, программы удалённого доступа не относятся к такому классу ограничений, а потому мы можем их реализовывать вполне корректно, руководствуясь теоретически доказуемой анонимностью.
Теоретически доказуемая анонимность
Анонимными сетями с теоретически доказуемой анонимностью принято считать замкнутые, полностью прослушиваемые системы, в которых становится невозможным осуществление любых пассивных атак (в том числе и при существовании глобального наблюдателя) направленных на деанонимизацию факта отправления и/или получения информации, или на деанонимизацию связи между отправителем и получателем с минимальными условностями по количеству узлов неподчинённых сговору. Говоря иначе, с точки зрения пассивного атакующего, апостериорные знания, полученные вследствие наблюдений, должны оставаться равными априорным, до наблюдений, тем самым сохраняя равновероятность деанонимизации по N-ому множеству субъектов сети.
На текущий момент времени существует три задачи анонимизации с теоретической доказуемостью: DC (Dining Cryptographers, проблема обедающих криптографов), EI (Entropy Increase, проблема увеличения энтропии), QB (Queue Based, проблема на базе очередей). Анонимные сети Herbivore, Dissent, PriFi относятся к DC, сети Hidden Lake, M-A к QB, у EI нет представителей. Из данного списка ныне живой остаётся лишь сеть Hidden Lake. На её основе мы и попробуем написать программу удалённого доступа.
Немного о QB-задаче
О QB-задаче я писал достаточно много в своих предыдущих статьях и работах. Наиболее подробное описание данной задачи можно найти в работе «Анонимная сеть «Hidden Lake» со всеми особенностями, преимуществами и недостатками. Если говорить о практике вкратце, то суть задачи будет сводиться к выполнению следующих шагов:
Каждое сообщение шифруется ключом получателя,
Сообщение отправляется в период = T всем участникам сети,
Период T одного участника независим от периодов T1, T2, …, Tn других участников,
Если на период T сообщения не существует, то в сеть отправляется ложное сообщение без получателя,
Каждый участник пытается расшифровать принятое им сообщение из сети.
QB-сеть из трёх участников
QB-задача называется так, потому что перед отправлением сообщения в сеть каждый узел сначала сохраняет его в своей очереди и лишь спустя период времени равный T достаёт сообщение из очереди и отправляет его в сеть.
QB-сети достаточно успешно могут противостоять глобальным наблюдателям, особенно если таковые являются пассивными слушателями трафика. Так например, достаточно легко показать, что глобальный наблюдатель не получает никакой информации от того, что он способен слушать входной и выходной трафик всей сети. Он видит лишь картину, при которой каждый узел отправляет шифрованное сообщение в свой заданный период времени всем участникам сети, не понимая при этом является ли сообщение ложным или истинным. Другими словами, QB-сеть можно представить как генератор ложного трафика с функцией кратковременной замены определённой части шума на истинный трафик. На этом свойстве и формируется анонимность.
Тем не менее у QB-задачи есть и ряд недостатков — один из них таков, что QB-сети плохо анонимизируют связь самих абонентов друг к другу. Иными словами, предполагается, что отправитель и получатель знают друг друга и доверяют друг другу. Эта проблема базируется на отсутствии в QB-задачи полиморфизма информации, т.е. её изменчивости по мере передачи от одного узла к другому. Вследствие этого, если один из абонентов будет атакующим и у него в союзниках будет глобальный наблюдатель, то деанонимизация второго абонента, а точнее связывание его IP (фактического сетевого адреса) с его публичным ключом (криптографическим адресом), будет считаться тривиальной задачей. Тем не менее, такая деанонимизация даст лишь связывание адресов одного абонента, но ничего не скажет о связи данного абонента с другими участниками в сети, чего и предполагает QB-задача.
Полиморфизм информации
Полиморфизм информации — это есть свойство изменчивости передаваемого объекта при множественной маршрутизации несколькими субъектами сети, разграничивающее связь субъектов посредством анализа объекта. Так например, если существует три субъекта сети {A, B, C} и объект P, который передаётся от A к B и от B к C соответственно, то внешний вид информации P1 и P2 должен определяться как [P1 = (A → B)] ≠ [P2 = (B → C)], где P ∉ {P1, P2} и P1 ≠ P2, (B не связывает {P1, P2} с P)и (A не связывает {P1, P} с P2) и/или (Cне связывает {P2, P} с P1). В большинстве случаев полиморфизм информации достигается множественным шифрованием объекта: [E2(E1(P)) = (A → B)] ≠ [E1(P) = (B → C)], (где E — функция шифрования), при котором интерстициальный, промежуточный субъект B становится неспособным связать {E2(E1(P)), E1(P)} с P, а субъект C неспособен связать {E1(P), P} с E2(E1(P)).
Немного о сети Hidden Lake
Анонимная сеть Hidden Lake (HL) — это децентрализованная F2F (friend-to-friend) анонимная сеть с теоретической доказуемостью. В отличие от известных анонимных сетей, подобия Tor, I2P, Mixminion, Crowds и т.п., сеть HL способна противостоять атакам глобального наблюдателя. Сети Hidden Lake для анонимизации своего трафика не важны такие критерии как: 1) уровень сетевой централизации, 2) количество узлов, 3) расположение узлов и 4) связь между узлами в сети. На основе таковых свойств, HL способна внедряться в уже готовые и существующие централизованные системы, формируя тем самым анонимность её пользователей, как было продемонстрировано в статье «Анонимная P2P-сеть внутри централизованного HTTPS-сервера: вшиваем паразитный трафик всеми правдами и неправдами».
Сеть Hidden Lake базируется на микросервисной архитектуре. Это означает, что каждая новая функциональность, каждое новое приложение добавляется в сеть посредством реализации нового сервиса. Плюсом такой архитектуры является гибкость реализации, потому как можно добавлять нужные и удалять ненужные функции, не редактируя и не изменяя при этом код сети. Плюс к этому, новые сервисы могут писаться на любой удобной технологии и языке программирования, тем самым, легко интегрируясь в общую архитектуру.
Ядром сети является приложение HLS (HIdden Lake Service). Оно исполняет две функции: анонимизирует трафик и маршрутизирует запросы. Анонимизация трафика основывается на QB-задаче. Маршрутизация запросов завязана на перенаправлении входного трафика к прикладным сервисам и выходного трафика к внешней сети. Таким образом, чтобы написать новое прикладное приложение, по типу программы удалённого доступа, нам необходимо работать с API HLS.
Пишем RAT поверх Hidden Lake
Будущую прикладную программу, исполняющую функцию удалённого доступа, мы будем именовать вкратце HLR (Remoter), по типу других прикладных приложений (HLM — Messenger, HLF — Filesharer) в сети Hidden Lake.
Для того чтобы HLR был более безопасен, необходимо также реализовать в нём дополнительный слой аутентификации поверх аутентификации используемой в HLS по публичным ключам. Для этого вполне подойдут пароли, которые можно отправлять в HTTP заголовках. Всё это необходимо сделать по двум причинам:
HLS может быть завязан на исполнении нескольких прикладных приложений. Если бы не существовало пароля, то предназначенная аутентификация, например, для мессенджера или файлообменника, ровно также бы влияла и на удалённый доступ, что не являлось бы корректным. HLR мог бы неявным образом влиять и на HLM, и на HLF, нарушая их изолированность и безопасность,
HLS может быть запущен с отключенной F2F опцией. Это является вполне допустимым действием для некоторых прикладных использований, позволяющим клиентам сети подключаться и сразу отправлять сообщения / запросы без предварительного согласования. Если бы пароля не существовало, то HLR не мог бы безопасно функционировать с отключенной F2F опцией.
Для реализации HLR, в первую очередь необходимо написать HTTP обработчик, который будет принимать запрос извне, вытаскивать тело запроса и выполнять команду, находящуюся в теле.
// С целью упрощения из кода удалены строки с логгированием.
func HandleIncomigExecHTTP(pCtx context.Context, pConfig config.IConfig) http.HandlerFunc {
return func(pW http.ResponseWriter, pR *http.Request) {
// Говорим HLS, что наш обработчик должен отвечать
// на принимаемый запрос.
pW.Header().Set(hls_settings.CHeaderResponseMode, hls_settings.CHeaderResponseModeON)
// Устанавливаем, что команды мы можем принимать только
// по HTTP методу POST.
if pR.Method != http.MethodPost {
_ = api.Response(pW, http.StatusMethodNotAllowed, "failed: incorrect method")
return
}
// Проверяем корректность пароля к функции удалённого доступа.
sett := pConfig.GetSettings()
if pR.Header.Get(hlr_settings.CHeaderPassword) != sett.GetPassword() {
_ = api.Response(pW, http.StatusForbidden, "failed: request forbidden")
return
}
// Читаем тело запроса.
cmdBytes, err := io.ReadAll(pR.Body)
if err != nil {
_ = api.Response(pW, http.StatusConflict, "failed: response message")
return
}
// Проверяем, что в теле запроса все символы читаемы,
// отсутствуют бинарные данные.
cmdStr := string(cmdBytes)
if chars.HasNotGraphicCharacters(cmdStr) {
_ = api.Response(pW, http.StatusBadRequest, "failed: has not graphic characters")
return
}
// Устанавливаем время за которое должна быть исполнена команда.
execTimeout := time.Duration(sett.GetExecTimeoutMS()) * time.Millisecond
ctx, cancel := context.WithTimeout(pCtx, execTimeout)
defer cancel()
// Разделяем команду по вызову конкретной программы и её аргументам.
// И далее исполняем команду с выводом результата.
cmdSplited := strings.Split(cmdStr, hlr_settings.CExecSeparator)
out, err := exec.CommandContext(ctx, cmdSplited[0], cmdSplited[1:]...).Output() // nolint: gosec
if err != nil {
_ = api.Response(pW, http.StatusInternalServerError, "failed: "+err.Error())
return
}
// Возвращаем полученный результат команды в качестве ответа.
_ = api.Response(pW, http.StatusOK, out)
}
}
Используемые функции, интерфейсы и константы
Response
const (
CTextPlain = "text/plain"
CApplicationJSON = "application/json"
CApplicationOctetStream = "application/octet-stream"
)
func Response(
pW http.ResponseWriter,
pRet int,
pRes interface{},
) error {
var (
contentType string
respBytes []byte
)
switch x := pRes.(type) {
case []byte:
contentType = CApplicationOctetStream
respBytes = x
case string:
contentType = CTextPlain
respBytes = []byte(x)
default:
contentType = CApplicationJSON
respBytes = encoding.SerializeJSON(x)
}
pW.Header().Set("Content-Type", contentType)
pW.WriteHeader(pRet)
if _, err := io.Copy(pW, bytes.NewBuffer(respBytes)); err != nil {
return utils.MergeErrors(ErrCopyBytes, err)
}
return nil
}
HasNotGraphicCharacters
func HasNotGraphicCharacters(pS string) bool {
for _, c := range pS {
if !unicode.IsGraphic(c) {
return true
}
}
return false
}
IConfig
type IConfig interface {
GetSettings() IConfigSettings
// <...>
}
type IConfigSettings interface {
GetExecTimeoutMS() uint64
GetPassword() string
}
Константы
const (
CHeaderPassword = "Hl-Remoter-Password"
CExecSeparator = "[@remoter-separator]"
)
И это весь код. Осталось его лишь связать с роутом на путь /exec
, запустить полученный сервис и привязать его адрес к HLS. Сделать последнее действие можно просто добавив в файл hls.yml (который создаётся при первом запуске HLS) путь к адресу нашего HLR приложения.
# <...>
# Конфигурационный файл HLS
services:
hidden-lake-remoter:
# (HLR был запущен на port=9532)
host: localhost:9532
# <...>
Теперь, всё что нам остаётся, так это протестировать работу HLR. Для этого я подготовил простой docker-compose сценарий со всеми настроенными конфигурационными файлами в директориях recv_hlc и send_hls. Его можно посмотреть по этой ссылке.
В примере используется похожая схема взаимодействия сервисом между собой. Отличия только в двух деталях. Первое: вместо двух прикладных приложений — HLM, HLF используется одно — HLR. Второе: вместо одного HLT используется три HLT выстроенных в цепочку.
Вкратце, recv_hlc является узлом Bob, у которого запущен HLR. Узел send_hls является узлом Alice, отправляющим команду на recv_hlc узел поверх анонимизированного трафика. Оба узла связываются между собой посредством цепочки из трёх ретрансляторов (HLT — Traffic). Alice подключается к первому ретранслятору, Bob к третьему, первый и третий ретрансляторы связаны между собой посредством второго. Как-либо на функциональность или качество анонимности это никак не влияет. Сделано всё это лишь с той целью, чтобы проверить корректность работы при условии, когда узлы не соединяются напрямую.
Конфигурационные файлы
На стороне recv_hlc (Bob)
# hls.yml
settings:
message_size_bytes: 8192
work_size_bits: 22
key_size_bits: 4096
fetch_timeout_ms: 60000
queue_period_ms: 5000
rand_queue_period_ms: 5000
rand_message_size_bytes: 4096
logging:
- info
- warn
- erro
services:
hidden-lake-remoter:
host: recv_hlc:9532
connections:
- middle_hlt_3:6581
friends:
Alice: >-
PubKey{3082020A0282020100C17B6FA53983050B0339A0AB60D20A8A5FF5F8210564464C45CD2FAC2F266E8DDBA3B36C6F356AE57D1A71EED7B612C4CBC808557E4FCBAF6EDCFCECE37494144F09D65C7533109CE2F9B9B31D754453CA636A4463594F2C38303AE1B7BFFE738AC57805C782193B4854FF3F3FACA2C6BF9F75428DF6C583FBC29614C0B3329DF50F7B6399E1CC1F12BED77F29F885D7137ADFADE74A43451BB97A32F2301BE8EA866AFF34D6C7ED7FF1FAEA11FFB5B1034602B67E7918E42CA3D20E3E68AA700BE1B55A78C73A1D60D0A3DED3A6E5778C0BA68BAB9C345462131B9DC554D1A189066D649D7E167621815AB5B93905582BF19C28BCA6018E0CD205702968885E92A3B1E3DB37A25AC26FA4D2A47FF024ECD401F79FA353FEF2E4C2183C44D1D44B44938D32D8DBEDDAF5C87D042E4E9DAD671BE9C10DD8B3FE0A7C29AFE20843FE268C6A8F14949A04FF25A3EEE1EBE0027A99CE1C4DC561697297EA9FD9E23CF2E190B58CA385B66A235290A23CBB3856108EFFDD775601B3DE92C06C9EA2695C2D25D7897FD9D43C1AE10016E51C46C67F19AC84CD25F47DE2962A48030BCD8A0F14FFE4135A2893F62AC3E15CC61EC2E4ACADE0736C9A8DBC17D439248C42C5C0C6E08612414170FBE5AA6B52AE64E4CCDAE6FD3066BED5C200E07DBB0167D74A9FAD263AF253DFA870F44407F8EF3D9F12B8D910C4D803AD82ABA136F93F0203010001}
# hlr.yml
settings:
exec_timeout_ms: 5000
password: DpxJFjAlrs4HOWga0wk14mZqQSBo9DxK
logging:
- info
- warn
- erro
address:
incoming: :9532
На стороне send_hls (Alice)
# hls.yml
settings:
message_size_bytes: 8192
work_size_bits: 22
key_size_bits: 4096
fetch_timeout_ms: 60000
queue_period_ms: 5000
rand_queue_period_ms: 5000
rand_message_size_bytes: 4096
logging:
- info
- warn
- erro
address:
http: :7572
connections:
- middle_hlt_1:6581
friends:
Bob: >-
PubKey{3082020A0282020100B752D35E81F4AEEC1A9C42EDED16E8924DD4D359663611DE2DCCE1A9611704A697B26254DD2AFA974A61A2CF94FAD016450FEF22F218CA970BFE41E6340CE3ABCBEE123E35A9DCDA6D23738DAC46AF8AC57902DDE7F41A03EB00A4818137E1BF4DFAE1EEDF8BB9E4363C15FD1C2278D86F2535BC3F395BE9A6CD690A5C852E6C35D6184BE7B9062AEE2AFC1A5AC81E7D21B7252A56C62BB5AC0BBAD36C7A4907C868704985E1754BAA3E8315E775A51B7BDC7ACB0D0675D29513D78CB05AB6119D3CA0A810A41F78150E3C5D9ACAFBE1533FC3533DECEC14387BF7478F6E229EB4CC312DC22436F4DB0D4CC308FB6EEA612F2F9E00239DE7902DE15889EE71370147C9696A5E7B022947ABB8AFBBC64F7840BED4CE69592CAF4085A1074475E365ED015048C89AE717BC259C42510F15F31DA3F9302EAD8F263B43D14886B2335A245C00871C041CBB683F1F047573F789673F9B11B6E6714C2A3360244757BB220C7952C6D3D9D65AA47511A63E2A59706B7A70846C930DCFB3D8CAFB3BD6F687CACF5A708692C26B363C80C460F54E59912D41D9BB359698051ABC049A0D0CFD7F23DC97DA940B1EDEAC6B84B194C8F8A56A46CE69EE7A0AEAA11C99508A368E64D27756AD0BA7146A6ADA3D5FA237B3B4EDDC84B71C27DE3A9F26A42197791C7DC09E2D7C4A7D8FCDC8F9A5D4983BB278FCE9513B1486D18F8560C3F31CC70203010001}
Ретранслятор 1
settings:
message_size_bytes: 8192
key_size_bits: 4096
work_size_bits: 22
messages_capacity: 2048
rand_message_size_bytes: 4096
logging:
- info
- warn
- erro
address:
tcp: :6581
Ретранслятор 2
settings:
message_size_bytes: 8192
key_size_bits: 4096
work_size_bits: 22
messages_capacity: 2048
rand_message_size_bytes: 4096
logging:
- info
- warn
- erro
address:
tcp: :6581
connections:
- middle_hlt_1:6581
Ретранслятор 3
settings:
message_size_bytes: 8192
key_size_bits: 4096
work_size_bits: 22
messages_capacity: 2048
rand_message_size_bytes: 4096
logging:
- info
- warn
- erro
address:
tcp: :6581
connections:
- middle_hlt_2:6581
Теперь, нам необходимо как-то отправить запрос со стороны Alice на сторону Bob. Для этого можно использовать простой bash скрипт. С помощью него мы попробуем создать file.txt
на стороне Bob’a с содержимым hello, world
, и далее прочитать его.
#!/bin/bash
BASE64_BODY="$(\
echo -n 'bash[@remoter-separator]-c[@remoter-separator]echo 'hello, world' > file.txt && cat file.txt' | \
base64 -w 0 \
)";
PUSH_FORMAT='{
"receiver":"Bob",
"req_data":{
"method":"POST",
"host":"hidden-lake-remoter",
"path":"/exec",
"head":{
"Hl-Remoter-Password": "DpxJFjAlrs4HOWga0wk14mZqQSBo9DxK"
},
"body":"'${BASE64_BODY}'"
}
}';
d="$(date +%s)";
curl -i -X POST -H 'Accept: application/json' http://localhost:7572/api/network/request --data "${PUSH_FORMAT}";
echo && echo "Request took $(($(date +%s)-d)) seconds";
Запускаем docker контейнеры:
cd examples/anonymity/remoter/routing
make
> docker-compose build
> docker-compose up
Запускаем скрипт:
cd examples/anonymity/remoter
./_request/request.sh
Получаем ответ:
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Sun, 21 Jul 2024 20:43:45 GMT
Content-Length: 93
{"code":200,"head":{"Content-Type":"application/octet-stream"},"body":"aGVsbG8sIHdvcmxkCg=="}
Request took 9 seconds
Декодируем base64 из тела ответа, получая тем самым нужную строку:
echo "aGVsbG8sIHdvcmxkCg" | base64 -d INT ✘
> hello, world
Заключение
В результате всего нами написанного была реализована программа удалённого доступа — HLR поверх анонимной сети Hidden Lake. Из преимуществ такой программы, в сравнении например с SSH, можно выделить скрытие реальной активности для глобального наблюдателя, а также возможность выступать в роли клиента, не заботясь о поднятии сервера на определённо заданном адресе и порту.
Все исходные коды мною написанного можно найти тут. Проверить локально работоспособность сервиса HLR можно тут. Статьи, презентации, книги, схемы и прочую документацию по сети Hidden Lake можно найти здесь.