windows默认会用异常处理程序包含SetTimer的回调函数TimerProc,所以可以利用这一点给远程注入代码加上异常处理过程,防止注入代码导致远程程序异常崩溃。并且异常处理过程由windows自动处理,不需要编写额外代码,非常方便。
微软文档所述:
Windows 会将其对 TimerProc 的 调用括起来,其中包含使用和放弃所有异常的异常处理程序。 这是自 Windows 2000 以来的默认行为,但在 Windows 的未来版本中,这一行为可能会更改。
https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-setuserobjectinformationw
验证SetTimer函数自带异常处理
首先必须知道的是由windows消息队列来调用TimerProc函数。具体的说是SetTimer函数带回调函数时,由DispatchMessage函数调用TimerProc函数,且触发异常后,异常以后的代码不会被执行。
新建VS工程,选windows控制台程序,完整代码如下。
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 | #include <windows.h> #include <iostream> using namespace std; // 计时器回调函数 VOID CALLBACK Timerproc( HWND hwnd, // handle to window for timer messages UINT message, // WM_TIMER message UINT idTimer, // timer identifier DWORD dwTime) // current system time { cout << "Timerproc" << endl; int a = 1, b = 0, c = 0; c = a / b; // 这句代码会引发异常,除数为零 cout << c << endl; } int main() { //const HWND hwnd = GetConsoleWindow(); // 获取本进程句柄 const UINT_PTR timer_id = SetTimer(nullptr, // 绑定的窗口句柄 0, // timer ID 0x0, // 倒计时数,毫秒 Timerproc); // 回调函数 int Counter = 0; MSG msg; while (GetMessage(&msg, nullptr, 0, 0)) { ++Counter; cout << "Counter: " << Counter << "; message: " << msg.message << '\n'; DispatchMessage(&msg); } return 0; } |
调试运行,VS输出标签里重复提示除数为零异常,但程序仍然正常运行,没有显示cout << c << endl;
这句代码,说明异常处理程序确实包含了TimerProc函数。
新线程内执行TimerProc函数
控制台程序
远程注入的代码是通过CreateRemoteThread函数执行,也就是新建一个线程。所以先来模拟本地新线程执行情景。
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 | #include <windows.h> #include <iostream> #include <process.h> using namespace std; HANDLE hEvent; // 计时器回调函数 VOID CALLBACK Timerproc( HWND hwnd, // handle to window for timer messages UINT message, // WM_TIMER message UINT idTimer, // timer identifier DWORD dwTime) // current system time { cout << "Timerproc" << endl; KillTimer(hwnd, idTimer); int a = 1, b = 0, c = 0; c = a / b; // 这句代码会引发异常,除数为零 cout << c << endl; SetEvent(hEvent); } DWORD WINAPI thread(_In_ LPVOID lpParameter) { HWND process_hwnd = *(HWND*)lpParameter; cout << "process_hwnd=" << process_hwnd << endl; cout << "thread start PID=" << GetCurrentThreadId() << endl; UINT_PTR timer_id; const HANDLE thread_hwnd = GetCurrentThread(); timer_id = SetTimer(nullptr, 0, 0, nullptr); timer_id = SetTimer(process_hwnd, // 绑定的窗口句柄 timer_id, // timer ID 0x0, // 倒计时数,毫秒 Timerproc); // 回调函数 hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); WaitForSingleObject(hEvent, 5000); cout << "WaitForSingleObject end" << endl; CloseHandle(hEvent); return 0; } int main() { //HANDLE process_handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId()); //cout << "process_handle=" << process_handle << endl; HWND process_hwnd = GetConsoleWindow(); cout << "process_hwnd=" << process_hwnd << endl; const HANDLE thread_handle = CreateThread(nullptr, 0, thread, &process_hwnd, 0, nullptr); cout << "CreateThread thread_handle=" << thread_handle << endl; BOOL bRet; int Counter = 0; MSG msg; while ((bRet = GetMessage(&msg, nullptr, 0, 0)) != 0) { if (bRet == -1) { // handle the error and possibly exit } else { ++Counter; cout << "Counter: " << Counter << "; message: " << msg.message << '\n'; TranslateMessage(&msg); DispatchMessage(&msg); } } return 0; } |
上面的代码无法运行成功,原因是TimeProc函数并未执行。没有执行是因为消息队列没有消息,那么GetMessage就会阻塞进程,不执行while循环体。不进入循环体,DispatchMessage函数就无法调用TimeProc函数。
至于为什么这里SetTimer函数没有触发消息……我也不知道。
窗口程序
控制台程序无法成功运行,但是换窗口程序却可以成功运行。
代码流程:
- WinMain函数是主函数,主函数中创建新线程执行thread函数。
- thread函数中创建事件,新建空计时器。新建空计时器是为了避免覆盖注入进程中已有的计时器(当SetTimer函数的第1、2参数为0时,windows会自动在当前进程创建新的计时器ID)。然后再次设置计时器,传入注入进程句柄、新建空计时器ID、Timerproc回调函数。最后执行WaitForSingleObject函数阻塞等待事件信号,如果Timerproc函数触发异常了,则会超时指定时间后继续执行。
- Timerproc函数可以自动处理异常,需要注意的是触发异常后的代码不会被执行。函数首先关闭计时器,接着直接插入注入代码或调用call函数,最后设置信号为有状态,以便让WaitForSingleObject函数接收信号。
- call函数有没有都可以,单独的call函数方便编程和调试。
改进的代码如下:
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 | #include <iostream> #include <windows.h> using namespace std; static const TCHAR gc_szClassName[] = TEXT("Test"); static const TCHAR gc_szWindowTitle[] = TEXT("Test"); HANDLE hEvent; void call() { int a = 1, b = 0, c = 0; c = a / b; // 这句代码会引发异常,除数为零 cout << c << endl; } // 计时器回调函数 VOID CALLBACK Timerproc( HWND hwnd, // handle to window for timer messages UINT message, // WM_TIMER message UINT idTimer, // timer identifier DWORD dwTime) // current system time { cout << "Timerproc" << endl; if (KillTimer(hwnd, idTimer)) cout << "KillTimer done" << endl; else cout << "KillTimer error" << endl; call(); // 如果函数内异常导致退出,则不会执行下面的代码 SetEvent(hEvent); } DWORD WINAPI thread(_In_ LPVOID lpParameter) { HWND process_hwnd = *(HWND*)lpParameter; cout << "process_hwnd=" << process_hwnd << endl; cout << "thread start PID=" << GetCurrentThreadId() << endl; UINT_PTR timer_id; const HANDLE thread_hwnd = GetCurrentThread(); hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); timer_id = SetTimer(nullptr, 0, 0, nullptr); timer_id = SetTimer(process_hwnd, // 绑定的窗口句柄 timer_id, // timer ID 0x0, // 倒计时数,毫秒 Timerproc); // 回调函数 cout << "timer_id=" << timer_id << endl; WaitForSingleObject(hEvent, 5000); cout << "WaitForSingleObject end" << endl; CloseHandle(hEvent); return 0; } LRESULT CALLBACK WndProc(HWND hWnd, UINT uMessage, WPARAM wParam, LPARAM lParam) { switch (uMessage) { case WM_CREATE: break; case WM_CLOSE: DestroyWindow(hWnd); break; case WM_DESTROY: PostQuitMessage(EXIT_SUCCESS); break; default:; } return DefWindowProc(hWnd, uMessage, wParam, lParam); } int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCommandLine, int nShowCommand) { // define variables HWND hWnd; WNDCLASS wc; MSG msg; // unused parameters UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpszCommandLine); UNREFERENCED_PARAMETER(nShowCommand); // initialize WNDCLASS structure ZeroMemory(&wc, sizeof(wc)); wc.lpfnWndProc = WndProc; wc.hInstance = hInstance; wc.lpszClassName = gc_szClassName; // attempt to register the class if (RegisterClass(&wc) != 0) { // show console AllocConsole(); std::ignore = freopen_s((FILE**)stdout, "CONOUT$", "w", stdout); std::ignore = freopen_s((FILE**)stdout, "CONOUT$", "w", stderr); std::ignore = freopen_s((FILE**)stdout, "CONIN$", "r", stdin); std::cout.clear(); std::clog.clear(); std::cerr.clear(); std::cin.clear(); // attempt to create the window hWnd = CreateWindow(gc_szClassName, gc_szWindowTitle, WS_CAPTION | WS_SYSMENU | WS_VISIBLE, 100, 100, 100, 100, NULL, NULL, hInstance, NULL); cout << "window hwnd=" << hWnd << endl; const HANDLE thread_handle = CreateThread(nullptr, 0, thread, &hWnd, 0, nullptr); int Counter = 0; // retrieve messages while (GetMessage(&msg, NULL, 0, 0) > 0) { ++Counter; std::cout << "Counter: " << Counter << "; message: " << msg.message << '\n'; TranslateMessage(&msg); DispatchMessage(&msg); } // use the return-code from the window return (int)msg.wParam; } return EXIT_FAILURE; } |
将注入函数转为汇编语言
转为汇编语言这一步骤我卡住好几天。转为汇编语言本进程执行很简单,但如果要把thread()、timerproc()、call()三个函数在远程进程运行则有难度。主要难度为注入到远程进程后,由于没有基址重定位,无法执行call系统函数语句CALL KillTimer
,也无法执行call自有函数语句CALL call
。
研究了几天之后发现受限于内联汇编的功能局限性,不能用内联汇编完成目的。所以简单学习了一下asmjit,改用asmjit汇编引擎实现功能。
本进程执行代码
接下来需要将thread()、Timerproc()、call()函数转换为汇编语言。转换后的完整代码如下:
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 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | #include <iostream> #include <windows.h> using namespace std; HANDLE hEvent; void call() { //cout << "func call start" << endl; //int a = 1, b = 0, c = 0; //c = a / b; // 这句代码会引发异常,除数为零 //cout << "func call end" << endl; char start[] = "func call start\n"; char end[] = "func call end\n"; __asm { lea eax, start push eax call printf xor eax, eax div eax // 0除以0 异常语句 lea eax, end push eax call printf } } // 计时器回调函数 VOID CALLBACK Timerproc( HWND hwnd, // handle to window for timer messages UINT message, // WM_TIMER message UINT idTimer, // timer identifier DWORD dwTime) // current system time { //cout << "Timerproc" << endl; //if (KillTimer(hwnd, idTimer)) // cout << "KillTimer done" << endl; //else // cout << "KillTimer error" << endl; //call(); // 如果函数内异常导致退出,则不会执行下面的代码 //SetEvent(hEvent); __asm { push idTimer push hwnd call KillTimer call call //mov dword ptr ds : [0x12345678] , eax; // 把call函数的返回值结果存放到指定的内存地址 push hEvent call SetEvent } } DWORD WINAPI thread(_In_ LPVOID lpParameter) { HWND process_hwnd = *(HWND*)lpParameter; UINT_PTR timer_id; //cout << "process_hwnd=" << process_hwnd << endl; //cout << "thread start PID=" << GetCurrentThreadId() << endl; //const HANDLE thread_hwnd = GetCurrentThread(); ////hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); //timer_id = SetTimer(nullptr, 0, 0, nullptr); //timer_id = SetTimer(process_hwnd, // 绑定的窗口句柄 // timer_id, // timer ID // 0x0, // 倒计时数,毫秒 // Timerproc); // 回调函数 //cout << "timer_id=" << timer_id << endl; //WaitForSingleObject(hEvent, 5000); //cout << "WaitForSingleObject end" << endl; //CloseHandle(hEvent); char start[] = "func thread start\n"; char end[] = "func thread end\n"; __asm { lea eax, start push eax call printf push 0 push 0 push 0 push 0 call CreateEvent mov hEvent, eax push 0 push 0 push 0 push 0 call SetTimer push Timerproc push 0 push eax push process_hwnd call SetTimer mov timer_id, eax push 0x1388; 5000毫秒 push hEvent call WaitForSingleObject push hEvent call CloseHandle lea eax, end push eax call printf } return 0; } LRESULT CALLBACK WndProc(HWND hWnd, UINT uMessage, WPARAM wParam, LPARAM lParam) { switch (uMessage) { case WM_CREATE: break; case WM_CLOSE: DestroyWindow(hWnd); break; case WM_DESTROY: PostQuitMessage(EXIT_SUCCESS); break; default:; } return DefWindowProc(hWnd, uMessage, wParam, lParam); } int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCommandLine, int nShowCommand) { // define variables HWND hWnd; WNDCLASS wc; MSG msg; // unused parameters UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpszCommandLine); UNREFERENCED_PARAMETER(nShowCommand); // initialize WNDCLASS structure ZeroMemory(&wc, sizeof(wc)); wc.lpfnWndProc = WndProc; wc.hInstance = hInstance; wc.lpszClassName = TEXT("class name"); // attempt to register the class if (RegisterClass(&wc) != 0) { AllocConsole(); std::ignore = freopen_s((FILE**)stdout, "CONOUT$", "w", stdout); std::ignore = freopen_s((FILE**)stdout, "CONOUT$", "w", stderr); std::ignore = freopen_s((FILE**)stdout, "CONIN$", "r", stdin); std::cout.clear(); std::clog.clear(); std::cerr.clear(); std::cin.clear(); // attempt to create the window hWnd = CreateWindow(TEXT("class name"), TEXT("window title"), WS_CAPTION | WS_SYSMENU | WS_VISIBLE, 1100, 600, 100, 100, NULL, NULL, hInstance, NULL); cout << "window hwnd=" << hWnd << endl; const HANDLE thread_handle = CreateThread(nullptr, 0, thread, &hWnd, 0, nullptr); int Counter = 0; // retrieve messages while (GetMessage(&msg, NULL, 0, 0) > 0) { ++Counter; std::cout << "Counter: " << Counter << "; message: " << msg.message << '\n'; TranslateMessage(&msg); DispatchMessage(&msg); } // use the return-code from the window return (int)msg.wParam; } return EXIT_FAILURE; } |
运行成功。实际应用时可以把call函数里的两个调试printf语句删了。
远程进程执行代码
代码只适用于x86程序,如果你系统是64位系统,测试用的笔记本程序需要使用c:\windows\SysWOW64\notepad.exe
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 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | #include <algorithm> #include <format> #include <iomanip> #include <windows.h> #include <iostream> #include <psapi.h> #include <string> #include <asmjit/asmjit.h> using namespace std; using namespace asmjit; using namespace x86; struct THREAD_PARAM { DWORD ret_value; // alloc_addr + 0x0 FARPROC settimer; // alloc_addr + 0x4 FARPROC killtimer; // alloc_addr + 0x8 HWND remote_hwnd; // alloc_addr + 0xC DWORD timerproc; // alloc_addr + 0x10 DWORD timer_id; // alloc_addr + 0x14 DWORD custom_call; // alloc_addr + 0x18 }; /** * \brief 获取指定进程中的模块函数句柄 * \param hRemoteProcess 指定进程句柄 * \param lpModuleName 指定函数名所在的模块名,大小写任意 * \param lpProcName 指定要获取的函数名,大小写必须匹配 * \return 成功则返回指定的函数地址,未找到返回空指针 * \note 可至微软learn网站查询函数所在的模块 */ FARPROC GetRemoteModule(_In_ const HANDLE hRemoteProcess, _In_ const LPCWSTR lpModuleName, _In_ const LPCSTR lpProcName) { // https://learn.microsoft.com/zh-cn/windows/win32/psapi/enumerating-all-modules-for-a-process // 获取远程模块信息 HMODULE hModule[MAX_PATH]; DWORD cbNeeded; if (!EnumProcessModules(hRemoteProcess, hModule, MAX_PATH, &cbNeeded)) return nullptr; // 遍历枚举到的模块 for (size_t i = 0; i < cbNeeded / sizeof(HMODULE); i++) { TCHAR szModName[MAX_PATH]; // Get the full path to the module's file. if (GetModuleFileNameEx(hRemoteProcess, hModule[i], szModName, sizeof(szModName) / sizeof(TCHAR))) { // 转为wstring字符串,并全部转换为小写以便比较 wstring ModName(szModName); wstring ModuleName(lpModuleName); ranges::transform(ModName, ModName.begin(), ::tolower); ranges::transform(ModuleName, ModuleName.begin(), ::tolower); //wcout << format(L"\t0x{:08X} {}\n", (DWORD)hModule[i], ModName); // 如果是要找的模块 if (ModName.find(ModuleName) != wstring::npos) // 如果GetProcAddress函数成功,则返回值是导出的函数或变量的地址。如果GetProcAddress函数失败,则返回值为 NULL。 要获得更多的错误信息,请调用 GetLastError。 return GetProcAddress(hModule[i], lpProcName); } } return nullptr; } int main() { const HWND remote_hwnd = FindWindow(L"Notepad", nullptr); cout << "remote_hwnd=" << remote_hwnd << endl; DWORD remote_pid = 0; GetWindowThreadProcessId(remote_hwnd, &remote_pid); cout << "remote_pid=" << remote_pid << endl; const HANDLE remote_handle = OpenProcess(PROCESS_ALL_ACCESS, false, remote_pid); if (!remote_handle) return 0; // 获取远程进程模块函数信息 auto settimer = GetRemoteModule(remote_handle, TEXT("User32.dll"), "SetTimer"); auto killtimer = GetRemoteModule(remote_handle, TEXT("User32.dll"), "KillTimer"); //cout << format("SetTimer addr=0x{:08X}\n", (DWORD)settimer); //cout << format("KillTimer addr=0x{:08X}\n", (DWORD)killtimer); // 申请远程空间,写入和执行权限 DWORD alloc_addr = (DWORD)VirtualAllocEx(remote_handle, nullptr, MAX_PATH, MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (!alloc_addr) { cout << "GetLastError()=" << GetLastError() << endl; return 0; } cout << setw(8) << setfill('0') << hex << "VirtualAlloc addr=0x" << alloc_addr << endl; DWORD addr_param = alloc_addr; // 参数内存地址 DWORD addr_call = alloc_addr + 0x21; // 函数地址 // 构造和写入函数 THREAD_PARAM thread_param{}; thread_param.remote_hwnd = remote_hwnd; thread_param.settimer = settimer; thread_param.killtimer = killtimer; // 生成代码 JitRuntime rt; auto a = x86::Assembler(); CodeHolder code; code.init(rt.environment(), rt.cpuFeatures()); code.attach(&a); // 构造thread函数 a.push(0); a.push(0); a.push(0); a.push(0); a.call(dword_ptr(alloc_addr + 0x4)); // settimer a.mov(dword_ptr(alloc_addr + 0x14), eax); //timer_id a.push(dword_ptr(alloc_addr + 0x10)); // timerproc a.push(0); a.push(eax); a.push(dword_ptr(alloc_addr + 0xC)); // remote_hwnd a.call(dword_ptr(alloc_addr + 0x4)); // settimer a.ret(); CodeBuffer& thread = code.textSection()->buffer(); // 获取对应的机器码 // 打印生成的机器码 //cout << "assembly="; //for (auto& i : thread) // cout << format("{:02X} ", i); //cout << endl; size_t nSize = thread.size(); cout << format("线程函数写入地址=0x{:08X} 大小=0x{:08X}\n", addr_call, nSize); WriteProcessMemory(remote_handle, (LPVOID)addr_call, thread.data(), nSize, nullptr); DWORD thread_addr = addr_call; // 记录线程函数地址 addr_call = addr_call + nSize + 0x8; // 重置并复用CodeHolder变量code code.reset(); code.init(rt.environment(), rt.cpuFeatures()); code.attach(&a); // 构造timerproc a.push(dword_ptr(alloc_addr + 0x14)); // idTimer a.push(dword_ptr(alloc_addr + 0xC)); // hwnd a.call(dword_ptr(alloc_addr + 0x8)); // call KillTimer a.call(dword_ptr(alloc_addr + 0x18)); // custom_call a.ret(); CodeBuffer& timerproc = code.textSection()->buffer(); thread_param.timerproc = addr_call; // 记录therad地址 nSize = timerproc.size(); cout << format("回调函数写入地址=0x{:08X} 大小=0x{:08X}\n", addr_call, nSize); WriteProcessMemory(remote_handle, (LPVOID)addr_call, timerproc.data(), nSize, nullptr); addr_call = addr_call + nSize + 0x8; // 重置并复用CodeHolder变量code code.reset(); code.init(rt.environment(), rt.cpuFeatures()); code.attach(&a); // 构造自定函数 a.xor_(eax, eax); a.div(eax); // 异常语句 a.mov(dword_ptr(alloc_addr), eax); // 写入返回值到alloc_addr,程序读取alloc_addr地址的数值即可知道返回值 a.ret(); CodeBuffer& custom_call = code.textSection()->buffer(); thread_param.custom_call = addr_call; // 记录custom_call地址 nSize = custom_call.size(); cout << format("自定函数写入地址=0x{:08X} 大小=0x{:08X}\n", addr_call, nSize); WriteProcessMemory(remote_handle, (LPVOID)addr_call, custom_call.data(), nSize, nullptr); addr_call = addr_call + nSize + 0x8; // 写入参数 cout << format("线程参数写入地址=0x{:08X} 大小=0x{:08X}\n", addr_param, sizeof(THREAD_PARAM)); WriteProcessMemory(remote_handle, (LPVOID)addr_param, &thread_param, sizeof(THREAD_PARAM), nullptr); const HANDLE hRemoteThread = CreateRemoteThread(remote_handle, nullptr, 0, (LPTHREAD_START_ROUTINE)thread_addr, (LPVOID)addr_param, 0, nullptr); //system("pause"); return 0; } |
文章评论