远控大家都知道最重要的就是框架的编写,框架不好,后面加功能会很累人,很多时候,可能会因为添加一个功能点,就要对框架进行修改,修改后可能所有的功能都要测试,耗时耗力
尤其是对于我这种半路出家的人来说,很折磨人。
因为项目原因,代码不能放出来,我会尽可能的说明问题现象,以及我在处理这些问题时的方法。
基本流程:
建立连接 - 数据加密 - 数据发送 - 数据接收 - 数据解密 -发送到模块 - 不同类型的处理
很多项目可能不止要求一种协议tcp,udp,http,https,icmp 等等,不管是哪种协议,无非就是 建立连接,数据发送,数据接收。我之前写的时候,会存在一个比较长的switch ... case ... 结构,用来对不同的协议进行区分,不同的协议调用不同的函数,大概如下:
这样我每次在添加一种新的协议的时候,都需要对这些switch 进行维护。
这里可以对连接本身进行一种抽象,主框架代码中用类似这样的一种结构来进行编程,代码最开运行的时候,对整个结构体进行初始化就可以了。
粘包和分包问题并不是tcp协议的问题,而是程序员使用tcp协议时,需要主动了解的机制问题。tcp 是流式套接字,数据在发送的时候是以字节流的形式进行发送的,字节流是无界的。举个例子,客户端调用了两次send 函数,每次send 一个字节,控制端 调用recv 进行接收,他可能一次接收就接收到了两个字节,这个就是粘包;客户端一次发送了 100 个字节,控制端接收可能需要两次接收,第一次接收20 个字节,第二次接收到了80 个字节,这个就是分包。究其原因,和tcp本身数据发送的逻辑有关。查看send 和 recv 的api 文档,send 的返回值表示发送成功的数据字节数,其实这里的发送成功并不是真正 的发送成功,他只是表示成功写入到了内核中的发送缓冲区数据的字节数,真正的发送要由内核完成,内核在数据发送的时候,又需要考虑流量控制,拥趸控制等,涉及滑动窗口,mss/mtu,nagle算法等,详细的大家可以看其他文档,他们讲的比我专业的多 。之前在处理粘包分包问题的时候都是简单的使用sleep函数,在项目中加这种sleep 是很恶心的一件事。
常见的解决方案,一般是两个:
一个是添加数据包头,指明数据包的大小;
另一个是添加标志字符串,指明数据的结尾或者开头。
也有说可以通过禁用nagle 算法的,windows下也有相关的socket 选项TCP_NODELAY ,但是从我查阅的文档来看,大部分的windows 操作系统是不支持禁用的。
用户层缓冲区的设计,可以阅读陈硕的 linux多线程服务端编程中关于缓冲区的设计。
这里大概说下缓冲区处理粘包问题的基本思路,接收到数据之后,先将数据放到缓冲区中,再解析数据包头的长度字段,如果整个缓冲区的数据的长度小于这个值,则不进行数据处理;如果大于这个值,则进行循环解析。因为可能出现粘包的情况,缓冲区中可能有多个数据包,所以需要一个循环处理。
不要在多线程中对同一个socket 进行读写操作,即使是加锁。正确的处理思路是维护一个发送队列,使用一个发送线程从发送队列中取数据进行发送。
考虑这么一个场景:
在对socket进行监听的时候,经常会单独起一个线程,然后使用select 函数监听可读,可写。但是如果同时我们的心跳线程检测到了目标端的退出,需要对socket 进行关闭操作,同时希望select 线程进行退出。这个时候如果我们在心跳线程中进行close操作,处于阻塞状态的select 函数是不会做出响应的,select线程将不会退出。
这种情况有两种处理方法:
一种是改变socket 为非阻塞式socket,同时设置一个标志位,然后在心跳线程中设置该标志位为 0 。
另一种方法是使用shutdown函数,关闭socket 的可读可写。
shutdown( SHUT_RDWR);
如果想要适配的机器更广,都需要对大小端进行处理。大小端处理的方法一般就两种思路。
一个是使用 ntohs / htons。将所有的网络传输中的数字转换为网络序,也就是大端序,数据到达机器之后,然后根据机器不同,考虑是否需要转换成小端序;
另一种方法就是使用atoi。在数据发送的时候,数据统一转换成字符串,到达机器之后,再是使用atoi 函数,转换成数字。
正向的s5 代理好理解,就是在被控端开启一个s5 的代理,直接连接被控端的s5 代理;s5反向代理,其实就是在控制端开一个端口转发的程序,数据传输复用远控框架的通讯通道,到达被控端之后,再连接本地的s5 代理程序。消息类型大概就是这么几种类型,建立连接,数据传输,关闭连接。这里其实最重要的就是连接的管理问题,或者说是连接的对应关系问题。这里我是在被控端和控制端同时维护了一个map,map的key是唯一的,值就是对应的socket 句柄。当控制端接收到一个新的socket之后,就会生成这样唯一key,将key 和 fd 放入到map 中,同时产生一个建立连接的消息,这个消息将携带刚才生成的key值,发送给被控端,被控端接收到消息之后,将会与本地的s5建立连接,连接建立之后将得到的fd 和 key 放入被控端的map中。通过key 这个中间变量,我们就有了控制端fd 和 被控端fd 的一一对应关系了。这种通过关系表处理关联关系的方式,在操作系统中非常常见。
这里还要说下调试的问题,这种s5 反向的调试真的是非常恶心,多线程,缺乏很好的定位手段,尤其是程序不崩溃,部分结果异常。
这里说下我在调试时 的排查点:
1、两边的连接数量是不是一致。
2、两边的发送数据流量 和 接收数据流量是不是一致,单个线程的去统计。
icmp本身携带的数据量比较小,遇到类似 netstat -ano 这种返回结果的时候,单次肯定不能完全返回,这里就涉及到一个拆包,多次返回的问题。
其实下面这种方法对于短链接的模型,应该都是可以用的。
模型大概就是这样的:
控制端发送命令执行请求,目标端获得结果之后,不立即返回,将数据拆包,放在待返回队列中;
控制端发送数据获取请求,目标端从队列中获取数据,发送给控制端。
涉及一个数据返回的结构体
根据请求包的编号,可以按发送顺序获得返回结果,根据分包总个数和当前数据包位于分包的第几个 可以获得完整的返回结果。
并不是所有的功能模块都适合做成插件,像s5,端口转发这种的做成插件就比较鸡肋,因为他们本身对同步性的要求就比较高,这种的做成静态的功能就挺好的。
像远程shell,文件管理,进程管理这种的在做插件化设计时要考虑的点,说到底,其实就是数据如何与主模块进行传输。
我知道的两种吧:
一种是通过虚函数,主模块留出一些和插件之间进行数据交互的接口,然后插件模块对虚函数进行实现,mysend 和 myrecv之类的;
第二种就是导出函数,主模块寻找模块插件中的特定函数,通过调用这些特定函数,实现与主模块的交互。
进程,线程间数据同步的方法很多,个人觉得在项目里面不需要使用太多的方法,一种有效的手段就足够了
共享内存,文件,socket,管道,事件对象,互斥体
数据处理逻辑和页面展示逻辑分开处理,通过消息进行同步。
linux 下进行端口复用的方法比较多,这里说一个我在使用rawsocket 进行端口复用时遇到的问题。
rawsoket 进行端口复用时,如果复用的端口是一个短链接的或者说存在协议校验的端口,如 22 端口,那么使用rawsocket 进行复用的时候,是会出问题的,具体表现就是发送的流量和接收的流量不一致。
猜想如下,如果发送端存在了分包发送逻辑,内核在第一个分包发送完,第二个包还没发送的时候,接收端的22协议此时进行完了协议校验,进行了close 操作,那么第二个分包将无法发送,且数据包将被发送端丢弃,而send 函数只是将数据写入了内核的缓冲区,返回的是成功,无法得知是哪一段数据被丢弃了。
这里提供一种解决方法,通过 lkm 编程,hook close 函数。close 函数的参数是fd,而我们能够进行判断的只有 client 的端口信息,这里想到了3环下 get_peername 函数。可以在__sys_getpeername 内核源码逻辑的基础上,通过转换 fd,struct socket,struct sock之间的关系,得到fd 对应的端口信息。拦截close 的同时,需要在内核中同时开启数据接收线程,将数据从内核缓冲区中取出来,不然会出现 windows zero size 的报错信息。
void myrecv()
{
switch (协议类型)
{
case tcp:
.......
case udp:
..
}
}
void mysend()
{
switch()
{
case tcp:
...
case udp:
..
}
}
void myrecv()
{
switch (协议类型)
{
case tcp:
.......
case udp:
..
}
}
void mysend()
{
switch()
{
case tcp:
...
case udp:
..
}
}
myconnect
{
socket sockfd;
int
protype;
void (
*
pconnect)(ip,port);
void (
*
pmysend)(void
*
data,
int
len
);
void (
*
pmyrecv)(void
*
data,
int
len
);
}
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2022-8-4 21:46
被Wow~~编辑
,原因: