如何找到执行bind()但不执行listen()的应用程序/端口?

如何找到执行bind()但不执行listen()的应用程序/端口?

当有故障的应用程序bind()使用 TCP 套接字调用某个端口P但没有跟随 时listen(),该端口P不会在开放端口中列出,即netstatssls /proc/net/tcp不显示它,但该端口被占用,并且没有其他应用程序可以使用它。有没有合理的方法来找到这样的应用程序和这样的端口?

答案1

在出现更合适的东西之前,这里有一个答案,它尝试以绝对非工业的方式查找bind(2)在 TCP 套接字上使用的进程,但随后既没有listen(2)也没有connect(2),并且还可以显示绑定的 TCP 地址是什么。

需要getfattrattr在大多数发行版中指定的包中找到加上内核 >= 3.7过滤掉非 TCP 套接字,并最小化安装gdb(例如在 Debian 上:gdb-minimal)。不需要开发环境。应该运行为用户(否则它只会找到相同用户的信息,但这甚至无法跨容器工作)。请参阅最后的注意事项。


成分:

  • 第一个 shell 脚本模仿了部分功能lsof,但仅适用于这种特定情况。在所有进程中搜索套接字 FD。对于具有属性TCP或的套接字TCPv6(可用作文件元属性,system.sockprotoname使用getfattr,正如发现的lsof那样会使用getxattr(2)以这种方式至少显示它是一个 TCP 套接字),检查是否(袜子文件系统伪文件系统的)inode 可以在各自的网络命名空间中找到tcptcp6 进程文件,如果没有,则将 pid、fd 和 inode 显示为候选 3-uple。仅此脚本就可以找到并列出“有缺陷”的进程。

    findbadtcpprocs.sh:

    #!/bin/sh
    
    find /proc -mindepth 1 -maxdepth 1 -name '[1-9]*' |
        xargs -I{} find {}/fd -follow -type s 2>/dev/null |
            while read procfd; do
                type=$(getfattr --absolute --only-values -L -n system.sockprotoname $procfd | tr '\0' '\n')
                if [ "$type" = "TCP" -o "$type" = "TCPv6" ]; then
                    inode=$(stat -L -c %i $procfd)
                    pid=$(echo $procfd | cut -d/ -f3)
                    if awk '$10 == inode { exit 1 }' inode=$inode /proc/$pid/net/tcp /proc/$pid/net/tcp6; then
                        fd=$(echo $procfd | cut -d/ -f5)
                        echo $pid $fd $inode
                    fi
                fi
            done 
    

    该脚本可以独立使用,仅查找候选进程,而无需附加信息。

  • 然后是一个gdb必须赋予正确的脚本FD信息。它附加在候选进程上,并将(首先分配一些内存)运行getsockname(2),显示绑定的套接字(并释放分配的资源)并释放进程。

    getsockname.gdb:

    set $malloc=(void *(*)(long long)) malloc
    set $ntohs=(unsigned short(*)(unsigned short)) ntohs
    p $malloc(64)
    p $malloc(4)
    set *(long *)$2=64
    p (int) getsockname($fd,$1,$2)
    set logging file /dev/stdout
    set logging on
    if *((short *) $1) == 2
        set $ip=(unsigned char *) ($1+4)
        printf "%hu.%hu.%hu.%hu",$ip[0],$ip[1],$ip[2],$ip[3]
    else
        if *((short *) $1) == 10
            set $ip6=(unsigned short *) ($1+8)
            printf "[%hx:%hx:%hx:%hx:%hx:%hx:%hx:%hx]",$ntohs($ip6[0]),$ntohs($ip6[1]),$ntohs($ip6[2]),$ntohs($ip6[3]),$ntohs($ip6[4]),$ntohs($ip6[5]),$ntohs($ip6[6]),$ntohs($ip6[7])
        end
    end
    printf ":%hu\n",$ntohs(*(unsigned short *)($1+2))
    set logging off
    call (void) free($2)
    call (void) free($1)
    quit
    
  • 最后,胶水脚本使用之前的两个脚本以方便操作。它将避免无用地附加到共享同一套接字的多个进程(或线程)。

    result.sh:

    #!/bin/sh
    
    oldinode=-1
    ./findbadtcpprocs.sh | sort -s -n -k 3 | while read pid fd inode; do
        printf '%d\t%d\t%d\t' $pid $fd $inode
        if [ $inode -ne $oldinode ]; then
            socketname=$(gdb -batch-silent -p $pid -ex 'set $fd'=$fd -x ./getsockname.gdb 2>/dev/null) || socketname=FAIL
            oldinode=$inode
        fi
        printf '%s\n' "$socketname"
    done
    

    只需运行此命令即可提供所有内容:

    chmod a+rx findbadtcpprocs.sh result.sh
    ./result.sh
    
  • 作为奖励,C 源代码中的简单重现器将使用相同的 TCP 套接字创建两个进程,而无需使用listen(2)它。用法:gcc -o badtcpbind badtcpbind.c./badtcpbind 5555

    badtcpbind.c:

    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    
    #include <strings.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    int main(int argc, char *argv[])
    {
        int sockfd;
        struct sockaddr_in myaddr;
        if (argc < 2) {
            exit(2);
        }
        if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
            perror("socket");
            exit(1);
        }
        bzero(&myaddr, sizeof myaddr);
        myaddr.sin_family = AF_INET;
        myaddr.sin_addr.s_addr = INADDR_ANY;
        myaddr.sin_port = htons(atoi(argv[1]));
        if (bind(sockfd, (struct sockaddr *) &myaddr, sizeof myaddr) < 0) {
            perror("bind");
            exit(1);
        }
    
    #if 0
         listen(sockfd,5);
    #endif
    
        fork();
        sleep(9999);
    }
    

例子:

# ./badtcpbind 5555 &
[1] 330845
# ./result.sh 
108762  20  303507  0.0.0.0:0
330845  3   586443  0.0.0.0:5555
330846  3   586443  0.0.0.0:5555

(是的,由于某种未知的原因,libvirtd这里出现一个进程来创建一个 TCP 套接字,但该套接字未被使用并被捕获在结果的第一行中)。


注意事项:

  • 也许应该使用比 shell 更好的语言,以提高可读性和效率。

  • 当然比 更有活力lsof

  • 以此处完成的方式附加到正在运行的进程存在问题:

    • 不适用于静态链接的二进制文件(malloc()此时函数或某些符号定义不可用)。
    • 由于没有可用的调试信息,大多数函数都有明确的作用域,并且如果不进行更改,这可能无法在所有环境中运行(在AMD64内核 5.10.x 的架构,在 Debian bullseye、Debian 10 和 CentOS 7 用户空间上)。
    • 同样,与通常的 glibc 相比,与其他 libc 链接的二进制文件可能无法按原样工作。
    • 具有侵入性,可能会导致脆弱的(尤其是多线程)应用程序崩溃。检查未完成(例如:malloc(3)getsockname(2)的失败)。
  • 最后一个脚本考虑袜子文件系统inode 是全局(而不是每个网络命名空间)唯一的,我没有尝试证明这一点,但使脚本更简单。

相关内容