HanDs
管理员

[Visual Studio文章] 截取系统API调用 





学习中请遵循国家相关法律法规,黑客不作恶。没有网络安全就没有国家安全

本站需要登陆后才能查看

作者:Seung-Woo Kim

在很多情况下,为了测试代码或扩展操作系统的功能,软件开发人员或测试人员必须截取系统函数调用。有一些软件包能够提供该功能,如微软公司的 Detours* 库,或 OK Thinking Software 的 Syringe*。但是从另一个角度而言,开发人员可能希望不需借助第三方软件,自己就能实现该功能。

本文描述了函数截取的几种不同方式,并详细介绍了无需使用商业软件包,也不需受 GNU*(通用公共许可证)许可的约束,就能够实现该功能的一种通用方法。本文所有材料由英特尔公司开发,或根据 MSDN* 样本代码修改而来。

截取系统函数调用的两项基本技术
大部分截取任意函数调用的方法都是准备一个 DLL,用来替代将被截取的目标函数,然后将 DLL 注入至目标进程;在与目标进程连接的基础上,DLL 将自己与目标函数相连。这种技术之所以适合此任务,是因为在大多数情况下我们无法获得目标应用程序的源代码,而这种技术只需相对简单地编写一个包含代换函数的 DLL,就可将其与软件的其它部分分离开来。

两种截取方法已经过研究和分析。Syringe 通过修改函数输入条目(thunking 表)运行。而Detours 库则直接修改目标函数(在目标进程空间内),并无条件地跳转至代换函数。此外,它还提供能够调用原始函数的 trampoline 函数。

Detours 技术之所以采用后一种方法,是因为在许多情况下,Syringe 无法找到 thunk,并且它不能提供 trampoline功能来调用原始函数。在这两种方法下,注入 DLL 的工作方式相同。

截取系统函数调用的全部工作流程如下所示:


         1.DLL 注入 — 首先,主软件打开目标进程,并使其加载包含代换函数的 DLL。


         2.目标函数修改 — 当 DLL 连接至进程时,它在目标进程空间内修改目标函数,从而直接跳转至 DLL 中的代换函数。Trampoline 函数能够随意调用原始函数。


        3.目标函数截取 — 当调用目标函数时,它直接跳转至 DLL 中的代换函数。如果开发人员希望调用原始的功能,则他或她就可以调用 trampoline 函数。

DLL 注入
本节内容完全以 MSDN 文章“定制调试诊断工具和实用程序 — 摆脱 DLL“地狱”      (DLL Hell)*”为基础,该文章还包括可下载的源代码。在本文附录中可获得 Inject.cpp 和 Inject.h 。已对它们进行了定制以便于集成——仅需将其包括在项目中然后调用 InjectLib 即可。使目标进程加载 DLL 的算法按如下步骤工作:


1.通过调用 OpenProcess 打开目标进程。


2.通过调用 VirtualAllocEx 在目标进程中分配内存。利用 WriteProcessMemory 将要被注入的 DLL 名称写入分配的内存。


3.通过调用 GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW") 来获取 LoadLibrary 的地址;


4.调用 CreateRemoteThread,指定 LoadLibrary 的入口点,并将 DLL (第 2 步中) 的名称作为其自变量。目标进程将加载 DLL。


5.利用 VirtualFreeEx 释放分配的内存。已不再需要该内存。


Inject.cpp 融合了包括稳固的安全特性等大量其它功能,但上述步骤已足够阐明其核心概念。

目标函数修改
目标函数修改为自我修改代码,尽管在将 jmp 注入进程内存的过程中存在一些缺陷,但它在 MSDN*      上具有完善的文件证明。为避免混淆,本节列出了几乎全部的样本代码。

目标函数修改的两个主要方面为代换函数和 trampoline 函数。下面的代码片断为截取 GetSystemPowerStatus API 的 DLL 示例:
这段代码为连接所做的第一件事就是调用 InterceptAPI。需要使用包含目标函数的模块名称、目标函数的名称以及代换函数的地址。 GetSystemPowerStatus 位于 kernel32.dll 中。其它基本的 Win32* API,如 MessageBox 和 PeekMessage,都能够在 user32.dll 中获得。MSDN 指定每个 API 所属的模块;未来的增强版中,将自动为给定的 API 找到正确的模块。

InterceptAPI 将目标函数的前五个字节覆盖为无条件跳转(opcode 0xE9),后面为四个字节的带符号整数(向代换函数的位移)。位移从下一个指令开始;因此需要使用 pbReplaced - (pbTargetCode +4)。进行该代码操作时,需要注意以下两点:


1.将区域覆盖的保护模式改为 VirtualProtect。否则,将发生非法访问错误。


2.必须使用 FlushInstructionCache 来支持指令已存在于高速缓存中的情况。否则,即使内存中的指令已经有所变化,旧代码仍将在高速缓存中运行。

现在,当调用 GetSystemPowerStatus 函数时,它跳转至代换函数,然后直接返回调用方,成功截取调用。


Trampoline 函数
在很多情况下,代换函数除使用自身代码外,还需调用原始目标函数,这样就能够扩展 API 的功能,而不是替换整个 API。Trampoline 函数可以提供该功能。Trampoline 函数的原理如下所示:


*编写一个具有相同声明的哑元函数(dummy function),将作为 trampoline 使用。确保哑元函数的长度超过 10 个字节。


*在覆盖目标函数的前五个字节之前,将它们复制到 trampoline 函数的起始处。


*利用无条件跳转,将trampoline的第六个字节覆盖为目标函数的第六个字节。


*与之前一样覆盖目标函数。


*当从代换函数或其它地方调用 trampoline 函数时,它执行复制出的原始代码的前五个字节,然后跳转至实际原始代码的第六个字节。控制返回至 trampoline 的调用方。当完成其它任务时,控制返回至 API 的控制方。

可能存在一种复杂的情况,即原始代码的第六个字节可能是先前指令的一部分。在这种情况下,函数会覆盖部分先前指令,然后崩溃。在 GetSystemPowerStatus 的情况中,前五个字节后的新指令开始于第七个字节。因此,对于这种工作机制,需要将六个字节复制到 trampoline,并且代码必须相应地调整这个偏移量。

代码需要复制的字节数取决于 API。查看原始目标代码(利用调试器或反汇编器)并计算需要复制的字节数是非常必要的。未来的增强版将自动检测正确的偏移量。假设我们已经知道正确的偏移量,下面的代码则显示出可建立 trampoline 函数的可扩展 InterceptAPI 函数:
BOOL InterceptAPI(HMODULE hLocalModule, const char* c_szDllName, const char* c_szApiName,
             DWORD dwReplaced, DWORD dwTrampoline, int offset)
{
         int i;
         DWORD dwOldProtect;
             DWORD dwAddressToIntercept = (DWORD)GetProcAddress(
                 GetModuleHandle((char*)c_szDllName), (char*)c_szApiName);

         BYTE *pbTargetCode = (BYTE *) dwAddressToIntercept;
         BYTE *pbReplaced = (BYTE *) dwReplaced;
         BYTE *pbTrampoline = (BYTE *) dwTrampoline;

         // Change the protection of the trampoline region
         // so that we can overwrite the first 5 + offset bytes.
         VirtualProtect((void *) dwTrampoline, 5+offset, PAGE_WRITECOPY, &dwOldProtect);
         for (i=0;i<offset;i++)
             *pbTrampoline++ = *pbTargetCode++;
         pbTargetCode = (BYTE *) dwAddressToIntercept;

         // Insert unconditional jump in the trampoline.
         *pbTrampoline++ = 0xE9;             // jump rel32
         *((signed int *)(pbTrampoline)) = (pbTargetCode+offset) - (pbTrampoline + 4);
         VirtualProtect((void *) dwTrampoline, 5+offset, PAGE_EXECUTE, &dwOldProtect);
    
         // Overwrite the first 5 bytes of the target function
         VirtualProtect((void *) dwAddressToIntercept, 5, PAGE_WRITECOPY, &dwOldProtect);
         *pbTargetCode++ = 0xE9;             // jump rel32
         *((signed int *)(pbTargetCode)) = pbReplaced - (pbTargetCode +4);
         VirtualProtect((void *) dwAddressToIntercept, 5, PAGE_EXECUTE, &dwOldProtect);
    
         // Flush the instruction cache to make sure
         // the modified code is executed.
         FlushInstructionCache(GetCurrentProcess(), NULL, NULL);
         return TRUE;
}

结论
本文描述了截取系统函数调用的一种通用方法,同时还提供了 trampoline 函数,从而保留了原始功能。本文仅对方法进行简要描述,并未对完整的软件包作出说明,因此如下一些细节并没有实现:


1.自动检测包含目标 API 的模块。


2.自动检测 trampoline 函数的偏移量。


3.删除代换函数,并注入 DLL。(到目前为止,清空代换函数的唯一方法是关闭应用程序。)

然而,对于开发人员而言,无需依赖第三方软件包,执行截取任意系统函数调用的软件,本文中涉及的技术、说明及源代码已经足够。

作者介绍

Seung-Woo Kim 拥有明尼苏达大学的计算机科学博士学位,目前为英特尔公司的高级应用工程师。他致力于技术与商业软件的性能优化。可通过此电子邮件地址与他取得联系。
截取系统 API 调用
附录:
#include "stdafx.h"
#include "Inject.h"
#include <tchar.h>
#include <malloc.h>        // For alloca
#include <pi.h>
#ifdef UNICODE
#define InjectLib InjectLibW
#else
#define InjectLib InjectLibA
#endif       // !UNICODE
BOOL AdjustDacl(HANDLE h, DWORD DesiredAccess)
{
        // the WORLD Sid is trivial to form programmatically (S-1-1-0)
        SID world = { SID_REVISION, 1, SECURITY_WORLD_SID_AUTHORITY, 0 };
    
        EXPLICIT_ACCESS ea =
        {
            DesiredAccess,
                SET_ACCESS,
                NO_INHERITANCE,
            {
                0, NO_MULTIPLE_TRUSTEE,
                    TRUSTEE_IS_SID,
                    TRUSTEE_IS_USER,
                    reinterpret_cast<LPTSTR>(&world)
            }
        };
        ACL* pdacl = 0;
        DWORD err = SetEntriesInAcl(1, &ea, 0, &pdacl);
        if (err == ERROR_SUCCESS)
        {
            err = SetSecurityInfo(h, SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION, 0, 0, pdacl, 0);
            LocalFree(pdacl);
            return(err == ERROR_SUCCESS);
        }
        else
            return(FALSE);
}
// Useful helper function for enabling a single privilege
BOOL EnableTokenPrivilege(HANDLE htok, LPCTSTR szPrivilege, TOKEN_PRIVILEGES& tpOld)
{
        TOKEN_PRIVILEGES tp;
        tp.PrivilegeCount = 1;
        tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
        if (LookupPrivilegeValue(0, szPrivilege, &tp.Privileges[0].Luid))
        {
            // htok must have been opened with the following permissions:
            // TOKEN_QUERY (to get the old priv setting)
            // TOKEN_ADJUST_PRIVILEGES (to adjust the priv)
            DWORD cbOld = sizeof tpOld;
            if (AdjustTokenPrivileges(htok, FALSE, &tp, cbOld, &tpOld, &cbOld))
                // Note that AdjustTokenPrivileges may succeed, and yet
                // some privileges weren't actually adjusted.
                // You've got to check GetLastError() to be sure!
                return(ERROR_NOT_ALL_ASSIGNED != GetLastError());
            else
                return(FALSE);
        }
        else
            return(FALSE);
}

// Corresponding restoration helper function
BOOL RestoreTokenPrivilege(HANDLE htok, const TOKEN_PRIVILEGES& tpOld)
{
        return(AdjustTokenPrivileges(htok, FALSE, const_cast<TOKEN_PRIVILEGES*>(&tpOld), 0, 0, 0));
}
HANDLE GetProcessHandleWithEnoughRights(DWORD PID, DWORD AccessRights)
{
        HANDLE hProcess = ::OpenProcess(AccessRights, FALSE, PID);
        if (hProcess == NULL)
        {
            HANDLE hpWriteDAC = OpenProcess(WRITE_DAC, FALSE, PID);
            if (hpWriteDAC == NULL)
            {
                // hmm, we don't have permissions to modify the DACL...
                // time to take ownership...
                HANDLE htok;
                if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, &htok))
                    return(FALSE);
            
                TOKEN_PRIVILEGES tpOld;
                if (EnableTokenPrivilege(htok, SE_TAKE_OWNERSHIP_NAME, tpOld))
                {
                    // SeTakeOwnershipPrivilege allows us to open objects with
                    // WRITE_OWNER, but that's about it, so we'll update the owner,
                    // and dup the handle so we can get WRITE_DAC permissions.
                    HANDLE hpWriteOwner = OpenProcess(WRITE_OWNER, FALSE, PID);
                    if (hpWriteOwner != NULL)
                    {
                        BYTE buf[512]; // this should always be big enough
                        DWORD cb = sizeof buf;
                        if (GetTokenInformation(htok, TokenUser, buf, cb, &cb))
                        {
                            DWORD err =
                                SetSecurityInfo(
                                hpWriteOwner,
                                SE_KERNEL_OBJECT,
                                OWNER_SECURITY_INFORMATION,
                                reinterpret_cast<TOKEN_USER*>(buf)->User.Sid,
                                0, 0, 0
                                );
                            if (err == ERROR_SUCCESS)
                            {
                                // now that we're the owner, we've implicitly got WRITE_DAC
                                // permissions, so ask the system to reevaluate our request,
                                // giving us a handle with WRITE_DAC permissions
                                if (
                                    !DuplicateHandle(
                                    GetCurrentProcess(),
                                    hpWriteOwner,
                                    GetCurrentProcess(),
                                    &hpWriteDAC,
                                    WRITE_DAC, FALSE, 0
                                    )
                                    )
                                    hpWriteDAC = NULL;
                            }
                        }
                    
                        // don't forget to close handle
                        ::CloseHandle(hpWriteOwner);
                    }
                
                    // not truly necessary in this app,
                    // but included for completeness
                    RestoreTokenPrivilege(htok, tpOld);
                }
            
                // don't forget to close the token handle
                ::CloseHandle(htok);
            }
        
            if (hpWriteDAC)
            {
                // we've now got a handle that allows us WRITE_DAC permission
                AdjustDacl(hpWriteDAC, AccessRights);
            
                // now that we've granted ourselves permission to access
                // the process, ask the system to reevaluate our request,
                // giving us a handle with right permissions
                if (
                    !DuplicateHandle(
                    GetCurrentProcess(),
                    hpWriteDAC,
                    GetCurrentProcess(),
                    &hProcess,
                    AccessRights,
                    FALSE,
                    0
                    )
                    )
                    hProcess = NULL;
            
                CloseHandle(hpWriteDAC);
            }
        }
    
        return(hProcess);
}
BOOL WINAPI InjectLibW(DWORD dwProcessId, PCWSTR pszLibFile)
{
        BOOL fOk = FALSE; // Assume that the function fails
        HANDLE hProcess = NULL, hThread = NULL;
        PWSTR pszLibFileRemote = NULL;
    
        // Get a handle for the target process.
        hProcess =
            GetProcessHandleWithEnoughRights(
            dwProcessId,
            PROCESS_QUERY_INFORMATION |       // Required by Alpha
            PROCESS_CREATE_THREAD         |       // For CreateRemoteThread
            PROCESS_VM_OPERATION          |       // For VirtualAllocEx/VirtualFreeEx
            PROCESS_VM_WRITE                  // For WriteProcessMemory
            );
        if (hProcess == NULL)
            return(FALSE);
    
        // Calculate the number of bytes needed for the DLL's pathname
        int cch = 1 + lstrlenW(pszLibFile);
        int cb      = cch * sizeof(WCHAR);
    
        // Allocate space in the remote process for the pathname
        pszLibFileRemote =
            (PWSTR) VirtualAllocEx(hProcess, NULL, cb, MEM_COMMIT, PAGE_READWRITE);
    
        if (pszLibFileRemote != NULL)
        {
            // Copy the DLL's pathname to the remote process's address space
            if (WriteProcessMemory(hProcess, pszLibFileRemote,
                (PVOID) pszLibFile, cb, NULL))
            {
                // Get the real address of LoadLibraryW in Kernel32.dll
                PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)
                    GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");
                if (pfnThreadRtn != NULL)
                {
                    // Create a remote thread that calls LoadLibraryW(DLLPathname)
                    hThread = CreateRemoteThread(hProcess, NULL, 0,
                        pfnThreadRtn, pszLibFileRemote, 0, NULL);
                    if (hThread != NULL)
                    {
                        // Wait for the remote thread to terminate
                        WaitForSingleObject(hThread, INFINITE);
                    
                        fOk = TRUE; // Everything executed successfully
                    
                        CloseHandle(hThread);
                    }
                }
            }
            // Free the remote memory that contained the DLL's pathname
            VirtualFreeEx(hProcess, pszLibFileRemote, 0, MEM_RELEASE);
        }
    
        CloseHandle(hProcess);
    
        return(fOk);
}

BOOL WINAPI InjectLibA(DWORD dwProcessId, PCSTR pszLibFile) {
    
        // Allocate a (stack) buffer for the Unicode version of the pathname
        PWSTR pszLibFileW = (PWSTR)
            _alloca((lstrlenA(pszLibFile) + 1) * sizeof(WCHAR));
    
        // Convert the ANSI pathname to its Unicode equivalent
        wsprintfW(pszLibFileW, L"%S", pszLibFile);
    
        // Call the Unicode version of the function to actually do the work.
        return(InjectLibW(dwProcessId, pszLibFileW));
}


学习中请遵守法律法规,本网站内容均来自于互联网,本网站不负担法律责任
截取
#1楼
发帖时间:2016-7-9   |   查看数:0   |   回复数:0
游客组
快速回复