D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Дорогие пользователи xss.is, пишу данную статью для вас, чтобы ввести вас немного в курс дела, как можно достаточно просто написать свой backconnect socks5 сервер и клиент.
Это подробный пошаговый гайд, как "с нуля" написать backconnect SOCKS5.
Рассмотрим сервер и клиент по отдельности: начнём с пустого файла, постепенно дополним его нужными методами. Покажу логику и смысл каждой ключевой функции. Затем объединим всё в работающее решение.
Часть 1. Сервер (server_socks5.cpp)
1. Создаём скелет программы
Начинаем с самого простого каркаса main(), который принимает аргументы:
C++: Скопировать в буфер обмена
Разбор
2. Подключаем заголовки для сетей и определяем платформозависимые вещи
Чтобы всё работало и на Windows, и на Linux/macOS, нужно аккуратно подключить разные заголовки.
Создадим блок:
C++: Скопировать в буфер обмена
Разбор
3. Функции инициализации сетей
Подготовим функции, которые будут удобными для старта/завершения:
C++: Скопировать в буфер обмена
Разбор
4. Создаём слушающий сокет
Нам нужно открыть TCP-сокет, привязать к порту, вызвать listen(...). Сделаем универсальную функцию:
C++: Скопировать в буфер обмена
Разбор
5. XOR-функция
Нужно шифровать/дешифровать любой буфер. С помощью XOR всё просто:
C++: Скопировать в буфер обмена
Разбор
6. Отправка/приём с учётом "досылки" (sendAll, recvAll)
Часто send или recv возвращают меньше байт, чем запрашивали. Сделаем утилиты:
C++: Скопировать в буфер обмена
Разбор
7. sendEnc / recvEnc
Чтобы перед отправкой/приёмом выполнить XOR, сделаем, например, sendEnc (достаточно одной функции для отправки). Для приёма мы можем сразу применять XOR после получения. Ниже пример для отправки:
C++: Скопировать в буфер обмена
Разбор
8. Реализация SOCKS5 (в режиме сервера)
8.1 Выбираем метод аутентификации
Когда к нашему SOCKS5-порту подключается клиент, он сначала отправляет:
1. Версию: 0x05
2. Количество поддерживаемых методов: N
3. Список методов.
Мы должны прочитать это, проверить, есть ли 0x00 (No Auth) или 0x02 (User/Pass). Затем ответить, какой метод выбрали.
C++: Скопировать в буфер обмена
8.2 Аутентификация по user/pass
Если выбрано useUserPass, клиент отправит:
C++: Скопировать в буфер обмена
8.3 Обработка CONNECT
После выбора метода клиент посылает:
C++: Скопировать в буфер обмена
8.4 Ответ CONNECT
Когда мы разобрались, нужно послать "ответ" от SOCKS5:
9. Логика "backconnect" на сервере
На сервере есть глобальные переменные:
C++: Скопировать в буфер обмена
9.1 Обработка SOCKS-подключения
В отдельном потоке (на каждое подключение к SOCKS-порту) делаем:
C++: Скопировать в буфер обмена
Разбор по шагам
10. Обработка control-порта
В отдельном потоке слушаем -c <control_port> и принимаем единственного NAT-клиента:
C++: Скопировать в буфер обмена
11. Запуск потока SOCKS5 и control
В main() прописываем логику:
C++: Скопировать в буфер обмена
Таким образом, сервер готов.
Часть 2. Клиент (client_socks5.cpp)
Теперь клиент, который сидит за NAT. Он сам коннектится к серверу и держит соединение. Когда сервер просит "создать туннель", мы выполняем команду C.
1. Структура main()
C++: Скопировать в буфер обмена
2. Цикл переподключения
Нам нужен вечный цикл:
C++: Скопировать в буфер обмена
3. controlChannelLoop(): отправляем "HELLO", ждём "OK"
C++: Скопировать в буфер обмена
4. Keep-Alive-поток
C++: Скопировать в буфер обмена
5. Обработка команды C: открыть локальное соединение и "сшить" его с ephemeral
C++: Скопировать в буфер обмена
Разбор
6. Итоговый main()
C++: Скопировать в буфер обмена
Итог
Мы прошлись шаг за шагом, как "с нуля" написать backconnect SOCKS5:
Такую схему можно усложнить или облегчить: добавить более надёжную криптографию, управлять списком доступных ресурсов, реализовать UDP и т.д. Но базовая идея уже есть.
Исходный код проекта из статьи опубликован тут https://github.com/keklick1337/backconnect_socks5
В ближайшем будущем (наверное) добавлю версию клиента на чистом WINAPI для сборки с /NODEFAULTLIB
---
Автор: Vladislav Tislenko aka keklick1337 (https://github.com/keklick1337)
Статья специально для xss.is
Это подробный пошаговый гайд, как "с нуля" написать backconnect SOCKS5.
Рассмотрим сервер и клиент по отдельности: начнём с пустого файла, постепенно дополним его нужными методами. Покажу логику и смысл каждой ключевой функции. Затем объединим всё в работающее решение.
Часть 1. Сервер (server_socks5.cpp)
1. Создаём скелет программы
Начинаем с самого простого каркаса main(), который принимает аргументы:
C++: Скопировать в буфер обмена
Код:
#include <iostream>
#include <string>
int main(int argc, char* argv[]) {
// 1) Считать аргументы командной строки (порты, ключ и т.п.)
// 2) Инициализация сетевых функций (Windows: WSAStartup)
// 3) Запуск потоков: один для control-порта, один для SOCKS5
// 4) Ожидать их завершения
return 0;
}
Разбор
- int main(int argc, char* argv[]) - обычная точка входа.
- Мы планируем принимать:
- -c <control_port> - порт для "управляющих" подключений от клиента (что сидит за NAT).
- -S <socks_port> - порт, на котором будем принимать SOCKS5-запросы.
- -x <xor_key> - ключ для XOR (для простенького шифрования).
- -u <user> -p <pass> - логин/пароль (необязательно).
- -d - отладочный режим (вывод детальной информации).
2. Подключаем заголовки для сетей и определяем платформозависимые вещи
Чтобы всё работало и на Windows, и на Linux/macOS, нужно аккуратно подключить разные заголовки.
Создадим блок:
C++: Скопировать в буфер обмена
Код:
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
typedef SOCKET SocketType;
#define CLOSESOCK closesocket
#define SOCKERROR WSAGetLastError()
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netdb.h>
typedef int SocketType;
#define CLOSESOCK close
#define INVALID_SOCKET -1
#define SOCKERROR errno
#endif
Разбор
- #ifdef _WIN32 - компиляция под Windows. Используем WinSock2 (winsock2.h) и т.д.
- Имена типов и функций в Windows отличаются, поэтому вводим алиасы:
- typedef SOCKET SocketType;
- #define CLOSESOCK closesocket
- В Unix-системах всё проще (сокеты - это целые числа).
3. Функции инициализации сетей
Подготовим функции, которые будут удобными для старта/завершения:
C++: Скопировать в буфер обмена
Код:
bool initSockets() {
#ifdef _WIN32
WSADATA wd;
int res = WSAStartup(MAKEWORD(2,2), &wd);
if(res != 0){
std::cerr << "[Server] WSAStartup error=" << res << "\n";
return false;
}
#endif
return true;
}
void cleanupSockets() {
#ifdef _WIN32
WSACleanup();
#endif
}
Разбор
- В Windows надо один раз вызвать WSAStartup(...). В Linux/macOS - ничего не нужно.
4. Создаём слушающий сокет
Нам нужно открыть TCP-сокет, привязать к порту, вызвать listen(...). Сделаем универсальную функцию:
C++: Скопировать в буфер обмена
Код:
SocketType createListeningSocket(uint16_t port){
SocketType sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == INVALID_SOCKET){
std::cerr << "[Server] socket() error=" << SOCKERROR << "\n";
return INVALID_SOCKET;
}
// Разрешим переиспользовать адрес
int opt = 1;
#ifdef _WIN32
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt));
#else
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
#endif
sockaddr_in addr;
std::memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
// bind
if(bind(sock, (sockaddr*)&addr, sizeof(addr)) < 0){
std::cerr << "[Server] bind error=" << SOCKERROR << "\n";
CLOSESOCK(sock);
return INVALID_SOCKET;
}
// listen
if(listen(sock, 10) < 0){
std::cerr << "[Server] listen error=" << SOCKERROR << "\n";
CLOSESOCK(sock);
return INVALID_SOCKET;
}
return sock;
}
- socket(AF_INET, SOCK_STREAM, 0) создаёт TCP-сокет (Stream).
- bind(...) связывает сокет с нужным портом.
- listen(...) переводит сокет в режим "принимать входящие".
5. XOR-функция
Нужно шифровать/дешифровать любой буфер. С помощью XOR всё просто:
C++: Скопировать в буфер обмена
Код:
void xorData(char* data, int len, const std::string &key){
if(key.empty()) return; // если ключ пустой - ничего не делаем
for(int i = 0; i < len; i++){
data[i] ^= key[i % key.size()];
}
}
- data ^= key[i % key.size()]- это и есть "XOR".
- Один и тот же ключ применяем по кругу.
6. Отправка/приём с учётом "досылки" (sendAll, recvAll)
Часто send или recv возвращают меньше байт, чем запрашивали. Сделаем утилиты:
C++: Скопировать в буфер обмена
Код:
bool sendAll(SocketType s, const char* data, int len){
int total = 0;
while(total < len){
int sent = send(s, data + total, len - total, 0);
if(sent <= 0) return false;
total += sent;
}
return true;
}
bool recvAll(SocketType s, char* buf, int len){
int total = 0;
while(total < len){
int r = recv(s, buf + total, len - total, 0);
if(r <= 0) return false;
total += r;
}
return true;
}
Разбор
- sendAll(...) в цикле шлёт, пока все байты не "утолкнёт" в сокет.
- recvAll(...) аналогично ждёт, пока не получит все нужные байты (или вернётся с ошибкой).
7. sendEnc / recvEnc
Чтобы перед отправкой/приёмом выполнить XOR, сделаем, например, sendEnc (достаточно одной функции для отправки). Для приёма мы можем сразу применять XOR после получения. Ниже пример для отправки:
C++: Скопировать в буфер обмена
Код:
bool sendEnc(SocketType s, const char* data, int len, const std::string &key){
if(s == INVALID_SOCKET) return false;
std::vector<char> tmp(data, data + len);
xorData(tmp.data(), len, key); // "зашифровать" tmp
return sendAll(s, tmp.data(), len);
}
Разбор
- Копируем исходный буфер в tmp.
- Делаем xorData(...).
- Шлём зашифрованный результат.
8. Реализация SOCKS5 (в режиме сервера)
8.1 Выбираем метод аутентификации
Когда к нашему SOCKS5-порту подключается клиент, он сначала отправляет:
1. Версию: 0x05
2. Количество поддерживаемых методов: N
3. Список методов.
Мы должны прочитать это, проверить, есть ли 0x00 (No Auth) или 0x02 (User/Pass). Затем ответить, какой метод выбрали.
C++: Скопировать в буфер обмена
Код:
bool socks5Handshake_SelectMethod(SocketType s, bool &useUserPass, const std::string &user, const std::string &pass) {
unsigned char hdr[2];
int r = recv(s, (char*)hdr, 2, 0);
if(r < 2) return false;
if(hdr[0] != 0x05) return false; // версия SOCKS5
int nMethods = hdr[1];
std::vector<unsigned char> methods(nMethods);
r = recv(s, (char*)methods.data(), nMethods, 0);
if(r < nMethods) return false;
// Проверяем, нужно ли нам вообще auth (заданы ли user/pass)
bool needAuth = (!user.empty() || !pass.empty());
if(needAuth) {
// Ищем METHOD_USERPASS (0x02)
bool found = false;
for(unsigned char m: methods){
if(m == 0x02){
found = true;
break;
}
}
if(!found) {
// Отправляем REJECT (0xFF)
unsigned char resp[2] = {0x05, 0xFF};
sendAll(s, (char*)resp, 2);
return false;
}
// Сигнализируем, что выбрали user/pass
unsigned char resp[2] = {0x05, 0x02};
sendAll(s, (char*)resp, 2);
useUserPass = true;
} else {
// Без аутентификации
bool found = false;
for(unsigned char m: methods){
if(m == 0x00){
found = true;
break;
}
}
if(!found) {
unsigned char resp[2] = {0x05, 0xFF};
sendAll(s, (char*)resp, 2);
return false;
}
unsigned char resp[2] = {0x05, 0x00};
sendAll(s, (char*)resp, 2);
useUserPass = false;
}
return true;
}
8.2 Аутентификация по user/pass
Если выбрано useUserPass, клиент отправит:
- Версию subnegotiation (0x01).
- Длину имени пользователя (1 байт).
- Само имя пользователя.
- Длину пароля (1 байт).
- Сам пароль.
C++: Скопировать в буфер обмена
Код:
bool socks5Handshake_UserPass(SocketType s, const std::string &user, const std::string &pass){
unsigned char ver;
if(recv(s, (char*)&ver, 1, 0) < 1) return false;
if(ver != 0x01) return false; // subneg version = 1
unsigned char ulen;
if(recv(s, (char*)&ulen, 1, 0) < 1) return false;
std::vector<char> uname(ulen);
if(!recvAll(s, uname.data(), ulen)) return false;
unsigned char plen;
if(recv(s, (char*)&plen, 1, 0) < 1) return false;
std::vector<char> upass(plen);
if(!recvAll(s, upass.data(), plen)) return false;
std::string su(uname.begin(), uname.end());
std::string sp(upass.begin(), upass.end());
// Сравним
unsigned char status = 0x00;
if(su != user || sp != pass){
status = 0x01; // auth fail
}
unsigned char resp[2] = {0x01, status};
sendAll(s, (char*)resp, 2);
return (status == 0x00);
}
После выбора метода клиент посылает:
- 1 байт: 0x05 (версия)
- 1 байт: 0x01 (команда CONNECT)
- 1 байт: 0x00 (зарезервировано)
- 1 байт: тип адреса: 0x01 (IPv4) или 0x03 (домен)
C++: Скопировать в буфер обмена
Код:
bool socks5ParseConnect(SocketType s, uint32_t &ip, uint16_t &port) {
unsigned char hdr[4];
if(!recvAll(s, (char*)hdr, 4)) return false;
// hdr[0] = 0x05 (версия), hdr[1] = 0x01 (CONNECT), hdr[2]=0x00, hdr[3]=ATYP
if(hdr[0] != 0x05 || hdr[1] != 0x01 || hdr[2] != 0x00) return false;
unsigned char atyp = hdr[3];
if(atyp == 0x01){
// IPv4
unsigned char a4[4];
if(!recvAll(s, (char*)a4, 4)) return false;
// Собираем в 32-битное число (host order)
ip = ( (uint32_t)a4[0] << 24 )
| ( (uint32_t)a4[1] << 16 )
| ( (uint32_t)a4[2] << 8 )
| ( (uint32_t)a4[3] );
unsigned char pbuf[2];
if(!recvAll(s, (char*)pbuf, 2)) return false;
port = ((uint16_t)pbuf[0] << 8) | (uint16_t)pbuf[1];
}
else if(atyp == 0x03){
// Доменное имя
unsigned char dlen;
if(!recvAll(s, (char*)&dlen, 1)) return false;
std::vector<char> dom(dlen+1);
if(!recvAll(s, dom.data(), dlen)) return false;
dom[dlen] = '\0';
unsigned char pbuf[2];
if(!recvAll(s, (char*)pbuf, 2)) return false;
port = ((uint16_t)pbuf[0] << 8) | (uint16_t)pbuf[1];
// Резолвим домен
struct hostent* he = gethostbyname(dom.data());
if(!he) return false;
struct in_addr[b] alist = (struct in_addr[/b])he->h_addr_list;
if(!alist[0]) return false;
ip = ntohl(alist[0]->s_addr);
} else {
return false;
}
return true;
}
8.4 Ответ CONNECT
Когда мы разобрались, нужно послать "ответ" от SOCKS5:
- 0x05 (версия)
- <rep> (код результата)
- 0x00 (зарезервировано)
- 0x01 (тип адреса = IPv4)
- <4 байта IP>
- <2 байта порт>
Код:
void socks5SendConnectReply(SocketType s, unsigned char rep, uint32_t ip=0, uint16_t port=0){
unsigned char buf[10];
buf[0] = 0x05;
buf[1] = rep; // 0x00 = успех, иначе ошибка
buf[2] = 0x00;
buf[3] = 0x01; // IPv4
uint32_t ip_n = htonl(ip);
std::memcpy(buf + 4, &ip_n, 4);
uint16_t p_n = htons(port);
std::memcpy(buf + 8, &p_n, 2);
sendAll(s, (char*)buf, 10);
}
9. Логика "backconnect" на сервере
На сервере есть глобальные переменные:
C++: Скопировать в буфер обмена
Код:
#include <mutex>
std::mutex g_mutex;
SocketType g_natClientSock = INVALID_SOCKET; // сокет с NAT-клиентом
bool g_natClientConnected = false;
std::string g_xorKey; // ключ XOR
9.1 Обработка SOCKS-подключения
В отдельном потоке (на каждое подключение к SOCKS-порту) делаем:
C++: Скопировать в буфер обмена
Код:
void handleSocksClient(SocketType sock,
const std::string &user,
const std::string &pass,
bool debug)
{
bool useUserPass = false;
// 1) Выбор метода (NoAuth или UserPass)
if(!socks5Handshake_SelectMethod(sock, useUserPass, user, pass)){
CLOSESOCK(sock);
return;
}
// 2) Если выбрано user/pass, выполнить subneg
if(useUserPass) {
if(!socks5Handshake_UserPass(sock, user, pass)){
CLOSESOCK(sock);
return;
}
}
// 3) Достаём IP, порт для CONNECT
uint32_t tip = 0;
uint16_t tport = 0;
if(!socks5ParseConnect(sock, tip, tport)){
socks5SendConnectReply(sock, 0x01); // ошибка
CLOSESOCK(sock);
return;
}
// 4) Создаем ephemeral socket (он примет соединение от NAT-клиента)
SocketType epSock = socket(AF_INET, SOCK_STREAM, 0);
if(epSock == INVALID_SOCKET){
socks5SendConnectReply(sock, 0x01);
CLOSESOCK(sock);
return;
}
sockaddr_in ep;
std::memset(&ep, 0, sizeof(ep));
ep.sin_family = AF_INET;
ep.sin_port = 0; // сами выберем порт
ep.sin_addr.s_addr = INADDR_ANY;
if(bind(epSock, (sockaddr*)&ep, sizeof(ep)) < 0){
socks5SendConnectReply(sock, 0x01);
CLOSESOCK(epSock);
CLOSESOCK(sock);
return;
}
if(listen(epSock, 1) < 0){
socks5SendConnectReply(sock, 0x01);
CLOSESOCK(epSock);
CLOSESOCK(sock);
return;
}
sockaddr_in tmp;
socklen_t sz = sizeof(tmp);
getsockname(epSock, (sockaddr*)&tmp, &sz);
uint16_t ephemeralPort = ntohs(tmp.sin_port);
// 5) Отправляем команду 'C' нашему NAT-клиенту
bool okSend = false;
{
std::lock_guard<std::mutex> lock(g_mutex);
if(g_natClientSock != INVALID_SOCKET && g_natClientConnected){
// Формируем пакет: 1 байт 'C' + 2 байта ephemeralPort + 4 байта IP + 2 байта targetPort
char cmd[1 + 2 + 4 + 2];
cmd[0] = 'C';
uint16_t ep_n = htons(ephemeralPort);
std::memcpy(cmd+1, &ep_n, 2);
uint32_t tip_n = htonl(tip);
std::memcpy(cmd+3, &tip_n, 4);
uint16_t tpt_n = htons(tport);
std::memcpy(cmd+7, &tpt_n, 2);
okSend = sendEnc(g_natClientSock, cmd, sizeof(cmd), g_xorKey);
}
}
if(!okSend){
socks5SendConnectReply(sock, 0x05);
CLOSESOCK(epSock);
CLOSESOCK(sock);
return;
}
// 6) Ждем подключение на epSock (нат-клиент туда зайдет)
sockaddr_in from;
socklen_t flen = sizeof(from);
SocketType esock = accept(epSock, (sockaddr*)&from, &flen);
CLOSESOCK(epSock); // уже не нужен
if(esock == INVALID_SOCKET){
socks5SendConnectReply(sock, 0x05);
CLOSESOCK(sock);
return;
}
// 7) Сообщаем SOCKS5-клиенту, что всё ок
socks5SendConnectReply(sock, 0x00, tip, tport);
// 8) Пересылаем данные в обоих направлениях
std::thread tFwd([=](){
char buf[4096];
while(true){
int rx = recv(sock, buf, 4096, 0);
if(rx <= 0) break;
int tx = send(esock, buf, rx, 0);
if(tx <= 0) break;
}
CLOSESOCK(esock);
});
{
char buf[4096];
while(true){
int rx = recv(esock, buf, 4096, 0);
if(rx <= 0) break;
int tx = send(sock, buf, rx, 0);
if(tx <= 0) break;
}
}
CLOSESOCK(esock);
tFwd.join();
CLOSESOCK(sock);
}
Разбор по шагам
- Считываем SOCKS5-запрос.
- Создаём временный порт (epSock).
- Шлём через NAT-клиенту команду C.
- Ждём, когда NAT-клиент "зайдёт" в этот epSock.
- После удачного соединения отвечаем SOCKS-клиенту 0x00 (OK).
- Гоним трафик туда-сюда.
10. Обработка control-порта
В отдельном потоке слушаем -c <control_port> и принимаем единственного NAT-клиента:
C++: Скопировать в буфер обмена
Код:
#include <chrono>
void controlAcceptLoop(uint16_t cPort, bool debug){
SocketType listener = createListeningSocket(cPort);
if(listener == INVALID_SOCKET){
std::cerr << "[Server] Failed to listen on controlPort=" << cPort << "\n";
return;
}
std::cout << "[Server] Waiting for NAT client on port " << cPort << "...\n";
while(true){
sockaddr_in caddr;
socklen_t clen = sizeof(caddr);
SocketType cs = accept(listener, (sockaddr*)&caddr, &clen);
if(cs == INVALID_SOCKET){
std::cerr << "[Server] accept() error on control channel\n";
break;
}
// Проверим, не занят ли уже кто-то
{
std::lock_guard<std::mutex> lock(g_mutex);
if(g_natClientConnected){
// Уже подключен NAT-клиент
const char msg[] = "OCCUP";
// зашифруем
sendEnc(cs, msg, 5, g_xorKey);
CLOSESOCK(cs);
continue;
}
}
// Считываем 5 байт "HELLO" (XOR)
char buf[5];
bool okHello = false;
// Хотим таймаут, например 5 секунд
#ifdef _WIN32
DWORD tmo = 5000;
setsockopt(cs, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tmo, sizeof(tmo));
#else
struct timeval tv;
tv.tv_sec = 5;
tv.tv_usec = 0;
setsockopt(cs, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv));
#endif
if(recvAll(cs, buf, 5)) {
// XOR
xorData(buf, 5, g_xorKey);
if(std::string(buf, 5) == "HELLO"){
okHello = true;
}
}
if(!okHello){
CLOSESOCK(cs);
continue;
}
// Отправим "OK"
const char msgOk[] = "OK";
sendEnc(cs, msgOk, 2, g_xorKey);
// Считаем, что клиент подключился
{
std::lock_guard<std::mutex> lock(g_mutex);
g_natClientSock = cs;
g_natClientConnected = true;
}
std::cout << "[Server] NAT client connected!\n";
// Читаем keep-alive ('K') или EOF
while(true){
char c;
int r = recv(cs, &c, 1, 0);
if(r <= 0){
// отключился
CLOSESOCK(cs);
std::lock_guard<std::mutex> lock(g_mutex);
g_natClientSock = INVALID_SOCKET;
g_natClientConnected = false;
break;
}
c ^= g_xorKey[0];
if(c == 'K'){
// keep-alive
} else {
// неизвестно, игнорируем
}
}
}
CLOSESOCK(listener);
}
11. Запуск потока SOCKS5 и control
В main() прописываем логику:
C++: Скопировать в буфер обмена
Код:
#include <thread>
#include <cstdlib>
int main(int argc, char* argv[]){
// 1) Парсим аргументы
uint16_t controlPort = 0;
uint16_t socksPort = 0;
std::string user, pass;
bool debug = false;
// ... (парсим -c, -S, -x, -u, -p, -d и т.д.)
if(!initSockets()){
return 1;
}
// Стартуем поток, слушающий контрольный порт
std::thread tCtl(controlAcceptLoop, controlPort, debug);
// Запустим SOCKS5-listener
SocketType socksListener = createListeningSocket(socksPort);
if(socksListener == INVALID_SOCKET){
std::cerr << "[Server] Unable to listen on " << socksPort << "\n";
return 1;
}
while(true){
sockaddr_in saddr;
socklen_t slen = sizeof(saddr);
SocketType c = accept(socksListener, (sockaddr*)&saddr, &slen);
if(c == INVALID_SOCKET){
// ошибка или завершение
break;
}
// Запустим поток handleSocksClient
std::thread th(handleSocksClient, c, user, pass, debug);
th.detach();
}
CLOSESOCK(socksListener);
tCtl.join();
cleanupSockets();
return 0;
}
Часть 2. Клиент (client_socks5.cpp)
Теперь клиент, который сидит за NAT. Он сам коннектится к серверу и держит соединение. Когда сервер просит "создать туннель", мы выполняем команду C.
1. Структура main()
C++: Скопировать в буфер обмена
Код:
int main(int argc, char* argv[]){
// 1) Парсим -s <server_ip>, -c <control_port>, -x <xor_key>, -d (debug).
// 2) initSockets()
// 3) Запускаем цикл connect -> если ок, controlChannelLoop()
// 4) Если отвалилось, повторяем
return 0;
}
2. Цикл переподключения
Нам нужен вечный цикл:
C++: Скопировать в буфер обмена
Код:
void runClientLoop(const std::string &serverIP,
uint16_t controlPort,
const std::string &xorKey,
bool debug)
{
while(true){
// создаём сокет
SocketType s = socket(AF_INET, SOCK_STREAM, 0);
if(s == INVALID_SOCKET){
if(debug) std::cerr << "socket() error\n";
#ifdef _WIN32
Sleep(5000);
#else
sleep(5);
#endif
continue;
}
// Подключаемся к serverIP:controlPort
sockaddr_in addr;
std::memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(controlPort);
inet_pton(AF_INET, serverIP.c_str(), &addr.sin_addr);
if(connect(s, (sockaddr*)&addr, sizeof(addr)) < 0){
if(debug) std::cerr << "Can't connect to server\n";
CLOSESOCK(s);
#ifdef _WIN32
Sleep(5000);
#else
sleep(5);
#endif
continue;
}
std::cout << "[Client] Connected to " << serverIP << ":" << controlPort << "\n";
// Переходим в функцию, которая ведёт обмен по control-каналу
controlChannelLoop(s, xorKey, debug);
// Если вышли - значит соединение отвалилось
CLOSESOCK(s);
std::cerr << "[Client] Control connection closed. Retry in 5s...\n";
#ifdef _WIN32
Sleep(5000);
#else
sleep(5);
#endif
}
}
3. controlChannelLoop(): отправляем "HELLO", ждём "OK"
C++: Скопировать в буфер обмена
Код:
void controlChannelLoop(SocketType ctrlSock,
const std::string &xorKey,
bool debug)
{
// 1) Шлём "HELLO"
{
char hello[5] = {'H','E','L','L','O'};
if(!sendEnc(ctrlSock, hello, 5, xorKey)){
if(debug) std::cerr << "[Client] fail to send HELLO\n";
return;
}
}
// 2) Ждём ответ (5 байт, может быть "OK" или "OCCUP")
char resp[5];
int r = recv(ctrlSock, resp, 5, 0);
if(r <= 0){
if(debug) std::cerr << "[Client] no response\n";
return;
}
// XOR
for(int i=0; i<r; i++){
resp[i] ^= xorKey[i % xorKey.size()];
}
std::string sresp(resp, r);
if(sresp == "OCCUP"){
std::cerr << "[Client] Server is busy.\n";
return;
} else if(sresp != "OK"){
if(debug) std::cerr << "[Client] unknown handshake response\n";
return;
}
std::cout << "[Client] XOR-handshake succeeded (OK)\n";
// 3) Запускаем keepAlive
std::thread ka(keepAliveThread, ctrlSock, xorKey, debug);
// 4) Читаем команды: 'C' ...
while(true){
char cmd;
int rc = recv(ctrlSock, &cmd, 1, 0);
if(rc <= 0){
// разрыв
break;
}
// XOR
cmd ^= xorKey[0];
if(cmd == 'C'){
// Читаем 8 байт:
// ephemeralPort (2 байта), targetIP (4 байта), targetPort (2 байта)
char buf[8];
if(!recvAll(ctrlSock, buf, 8)) break;
for(int i=0; i<8; i++){
buf[i] ^= xorKey[(1 + i) % xorKey.size()];
}
// Разбираем
uint16_t ep_n;
std::memcpy(&ep_n, buf, 2);
uint16_t ephemeralPort = ntohs(ep_n);
uint32_t tip_n;
std::memcpy(&tip_n, buf+2, 4);
uint16_t tpt_n;
std::memcpy(&tpt_n, buf+6, 2);
uint16_t targetPort = ntohs(tpt_n);
// Запустим отдельный поток, который сделает connectLocal() и connectServerEphemeral()
std::thread th(handleCommandC, ephemeralPort, tip_n, targetPort, xorKey, debug);
th.detach();
} else {
// неизвестно
}
}
ka.join();
}
4. Keep-Alive-поток
C++: Скопировать в буфер обмена
Код:
void keepAliveThread(SocketType ctrlSock,
const std::string &xorKey,
bool debug)
{
while(true){
#ifdef _WIN32
Sleep(15000);
#else
sleep(15);
#endif
char c = 'K';
c ^= xorKey[0];
int rc = send(ctrlSock, &c, 1, 0);
if(rc <= 0){
if(debug) std::cerr << "[Client] keepAlive send fail\n";
break;
}
}
}
5. Обработка команды C: открыть локальное соединение и "сшить" его с ephemeral
C++: Скопировать в буфер обмена
Код:
SocketType connectLocal(uint32_t ip_n, uint16_t port_h){
// ip_n - в сетевом порядке
// port_h - в host-порядке
SocketType s = socket(AF_INET, SOCK_STREAM, 0);
if(s == INVALID_SOCKET) return INVALID_SOCKET;
sockaddr_in addr;
std::memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = ip_n; // уже network order
addr.sin_port = htons(port_h);
if(connect(s, (sockaddr*)&addr, sizeof(addr)) < 0){
CLOSESOCK(s);
return INVALID_SOCKET;
}
return s;
}
SocketType connectServerEphemeral(const std::string &serverIP,
uint16_t ephemeralPort)
{
SocketType s = socket(AF_INET, SOCK_STREAM, 0);
if(s == INVALID_SOCKET) return INVALID_SOCKET;
sockaddr_in addr;
std::memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(ephemeralPort);
inet_pton(AF_INET, serverIP.c_str(), &addr.sin_addr);
if(connect(s, (sockaddr*)&addr, sizeof(addr)) < 0){
CLOSESOCK(s);
return INVALID_SOCKET;
}
return s;
}
void handleCommandC(uint16_t ephemeralPort,
uint32_t targetIP_n,
uint16_t targetPort_h,
const std::string &xorKey,
bool debug)
{
// Шаг 1: подключиться к ephemeral-порту на сервере
SocketType esock = connectServerEphemeral(g_serverIP, ephemeralPort);
if(esock == INVALID_SOCKET){
if(debug) std::cerr << "[Client] can't connect ephemeral\n";
return;
}
// Шаг 2: локальное соединение
SocketType localSock = connectLocal(targetIP_n, targetPort_h);
if(localSock == INVALID_SOCKET){
if(debug) std::cerr << "[Client] can't connect local\n";
CLOSESOCK(esock);
return;
}
// Шаг 3: пересылка
std::thread tFwd([=](){
char b[4096];
while(true){
int rx = recv(esock, b, 4096, 0);
if(rx <= 0) break;
int tx = send(localSock, b, rx, 0);
if(tx <= 0) break;
}
CLOSESOCK(localSock);
});
{
char b[4096];
while(true){
int rx = recv(localSock, b, 4096, 0);
if(rx <= 0) break;
int tx = send(esock, b, rx, 0);
if(tx <= 0) break;
}
}
CLOSESOCK(esock);
tFwd.join();
}
Разбор
- connectLocal(...) подключается внутри домашней сети (куда попросил сервер).
- connectServerEphemeral(...) идёт на сервер, который слушает ephemeralPort.
- "Сшиваем" оба сокета, гоняя байты в обоих направлениях.
6. Итоговый main()
C++: Скопировать в буфер обмена
Код:
int main(int argc, char* argv[]){
// 1) Парсим (примерно):
// -s <server_ip>, -c <control_port>, -x <xor_key>, -d
// 2) initSockets()
// 3) runClientLoop(serverIP, controlPort, xorKey, debug)
// 4) cleanupSockets()
return 0;
}
Итог
Мы прошлись шаг за шагом, как "с нуля" написать backconnect SOCKS5:
- Сервер:
- Слушает control-порт.
- Слушает SOCKS5-порт, обрабатывает CONNECT-запросы.
- Когда приходит CONNECT, создаём ephemeral сокет, шлём команду C клиенту.
- Клиент возвращается на этот ephemeral, и мы пересылаем данные.
- Клиент:
- Постоянно переподключается к серверу (control-порт).
- При подключении шлёт "HELLO", ждёт "OK".
- Слушает команды C: открывает локальное соединение и цепляется к ephemeral порту на сервере.
- Гонит байты туда-сюда.
- XOR:
- Зашифровывает простым XOR все служебные данные ("HELLO", "OK", "C" и т.д.).
Такую схему можно усложнить или облегчить: добавить более надёжную криптографию, управлять списком доступных ресурсов, реализовать UDP и т.д. Но базовая идея уже есть.
Исходный код проекта из статьи опубликован тут https://github.com/keklick1337/backconnect_socks5
В ближайшем будущем (наверное) добавлю версию клиента на чистом WINAPI для сборки с /NODEFAULTLIB
---
Автор: Vladislav Tislenko aka keklick1337 (https://github.com/keklick1337)
Статья специально для xss.is