一个简单CPP程序的防护

前几天做了个cpp程序但是考虑安全做了些防护,来看看怎么做的。

#define _WIN32_WINNT 0x0600
#include <windows.h>
#include <wininet.h>
#include <string>
#include <sstream>
#include <iomanip>
#include <ctime>
#include <wincrypt.h>
#include "VMProtectSDK.h"

#pragma comment(lib, "wininet.lib")
#pragma comment(lib, "crypt32.lib")

using namespace std;

string GetSecretKey(){
//这里原来是和服务器通信的token这里我给hide了
    const char* part1 = "";
    const char* part2 = "";
    const char* part3 = "";
    const char* part4 = "";
    return string(part1) + part2 + part3 + part4;
}

void SecureClearString(string& str) {
    if (str.empty()) return;
    VirtualLock((void*)str.c_str(), str.length());
    RtlZeroMemory((void*)str.c_str(), str.length());
    VirtualUnlock((void*)str.c_str(), str.length());
    str.clear();
}

bool IsApiHooked(const char* module, const char* func) {
    HMODULE hMod = GetModuleHandleA(module);
    if (!hMod) return false;
    FARPROC addr = GetProcAddress(hMod, func);
    if (!addr) return false;

    unsigned char* p = (unsigned char*)addr;
    if (*p == 0xCC) return true;
    if (*p == 0xE9) return true;
    if (*p == 0x68 && p[5] == 0xC3) return true;
    return false;
}

bool IsHardwareBreakpointPresent() {
    CONTEXT ctx = { 0 };
    ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
    if (GetThreadContext(GetCurrentThread(), &ctx)) {
        if (ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3) {
            return true;
        }
    }
    return false;
}

bool IsRunningInVM() {
    if (GetModuleHandleA("VBoxGuest")) return true;
    return false;
}

string md5_multipart(const string& part1, const string& part2, const string& part3) {
    HCRYPTPROV hProv = 0;
    HCRYPTHASH hHash = 0;
    BYTE pbHash[16];
    DWORD dwHashLen = 16;
    string result = "";

    if (!CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) {
        return "";
    }
    if (!CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash)) {
        CryptReleaseContext(hProv, 0);
        return "";
    }

    CryptHashData(hHash, (PBYTE)(part2.c_str()), part2.length(), 0);
    CryptHashData(hHash, (PBYTE)(part1.c_str()), part1.length(), 0);
    CryptHashData(hHash, (PBYTE)(part3.c_str()), part3.length(), 0);

    CryptGetHashParam(hHash, HP_HASHVAL, pbHash, &dwHashLen, 0);

    stringstream ss;
    for (int i = 0; i < dwHashLen; i++) {
        ss << hex << setw(2) << setfill('0') << (int)pbHash[i];
    }
    result = ss.str();

    CryptDestroyHash(hHash);
    CryptReleaseContext(hProv, 0);
    return result;
}

string fetchCipher() {
    VMProtectBeginUltra("fetchCipher");

    if (IsRunningInVM()) {
        VMProtectEnd();
        return "ERROR_VM_DETECTED";
    }
    if (IsHardwareBreakpointPresent()) {
        VMProtectEnd();
        return "ERROR_HARDWARE_BP";
    }
    if (IsApiHooked("crypt32.dll", "CryptHashData") ||
        IsApiHooked("wininet.dll", "HttpOpenRequestA") ||
        IsApiHooked("wininet.dll", "InternetConnectA")) {
        VMProtectEnd();
        return "ERROR_HOOK_DETECTED";
    }

    string token = VMProtectDecryptStringA("Sakurazuki291A");
    string secret = GetSecretKey();
    string timestamp = to_string(time(nullptr));

    string sign = md5_multipart(token, timestamp, secret);

    string base_path = VMProtectDecryptStringA("/proxy.php");
    string url = base_path + "?token=" + token + "&t=" + timestamp + "&sign=" + sign;

    HINTERNET hSession = InternetOpenA("User-Agent", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0);
    HINTERNET hConnect = InternetConnectA(hSession, "moongs.asia", INTERNET_DEFAULT_HTTPS_PORT, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
    HINTERNET hRequest = HttpOpenRequestA(hConnect, "GET", url.c_str(), NULL, NULL, NULL, INTERNET_FLAG_SECURE, 0);

    BOOL bSent = HttpSendRequestA(hRequest, NULL, 0, NULL, 0);

    string response;
    if (bSent) {
        char buffer[4096];
        DWORD bytesRead;
        while (InternetReadFile(hRequest, buffer, sizeof(buffer) - 1, &bytesRead) && bytesRead > 0) {
            buffer[bytesRead] = '\0';
            response += buffer;
        }
    }

    InternetCloseHandle(hRequest);
    InternetCloseHandle(hConnect);
    InternetCloseHandle(hSession);

    string key = "\"cipher\":\"";
    size_t start = response.find(key);
    if (start != string::npos) {
        start += key.length();
        size_t end = response.find("\"", start);
        if (end != string::npos) {
            string result = response.substr(start, end - start);

            SecureClearString(token);
            SecureClearString(secret);
            SecureClearString(timestamp);
            SecureClearString(sign);
            SecureClearString(url);

            VMProtectEnd();
            return result;
        }
    }

    SecureClearString(token);
    SecureClearString(secret);
    SecureClearString(timestamp);
    SecureClearString(sign);
    SecureClearString(url);

    VMProtectEnd();
    return "ERROR";
}

HWND resultLabel = NULL;
HWND statusLabel = NULL;

DWORD WINAPI fetchCipherThread(LPVOID lpParam) {
    SetWindowTextA(statusLabel, VMProtectDecryptStringA("Requesting..."));
    string result = fetchCipher();

    if (result == "ERROR" || result == "ERROR_HOOK_DETECTED" ||
        result == "ERROR_HARDWARE_BP" || result == "ERROR_VM_DETECTED") {
        SetWindowTextA(statusLabel, VMProtectDecryptStringA("Failed"));
        SetWindowTextA(resultLabel, "");
    } else {
        SetWindowTextA(statusLabel, VMProtectDecryptStringA("Success"));
        SetWindowTextA(resultLabel, ("Cipher: " + result).c_str());
    }
    return 0;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
        case WM_CREATE: {
            CreateWindowA("STATIC", VMProtectDecryptStringA("Cipher Tool"), WS_VISIBLE | WS_CHILD | SS_CENTER,
                          0, 10, 400, 30, hwnd, NULL, NULL, NULL);
            CreateWindowA("BUTTON", VMProtectDecryptStringA("Get Cipher"), WS_VISIBLE | WS_CHILD,
                          140, 50, 120, 40, hwnd, (HMENU)1, NULL, NULL);
            statusLabel = CreateWindowA("STATIC", VMProtectDecryptStringA("Click to get cipher"), WS_VISIBLE | WS_CHILD | SS_CENTER,
                                        0, 110, 400, 25, hwnd, NULL, NULL, NULL);
            resultLabel = CreateWindowA("STATIC", "", WS_VISIBLE | WS_CHILD | SS_CENTER,
                                        0, 150, 400, 35, hwnd, NULL, NULL, NULL);
            CreateWindowA("STATIC", VMProtectDecryptStringA("(C) Moon STUDIO / Sakurazuki"), WS_VISIBLE | WS_CHILD | SS_CENTER,
                          0, 195, 400, 20, hwnd, NULL, NULL, NULL);
            break;
        }
        case WM_COMMAND: {
            if (LOWORD(wParam) == 1) {
                SetWindowTextA(statusLabel, VMProtectDecryptStringA("Requesting..."));
                SetWindowTextA(resultLabel, "");
                CreateThread(NULL, 0, fetchCipherThread, NULL, 0, NULL);
            }
            break;
        }
        case WM_DESTROY: {
            PostQuitMessage(0);
            return 0;
        }
    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    VMProtectBeginUltra("WinMain");

    WNDCLASSA wc = {};
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = "CipherWindowClass";
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    RegisterClassA(&wc);

    HWND hwnd = CreateWindowExA(
        0,
        "CipherWindowClass",
        VMProtectDecryptStringA("Cipher Getter"),
        WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX,
        CW_USEDEFAULT, CW_USEDEFAULT, 410, 270,
        NULL, NULL, hInstance, NULL
    );

    ShowWindow(hwnd, nCmdShow);

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    VMProtectEnd();
    return 0;
}

程序本身其实并没有什么 所以主要就是来看这上面做的一些安全措施

string timestamp = to_string(time(nullptr));
string sign = md5(timestamp + token + secret);

string url = "/proxy.php?token=" + token + "&t=" + timestamp + "&sign=" + sign;

这里用当前时间戳 + Token + Secret 拼成字符串计算MD5,然后发起请求,从服务器返回的json中提取出所需的片段再在UI里表示出来

好的那么再来看网页端:

if ($token !== '-----') {
    //403
}

这里是对客户端发送的GET进行校验,如果token参数不等于这个值那么就返回403,

if (abs($now - intval($timestamp)) > 0.7) {
    // 403
}

这里是校验时间窗口,为了防止重放只留了0.7s(所以会导致某些网特别卡的使用不了)

$expected_sign = md5($timestamp . $token . $secret);
if ($sign !== $expected_sign) {
    // 403
}

确保请求没有被篡改,只有知道 Secret 的客户端才能生成正确的签名

$nonce_file = '/tmp/proxy_nonces/' . md5($token . '_' . $timestamp);
if (file_exists($nonce_file)) {
    // 返回 403
}
file_put_contents($nonce_file, 'used');
touch($nonce_file, $timestamp + 10);

nonce防止重放,一个token只能用10s,过了10s自动过期

下一步就是给另外一层服务器转发并且将结果返回了,这里就不放了。

那么cpp端还使用VMP超级模式进行了虚拟化和变异这点在代码中也是可以看出来的。