码农知识堂 - 1000bd
  •   Python
  •   PHP
  •   JS/TS
  •   JAVA
  •   C/C++
  •   C#
  •   GO
  •   Kotlin
  •   Swift
  • 1.8 运用C编写ShellCode代码


    合集 - 灰帽黑客:攻守道(83)
    1.1.13 导出表劫持ShellCode加载09-012.1.12 进程注入ShellCode套接字09-013.1.10 内存ShellCode注入与格式化08-314.1.9 动态解密ShellCode反弹08-315.1.8 运用C编写ShellCode代码08-306.1.7 完善自定位ShellCode08-307.1.6 编写双管道ShellCode08-298.1.5 编写自定位ShellCode弹窗08-299.1.4 编写简易ShellCode弹窗08-2810.1.3 Metasploit 生成SSL加密载荷08-2811.1.1 Metasploit 工具简介08-2812.5.14 汇编语言:仿写Switch选择结构08-2413.5.13 汇编语言:仿写For循环语句08-2414.5.12 汇编语言:仿写While循环语句08-2415.5.11 汇编语言:仿写IF条件语句08-2416.5.10 汇编语言:汇编过程与结构08-2417.5.9 汇编语言:浮点数操作指令08-2318.5.8 汇编语言:汇编高效除法运算08-2319.5.7 汇编语言:汇编高效乘法运算08-2320.5.6 汇编语言:汇编高效数组寻址08-2321.5.5 汇编语言:函数调用约定08-2222.5.4 汇编语言:算数运算指令集08-2223.5.3 汇编语言:字符串操作指令08-2224.5.2 汇编语言:标志位测试指令08-2225.5.1 汇编语言:汇编语言概述08-2226.4.9 C++ Boost 命令行解析库08-2227.4.8 C++ Boost 应用JSON解析库08-2228.4.7 C++ Boost 多线程并发库08-2129.4.6 C++ Boost 函数绑定回调库08-2130.4.5 C++ Boost 文件目录操作库08-2131.4.4 C++ Boost 数据集序列化库08-2132.4.3 C++ Boost 日期时间操作库08-1833.4.2 C++ Boost 内存池管理库08-1834.4.1 C++ Boost 字符串处理库08-1835.11.1 C++ STL 应用字典与列表08-1736.10.1 C++ STL 模板适配与迭代器08-1737.9.1 C++ STL 排序、算数与集合08-1738.8.1 C++ STL 变易拷贝算法08-1639.7.1 C++ STL 非变易查找算法08-1640.6.1 C++ STL 序列映射容器08-1641.5.1 C++ STL 集合数据容器08-1642.4.1 C++ STL 动态链表容器08-1643.3.1 C++ STL 双向队列容器08-1644.2.1 C++ STL 数组向量容器08-1645.1.1 C++ STL 字符串构造函数08-1646.7.5 C/C++ 实现链表队列08-1547.7.4 C/C++ 实现链表栈08-1548.7.3 C/C++ 实现顺序栈08-1549.7.2 C/C++ 实现动态链表08-1550.7.1 C/C++ 实现动态数组08-1551.9.0 Python 内置模块应用08-1452.8.0 Python 使用进程与线程08-1453.7.0 Python 面向对象编程08-1454.6.0 Python 使用函数装饰器08-1455.5.0 Python 定义并使用函数08-1356.4.0 Python 变量与作用域08-1357.3.0 Python 迭代器与生成器08-1258.2.0 Python 数据结构与类型08-1159.21.1 使用PEfile分析PE文件08-1060.1.0 Python 标准输入与输出08-09
    61.1.8 运用C编写ShellCode代码07-13
    62.5.2 基于ROP漏洞挖掘与利用07-1263.5.1 缓冲区溢出与攻防博弈07-1264.2.0 熟悉CheatEngine修改器07-1165.4.9 x64dbg 内存处理与差异对比07-1166.4.10 x64dbg 反汇编功能的封装07-1167.4.8 x64dbg 学会扫描应用堆栈07-1068.4.7 x64dbg 应用层的钩子扫描07-1069.4.6 x64dbg 内存扫描与查壳实现07-0970.4.5 x64dbg 探索钩子劫持技术07-0971.4.4 x64dbg 绕过反调试保护机制07-0872.4.3 x64dbg 搜索内存可利用指令07-0773.4.2 x64dbg 针对PE文件的扫描07-0774.4.1 探索LyScript漏洞挖掘插件07-0675.1.5 为x64dbg编写插件07-0676.1.1 熟悉x64dbg调试器07-0677.1.7 完善自定位ShellCode后门07-0578.1.6 编写双管道ShellCode后门07-0479.1.5 编写自定位ShellCode弹窗07-0380.1.4 编写简易ShellCode弹窗07-0281.1.3 Metasploit 生成SSL加密载荷07-0182.1.1 Metasploit 工具简介06-3083.2.1 PE结构:文件映射进内存09-04
    收起

    在笔者前几篇文章中,我们使用汇编语言并通过自定位的方法实现了一个简单的MessageBox弹窗功能,但由于汇编语言过于繁琐在编写效率上不仅要考验开发者的底层功底,还需要写出更多的指令集,这对于普通人来说是非常困难的,当然除了通过汇编来实现ShellCode的编写以外,使用C同样可以实现编写,在多数情况下读者可以直接使用C开发,只有某些环境下对ShellCode条件有极为苛刻的长度限制时才会考虑使用汇编。

    相较于汇编语言,使用C编写Shellcode可以更加方便、高效,特别是对于需要大量计算的操作。在编写Shellcode时,读者需要注意以下几点:

    • 1.使用纯C语言进行编写:在编写Shellcode时,需要避免使用C++标准库或其他外部依赖库,因为这些库往往会增加代码的长度和复杂度。
    • 2.关闭编译器优化:在编写Shellcode时,需要关闭编译器的优化功能,因为优化可能会改变代码的执行顺序,导致Shellcode无法正常工作。
    • 3.避免使用全局变量和静态变量:在Shellcode中,全局变量和静态变量往往会导致代码长度过长,并且这些变量的地址也可能与Shellcode中其他代码的地址产生冲突。
    • 4.使用裸指针和裸内存管理:为了减小Shellcode的长度和复杂度,需要使用裸指针和裸内存管理,这可以减少代码中不必要的辅助函数调用。
    • 5.不能使用全局变量,或者用static修饰的变量,在Shellcode中要自定义入口函数,所有的字符串都要用字符串数组的方式代替。

    首先读者应自行新建一个开发项目,并将编译模式调整为Release模式,这是因为Debug模式下的代码在转换成汇编后首先都是一个JMP指令,然后再跳到我们的功能代码处,但JMP指令是地址相关的 ,所以在转换成ShellCode时就会出错。此外在读者新建项目文件时请最好使用*.c结尾而不要使用*.cpp结尾。

    当读者新建文件以后,接下来请修改配置属性,将运行库修改为多线程(MT)并关闭安全检查机制,如下图所示;

    接着在连接器部分,新增一个EntryMain入口点,默认的Main入口点显然时不能使用的,如下图所示;

    与前几章中的内容原理一致,首先我们需要得到kernel32.dll模块的基址,这段代码我们依然采用汇编实现,这里需要注意__declspec(naked)的含义,该声明是微软编译器提供的一个扩展,它用于指示编译器不要为函数自动生成函数头和尾,并将函数转化为裸函数。这种函数不会自动生成函数前缀和后缀的代码,也不会创建任何本地变量或保护寄存器。

    在使用__declspec(naked)声明的函数中,开发者需要自己手动管理堆栈和调用函数的传递参数,然后在函数体中使用汇编指令实现所需的功能。使用__declspec(naked)声明的函数可以有效地减小生成的代码大小,因为不需要在函数前后添加额外的代码,而且可以精确控制函数内部的代码。

    注意:使用__declspec(naked)声明的函数需要开发者对汇编语言有一定的了解,否则容易出现错误。在使用时,需要非常小心,确保在函数内部正确地管理堆栈和传递参数,以确保函数能够正常工作。

    // ----------------------------------------------
    // 32位获取模块基址
    // ----------------------------------------------
    __declspec(naked) DWORD getKernel32()
    {
        __asm
        {
            mov eax, fs: [30h]
            mov eax, [eax + 0ch]
            mov eax, [eax + 14h]
            mov eax, [eax]
            mov eax, [eax]
            mov eax, [eax + 10h]
            ret
        }
    }
    
    // ----------------------------------------------
    // 64位获取模块基址
    // ----------------------------------------------
    /*
    .code
    getKernel32 proc
        mov rax,gs:[60h]
        mov rax,[rax+18h]
        mov rax,[rax+30h]
        mov rax,[rax]
        mov rax,[rax]
        mov rax,[rax+10h]
        ret
    getKernel32 endp
    end
    */
    

    当我们能够拿到kernel32.dll的模块基址时,则接下来就是通过该基址得到Kernel32的模块导出表,并获取该导出表内的GetProcessAddress函数的基址,至于为什么需要这么做,在读者前面的文章中有详细的分析,这里就不再重复叙述。

    // ----------------------------------------------
    // 32位取函数地址
    // ----------------------------------------------
    FARPROC getProcAddress(HMODULE hModuleBase)
    {
        PIMAGE_DOS_HEADER lpDosHeader = (PIMAGE_DOS_HEADER)hModuleBase;
        PIMAGE_NT_HEADERS32 lpNtHeader = (PIMAGE_NT_HEADERS)((DWORD)hModuleBase + lpDosHeader->e_lfanew);
        if (!lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size)
        {
            return NULL;
        }
        if (!lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress)
        {
            return NULL;
        }
    
        PIMAGE_EXPORT_DIRECTORY lpExports = (PIMAGE_EXPORT_DIRECTORY)((DWORD)hModuleBase + (DWORD)lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
        PDWORD lpdwFunName = (PDWORD)((DWORD)hModuleBase + (DWORD)lpExports->AddressOfNames);
        PWORD lpword = (PWORD)((DWORD)hModuleBase + (DWORD)lpExports->AddressOfNameOrdinals);
        PDWORD lpdwFunAddr = (PDWORD)((DWORD)hModuleBase + (DWORD)lpExports->AddressOfFunctions);
    
        DWORD dwLoop = 0;
        FARPROC pRet = NULL;
        for (; dwLoop <= lpExports->NumberOfNames - 1; dwLoop++)
        {
            char* pFunName = (char*)(lpdwFunName[dwLoop] + (DWORD)hModuleBase);
    
            if (pFunName[0] == 'G' &&
                pFunName[1] == 'e' &&
                pFunName[2] == 't' &&
                pFunName[3] == 'P' &&
                pFunName[4] == 'r' &&
                pFunName[5] == 'o' &&
                pFunName[6] == 'c' &&
                pFunName[7] == 'A' &&
                pFunName[8] == 'd' &&
                pFunName[9] == 'd' &&
                pFunName[10] == 'r' &&
                pFunName[11] == 'e' &&
                pFunName[12] == 's' &&
                pFunName[13] == 's')
            {
                pRet = (FARPROC)(lpdwFunAddr[lpword[dwLoop]] + (DWORD)hModuleBase);
                break;
            }
        }
        return pRet;
    }
    
    // ----------------------------------------------
    // 64位取函数地址
    // ----------------------------------------------
    /*
    FARPROC getProcAddress(HMODULE hModuleBase)
    {
        PIMAGE_DOS_HEADER lpDosHeader = (PIMAGE_DOS_HEADER)hModuleBase;
        PIMAGE_NT_HEADERS64 lpNtHeader = (PIMAGE_NT_HEADERS64)((ULONG64)hModuleBase + lpDosHeader->e_lfanew);
        if (!lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size)
        {
            return NULL;
        }
        if (!lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress)
        {
            return NULL;
        }
        PIMAGE_EXPORT_DIRECTORY lpExports = (PIMAGE_EXPORT_DIRECTORY)((ULONG64)hModuleBase + (ULONG64)lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
        PDWORD lpdwFunName = (PDWORD)((ULONG64)hModuleBase + (ULONG64)lpExports->AddressOfNames);
        PWORD lpword = (PWORD)((ULONG64)hModuleBase + (ULONG64)lpExports->AddressOfNameOrdinals);
        PDWORD  lpdwFunAddr = (PDWORD)((ULONG64)hModuleBase + (ULONG64)lpExports->AddressOfFunctions);
    
        DWORD dwLoop = 0;
        FARPROC pRet = NULL;
        for (; dwLoop <= lpExports->NumberOfNames - 1; dwLoop++)
        {
            char* pFunName = (char*)(lpdwFunName[dwLoop] + (ULONG64)hModuleBase);
    
            if (pFunName[0] == 'G' &&
                pFunName[1] == 'e' &&
                pFunName[2] == 't' &&
                pFunName[3] == 'P' &&
                pFunName[4] == 'r' &&
                pFunName[5] == 'o' &&
                pFunName[6] == 'c' &&
                pFunName[7] == 'A' &&
                pFunName[8] == 'd' &&
                pFunName[9] == 'd' &&
                pFunName[10] == 'r' &&
                pFunName[11] == 'e' &&
                pFunName[12] == 's' &&
                pFunName[13] == 's')
            {
                pRet = (FARPROC)(lpdwFunAddr[lpword[dwLoop]] + (ULONG64)hModuleBase);
                break;
            }
        }
        return pRet;
    }
    */
    

    接着我们需要编写主代码逻辑,主代码逻辑中使用GetProcAddress和LoadLibraryW来加载user32.dll并调用其中的MessageBoxW函数弹出一个消息框的示例。

    下面是代码的详细实现流程:

    • 1.定义函数指针类型FN_GetProcAddress,用于存储GetProcAddress函数的地址,该函数用于在加载的DLL中查找导出函数的地址。
    • 2.通过getProcAddress函数获取kernel32.dll中的GetProcAddress函数地址,并将其转换为FN_GetProcAddress类型的函数指针fn_GetProcAddress。
    • 3.定义函数指针类型FN_LoadLibraryW,用于存储LoadLibraryW函数的地址,该函数用于加载指定的DLL文件。
    • 4.定义名为xyLoadLibraryW的字符数组,存储字符串"LoadLibraryW"。
    • 5.使用fn_GetProcAddress函数指针获取kernel32.dll中的LoadLibraryW函数的地址,并将其转换为FN_LoadLibraryW类型的函数指针fn_LoadLibraryW。
    • 6.定义函数指针类型FN_MessageBoxW,用于存储MessageBoxW函数的地址,该函数用于弹出消息框。
    • 7.定义名为xy_MessageBoxW的字符数组,存储字符串"MessageBoxW"。
    • 8.定义名为xy_user32的字符数组,存储字符串"user32.dll"。
    • 9.使用fn_LoadLibraryW函数指针加载user32.dll,并使用fn_GetProcAddress函数指针获取其中的MessageBoxW函数地址,并将其转换为FN_MessageBoxW类型的函数指针fn_MessageBoxW。
    • 10.定义名为MsgBox和Title的wchar_t数组,用于存储消息框的文本内容和标题。
    • 11.使用fn_MessageBoxW函数指针弹出一个消息框,显示MsgBox中的文本内容,并使用Title中的文本作为标题。
    #include 
    
    FARPROC getProcAddress(HMODULE hModuleBase);
    DWORD getKernel32();
    // extern "C" PVOID64  getKernel32();
    
    // ----------------------------------------------
    // 32位主函数
    // ----------------------------------------------
    int EntryMain()
    {
        // 定义指针,用于存储GetProcAddress入口地址
        typedef FARPROC(WINAPI* FN_GetProcAddress)(
            _In_ HMODULE hModule,
            _In_ LPCSTR lpProcName
            );
    
        FN_GetProcAddress fn_GetProcAddress = (FN_GetProcAddress)getProcAddress((HMODULE)getKernel32());
    
        // 定义指针,用于存储LoadLibraryW入口地址
        typedef HMODULE(WINAPI* FN_LoadLibraryW)(
            _In_ LPCWSTR lpLibFileName
            );
        char xyLoadLibraryW[] = { 'L', 'o', 'a', 'd', 'L', 'i', 'b', 'r', 'a', 'r', 'y', 'W', 0 };
        FN_LoadLibraryW fn_LoadLibraryW = (FN_LoadLibraryW)fn_GetProcAddress((HMODULE)getKernel32(), xyLoadLibraryW);
    
        // 定义指针,用于存储MessageBoxW入口地址
        typedef int (WINAPI* FN_MessageBoxW)(
            _In_opt_ HWND hWnd,
            _In_opt_ LPCWSTR lpText,
            _In_opt_ LPCWSTR lpCaption,
            _In_ UINT uType);
        wchar_t xy_user32[] = { 'u', 's', 'e', 'r', '3', '2', '.', 'd', 'l', 'l', 0 };
        char xy_MessageBoxW[] = { 'M', 'e', 's', 's', 'a', 'g', 'e', 'B', 'o', 'x', 'W', 0 };
        FN_MessageBoxW fn_MessageBoxW = (FN_MessageBoxW)fn_GetProcAddress(fn_LoadLibraryW(xy_user32), xy_MessageBoxW);
    
        // 此处用于设置MessageBoxW弹窗的文本内容
        wchar_t MsgBox[] = { 'H', 'e', 'l', 'l', 'o', 'L', 'y', 'S', 'h','a','r','k', 0 };
        wchar_t Title[] = { 'T', 'E', 'S', 'T', 0 };
        fn_MessageBoxW(NULL, MsgBox, Title, NULL);
    
        return 0;
    }
    
    // ----------------------------------------------
    // 64位主函数
    // ----------------------------------------------
    /*
    int EntryMain()
    {
        typedef FARPROC(WINAPI* FN_GetProcAddress)(
            _In_ HMODULE hModule,
            _In_ LPCSTR lpProcName
            );
        FN_GetProcAddress fn_GetProcAddress = (FN_GetProcAddress)getProcAddress((HMODULE)getKernel32());
    
        typedef HMODULE(WINAPI* FN_LoadLibraryW)(
            _In_ LPCWSTR lpLibFileName
            );
        char xyLoadLibraryW[] = { 'L', 'o', 'a', 'd', 'L', 'i', 'b', 'r', 'a', 'r', 'y', 'W', 0 };
        FN_LoadLibraryW fn_LoadLibraryW = (FN_LoadLibraryW)fn_GetProcAddress((HMODULE)getKernel32(), xyLoadLibraryW);
    
        typedef int (WINAPI* FN_MessageBoxW)(
            _In_opt_ HWND hWnd,
            _In_opt_ LPCWSTR lpText,
            _In_opt_ LPCWSTR lpCaption,
            _In_ UINT uType);
        wchar_t xy_user32[] = { 'u', 's', 'e', 'r', '3', '2', '.', 'd', 'l', 'l', 0 };
        char xy_MessageBoxW[] = { 'M', 'e', 's', 's', 'a', 'g', 'e', 'B', 'o', 'x', 'W', 0 };
        FN_MessageBoxW fn_MessageBoxW = (FN_MessageBoxW)fn_GetProcAddress(fn_LoadLibraryW(xy_user32), xy_MessageBoxW);
    
        wchar_t xy_Hello[] = { 'H', 'e', 'l', 'l', 'o', 'L', 'y', 'S', 'h', 'a', 'r', 'k', 0 };
        wchar_t xy_tip[] = { 'T', 'E', 'S', 'T', 0 };
        fn_MessageBoxW(NULL, xy_Hello, xy_tip, NULL);
    
        return 0;
    }
    */
    

    至此读者需要手动编译上述代码,当编译通过之后,请打开WinHex工具,并定位到ShellCode的开头位置,如下图所示则是我们需要提取的指令集;

    选中这片区域,并右键点击编辑按钮,找到复制,C源码格式,此时读者即可得到一个完整的源代码格式;

    至此读者只需要一个注入器用于测试代码的完善性,此处是简单实现的一个注入器,代码中shellcode是我们上图中提取出来的片段,读者需要修改targetPid为任意一个32位应用程序,并运行注入即可;

    #include 
    #include 
    
    using namespace std;
    
    unsigned char shellcode[450] =
    {
        0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x5C, 0x53, 0x56, 0x57, 0xE8, 0xD2, 0x00, 0x00, 0x00, 0x8B, 0xC8,
        0xE8, 0xEB, 0x00, 0x00, 0x00, 0x8B, 0xF0, 0xC7, 0x45, 0xD8, 0x4C, 0x6F, 0x61, 0x64, 0x8D, 0x45,
        0xD8, 0xC7, 0x45, 0xDC, 0x4C, 0x69, 0x62, 0x72, 0x50, 0xC7, 0x45, 0xE0, 0x61, 0x72, 0x79, 0x57,
        0xC6, 0x45, 0xE4, 0x00, 0xE8, 0xA7, 0x00, 0x00, 0x00, 0x50, 0xFF, 0xD6, 0x33, 0xC9, 0xC7, 0x45,
        0xC0, 0x75, 0x00, 0x73, 0x00, 0x66, 0x89, 0x4D, 0xD4, 0x8D, 0x4D, 0xE8, 0x51, 0x8D, 0x4D, 0xC0,
        0x5D, 0xC3
    };
    
    int main(int argc, char *argv[])
    {
        DWORD targetPid = 2816;
    
        HANDLE h_target = NULL;
        LPVOID p_base = NULL;
        HANDLE h_thread = NULL;
    
        h_target = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPid);
        if (h_target == NULL)
        {
            goto main_end;
        }
    
        p_base = VirtualAllocEx(h_target, NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
        if (p_base == NULL)
        {
    
            goto main_end;
        }
    
        if (!WriteProcessMemory(h_target, p_base, (LPVOID)shellcode, sizeof(shellcode), NULL)) {
            goto main_end;
        }
    
        h_thread = CreateRemoteThread(h_target, 0, 0, (LPTHREAD_START_ROUTINE)p_base, NULL, 0, NULL);
        if (h_thread == NULL)
        {
            goto main_end;
        }
    
    main_end:
        if (h_target)
            CloseHandle(h_target);
        if (h_thread)
            CloseHandle(h_thread);
        getchar();
        return 0;
    }
    

    如果一切顺利,则读者可看到这段ShellCode已经在特定进程内实现运行了,并输出了如下图所示的弹窗提示;

    原文地址

    https://www.lyshark.com/post/9cc4ded5.html

  • 相关阅读:
    shell脚本练习及小总结
    在STM32F103C8T6上使用RT_Thread Nano移植控制台和Finsh
    微服务实战|熔断器Hystrix初体验
    【修复版】2023新版塔罗 算八字测运易理风水 取名 源码平台 搭建教程
    14、顺时针打印矩阵
    Docker部署gitlab_ce(避坑版---社区版)
    Filter(过滤器)与Listener(监听器)详解
    Ubuntu16.04搭建UbertoothOne环境
    【连网】Win10总是自动断网,检测默认网关不可用
    你不知道的Linux shell操作
  • 原文地址:https://www.cnblogs.com/LyShark/p/17549459.html
  • 最新文章
  • 攻防演习之三天拿下官网站群
    数据安全治理学习——前期安全规划和安全管理体系建设
    企业安全 | 企业内一次钓鱼演练准备过程
    内网渗透测试 | Kerberos协议及其部分攻击手法
    0day的产生 | 不懂代码的"代码审计"
    安装scrcpy-client模块av模块异常,环境问题解决方案
    leetcode hot100【LeetCode 279. 完全平方数】java实现
    OpenWrt下安装Mosquitto
    AnatoMask论文汇总
    【AI日记】24.11.01 LangChain、openai api和github copilot
  • 热门文章
  • 十款代码表白小特效 一个比一个浪漫 赶紧收藏起来吧!!!
    奉劝各位学弟学妹们,该打造你的技术影响力了!
    五年了,我在 CSDN 的两个一百万。
    Java俄罗斯方块,老程序员花了一个周末,连接中学年代!
    面试官都震惊,你这网络基础可以啊!
    你真的会用百度吗?我不信 — 那些不为人知的搜索引擎语法
    心情不好的时候,用 Python 画棵樱花树送给自己吧
    通宵一晚做出来的一款类似CS的第一人称射击游戏Demo!原来做游戏也不是很难,连憨憨学妹都学会了!
    13 万字 C 语言从入门到精通保姆级教程2021 年版
    10行代码集2000张美女图,Python爬虫120例,再上征途
Copyright © 2022 侵权请联系2656653265@qq.com    京ICP备2022015340号-1
正则表达式工具 cron表达式工具 密码生成工具

京公网安备 11010502049817号