本文编写于106 天前,最后修改于106 天前,其中某些信息可能已经过时。

需求背景

前几天,我接到了一个这样的需求:大概功能是对某个软件进行dll注入,对软件中的一些函数进行Hook,从而监控特定的数据变化,并制作出HTTP API外部调用使用。
那么,我们来看一下这个需求需要什么东西吧qwq

首先毫无疑问,dll注入并对已知的函数进行Hook,这本身不是什么问题。这个技术非常成熟,无非就是远程线程插入即可。
至于Hook,我们调用VirtualProtect对内存属性进行修改,然后直接写入一个jmp指令跳转到我们的代码就可以了。
这都是非常成熟且被大量应用的技术,难度并不是很高。当然这一部分的代码实现我也会在后面做一个专门的文章记录一下,不过在这里并不是重点,因此暂时先放一下。

那么重点来了,我们怎么实现Web服务器?
这是一个很有意思的事情唔。

首先,常规的Web服务器,比如Apache、Nginx,肯定是没办法塞进一个dll里面的。就算是可以,将这么庞大的一个Web服务器移植到一个dll里,想想就觉得不可能吧……
那么我们需要想办法做一个非常非常简单的HTTP服务器,并不需要支持全部的HTTP协议,能够满足需求即可。
而为了方便插入到dll里,文件最好是精简到核心只有简单的一个文件,最好是只调用系统API和C++ STL,其他的轮子一概不要。
emmmmmm,那看来我们只能自己写一个了呢。


HTTP数据报文格式

首先,要写一个Web服务器,我们必须要弄清楚HTTP报文是什么样子的。
由于我们的API服务将会用GET和POST,以及用于浏览器跨域请求的OPTIONS,所以我们将着重研究并实现这3个HTTP请求。

首先我们来看GET请求:

GET /index.html HTTP/1.1\r\n
Host: www.angelic47.com\r\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36\r\n
Referer: https://www.angelic47.com/\r\n
\r\n

这就是GET请求的HTTP报文。可以看到,格式基本上是如下的样子:

  1. 列表项目
  2. 开头固定为GET、路径、HTTP版本,中间以空格隔开;
  3. 后续每一行为HTTP头信息,每一行是一个键-值对,中间用冒号隔开;
  4. 每行消息都以rn结尾;
  5. 消息的末尾发送一行rn,前面没有任何其他数据,代表数据报文的结束.

那么,我们再来看看POST报文:

POST /index.html HTTP/1.1\r\n
Host: www.angelic47.com\r\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36\r\n
Referer: https://www.angelic47.com/\r\n
Content-Length: 11\r\n
\r\n
a=123&b=456

可以看到,POST报文格式基本是这个样子:

  1. 开头固定为POST、路径、HTTP版本,中间以空格隔开;
  2. 后续内容大部分和GET报文完全一致;
  3. 多了一个Content-Length的HTTP头,用于告知服务器POST的正文内容长度
  4. HTTP头结束后,紧跟着的是POST正文,正文的内容长度刚好是Content-Length的长度
  5. 若无特殊情况,我们要解析的POST正文是用&号分割的键值对字符串

而对于OPTIONS请求,其数据包格式和GET报文格式完全一致,只是开头为OPTIONS,HTTP头的内容有些不同而已。这里不再列出。

那么,有了HTTP头,我们还要知道HTTP服务器返回的报文是什么样子的。
这里列出一个正常的返回信息报文:

HTTP/1.1 200 OK\r\n
Access-Control-Allow-Origin: *\r\n
Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n
Access-Control-Allow-Headers: *\r\n
Access-Control-Max-Age: 86400\r\n
Server: Angelic47Server\r\n
Connection: close\r\n
Content-Type: text/html;charset=utf-8\r\n
Content-Length: 39\r\n
\r\n
<h1>Http Server</h1><p>Hello World!</p>

这里再来列出一个404的返回信息报文:

HTTP/1.1 404 Not Found\r\n
Access-Control-Allow-Origin: *\r\n
Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n
Access-Control-Allow-Headers: *\r\n
Access-Control-Max-Age: 86400\r\n
Server: Angelic47Server\r\n
Connection: close\r\n
Content-Type: text/html;charset=utf-8\r\n
Content-Length: 40\r\n
\r\n
<h1>Http Server</h1><p>404 Not Found</p>

可以看到返回内容大概是这个样子的:

  1. 开头第一行,分别为HTTP版本、HTTP响应码、HTTP响应信息,三项使用空格隔开;
  2. 和请求的报文一样,每行都是一个HTTP头信息,冒号隔开;
  3. 每行都是以rn结尾,最后再次使用rn表示HTTP头信息的结束, 这和HTTP的请求报文是一致的;
  4. HTTP头结尾后紧跟着Content-Length长度的数据,这是HTTP返回内容的正文;

值得注意的是,这里多了一个Content-Type和Access-Control-Allow。
其中,Content-Type是返回的内容数据mime类型,这里是text/html,浏览器会将返回的内容作为html进行解析。当然,也可以是application/json,浏览器会将返回的内容作为json看待;也可以是image/jpg,那么浏览器会将返回的内容当作jpg文件看待并直接显示,以此类推……
而Access-Control-Allow相关字样,这里是为了方便浏览器跨域调用API,以至于不会出现跨域问题。如果不希望本页面被跨域访问,则不需要这个字段。


编写代码

HTTP报文格式看完了,接下来该编写Web服务器了呢。
首先我们需要对来自客户端的HTTP请求进行解析处理,将HTTP数据包解析出来。
值得注意的是,客户端可能会多次发送tcp数据包来发送整个HTTP请求,因此我们可能会遇到单次recv无法接收到全部HTTP数据报文的情况。
因此,解析HTTP数据包的时候,需要考虑到这个问题。
而返回给客户端的返回报文就很简单了,我们甚至可以手工拼凑要返回的HTTP报文,直接发送即可。

这样一来,整个思路就清晰明了了。
下面将给出具体的实现代码(HTTP解析部分是闺蜜大人写的ww)。

// Httpd.cpp

#include "pch.h"
#include "Httpd.h"
#include "LogUtil.h"
#pragma comment(lib,"ws2_32.lib")
#include <Winsock2.h>
#include <string>
#include "HttpService.h"

// Http默认监听端口
int g_HttpListenPort = 8081;

map<string, HttpService> g_HttpServiceMap;

void HttpRecvFromSocket(SOCKET clientSocket, char* buffer, unsigned int bufferLen, unsigned int& recvLen)
{
    recvLen = recv(clientSocket, buffer, bufferLen, 0);
}

void HttpDeleteHttpRequest(HTTPRequest* t) {
    if (t->body != NULL)
        delete[]t->body;
    delete t;
}

void RegisterHttpServieAPI(string name, int(*service)(HttpClient*, HTTPRequest*, HttpUrl*))
{
    g_HttpServiceMap.insert(std::pair<string, HttpService>(name, service));
}

void *GetHttpServiceAPI(string name)
{
    auto result = g_HttpServiceMap.find(name);
    if (result == g_HttpServiceMap.end())
        return NULL;
    return result->second;
}

map<string, string>* HttpKeyValueParser(string keyvalstr) {
    const char* str = keyvalstr.c_str();
    map<string, string>* keyValueMap = new map<string, string>;
    bool isArg = false;
    string key = "";
    string val = "";
    bool isEscape = false;
    short escapeLen = 0;
    char escapeCode = 0;
    string escapeBuffer = "";
    for (int i = 0; i < keyvalstr.length(); i++)
    {
        char chr = str[i];
        if (isEscape)
        {
            if (chr >= 'A' && chr <= 'F')
                escapeCode = (escapeCode << 4) + (chr - 'A' + 10);
            else if (chr >= 'a' && chr <= 'f')
                escapeCode = (escapeCode << 4) + (chr - 'a' + 10);
            else if (chr >= '0' && chr <= '9')
                escapeCode = (escapeCode << 4) + (chr - '0');
            else
            {
                escapeBuffer += chr;
                escapeLen = 0;
                escapeCode = 0;
                if (isArg)
                    val += escapeBuffer;
                else
                    key += escapeBuffer;
                escapeBuffer = "";
                isEscape = false;

            }
            escapeBuffer += chr;
            escapeLen += 1;
            if (escapeLen == 2)
            {
                if (isArg)
                    val += escapeCode;
                else
                    key += escapeCode;
                escapeLen = 0;
                escapeCode = 0;
                escapeBuffer = "";
                isEscape = false;
            }
        }
        else if (chr == '%')
        {
            escapeBuffer += '%';
            isEscape = true;
        }
        else if (chr == '+')
        {
            if (isArg)
                val += ' ';
            else
                key += ' ';
        }
        else if (chr == '=' && isArg == false)
        {
            isArg = true;
        }
        else if (chr == '&' && isArg == true)
        {
            isArg = false;
            keyValueMap->insert(std::pair<string, string>(key, val));
            key = "";
            val = "";
        }
        else
        {
            if (isArg)
                val += chr;
            else
                key += chr;
        }
    }

    if (isEscape)
    {
        if (isArg)
            val += escapeBuffer;
        else
            key += escapeBuffer;
    }

    if(key != "")
        keyValueMap->insert(std::pair<string, string>(key, val));

    return keyValueMap;
}

HttpUrl* HttpUrlParser(string url) {
    const char* str = url.c_str();
    HttpUrl* httpUrl = new HttpUrl;
    bool isArg = false;
    for (int i = 0; i < url.length(); i++)
    {
        if (isArg)
            httpUrl->argstring += str[i];
        else if (str[i] == '?')
            isArg = true;
        else
            httpUrl->path += str[i];
    }
    return httpUrl;
}

HTTPRequest* HttpParser(SOCKET clientSocket) {
    short state = 0;
    string cache = "", mapFirstCache;
    unsigned int bodyPointer = 0;
    char buffer[1024];
    HTTPRequest* ans;
    bool if_r = false;
    bool if_r_n = false;

    ans = new HTTPRequest;
    ans->body = NULL;
    ans->bodyLen = 0;
    ans->method = HTTPMethod::GET;

    while (true) {
        unsigned int bufferLen;
        HttpRecvFromSocket(clientSocket, buffer, 1024, bufferLen);
        if (bufferLen == 0)
            goto parserEnd;
        char temp;
        unsigned int pointer = 0;
        if (cache == "")
            while (buffer[pointer] == ' ')
                pointer++;
        while (pointer != bufferLen) {
            temp = buffer[pointer];

            if (state == 6) {
                ans->body[bodyPointer] = temp;
                bodyPointer++;
                pointer++;
                if (bodyPointer == ans->bodyLen)
                    return ans;
                continue;
            }
            else if (temp == '\r') {
                if_r = true;
                pointer++;
                continue;
            }
            else if (if_r) {
                if (temp != '\n')
                    goto parserEnd;
                if_r = false;
                if_r_n = true;
            }

            if (state == 0) {
                if (temp == ' ') {
                    bool ifCorrectStart = false;
                    HTTPMethod method;
                    for (int i = 0; i < HTTPMethod::MAX; i++)
                        if (cache == g_HttpMethod[i]) {
                            ifCorrectStart = true;
                            method = (HTTPMethod)i;
                            break;
                        }
                    if (!ifCorrectStart)
                        goto parserEnd;
                    ans->method = method;
                    while (buffer[pointer] == ' ')
                        pointer++;
                    cache = "";
                    state++;
                    continue;
                }
                else if (cache.length() == 10)
                    goto parserEnd;
                cache += temp;
                pointer++;
            }
            else if (state == 1) {
                if (if_r_n)
                    goto parserEnd;
                if (temp == ' ') {
                    ans->url = cache;
                    while (buffer[pointer] == ' ')
                        pointer++;
                    cache = "";
                    state++;
                    continue;
                }
                else if (cache.length() == 10240)
                    goto parserEnd;
                cache += temp;
                pointer++;
            }
            else if (state == 2) {
                if (if_r_n) {
                    if_r_n = false;
                    ans->HTTPVersion = cache;
                    cache = "";
                    pointer++;
                    state++;
                    continue;
                }
                if (cache.length() == 10)
                    goto parserEnd;
                cache += temp;
                pointer++;
            }
            else if (state == 3) {
                if (if_r_n) {
                    if_r_n = false;
                    if (cache == "")
                    {
                        if (ans->method == POST)
                            state = 5;
                        else if (ans->method == GET || ans->method == OPTIONS)
                            return ans;
                        else
                            goto parserEnd;
                    }
                    else
                        goto parserEnd;
                }
                if (temp == ':') {
                    mapFirstCache = cache;
                    cache = "";
                    pointer++;
                    while (buffer[pointer] == ' ')
                        pointer++;
                    state++;
                    continue;
                }
                if (cache.length() == 50)
                    goto parserEnd;
                cache += temp;
                pointer++;
            }
            else if (state == 4) {
                if (if_r_n) {
                    if_r_n = false;
                    ans->header.insert(std::pair<string, string>(mapFirstCache, cache));
                    cache = "";
                    pointer++;
                    while (buffer[pointer] == ' ')
                        pointer++;
                    state--;
                    continue;
                }
                if (cache.length() == 500)
                    goto parserEnd;
                cache += temp;
                pointer++;
            }
            else if (state == 5) {
                if (ans->header.find("Content-Length") != ans->header.end()) {
                    ans->bodyLen = atoi(ans->header.find("Content-Length")->second.c_str());
                    if (ans->bodyLen == 0)
                        return ans;
                    else if (ans->bodyLen > 500000000)
                        goto parserEnd;
                    ans->body = new char[ans->bodyLen];
                    if (ans->body == NULL)
                        goto parserEnd;
                    state = 6;
                    cache = " "; // 避免中间断开后下一段开头为‘ ’
                }
                else
                    goto parserEnd;
            }
        }
    }
parserEnd:
    HttpDeleteHttpRequest(ans);
    return NULL;
}

void HttpHandleClient(HttpClient *client)
{
    HTTPRequest *request = HttpParser(client->socket);
    if (request == NULL)
    {
        LOGI("WebAPI", "%s:%d 请求无效或意外断开连接", client->ClientIPAddress, client->ClientPort);
        closesocket(client->socket);
        delete client;
        return;
    }

    HttpUrl* httpUrl = HttpUrlParser(request->url);

    int resultcode = 0;
    int (*httpService)(HttpClient*, HTTPRequest*, HttpUrl*) = (HttpService)GetHttpServiceAPI(httpUrl->path);

    if (httpService == NULL)
    {
        resultcode = 404;
        string sendbyte = "HTTP/1.1 404 Not Found\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: POST, GET, OPTIONS\r\nAccess-Control-Allow-Headers: *\r\nAccess-Control-Max-Age: 86400\r\nServer: Angelic47Server\r\nConnection: close\r\nContent-Type: text/html;charset=utf-8\r\nContent-Length: 40\r\n\r\n<h1>Http Server</h1><p>404 Not Found</p>";
        send(client->socket, sendbyte.c_str(), sendbyte.size(), 0);
    }
    else if (request->method == OPTIONS)
    {
        resultcode = 204;
        string sendbyte = "HTTP/1.1 204 No Content\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: POST, GET, OPTIONS\r\nAccess-Control-Allow-Headers: *\r\nAccess-Control-Max-Age: 86400\r\nServer: Angelic47Server\r\nConnection: close\r\nContent-Length: 0\r\n\r\n";
        send(client->socket, sendbyte.c_str(), sendbyte.size(), 0);
    }
    else
    {
        resultcode = httpService(client, request, httpUrl);
    }

    LOGI("WebAPI", "%s:%d [%s] %s - %d", client->ClientIPAddress, client->ClientPort, (request->method == GET ? "GET" : (request->method == POST ? "POST" : "OPTIONS")), request->url.c_str(), resultcode);

    closesocket(client->socket);

    delete httpUrl;
    HttpDeleteHttpRequest(request);
    delete client;
}

void HttpdSetupServer()
{
    LOGI("WebAPI", "正在启动WebAPI服务器");

    startRegisterHttpAPI();

    WSADATA wsaData;
    SOCKET sListen, sAccept;
    struct sockaddr_in ser, cli; //服务器和客户的地址

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        LOGE("WebAPI", "WSA服务初始化失败, 请检查系统资源是否耗尽");
        return;
    }

    sListen = socket(AF_INET, SOCK_STREAM, 0);
    if (sListen == INVALID_SOCKET)
    {
        LOGE("WebAPI", "socket链接创建失败: 错误 %d, 请检查系统资源是否耗尽", WSAGetLastError());
        return;
    }

    //以下初始化服务器端地址
    ser.sin_family = AF_INET; //使用 IP 地址族
    ser.sin_port = htons(g_HttpListenPort); //主机序端口号转换为网络字节序端口号
    ser.sin_addr.s_addr = htonl(INADDR_ANY); //主机序 IP 地址转换为网络字节序主机地址
    //使用系统指定的 IP 地址 INADDR_ANY
    if (bind(sListen, (LPSOCKADDR)&ser, sizeof(ser)) == SOCKET_ERROR) //套接定与地址的绑定
    {
        LOGE("WebAPI", "无法绑定端口号%d: 错误 %d , 请检查端口是否已被占用", g_HttpListenPort, WSAGetLastError());
        return;
    }
    if (listen(sListen, 5) == SOCKET_ERROR) //进入监听状态
    {
        LOGE("WebAPI", "无法在%d端口上建立Http监听: 错误 %d , 请检查是否有足够的权限", g_HttpListenPort, WSAGetLastError());
        return;
    }

    LOGI("WebAPI", "Http服务启动成功, 监听于端口 %d", g_HttpListenPort);

    int iLen = sizeof(cli); //初始化客户端地址长度参数
    while (1) //进入循环等待客户的连接请求
    {
        sAccept = accept(sListen, (struct sockaddr*) & cli, &iLen);
        if (sAccept == INVALID_SOCKET)
        {
            LOGE("WebAPI", "Http服务监听异常, 错误: %d", WSAGetLastError());
            LOGE("WebAPI", "Http服务已崩溃");
            return;
        }

        HttpClient* client = new HttpClient;
 
        strcpy_s(client->ClientIPAddress, inet_ntoa(cli.sin_addr));
        client->ClientPort = ntohs(cli.sin_port);
        client->socket = sAccept;

        CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)HttpHandleClient, client, 0, NULL);
    }
}
#pragma once
// Httpd.h

#include "pch.h"
#include <iostream>
#include <map>
#include <Winsock2.h>
#include "HttpService.h"

using std::string;
using std::map;

enum HTTPMethod {
    GET = 0,
    POST = 1,
    OPTIONS = 2,
    MAX = 3,
};

struct HTTPRequest {
    HTTPMethod method;
    string url;
    string HTTPVersion;
    map<string, string> header;
    char* body;
    unsigned int bodyLen;
};

struct HttpClient {
    char ClientIPAddress[46];
    int ClientPort;
    SOCKET socket;
};

struct HttpUrl {
    string path;
    string argstring;
};

const string g_HttpMethod[3] = { "GET","POST","OPTIONS" };
extern int g_HttpListenPort;
extern map<string, HttpService> g_HttpServiceMap;

// Http服务初始化,调用这个函数启动Http服务器
void HttpdSetupServer();
// 将客户端请求的URL中的路径和argstring分离,返回一个HTTPUrl结构体,用后需要delete
HttpUrl* HttpUrlParser(string url);
// 用于解析键值对字符串,返回一个map对象,用后需要delete
map<string, string>* HttpKeyValueParser(string keyvalstr);
// 注册Http请求API,用法参考HttpService.h
void RegisterHttpServieAPI(string name, int(*service)(HttpClient*, HTTPRequest*, HttpUrl*));
// 根据路径名获取Http请求API
void* GetHttpServiceAPI(string name);
// HttpService.cpp

#include "pch.h"
#include "Httpd.h"
#include <stdio.h>

int IndexService(HttpClient* client, HTTPRequest* request, HttpUrl* url)
{
    string sendbyte = "HTTP/1.1 200 OK\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: POST, GET, OPTIONS\r\nAccess-Control-Allow-Headers: *\r\nAccess-Control-Max-Age: 86400\r\nServer: Angelic47Server\r\nConnection: close\r\nContent-Type: text/html;charset=utf-8\r\nContent-Length: 39\r\n\r\n<h1>Http Server</h1><p>Hello World!</p>";
    send(client->socket, sendbyte.c_str(), sendbyte.size(), 0);
    return 200;
}

void startRegisterHttpAPI()
{
    RegisterHttpServieAPI("/", IndexService);
}
#pragma once
// HttpService.cpp
// Http服务相关实现
#include "pch.h"
#include "Httpd.h"

#define HttpService int(*)(HttpClient*, HTTPRequest*, HttpUrl*)

// 初始化API注册,在HTTP服务器启动的时候会调用
void startRegisterHttpAPI();

最终效果

最终执行效果如图:


整个HTTP从发起到响应仅用36ms,效率已经非常高了。
中间的36ms是操作系统创建线程所消耗的时间,感兴趣的话可以试试把这段代码优化成线程池,估计性能会更好吧w

不过作为一个简单的API服务器使用已经完全满足需求了呢!