Linux

如何找到執行bind()但不執行listen()的應用程序/埠?

  • April 21, 2021

當有故障的應用程序bind()使用 TCP 套接字呼叫某個埠P但沒有跟上 時listen(),該埠P不在打開的埠中列出,即netstatssls /proc/net/tcp顯示,但該埠已被佔用,其他應用程序無法使用該埠。是否有合理的方法可以找到此類應用程序和此類埠?

在提供更合適的東西之前,這裡有一個答案,它嘗試以絕對非工業的方式查找bind(2)在 TCP 套接字上使用的程序,但隨後既沒有listen(2)也沒有connect(2),也可以顯示綁定的 TCP 地址是什麼。

需要在大多數發行版getfattr中命名的包中找到,attr加上kernel >= 3.7以過濾掉非 TCP 套接字,以及最小的安裝gdb(例如在 Debian: 上gdb-minimal)。不需要開發環境。應該以root使用者身份執行(否則它只會找到相同使用者的資訊,但這甚至無法跨容器工作)。請參閱最後的注意事項。


成分:

  • 第一個 shell 腳本模擬了一部分會lsof做的事情,但僅適用於這種特定情況。在所有程序中搜尋套接字 FD。對於具有屬性TCP或的套接字TCPv6(可作為文件元屬性system.sockprotoname使用getfattr,如發現的lsof那樣getxattr(2)以至少顯示它是 TCP 套接字的方式使用),檢查是否可以找到(sockfs偽文件系統的)inode在它們各自的網路命名空間tcptcp6 proc文件中,如果沒有,則將 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()函式或某些符號定義不可用)。
    • 由於沒有可用的調試資訊,大多數功能都明確限定了範圍,並且如果不進行更改,這可能無法在所有環境中執行(在核心 5.10.x的amd64架構上、在 Debian Bullseye、Debian 10 和 CentOS 7 使用者空間上進行了測試)。
    • 同樣,與通常 glibc 之外的其他 libc 連結的二進製文件也可能無法按原樣工作。
    • 是侵入性的,可能會使脆弱的(尤其是多執行緒的)應用程序崩潰。檢查未完成(例如:malloc(3)’s 或getsockname(2)’ 失敗)。
  • 最後一個腳本認為sockfs inode 是全域(而不是每個網路命名空間)唯一的,我沒有試圖證明這一點,但使腳本更簡單。

引用自:https://unix.stackexchange.com/questions/645935