• 转:EasyHook远程代码注入


     

    EasyHook远程代码注入

      

        最近一段时间由于使用MinHook的API挂钩不稳定,经常因为挂钩地址错误而导致宿主进程崩溃。听同事介绍了一款智能强大的挂钩引擎EasyHook。它比微软的detours好的一点是它的x64注入支持是免费开源的。不想微软的detours,想搞x64还得购买。

        好了,闲话不多说,先下载EasyHook的开发库,当然有兴趣的同学可以下载源码进行学习。下载地址:http://easyhook.codeplex.com/releases/view/24401。我给的这个是2.6版本的。

        EasyHook提供了两种模式的注入管理。一种是托管代码的注入,另一种是非托管代码的注入。我是学习C++的,所以直接学习了例子中的非托管项目UnmanagedHook。里面给了一个简单的挂钩MessageBeep API的示例。我需要将其改造成支持远程注入的。下面先给出钩子DLL代码:

    1. // dllmain.cpp : 定义 DLL 应用程序的入口点。  
    2. #include "stdafx.h"  
    3. #include "HookApi.h"  
    4. #include "easyhook.h"  
    5. #include "ntstatus.h"  
    6.   
    7. ptrCreateFileW realCreateFileW = NULL;  
    8. ptrCreateFileA realCreateFileA = NULL;  
    9. HMODULE                 hKernel32 = NULL;  
    10. TRACED_HOOK_HANDLE      hHookCreateFileW = new HOOK_TRACE_INFO();  
    11. TRACED_HOOK_HANDLE      hHookCreateFileA = new HOOK_TRACE_INFO();  
    12. NTSTATUS                statue;  
    13. ULONG                   HookCreateFileW_ACLEntries[1] = {0};  
    14. ULONG                   HookCreateFileA_ACLEntries[1] = {0};  
    15.   
    16. int PrepareRealApiEntry()  
    17. {  
    18.     OutputDebugString(L"PrepareRealApiEntry() ");  
    19.   
    20.     // 获取真实函数地址  
    21.     HMODULE hKernel32 = LoadLibrary(L"Kernel32.dll");  
    22.     if (hKernel32 == NULL)  
    23.     {  
    24.         OutputDebugString(L"LoadLibrary(L"Kernel32.dll") Error ");  
    25.         return -6002;  
    26.     }  
    27.     OutputDebugString(L"LoadLibrary(L"Kernel32.dll") OK ");  
    28.   
    29.     realCreateFileW = (ptrCreateFileW)GetProcAddress(hKernel32, "CreateFileW");  
    30.     if (realCreateFileW == NULL)  
    31.     {  
    32.         OutputDebugString(L"(ptrCreateFileW)GetProcAddress(hKernel32, "CreateFileW") Error ");  
    33.         return -6007;  
    34.     }  
    35.     OutputDebugString(L"(ptrCreateFileW)GetProcAddress(hKernel32, "CreateFileW") OK ");  
    36.   
    37.     realCreateFileA = (ptrCreateFileA)GetProcAddress(hKernel32, "CreateFileA");  
    38.     if (realCreateFileA == NULL)  
    39.     {  
    40.         OutputDebugString(L"(ptrCreateFileA)GetProcAddress(hKernel32, "CreateFileA") Error ");  
    41.         return -6007;  
    42.     }  
    43.     OutputDebugString(L"(ptrCreateFileA)GetProcAddress(hKernel32, "CreateFileA") OK ");  
    44.   
    45.     return 0;  
    46. }  
    47.   
    48. void DoHook()  
    49. {  
    50.     OutputDebugString(L"DoHook() ");  
    51.   
    52.     statue = LhInstallHook(realCreateFileW,  
    53.         MyCreateFileW,  
    54.         /*(PVOID)0x12345678*/NULL,  
    55.         hHookCreateFileW);  
    56.     if(!SUCCEEDED(statue))  
    57.     {  
    58.         switch (statue)  
    59.         {  
    60.         case STATUS_NO_MEMORY:  
    61.             OutputDebugString(L"STATUS_NO_MEMORY ");  
    62.             break;  
    63.         case STATUS_NOT_SUPPORTED:  
    64.             OutputDebugString(L"STATUS_NOT_SUPPORTED ");  
    65.             break;  
    66.         case STATUS_INSUFFICIENT_RESOURCES:  
    67.             OutputDebugString(L"STATUS_INSUFFICIENT_RESOURCES ");  
    68.             break;  
    69.         default:  
    70.             WCHAR dbgstr[512] = {0};  
    71.             wsprintf(dbgstr, L"%d ", statue);  
    72.             OutputDebugString(dbgstr);  
    73.         }  
    74.         OutputDebugString(L"LhInstallHook(GetProcAddress(hKernel32, "CreateFileW"),MyCreateFileW,(PVOID)0x12345678,hHookCreateFileW); Error ");  
    75.         return;  
    76.     }  
    77.     OutputDebugString(L"Hook CreateFileW OK ");  
    78.   
    79.     statue = LhInstallHook(realCreateFileA,  
    80.         MyCreateFileA,  
    81.         /*(PVOID)0x12345678*/NULL,  
    82.         hHookCreateFileA);  
    83.     if(!SUCCEEDED(statue))  
    84.     {  
    85.         switch (statue)  
    86.         {  
    87.         case STATUS_NO_MEMORY:  
    88.             OutputDebugString(L"STATUS_NO_MEMORY ");  
    89.             break;  
    90.         case STATUS_NOT_SUPPORTED:  
    91.             OutputDebugString(L"STATUS_NOT_SUPPORTED ");  
    92.             break;  
    93.         case STATUS_INSUFFICIENT_RESOURCES:  
    94.             OutputDebugString(L"STATUS_INSUFFICIENT_RESOURCES ");  
    95.             break;  
    96.         default:  
    97.             WCHAR dbgstr[512] = {0};  
    98.             wsprintf(dbgstr, L"%d ", statue);  
    99.             OutputDebugString(dbgstr);  
    100.         }  
    101.         OutputDebugString(L"LhInstallHook(GetProcAddress(hKernel32, "CreateFileA"),MyCreateFileA,(PVOID)0x12345678,hHookCreateFileA); Error ");  
    102.         return;  
    103.     }  
    104.     OutputDebugString(L"Hook CreateFileA OK ");  
    105.   
    106.       
    107.         // 一定要调用这个函数,否则注入的钩子无法正常运行。  
    108.         LhSetExclusiveACL(HookCreateFileA_ACLEntries, 1, hHookCreateFileA);  
    109.     LhSetExclusiveACL(HookCreateFileW_ACLEntries, 1, hHookCreateFileW);  
    110.   
    111. }  
    112.   
    113. void DoneHook()  
    114. {  
    115.     OutputDebugString(L"DoneHook() ");  
    116.   
    117.     // this will also invalidate "hHook", because it is a traced handle...  
    118.     LhUninstallAllHooks();  
    119.   
    120.     // this will do nothing because the hook is already removed...  
    121.     LhUninstallHook(hHookCreateFileA);  
    122.     LhUninstallHook(hHookCreateFileW);  
    123.   
    124.     // now we can safely release the traced handle  
    125.     delete hHookCreateFileA;  
    126.     hHookCreateFileA = NULL;  
    127.   
    128.     delete hHookCreateFileW;  
    129.     hHookCreateFileW = NULL;  
    130.   
    131.     // even if the hook is removed, we need to wait for memory release  
    132.     LhWaitForPendingRemovals();  
    133. }  
    134.   
    135. BOOL APIENTRY DllMain( HMODULE hModule,  
    136.                        DWORD  ul_reason_for_call,  
    137.                        LPVOID lpReserved  
    138.                      )  
    139. {  
    140.     switch (ul_reason_for_call)  
    141.     {  
    142.     case DLL_PROCESS_ATTACH:  
    143.         {  
    144.             OutputDebugString(L"DllMain::DLL_PROCESS_ATTACH ");  
    145.   
    146.             // 准备好原始地址与目的地址  
    147.             int errCode = PrepareRealApiEntry();  
    148.             if (errCode != 0)  
    149.             {  
    150.                 OutputDebugString(L"PrepareRealApiEntry() Error ");  
    151.                 return FALSE;  
    152.             }  
    153.   
    154.             // 开始挂钩  
    155.             DoHook();  
    156.   
    157.             break;  
    158.         }  
    159.     case DLL_THREAD_ATTACH:  
    160.         {  
    161.             OutputDebugString(L"DllMain::DLL_THREAD_ATTACH ");  
    162.   
    163.             break;  
    164.         }  
    165.     case DLL_THREAD_DETACH:  
    166.         {  
    167.             OutputDebugString(L"DllMain::DLL_THREAD_DETACH ");  
    168.   
    169.             break;  
    170.         }  
    171.           
    172.     case DLL_PROCESS_DETACH:  
    173.         {  
    174.             OutputDebugString(L"DllMain::DLL_PROCESS_DETACH ");  
    175.   
    176.             // 卸载钩子  
    177.             DoneHook();  
    178.   
    179.             break;  
    180.         }  
    181.     }  
    182.     return TRUE;  
    183. }  
    1. <pre name="code" class="cpp">// HookSvr.cpp  
    2.   
    3. #include "stdafx.h"  
    4. #include "HookApi.h"  
    5. #include "easyhook.h"  
    6.   
    7. HANDLE WINAPI MyCreateFileW(  
    8.               __in     LPCWSTR lpFileName,  
    9.               __in     DWORD dwDesiredAccess,  
    10.               __in     DWORD dwShareMode,  
    11.               __in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes,  
    12.               __in     DWORD dwCreationDisposition,  
    13.               __in     DWORD dwFlagsAndAttributes,  
    14.               __in_opt HANDLE hTemplateFile  
    15.               )  
    16. {  
    17.     HANDLE hHandle = NULL;  
    18.   
    19.     // 执行钩子  
    20.     if (realCreateFileW == NULL)  
    21.     {  
    22.         OutputDebugString(L"realCreateFileW is NULL ");  
    23.         return INVALID_HANDLE_VALUE;  
    24.     }  
    25.     else  
    26.     {  
    27.         OutputDebugString(L"realCreateFileW is not NULL ");  
    28.         hHandle = (realCreateFileW)(lpFileName, dwDesiredAccess, dwShareMode,  
    29.             lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);  
    30.   
    31.         OutputDebugString(L"MyCreateFileW : ");  
    32.         OutputDebugString(lpFileName);  
    33.         OutputDebugString(L" ");  
    34.     }  
    35.   
    36.     return hHandle;  
    37. }  
    38.   
    39. HANDLE WINAPI MyCreateFileA(  
    40.                   __in     LPCSTR lpFileName,  
    41.                   __in     DWORD dwDesiredAccess,  
    42.                   __in     DWORD dwShareMode,  
    43.                   __in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes,  
    44.                   __in     DWORD dwCreationDisposition,  
    45.                   __in     DWORD dwFlagsAndAttributes,  
    46.                   __in_opt HANDLE hTemplateFile  
    47.                   )  
    48. {  
    49.     HANDLE hHandle = NULL;  
    50.   
    51.     // 执行钩子  
    52.     if (realCreateFileA == NULL)  
    53.     {  
    54.         OutputDebugString(L"realCreateFileA is NULL ");  
    55.         return INVALID_HANDLE_VALUE;  
    56.     }  
    57.     else  
    58.     {  
    59.         OutputDebugString(L"realCreateFileA is not NULL ");  
    60.         hHandle = (realCreateFileA)(lpFileName, dwDesiredAccess, dwShareMode,  
    61.             lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);  
    62.   
    63.         OutputDebugString(L"MyCreateFileW : ");  
    64.         OutputDebugStringA(lpFileName);  
    65.         OutputDebugString(L" ");  
    66.     }  
    67.   
    68.     return hHandle;  
    69. }</pre><br>  
    70. 钩子这一部分我弄了比较久,主要是API不熟悉,不过好在弄好了。  
    71. <pre></pre>  
    72. <p><br>  
    73. </p>  
    74. <p></p><pre name="code" class="cpp">// HookSvr.h  
    75.   
    76. #pragma once  
    77. #include <Windows.h>  
    78.   
    79. #ifndef _M_X64  
    80. #pragma comment(lib, "EasyHook32.lib")  
    81. #else  
    82. #pragma comment(lib, "EasyHook64.lib")  
    83. #endif  
    84.   
    85. HANDLE WINAPI MyCreateFileW(  
    86.     __in     LPCWSTR lpFileName,  
    87.     __in     DWORD dwDesiredAccess,  
    88.     __in     DWORD dwShareMode,  
    89.     __in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes,  
    90.     __in     DWORD dwCreationDisposition,  
    91.     __in     DWORD dwFlagsAndAttributes,  
    92.     __in_opt HANDLE hTemplateFile  
    93.     );  
    94.   
    95. typedef HANDLE (WINAPI *ptrCreateFileW)(  
    96.     __in     LPCWSTR lpFileName,  
    97.     __in     DWORD dwDesiredAccess,  
    98.     __in     DWORD dwShareMode,  
    99.     __in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes,  
    100.     __in     DWORD dwCreationDisposition,  
    101.     __in     DWORD dwFlagsAndAttributes,  
    102.     __in_opt HANDLE hTemplateFile  
    103.     );  
    104.   
    105. extern ptrCreateFileW realCreateFileW;  
    106.   
    107. HANDLE WINAPI MyCreateFileA(  
    108.     __in     LPCSTR lpFileName,  
    109.     __in     DWORD dwDesiredAccess,  
    110.     __in     DWORD dwShareMode,  
    111.     __in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes,  
    112.     __in     DWORD dwCreationDisposition,  
    113.     __in     DWORD dwFlagsAndAttributes,  
    114.     __in_opt HANDLE hTemplateFile  
    115.     );  
    116.   
    117. typedef HANDLE (WINAPI *ptrCreateFileA)(  
    118.                                         __in     LPCSTR lpFileName,  
    119.                                         __in     DWORD dwDesiredAccess,  
    120.                                         __in     DWORD dwShareMode,  
    121.                                         __in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes,  
    122.                                         __in     DWORD dwCreationDisposition,  
    123.                                         __in     DWORD dwFlagsAndAttributes,  
    124.                                         __in_opt HANDLE hTemplateFile  
    125.                                         );  
    126.   
    127. extern ptrCreateFileA realCreateFileA;</pre><br>  
    128. <br>  
    129. <p></p>  
    130. <p>接下来是注入工具,这里指提供核心代码。本来EasyHook还提供了一个叫<span style="color:black">Rh</span>InjectLibrary()方法直接注入,这种方法相当稳定,推荐使用。我本来也用它,但是发现注入会失败,所以就采用了比较通用的远程注入代码,如下:</p>  
    131. <pre name="code" class="cpp">BOOL RtlFileExists(WCHAR* InPath)  
    132. {  
    133.     HANDLE          hFile;  
    134.   
    135.     if((hFile = CreateFileW(InPath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE)  
    136.         return FALSE;  
    137.   
    138.     CloseHandle(hFile);  
    139.   
    140.     return TRUE;  
    141. }  
    142.   
    143. BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege)  
    144. {  
    145.     TOKEN_PRIVILEGES tp;  
    146.     HANDLE hToken;  
    147.     LUID luid;  
    148.   
    149.     if( !OpenProcessToken(GetCurrentProcess(),  
    150.         TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,   
    151.         &hToken) )  
    152.     {         
    153.         return FALSE;  
    154.     }  
    155.   
    156.     if( !LookupPrivilegeValue(NULL,             // lookup privilege on local system  
    157.         lpszPrivilege,    // privilege to lookup   
    158.         &luid) )          // receives LUID of privilege  
    159.     {         
    160.         return FALSE;   
    161.     }  
    162.   
    163.     tp.PrivilegeCount = 1;  
    164.     tp.Privileges[0].Luid = luid;  
    165.     if( bEnablePrivilege )  
    166.         tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;  
    167.     else  
    168.         tp.Privileges[0].Attributes = 0;  
    169.   
    170.     // Enable the privilege or disable all privileges.  
    171.     if( !AdjustTokenPrivileges(hToken,   
    172.         FALSE,   
    173.         &tp,   
    174.         sizeof(TOKEN_PRIVILEGES),   
    175.         (PTOKEN_PRIVILEGES) NULL,   
    176.         (PDWORD) NULL) )  
    177.     {         
    178.         return FALSE;   
    179.     }   
    180.   
    181.     if( GetLastError() == ERROR_NOT_ALL_ASSIGNED )  
    182.     {  
    183.         //The token does not have the specified privilege.  
    184.         return FALSE;  
    185.     }   
    186.   
    187.     return TRUE;  
    188. }  
    189.   
    190. typedef DWORD (WINAPI *PFNTCREATETHREADEX)  
    191. (   
    192.  PHANDLE                 ThreadHandle,    
    193.  ACCESS_MASK             DesiredAccess,   
    194.  LPVOID                  ObjectAttributes,    
    195.  HANDLE                  ProcessHandle,   
    196.  LPTHREAD_START_ROUTINE  lpStartAddress,      
    197.  LPVOID                  lpParameter,     
    198.  BOOL                   CreateSuspended,      
    199.  DWORD                   dwStackSize,     
    200.  DWORD                   dw1,   
    201.  DWORD                   dw2,   
    202.  LPVOID                  Unknown   
    203.  );   
    204.   
    205. BOOL MyCreateRemoteThread(HANDLE hProcess, LPTHREAD_START_ROUTINE pThreadProc, LPVOID pRemoteBuf)  
    206. {  
    207.     HANDLE      hThread = NULL;  
    208.     FARPROC     pFunc = NULL;  
    209.     BOOL bHook;  
    210.   
    211.     // 判断系统版本  
    212.     OSVERSIONINFO osvi;  
    213.     //BOOL bIsWindowsXPorLater;  
    214.   
    215.     ZeroMemory(&osvi, sizeof(OSVERSIONINFO));  
    216.     osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);  
    217.   
    218.     GetVersionEx(&osvi);  
    219.   
    220.     if (osvi.dwMajorVersion == 6)  
    221.     {  
    222.         bHook = TRUE;  
    223.     }  
    224.     else  
    225.     {  
    226.         bHook = FALSE;  
    227.     }  
    228.   
    229.     if(bHook)    // Vista, 7, Server2008  
    230.     {  
    231.         pFunc = GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtCreateThreadEx");  
    232.         if( pFunc == NULL )  
    233.         {  
    234.             //GetLastError());  
    235.             return FALSE;  
    236.         }  
    237.   
    238.         OutputDebugString(L"MyCreateRemoteThread");  
    239.         ((PFNTCREATETHREADEX)pFunc)(&hThread,  
    240.             0x1FFFFF,  
    241.             NULL,  
    242.             hProcess,  
    243.             pThreadProc,  
    244.             pRemoteBuf,  
    245.             FALSE,  
    246.             NULL,  
    247.             NULL,  
    248.             NULL,  
    249.             NULL);  
    250.         if( hThread == NULL )  
    251.         {             
    252.             return FALSE;  
    253.         }  
    254.     }  
    255.     else                    // 2000, XP, Server2003  
    256.     {  
    257.         hThread = CreateRemoteThread(hProcess,   
    258.             NULL,   
    259.             0,   
    260.             pThreadProc,   
    261.             pRemoteBuf,   
    262.             0,   
    263.             NULL);  
    264.         if( hThread == NULL )  
    265.         {             
    266.             return FALSE;  
    267.         }  
    268.     }  
    269.   
    270.     if( WAIT_FAILED == WaitForSingleObject(hThread, INFINITE) )  
    271.     {         
    272.         return FALSE;  
    273.     }  
    274.   
    275.     return TRUE;  
    276. }  
    277.   
    278. BOOL InjectDll(DWORD dwPID, const wchar_t *szDllName)  
    279. {  
    280.     HANDLE hProcess = NULL;  
    281.     LPVOID pRemoteBuf = NULL;  
    282.     FARPROC pThreadProc = NULL;  
    283.     DWORD dwBufSize = wcslen(szDllName)*sizeof(wchar_t)+2;  
    284.   
    285.     if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) )  
    286.     {         
    287.         return FALSE;  
    288.     }  
    289.   
    290.     pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize,   
    291.         MEM_COMMIT, PAGE_READWRITE);  
    292.   
    293.     WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllName,   
    294.         dwBufSize, NULL);  
    295.   
    296.     pThreadProc = GetProcAddress(GetModuleHandle(L"kernel32.dll"),   
    297.         "LoadLibraryW");  
    298.   
    299.     if( !MyCreateRemoteThread(hProcess, (LPTHREAD_START_ROUTINE)pThreadProc, pRemoteBuf) )  
    300.     {         
    301.         return FALSE;  
    302.     }  
    303.   
    304.     VirtualFreeEx(hProcess, pRemoteBuf, dwBufSize, MEM_RELEASE);  
    305.     CloseHandle(hProcess);  
    306.     return TRUE;  
    307. }  
    308.   
    309. int DoInject(DWORD aPid, const WCHAR *aFullpath)  
    310. {  
    311.     if (wcslen(aFullpath) <= 0)  
    312.     {  
    313.         return -1;  
    314.     }  
    315.   
    316.     //判断dll是否存在  
    317.     HANDLE hFile = CreateFile(aFullpath, GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE,NULL, OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);  
    318.     if(hFile != INVALID_HANDLE_VALUE)  
    319.     {                 
    320.         DWORD dwsize = GetFileSize(hFile, NULL);  
    321.         CloseHandle(hFile);  
    322.         if (dwsize < 10)  
    323.         {             
    324.             return -2;  
    325.         }         
    326.     }  
    327.     else  
    328.     {                 
    329.         return -3;  
    330.     }  
    331.   
    332.     BOOL bSuc=SetPrivilege(SE_DEBUG_NAME, TRUE);  
    333.     bSuc=InjectDll((DWORD)aPid, aFullpath);  
    334.     if (bSuc)  
    335.     {  
    336.         return -4;  
    337.     }  
    338.   
    339.     return 0;  
    340. }  
    341.   
    342.   
    343. // 真实注入的时候应该这样调用  
    344. DoInject(m_processId, L"E:\src\easyhook\trunk\Debug\x86\HookSvr.dll");  
    345.   
    346.   
    347. </pre><br>  
    348. 这样就能保证注入的钩子能正常工作了。  
     
     
    查看评论
    4楼 SpiritMFC 2013-03-26 15:43发表 [回复]
    你好~ 能提供能运行的源码嘛?
    我用你的方法正常exe可以hook成功,
    但是DLL注入后钩子无法工作。
    困扰数天的问题了。
    求帮助!
    还有 C++的话这个库能实现全局钩子嘛?
    Re: baggiowangyu 2013-03-29 09:24发表 [回复]
    回复SpiritMFC:我给的例子就是源代码了哇,那时候研究到那里就没有继续往下了。应该是你注入之后挂钩写的不对导致的。

    你指的全局钩子是什么概念?全局消息钩子么?目前我知道的是这个可以实现指定进程的挂钩。
    3楼 lsssml1990 2012-10-26 12:22发表 [回复]
    这个代码可以直接用不?
    2楼 baggiowangyu 2012-06-23 14:02发表 [回复]
    一同学习,一同学习
    1楼 Wentasy 2012-06-19 11:46发表 [回复]
    不错,学习了。
  • 相关阅读:
    vue学习之router
    vue学习之组件
    xshell操作
    Webstorm快捷操作
    javascript判断节点是否在dom
    影子节点 shadowDOM
    虚拟节点操作——DocumentFragment
    理解浏览器的历史记录
    浏览器渲染
    web请求流程
  • 原文地址:https://www.cnblogs.com/shenlian/p/3359003.html
Copyright © 2020-2023  润新知