浅谈使用C语言开发服务端漏洞扫描设计

本文记录一下自己学习这么久以来,目前设计的漏洞扫描最好的方式

0x00 前言

都是自己闷头搞出来的经验,对于自己是经验,对大家就不知道合不合口味了。

本文可能过多的穿插以下知识点:

能尽量图示就图示了。顺便总结一下自己网络编程的经验。 :D

0x01 网络套接字(SOCKET)

什么是套接字

在我的理解中,网络套接字是一个被封装在传输层与应用层之间的API接口。

enter description here

每一个方法都被操作系统支持,我们只需要知道创建套接字的流程以及网络基本知识就可以进行套接字的编程了。

许多的远程利用攻击、漏洞验证工具都离不开套接字,没有套接字就没有现在能够进行“进程”与“进程”之间通信的过程实现。

下面引用百科的解释:

TCP用主机的IP地址加上主机上的端口号作为TCP连接的端点,这种端点就叫做套接字(socket)或插口。

套接字用(IP地址:端口号)表示。

它是网络通信过程中端点的抽象表示,包含进行网络通信必需的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。

听起来还是非常模糊、太抽象了!

那么我们来看看它到底是什么?

套接字的地址结构

刚才百科告诉我们,套接字用(IP地址:端口号)表示。

那么在网络编程中如何告诉计算机,什么是端口,什么是端口号呢?

于是出现了被计算机界公认的结构体,这个结构体保存在系统的标准库中:


#include <arpa/inet.h>
struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;			/* Port number.  */
    struct in_addr sin_addr;		/* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
			   __SOCKADDR_COMMON_SIZE -
			   sizeof (in_port_t) -
			   sizeof (struct in_addr)];
  };

看起来还是比较复杂的,但是用起来一点都不复杂。

套接字的类型(3种)

套接字的类型一般在创建套接字描述符的时候用到。

套接字描述符

这里就要引入一个socket函数了,它在C语言的头文件中:


#include <sys/socket.h>
int socket (int __domain, int __type, int __protocol)

该函数用于创建描述符

我们的操作系统中此时此刻有很多网络连接,为了区分他们,我们就给它们编个号,也就相当于我们自己的身份证。有了身份证做其他事情就比较方便了。

网络字节序

不同的CPU有不同的字节序类型,这些字节序是指 整数 在内存中保存的顺序,这个叫做 主机序。

最常见的有两种:

俗称 大端、小端字节序

网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用大端排序方式。

而我们的操作系统,一般是小端排序方式,所以需要进行字节序的转换。

创建套接字的过程

enter description here

到这里,我们就需要动手写代码了!

enter description here


#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main(int argc,char ** argv) {

    int clientSock,serverSock,serverPort;
    serverPort = 8877;

    serverSock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

    struct sockaddr_in serverAddr,clientAddr;

    socklen_t clientSockLen;
    clientSockLen = sizeof(clientAddr);

    serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    serverAddr.sin_port = htons(serverPort);

    serverAddr.sin_family = AF_INET;

    bind(serverSock,(struct sockaddr *)&serverAddr, sizeof(serverAddr));

    listen(serverSock,5);
    printf("[*]Listen Port : %d \n",serverPort);
    while((clientSock = accept(serverSock,(struct sockaddr *)&clientAddr,&clientSockLen))){
        char recvBuff[100];
        printf("[*]Client %s Connected .. \n",inet_ntoa(clientAddr.sin_addr));
        if(recv(clientSock,recvBuff, sizeof(recvBuff)-1,0)>0){
            printf("[*]Recv : %s\n",recvBuff);
        }
        close(clientSock);
    }

    return 0;
}

这只是一个简单的TCP服务端,只能发送一次内容,就会断开连接

0x02 协议选择

由于是任务处理的服务端,我比较倾向于UDP协议,无需繁琐的握手,只需要发送接收一次即可。

这个后面的服务端模型会介绍到为什么选择UDP协议。

TCP与UDP区别总结:

0x03 服务端模型

终于等到本文的重点了!

先来个图:

enter description here

模型解读

首先,主进程用于创建套接字,管理僵尸进程,子进程用于服务监听,接收客户端发送来的数据,它主要用于创建孙进程,客户端派发N个任务就创建N个孙进程。

这个模型很适合UDP协议呀,有木有!

模型代码


bool Server::startServer() {

    signal(SIGCHLD, _signalHandler);
    pid_t pid=fork(); // 创建子进程

    if(pid==0){  //子进程处理开始

        // 创建SOCKET描述符
        _serverSock = socket(AF_INET,SOCK_STREAM,0);

        // 设置超时
        struct linger timeWit;
        timeWit.l_onoff = 0;
        timeWit.l_linger = 2;
        setsockopt(_serverSock,SOL_SOCKET,SO_LINGER,&timeWit, sizeof(timeWit));

        // 地址重用
        int reuse = 1;
        setsockopt(_serverSock,SOL_SOCKET,SO_REUSEADDR,&reuse, sizeof(reuse));

        _serverAddr.sin_family = AF_INET;
        _serverAddr.sin_addr.s_addr = INADDR_ANY;

        // 绑定端口
        _serverAddr.sin_port = htons((uint16_t)_listenPort);

        if(bind(_serverSock,(struct sockaddr *)&_serverAddr, sizeof(_serverAddr))== 0){
            std::cout << "[*]Server bind Success ..." << std::endl;
        }else{
            std::cout << "[!]Server bind Fail ..." << std::endl;
            exit(0);
        }
        _clientSize = sizeof(_clientAddr);

        listen(_serverSock,_clientMaxNum);
        while(1){
            // 接收客户端请求
            if((_clientSock = accept(_serverSock,(struct sockaddr *)&_clientAddr,&_clientSize))!=-1){
                _clientPID = fork();
                if(_clientPID == 0){
                    close(_serverSock); // 关闭服务端Socket
                    recv(_clientSock,_fromClientBUFF,500,0); // 接收数据

						// ........

                    shutdown(_clientSock,SHUT_RDWR);
                    exit(0);
                }
            }
        }
    }else{
        int status;
        std::cout << "[*]Server is running ..." << std::endl;
        waitpid(pid,&status,0);
    }
}

0x04 信号处理

信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称软中断。从它的命名可以看出,它的实质和使用很像中断,所有,信号可以说是进程控制的一部分

这段话写的可能有点晦涩难懂,我就写一个程序给你看看:

enter description here

这段程序运行以后,我们一直按一次CTRL+C,会输出:“Are you sure quit this program ??”

然后过10秒后,程序会输出“Exiting…..”,然后会自动退出……

信号列表

处理僵尸进程

waitpid()会暂时停止目前进程的执行,直到有信号来到或子进程结束。

 
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid,int * status,int options);

0x05 多进程、多线程

由于客户端派发过来的任务需要子进程处理,来创建孙进程。

孙进程处理任务会创建N个线程

进程的创建fork()


#include <unistd.h>
pid_t fork();

实例代码:


#include <stdio.h>
#include <unistd.h>
pid_t childPid = fork();
if(childPid == 0){

printf("child ...");
// .... 子进程处理
}else{
/// 父进程处理
}

为什么两个进程的fpid不同呢,这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:

多线程

Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a。

#include <pthread.h>
int pthread_create(pthread_t  *  thread,

pthread_attr_t * attr,

void * (*start_routine)(void *),

void * arg)

返回值:成功返回0,出错返回-1

0x06 任务派发过程

我的项目在:https://github.com/KoonsTools/PenloopGather

为了锻炼编程能力,我选择了TCP协议,后面优化的时候自己再改成UDP吧

首先,需要自己拟定一个协议,让客户端与服务器端能解析报文。


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
typedef struct shadowPls{
unsigned short int id;
unsigned short int  rq;
unsigned short int  type;
char  authcode[33];
char  target[100];
char  comment[100];
char  jobname[100];
char  username[100];
}Pls;
int main(int argc,char * argv[]) {
char sendBuff[500];

Pls * data = (Pls * )sendBuff;

data->id=htons(12);
data->rq=htons(1);
data->type=htons(1);
strcpy(data->authcode,"6848d756da66e55b42f79c0728e351ad");
strcpy(data->target,argv[1]);
strcpy(data->comment,"wwww");
strcpy(data->jobname,"123");
strcpy(data->username,"admin");
int clientSock = socket(AF_INET,SOCK_STREAM,0);

struct sockaddr_in serverAddr;
serverAddr.sin_port = htons(7788);
serverAddr.sin_addr.s_addr = inet_addr("10.211.55.14");
serverAddr.sin_family = AF_INET;
if(connect(clientSock,(struct sockaddr *)&serverAddr, sizeof(serverAddr))==0){
    send(clientSock,sendBuff, sizeof(sendBuff),0);
    printf("buff : %s\n",sendBuff);
}

    //sleep(13);
    shutdown(clientSock,SHUT_RDWR);
return 0;
}

上面是客户端,主要是封装了一个报文,把扫描任务发给服务器。


typedef struct shadowPls{
unsigned short int id; // ID
unsigned short int  rq; // 请求状态
unsigned short int  type; // 扫描类型,可以是 1=>域名,2=>IP
char  authcode[33]; // KEY
char  target[100]; // 扫描目标
char  comment[100]; // 任务说明
char  jobname[100]; // 任务名称
char  username[100]; // 用户名
}Pls;

服务器根据数据包格式来解析,读取目标、描述、注释,并且在创建子进程之前认证用户是否有权限派发任务。

有了独有的协议,我们可以使用:任何语言来做客户端,大大的方便了我们的工作!

参考