Пишем backconnect socks5 на C++ с нуля (Win/Linux/MacOS)

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Дорогие пользователи xss.is, пишу данную статью для вас, чтобы ввести вас немного в курс дела, как можно достаточно просто написать свой backconnect socks5 сервер и клиент.
Это подробный пошаговый гайд, как "с нуля" написать 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(...).
  • Шлём зашифрованный результат.
Для приёма можно либо аналогично сделать recvEnc, либо вручную читать recvAll(...) и потом прогонять через 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 байт).
  • Сам пароль.
Мы проверим, совпадает ли с нашим user/pass.

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);
}
8.3 Обработка CONNECT
После выбора метода клиент посылает:
  • 1 байт: 0x05 (версия)
  • 1 байт: 0x01 (команда CONNECT)
  • 1 байт: 0x00 (зарезервировано)
  • 1 байт: тип адреса: 0x01 (IPv4) или 0x03 (домен)
Далее, если тип 0x01, идут 4 байта IPv4-адреса, потом 2 байта порта. Если 0x03, то 1 байт длины домена, далее сам домен, далее 2 байта порта.

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 байта порт>
C++: Скопировать в буфер обмена
Код:
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);
}

Разбор по шагам
  1. Считываем SOCKS5-запрос.
  2. Создаём временный порт (epSock).
  3. Шлём через NAT-клиенту команду C.
  4. Ждём, когда NAT-клиент "зайдёт" в этот epSock.
  5. После удачного соединения отвечаем SOCKS-клиенту 0x00 (OK).
  6. Гоним трафик туда-сюда.

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:
  1. Сервер:
    1. Слушает control-порт.
    2. Слушает SOCKS5-порт, обрабатывает CONNECT-запросы.
    3. Когда приходит CONNECT, создаём ephemeral сокет, шлём команду C клиенту.
    4. Клиент возвращается на этот ephemeral, и мы пересылаем данные.
  2. Клиент:
    1. Постоянно переподключается к серверу (control-порт).
    2. При подключении шлёт "HELLO", ждёт "OK".
    3. Слушает команды C: открывает локальное соединение и цепляется к ephemeral порту на сервере.
    4. Гонит байты туда-сюда.
  3. XOR:
    1. Зашифровывает простым XOR все служебные данные ("HELLO", "OK", "C" и т.д.).

Такую схему можно усложнить или облегчить: добавить более надёжную криптографию, управлять списком доступных ресурсов, реализовать UDP и т.д. Но базовая идея уже есть.

Исходный код проекта из статьи опубликован тут https://github.com/keklick1337/backconnect_socks5

В ближайшем будущем (наверное) добавлю версию клиента на чистом WINAPI для сборки с /NODEFAULTLIB
---

Автор: Vladislav Tislenko aka keklick1337 (https://github.com/keklick1337)
Статья специально для xss.is
 
Сверху Снизу