- PVSM.RU - https://www.pvsm.ru -

Хабр, всем привет! Когда инфраструктура созрела до состояния персика и уже пора расширять классический SOC или вам всегда было интересно, как работает ВПО, то необходимо переходить к Threat Intelligence! Сегодня мы соберем свою лабу по исследованию вирусни/инструментов и процедур(PoC) в виртуальной среде для Linux-платформ.
Материал пригодиться, для понимания работы ВПО, написания митигации и детекции против них, а так же поиска паттернов в вашей инфраструктуре! На выходе мы получим список из артефактов, которые сможем использовать в работе.
Ожидается, что мы сможем трассировать системные вызовы, дампировать сетевой трафик, а так же рассматривать открытый и закрытый исходный код сэмпла в поисках подозрительных паттернов.
Преимущества такой лаборатории видны сразу:
существует мало современных SandBox решений, которые могут предоставить детализированный сырой лог сетевой и системной активности для Linux-платформ;
обычно SandBox очень легко обходят, уводя в сон вредонос, проверяя локации, раскладки и прочие атрибуты. В данном решении вы сможете увидеть, спит ли сэмпл, а так же использовать свои ресурсы, чтобы увеличить время трассировки в отличии от стандартных 500 секунд песка.
user execution очень органичен в стандартных решениях, необходимо использовать скрипты для выполнения дополнительных задач. В данном, ваша лаба полностью управляема вами.
возможность имитировать ифнраструктуру вокруг сэмпла, часто может пригодиться для исследования инструментов.
Все круто, но не без недостатков:
остается возможность прикрутить движки для вывода вердиктов, но лаба предназначена для ручного анализа;
нужна другая архитектура для тестирования атак на пространство ядра, так как здесь ядро ОС общее;
использование собственных ресурсов, вместо сервисных;
Архитектура представляет контур в рамках узла, где основные инструменты исследования будут развернуты в качестве драйвера pcap, бинарного файла strings, механизма eBPF и нормализатора на Python, а сэмпл будет воспроизводиться в контейнере.
Для изоляции двух сред будем использовать возможности Podman, который создаст нам виртуальное пространство с новыми namespace. Они должны ограничить возможность коммуникации, с другими обьектами. Seccomp запретит заражение пространства ядра, ограничив bpf, pivot_root, unshare, setns. Чтобы процесс не мог выйти из своего namespace или создать новый. Сgroup позволит выдать нужное количество ресурсов, а iptables разграничит по сети.
Для тех, кто не знаком с eBPF подкатом можно прочитать больше!
eBPF - это инструмент трассирования в Linux, который позволяет к точкам ОС крепить пользовательский код для мониторинга, фильтраций или изменения выполняемых функций. Работает он без системных вызовов поэтому зона его влияния - это функции разных пространств, сетевой трафик, а так же контроль точек LSM. Как упоминали раннее необходимо выбрать механизм и точку трассировки, такие как:
uprobe / uretprobe - функции пользовательского пространства
kprobe / kretprobe - функции пространства ядра
tracepoint - точки для событий между пространствами (syscalls)
network filter - точки сетевого трассирования
LSM-hooks - хуки системы безопасности (Linux Security Modules)
Конфигурировать механизм можно используя системные вызовы bpf(), а так же же готовые инструменты bpftrace со скриптами bt. Схема работы такова:

Такие решения позволяют реализовать низкоуровневый функционал управления сетевым трафиком, запуском функций СЗИ, а так же становятся целью злоумышленников.
В рамках развертывания лабы реализована частичная автоматизация, что упрощает работу, но основной массив действий будет описан ниже.
Для работы развернем дистрибутив Kali Linux последней версии и предустановим зависимости:
sudo apt update && apt install seccomp iptables binutils tcpdump wget podman -y
sudo apt-get install -y bpfcc-tools libbpfcc libbpfcc-dev linux-headers-$(uname -r)
sudo wget https://github.com/bpftrace/bpftrace/releases/download/v0.23.5/bpftrace
sudo cp ./bpftrace /usr/local/bin/bpftrace
chmod +x /usr/local/bin/bpftrace
wget https://github.com/gojue/ecapture/releases/download/v1.4.1/ecapture_v1.4.1_linux_amd64.deb
sudo apt install ./ecapture_v1.4.1_linux_amd64.deb
Перед тем, как развернуть контейнер выберите стратегию изоляции. ВПО часто останавливается, как только в процессе Discovery увидит, хоть что-то подозрительное. К примеру, если процесс вызовет ptrace (PTRACE_SYSCALL, pid, 0, 0) на себя и получит ответ -1, то этот отказ будет индикатором, того что его уже исследуют. Здесь есть три варианта:
Стратегия 1: Блокировка запрещенных системных вызовов
Контейнер (Namespaces, Cgroups, cap-drop) + Seccomp
Базовая изоляция и лишение привилегий через гранулирование root, а также черный список системных вызовов, которые будут заблокированы через seccomp. Блокируем ключевые системные вызовы, которые могут быть использованы для эскалации привилегий или побега из контейнера.
Стратегия 2: Подмена ответов syscall
Контейнер (Namespaces, Cgroups) + LD_PRELOAD
Использовать eBPF-программы/библиотеки для критически важных системных вызовов, которые малварь использует для проверки окружения (uname, stat, openat для чтения /proc/..., sysinfo). Для этих вызовов программа не будет блокировать доступ, а будет подменять возвращаемые данные на "безопасные" и "ожидаемые" малварью.
Стратегия 3: Гибрид первой и второй стратегии
Их комбинация позволит обойти большинство Anti-Debug проверок, LD_PRELOAD заменит ключевые возвращаемые значения, а Seccomp будет запрещать и выдавать кастомные ошибки ("errno": 2 // ENOENT - No such file or directory), вместо классической на отсутсвие привилегий.
После выбора стратегии развернем контейнер, в заранее определим сеть, выделяемые ресурсы, где так же укажем профиль seccomp.
Сеть для Podman:
sudo podman network create -d bridge
--subnet 172.20.0.0/24
--gateway 172.20.0.1
--ip-range 172.20.0.0/24
malwarenet

Многие ВПО любят проверять наличие systemd, так как в случае его отсутствия высока вероятность, что они запущены в виртуализованной среде. Превентивно выбираем podman вместо Docker из-за поддержки cgroup и возможности использовать systemd. Обязательно, добавим службу, а значит и пересоберем образ через Dockerfile:
FROM fedora:latest
# Установка Apache и systemd
RUN dnf -y install
httpd && dnf clean all
# Включение Apache
RUN systemctl enable httpd
# Экспорт порта
EXPOSE 80
# Сигнал остановки для systemd
STOPSIGNAL SIGRTMIN+3
# Запуск systemd
CMD ["/sbin/init"]
Сбилдим его командой:
sudo podman build -t fedora-systemd .
Так же в заранее подготовим конфигурационный файл Seccomp, который позволит снизить площадь атаки на ядро, но и не запретит работу systemd:
{
"defaultAction": "SCMP_ACT_ALLOW",
"architectures": [
"SCMP_ARCH_X86_64",
"SCMP_ARCH_X86",
"SCMP_ARCH_X32"
],
"syscalls": [
{
"names": [
"acct",
"add_key",
"bpf",
"clock_adjtime",
"clock_settime",
"create_module",
"delete_module",
"finit_module",
"get_kernel_syms",
"get_mempolicy",
"init_module",
"ioperm",
"iopl",
"kcmp",
"kexec_file_load",
"kexec_load",
"keyctl",
"lookup_dcookie",
"mbind",
"mount",
"move_pages",
"name_to_handle_at",
"nfsservctl",
"open_by_handle_at",
"perf_event_open",
"personality",
"pivot_root",
"process_vm_readv",
"process_vm_writev",
"ptrace",
"query_module",
"quotactl",
"reboot",
"request_key",
"set_mempolicy",
"setns",
"settimeofday",
"stime",
"swapon",
"swapoff",
"sysfs",
"syslog",
"_sysctl",
"umount",
"umount2",
"unshare",
"uselib",
"userfaultfd",
"ustat",
"vhangup"
],
"action": "SCMP_ACT_ERRNO"
}
]
}
После чего поднимем контейнер, где ограничим его ресурсы, прикрутим сетку, выберем образ и поставим фильтрацию syscall через seccomp:
sudo podman run -d
--name malware-analysis-container
--network malware-net
--ip 172.20.0.100
--security-opt seccomp=./file.json
--memory=1024m --cpus=1
--systemd=always
localhost/fedora-systemd:latest /sbin/init
Как видим seccomp отлично фильтрует системные вызовы, не давая выполнить опасные функции.

Так как схема соединения в виртуальных сетях NAT, примерно, такова:

То сделаем для нашей среды DMZ. Во избежание атаки на локальные компоненты, вы можете вносить белый список, при расширении вашей лаборатории.
#!/bin/bash
IFACE="eth0" # Основной интерфейс в интернет
NETWORK_INTERFACE=$(sudo podman network inspect malware-net -f '{{(index .Plugins 0).InterfaceName}}')
DOCKER_IFACE=$NETWORK_INTERFACE
CONTAINER_IP="172.20.0.100"
# Сброс правил
iptables -F
iptables -t nat -F
iptables -X
iptables -t nat -X
# Политики по умолчанию
iptables -P INPUT DROP
iptables -P OUTPUT DROP
iptables -P FORWARD DROP
# Разрешаем localhost
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
# DMZ правила
iptables -A FORWARD -s $CONTAINER_IP -o $IFACE -j ACCEPT
iptables -A FORWARD -d $CONTAINER_IP -i $IFACE -m state --state ESTABLISHED,RELATED -j ACCEPT
# Запрет LAN
iptables -A FORWARD -s $CONTAINER_IP -d 192.168.0.0/16 -j DROP
iptables -A FORWARD -s $CONTAINER_IP -d 10.0.0.0/8 -j DROP
iptables -A FORWARD -s $CONTAINER_IP -d 172.16.0.0/12 -j DROP
echo "DMZ zone for $CONTAINER_IP has been configured!"
В рамках Anti-Debug мер можно использовать eBPF-программу, которая заменит return для популярных discovery запросов на ложные. Однако не все вызовы возвращают ответ в syscall, там где данные превышают лимит - сразу записываются в userspace память процесса. Возникают сложности со смещениями, трудности в хранении больших данных и прочие. Однако можно использовать и его [1], но в данном кейсе мы рассмотрим механизм LD_PRELOAD.
Указав кастомную библиотеку через переменную LD_PRELOAD, мы сможем перехватить syscall за счет того, что загрузимся раньше системной библиотеки. Создаем файл с форматом file.c:
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <sys/utsname.h>
#include <dlfcn.h>
int (*orig_uname)(struct utsname *buf);
int uname(struct utsname *buf) {
if (!orig_uname) {
orig_uname = dlsym(RTLD_NEXT, "uname");
}
int ret = orig_uname(buf);
strcpy(buf->sysname, "SuperSecureOS");
strcpy(buf->nodename, "fakehost");
strcpy(buf->release, "9.9.9");
strcpy(buf->version, "v999");
strcpy(buf->machine, "x86_999");
strcpy(buf->domainname, "local.lan");
return ret;
}
Сначала мы обьявляем указатель на функцию, после перехватываем вызов, вызываем оригинальную функцию и полученный буфер меняем под собственные данные в контексте процесса.
Соберем библиотеку командой сразу в контейнере, чтобы совпадали версии glibc и контейнер не схлопнулся. Перекинем полученную библиотеку на хост и пересоберем образ:
gcc -shared -fPIC -ldl fake_uname.c -o libfakeuname.so
А после прикрепим к нашему контейнеру, заранее прокинув файл в него или можно создать новый образ через Dockerfile:
sudo podman run -d
--security-opt label=disable
--name malware-analysis-container
--network malware-net
--ip 172.20.0.100
--security-opt seccomp=./file.json
--memory=1024m --cpus=1
--env LD_PRELOAD=/lib1/libfakeuname.so
--systemd=always
localhost/fedora-systemd:latest /sbin/init
Запустим и проверим работоспособность:

Отлично, теперь ее можно использовать для подмены любых syscall, вы можете написать ложные ответы на самые популярные Discovery!
На данном этапе мы проводим сбор артефактов TTP’s, которые содержатся в системных вызовах, сетевом трафике, а так же в хранятся файле. Для исследования будем использовать bpftrace, strings и mitmproxy/tcpdump.
Обратите внимание, так как мы проводим исследование в ограниченной среде, то необходимо захватить для фильтрации только процессы контейнера. Здесь это реализовано через cgroup, которое динамически подставляется в скрипте. Трассируем syscall через шаблон:
/\* ===================== ПРОЦЕССЫ ===================== \*/
tracepoint:syscalls:sys_enter_execve
/ cgroup == $CGROUP_ID /
{
printf("{\\"time\\":\\"%s\\",\\"probe\\":\\"%s\\",\\"pid\\":%d,\\"ppid\\":%d,\\"comm\\":\\"%s\\",\\"exec\\":\\"%s\\"}\\n",
strftime("%Y-%m-%d %H:%M:%S", nsecs),
probe,
pid,
curtask->real_parent->tgid, // <-- ppid
comm,
str(args->filename));
}
Для дампа трафика нам необходимо видеть внутренности не только обычных пакетов, но и зашифрованных SSL.
eCapture - это решение на eBPF, которое позволяет прикрепиться к конкретной пользовательской функции OpenSSL и ей подобным, для сбора трафика перед его шифрованием. Решение отличное, но не совсем подходящее. Очень важно, чтобы библиотека хоста совпадала с версией той, которая в контейнере и ВПО или инструмент использовал туже библиотеку, а не свою принесенную. Такое поведение возможно только в лабораторных условиях, поэтому ее можно использовать для некоторых случаев, когда все требования совпали.

Будем использовать mitmproxy и tcpdump, данные от которых сохраняем в файлы. Выбираем необходимый интерфес, с которого будет идти трафик из нашего изолированного пространства. А для mitm proxy не забудем указать локальный адрес в bash.rc пользователя:
NETWORK_INTERFACE=$(sudo podman network inspect malware-net -f '{{(index .Plugins 0).InterfaceName}}')
sudo tcpdump -i $NETWORK_ID > ./net.txt &
Не забудем про открытые константы и вызываемые функции в бинарных файлах, а так же shellcode решений, соберем с них артефакты:
sudo strings filename > strings.txt
Объединим все решения выше в один скрипт для трассировки:
#!/bin/bash
if [ $# -eq 0 ]; then
echo "Usage: $0 <filename>"
exit 1
fi
if [ ! -f "$1" ]; then
echo "File $1 does not exist!"
exit 1
fi
CONTAINER_PID=$(sudo podman inspect --format '{{.State.Pid}}' $(sudo podman ps -q | head -n 1))
CGROUP_PATH=$(cut -d: -f3 /proc/$CONTAINER_PID/cgroup)
CGROUP_ID=$(stat -c %i /sys/fs/cgroup$CGROUP_PATH)
echo "Tracing container with PID=$CONTAINER_PID, cgroup inode=$CGROUP_ID"
cat <<EOF > ./script.bt
#!/usr/bin/env bpftrace
/* ===================== ПРОЦЕССЫ ===================== */
tracepoint:syscalls:sys_enter_execve
/ cgroup == $CGROUP_ID /
{
printf("{"time":"%s","probe":"%s","pid":%d,"ppid":%d,"comm":"%s","exec":"%s"}n",
strftime("%Y-%m-%d %H:%M:%S", nsecs),
probe,
pid,
curtask->real_parent->tgid, // <-- ppid корректно
comm,
str(args->filename));
}
/* ===================== ФАЙЛЫ ===================== */
tracepoint:syscalls:sys_enter_openat
/ cgroup == $CGROUP_ID /
{
printf("{"time":"%s","probe":"%s","pid":%d,"comm":"%s","pathname":"%s","flags":%d}n",
strftime("%Y-%m-%d %H:%M:%S", nsecs),
probe,
pid,
comm,
str(args->filename),
args->flags);
}
tracepoint:syscalls:sys_enter_unlinkat
/ cgroup == $CGROUP_ID /
{
printf("{"time":"%s","probe":"%s","pid":%d,"comm":"%s","target":"%s"}n",
strftime("%Y-%m-%d %H:%M:%S", nsecs),
probe,
pid,
comm,
str(args->pathname));
}
tracepoint:syscalls:sys_enter_renameat
/ cgroup == $CGROUP_ID /
{
printf("{"time":"%s","probe":"%s","pid":%d,"comm":"%s","oldpath":"%s","newpath":"%s"}n",
strftime("%Y-%m-%d %H:%M:%S", nsecs),
probe,
pid,
comm,
str(args->oldname),
str(args->newname));
}
/* ===================== СЕТЬ ===================== */
tracepoint:syscalls:sys_enter_socket
/ cgroup == $CGROUP_ID /
{
printf("{"time":"%s","probe":"%s","pid":%d,"comm":"%s","domain":%d,"type":%d,"protocol":%d}n",
strftime("%Y-%m-%d %H:%M:%S", nsecs),
probe,
pid,
comm,
args->family,
args->type,
args->protocol);
}
tracepoint:syscalls:sys_enter_connect
/ cgroup == $CGROUP_ID /
{
printf("{"time":"%s","probe":"%s","pid":%d,"comm":"%s","fd":%d,"addrlen":%d}n",
strftime("%Y-%m-%d %H:%M:%S", nsecs),
probe,
pid,
comm,
args->fd,
args->addrlen);
}
tracepoint:syscalls:sys_enter_accept
/ cgroup == $CGROUP_ID /
{
printf("{"time":"%s","probe":"%s","pid":%d,"comm":"%s","fd":%d}n",
strftime("%Y-%m-%d %H:%M:%S", nsecs),
probe,
pid,
comm,
args->fd);
}
/* ===================== UID/GID ===================== */
tracepoint:syscalls:sys_enter_setuid
/ cgroup == $CGROUP_ID /
{
printf("{"time":"%s","probe":"%s","pid":%d,"comm":"%s","uid":%d}n",
strftime("%Y-%m-%d %H:%M:%S", nsecs),
probe,
pid,
comm,
args->uid);
}
tracepoint:syscalls:sys_enter_setgid
/ cgroup == $CGROUP_ID /
{
printf("{"time":"%s","probe":"%s","pid":%d,"comm":"%s","gid":%d}n",
strftime("%Y-%m-%d %H:%M:%S", nsecs),
probe,
pid,
comm,
args->gid);
}
tracepoint:syscalls:sys_enter_setfsuid
/ cgroup == $CGROUP_ID /
{
printf("{"time":"%s","probe":"%s","pid":%d,"comm":"%s","fsuid":%d}n",
strftime("%Y-%m-%d %H:%M:%S", nsecs),
probe,
pid,
comm,
args->uid);
}
/* ===================== SECURITY-SENSITIVE ===================== */
tracepoint:syscalls:sys_enter_bpf
/ cgroup == $CGROUP_ID /
{
printf("{"time":"%s","probe":"%s","pid":%d,"comm":"%s","cmd":%d}n",
strftime("%Y-%m-%d %H:%M:%S", nsecs),
probe,
pid,
comm,
args->cmd);
}
tracepoint:syscalls:sys_enter_ptrace
/ cgroup == $CGROUP_ID /
{
printf("{"time":"%s","probe":"%s","pid":%d,"comm":"%s","request":%d,"target":%d}n",
strftime("%Y-%m-%d %H:%M:%S", nsecs),
probe,
pid,
comm,
args->request,
args->pid);
}
EOF
echo "Start tracing..."
sudo bpftrace ./script.bt > ./syscall.txt &
echo "Start network dumping..."
NETWORK_INTERFACE=$(sudo podman network inspect malware-net -f '{{(index .Plugins 0).InterfaceName}}')
sudo tcpdump -i $NETWORK_ID > ./net.txt &
echo "Start strings search..."
sudo strings "$1" > ./strings.txt &
Синтаксис script <malware.file> требует вредоносное ПО, которое будет проверено strings и выведены строки, как и телеметрия по остальным трассировщикам.

Напишем нормализатор, который позволит текущие данные привести в единый вид и файл:
import json
import argparse
from datetime import datetime
import requests
def parse_syscalls(sys_file):
events = []
with open(sys_file) as f:
for line in f:
try:
ev = json.loads(line)
ev["source"] = "syscall"
events.append(ev)
except json.JSONDecodeError:
events.append({"source": "syscall", "raw": line.strip(), "parse_error": True})
return events
def parse_network(net_file, date=None):
events = []
with open(net_file) as f:
for line in f:
line = line.strip()
if not line:
continue
time_str, rest = line.split(" ", 1)
if date:
ts = f"{date} {time_str}"
else:
ts = datetime.now().strftime("%Y-%m-%d ") + time_str
events.append({
"time": ts,
"source": "net",
"raw": line
})
return events
def parse_strings(strings_file, date=None):
events = []
with open(strings_file) as f:
for line in f:
line = line.strip()
if not line:
continue
ts = date + " 00:00:00" if date else datetime.now().strftime("%Y-%m-%d %H:%M:%S")
events.append({
"time": ts,
"source": "strings",
"raw": line
})
return events
def merge_events(*lists):
all_events = []
for lst in lists:
all_events.extend(lst)
# сортировка по времени
def parse_time(ev):
try:
return datetime.strptime(ev["time"], "%Y-%m-%d %H:%M:%S.%f")
except ValueError:
try:
return datetime.strptime(ev["time"], "%Y-%m-%d %H:%M:%S")
except:
return datetime.min
return sorted(all_events, key=parse_time)
def send_to_ai(events, api_key):
url = "https://gpt.serverspace.ru/v1/chat/completions"
payload = {
"model": "openai/gpt-4o",
"max_tokens": 16384,
"top_p": 0.1,
"temperature": 0.6,
"messages": [
{
"role": "user",
"content": json.dumps(events)
}
]
}
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
r = requests.post(url, headers=headers, data=json.dumps(payload))
return r.json()
parser = argparse.ArgumentParser()
parser.add_argument("--sys", required=True, help="Syscall JSONL file")
parser.add_argument("--net", required=True, help="Network log txt file")
parser.add_argument("--strings", required=True, help="Strings output file")
parser.add_argument("--date", help="Date for network/strings events YYYY-MM-DD")
parser.add_argument("--out", help="Output JSONL file")
parser.add_argument("--api_key", help="GPT API key")
args = parser.parse_args()
sys_events = parse_syscalls(args.sys)
net_events = parse_network(args.net, date=args.date)
str_events = parse_strings(args.strings, date=args.date)
all_events = merge_events(sys_events, net_events)
if args.out:
with open(args.out, "w") as f:
for ev in all_events:
f.write(json.dumps(ev) + "n")
else:
for ev in all_events:
print(json.dumps(ev))
if args.api_key:
result = send_to_ai(all_events, args.api_key)
print(json.dumps(result, indent=2))
Преимущество подобной лабы в подробном логе, который остается после дампа, но аналитика все равно нужна. Как аналог движков будем использовать GPT-4o на базе платформы Serverspace [2], которая подсветит TTP’s и суммаризирует данные.
Синтаксис достаточно прост, необходимо указать пути к вердиктам и API, при запуске скрипта:
python normalize_and_send.py
--sys syscalls.jsonl
--net net.txt
--strings malware_strings.txt
--date 2025-09-02
--out merged.jsonl
--api_key "YOUR_API_KEY"
После выполнения получим следующий результат:

В файле лежит полноценный лог активности процесса в ОС Linux с timestep, который отображает потенциальные артефакты системной, сетевой коммуникации, а так же вердикт от AI о вредоносных событиях. Данное решение не является конечным, безусловно, можно докрутить движки, оптимизировать код и автоматизировать развертку. Однако оно отлично подходит для решения операционных задач по исследованию сэмплов на вредоносную активность!
Статья поддерживается командой Serverspace [3].
Serverspace [4] — провайдер облачных сервисов, предоставляющий в аренду виртуальные серверы [5] с ОС Linux и Windows в 8 дата-центрах: Россия, Беларусь, Казахстан, Нидерланды, Турция, США, Канада и Бразилия. Для построения ИТ-инфраструктуры провайдер также предлагает: создание сетей, шлюзов, бэкапы, сервисы CDN, DNS, объектное хранилище S3 [6].
IT-инфраструктура | Удвоение первого платежа по коду HABR

Автор: an1ik
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/linux/429708
Ссылки в тексте:
[1] использовать и его: https://docs.ebpf.io/linux/helper-function/bpf_probe_write_user/
[2] на базе платформы Serverspace: https://serverspace.ru/services/serverspace-gpt-api/
[3] Serverspace: https://serverspace.ru/?utm%5C_source=habr&utm%5C_medium=social&utm%5C_campaign=habr%5C_bannerss&utm%5C_term=banner%5C_habr
[4] Serverspace: https://serverspace.ru/?utm_source=habr&utm_medium=social&utm_campaign=habr_bannerss&utm_term=banner_habr">Serverspace</a>
[5] виртуальные серверы: https://serverspace.ru/services/cloud-servers/?utm_source=habr&utm_medium=social&utm_campaign=habr_banner_cloud_servers&utm_term=habr_banner_cloud_servers
[6] объектное хранилище S3: https://serverspace.ru/services/storage/?utm_source=habr&utm_medium=social&utm_campaign=habr_banner_storage&utm_term=habr_banner_storage
[7] Источник: https://habr.com/ru/companies/serverspace/articles/944056/?utm_campaign=944056&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.