Leftme的文章

  • 突破SESSION0隔离的的远线程注入DLL技术剖析

    背景之前写过 “传统的远线程注入DLL技术剖析“ 这篇文章,里面主要介绍使用传统的 CreateRemoteThread 函数来实现向指定进程注入 DLL。但是,这种方法有一个问题就是,不能突破 SESSION 0 隔离。也就是不能成功将指定 DLL 注入到系统服务进程中。
    现在,我们来介绍另一种的远线程注入 DLL,它可以突破 SESSION 0 隔离,成功注入 DLL。现在我就把实现过程和原理整理成文档,分享给大家。
    实现原理和传统的 CreateRemoteThread 函数实现的远线程注入 DLL 的唯一一个区别就是,我们这次是使用 Z我Create ThreadEx 函数来实现创建远线程。其它的均和传统的实现方法是一样的,原理也是一样的。
    使用 ZwCreateThreadEx 函数可以突破 SESSION 0 隔离,成功将 DLL 注入到 SESSION 0 的系统服务进程中。其中,ZwCreateThreadEx 在 ntdll.dll 中并没有声明,所以我们需要使用 GetProcAddress 从 ntdll.dll 中获取该函数的导出地址。
    64 位下,ZwCreateThreadEx 函数声明为:
    DWORD WINAPI ZwCreateThreadEx( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, LPVOID ObjectAttributes, HANDLE ProcessHandle, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, ULONG CreateThreadFlags, SIZE_T ZeroBits, SIZE_T StackSize, SIZE_T MaximumStackSize, LPVOID pUnkown);
    32 位下,ZwCreateThreadEx 函数声明为:
    DWORD WINAPI ZwCreateThreadEx( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, LPVOID ObjectAttributes, HANDLE ProcessHandle, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, BOOL CreateSuspended, DWORD dwStackSize, DWORD dw1, DWORD dw2, LPVOID pUnkown);
    编码实现// 使用 ZwCreateThreadEx 实现远线程注入BOOL ZwCreateThreadExInjectDll(DWORD dwProcessId, char *pszDllFileName){ HANDLE hProcess = NULL; SIZE_T dwSize = 0; LPVOID pDllAddr = NULL; FARPROC pFuncProcAddr = NULL; HANDLE hRemoteThread = NULL; DWORD dwStatus = 0; // 打开注入进程,获取进程句柄 hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (NULL == hProcess) { ShowError("OpenProcess"); return FALSE; } // 在注入进程中申请内存 dwSize = 1 + ::lstrlen(pszDllFileName); pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE); if (NULL == pDllAddr) { ShowError("VirtualAllocEx"); return FALSE; } // 向申请的内存中写入数据 if (FALSE == ::WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL)) { ShowError("WriteProcessMemory"); return FALSE; } // 加载 ntdll.dll HMODULE hNtdllDll = ::LoadLibrary("ntdll.dll"); if (NULL == hNtdllDll) { ShowError("LoadLirbary"); return FALSE; } // 获取LoadLibraryA函数地址 pFuncProcAddr = ::GetProcAddress(::GetModuleHandle("Kernel32.dll"), "LoadLibraryA"); if (NULL == pFuncProcAddr) { ShowError("GetProcAddress_LoadLibraryA"); return FALSE; } // 获取ZwCreateThread函数地址#ifdef _WIN64 typedef DWORD(WINAPI *typedef_ZwCreateThreadEx)( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, LPVOID ObjectAttributes, HANDLE ProcessHandle, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, ULONG CreateThreadFlags, SIZE_T ZeroBits, SIZE_T StackSize, SIZE_T MaximumStackSize, LPVOID pUnkown);#else typedef DWORD(WINAPI *typedef_ZwCreateThreadEx)( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, LPVOID ObjectAttributes, HANDLE ProcessHandle, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, BOOL CreateSuspended, DWORD dwStackSize, DWORD dw1, DWORD dw2, LPVOID pUnkown);#endif typedef_ZwCreateThreadEx ZwCreateThreadEx = (typedef_ZwCreateThreadEx)::GetProcAddress(hNtdllDll, "ZwCreateThreadEx"); if (NULL == ZwCreateThreadEx) { ShowError("GetProcAddress_ZwCreateThread"); return FALSE; } // 使用 ZwCreateThreadEx 创建远线程, 实现 DLL 注入 dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, 0, 0, 0, NULL); if (NULL == hRemoteThread) { ShowError("ZwCreateThreadEx"); return FALSE; } // 关闭句柄 ::CloseHandle(hProcess); ::FreeLibrary(hNtdllDll); return TRUE;}
    程序测试我们对 svchost.exe 进程,处于 SESSION 0 中,以管理员权限运行我们的程序,注入我们的测试 DLL,DLL 成功注入到 svchost.exe 进程空间中:

    总结要注意以管理员权限运行程序,因为由于 OpenProcess 函数的缘故,打开高权限的进程时,会因进程权限不足而无法打开进程,获取进程句柄。其中,我们将当前进程令牌权限提升至 SE_DEBUG_NAME权限,具体的进程令牌权限提升可以参考 “使用AdjustTokenPrivileges函数提升进程访问令牌的权限“ 这篇文章。
    与传统的 CreateRemoteThread 相比,就是创建远线程时使用的函数不同之外,其它都是相同的,而且原理部分也是相同的。
    其中,要特别注意一点就是,ZwCreateThreadEx 函数在 32 位和 64 位系统下,它的函数声明中的参数是有区别的,一定要区分开来。
    参考参考自《Windows黑客编程技术详解》一书
    9  留言 2019-01-12 20:44:42
  • 使用AdjustTokenPrivileges函数提升进程访问令牌的权限

    背景在我们编程实现一些系统操作的时候,往往要求我们执行操作的进程拥有足够的权限方可成功操作。比如,我们使用 ExitWindows 函数实现关机或重启操作的时候,就要求我们的进程要有 SE_SHUTDOWN_NAME 的权限,否则,会忽视不执行操作。这时,我们唯一能够做的,就是按照要求,提升我们进程的权限。
    本文要讲解的就是这样一个程序,编程实现提升进程访问令牌的权限。主要涉及到 3 个WIN32 API函数的使用:OpenProcessToken、LookupPrivilegeValue 以及 AdjustTokenPrivileges 函数。
    现在,我就把提取权限的实现过程整理成文档,分享给大家。
    函数介绍OpenProcessToken 函数
    打开与进程关联的访问令牌。
    函数声明
    BOOL WINAPI OpenProcessToken( _In_ HANDLE ProcessHandle, _In_ DWORD DesiredAccess, _Out_ PHANDLE TokenHandle);
    参数

    ProcessHandle [in]要打开访问令牌的进程的句柄。 该进程必须具有PROCESS_QUERY_INFORMATION访问权限。DesiredAccess [in]指定一个访问掩码,指定访问令牌的请求类型。 这些请求的访问类型与令牌的自由访问控制列表(DACL)进行比较,以确定哪些访问被授予或拒绝。TokenHandle [出]指向一个句柄的指针,用于标识当函数返回时新打开的访问令牌。
    返回值

    如果函数成功,则返回值不为零。如果函数失败,返回值为零。 要获取扩展错误信息,请调用GetLastError。

    LookupPrivilegeValue 函数
    查看系统权限的特权值,返回信息到一个LUID结构体里。
    函数声明
    BOOL WINAPI LookupPrivilegeValue( _In_opt_ LPCTSTR lpSystemName, _In_ LPCTSTR lpName, _Out_ PLUID lpLuid);
    参数

    lpSystemName [in]指向以NULL结尾的字符串的指针,该字符串是指向要获取特权值的系统名称。 如果指定了空字符串,则该函数尝试在本地系统上查找特权名称。lpName [in]指向空终止字符串的指针,指定特权的名称,在Winnt.h头文件中定义。 例如,该参数可以指定常量SE_SECURITY_NAME或其相应的字符串“SeSecurityPrivilege”。lpLuid [out]指向LUID变量的指针,该变量接收由lpSystemName参数指定的系统上已知权限的LUID。
    返回值

    如果函数成功,函数将返回非零值。如果函数失败,则返回零。 要获取扩展错误信息,请调用GetLastError。

    AdjustTokenPrivileges 函数
    启用或禁用指定的访问令牌中的权限。 在访问令牌中启用或禁用权限需要TOKEN_ADJUST_PRIVILEGES访问。
    函数声明
    BOOL WINAPI AdjustTokenPrivileges( _In_ HANDLE TokenHandle, _In_ BOOL DisableAllPrivileges, _In_opt_ PTOKEN_PRIVILEGES NewState, _In_ DWORD BufferLength, _Out_opt_ PTOKEN_PRIVILEGES PreviousState, _Out_opt_ PDWORD ReturnLength);
    参数

    TokenHandle [in]访问令牌的句柄,其中包含要修改的权限。 句柄必须有TOKEN_ADJUST_PRIVILE GES访问令牌。 如果PreviousState参数不为NULL,则该句柄还必须具有TOKEN_Q UERY访问权限。
    DisableAllPrivileges [in]指定该功能是否禁用所有令牌的权限。 如果此值为TRUE,该函数将禁用所有权限,并忽略NewState参数。 如果为FALSE,则该函数将根据NewState参数指向的信息修改权限。
    NewState [in]指向TOKEN_PRIVILEGES结构的指针,该结构指定特权数组及其属性。 如果DisableAllPrivileges参数为FALSE,则AdjustTokenPrivileges函数将启用,禁用或删除令牌的这些权限。 下表描述了基于特权属性的AdjustTokenPrivileges函数执行的操作:




    VALUE
    MEANING




    SE_PRIVILEGE_ENABLED
    启用此特权


    SE_PRIVILEGE_REMOVED
    特权从令牌中的特权列表中删除


    None
    禁用此特权




    BufferLength [in]指定由PreviousState参数指向的缓冲区的大小(以字节为单位)。如果PreviousState参数为NULL,则此参数可以为 0。
    PreviousState[out]指向缓冲区的指针,该函数使用包含函数修改的任何特权的先前状态的TOKEN_PRIVILEGES结构填充。也就是说,如果此功能已修改特权,则该特权及其先前状态将包含在由PreviousState引用的TOKEN_PRIVILEGES结构中。如果TOKEN_PRIVILEGES的PrivilegeCount成员为零,则此功能不会更改任何权限。此参数可以为NULL。如果指定的缓冲区太小,无法接收修改权限的完整列表,则该函数将失败,并且不调整任何权限。在这种情况下,该函数将ReturnLength参数指向的变量设置为保存已修改权限的完整列表所需的字节数。
    ReturnLength [out]指向一个变量的指针,该变量接收由PreviousState参数指向的缓冲区所需的大小(以字节为单位)。如果PreviousState为NULL,则此参数可以为NULL。

    返回值

    如果函数成功,则返回值不为零。 要确定该函数是否调整了所有指定的权限,请调用GetLastError,该函数在函数成功时返回以下值之一:



    VALUE
    MEANING




    ERROR_SUCCESS
    该函数调整所有指定的权限


    ERROR_NOT_ALL_ASSIGNED
    该标记没有在NewState参数中指定的一个或多个特权。 即使没有调整任何权限,该功能也可能会以此错误值成功。 PreviousState参数指示已调整的权限。





    如果函数失败,返回值为零。 要获取扩展错误信息,请调用GetLastError。

    实现过程首先,我们需要调用 OpenProcessToken 函数打开指定进程令牌,并获取 TOKEN_ADJUST_PRIVILEGES 权限的令牌句柄。之所以要获取进程令牌权限为 TOKEN_ADJUST_PRIVILEGES,是因为 AdjustTokenPrivileges 函数,要求要有此权限,方可修改进程令牌的访问权限。
    其中,第 1 个参数表示要打开进程令牌的进程句柄;第 2 个参数表示我们对进程令牌具有的权限,TOKEN_ADJUST_PRIVILEGES就表示,我们有修改进程令牌的权限;第 3 个参数表示返回的进程令牌句柄。
    //打开进程令牌并获取具有 TOKEN_ADJUST_PRIVILEGES 权限的进程令牌句柄bRet = ::OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES, &hToken);if (FALSE == bRet){ ShowError("OpenProcessToken"); return FALSE;}
    然后,我们调用 LookupPrivilegeValue 函数,获取本地系统指定特权名称的LUID值,这个LUID值就相当于该特权的身份标号。
    其中,第 1 个参数表示系统,NULL表示本地系统,即要获取本地系统的指定特权的LUID值;第 2 个参数表示特权名称;第 3 个参数表示获取到的LUID返回值。
    // 获取本地系统的 pszPrivilegesName 特权的LUID值bRet = ::LookupPrivilegeValue(NULL, pszPrivilegesName, &luidValue);if (FALSE == bRet){ ShowError("LookupPrivilegeValue"); return FALSE;}
    接着,我们就开始对 TOKEN_PRIVILEGES 进程令牌特权结构体进行赋值设置,设置设置新特权的数量、特权对应的LUID值以及特权的属性状态。其中,tokenPrivileges.PrivilegeCount表示设置新特权的特权数量;tokenPrivileges.Privileges[i].Luid表示第 i 个特权对应的LUID值;tokenPrivileges.Privileges[0].Attributes表示特权的属性;SE_PRIVILEGE_ENABLED就表示启用该特权。
    // 设置提升权限信息tokenPrivileges.PrivilegeCount = 1;tokenPrivileges.Privileges[0].Luid = luidValue;tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    最后,我们调用 AdjustTokenPrivileges 函数对进程令牌的特权进行修改,将上面设置好的新特权设置到进程令牌中。
    其中,第 1个参数表示进程令牌;第 2 个参数表示能是否禁用所有令牌的权限,FALSE则不禁用;第 3个参数是新设置的特权,指向设置好的令牌特权结构体;第 4 个参数表示返回上一个特权数据缓冲区的大小,不获取,则可以设为 0;第 5 个参数表示返回上一个特权数据缓冲区,不接收返回数据,可以设为 NULL;第 6 个参数表示接收返回上一个特权数据缓冲区应该有的大小。
    // 提升进程令牌访问权限bRet = ::AdjustTokenPrivileges(hToken, FALSE, &tokenPrivileges, 0, NULL, NULL);if (FALSE == bRet){ ShowError("AdjustTokenPrivileges"); return FALSE;}
    但是,需要注意的是,AdjustTokenPrivileges 返回 TRUE,并不代表特权就设置成功,还需要使用 GetLastError 来判断错误吗返回值。若错误码返回值为ERROR_SUCCESS,则所有特权设置成功;若为 ERROR_NOT_ALL_ASSIGNED,则表示并不是所有特权都设置成功。
    dwRet = ::GetLastError();if (ERROR_SUCCESS == dwRet){ return TRUE;}else if (ERROR_NOT_ALL_ASSIGNED == dwRet){ ShowError("ERROR_NOT_ALL_ASSIGNED"); return FALSE;}
    换句话说,如果你只提升了一个特权,且错误码为ERROR_NOT_ALL_ASSIGNED,那么这就是说明提升失败了。如果程序运行在 Win7 或者 Win7 以上版本的操作系统,可以试着以管理员身份运行程序,这样就可以成功提升进程令牌的访问权限。
    编码实现BOOL EnbalePrivileges(HANDLE hProcess, char *pszPrivilegesName){ HANDLE hToken = NULL; LUID luidValue = {0}; TOKEN_PRIVILEGES tokenPrivileges = {0}; BOOL bRet = FALSE; DWORD dwRet = 0; // 打开进程令牌并获取进程令牌句柄 bRet = ::OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES, &hToken); if (FALSE == bRet) { ShowError("OpenProcessToken"); return FALSE; } // 获取本地系统的 pszPrivilegesName 特权的LUID值 bRet = ::LookupPrivilegeValue(NULL, pszPrivilegesName, &luidValue); if (FALSE == bRet) { ShowError("LookupPrivilegeValue"); return FALSE; } // 设置提升权限信息 tokenPrivileges.PrivilegeCount = 1; tokenPrivileges.Privileges[0].Luid = luidValue; tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; // 提升进程令牌访问权限 bRet = ::AdjustTokenPrivileges(hToken, FALSE, &tokenPrivileges, 0, NULL, NULL); if (FALSE == bRet) { ShowError("AdjustTokenPrivileges"); return FALSE; } else { // 根据错误码判断是否特权都设置成功 dwRet = ::GetLastError(); if (ERROR_SUCCESS == dwRet) { return TRUE; } else if (ERROR_NOT_ALL_ASSIGNED == dwRet) { ShowError("ERROR_NOT_ALL_ASSIGNED"); return FALSE; } } return FALSE;}
    总结要特别注意一点,如果 AdjustTokenPrivileges 函数执行的返回值为 TRUE,但是使用 GetLastError 获取错误码却是 ERROR_NOT_ALL_ASSIGNED ,则表示并没有将所有设置的特权全部提升成功。换句话说,如果你只提升了一个特权,那么这就是说明提升失败了。如果程序运行在 Windows7 或者 Windows7 以上版本的操作系统,可以试着以管理员身份运行程序,这样就可以成功提升进程令牌的访问权限了。
    参考参考自《Windows黑客编程技术详解》一书
    1  留言 2019-01-12 11:57:43
  • 传统的远线程注入DLL技术剖析

    背景想必很多人应该都听说过远线程注入DLL技术这个概念,的确,这是一个很巧妙,也很经典的DLL注入技术。为何说是巧妙,等你看完这篇文章就了解了。
    本文讲解的是传统的远线程注入方法,也就是使用 CreateRemoteThread 函数实现的。那么,之所以说是传统,是因为在讲完传统的远线程注入方法后,我们会介绍目前最新的远线程注入方式,注入功能比传统的还要强大。
    现在,我就先讲解传统的远线程注入,把实现过程和原理整理成文档,分享给大家。
    函数介绍OpenProcess 函数
    打开现有的本地进程对象。
    函数声明
    HANDLE WINAPI OpenProcess( _In_ DWORD dwDesiredAccess, _In_ BOOL bInheritHandle, _In_ DWORD dwProcessId);
    参数

    dwDesiredAccess [in]访问进程对象。此访问权限针对进程的安全描述符进行检查。此参数可以是一个或多个进程访问权限。如果调用该函数的进程启用了SeDebugPrivilege权限,则无论安全描述符的内容如何,都会授予所请求的访问权限。bInheritHandle [in]若此值为TRUE,则此进程创建的进程将继承该句柄。否则,进程不会继承此句柄。dwProcessId [in]要打开的本地进程的标识符。如果指定的进程是系统进程(0x00000000),则该函数失败,最后一个错误代码为ERROR_INVALID_PARAMETER。如果指定的进程是空闲进程或CSRSS进程之一,则此功能将失败,并且最后一个错误代码为ERROR_ACCESS_DENIED,因为它们的访问限制会阻止用户级代码打开它们。如果您使用GetCurrentProcessId作为此函数的参数,请考虑使用GetCurrentProcess而不是OpenProcess,以提高性能。
    返回值

    如果函数成功,则返回值是指定进程的打开句柄。如果函数失败,返回值为NULL。 要获取扩展错误信息,请调用GetLastError。

    VirtualAllocEx 函数
    在指定进程的虚拟地址空间内保留,提交或更改内存区域的状态。 该函数初始化其分配给零的内存。
    函数声明
    LPVOID WINAPI VirtualAllocEx( _In_ HANDLE hProcess, _In_opt_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flAllocationType, _In_ DWORD flProtect);
    参数

    hProcess [in]过程的句柄。该函数在该进程的虚拟地址空间内分配内存。句柄必须具有PROCESS_VM_OPERATION权限。有关更多信息,请参阅流程安全和访问权限。
    lpAddress [in]指定要分配的页面的所需起始地址的指针。如果您正在保留内存,则该函数会将该地址舍入到分配粒度的最接近的倍数。如果您提交已经保留的内存,该功能会将该地址舍入到最接近的页面边界。要确定页面的大小和主机上的分配粒度,请使用GetSystemInfo函数。如果lpAddress为NULL,则该函数确定在哪里分配该区域。
    dwSize [in]要分配的内存大小,以字节为单位。如果lpAddress为NULL,则函数将dwSize循环到下一个页面边界。如果lpAddress不为NULL,则该函数将从lpAddress到lpAddress + dwSize的范围内分配包含一个或多个字节的所有页面。这意味着,例如,跨越页面边界的2字节范围会导致功能分配两个页面。
    flAllocationType [in]内存分配类型。此参数必须包含以下值之一:




    VALUE
    MEANING




    MEM_COMMIT
    为指定的预留内存页分配内存费用(从磁盘上的内存和分页文件的总体大小)。 该函数还保证当调用者稍后初次访问存储器时,内容将为零。 除非/直到虚拟地址被实际访问,实际的物理页面才被分配


    MEM_RESERVE
    保留进程的虚拟地址空间的范围,而不会在内存或磁盘上的分页文件中分配任何实际物理存储


    MEM_RESET
    表示由lpAddress和dwSize指定的内存范围内的数据不再受关注。 页面不应从页面文件中读取或写入页面文件。 然而,内存块将在以后再次被使用,所以不应该被分解。 该值不能与任何其他值一起使用


    MEM_RESET_UNDO
    只能在早期成功应用了MEM_RESET的地址范围上调用MEM_RESET_UNDO。 它指示由lpAddress和dwSize指定的指定内存范围内的数据对呼叫者感兴趣,并尝试反转MEM_RESET的影响。 如果功能成功,则表示指定地址范围内的所有数据都是完整的。 如果功能失败,地址范围中的至少一些数据已被替换为零




    flProtect [in]要分配的页面区域的内存保护。 如果页面被提交,您可以指定任何一个内存保护常量。如果lpAddress指定了一个地址,flProtect不能是以下值之一:PAGE_NOACCESSPAGE_GUARDPAGE_NOCACHEPAGE_WRITECOMBINE
    返回值

    如果函数成功,则返回值是分配的页面区域的基址。如果函数失败,返回值为NULL。 要获取扩展错误信息,请调用GetLastError。

    WriteProcessMemory 函数
    在指定的进程中将数据写入内存区域。 要写入的整个区域必须可访问或操作失败。
    函数声明
    BOOL WINAPI WriteProcessMemory( _In_ HANDLE hProcess, _In_ LPVOID lpBaseAddress, _In_ LPCVOID lpBuffer, _In_ SIZE_T nSize, _Out_ SIZE_T *lpNumberOfBytesWritten);
    参数

    hProcess [in]要修改的进程内存的句柄。 句柄必须具有PROCESS_VM_WRITE和PROCESS_VM_OPERATION访问进程。lpBaseAddress [in]指向写入数据的指定进程中的基地址的指针。 在数据传输发生之前,系统会验证指定大小的基地址和内存中的所有数据是否可以进行写入访问,如果不可访问,则该函数将失败。lpBuffer [in]指向缓冲区的指针,其中包含要写入指定进程的地址空间的数据。nSize [in]要写入指定进程的字节数。lpNumberOfBytesWritten [out]指向变量的指针,该变量接收传输到指定进程的字节数。 此参数是可选的。 如果lpNumberOfBytesWritten为NULL,则忽略该参数。
    返回值

    如果函数成功,则返回值不为零。如果函数失败,返回值为0(零)。 要获取扩展错误信息,请调用GetLastError。

    CreateRemoteThread 函数
    创建在另一个进程的虚拟地址空间中运行的线程。使用CreateRemoteThreadEx函数创建在另一个进程的虚拟地址空间中运行的线程,并可选地指定扩展属性。
    函数声明
    HANDLE WINAPI CreateRemoteThread( _In_ HANDLE hProcess, _In_ LPSECURITY_ATTRIBUTES lpThreadAttributes, _In_ SIZE_T dwStackSize, _In_ LPTHREAD_START_ROUTINE lpStartAddress, _In_ LPVOID lpParameter, _In_ DWORD dwCreationFlags, _Out_ LPDWORD lpThreadId);
    参数

    hProcess [in]要创建线程的进程的句柄。 句柄必须具有PROCESS_CREATE_THREAD,PROCESS_QUERY_INFORMATION,PROCESS_VM_OPERATION,PROCESS_VM_WRITE和PROCESS_VM_READ访问权限,如果某些平台上没有这些权限,可能会失败。 有关更多信息,请参阅流程安全和访问权限。lpThreadAttributes [in]指向SECURITY_ATTRIBUTES结构的指针,该结构指定新线程的安全描述符,并确定子进程是否可以继承返回的句柄。 如果lpThreadAttributes为NULL,则线程将获得默认安全描述符,并且该句柄不能被继承。 线程的默认安全描述符中的访问控制列表(ACL)来自创建者的主令牌。dwStackSize [in]堆栈的初始大小,以字节为单位。 系统将此值循环到最近的页面。 如果此参数为0(零),则新线程使用可执行文件的默认大小。 有关更多信息,请参阅线程堆栈大小。lpStartAddress [in]指向由线程执行的类型为LPTHREAD_START_ROUTINE的应用程序定义函数的指针,并表示远程进程中线程的起始地址。 该功能必须存在于远程进程中。 有关更多信息,请参阅ThreadProc。lpParameter [in]指向要传递给线程函数的变量的指针。dwCreationFlags [in]控制线程创建的标志。若是 0,则表示线程在创建后立即运行。lpThreadId [out]指向接收线程标识符的变量的指针。如果此参数为NULL,则不返回线程标识符。
    返回值

    如果函数成功,则返回值是新线程的句柄。如果函数失败,返回值为NULL。 要获取扩展错误信息,请调用GetLastError。

    实现原理远线程注入 DLL 中的远线程,是因为使用的关键函数是 CreateRemoteThread,来在其它的进程空间中创建一个线程。那么,它为何能够使其它进程加载一个 DLL,实现 DLL 注入呢?接下来我就为大家一一分析。
    首先,我们加载一个 DLL,通常使用 LoadLibrary 函数来实现 DLL 的动态加载。那么,先来看下 LoadLibrary 函数的声明:
    HMODULE WINAPI LoadLibrary( _In_ LPCTSTR lpFileName);
    从上面的函数声明可以知道,LoadLibrary 函数的参数只有一个,传递的是要加载的 DLL 的路径字符串。
    然后,我们再看下创建远线程的函数 CreateRemoteThread 的函数声明:
    HANDLE WINAPI CreateRemoteThread( _In_ HANDLE hProcess, _In_ LPSECURITY_ATTRIBUTES lpThreadAttributes, _In_ SIZE_T dwStackSize, _In_ LPTHREAD_START_ROUTINE lpStartAddress, _In_ LPVOID lpParameter, _In_ DWORD dwCreationFlags, _Out_ LPDWORD lpThreadId);
    我们可以从声明中知道,CreateRemoteThread 需要传递的是目标进程空间的多线程函数地址,以及多线程的参数。
    接下来,我们将上述两者结合,开始大胆设想一下,如果,我们能够获取目标进程的 LoadLibrary 函数的地址,而且还能够获取目标进程空间中某个 DLL 路径的字符串地址,那么,将LoadLibrary 函数的地址作为多线程函数的地址,某个 DLL 路径字符串作为多线程函数的参数,传递给 CreateRemoteThread 函数在目标进程空间创建一个多线程,这样能不能创建成功呢?答案是可以的。这样,就可以在目标进程空间中创建一个多线程,这个多线程就是 LoadLibrary 函数加载 DLL。
    那么,这样远线程注入的原理大概就了解了吧。那么要实现远线程注入 DLL,还需要解决以下两个问题:一是目标进程空间的 LoadLibrary 函数地址是多少呢?二是如何向目标进程空间中写入 DLL 路径字符串数据呢?
    对于第一个问题,我们知道由于机制随机化ASLR(Address space layout randomization),导致每次开机时加载的系统 DLL 的加载基址都不一样,从而导致了 DLL 的导出函数的地址也都不一样。即使如此,但要注意一个关键的一个知识点就是:

    有些系统DLL中的指令是Position dependent的,要求所有进程中必须一致。比如kernel32中的新线程入口,ntdll中的异常处理入口等。其实这个地址只是要求系统启动之后必须固定,如果系统重新启动,其地址可以不同。
    Copy-On-Write机制,不改多进程共享,改写内容的页系统会立即给当前进程复制一份,这样这个地址与其它进程中相同地址所映射的物理内存就不同了,怎么改都不会影响其它进程。

    也就是说,虽然不同进程,但是其 Kernel32.dll 的加载基址是相同的,也就是说,自己程序空间的 LoadLibrary 函数地址和其它进程空间的 LoadLibrary 函数地址相同。
    对于第二个问题,我们可以直接调用 VirtualAllocEx 函数在目标进程空间中申请一块内存,然后再调用 WriteProcessMemory 函数将指定的 DLL 路径写入到目标进程空间中。
    这样,我们就可以调用 CreateRemoteThread 函数,实现远线程注入DLL了。
    编码实现// 使用 CreateRemoteThread 实现远线程注入BOOL CreateRemoteThreadInjectDll(DWORD dwProcessId, char *pszDllFileName){ HANDLE hProcess = NULL; DWORD dwSize = 0; LPVOID pDllAddr = NULL; FARPROC pFuncProcAddr = NULL; // 打开注入进程,获取进程句柄 hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (NULL == hProcess) { ShowError("OpenProcess"); return FALSE; } // 在注入进程中申请内存 dwSize = 1 + ::lstrlen(pszDllFileName); pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE); if (NULL == pDllAddr) { ShowError("VirtualAllocEx"); return FALSE; } // 向申请的内存中写入数据 if (FALSE == ::WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL)) { ShowError("WriteProcessMemory"); return FALSE; } // 获取LoadLibraryA函数地址 pFuncProcAddr = ::GetProcAddress(::GetModuleHandle("kernel32.dll"), "LoadLibraryA"); if (NULL == pFuncProcAddr) { ShowError("GetProcAddress_LoadLibraryA"); return FALSE; } // 使用 CreateRemoteThread 创建远线程, 实现 DLL 注入 HANDLE hRemoteThread = ::CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, NULL); if (NULL == hRemoteThread) { ShowError("CreateRemoteThread"); return FALSE; } // 关闭句柄 ::CloseHandle(hProcess); return TRUE;}
    程序测试我们对 520.exe 进程注入我们的测试 DLL,DLL 成功注入到 520.exe 进程空间中:


    总结要注意以管理员权限运行程序,因为由于 OpenProcess 函数的缘故,打开高权限的进程时,会因进程权限不足而无法打开进程,获取进程句柄。其中,我们将当前进程令牌权限提升至 SE_DEBUG_NAME权限,具体的进程令牌权限提升可以参考 “使用AdjustTokenPrivileges函数提升进程访问令牌的权限“ 这篇文章。
    这个是传统的远线程注入 DLL 方法,有一个问题就是,不能成功注入到一些系统服务进程,因为系统存在 SESSION 0 隔离。接下来,就继续讲解突破 SESSION 0 隔离的远线程注入,成功向系统服务进程注入 DLL。
    参考参考自《Windows黑客编程技术详解》一书
    1  留言 2019-01-12 09:21:05

发送私信

告别错的,方可遇见对的

16
文章数
17
评论数
eject