探秘 SSL/TLS 服务密码套件检测:原理、实现与核心设计(C/C++代码实现)

在网络通信安全领域,检测启用SSL/TLS协议的服务所支持的密码套件是保障通信安全的关键环节。这类检测工具能帮我们摸清目标服务的加密配置,识别弱密码套件、过时加密协议等安全风险,今天我们就从底层原理、代码实现和设计思路入手,聊聊这项技术到底是怎么回事。

一、核心概念:密码套件与检测的意义

先说说基础概念------密码套件 其实就是SSL/TLS通信中一套"加密组合拳",包含了密钥交换算法(比如RSA、ECDHE)、对称加密算法(比如AES、3DES)、哈希算法(比如SHA256)等。不同的密码套件安全等级天差地别:比如RC4、3DES属于弱密码套件,容易被破解;而AES-GCM搭配ECDHE的套件则是目前的安全首选。

检测服务支持的密码套件,本质是模拟客户端与目标服务建立SSL/TLS连接,通过"试探"不同的密码套件和协议版本,记录服务端的响应------哪些套件能成功握手,哪些被拒绝,从而梳理出服务的加密配置,找出其中的安全漏洞(比如支持SSLv2/SSLv3这类已被淘汰的协议,或启用了Heartbleed漏洞相关的配置)。

二、底层实现原理:从代码看检测流程

这类检测工具的核心逻辑,本质是"模拟SSL/TLS握手+逐一套件试探",我们结合核心代码来拆解关键步骤:

c 复制代码
...
struct sslCipher
{

    const char *name;
    const char *version;
    int bits;
    char description[512];
    const SSL_METHOD *sslMethod;
    struct sslCipher *next;
};

struct sslCheckOptions
{
    char host[512];
    char sniname[512];
    int sni_set;
    char addrstr[INET6_ADDRSTRLEN];
    int port;
    int showCertificate;
    int showCertificates;
    int checkCertificate;
    int showTrustedCAs;
    int showClientCiphers;
    int showCipherIds;
    int showTimes;
    int ciphersuites;
    int reneg;
    int fallback;
    int compression;
    int heartbleed;
    int groups;
    int signature_algorithms;
    int starttls_ftp;
    int starttls_imap;
    int starttls_irc;
    int starttls_ldap;
    int starttls_pop3;
    int starttls_smtp;
    int starttls_mysql;
    int starttls_xmpp;
    int starttls_psql;
    int xmpp_server;
    int sslVersion;
    int targets;
    int sslbugs;
    int rdp;
    int verbose;
    int cipher_details;
    int ipv4;
    int ipv6;
    int ocspStatus;
    int ianaNames;
    char cipherstring[65536];


    FILE *xmlOutput;


    short h_addrtype;
    struct sockaddr_in serverAddress;
    struct sockaddr_in6 serverAddress6;
    struct timeval timeout;
    int connect_timeout;
    unsigned int sleep;

    SSL_CTX *ctx;
    struct sslCipher *ciphers;
    char *clientCertsFile;
    char *privateKeyFile;
    char *privateKeyPassword;


    unsigned int tls10_supported;
    unsigned int tls11_supported;
    unsigned int tls12_supported;
    unsigned int tls13_supported;
};


struct renegotiationOutput
{
    int supported;
    int secure;
};


struct ocsp_response_st {
    ASN1_ENUMERATED *responseStatus;
    OCSP_RESPBYTES *responseBytes;
};

struct ocsp_resp_bytes_st {
    ASN1_OBJECT *responseType;
    ASN1_OCTET_STRING *response;
};

struct ocsp_responder_id_st {
    int type;
    union {
        X509_NAME *byName;
        ASN1_OCTET_STRING *byKey;
    } value;
};
typedef struct ocsp_responder_id_st OCSP_RESPID;

struct ocsp_response_data_st {
    ASN1_INTEGER *version;
    OCSP_RESPID responderId;
    ASN1_GENERALIZEDTIME *producedAt;
    STACK_OF(OCSP_SINGLERESP) *responses;
    STACK_OF(X509_EXTENSION) *responseExtensions;
};
typedef struct ocsp_response_data_st OCSP_RESPDATA;

struct ocsp_basic_response_st {
    OCSP_RESPDATA tbsResponseData;
    X509_ALGOR signatureAlgorithm;
    ASN1_BIT_STRING *signature;
    STACK_OF(X509) *certs;
};

struct ocsp_single_response_st {
    OCSP_CERTID *certId;
    OCSP_CERTSTATUS *certStatus;
    ASN1_GENERALIZEDTIME *thisUpdate;
    ASN1_GENERALIZEDTIME *nextUpdate;
    STACK_OF(X509_EXTENSION) *singleExtensions;
};

struct ocsp_cert_status_st {
    int type;
    union {
        ASN1_NULL *good;
        OCSP_REVOKEDINFO *revoked;
        ASN1_NULL *unknown;
    } value;
};

struct ocsp_revoked_info_st {
    ASN1_GENERALIZEDTIME *revocationTime;
    ASN1_ENUMERATED *revocationReason;
};

struct ocsp_cert_id_st {
    X509_ALGOR hashAlgorithm;
    ASN1_OCTET_STRING issuerNameHash;
    ASN1_OCTET_STRING issuerKeyHash;
    ASN1_INTEGER serialNumber;
};

#define BS_DEFAULT_NEW_SIZE 256 
struct _bs {
  unsigned char *buf;
  size_t size;  
  size_t len;   
};
typedef struct _bs bs; 


#ifndef SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION
#    define SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION 0x00040000L
#endif
#ifndef SSL3_FLAGS_ALLOW_UNSAFE_LEGACY_RENEGOTIATION
#    define SSL3_FLAGS_ALLOW_UNSAFE_LEGACY_RENEGOTIATION 0x0010
#endif


void bs_new(bs **);
void bs_new_size(bs **, size_t);
void bs_free(bs **);
void bs_append_bytes(bs *, unsigned char *, size_t);
void bs_append_uint32_t(bs *, uint32_t);
void bs_append_ushort(bs *, unsigned short);
void bs_append_bs(bs *, bs *);
size_t bs_get_len(bs *);
size_t bs_get_size(bs *);
unsigned char *bs_get_bytes(bs *);
unsigned char bs_get_byte(bs *, size_t);
void bs_set_byte(bs *, size_t, unsigned char);
void bs_set_ushort(bs *b, size_t offset, unsigned short length);
int bs_read_socket(bs *b, int s, size_t num_bytes);
unsigned int checkIfTLSVersionIsSupported(struct sslCheckOptions *options, unsigned int tls_version);
unsigned int checkIfTLSVersionIsSupported_Backup(struct sslCheckOptions *options, unsigned int tls_version);
SSL_CTX *CTX_new(const SSL_METHOD *method);
int fileExists(char *);
void findMissingCiphers();
char *getPrintableTLSName(unsigned int tls_version);
bs *getServerHello(int s);
bs *makeCiphersuiteListAll(unsigned int tls_version);
bs *makeCiphersuiteListTLS13All();
bs *makeCiphersuiteListMissing(unsigned int tls_version);
bs *makeClientHello(struct sslCheckOptions *options, unsigned int version, bs *ciphersuite_list, bs *tls_extensions);
bs *makeTLSExtensions(struct sslCheckOptions *options, unsigned int include_signature_algorithms);
void markFoundCiphersuite(unsigned short server_cipher_id, unsigned int tls_version);
int ocsp_certid_print(BIO *bp, OCSP_CERTID *a, int indent);
static int ocsp_resp_cb(SSL *s, void *arg);
void readLine(FILE *, char *, int);
int readOrLogAndClose(int, void *, size_t, const struct sslCheckOptions *);
char *resolveCipherID(unsigned short cipher_id, int *cipher_bits);
static int password_callback(char *, int, int, void *);
const char *printableSslMethod(const SSL_METHOD *);
ssize_t sendString(int, const char[]);
int ssl_print_tmp_key(struct sslCheckOptions *, SSL *s);
void tlsExtensionAddDefaultKeyShare(bs *tls_extensions);
void tlsExtensionAddSupportedGroups(unsigned int tls_version, bs *tls_extensions);
void tlsExtensionAddTLSv1_3(bs *tls_extensions);
void tlsExtensionUpdateLength(bs *tls_extensions);
int tcpConnect(struct sslCheckOptions *);


void tls_reneg_init(struct sslCheckOptions *);
int outputRenegotiation(struct sslCheckOptions *, struct renegotiationOutput *);
struct renegotiationOutput *newRenegotiationOutput(void);
int freeRenegotiationOutput(struct renegotiationOutput *);

int testCompression(struct sslCheckOptions *, const SSL_METHOD *);
int testRenegotiation(struct sslCheckOptions *, const SSL_METHOD *);
#ifdef SSL_MODE_SEND_FALLBACK_SCSV
int testfallback(struct sslCheckOptions *, const SSL_METHOD *);
#endif
int testHeartbleed(struct sslCheckOptions *, const SSL_METHOD *);
int testSupportedGroups(struct sslCheckOptions *options);
int testSignatureAlgorithms(struct sslCheckOptions *options);
int testCipher(struct sslCheckOptions *, const SSL_METHOD *);
int testMissingCiphers(struct sslCheckOptions *options, unsigned int version);
int testProtocolCiphers(struct sslCheckOptions *, const SSL_METHOD *);
int testConnection(struct sslCheckOptions *);
int testHost(struct sslCheckOptions *);
int loadCerts(struct sslCheckOptions *);
int checkCertificateProtocols(struct sslCheckOptions *, const SSL_METHOD *);
int checkCertificate(struct sslCheckOptions *, const SSL_METHOD *);
int showCertificate(struct sslCheckOptions *);

int runSSLv2Test(struct sslCheckOptions *options);
int runSSLv3Test(struct sslCheckOptions *options);
#endif

...

int main(int argc, char *argv[])
{
...
    hConsole = GetStdHandle(STD_OUTPUT_HANDLE);

    if (!GetConsoleMode(hConsole, &consoleMode)) {
      hConsole = CreateFile("CONIN$", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);


      setvbuf(stdout, NULL, _IOLBF, 80);

      if (!GetConsoleMode(hConsole, &consoleMode))
	enable_colors = 0;
    }

    if (enable_colors && ((consoleMode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0)) {
      if (!SetConsoleMode(hConsole, consoleMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING))
	enable_colors = 0;
    }

    if (!enable_colors) {
        RESET = "";
        COL_RED = "";
        COL_YELLOW = "";
        COL_BLUE = "";
        COL_GREEN = "";
        COL_PURPLE = "";
        COL_GREY = "";
        COL_RED_BG = "";
    }

    wVersionRequested = MAKEWORD(2, 2);
    err = WSAStartup(wVersionRequested, &wsaData);
    if (err != 0)
    {
        printf_error("WSAStartup failed: %d", err);
        return -1;
    }
#endif

    for (argLoop = 1; argLoop < argc; argLoop++)
    {
        if ((strcmp("--help", argv[argLoop]) == 0) || (strcmp("-h", argv[argLoop]) == 0))
            mode = mode_help;

        else if ((strncmp("--targets=", argv[argLoop], 10) == 0) && (strlen(argv[argLoop]) > 10))
        {
            mode = mode_multiple;
            options->targets = argLoop;
        }

        else if (strcmp("--show-certificate", argv[argLoop]) == 0)
            options->showCertificate = true;

        else if (strcmp("--show-certificates", argv[argLoop]) == 0)
            options->showCertificates = true;

        else if (strcmp("--no-check-certificate", argv[argLoop]) == 0)
            options->checkCertificate = false;

        else if (strcmp("--show-ciphers", argv[argLoop]) == 0)
            options->showClientCiphers = true;

        else if (strcmp("--show-cipher-ids", argv[argLoop]) == 0)
        {
            options->showCipherIds = true;
        }

        else if (strcmp("--show-times", argv[argLoop]) == 0)
        {
            options->showTimes = true;
        }

        else if (strcmp("--show-client-cas", argv[argLoop]) == 0)
            options->showTrustedCAs = true;

        else if (strcmp("--version", argv[argLoop]) == 0)
            mode = mode_version;

        else if (strncmp("--xml=", argv[argLoop], 6) == 0)
            xmlArg = argLoop;

        else if (strcmp("--verbose", argv[argLoop]) == 0)
            options->verbose = true;

        else if (strcmp("--no-cipher-details", argv[argLoop]) == 0)
            options->cipher_details = false;

        else if ((strcmp("--no-colour", argv[argLoop]) == 0) || (strcmp("--no-color", argv[argLoop]) == 0))
        {
            RESET = "";
            COL_RED = "";
            COL_YELLOW = "";
            COL_BLUE = "";
            COL_GREEN = "";
            COL_PURPLE = "";
            COL_RED_BG = "";
            COL_GREY = "";
        }

        // Client Certificates
        else if (strncmp("--certs=", argv[argLoop], 8) == 0)
            options->clientCertsFile = argv[argLoop] +8;

        // Private Key File
        else if (strncmp("--pk=", argv[argLoop], 5) == 0)
            options->privateKeyFile = argv[argLoop] +5;

        // Private Key Password
        else if (strncmp("--pkpass=", argv[argLoop], 9) == 0)
            options->privateKeyPassword = argv[argLoop] +9;

        // Should we check for supported cipher suites
        else if (strcmp("--no-ciphersuites", argv[argLoop]) == 0)
            options->ciphersuites = false;


        else if (strcmp("--no-fallback", argv[argLoop]) == 0)
            options->fallback = false;

        else if (strcmp("--no-renegotiation", argv[argLoop]) == 0)
            options->reneg = false;

        else if (strcmp("--no-compression", argv[argLoop]) == 0)
            options->compression = false;

        else if (strcmp("--no-heartbleed", argv[argLoop]) == 0)
            options->heartbleed = false;

        else if (strcmp("--no-groups", argv[argLoop]) == 0)
            options->groups = false;

        else if (strcmp("--show-sigs", argv[argLoop]) == 0)
            options->signature_algorithms = true;

        else if (strcmp("--iana-names", argv[argLoop]) == 0)
            options->ianaNames = true;

        else if (strcmp("--starttls-ftp", argv[argLoop]) == 0)
            options->starttls_ftp = true;

        else if (strcmp("--starttls-imap", argv[argLoop]) == 0)
            options->starttls_imap = true;

        else if (strcmp("--starttls-irc", argv[argLoop]) == 0)
            options->starttls_irc = true;

        else if (strcmp("--starttls-ldap", argv[argLoop]) == 0)
            options->starttls_ldap = true;

        else if (strcmp("--starttls-pop3", argv[argLoop]) == 0)
            options->starttls_pop3 = true;

        else if (strcmp("--starttls-smtp", argv[argLoop]) == 0)
            options->starttls_smtp = true;

        else if (strcmp("--starttls-mysql", argv[argLoop]) == 0)
            options->starttls_mysql = true;

        else if (strcmp("--starttls-xmpp", argv[argLoop]) == 0)
            options->starttls_xmpp = true;

        else if (strcmp("--starttls-psql", argv[argLoop]) == 0)
            options->starttls_psql = true;

        else if (strcmp("--ssl2", argv[argLoop]) == 0)
            options->sslVersion = ssl_v2;

        else if (strcmp("--ssl3", argv[argLoop]) == 0)
            options->sslVersion = ssl_v3;

        else if (strcmp("--tls10", argv[argLoop]) == 0)
            options->sslVersion = tls_v10;

        else if (strcmp("--tls11", argv[argLoop]) == 0)
            options->sslVersion = tls_v11;

        else if (strcmp("--tls12", argv[argLoop]) == 0)
            options->sslVersion = tls_v12;

        else if (strcmp("--tls13", argv[argLoop]) == 0)
            options->sslVersion = tls_v13;

 
        else if (strcmp("--tlsall", argv[argLoop]) == 0)
            options->sslVersion = tls_all;

        else if (strcmp("--xmpp-server", argv[argLoop]) == 0)
            options->xmpp_server = true;

        else if (strcmp("--bugs", argv[argLoop]) == 0)
            options->sslbugs = 1;

        else if (strncmp("--timeout=", argv[argLoop], 10) == 0)
            options->timeout.tv_sec = atoi(argv[argLoop] + 10);

        else if (strncmp("--connect-timeout=", argv[argLoop], 18) == 0)
            options->connect_timeout = atoi(argv[argLoop] + 18);

        else if (strncmp("--sleep=", argv[argLoop], 8) == 0)
        {
            msec = atoi(argv[argLoop] + 8);
            if (msec >= 0) {
                options->sleep = msec;
            }
        }

        else if (strcmp("--rdp", argv[argLoop]) == 0)
            options->rdp = 1;

        else if ((strcmp("--ipv4", argv[argLoop]) == 0) || (strcmp("-4", argv[argLoop]) == 0))
            options->ipv6 = false;

        else if ((strcmp("--ipv6", argv[argLoop]) == 0) || (strcmp("-6", argv[argLoop]) == 0))
            options->ipv4 = false;

        else if (strcmp("--ocsp", argv[argLoop]) == 0)
            options->ocspStatus = true;

        else if (strncmp("--sni-name=", argv[argLoop], 11) == 0)
        {
            strncpy(options->sniname, argv[argLoop]+11, strlen(argv[argLoop])-11);
            options->sni_set = 1;
        }

        else if (argLoop + 1 == argc)
        {
            mode = mode_single;


            tempInt = 0;
            char *hostString = argv[argLoop];

            maxSize = strlen(hostString);

            if (strncmp((char*)hostString, "https://", 8) == 0)
            {

                memmove(hostString, hostString + 8, (maxSize - 8));
                memset(hostString + (maxSize - 8), 0, 8);
                maxSize = strlen(hostString);
            }

            int squareBrackets = false;
            if (hostString[0] == '[')
            {
                squareBrackets = true;
                hostString++;
            }

            while ((hostString[tempInt] != 0) && ((squareBrackets == true && hostString[tempInt] != ']')
                        || (squareBrackets == false && hostString[tempInt] != ':' && hostString[tempInt] != '/')))
            {
                tempInt++;
            }

            if (squareBrackets == true && hostString[tempInt] == ']')
            {
                hostString[tempInt] = 0;
                if (tempInt < maxSize && (hostString[tempInt + 1] == ':' || hostString[tempInt + 1] == '/'))
                {
                    tempInt++;
                    hostString[tempInt] = 0;
                }
            }
            else
            {
                hostString[tempInt] = 0;
            }
            strncpy(options->host, hostString, sizeof(options->host) -1);


            if (!options->sni_set)
            {
                strncpy(options->sniname, options->host, sizeof(options->host) -1);
            }


            tempInt++;
            if (tempInt < maxSize)
            {
                errno = 0;
                options->port = strtol((hostString + tempInt), NULL, 10);
                if (options->port < 1 || options->port > 65535)
                {
                    printf_error("Invalid target specified.");
                    exit(1);
                }
            }
            else if (options->port == 0) {
                if (options->starttls_ftp)
                    options->port = 21;
                else if (options->starttls_imap)
                    options->port = 143;
                else if (options->starttls_irc)
                    options->port = 6667;
                else if (options->starttls_ldap)
                    options->port = 389;
                else if (options->starttls_pop3)
                    options->port = 110;
                else if (options->starttls_smtp)
                    options->port = 25;
                else if (options->starttls_mysql)
                    options->port = 3306;
                else if (options->starttls_xmpp)
                    options->port = 5222;
                else if (options->starttls_psql)
                    options->port = 5432;
                else if (options->rdp)
                    options->port = 3389;
                else
                    options->port = 443;
            }
        }

        else
            mode = mode_help;
    }


    if ((xmlArg > 0) && (mode != mode_help))
    {
        if (strcmp(argv[xmlArg] + 6, "-") == 0)
        {
            options->xmlOutput = stdout;
            xml_to_stdout = 1;
        }
        else
        {
            options->xmlOutput = fopen(argv[xmlArg] + 6, "w");
            if (options->xmlOutput == NULL)
            {
                printf_error("Could not open XML output file %s.", argv[xmlArg] + 6);
                exit(0);
            }
        }


        fprintf(options->xmlOutput, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document title=\"SSLScan Results\" version=\"%s\" web=\"http://github.com/rbsec/sslscan\">\n", VERSION);
    }


    findMissingCiphers();

    switch (mode)
    {
        case mode_version:
            printf("%s\n%s", VERSION, OpenSSL_version(OPENSSL_VERSION));
            break;

        case mode_help:

            printf("%s%s%s\n", COL_BLUE, program_banner, RESET);
            printf("%s\t\t%s\n\t\t%s\n%s\n\n", COL_BLUE, VERSION,
                    OpenSSL_version(OPENSSL_VERSION), RESET);
            printf("%sCommand:%s\n", COL_BLUE, RESET);
            printf("  %s%s [options] [host:port | host]%s\n\n", COL_GREEN, argv[0], RESET);
            printf("%sOptions:%s\n", COL_BLUE, RESET);
            printf("  %s--targets=<file>%s     A file containing a list of hosts to check.\n", COL_GREEN, RESET);
            printf("                       Hosts can  be supplied  with ports (host:port)\n");
            printf("  %s--sni-name=<name>%s    Hostname for SNI\n", COL_GREEN, RESET);
            printf("  %s--ipv4, -4%s           Only use IPv4\n", COL_GREEN, RESET);
            printf("  %s--ipv6, -6%s           Only use IPv6\n", COL_GREEN, RESET);
            printf("\n");
            printf("  %s--show-certificate%s   Show full certificate information\n", COL_GREEN, RESET);
            printf("  %s--show-certificates%s  Show chain full certificates information\n", COL_GREEN, RESET);
            printf("  %s--show-client-cas%s    Show trusted CAs for TLS client auth\n", COL_GREEN, RESET);
            printf("  %s--no-check-certificate%s  Don't warn about weak certificate algorithm or keys\n", COL_GREEN, RESET);
            printf("  %s--ocsp%s               Request OCSP response from server\n", COL_GREEN, RESET);
            printf("  %s--pk=<file>%s          A file containing the private key or a PKCS#12 file\n", COL_GREEN, RESET);
            printf("                       containing a private key/certificate pair\n");
            printf("  %s--pkpass=<password>%s  The password for the private  key or PKCS#12 file\n", COL_GREEN, RESET);
            printf("  %s--certs=<file>%s       A file containing PEM/ASN1 formatted client certificates\n", COL_GREEN, RESET);
            printf("\n");
            printf("  %s--ssl2%s               Only check if SSLv2 is enabled\n", COL_GREEN, RESET);
            printf("  %s--ssl3%s               Only check if SSLv3 is enabled\n", COL_GREEN, RESET);
            printf("  %s--tls10%s              Only check TLSv1.0 ciphers\n", COL_GREEN, RESET);
            printf("  %s--tls11%s              Only check TLSv1.1 ciphers\n", COL_GREEN, RESET);
            printf("  %s--tls12%s              Only check TLSv1.2 ciphers\n", COL_GREEN, RESET);
            printf("  %s--tls13%s              Only check TLSv1.3 ciphers\n", COL_GREEN, RESET);
            printf("  %s--tlsall%s             Only check TLS ciphers (all versions)\n", COL_GREEN, RESET);
            printf("  %s--show-ciphers%s       Show supported client ciphers\n", COL_GREEN, RESET);
            printf("  %s--show-cipher-ids%s    Show cipher ids\n", COL_GREEN, RESET);
            printf("  %s--iana-names%s         Use IANA/RFC cipher names rather than OpenSSL ones\n", COL_GREEN, RESET);
            printf("  %s--show-times%s         Show handshake times in milliseconds\n", COL_GREEN, RESET);
            printf("\n");
            printf("  %s--no-cipher-details%s  Disable EC curve names and EDH/RSA key lengths output\n", COL_GREEN, RESET);
            printf("  %s--no-ciphersuites%s    Do not check for supported ciphersuites\n", COL_GREEN, RESET);
            printf("  %s--no-compression%s     Do not check for TLS compression (CRIME)\n", COL_GREEN, RESET);
            printf("  %s--no-fallback%s        Do not check for TLS Fallback SCSV\n", COL_GREEN, RESET);
            printf("  %s--no-groups%s          Do not enumerate key exchange groups\n", COL_GREEN, RESET);
            printf("  %s--no-heartbleed%s      Do not check for OpenSSL Heartbleed (CVE-2014-0160)\n", COL_GREEN, RESET);
            printf("  %s--no-renegotiation%s   Do not check for TLS renegotiation\n", COL_GREEN, RESET);
            printf("  %s--show-sigs%s          Enumerate signature algorithms\n", COL_GREEN, RESET);
            printf("\n");
            printf("  %s--starttls-ftp%s       STARTTLS setup for FTP\n", COL_GREEN, RESET);
            printf("  %s--starttls-imap%s      STARTTLS setup for IMAP\n", COL_GREEN, RESET);
            printf("  %s--starttls-irc%s       STARTTLS setup for IRC\n", COL_GREEN, RESET);
            printf("  %s--starttls-ldap%s      STARTTLS setup for LDAP\n", COL_GREEN, RESET);
            printf("  %s--starttls-mysql%s     STARTTLS setup for MYSQL\n", COL_GREEN, RESET);
            printf("  %s--starttls-pop3%s      STARTTLS setup for POP3\n", COL_GREEN, RESET);
            printf("  %s--starttls-psql%s      STARTTLS setup for PostgreSQL\n", COL_GREEN, RESET);
            printf("  %s--starttls-smtp%s      STARTTLS setup for SMTP\n", COL_GREEN, RESET);
            printf("  %s--starttls-xmpp%s      STARTTLS setup for XMPP\n", COL_GREEN, RESET);
            printf("  %s--xmpp-server%s        Use a server-to-server XMPP handshake\n", COL_GREEN, RESET);
            printf("  %s--rdp%s                Send RDP preamble before starting scan\n", COL_GREEN, RESET);
            printf("\n");
            printf("  %s--bugs%s               Enable SSL implementation bug work-arounds\n", COL_GREEN, RESET);
            printf("  %s--no-colour%s          Disable coloured output\n", COL_GREEN, RESET);
            printf("  %s--sleep=<msec>%s       Pause between connection request. Default is disabled\n", COL_GREEN, RESET);
            printf("  %s--timeout=<sec>%s      Set socket timeout. Default is 3s\n", COL_GREEN, RESET);
            printf("  %s--connect-timeout=<sec>%s  Set connect timeout. Default is 75s\n", COL_GREEN, RESET);
            printf("  %s--verbose%s            Display verbose output\n", COL_GREEN, RESET);
            printf("  %s--version%s            Display the program version\n", COL_GREEN, RESET);
            printf("  %s--xml=<file>%s         Output results to an XML file. Use - for STDOUT.\n", COL_GREEN, RESET);
            printf("  %s--help%s               Display the help text you are now reading\n\n", COL_GREEN, RESET);
            printf("%sExample:%s\n", COL_BLUE, RESET);
            printf("  %s%s 127.0.0.1%s\n", COL_GREEN, argv[0], RESET);
            printf("  %s%s [::1]%s\n\n", COL_GREEN, argv[0], RESET);
            break;


        case mode_single:
        case mode_multiple:
            printf("Version: %s%s%s\n%s\n%s\n", COL_GREEN, VERSION, RESET,
                    OpenSSL_version(OPENSSL_VERSION), RESET);

    
            if (mode == mode_single)
            {
                if (testConnection(options))
                {
                    testHost(options);
                }
            }
            else
            {
                if (fileExists(argv[options->targets] + 10) == true)
                {
             
                    targetsFile = fopen(argv[options->targets] + 10, "r");
                    if (targetsFile == NULL)
                    {
                        printf_error("Could not open targets file %s.", argv[options->targets] + 10);
                    }
                    else
                    {
                        readLine(targetsFile, line, sizeof(line));
                        while (feof(targetsFile) == 0)
                        {
                            if (strlen(line) != 0)
                            {
              
                                if (strncmp(line, "https://", 8) == 0)
                                {
                                    memmove(line, line + 8, (strlen(line) - 8));
                                    memset(line + (strlen(line) - 8), 0, 8);
                                }
                    
                                tempInt = 0;
                                while ((line[tempInt] != 0) && (line[tempInt] != ':'))
                                    tempInt++;
                                line[tempInt] = 0;
                                strncpy(options->host, line, sizeof(options->host) -1);

                                if (!options->sni_set)
                                {
                                    strncpy(options->sniname, options->host, sizeof(options->host) -1);
                                }

                         
                                tempInt++;
                                if (strlen(line + tempInt) > 0)
                                {
                                    int port;
                                    port = atoi(line + tempInt);
                               
                                    if (port == 0)
                                    {
                                        printf_error("Invalid port specified.");
                                        exit(1);
                                    }
                                    else
                                    {
                                        options->port = port;
                                    }
                                }
                                // Otherwise assume 443
                                else
                                {
                                    options->port = 443;
                                }

                                // Test the host...
                                if (testConnection(options))
                                {
                                    testHost(options);
                                }
                                printf("\n\n");
                            }
                            readLine(targetsFile, line, sizeof(line));
                        }
                    }
                }
                else
                    printf_error("Targets file %s does not exist.", argv[options->targets] + 10);
            }


            while (options->ciphers != 0)
            {
                sslCipherPointer = options->ciphers->next;
                free(options->ciphers);
                options->ciphers = sslCipherPointer;
            }
            break;
    }


    if ((xmlArg > 0) && (mode != mode_help))
    {
        fprintf(options->xmlOutput, "</document>\n");
        fclose(options->xmlOutput);
    }

    return 0;
}

If you need the complete source code, please add the WeChat number (c17865354792)

1. 核心数据结构与配置初始化

代码里首先定义了一套配置结构体(sslCheckOptions),用来存储检测的核心参数:

  • 目标信息:主机名、端口、SNI(服务器名称指示)、IP协议版本(IPv4/IPv6);
  • 检测开关:是否检查压缩、心跳漏洞(Heartbleed)、重协商、密钥交换组等;
  • 协议范围:指定只检测SSLv2、TLSv1.3等特定版本,还是全版本扫描;
  • 输出配置:是否生成XML报告、是否显示详细信息(证书、密码套件ID)。

初始化时会把这些参数默认值设好(比如默认端口443、超时3秒),再解析命令行参数(比如--tls13指定只扫TLS1.3,--no-heartbleed跳过心跳漏洞检测),覆盖默认配置。

2. 核心检测流程

整个检测过程就像"客户端上门拜访服务器,挨个试不同的'加密暗号'":




解析命令行参数
初始化检测配置
建立TCP连接
生成客户端Hello报文
是否指定单一密码套件?
携带该套件发起握手
携带批量套件发起握手
接收服务器Hello响应
解析响应:记录支持的套件/协议
还有未检测的套件?
输出检测结果

关键步骤拆解:
  • TCP连接建立 :先和目标服务器建立基础的TCP连接,这是SSL/TLS握手的前提,代码里通过tcpConnect函数实现,还会处理超时、IPv4/IPv6选择等问题;
  • 构造Client Hello报文 :这是检测的核心------代码里makeClientHello函数会根据配置,生成包含指定协议版本(比如TLS1.2)、密码套件列表(比如TLS_AES_128_GCM_SHA256)、TLS扩展(比如SNI、密钥交换组)的客户端握手报文;
  • 解析Server Hello响应 :服务器收到客户端报文后,会返回支持的密码套件和协议版本,getServerHello函数读取并解析这个响应,记录下"服务器认哪个暗号";
  • 逐一套件试探 :工具会遍历预设的密码套件列表(包括OpenSSL内置的,还有代码里手动补充的missing_ciphersuites),重复"发Hello报文-读响应"的过程,直到把所有套件试完。

3. 特殊场景处理

代码里还考虑了各种实战场景:

  • STARTTLS支持:比如扫描FTP/IMAP/SMTP等服务时,需要先发送STARTTLS命令切换到加密模式,再进行SSL/TLS检测;
  • 证书检测:除了密码套件,还会检查证书的算法(比如是否用SHA1签名)、密钥长度(比如RSA是否小于2048位)、是否过期等;
  • 漏洞专项检测 :比如Heartbleed漏洞检测(testHeartbleed函数),会发送特制的心跳报文,看服务器是否会泄露内存数据;重协商检测(testRenegotiation)则验证是否存在重协商漏洞。

三、设计思路:为什么这么做?

1. 脱离OpenSSL版本依赖(核心设计亮点)

早期的检测工具很依赖系统安装的OpenSSL版本------比如老版本OpenSSL不支持TLS1.3,工具就没法扫;新版本不支持SSLv2,工具也扫不了。现在的设计思路是:

  • 手动维护TLS1.3密码套件列表(代码里的TLSV13_CIPHERSUITES宏),不依赖OpenSSL的内置列表;
  • 自己构造SSL/TLS握手报文(而不是完全用OpenSSL的API),哪怕系统OpenSSL不支持某个协议版本,也能模拟报文进行检测。

2. 兼顾兼容性与安全性

  • 兼容性 :支持Windows/Linux/macOS,处理了不同系统的差异(比如Windows的控制台颜色、睡眠函数SLEEPMS,Linux的select实现延时);
  • 安全性:检测过程中会主动规避弱密码套件、过时协议的风险提示,比如高亮显示SSLv3(POODLE漏洞)、3DES/RC4等弱套件。

3. 可扩展性设计

代码里把检测逻辑拆分成多个独立函数(比如testCompression检测压缩漏洞、testSupportedGroups检测密钥交换组),新增检测项时只需要加新函数,不用改核心流程,比如想加一个"检测TLS1.3零RTT支持",只需要新增test0RTT函数,在检测流程里调用即可。

四、核心知识点总结(领域视角)

1. 网络安全领域

  • SSL/TLS协议分层:握手层(负责协商密码套件)、记录层(负责数据加密)是检测的核心关注层;
  • 密码套件的安全等级划分:弱套件(RC4、3DES)、安全套件(AES-GCM+ECDHE)、匿名套件(ADH,无身份验证)的识别是检测的核心目标;
  • 常见漏洞:Heartbleed(CVE-2014-0160)、POODLE(SSLv3漏洞)、CRIME(TLS压缩漏洞)都是这类工具重点检测的方向。

2. 编程实现领域

  • 网络编程:手动构造/解析TCP报文(而非仅用封装好的SSL库),需要熟悉SSL/TLS报文格式;
  • 跨平台开发:处理不同系统的API差异(比如Windows的WSAStartup、Linux的close);
  • 内存安全:代码里的FREE/CLOSE宏(释放内存/文件句柄后置NULL),避免使用后释放(use-after-free)漏洞。

3. 工程设计领域

  • 模块化设计:把配置、检测、输出拆分成独立模块,便于维护和扩展;
  • 容错设计:处理超时、连接失败、非法端口等异常情况,避免工具崩溃;
  • 用户体验:支持彩色输出、XML报告、详细/简化输出模式,适配不同使用场景。

五、测试基础扫描示例

1. 扫描单个HTTPS服务(默认443端口)

bash 复制代码
# 扫描目标:github.com(默认检测所有SSL/TLS版本、密码套件、核心漏洞)
./ssl_scan github.com

核心检测内容

  • 自动检测支持的SSLv2/SSLv3/TLSv1.0~1.3协议版本;
  • 列出服务端支持的所有密码套件(高亮弱套件/安全套件);
  • 检查Heartbleed、TLS压缩(CRIME)、重协商等核心漏洞;
  • 简要输出证书信息(默认隐藏详细证书,仅提示弱证书问题如SHA1签名、RSA密钥<2048位)。

2. 扫描指定端口的服务

bash 复制代码
# 扫描目标:192.168.1.100的8443端口(常见的HTTPS管理端口)
./ssl_scan 192.168.1.100:8443

适用场景:目标服务未使用443默认端口(如Tomcat默认8443、Jenkins默认8443等)。

3. 仅使用IPv4/IPv6扫描

bash 复制代码
# 仅IPv4扫描
./ssl_scan --ipv4 google.com
# 仅IPv6扫描(需目标支持IPv6)
./ssl_scan --ipv6 [2404:6800:4008:800::2004]

关键说明 :v2版本原生支持IPv6,--ipv6参数强制仅使用IPv6协议栈,适合检测双栈服务器的IPv6加密配置。

专项协议/密码套件检测示例

1. 仅检测特定TLS版本

bash 复制代码
# 仅检测TLSv1.3
./ssl_scan --tls13 www.baidu.com
# 仅检测TLSv1.0(排查弱协议)
./ssl_scan --tls10 www.example.com
# 仅检测SSLv3(排查POODLE漏洞)
./ssl_scan --ssl3 old-server.example.com

核心价值:v2版本脱离OpenSSL依赖,即使系统OpenSSL不支持SSLv2/SSLv3,也能检测这些遗留协议是否开启。

2. 跳过指定漏洞/项检测(减少扫描耗时)

bash 复制代码
# 扫描时跳过Heartbleed、压缩、重协商检测
./ssl_scan --no-heartbleed --no-compression --no-renegotiation www.taobao.com

适用场景:批量扫描大量服务器时,跳过非核心检测项提升效率。

3. 检测密钥交换组和签名算法(v2新特性)

bash 复制代码
# 显示服务端支持的密钥交换组+签名算法
./ssl_scan --show-sigs --no-cipher-details www.cloudflare.com

输出说明

  • --show-sigs:枚举服务端支持的签名算法(如rsa_pkcs1_sha256、ecdsa_secp256r1_sha256);
  • --no-cipher-details:关闭EC曲线名称、DHE密钥长度的详细输出(简化结果)。

特殊服务(STARTTLS/RDP)扫描示例

1. 扫描启用STARTTLS的邮件服务(SMTP)

bash 复制代码
# 扫描SMTP服务(25端口)的STARTTLS加密配置
./ssl_scan --starttls-smtp smtp.163.com:25
# 扫描POP3服务(110端口)的STARTTLS
./ssl_scan --starttls-pop3 pop.163.com:110

核心逻辑 :先发送STARTTLS命令切换到加密模式,再执行SSL/TLS握手检测(对应代码中starttls_smtp/starttls_pop3配置项)。

2. 扫描数据库服务的加密配置

bash 复制代码
# 扫描MySQL(3306端口)的STARTTLS
./ssl_scan --starttls-mysql 192.168.1.200:3306
# 扫描PostgreSQL(5432端口)的STARTTLS
./ssl_scan --starttls-psql 192.168.1.201:5432

3. 扫描RDP服务(Windows远程桌面)

bash 复制代码
# 扫描Windows服务器RDP的SSL/TLS配置(3389端口)
./ssl_scan  --rdp 192.168.1.10:3389

关键说明:发送RDP前置报文后再执行SSL/TLS检测,适配RDP服务的特殊握手逻辑。

输出定制与高级功能示例

1. 生成XML报告(v2格式变更)

bash 复制代码
# 输出XML报告到文件
./ssl_scan --xml=scan-result.xml www.jd.com
# 输出XML到标准输出(便于管道处理)
./ssl_scan --xml=- www.alipay.com > alipay-ssl.xml

v2 XML变更注意

  • 新增<certificates>父节点,包含<certificate type="short/full">子节点;
  • <signature-algorithm>不再包含冗余前缀(如"Signature Algorithm: "),解析时无需额外处理。

2. 显示详细证书信息+证书链

bash 复制代码
# 显示完整证书链+详细证书信息
./ssl_scan --show-certificates --show-certificate www.gov.cn

输出说明

  • --show-certificate:显示单证书的完整信息(如颁发机构、有效期、公钥长度);
  • --show-certificates:显示整个证书链(根证书、中间证书、服务器证书)。

3. 批量扫描多个目标(从文件读取)

bash 复制代码
# 先创建目标文件 targets.txt,内容示例:
# github.com:443
# 192.168.1.1:8443
# smtp.qq.com:25

# 批量扫描目标文件中的所有服务器
./ssl_scan --targets=targets.txt --sleep=100

参数说明

  • --targets:指定包含目标列表的文件;
  • --sleep=100:每两次请求间隔100ms(避免触发目标服务器的频率限制)。

兼容性/特殊配置示例

1. 启用SNI(多域名服务器)

bash 复制代码
# 扫描共享IP的服务器,指定SNI名称
./ssl_scan --sni-name=blog.example.com 192.168.1.50:443

核心作用 :针对一台服务器托管多个域名的场景,指定SNI名称才能获取对应域名的正确加密配置(对应代码中sniname/sni_set参数)。

2. 静态编译版本扫描(跨系统使用)

bash 复制代码
# 静态编译的ssl_scan (无系统依赖)扫描Windows服务器
./ssl_scan-static --ipv4 --timeout=5 10.0.0.1:443

适用场景:在无OpenSSL依赖的环境(如精简版Linux、容器)中使用。

六、实际应用价值

这类检测工具的核心价值,是帮运维/安全人员:

  1. 合规检查:满足等保、PCI-DSS等合规要求(比如禁用SSLv3、弱密码套件);
  2. 风险排查:提前发现服务端的加密配置漏洞,避免被攻击者利用;
  3. 配置优化:指导管理员关闭弱套件、启用安全套件,提升通信安全性。

简单来说,这项技术就是网络通信安全的"体检仪"------通过模拟真实的加密握手过程,摸清服务端的加密家底,让安全问题无所遁形。而其底层设计的核心,就是"脱离第三方库依赖+精准模拟协议交互+全面覆盖安全检测点",既保证了检测的准确性,又兼顾了工具的兼容性和扩展性。

总结

  1. 密码套件检测的本质是模拟SSL/TLS握手,通过试探不同加密配置获取服务端支持的套件和协议;
  2. 核心设计亮点是脱离OpenSSL版本依赖,手动构造协议报文,兼容新旧协议(SSLv2到TLS1.3);
  3. 实现上采用模块化思路,兼顾跨平台兼容性、漏洞专项检测和用户可配置性,是网络安全检测领域的典型工程实践。

Welcome to follow WeChat official account【程序猿编码

相关推荐
故事和你912 小时前
sdut-程序设计基础Ⅰ-实验二选择结构(1-8)
大数据·开发语言·数据结构·c++·算法·优化·编译原理
江南西肥肥2 小时前
养虾日记[特殊字符]:多Agent在飞书群辩论--踩坑篇
网络·飞书·openclaw
网云工程师手记2 小时前
企业多出口负载与故障切换实战:4 种调度模式 + 主备线路高可用
运维·服务器·网络·安全·网络安全
上海云盾-高防顾问3 小时前
扫段攻击防御指南:简单几步,守住网络安全防线
网络·安全·web安全
min1811234563 小时前
组织结构图导出PDF 高清无水印在线生成
网络·人工智能·架构·pdf·流程图·copilot
像素猎人3 小时前
数据结构之顺序表的插入+删除+查找+修改操作【主函数一步一输出,代码更加清晰直观】
数据结构·c++·算法
蜡笔小马3 小时前
32.Boost.Geometry 空间索引:R-Tree 接口详解
c++·boost·r-tree
木子欢儿3 小时前
Caddy存放ssl/tls证书的位置
网络·网络协议·ssl
桌面运维家3 小时前
Windows网络管控:VHD虚拟磁盘访问限制详解
网络