倾旋的博客

倾旋的博客

现阶段在进行有效性验证/攻击模拟相关的安全研究工作,我的博客会记录一些我的学习过程和部分安全技术研究成果。

20 Mar 2018

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

0x00 前言

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

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

  • 套接字
  • 协议选择
  • 服务端模型
  • 信号处理
  • 多进程、多线程
  • 任务派发过程

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

0x01 网络套接字(SOCKET)

什么是套接字

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

enter description here
0x01

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

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

下面引用百科的解释:

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

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

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

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

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

套接字的地址结构

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

#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种)

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

  • SOCK_STREAM 字节流套接字 — TCP
  • SOCK_DGRAM 数据报套接字 — UDP
  • SOCK_SEQPACKET 有序分组套接字 — ALL
  • SOCK_RAW 原始套接字 — ALL

套接字描述符

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

1
2
3

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

该函数用于创建描述符

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

网络字节序

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

最常见的有两种:

  • 1.将低序字节存储在起始地址(小端)
  • 2.将高序字节存储在起始地址(大端)

俗称 大端、小端字节序

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

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

创建套接字的过程

enter description here
0x02

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

enter description here
0x03

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

#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区别总结:

  • 1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
  • 2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保 证可靠交付
  • 3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的 UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
  • 4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
  • 5、TCP首部开销20字节;UDP的首部开销小,只有8个字节
  • 6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道

0x03 服务端模型

终于等到本文的重点了!

先来个图:

enter description here
0x04

模型解读

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

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

模型代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

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
0x05

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

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

信号列表

  • SIGHUP:本信号在用户终端连接(正常或非正常)结束时发出,通常是在终端的控制进程结束的,通知同一session内的各个作用,这是它们与控制终端不再关联
  • SIGINT:程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程
  • SIGQUIT:和SIGINT类似, 但由QUIT字符(通常是Ctrl-/)来控制. 进程在因收到SIGQUIT退出时会产生core文件,在这个意义上类似于一个程序错误信号
  • SIGILL:执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号
  • SIGTRAP:由断点指令或其它trap指令产生. 由debugger使用
  • SIGABRT:调用abort函数生成的信号
  • SIGBUS:非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)
  • SIGFPE:在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。
  • SIGKILL:用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。
  • SIGUSR1:留给用户使用
  • SIGSEGV:试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据
  • SIGUSR2:留给用户使用
  • SIGPIPE:管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止
  • SIGALRM:时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号
  • SIGTERM:程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL
  • SIGCHLD:子进程结束时, 父进程会收到这个信号
  • SIGCONT:让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符
  • SIGSTOP:停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束,只是暂停执行. 本信号不能被阻塞, 处理或忽略
  • SIGTSTP:停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号
  • SIGTTIN:当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行
  • SIGTTOU:类似于SIGTTIN, 但在写终端(或修改终端模式)时收到
  • SIGURG:有"紧急"数据或out-of-band数据到达socket时产生
  • SIGXCPU:超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变
  • SIGXFSZ:当进程企图扩大文件以至于超过文件大小资源限制
  • SIGVTALRM:虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间
  • SIGPROF:类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间
  • SIGWINCH:窗口大小改变时发出
  • SIGIO:文件描述符准备就绪, 可以开始进行输入/输出操作
  • SIGPWR:Powerfailure
  • SIGSYS:非法的系统调用。

处理僵尸进程

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

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

0x05 多进程、多线程

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

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

进程的创建fork()

1
2
3

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

实例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

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

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

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

  • 1)在父进程中,fork返回新创建子进程的进程ID;
  • 2)在子进程中,fork返回0;
  • 3)如果出现错误,fork返回一个负值;

多线程

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

1
2
3
4
5
6
7
8
#include <pthread.h>
int pthread_create(pthread_t  *  thread,

pthread_attr_t * attr,

void * (*start_routine)(void *),

void * arg)
  • thread:返回创建的线程的ID

  • attr:线程属性,调度策略、优先级等都在这里设置,如果为NULL则表示用默认属性

  • start_routine:线程入口函数,可以返回一个void*类型的返回值,该返回值可由pthread_join()捕获

  • arg:传给start_routine的参数, 可以为NULL

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

0x06 任务派发过程

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

#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;
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

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;

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

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

参考