修改http请求文件为本地文件的一种方法:hook InternetReadFile 和 HttpOpenRequest
今天没事的时候学了一下easyhook来hook本进程API,确实很简单就能hook。然后想到这个问题:替换webbrowser请求的文件为本地文件。有什么用就不说了,都懂。因为没有用API写过http方面的东西,所以先hook了几个函数,其中InternetReadFile是webbrowser用来获取文件的,而文件句柄可以来源于internetopenurl和 HttpOpenRequest等API,挨个下一下钩子就知道用的是 HttpOpenRequest。当然,获取方法是多种多样的,也可以用x64dbg等调试工具。确定下来hook这两个可以达成目标就可以了:
1、从HttpOpenRequest知道要下载的是哪个文件,过滤需要替换的那个。
2、HttpOpenRequest的返回值就是打开文件句柄了,在InternetReadFile中识别这个句柄就可以。
3、在给InternetReadFile的自定义例程中,对需要替换的文件进行替换。
接下来,让我们考虑一下替换的流程:webbrowser调用InternetReadFile的时候,先到达我们的自定义例程,然后由我们调用API函数InternetReadFile,于是我们可以在自定义函数中把相应文件句柄的数据请求吃掉——把我们的数据写入缓冲区,而后直接返回不掉用API。对于我们不关心的文件句柄调用API。
查看InternetReadFile的API声明:
<DllImport("wininet.dll", SetLastError:=True)> Public Shared Function InternetReadFile(ByVal hFile As IntPtr, ByVal lpBuffer As IntPtr, ByVal dwNumberOfBytesToRead As Integer, ByRef lpdwNumberOfBytesRead As Integer) As Boolean End Function
可以知道,第一个参数是HttpOpenRequest返回的文件句柄,lpbuffer是接收数据的缓冲区,dwnumberofbytestoread是期望读取的字节数,即缓冲区大小,lpdwnumberofbytesread是实际写入缓冲区的字节数。返回值表示函数调用是否成功。所以,当lpdwnumberofbytesread为0且函数返回值为true时,达到文件结尾——函数调用成功却没有数据写入,说明数据写完了。于是,在我们的自定义例程中也需要遵守该约定——当写入数据时返回实际写入的数据大小,写完数据后再次被调用时返回0并进行清理。首先,来看一下比较简单的一个hook:
Imports System.Runtime.InteropServices Imports System.Text Public Class HookHttpOpenRequest <DllImport("wininet.dll")> Public Shared Function HttpOpenRequestW(hConnect As IntPtr, szVerb As IntPtr, szURI As IntPtr, szHttpVersion As IntPtr, szReferer As IntPtr, accetpType As IntPtr, dwflags As Integer, dwcontext As IntPtr) As IntPtr End Function Private Delegate Function HttpOpenRequestDelegate(hConnect As IntPtr, szVerb As IntPtr, szURI As IntPtr, szHttpVersion As IntPtr, szReferer As IntPtr, accetpType As IntPtr, dwflags As Integer, dwcontext As IntPtr) As IntPtr Private Shared hook As EasyHook.LocalHook = Nothing Friend Shared Sub Install() Using hook If EasyHook.NativeAPI.GetModuleHandle("wininet.dll") = IntPtr.Zero Then EasyHook.NativeAPI.LoadLibrary("wininet.dll") End If hook = EasyHook.LocalHook.Create(EasyHook.LocalHook.GetProcAddress("wininet.dll", "HttpOpenRequestW"), New HttpOpenRequestDelegate(AddressOf sendProc), Nothing) hook.ThreadACL.SetInclusiveACL(New Integer() {0}) End Using End Sub Friend Shared Sub UnInstall() Using hook If hook IsNot Nothing Then hook.ThreadACL.SetExclusiveACL(New Integer() {0}) End If End Using End Sub Private Shared Function sendProc(hConnect As IntPtr, szVerb As IntPtr, szURI As IntPtr, szHttpVersion As IntPtr, szReferer As IntPtr, accetpType As IntPtr, dwflags As Integer, dwcontext As IntPtr) As IntPtr Dim uri As String = Marshal.PtrToStringUni(szURI) Dim result As IntPtr = HttpOpenRequestW(hConnect, szVerb, szURI, szHttpVersion, szReferer, accetpType, dwflags, dwcontext) If uri.Contains("/56896-20170216102630488-270057596.jpg") Then '根据名称区分要替换的图片.
HookInternetReadFile.CheatFileHandle = result
End If
Return result
End Function
End Class
easyhook用起来确实比较简单,首先是注入过程,因为webbrowser对wininte.dll的加载是请求第一个页面时,所以可能导致这个DLL不在进程空间,那么先加载它。之后的hook非常易懂(函数名我没有修改,复制粘贴的之前写的sendhook),唯一需要注意的是实际hook的过程调用的是ThreadACL.SetInclusiveACL,unhook类似。在自定义函数中,首先调用API,得到句柄,然后根据要替换的名称来确定是否启动给InternetReadFile的自定义例程。而后,看一下对数据的处理过程:
Imports System.IO Imports System.Runtime.InteropServices Public Class HookInternetReadFile <DllImport("wininet.dll", SetLastError:=True)> Public Shared Function InternetReadFile(ByVal hFile As IntPtr, ByVal lpBuffer As IntPtr, ByVal dwNumberOfBytesToRead As Integer, ByRef lpdwNumberOfBytesRead As Integer) As Boolean End Function Private Delegate Function InternetReadFileDelegate(ByVal hFile As IntPtr, ByVal lpBuffer As IntPtr, ByVal dwNumberOfBytesToRead As Integer, ByRef lpdwNumberOfBytesRead As Integer) As Boolean Private Shared hook As EasyHook.LocalHook = Nothing Friend Shared CheatFileHandle As IntPtr = IntPtr.Zero '要替换的文件的句柄,来源于HttpOpenRequest的返回值。 Friend Shared CheatFile() As Byte = File.ReadAllBytes(My.Application.Info.DirectoryPath & "\abc.jpg") '用于替换的文件 Private Shared curcnt As Integer = 0 Friend Shared Sub Install() Using hook If EasyHook.NativeAPI.GetModuleHandle("wininet.dll") = IntPtr.Zero Then EasyHook.NativeAPI.LoadLibrary("wininet.dll") End If hook = EasyHook.LocalHook.Create(EasyHook.LocalHook.GetProcAddress("wininet.dll", "InternetReadFile"), New InternetReadFileDelegate(AddressOf sendProc), Nothing) hook.ThreadACL.SetInclusiveACL(New Integer() {0}) End Using End Sub Friend Shared Sub UnInstall() Using hook If hook IsNot Nothing Then hook.ThreadACL.SetExclusiveACL(New Integer() {0}) End If End Using End Sub Private Shared Function sendProc(ByVal hFile As IntPtr, ByVal lpBuffer As IntPtr, ByVal dwNumberOfBytesToRead As Integer, ByRef lpdwNumberOfBytesRead As Integer) As Boolean If hFile = CheatFileHandle Then If curcnt = CheatFile.Length Then CheatFileHandle = IntPtr.Zero curcnt = 0 lpdwNumberOfBytesRead = 0 Else If curcnt + dwNumberOfBytesToRead <= CheatFile.Length Then lpdwNumberOfBytesRead = dwNumberOfBytesToRead Marshal.Copy(CheatFile, curcnt, lpBuffer, lpdwNumberOfBytesRead) curcnt += dwNumberOfBytesToRead Else lpdwNumberOfBytesRead = CheatFile.Length - curcnt Marshal.Copy(CheatFile, curcnt, lpBuffer, lpdwNumberOfBytesRead) curcnt = CheatFile.Length End If End If Return True Else Return InternetReadFile(hFile, lpBuffer, dwNumberOfBytesToRead, lpdwNumberOfBytesRead) End If End Function End Class
因为是一个基本结构范例,所以偷懒直接用了参数,这里应该有错误处理过程才行。hook的过程和前面一致,只是在自定义函数中处理把自定义数据写入缓冲区然后返回了。具体的API过程没有跟踪,所以不知道不调用InternetReadFile这个API会不会有内存泄漏之类的什么问题,如果需要调用也非常简单:在恰当的时机循环读取一次就可以了。最后是窗体代码:
Public Class Form1 Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load wb.Navigate("http://images2015.cnblogs.com/blog/56896/201702/56896-20170216102630488-270057596.jpg") End Sub Private Sub butGotoUrl_Click(sender As Object, e As EventArgs) Handles butGotoUrl.Click wb.Refresh() End Sub Private Sub chkCheat_CheckedChanged(sender As Object, e As EventArgs) Handles chkCheat.CheckedChanged If chkCheat.Checked Then HookHttpOpenRequest.Install() HookInternetReadFile.Install() Else HookHttpOpenRequest.UnInstall() HookInternetReadFile.UnInstall() End If End Sub End Class
窗体上一个webbrowser重命名为wb,一个checkbox重命名为chkcheat,一个button重命名为butgotourl,另外,在程序所在目录放一个abc.jpg。对,这个范例就是这么简陋,好在现在就可以测试了。如果图片换名字了,那需要修改url地址的同时修改
If uri.Contains("/56896-20170216102630488-270057596.jpg") Then '根据名称区分要替换的图片
才可以正确运行。