分类

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

文章列表

  • 使用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:58
  • 内核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:35:45
  • 根据PE文件格式从导入表中获取加载的DLL并遍历导入函数名称和地址

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

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

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

    总结这个程序不是很复杂,但是关键是要理解 PE 文件结构的导入表,要理解清楚导入表的工作原理。
    参考参考自《Windows黑客编程技术详解》一书
    2 回答 2018-12-24 09:15:37
  • 根据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:24:23
  • 基于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:40:04
  • 基于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黑客编程技术详解》一书
    2 回答 2018-12-22 10:17:21
  • 基于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黑客编程技术详解》一书
    2 回答 2018-12-21 09:00:18
  • 基于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黑客编程技术详解》一书
    3 回答 2018-12-20 17:50:18
  • 上传资源,获取积分

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

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

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

    课内资源

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

    课外资源

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


    使用须知** 在上传资源前,请花两分钟阅读 “使用须知” 部分内容 **
    21 回答 2019-01-24 09:26:15
  • 基于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 12:05:25
  • 磁盘盘符隐藏并访问隐藏磁盘的文件数据

    背景之前,帮一个小伙伴开发了一个程序,这个程序就是对磁盘盘符进行隐藏与显示。也就是说,我们打开资源管理器,在资源管理器中隐藏指定磁盘,不显示在界面上。而且,我们还可以使用程序对这个隐藏后的磁盘文件数据进行读写。
    这个程序的实现原理,主要是删除和创建卷加载点实现来实现的。其中,我们给出两种方法来创建隐藏磁盘,分别是使用WIN32 API 函数 DefineDosDeivce 以及 SetVolumeMountPoint 来实现。现在,我就把实现过程整理成文档,分享给大家。
    函数介绍QueryDosDevice 函数
    获取有关MS-DOS设备名称的信息。 该功能可以获得特定MS-DOS设备名称的当前映射。 该功能还可以获取所有现有MS-DOS设备名称的列表。
    函数声明
    DWORD WINAPI QueryDosDevice( _In_opt_ LPCTSTR lpDeviceName, _Out_ LPTSTR lpTargetPath, _In_ DWORD ucchMax);
    参数

    lpDeviceName [in,optional]指定查询目标的MS-DOS设备名称字符串。设备名称不能有尾随的反斜杠;例如,使用“C:”,而不是“C:\”。此参数可以为NULL。在这种情况下,QueryDosDevice功能将将所有现有的MS-DOS设备名称的列表存储到lpTargetPath指向的缓冲区中。lpTargetPath [out]指向将接收查询结果的缓冲区的指针。该函数用一个或多个以null结尾的字符串填充此缓冲区。最后以空值终止的字符串后跟一个额外的NULL。如果lpDeviceName不为NULL,则该函数将检索有关由lpDeviceName指定的特定MS-DOS设备的信息。存储在缓冲区中的第一个以null结尾的字符串是设备的当前映射。其他以null结尾的字符串表示设备的未删除的先前映射。如果lpDeviceName为NULL,则该函数将检索所有现有MS-DOS设备名称的列表。存储在缓冲区中的每个以null结尾的字符串都是现有MS-DOS设备的名称,例如\ Device \ HarddiskVolume1或\ Device \ Floppy0。ucchMax [in]lpTargetPath指向的缓冲区中可以存储的最大TCHAR数。
    返回值

    如果函数成功,则返回值是存储在lpTargetPath指向的缓冲区中的TCHAR数。如果函数失败,返回值为零。 要获取扩展错误信息,请调用GetLastError。如果缓冲区太小,则该函数失败,最后一个错误代码为ERROR_INSUFFICIENT_BUFFER。

    DefineDosDevice 函数
    定义,重新定义或删除MS-DOS设备名称。
    函数声明
    BOOL WINAPI DefineDosDevice( _In_ DWORD dwFlags, _In_ LPCTSTR lpDeviceName, _In_opt_ LPCTSTR lpTargetPath);
    参数

    dwFlags [in]DefineDosDevice功能的可控方面。 此参数可以是以下值中的一个或多个:



    VALUE
    MEANING




    DDD_EXACT_MATCH_ON_REMOVE
    如果此值与DDD_REMOVE_DEFINITION一起指定,则该函数将使用完全匹配来确定要删除的映射。 使用此值可确保不删除未定义的内容


    DDD_NO_BROADCAST_SYSTEM
    不要广播WM_SETTINGCHANGE消息。 默认情况下,该消息被广播以通知shell和应用程序的更改


    DDD_RAW_TARGET_PATH
    使用lpTargetPath字符串。 否则,它将从MS-DOS路径转换为路径


    DDD_REMOVE_DEFINITION
    删除指定设备的指定定义。 要确定要删除的定义,该函数将会遍历设备的映射列表,查找与此设备关联的每个映射的前缀的lpTargetPath的匹配。 匹配的第一个映射是删除的映射,然后该函数返回。如果lpTargetPath为NULL或指向NULL字符串的指针,则该函数将删除与设备关联的第一个映射,并弹出最近推送的映射。 如果没有什么可以弹出,设备名称将被删除。如果未指定此值,则由lpTargetPath参数指向的字符串将成为此设备的新映射。




    lpDeviceName [in]指向MS-DOS设备名称字符串的指针,指定功能正在定义,重新定义或删除的设备。 设备名称字符串不得有冒号作为最后一个字符,除非正在定义,重新定义或删除驱动器号。 例如,驱动器C将是字符串“C:”。 在任何情况下都不允许使用尾部反斜杠(“\”)。
    lpTargetPath [in]指向将实现此设备的路径字符串的指针。 字符串是一个MS-DOS路径字符串,除非指定了DDD_RAW_TARGET_PATH标志,在这种情况下,此字符串是一个路径字符串。

    返回值

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

    DeleteVolumeMountPoint 函数
    删除驱动器号或安装的文件夹。
    函数声明
    BOOL WINAPI DeleteVolumeMountPoint( _In_ LPCTSTR lpszVolumeMountPoint);
    参数

    lpszVolumeMountPoint [in]要删除的驱动器号或安装的文件夹。 需要尾随的反斜杠,例如“X:\”或“Y:\ MountX \”。
    返回值

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

    GetVolumeNameForVolumeMountPoint 函数
    检索与指定卷装入点(驱动器盘符,卷GUID路径或已装载文件夹)相关联的卷的卷GUID路径。
    函数声明
    BOOL WINAPI GetVolumeNameForVolumeMountPoint( _In_ LPCTSTR lpszVolumeMountPoint, _Out_ LPTSTR lpszVolumeName, _In_ DWORD cchBufferLength);
    参数

    lpszVolumeMountPoint [in]指向包含已安装文件夹路径(例如“Y:\ MountX \”)或驱动器盘符(例如“X:\”)的字符串的指针。 字符串必须以尾部反斜杠(’\’)结尾。lpszVolumeName [out]指向接收卷GUID路径的字符串的指针。 此路径的格式为“\?\ Volume {GUID} \”,其中GUID是用于标识卷的GUID。 如果该卷存在多个卷GUID路径,则仅返回安装管理器缓存中的第一个卷。cchBufferLength [in]输出缓冲区的长度,在TCHAR中。 缓冲区容纳最大容量GUID路径的合理大小为50个字符。
    返回值

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

    SetVolumeMountPoint 函数
    将卷与驱动器号或另一卷上的目录相关联。
    函数声明
    BOOL WINAPI SetVolumeMountPoint( _In_ LPCTSTR lpszVolumeMountPoint, _In_ LPCTSTR lpszVolumeName);
    参数

    lpszVolumeMountPoint [in]与卷关联的用户模式路径。 这可能是驱动器号(例如“X:\”)或其他卷上的目录(例如“Y:\ MountX \”)。 字符串必须以尾部反斜杠(’\’)结尾。lpszVolumeName [in]卷的卷GUID路径。 此字符串的格式必须为“\?\ Volume {GUID} \”,其中GUID是用于标识卷的GUID。 “\?\”关闭路径解析,并作为路径的一部分被忽略,如命名卷所述。
    返回值

    如果函数成功,则返回值不为零。如果函数失败,返回值为零。 要获取扩展错误信息,请调用GetLastError。如果lpszVolumeMountPoint参数包含已安装文件夹的路径,即使目录为空,GetLastError返回ERROR_DIR_NOT_EMPTY。

    实现原理在资源管理器中隐藏磁盘显示的原理就是:把磁盘对应的卷加载点删除,这样,磁盘就没有相应的驱动器号了,就不会在资源管理器中显示。对于删除卷加载点,我们可以使用 DeleteVolumeMountPoint 函数实现。
    在资源管理器中还原回隐藏的磁盘,原理就是:重新创建磁盘的卷加载点,并未磁盘分配一个驱动器号。但是,在创建卷加载点之前,也就是在删除卷加载点之前,我们就要使用 GetVolumeNameForVolumeMountPoint 获取卷加载点对应的卷名。因为,当我们使用 SetVolumeMountPoint 函数的时候,需要用到卷名,为相应的卷名创建卷加载点,分配驱动器号。
    创建隐藏盘符,方便我们程序访问的原理是:我们为删除卷加载点的磁盘,分配一个非字母的的驱动器号,这样,磁盘在资源管理器中是不显示的。但是,我们的程序可以通过这个非字母的盘符,正常访问盘符里的文件数据,和正常的字母盘符一样访问。在此,创建一个非字母的磁盘设备,我们可以有两种实现方式,均可以达到上述所说的效果:

    使用 DefineDosDevice 函数来实现,在使用 DefineDosDevice 之前,就需要获取磁盘对应的 Dos 路径,也就是说, 在删除卷加载点之前,先调用 QueryDosDevice 函数获取磁盘对应的 Dos 路径。之后,再使用 DefineDosDevice 函数将 Dos 路径对应的磁盘创建一个非字母驱动器的路径。
    使用 SetVolumeMountPoint 函数来实现,在使用 SetVolumeMountPoint 之前,需要通过 GetVolumeNameForVolumeMountPoint 函数来获取磁盘对应的卷名。后来,我们通过 SetVolumeMountPoint 为卷名对应的磁盘分配一个非字母驱动器号的卷加载点。

    编码实现删除卷加载点,隐藏盘符// 隐藏磁卷加载点, 实现磁盘隐藏BOOL HideValume(char *pszDriver){ BOOL bRet = ::DeleteVolumeMountPoint(pszDriver); if (FALSE == bRet) { ShowError("DeleteVolumeMountPoint"); return FALSE; } return TRUE;}
    获取磁盘对应的卷名// 获取磁盘对应的卷名::GetVolumeNameForVolumeMountPoint("E:\\", szVolumeName, MAX_PATH);
    设置卷加载点,显示磁盘// 显示卷加载点, 恢复磁盘显示BOOL ShowValume(char *pszDriver, char *pszVolumeName){ /* 注意在使用SetVolumeMountPoint的时候,挂载点目录必须存在,而且必须为空目录,否则程序会运行失败 */ while (ERROR_DIR_NOT_EMPTY == ::SetVolumeMountPoint(pszDriver, pszVolumeName)) { // 更改加载盘符 pszDriver[0]++; } return TRUE;}
    使用 DefineDosDevice 创建隐藏磁盘// 创建隐藏盘符BOOL CreateHideVolume(char *lpszDosPath){ // 创建隐藏盘符,对于非字母盘符,在"我的电脑"里是不可见的,只有程序可以访问 if (::DefineDosDevice(DDD_RAW_TARGET_PATH, MY_HIDEN_DRIVER, lpszDosPath)) { return TRUE; } return FALSE;}
    删除隐藏磁盘路径// 删除 1:DeleteHideVolume(szDosPath);// 删除 2:HideValume("2:\\");
    程序测试我们在 main 函数中,调用上述封装好的函数进行测试。首先,我们先获取将要隐藏磁盘对应的卷名以及 Dos 路径。然后,我们开始删除卷加载点,实现磁盘的隐藏。接着,我们使用 DefineDosDeivce 的方法创建一个非字母驱动器路径 1:,并拷贝 520.exe 文件到非字符驱动器路径的根目录下 1:\ ,测试非字母路径能否正常访问。然后,我们使用 SetVolumeMountPoint 创建一个非字母的驱动器 2:\,并拷贝 520.exe 文件到非字符驱动器路径的根目录下 2:\ ,测试非字母路径能否正常访问。最后,我们便删除上述两种方法创建的非字母驱动器号路径,并恢复正确的磁盘路径,显示磁盘。
    int _tmain(int argc, _TCHAR* argv[]){ char szVolumeName[MAX_PATH] = { 0 }; char szDosPath[MAX_PATH] = { 0 }; // 获取磁盘对应的卷名 ::GetVolumeNameForVolumeMountPoint("E:\\", szVolumeName, MAX_PATH); // 获取磁盘路径对应的Dos路径 ::QueryDosDevice("E:", szDosPath, MAX_PATH); // 删除卷加载点来实现磁盘隐藏 HideValume("E:\\"); system("pause"); // 使用 DefineDosDevice 创建一个非字母驱动器号的磁盘路径 1: CreateHideVolume(szDosPath); system("pause"); // 复制文件到隐藏磁盘 if (FALSE == ::CopyFile("520.exe", "1:\\520__111111.exe", FALSE)) { printf("copy file error[%d].\n", ::GetLastError()); } printf("copy file ok.\n"); system("pause"); // 使用 SetVolumeMountPoint 创建一个非字母驱动器号的磁盘路径 2: ShowValume("2:\\", szVolumeName); system("pause"); // 复制文件到隐藏磁盘 if (FALSE == ::CopyFile("520.exe", "2:\\520_22222222.exe", FALSE)) { printf("copy file error[%d].\n", ::GetLastError()); } printf("copy file ok.\n"); system("pause"); // 删除 1: DeleteHideVolume(szDosPath); // 删除 2: HideValume("2:\\"); // 恢复正确磁盘路径 ShowValume("E:\\", szVolumeName); system("pause"); return 0;}
    我们以管理员权限运行程序,测试结果正确:


    总结要注意的是,程序是需要管理员或者管理员以上权限才可以正常执行。同时,也需要理解上述的两种方法实现的对隐藏磁盘数据文件的读写。理解 DefineDosDevice 和 SetVolumeMountPoint 函数的参数含义以及具体的使用方法。
    参考参考自《Windows黑客编程技术详解》一书
    2 回答 2018-12-20 12:05:13
  • 编程实现对硬盘全盘数据进行读写数据擦除

    背景在 XP 系统下下,我们可以直接调用 WirteFile 函数对磁盘写入数据,但到了 Windows 7 以及 Windows 7 版本以上的系统,就已经开始变得不那么简单了。
    在 Windows 7 及以上版本中,对文件系统和存储堆栈进行的更改,限制对磁盘和卷的直接访问,但是,在以下情况,存储驱动器可以写入磁盘句柄:

    正要写入的扇区不位于卷内(空隙块与分区表)。注意:程序使用卷之外的扇区来存储元数据。分区表也位于卷之外的扇区中。由于这些扇区不受任何文件系统的控制,因此没有理由阻止对这些扇区的访问
    已经通过锁定请求或卸载请求显式锁定卷
    正要写入的扇区位于未安装或者无文件系统的卷内(这个是原生磁盘块,没有受操作系统的管理,当然可以随便写)

    本文介绍的是使用第 2 种方法,通过锁定请求或卸载请求显式锁定卷来实现对磁盘数据的读写。现在把实现过程的实现原理整理成文档,分享给大家。
    实现原理我们对于磁盘空隙块与分区表等磁盘位置是可以像 XP 系统那样直接使用 WriteFile 函数写的,但对于盘的数据区的写入则应该采用显式锁定的已安装卷或卸载请求显式锁定卷。
    一般是采用 FSCTL_DISMOUNT_VOLUME,FSCTL_LOCK_VOLUME 的前提是没有程序占用该盘的文件,所以可能会失败,FSCTL_DISMOUNT_VOLUME 是强制型的,当我们强制卸载请求显式锁定卷后,一些正在占用磁盘的程序会出现崩溃,原因是它不能读盘上资源。但是,要注意的是,对于FSCTL_DISMOUNT_VOLUME, 如果指定的卷是系统卷或包含页面文件,则操作失败,所以不能对系统盘强制卸载成功。
    首先,我们先介绍第一种方式:锁定请求显式锁定卷。

    首先,我们使用 CreateFile 打开逻辑卷,获取句柄
    然后,我们就可以使用 DeviceIoControl 传递 FSCTL_LOCK_VOLUME 控制码,锁定请求显式锁定卷
    那么,这时,我们就可以通过 SetFilePointer 一定文件指针,使用 WriteFile 函数对磁盘数据进行写入
    写入完成后,还需使用 DeviceIoControl 传递 FSCTL_UNLOCK_VOLUME 控制码,解锁请求显式锁定卷
    最后,我们便可以关闭文件句柄,完成操作

    然后,我们介绍第二种方式:卸载请求显式锁定卷。

    首先,我们使用 CreateFile 打开逻辑卷,获取句柄
    然后,我们就可以使用 DeviceIoControl 传递 FSCTL_DISMOUNT_VOLUME 控制码,卸载请求显式锁定卷
    那么,这时,我们就可以通过 SetFilePointer 一定文件指针,使用 WriteFile 函数对磁盘数据进行写入
    最后,我们便可以关闭文件句柄,完成操作

    这就是本文介绍的两种方法。要注意,使用CreateFile打开设备的时候,名称不能为\\\\.\\PHYSICALDRIVEn(n为0-256),即物理磁盘,只能对逻辑分区锁定与卸载操作。而且,这两种方法,都不能对系统盘进行操作。
    编码实现数据读取// 读取指定扇区数据BOOL ReadDisk(char cDriver, ULONGLONG ullOffsetSector, BYTE *pData){ DWORD dwRet = 0; BOOL bRet = FALSE; char szDriver[MAX_PATH] = { 0 }; // \\\\.\\C: ::wsprintf(szDriver, "\\\\.\\%c:", cDriver); // 打开硬盘物理设备 HANDLE hDisk = ::CreateFile(szDriver, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL); if (INVALID_HANDLE_VALUE == hDisk) { ShowError("CreateFile"); return FALSE; } // 移动文件指针到指定扇区偏移 LARGE_INTEGER li = { 0 }; li.QuadPart = SECTOR_SIZE * ullOffsetSector; ::SetFilePointer(hDisk, li.LowPart, &li.HighPart, FILE_BEGIN); // 读取数据 bRet = ::ReadFile(hDisk, pData, SECTOR_SIZE, &dwRet, NULL); if (FALSE == bRet) { ShowError("ReadFile"); return FALSE; } // 关闭句柄 ::CloseHandle(hDisk); return TRUE;}
    锁定请求显式锁定卷方式写入数据// 写入指定扇区数据 方法一 锁定请求显式锁定卷BOOL WriteDisk_Lock(char cDriver, ULONGLONG ullOffsetSector, BYTE *pData){ DWORD dwRet = 0; BOOL bRet = FALSE; char szDriver[MAX_PATH] = { 0 }; // \\\\.\\C: ::wsprintf(szDriver, "\\\\.\\%c:", cDriver); // 打开硬盘物理设备 // 这里名称不能为\\\\.\\PHYSICALDRIVEn(n为0-256),即物理磁盘,只能对逻辑分区锁定与DISMOUNT操作 HANDLE hDisk = ::CreateFile(szDriver, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL); if (INVALID_HANDLE_VALUE == hDisk) { ShowError("CreateFile"); return FALSE; } // 锁定请求显式锁定卷 bRet = ::DeviceIoControl(hDisk, FSCTL_LOCK_VOLUME, NULL, 0, NULL, 0, &dwRet, NULL); if (FALSE == bRet) { ShowError("DeivceIoControl"); return FALSE; } // 移动文件指针到指定扇区偏移 LARGE_INTEGER li = { 0 }; li.QuadPart = SECTOR_SIZE * ullOffsetSector; ::SetFilePointer(hDisk, li.LowPart, &li.HighPart, FILE_BEGIN); // 写入数据 bRet = ::WriteFile(hDisk, pData, SECTOR_SIZE, &dwRet, NULL); if (FALSE == bRet) { ShowError("WriteFile"); return FALSE; } // 解锁请求显式锁定卷 bRet = ::DeviceIoControl(hDisk, FSCTL_UNLOCK_VOLUME, NULL, 0, NULL, 0, &dwRet, NULL); if (FALSE == bRet) { ShowError("DeivceIoControl"); return FALSE; } // 关闭句柄 ::CloseHandle(hDisk); return TRUE;}
    卸载请求显式锁定卷方式写入数据// 写入指定扇区数据 方法二 卸载请求显式锁定卷BOOL WriteDisk_Dismount(char cDriver, ULONGLONG ullOffsetSector, BYTE *pData){ DWORD dwRet = 0; BOOL bRet = FALSE; char szDriver[MAX_PATH] = { 0 }; // \\\\.\\C: ::wsprintf(szDriver, "\\\\.\\%c:", cDriver); // 打开硬盘物理设备 // 这里名称不能为\\\\.\\PHYSICALDRIVEn(n为0-256),即物理磁盘,只能对逻辑分区锁定与DISMOUNT操作 HANDLE hDisk = ::CreateFile(szDriver, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL); if (INVALID_HANDLE_VALUE == hDisk) { ShowError("CreateFile"); return FALSE; } // 卸载请求显式锁定卷 bRet = ::DeviceIoControl(hDisk, FSCTL_DISMOUNT_VOLUME, NULL, 0, NULL, 0, &dwRet, NULL); if (FALSE == bRet) { ShowError("DeivceIoControl"); return FALSE; } // 移动文件指针到指定扇区偏移 LARGE_INTEGER li = { 0 }; li.QuadPart = SECTOR_SIZE * ullOffsetSector; ::SetFilePointer(hDisk, li.LowPart, &li.HighPart, FILE_BEGIN); // 写入数据 bRet = ::WriteFile(hDisk, pData, SECTOR_SIZE, &dwRet, NULL); if (FALSE == bRet) { ShowError("WriteFile"); return FALSE; } // 关闭句柄 ::CloseHandle(hDisk); return TRUE;}
    程序测试我们在 main 函数中调用上面的函数进行测试,向非系统盘 E 盘读取并使用 3 种方式写入数据,测试结果如下所示:


    数据读取成功,直接调用 WriteFile 函数写入数据方式失败;使用锁定请求显式锁定卷方式失败;使用卸载请求显式锁定卷方式成功。
    总结要注意两点:一是使用 CreateFile 打开设备的时候,名称不能为\\\\.\\PHYSICALDRIVEn(n为0-256),即物理磁盘,只能对逻辑分区锁定与卸载操作。二是,本文介绍的这两种方法,都不能对系统盘进行操作。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2018-12-20 12:05:35
  • 编程实现根据NTFS文件系统定位文件在磁盘上的偏移地址

    背景之前在上一篇博文中 “NTFS文件系统介绍及文件定位” 介绍过 NTFS 的基础概念和常用格式介绍,同时还详细给出了使用 NTFS 定位磁盘文件的例子。现在,这篇文章讲解的就是,编程实现 NTFS 文件定位。也就是把之前手动定位全部改成编程实现,输入一个文件路径,就可以得到文件的大小和数据在磁盘上的偏移地址。
    现在,就把实现的过程整理成文档,分享给大家。如果你之前没有了解过 NTFS 的相关概念,可以先阅读之前写的 “NTFS文件系统介绍及文件定位” 这篇文章。
    实现原理使用 NTFS 文件系统来定位文件,原理如下:

    首先打开磁盘,获取磁盘的分区引导扇区 DBR 中的数据。根据 DBR 的数据含义,从中获取扇区大小、簇大小以及\$MFT住文件记录的起始簇号
    根据根目录文件记录,一般处于 5 号文件,而且每个文件记录大小为 2 个扇区大小的知识前提。我们可以计算出,根目录文件记录的偏移地址。得到根目录文件记录偏移地址,就可以来到了磁盘文件根目录下了
    然后,我们根据文件记录头的数据格式,获取第一个属性的偏移位置,然后在获取属性的大小,以此计算出下一个属性的偏移位置。这样就可以对文件记录中的属性进行遍历了。我们按照 90H属性、A0H属性的处理顺序进行处理,获取文件或者目录的的$MFT参考号,然后继续跳转到下一个文件记录,重复处理 90H属性、A0H属性,获取文件或者目录的的$MFT参考号,直到获取到最终的文件
    这时,我们就可以根据 80H 属性获取文件数据的偏移位置

    这样文件定位就结束了。其中,我们需要继续介绍 90H 属性的处理过程、A0H属性的处理过程以及使用 80H 属性 定位文件数据偏移的过程。
    90H 属性的处理就是,直接扫描 90H 属性的数据,判断有没有我们要定位的文件或者目录的名称,若有,则获取该文件或者目录的\$MTF文件参考号;若没有,则不处理。
    A0H 属性的处理过程是:

    我们首先获取 Data Run 在属性中的偏移,然后从偏移中获取 Data Run 数据,并跳转到 Data Run 指向的偏移地址
    跳转到 Data Run 指向的偏移地址,便到了 INDX 索引。然后,我们就扫描 INDX 的数据,判断有没有我们要定位的文件或者目录的名称。若没有,则继续退出。若有,则获取该文件或者目录的\$MTF文件参考号
    如果有多个Data Run,则重复上面操作,若没有,执行完毕后,就退出

    根据 80H 属性定位文件数据:

    首先,我们先根据$MTF文件参考号来到文件记录处,并获取 80H 属性的偏移
    然后,判断 80H 属性是常驻属性还是非常驻属性。若是常驻属性,则直接在 80H 属性中获取文件数据。若为非常驻属性,则需要获取 80H 属性的 Data Run 数据,那么,Data Run 中执行的地址就是数据内容的存储地址

    编码实现从DBR中获取扇区大小、簇大小、\$MFT起始簇号// 从DBR中获取数据:每个扇区字节数、每个簇的扇区数、原文件$MFT的起始簇号BOOL GetDataFromDBR(HANDLE hFile, WORD &wSizeOfSector, BYTE &bSizeOfCluster, LARGE_INTEGER &liClusterNumberOfMFT){ // 获取扇区大小(2)、簇大小(1)、$MFT起始簇号(8) BYTE bBuffer[512] = { 0 }; DWORD dwRead = 0; // 注意:数据读取的大小最小单位是扇区!!! ::SetFilePointer(hFile, 0, NULL, FILE_BEGIN); ::ReadFile(hFile, bBuffer, 512, &dwRead, NULL); wSizeOfSector = MAKEWORD(bBuffer[0x0B], bBuffer[0x0C]); bSizeOfCluster = bBuffer[0x0D]; liClusterNumberOfMFT.LowPart = MAKELONG(MAKEWORD(bBuffer[0x30], bBuffer[0x31]), MAKEWORD(bBuffer[0x32], bBuffer[0x33])); liClusterNumberOfMFT.HighPart = MAKELONG(MAKEWORD(bBuffer[0x34], bBuffer[0x35]), MAKEWORD(bBuffer[0x36], bBuffer[0x37])); return TRUE;}
    文件定位// 定位文件BOOL LocationFile(HANDLE hFile, char *lpszFilePath, WORD wSizeOfSector, BYTE bSizeOfCluster, LARGE_INTEGER liMFTOffset, LARGE_INTEGER &liRootOffset){ BYTE bBuffer[1024] = { 0 }; DWORD dwRead = 0; // 分割文件路径 char szNewFile[MAX_PATH] = { 0 }; ::lstrcpy(szNewFile, (lpszFilePath + 3)); char szDelim[] = "\\"; char *lpResult = strtok(szNewFile, szDelim); BYTE bUnicode[MAX_PATH] = { 0 }; while (NULL != lpResult) { BOOL bFlag = FALSE; DWORD dwNameOffset = 0; // 将分割的目录转换成2字节表示的Unicode数据 DWORD dwLen = ::lstrlen(lpResult); ::RtlZeroMemory(bUnicode, MAX_PATH); for (DWORD i = 0, j = 0; i < dwLen; i++) { bUnicode[j++] = lpResult[i]; bUnicode[j++] = 0; } // 读取目录的数据,大小为1KB ::SetFilePointer(hFile, liRootOffset.LowPart, &liRootOffset.HighPart, FILE_BEGIN); ::ReadFile(hFile, bBuffer, 1024, &dwRead, NULL); // 获取第一个属性的偏移 WORD wAttributeOffset MAKEWORD(bBuffer[0x14], bBuffer[0x15]); // 遍历文件目录的属性 DWORD dwAttribute = 0; DWORD dwSizeOfAttribute = 0; while (TRUE) { if (bFlag) { break; } // 获取当前属性 dwAttribute = MAKELONG(MAKEWORD(bBuffer[wAttributeOffset], bBuffer[wAttributeOffset + 1]), MAKEWORD(bBuffer[wAttributeOffset + 2], bBuffer[wAttributeOffset + 3])); // 判断属性 if (0x90 == dwAttribute) { bFlag = HandleAttribute_90(bBuffer, wAttributeOffset, bUnicode, dwLen, liMFTOffset, liRootOffset); } else if (0xA0 == dwAttribute) { bFlag = HandleAttribute_A0(hFile, bBuffer, wSizeOfSector, bSizeOfCluster, wAttributeOffset, bUnicode, dwLen, liMFTOffset, liRootOffset); } else if (0xFFFFFFFF == dwAttribute) { bFlag = TRUE; break; } // 获取当前属性的大小 dwSizeOfAttribute = MAKELONG(MAKEWORD(bBuffer[wAttributeOffset + 4], bBuffer[wAttributeOffset + 5]), MAKEWORD(bBuffer[wAttributeOffset + 6], bBuffer[wAttributeOffset + 7])); // 计算下一属性的偏移 wAttributeOffset = wAttributeOffset + dwSizeOfAttribute; } // 继续分割目录 lpResult = strtok(NULL, szDelim); } return TRUE;}
    处理90H属性// 0x90属性的处理BOOL HandleAttribute_90(BYTE *lpBuffer, WORD wAttributeOffset, BYTE *lpUnicode, DWORD dwLen, LARGE_INTEGER liMFTOffset, LARGE_INTEGER &liRootOffset){ // 先遍历判断0x90属性里是否有此目录或文件(UNICODE) // 获取当前属性的大小 DWORD dwSizeOfAttribute = MAKELONG(MAKEWORD(lpBuffer[wAttributeOffset + 4], lpBuffer[wAttributeOffset + 5]), MAKEWORD(lpBuffer[wAttributeOffset + 6], lpBuffer[wAttributeOffset + 7])); for (DWORD i = 0; i < dwSizeOfAttribute; i++) { if (CompareMemory(lpUnicode, (lpBuffer + wAttributeOffset + i), 2 * dwLen)) { DWORD dwNameOffset = wAttributeOffset + i; // 计算文件号 dwNameOffset = dwNameOffset / 8; dwNameOffset = 8 * dwNameOffset; dwNameOffset = dwNameOffset - 0x50; // 获取文件号(6) LARGE_INTEGER liNumberOfFile; liNumberOfFile.LowPart = MAKELONG(MAKEWORD(lpBuffer[dwNameOffset], lpBuffer[dwNameOffset + 1]), MAKEWORD(lpBuffer[dwNameOffset + 2], lpBuffer[dwNameOffset + 3])); liNumberOfFile.HighPart = MAKELONG(MAKEWORD(lpBuffer[dwNameOffset + 4], lpBuffer[dwNameOffset + 5]), 0); // 计算文件号的偏移,文件号是相对$MFT为偏移说的 liRootOffset = liNumberOfFile; liRootOffset.QuadPart = liMFTOffset.QuadPart + liRootOffset.QuadPart * 0x400; return TRUE; } } // 读取Data Run List,去到索引处INDX遍历UNICODE,获取文件号 return FALSE;}
    处理A0H属性// 0xA0属性的处理BOOL HandleAttribute_A0(HANDLE hFile, BYTE *lpBuffer, WORD wSizeOfSector, BYTE bSizeOfCluster, WORD wAttributeOffset, BYTE *lpUnicode, DWORD dwLen, LARGE_INTEGER liMFTOffset, LARGE_INTEGER &liRootOffset){ // 读取Data Run List,去到索引处INDX遍历UNICODE,获取文件号 DWORD dwCount = 0; LONGLONG llClusterOffet = 0; // 获取索引号的偏移 WORD wIndxOffset = MAKEWORD(lpBuffer[wAttributeOffset + 0x20], lpBuffer[wAttributeOffset + 0x21]); // 读取Data Run List while (TRUE) { BYTE bTemp = lpBuffer[wAttributeOffset + wIndxOffset + dwCount]; // 读取Data Run List,分解并计算Data Run中的信息 BYTE bHi = bTemp >> 4; BYTE bLo = bTemp & 0x0F; if (0x0F == bHi || 0x0F == bLo || 0 == bHi || 0 == bLo) { break; } LARGE_INTEGER liDataRunSize, liDataRunOffset; liDataRunSize.QuadPart = 0; liDataRunOffset.QuadPart = 0; for (DWORD i = bLo; i > 0; i--) { liDataRunSize.QuadPart = liDataRunSize.QuadPart << 8; liDataRunSize.QuadPart = liDataRunSize.QuadPart | lpBuffer[wAttributeOffset + wIndxOffset + dwCount + i]; } if (0 == llClusterOffet) { // 第一个Data Run for (DWORD i = bHi; i > 0; i--) { liDataRunOffset.QuadPart = liDataRunOffset.QuadPart << 8; liDataRunOffset.QuadPart = liDataRunOffset.QuadPart | lpBuffer[wAttributeOffset + wIndxOffset + dwCount + bLo + i]; } } else { // 第二个及多个Data Run // 判断正负 if (0 != (0x80 & lpBuffer[wAttributeOffset + wIndxOffset + dwCount + bLo + bHi])) { // 负整数 for (DWORD i = bHi; i > 0; i--) { // 补码的原码=反码+1 liDataRunOffset.QuadPart = liDataRunOffset.QuadPart << 8; liDataRunOffset.QuadPart = liDataRunOffset.QuadPart | (BYTE)(~lpBuffer[wAttributeOffset + wIndxOffset + dwCount + bLo + i]); } liDataRunOffset.QuadPart = liDataRunOffset.QuadPart + 1; liDataRunOffset.QuadPart = 0 - liDataRunOffset.QuadPart; } else { // 正整数 for (DWORD i = bHi; i > 0; i--) { liDataRunOffset.QuadPart = liDataRunOffset.QuadPart << 8; liDataRunOffset.QuadPart = liDataRunOffset.QuadPart | lpBuffer[wAttributeOffset + wIndxOffset + dwCount + bLo + i]; } } } // 注意加上上一个Data Run的逻辑簇号(第二个Data Run可能是正整数、也可能是负整数(补码表示), 可以根据最高位是否为1来判断, 若为1, 则是负整数, 否则是正整数) liDataRunOffset.QuadPart = llClusterOffet + liDataRunOffset.QuadPart; llClusterOffet = liDataRunOffset.QuadPart; // 去到索引处INDX遍历UNICODE,获取文件号 LARGE_INTEGER liIndxOffset, liIndxSize; liIndxOffset.QuadPart = liDataRunOffset.QuadPart * bSizeOfCluster * wSizeOfSector; liIndxSize.QuadPart = liDataRunSize.QuadPart * bSizeOfCluster * wSizeOfSector; // 读取索引的数据,大小为1KB BYTE *lpBuf = new BYTE[liIndxSize.QuadPart]; DWORD dwRead = 0; ::SetFilePointer(hFile, liIndxOffset.LowPart, &liIndxOffset.HighPart, FILE_BEGIN); ::ReadFile(hFile, lpBuf, liIndxSize.LowPart, &dwRead, NULL); // 遍历Unicode数据 for (DWORD i = 0; i < liIndxSize.LowPart; i++) { if (CompareMemory(lpUnicode, (lpBuf + i), 2 * dwLen)) { DWORD dwNameOffset = i; // 计算文件号 dwNameOffset = dwNameOffset / 8; dwNameOffset = 8 * dwNameOffset; dwNameOffset = dwNameOffset - 0x50; // 获取文件号(6) LARGE_INTEGER liNumberOfFile; liNumberOfFile.LowPart = MAKELONG(MAKEWORD(lpBuf[dwNameOffset], lpBuf[dwNameOffset + 1]), MAKEWORD(lpBuf[dwNameOffset + 2], lpBuf[dwNameOffset + 3])); liNumberOfFile.HighPart = MAKELONG(MAKEWORD(lpBuf[dwNameOffset + 4], lpBuf[dwNameOffset + 5]), 0); // 计算文件号的偏移,文件号是相对$MFT为偏移说的 liRootOffset = liNumberOfFile; liRootOffset.QuadPart = liMFTOffset.QuadPart + liRootOffset.QuadPart * 0x400; return TRUE; } } delete[]lpBuf; lpBuf = NULL; // 计算下一个Data Run List偏移 dwCount = dwCount + bLo + bHi + 1; } return FALSE;}
    读取文件数据内容偏移BOOL FileContentOffset(HANDLE hFile, WORD wSizeOfSector, BYTE bSizeOfCluster, LARGE_INTEGER liMFTOffset, LARGE_INTEGER liRootOffset){ BYTE bBuffer[1024] = { 0 }; DWORD dwRead = 0; LARGE_INTEGER liContenOffset = liRootOffset; // 读取目录的数据,大小为1KB ::SetFilePointer(hFile, liRootOffset.LowPart, &liRootOffset.HighPart, FILE_BEGIN); ::ReadFile(hFile, bBuffer, 1024, &dwRead, NULL); // 获取第一个属性的偏移 WORD wAttributeOffset MAKEWORD(bBuffer[0x14], bBuffer[0x15]); // 遍历文件目录的属性 DWORD dwAttribute = 0; DWORD dwSizeOfAttribute = 0; while (TRUE) { // 获取当前属性 dwAttribute = MAKELONG(MAKEWORD(bBuffer[wAttributeOffset], bBuffer[wAttributeOffset + 1]), MAKEWORD(bBuffer[wAttributeOffset + 2], bBuffer[wAttributeOffset + 3])); // 判断属性 if (0x80 == dwAttribute) { // 读取偏移0x8出1字节,判断是否是常驻属性 BYTE bFlag = bBuffer[wAttributeOffset + 0x8]; if (0 == bFlag) // 常驻 { // 读取偏移0x14出2字节,即是内容的偏移 WORD wContenOffset = MAKEWORD(bBuffer[wAttributeOffset + 0x14], bBuffer[wAttributeOffset + 0x15]); liContenOffset.QuadPart = liContenOffset.QuadPart + wAttributeOffset + wContenOffset; printf("File Content Offset:0x%llx\n\n", liContenOffset.QuadPart); } else // 非常驻 { // 读取偏移0x20出2字节,即是数据运行列表偏移 DWORD dwCount = 0; LONGLONG llClusterOffet = 0; // 获取索引号的偏移 WORD wIndxOffset = MAKEWORD(bBuffer[wAttributeOffset + 0x20], bBuffer[wAttributeOffset + 0x21]); // 读取Data Run List while (TRUE) { BYTE bTemp = bBuffer[wAttributeOffset + wIndxOffset + dwCount]; // 读取Data Run List,分解并计算Data Run中的信息 BYTE bHi = bTemp >> 4; BYTE bLo = bTemp & 0x0F; if (0x0F == bHi || 0x0F == bLo || 0 == bHi || 0 == bLo) { break; } LARGE_INTEGER liDataRunSize, liDataRunOffset; liDataRunSize.QuadPart = 0; liDataRunOffset.QuadPart = 0; for (DWORD i = bLo; i > 0; i--) { liDataRunSize.QuadPart = liDataRunSize.QuadPart << 8; liDataRunSize.QuadPart = liDataRunSize.QuadPart | bBuffer[wAttributeOffset + wIndxOffset + dwCount + i]; } if (0 == llClusterOffet) { // 第一个Data Run for (DWORD i = bHi; i > 0; i--) { liDataRunOffset.QuadPart = liDataRunOffset.QuadPart << 8; liDataRunOffset.QuadPart = liDataRunOffset.QuadPart | bBuffer[wAttributeOffset + wIndxOffset + dwCount + bLo + i]; } } else { // 第二个及多个Data Run // 判断正负 if (0 != (0x80 & bBuffer[wAttributeOffset + wIndxOffset + dwCount + bLo + bHi])) { // 负整数 for (DWORD i = bHi; i > 0; i--) { // 补码的原码=反码+1 liDataRunOffset.QuadPart = liDataRunOffset.QuadPart << 8; liDataRunOffset.QuadPart = liDataRunOffset.QuadPart | (BYTE)(~bBuffer[wAttributeOffset + wIndxOffset + dwCount + bLo + i]); } liDataRunOffset.QuadPart = liDataRunOffset.QuadPart + 1; liDataRunOffset.QuadPart = 0 - liDataRunOffset.QuadPart; } else { // 正整数 for (DWORD i = bHi; i > 0; i--) { liDataRunOffset.QuadPart = liDataRunOffset.QuadPart << 8; liDataRunOffset.QuadPart = liDataRunOffset.QuadPart | bBuffer[wAttributeOffset + wIndxOffset + dwCount + bLo + i]; } } } // 注意加上上一个Data Run的逻辑簇号(第二个Data Run可能是正整数、也可能是负整数(补码表示), 可以根据最高位是否为1来判断, 若为1, 则是负整数, 否则是正整数) liDataRunOffset.QuadPart = llClusterOffet + liDataRunOffset.QuadPart; llClusterOffet = liDataRunOffset.QuadPart; // 显示逻辑簇号和大小 liContenOffset.QuadPart = liDataRunOffset.QuadPart*wSizeOfSector*bSizeOfCluster; printf("File Content Offset:0x%llx\nFile Content Size:0x%llx\n", liContenOffset.QuadPart, (liDataRunSize.QuadPart*wSizeOfSector*bSizeOfCluster)); // 计算下一个Data Run List偏移 dwCount = dwCount + bLo + bHi + 1; } } } else if (0xFFFFFFFF == dwAttribute) { break; } // 获取当前属性的大小 dwSizeOfAttribute = MAKELONG(MAKEWORD(bBuffer[wAttributeOffset + 4], bBuffer[wAttributeOffset + 5]), MAKEWORD(bBuffer[wAttributeOffset + 6], bBuffer[wAttributeOffset + 7])); // 计算下一属性的偏移 wAttributeOffset = wAttributeOffset + dwSizeOfAttribute; } return TRUE;}
    程序测试我们在 main 函数中调用上述封装好的函数,定位文件:H:\NtfsTest\520.exe,成功定位文件:

    总结这个程序需要管理员权限运行,因为我们使用 CreateFile 函数打开磁盘,这一步操作,需要权限才可操作成功。
    这个程序实现起来不是很难,关键是理解起来并不是那么容易。需要大家对 NTFS 要有足够的了解,熟练地使用掌握 80H属性、90H属性、A0H属性的数据含义,同时,还需要了解 Data Run 的分析。还是建议大家先阅读之前写的 “NTFS文件系统介绍及文件定位” 这篇文章,这里提到的知识点,都在这个程序中体现了。
    参考参考自《Windows黑客编程技术详解》一书
    2 回答 2018-12-20 12:06:16
  • NTFS文件系统介绍及文件定位

    背景在日常生活中,我们打开我们的电脑操作各种文件。我们都知道,文件数据都存储在硬盘上,但是,硬盘中存储的数据都是0、1的二进制数据,我们的电脑怎么从这一大堆0、1数据中知道哪个是哪个文件呢?
    这就是文件系统在起作用,它对硬盘的数据设置格式规则,存储数据的时候,就按这个存储规则进行存储,那么,在读取数据文件的时候,再按照相应规则读取还原数据,就形成了我们看到的文件了。
    现在,本文就介绍目前比较流行的 NTFS 文件系统及其格式定义,并给出一个使用 NTFS 文件系统进行文件定位的例子,模拟 NTFS 定位文件的过程原理。我把分析过程整理成文档,分享给大家。
    NTFS介绍NTFS 文件系统概念文件系统是操作系统用于明确磁盘或分区上的文件的方法和数据结构,即在磁盘上组织文件的方法。文件系统是对应硬盘的分区的,而不是整个硬盘,不管是硬盘只有一个分区,还是几个分区,不同的分区可以有着不同的文件系统。
    NTFS(New Technology File System)是运行在 Windows NT 操纵系统环境和Windows NT 高级服务器网络操作环境的文件系统,随着 Windows NT 操作系统的诞生而产生。NTFS 文件系统具有安全性高、稳定性好、不易产生文件碎片的优点,使得它成为主流的文件系统。
    NTFS 文件系统相关概念
    分区:分区是磁盘的基本组成部分,被划分的磁盘一部分
    卷:NTFS以卷为基础,卷建立在分区的基础上,当以NTFS来格式化磁盘分区时就创建了一个卷
    簇:NTFS使用簇作为磁盘空间的分配和回收的基本单位
    逻辑簇号(LCN):对卷中所有的簇从头至尾进行编号
    虚拟簇号(VCN):对于文件内的所有簇进行编号
    主文件表(\$MFT):$MFT是卷的核心,存放着卷中所有数据,包括:定位和恢复文件的数据结构、引导程序数据和记录整个卷的分配分配状态的位图等
    文件记录:NTFS不是将文件仅仅视为一个文本库或二进制数据,而是将文件作为许多属性和属性值的集合来处理;每个文件或文件夹在元文件\$MFT均有一个文件记录号
    常驻属性:文件属性值能直接存储在$MFT记录中
    非常驻属性:不能直接存储在\$MFT记录中,需要在\$MFT之外为其分配空间进行存储

    NTFS数据存放方式
    NTFS 文件系统以文件的形式来对数据进行管理,以簇为单位来存储数据的。在NTFS里边的分区的簇大小的规律:

    如果分区小于512M ,簇大小1扇区
    如果分区大于512M小于1G,簇大小为2个扇区
    如果分区大于1G小于2G,簇大小为4个扇区
    如果分区大于2GB,簇大小为8个扇区

    NTFS常见元文件列表
    分区引导扇区 DBR$Boot元文件由分区的第一个扇区(即 DBR)和后面的 15 个扇区(即 NTLDR 区域)组成,其中 DBR 由“跳转指令”、“OEM代号”、“BPB”、“引导程序”和“结束标志”组成:

    对于,DBR 部分的字段含义如下图所示。其中,我们需要重点关注每个扇区的字节总数、簇大小、\$MFT主文件记录表的开始簇号。

    文件记录在 NTFS 文件系统中,磁盘上的所有数据都是以文件的形式存储,其中包括元文件每个文件都有一个或多个文件记录,每个文件记录占用两个扇区,即 1024 字节。而 \$MFT 元文件就是专门记录每个文件的文件记录。
    由于 NTFS 文件系统是通过 \$MFT 来确定文件在磁盘上的位置以及文件的属性,所以\$MFT 是非常重要的,\$MFT 的起始位置在 DBR 中有描述。\$MFT 的文件记录在物理上是连续的,并且从 0 开始编号;\$MFT 的前 16 个文件记录总是元文件的,并且顺序是固定不变的。
    文件记录由两部分构成,一部分是文件记录头,另一部分是属性列表,最后结尾是四个“FF”:

    文件记录头解析对于文件记录头中每个数据的含义如下,其中,需要重点关注的是偏移为 0x14,长度为 2 字节的第一个属性的偏移地址,根据这个字段可以获取文件记录中第一个属性的位置。

    文件记录属性属性有两种,分为常驻属性和非常驻属性。在属性中,偏移 0x8,长度为 1 字节的字段,就是区分了常驻属性和非常驻属性。值为 0x00 表示常驻属性,0x01 表示非常驻属性。
    常驻属性头的每个字段含义如下所示。其中,要重点关注属性类型、属性长度、是否为常驻属性还是非常驻属性。

    非常驻属性头的每个字段含义如下所示。其中,要重点关注属性类型、属性长度、是否为常驻属性还是非常驻属性、Data Run 的偏移地址以及 Data Run 的数据信息。

    数据运行列表 Data Run List我们可以由上面知道,当属性为非常驻属性的时候,属性中就会有一个字段来表示 Data Run。当属性不能存放完数据,系统就会在NTFS数据区域开辟一个空间存放,这个区域是以簇为单位的。Data Run List 就是记录这个数据区域的起始簇号和大小。它的含义分析如下:

    Data Run的第一个字节分高4位和低4位。其中,高4位表示文件内容的起始簇号在Data Run List中占用的字节数。低4位表示文件内容簇数在Data Run List中占用的字节数。
    Data Run的第二个字节开始表示文件内容的簇数,接着表示文件内容的起始簇号。
    Data Run可以指示空间的大小以及偏移位置,例如上述中给的例子,起始簇号为:A2 59 00(10639616),数据大小为:C0 14(49172)。
    对于多个Data Run的情况,第一个Data Run的起始簇号是一个正整数,而第二个Data Run开始,起始簇号偏移是分正负的。可以根据起始簇号偏移的最高位是否来判断,若为1,则是负整数(补码表示);否则,是正整数。而且,从第二个Data Run开始,起始簇号偏移都是相对于上一个Data Run的起始簇号来说的。下面举个例子,方便大家理解。
    例如,有这么一个Data Run如下所示:
    31 01 FD 0A 28 21 01 AB FA 21 01 4A F5 21 01 91 C1 00我们可以看到上面一共有4个Data Run,分别如下:
    第1个Data Run

    31 01 FD 0A 28
    正整数:第一个Data Run的起始簇号都是正整数起始簇号:28 0A FD(2624253)

    第2个Data Run

    21 01 AB FA
    负整数:起始簇号偏移FA AB的最高位是1,所以是负整数(补码),所以FA AB(-1365)起始簇号:相对于上一个Data Run的偏移,所以为:2624253-1365=2622888

    第3个Data Run

    21 01 4A F5
    负整数:起始簇号偏移F5 4A的最高位是1,所以是负整数(补码),所以F5 4A(-2742)起始簇号:相对于上一个Data Run的偏移,所以为:2622888-2742=2620146

    第4个Data Run

    21 01 91 C1
    负整数:起始簇号偏移C1 91的最高位是1,所以是负整数(补码),所以C1 91(-15983)起始簇号:相对于上一个Data Run的偏移,所以为:2620146-15983=2604163

    几个重要的属性接下来,我们重点讲解下几个重要的属性:80H属性、90H属性以及 A0H属性。
    80H属性80H属性是文件数据属性,该属性容纳着文件的内容,文件的大小一般指的就是未命名数据流的大小。该属性没有最大最小限制,最小情况是该属性为常驻属性。当在数据在属性内没有办法展示完全的时候,就需要Data Run的帮助,那么这时属性就为常驻属性,文件数据就存储在 Data Run指向的簇当中。
    90H属性90H属性是索引根属性,该属性是实现NTFS的B+树索引的根节点,它总是常驻属性。该属性的结构如下图:

    其中,索引根的字段含义如下所示:

    索引头的字段含义如下所示:

    索引项的字段含义如下所示:

    A0属性A0属性是索引分配属性,也是一个索引的基本结构,存储着组成索引的B+树目录索引子节点的定位信息。它总是常驻属性:

    根据上图A0H属性的 Data Run List 可以找到索引区域,偏移到索引区域所在的簇:

    其中,标准索引头的解释如下。要注意,下面的索引项偏移加上0x18。

    索引项的解释如下:

    基于 NTFS 文件定位思路及例子NTFS定位文件大致过程如下:

    根据BDP,获取扇区大小、簇大小以及\$MFT起始扇区
    根据$MFT位置,计算根目录的文件记录,一般在 5 号文件记录
    查找80H、90H、A0H属性,注意常驻属性和非常驻属性
    获取 Data Run,从 Data Run 中定位到起始簇后,再分析索引项可以得到文件名等信息
    根据80H属性中的数据流就可以找到文件真正的数据

    接下来,我来演示怎么使用 NTFS 文件系统定位出 H:\NtfsTest\520.exe 文件。
    首先,我们使用 WinHex 软件,打开 H 盘的分区引导扇区 DBR,我们可以从中获取:每个扇区大小为 0x200 字节;每个簇大小为 0x08 个扇区;\$MFT 开始簇号为 0x0C0000。
    所以,我们根据以上信息计算出 $MFT 开始的偏移地址为:
    0x0C0000 * 0x08 * 0x200 = 0xC000 0000

    然后,我们就开始计算根目录的文件记录,它是 5 号文件记录,而且每个文件记录大小为两个扇区 1024 字节。所以,根目录的偏移地址为:
    0xC000 0000 + 0x5 * 0x2 * 0x200 = 0xC000 1400
    接着,我们跳转到 0xC000 1400 地址处,从从文件记录中,查找 80H、90H、10H 属性,因为是要获取 NtfsTest 文件夹的位置,所以,我们定位到 A0H 属性:

    我们可以从偏移 0x20 处获取 Data Run 的偏移地址 0x0048。然后,在偏移 0x0048 中获取 Data Run 数据:11 01 2C 00。从 Data Run 中,可以知道数据大小为 0x01 个簇,起始簇号为 0x2C。其中,0x2C 簇的偏移地址为 0x2C000。
    我们跳转到 0x2C000 地址处,开始按照标准索引头、索引项的含义,从标准索引头中获取第一个索引项的偏移位置,注意要加上0x18;然后,再从索引项中获取文件名称的偏移位置,查看名称是否为 NtfsTest 文件夹,若不是,则继续获取下一索引项的偏移位置,继续获取名称匹配。若找到名称,则获取文件的 \$MTF 参考号。
    按照上面的顺序,我们找到了 NtfsTest 所在的索引项:

    所以,我们可以获取到文件的 \$MTF 参考号为:0x58E0。那么,偏移地址为:
    0xC000 0000 + 0x58E0 * 2* 0x200 = 0xC163 8000
    我们继续跳转到偏移位置 0xC163 8000,接下来要寻找 520.exe 文件名称.:我们根据文件记录找到第一个属性的偏移位置,然后再根据属性大小,获取下一个属性的偏移位置。以此查找 80H 属性、90H 属性、A0H 属性。
    我们可以在 90H 属性中,从索引项中获取到 520.exe 的文件名称,然后可以得到 520.exe 文件的 \$MTF 参考号为:0x5A0B。那么偏移地址为:
    0xC000 0000 + 0x5A0B * 2* 0x200 = 0xC168 2C00

    然后,我们直接跳转到 0xC168 2C00 地址处,就是 520.exe 的文件记录了。我们直接找到 80H 文件数据属性,从偏移 0x20 获取 Data Run 的偏 移 0x40,然后在偏移 0x40 获取 Data Run 数据:32 42 2E C7 85 64 00。

    根据 Data Run,我们知道数据大小为 0x2E42 个簇,数据起始簇号为 0x6485C7,即偏移地址为:
    0x6485C7 * 0x8 * 0x200 = 0x6 485C 7000
    这样,0x6 485C 7000 地址处就存储着 H:\NtfsTest\520.exe 文件的数据。

    本文参考自《WINDOWS黑客编程技术详解》、内核篇、第十三章 文件管理技术、第三小节 文件管理之NTFS解析
    1 回答 2018-12-20 12:06:28
  • MBR主分区拓展分区逻辑分区介绍

    背景主引导记录(MBR,Main Boot Record)是位于磁盘最前边的一段引导(Loader)代码。它负责磁盘操作系统(DOS)对磁盘进行读写时分区合法性的判别、分区引导信息的定位,它由磁盘操作系统(DOS)在对硬盘进行初始化时产生的。
    MBR 位于硬盘的 0 磁头、0 柱面、1 扇区,大小为 512 字节。它里面包含着操作系统里的分区信息。现在,我就简单介绍怎么从MBR作为入口点,获取系统的主分区、拓展分区以及逻辑分区。
    硬盘基础知识介绍硬盘物理结构硬盘的物理结构如下面两幅图片所示。正面包括:空气过滤片、主轴、音圈马达、永磁铁、磁盘、磁头、磁头臂等。
    反面包括:主控制芯片、数据传输芯片、高速数据缓存芯片等,其中主控制芯片负责硬盘数据读写指令等工作。


    硬盘逻辑结构新买来的硬盘是不能直接使用的,必须对它进行分区并进行格式化才能储存数据经过格式化分区之后,逻辑上每个盘片的每一面都会被分为磁道、扇区、柱面这几个虚拟的概念。

    磁道当磁盘旋转时,磁头若保持在一个位置上,则每个磁头都会在磁盘表面划出一个圆形轨迹,这些圆形轨迹就叫做磁道。

    扇区分区格式化磁盘时,每个盘片的每一面都会划分很多同心圆的磁道,而且还会将每个同心圆进一步的分割为多个的圆弧,这些圆弧就是扇区。

    柱面硬盘通常由一个或多个盘片构成,而且每个面都被划分为数目相等的磁道,并从外缘开始编号(即最边缘的磁道为0磁道,往里依次累加)。如此磁盘中具有相同编号的磁道会形成一个圆柱,此圆柱称为磁盘的柱面磁盘的柱面数与一个盘面上的磁道数是相等的。每个盘面都有一个磁头,因此,盘面数等于总的磁头数
    硬盘工作原理硬盘的工作原理就是利用盘片上的磁性粒子的极性来记录保存数据的,其实就是电磁转换的原理。
    硬盘存储数据的位置是在盘片的表面,但是盘片的材料一般为合金材料或者是玻璃材料,并非磁性材料,因此不具备记录数据的条件,需要在盘片上附着一层磁性粉,这些磁粉在硬盘工作过程中,通过磁头释放磁性信号将磁粉做以不同的磁化从而记录数据,另外,硬盘在进行数字记录是,首先要将记录的数据信息转为二进制数,然后将磁化状态记录在磁粉介质之上。
    硬盘驱动器加电后,磁盘片由主轴电机驱动进行高速旋转,设置在盘片表面的磁头会在电路控制下径向移动到指定位置然后将数据存储或读出来;当系统向硬盘写入数据时,磁头中写数据电流产生磁场使盘片表面磁性物质状态发生改变,并在写电流磁场消失后仍能保持,当系统从硬盘中读取数据时,磁头经过盘片指定区域,盘片表面磁场使磁头产生感应电流或线圈阻抗产生变化,经过相关电路处理后还原成数据。
    MBR介绍MBR (Master Boot Record),即主引导记录,又叫做主引导扇区。是计算机开机后访问硬盘时所必须要读取的首个扇区,它在硬盘上的三维地址为:0磁头、0柱面、1扇区。
    MBR结构512 字节的 MBR ,可细分为 5 个部分:代码区(440字节)、选用磁盘标志(4字节)、保留字段(2字节)、主分区表(64字节)、结束标志(2字节)。其中,主分区表中,存储着 4 个主分区的信息,每个分区信息大小为 16 字节。

    主分区表结构硬盘分区表占据主引导扇区的 64 个字节,可以对 4 个分区的信息进行描述,其中每个分区的信息占据 16 个字节。那么,每个分区表 16 字节可以分成 8 个部分:分区状态(1字节)、分区起始磁头号(1字节)、分区起始扇区号(2字节)、文件系统标志(1字节)、分区结束磁头号(1字节)、分区结束扇区号(2字节)、分区起始相对扇区号(4字节)、分区总扇区数(4字节)。
    其中,只有活动分区才可以引导系统,只能设置一个活动分区,随便哪个分区都可以安装系统,但是只有活动分区可以引导系统,系统引导文件都是放在活动分区的。

    拓展分区和逻辑分区可以把扩展分区整体上也看做是一个“主分区”,在主分区表中也有一个表项描述了这个扩展分区的信息,就像上文中描述主分区的表项类似,这个扩展分区表项描述了扩展分区的起始扇区位置和整个扩展分区的大小,在这个扩展分区内部,我们可以创建多个逻辑分区,你可以把整个扩展分区当作是一个新的“硬盘”,在扩展分区内部的逻辑分区是由与之相对应的逻辑分区表项进行维护的。
    四个表项全部对应的是主分区,这四个分区全部都是由主分区表这个结点进行维护管理,也就是说一个结点对应多个主分区。
    在扩展分区中,每一个逻辑分区都有一个与之对应的逻辑管理结点,逻辑结点中,第一个表项描述了当前结点对应的逻辑分区信息,第二个表项描述的下一个逻辑管理结点的位置。逻辑分区表项的意义和主分区表项的意义是一致的,但是需要注意的是表项中给出的起始扇区地址,这些地址是相对的地址。

    例子Win7 旗舰版 32 位系统中,有 3 个磁盘:C盘(系统盘,大小28.9GB)、E盘(主分区,大小390MB)、F盘(逻辑分区,256MB)、G盘(逻辑分区,375MB)。
    主分区表分析我们直接使用 WinHex 获取从MBR 中获取 64 字节分区表的数据,数据截图如下所示:

    我们结合上述讲到的分区表字段意义来对这 64 字节的数据进行分析。
    第一个主分区表
    分区状态(1字节):0x80;分区起始磁头号(1字节):0x20;分区起始扇区号(2字节):0x0021;文件系统标志(1字节):0x07;分区结束磁头号(1字节):0xFE;分区结束扇区号(2字节):0xFFFF;分区起始相对扇区号(4字节):0x00000800;分区总扇区数(4字节):0x039FF000。
    其中,分区状态为0x80,表示激活分区;文件系统标志为 0x07,表示 NTFS 文件系统;分区总扇区数 0x039FF000,乘以每个扇区大小 512 字节,得到分区大小约为 28.9 GB。
    第二个主分区表
    分区状态(1字节):0x00;分区起始磁头号(1字节):0xFE;分区起始扇区号(2字节):0xFFFF;文件系统标志(1字节):0x07;分区结束磁头号(1字节):0xFE;分区结束扇区号(2字节):0xFFFF;分区起始相对扇区号(4字节):0x039FF800;分区总扇区数(4字节):0x000C3800。
    其中,分区状态为0x00,表示非激活分区;文件系统标志为 0x07,表示 NTFS 文件系统;分区总扇区数 0x000C3800,乘以每个扇区大小 512 字节,得到分区大小约为 391 M。
    第三个主分区表
    分区状态(1字节):0x00;分区起始磁头号(1字节):0xFE;分区起始扇区号(2字节):0xFFFF;文件系统标志(1字节):0x0F;分区结束磁头号(1字节):0xFE;分区结束扇区号(2字节):0xFFFF;分区起始相对扇区号(4字节):0x03AC3001;分区总扇区数(4字节):0x0013BFFF。
    其中,分区状态为0x00,表示非激活分区;文件系统标志为 0x0F,表示拓展分区;分区总扇区数 0x0013BFFF,乘以每个扇区大小 512 字节,得到分区大小约为 631 M,恰好是两个逻辑分区 F 盘和 G 盘的大小总和。
    拓展分区分析我们继续对上面的拓展分区进行详细分析。
    上面第 3 个主分区表的信息为拓展分区的信息:分区状态(1字节):0x00;分区起始磁头号(1字节):0xFE;分区起始扇区号(2字节):0xFFFF;文件系统标志(1字节):0x0F;分区结束磁头号(1字节):0xFE;分区结束扇区号(2字节):0xFFFF;分区起始相对扇区号(4字节):0x03AC3001;分区总扇区数(4字节):0x0013BFFF。
    于是,我们来到其实相对扇区为 0x03AC3001 的位置,这里就是拓展分区的开始。那么获取拓展分区的 64 字节的主分区表,在扩展分区中,每一个逻辑分区都有一个与之对应的逻辑管理结点,逻辑结点中,第一个表项描述了当前结点对应的逻辑分区信息,第二个表项描述的下一个逻辑管理结点的位置。

    第一个逻辑结点表项
    分区状态(1字节):0x00;分区起始磁头号(1字节):0xFE;分区起始扇区号(2字节):0xFFFF;文件系统标志(1字节):0x07;分区结束磁头号(1字节):0xFE;分区结束扇区号(2字节):0xFFFF;分区起始相对扇区号(4字节):0x0000003F;分区总扇区数(4字节):0x0007FFC0。
    其中,分区状态为0x00,表示非激活分区;文件系统标志为 0x07,表示 NTFS 文件系统;分区总扇区数 0x0007FFC0,乘以每个扇区大小 512 字节,得到分区大小约为 256 M。
    要额外注意的是,分区起始相对扇区号(4字节):0x0000003F,它是相对于主分区表中的拓展分区的起始扇区号来说的,所以,它实际上的绝对扇区号为 0x03AC3001 + 0x0000003F = 0x03AC3040。
    第二个逻辑节点表项
    分区状态(1字节):0x00;分区起始磁头号(1字节):0xFE;分区起始扇区号(2字节):0xFFFF;文件系统标志(1字节):0x05;分区结束磁头号(1字节):0xFE;分区结束扇区号(2字节):0xFFFF;分区起始相对扇区号(4字节):0x0007FFFF;分区总扇区数(4字节):0x000BC000。
    其中,分区状态为0x00,表示非激活分区;文件系统标志为 0x05,表示 拓展分区逻辑结点;分区总扇区数0x000BC000,乘以每个扇区大小 512 字节,得到分区大小约为 375 M。
    逻辑分区我们继续对上面的拓展分区中的第二项逻辑结点表项继续分析。
    分区状态(1字节):0x00;分区起始磁头号(1字节):0xFE;分区起始扇区号(2字节):0xFFFF;文件系统标志(1字节):0x05;分区结束磁头号(1字节):0xFE;分区结束扇区号(2字节):0xFFFF;分区起始相对扇区号(4字节):0x0007FFFF;分区总扇区数(4字节):0x000BC000。
    要额外注意的是,分区起始相对扇区号(4字节):0x0007FFFF,它是相对于主分区表中的拓展分区的起始扇区号来说的,所以,它实际上的绝对扇区号为 0x03AC3001 + 0x0007FFFF = 0x03B43000。
    于是,我们来到其实相对扇区为 0x03B43000 的位置,这里就是拓展分区的开始。那么获取拓展分区的 64 字节的主分区表:

    这时,只有一个逻辑节点节表项,以为是最后一个逻辑分区了。
    分区状态(1字节):0x00;分区起始磁头号(1字节):0xFE;分区起始扇区号(2字节):0xFFFF;文件系统标志(1字节):0x07;分区结束磁头号(1字节):0xFE;分区结束扇区号(2字节):0xFFFF;分区起始相对扇区号(4字节):0x00000800;分区总扇区数(4字节):0x000BB800。
    要额外注意的是,分区起始相对扇区号(4字节):0x00000800,它是相对于0x03B43000 来说的,所以,它实际上的绝对扇区号为 0x03B43000 + 0x00000800 = 0x03B43800。
    这样,我们这个例子到这里就结束了。我们直接从 MBR 扇区中,获取到了系统的所有主分区、拓展分区以及逻辑分区的大小及其绝对起始扇区号。接下来,只需要再了解 NTFS 文件系统的格式,就可以根据硬盘数据直接定位到硬盘上的文件数据了。这样,就可以实现直接修改硬盘数据来操作文件,而不需要装个操作系统。
    参考参考自《Windows黑客编程技术详解》一书
    2 回答 2018-12-20 12:06:54
显示 30 到 45 ,共 15 条
eject