Punching holes into firewalls
or "Why firewalls shouldn't be considered a ultimate weapon for network security"
or "Secure TCP-into-HTTP tunnelling guide"
This document is located at http://sebsauvage.net/punching/index.html
This document explains how to securely tunnel any TCP-based protocol (SMTP, POP3, telnet...) in simple HTTP requests.
It is heavily inspired from http://proxytunnel.sourceforge.net/paper.php, although it does not use the same tools and does not cover outside to inside data flows (backward tunnels).
Introduction
Firewalls are heavily used to secure private networks (home or corporate). Usually, they are used to protect the network from:
intrusions from outsiders
misuse from insiders
In a TCP/IP environment, the typical corporate firewall configuration is to block everything (both incoming and outgoing), and give access to the internet only through a HTTP proxy. The proxy usually has filtering capabilities (censors URLs and file types), and access to the proxy often requires credentials (login/password). This gives greater contol to the network administrator over what and who is going in and out of the network.
Still, this should not considered a ultimate weapon, and network administrators should not rely on the firewalls only.
Encapsulation is the basis of networking. For example, HTTP is encapsulated by TCP, TCP is encapsulated by IP, and IP is often encapsulated in PPP or Ethernet.
Encapsulating protocols in an unsual way is often reffered as tunnelling.
As soon as you let a single protocol out, tunelling allows to let anything go through this protocol, and thus through the firewall.
This paper demonstrates how to encapsulate any TCP-based protocol (SMTP, POP3, NNTP, telnet...) into HTTP, thus bypassing the firewall protection/censorship (depending on your point of view)
A word of warning:
In many countries and corporate environments, bypassing a firewall is forbidden and exposes you to sanctions, redundancy, legal proceedings and - in some countries - death penalty.
You are warned.
Nevertheless, in some countries this kind of firewall/proxy bypassing is the only way to ensure free speech (such as China or United Arab Emirates where the government severly censors the internet and where firewall bypassing is a national sport.)
Now you known what you're doing, let's move on.
--------------------------------------------------------------------------------
The problem
Say you want to fetch your mail from your ISP mail server. You usually simply connect to port 110 on the POP server of your ISP.
Trouble: there is a Big Bad firewall which blocks everything.
Well... it does not exactly block everything: it lets HTTP out through a proxy.
Let's encapsulate our POP3 connection into HTTP.
--------------------------------------------------------------------------------
The tools
We need:
A computer on the internet which has unrestricted access to the internet, such as a home ADSL computer.
GNU HTTP Tunnel (http://www.nocrew.org/software/httptunnel.html). It encapsulates TCP into HTTP requests.
SSH is a secure shell (http://www.openssh.com). It provides secure (and compressed) channels between two hosts using SSL. Besides providing a shell (like telnet), it also provides file copy (scp) and TCP port forwarding (tunnelling). We will use the port forwarding feature.
Why not use GNU HTTP Tunnel alone ?
In principle, only HTTP Tunnel is necessary. But this is not desirable:
the tunnel is public: anyone can use your tunnel. Your could be held liable for what anybody has done with your tunnel.
the tunnel is cleartext: anyone can spy on your connection. Your passwords (SMTP, POP3, telnet...) are transmitted in clear text.
the tunnel is not protected: anyone can alter the datastream.
you have to run a new instance of the HTTP Tunnel client and the server for each new tunnel you want to set up.
This is where ssh come in. ssh provides:
authentication (only authorised users can use the tunnel)
privacy (no one can spy on what's going through the tunnel)
integrity (no one can tamper data going through the tunnel)
easy tunnel set-up (you can create a new tunnel with a single ssh command on the client side).
These tools are available on Unix/Linux and Windows environments.
The whole chain
Let's see how this works. Here is the full chain:
Technically speaking, once this chain is established, connecting to OfficeComputer:800 is identical to connecting to pop3server:110.
The mail client will not see the difference.
On the office computer:
TCP data sent to port 800 is encrypted by ssh, which forwards data to port 900.
ssh stream sent to port 900 is chunked in individual HTTP requests by the HTTP Tunnel client and sent to the home computer through the proxy.
On the home computer:
the HTTP Tunnel server receives HTTP requests, decapsulates and re-assembles the ssh stream and forwards it to port 22 (to the ssh server).
the ssh server decrypts the datastream and forwards it to the pop3server on port 110.
As TCP is a bi-directionnaly datastream, once established, the TCP connection can pass data back and forth through the HTTP proxy.
The administrator of the HTTP proxy cannot see which protocol is used, which server is contacted (except the home computer), nor the nature of transmitted data.
Setting up the tunnel
To create the tunnel as in our example above:
On the home computer (server):
sshd (start the ssh server)
hts --forward-port localhost:22 80 (start the HTTP Tunnel server)
On the office computer (client):
htc --forward-port 900 --proxy HttpProxy:3128 HomeComputer:80 (start the HTTP Tunnel client)
ssh -L 800:pop3server:113 sshlogin@localhost -p 900 (start the ssh client)
Then read your email with your mail program at localhost:800
Notes:
If your proxy requires authentication, add --proxy-authorization login:password to the htc command line.
sshlogin is your ssh login name on the ssh server on the Home computer.
You can set up as many additionnal tunnels as you want with:
ssh -L localport:destinationServer:destinationPort sshlogin@localhost -p 900
(localport is the local port you want to map to a destination server outside the firewall (destinationServer:destinationPort)).
Drawbacks of this solution:
it does not work for UDP-based protocols (NFS, chat...).
it does not work for programs which act as server (most games, chat, peer-to-peer...)
HTTP encapsulations and proxy delays can add some latency.
Good point of this solution:
Setting up the server is easy.
By using ports above 1024, setting up the client does not require administratror (root) privileges.
Multiple users can use the server to create multiple tunnels to any destination. Each user has its own private tunnels.
This tunnel can secure communications even if the proxy does not accept to proxy HTTPS.
This tunnel does not require the HTTP proxy to accept the CONNECT command.
This tunnel can work on proxies which are not capable of - or forbid - proxying of HTTPS (port 443).
With Linux Live CDs like Knoppix this can be a great solution for cybercafés: Live Linux CD ensures there is no lurking keylogger or troyan, and the tunnel ensures that the cybercafé owner, a troyaned computer or the government cannot sniff your passwords, spy on your data or censor websites. I especially think of China here.
--------------------------------------------------------------------------------
Conlusion
As you can see, setting up such tunnels does not requires advanced skills, especially with the recent Linux distributions which come with pre-installed and pre-configured ssh servers.
With a little more skills, it is possible to tunnel just about everything into everything. For example, it is possible to tunnel PPP into HTTP, providing a full IP-stack tunnelling, including ICMP (ping...), DNS and servers (backward tunnels).
Opensource and commercial VPN solutions also come into mind.
See references for programs and papers about firewall bypassing below.
Security is not only a matter of firewall configuration, it must be seen at a larger scale. Do not rely on the firewall alone.
Censorship bypassing should not be only considered as a terrorist or hacker weapon, but also as tools for privacy, free speech, democraty and human rights protection (Please read papers written by PGP-author Philip Zimmerman, they are very instructive).
--------------------------------------------------------------------------------
References
Articles and software about tunnelling and firewall/proxy/censorship bypassing:
ProxyTunnel : http://proxytunnel.sourceforge.net
TCP-into-HTTP(S) tunneling program ; requires the HTTP proxy to accept the CONNECT command.
SSH Tunnelling howto : http://proxytunnel.sourceforge.net/papers/muppet-200204.html
Instructions for TCP-into-HTTP tunnelling using SSH and ProxyTunnel.
Bypassing internet censorship : http://www.zensur.freerk.com
Ways to bypass censorship, using various technics.
How to Bypass Most Firewall Restrictions and Access the Internet Privately : http://www.buzzsurf.com/surfatwork/
Document on firewalls bypassing and tunnelling.
Breaking Firewalls with OpenSSH and PuTTY : http://souptonuts.sourceforge.net/sshtips.htm
Using putty and OpenSSH when the firewall allows port 22 in.
The ennemy within: Firewalls and backdoors : http://www.securityfocus.com/infocus/1701
Article about firewalls and security.
GNU HTTP Tunnel : http://www.nocrew.org/software/httptunnel.html
Opensource TCP-into-HTTP tunnelling.
PlugDaemon : http://www.taronga.com/plugdaemon/
TCP port forwarder with HTTPS proxy support.
OpenSSH : http://www.openssh.com
Opensource ssh client and server.
OpenSSH for Windows: http://sshwindows.sourceforge.net/
Windows version of OpenSSH. (The server only works under 2000/XP, but a 9x version is planned.)
OpenVPN : http://openvpn.sourceforge.net/
Excellent, secure and flexible opensource SSL-based VPN program. Can work over UDP, TCP or even HTTP trough proxies.
1st April RFC 3093: http://ietf.org/rfc/rfc3093.txt
So-called Firewall Enhancement Protocol (FEP).
DesProxy : http://desproxy.sourceforge.net
Allows to make direct TCP connections through HTTP proxy which accept the CONNECT command. Does not require external server as in our solution above.
TransConnect: http://transconnect.sourceforge.net
Uses the CONNECT proxy HTTP command to make direct connections to the internet.
CorkScrew: http://www.agroman.net/corkscrew/
Tunnels SSH traffic through HTTP proxies.
HTTP Bridge: http://httpbridge.sourceforge.net
A CGI-based secure HTTP proxy written in Java. Requires Tomcat.
PsiPhon: http://psiphon.civisec.org/
Password-protected HTTP proxy server designed to circumvent censorship.
HTTP Proxy Lib: http://httppc.sourceforge.net
A library to add TCP-into-HTTP capability to your programs.
STunnel: http://stunnel.mirt.net
Generic TCP-into-SSL wrapper.
STunnel: http://www.stunnel.org
Generic TCP-into-SSL wrapper.
SSLProxy: http://www.obdev.at/products/ssl-proxy/
Generic TCP-into-SSL wrapper. No longuer maintained (Authors recommend STunnel instead).
TLSWrap : http://tlswrap.sunsite.dk
TLS/SSL wrapper/proxy for FTP.
HTTP Tunnel : http://www.http-tunnel.com
Commercial encrypted TCP-into-HTTP tunnelling service. Low-bandwith free service available.
HTTP Tunnel : http://http-tunnel.sourceforge.net/
Opensource SOCKS proxy capable of tunnelling traffic through HTTP proxies. Client and server provided. Server can run standalone (perl) or on a hosted server (php).
HTTPort : http://www.htthost.com
Commercial TCP-into-HTTP tunnelling service (encrypted).
BarracudaDrive : http://barracudaserver.com/examples/BarracudaDrive/index.html
Free TCP-into-HTTPS tunnelling server with HTTP proxy support (command-line java client), including a web-based file manager, web-based chat and graphical file transfer java client.
Hamachi : http://hamachi.cc/
Free and simplified UDP-based VPN solution capable of traversing NAT firewalls.
Your-Freedom : http://www.your-freedom.net/
Free TCP-into-HTTP tunnelling service. Additional sevices are not free.
Socks via HTTP : http://lightbox.ath.cx/socks/
A SOCKS proxy which tunnels all traffing into HTTP requests. Can also tunnel static ports. Client and server provided. Written in Java.
Zebedee : http://www.winton.org.uk/zebedee/
Opensource cross-plateform TCP/UDP-into-SSL tunnel.
Socks2HTTP : http://www.totalrc.net
Commercial Socks proxy which tunnels TCP and UDP into HTTP.
SSL Explorer : http://www.sshtools.com/products/enterprise/ssl-explorer/ssl-explorer.jsp
TCP-into-HTTPS tunnelling and more. The clients only requires a Java-enabled browser.
Tunnelier : http://www.bitvise.com/tunnelier.html
Commercial (free for personal use) SSH client for Windows with easy tunnelling features, graphical SFTP client, FTP-to-SFTP bridge, etc.
nph-proxy : http://www.jmarshall.com/tools/cgiproxy/
Free CGI-based HTTP proxy, capable of HTTPS proxying and URL obfuscation. Perl source code provided.
For more information, see:
http://directory.google.com/Top/Computers/Security/Internet/Privacy/
http://directory.google.com/Top/Computers/Security/Virtual_Private_Networks/
Tunnelling projects on SourceForge.net: http://sourceforge.net/search/?words=tunnel
2008年9月25日星期四
2008年9月19日星期五
an ideal firewall
Design of ideal personal firewall
Published: 2006/07/01
The following article describes the design of the ideal Windows personal firewall from programmers point of view. First of all the ideal personal firewall is secure. So, this article is about secure design leaving other features like easy of use in the background. At first we say something about the common concept of personal firewalls and then we show important rules for the security design of personal firewall that should be respected during the development of Windows personal firewalls. During our analyses we examine whether those below mentioned rules, that are important for the security, are respected by tested products. In the following article we often use a term 'firewall' but we always mean 'Windows personal firewall'.
Contents:
Common concept
Self-protection
Verification of own components
Inbound and outbound protection
Process protection
File and component protection
Driver protection
Service protection
Registry protection
Protection of other system resources
Parent process control
Control of automatically started programs
Sniffing protection
Protection of system resources
No ring3 hooks
--------------------------------------------------------------------------------
Common concept
This paragraph describes common concept of Windows personal firewalls. It is not necessary to implement the firewall in a similar way to have it secure. Common personal firewall is implemented as three or four separate components.
Kernel driver
The first part is kernel driver. Its has two main functions and that is why it is sometimes implemented in two components rather than in one. The first function is a packet filter. Usually on the NDIS, TDI or both levels this driver checks every packet that comes in from the network or goes out to the network. This is also known as inbound and outbound connection protection. As mentioned in the More about personal firewalls article there exist some personal firewalls that do not implement neither inbound nor outbound connection protection. However, these products also have kernel drivers because of their second function. The second function is called sandbox. The most common methods of the sandbox implementation are SSDT hooks and SSDT GDI hooks. The driver of the firewall replaces some system functions with its own code that verifies the rights of calling application and either denies the action or passes the execution to original code. These methods allows the firewall to control all the possible dangerous activity of applications such as attempts to open files, processes, registry keys, modify firewall settings, automatically respond to its queries etc.
System service
There are special user mode processes called system services. These processes have special functions and behaviour in the system. They run under privileged system user rather than under common user account. This fact allows services to run independently of user and they run also when no user is logged in. The role of service in the personal firewall is to secure the communication between main components. The service receives messages from the GUI and from the kernel driver and forwards this messages to each other. For example if the firewall is in the learning mode, the driver code in hooked SSDT function may be unable to decide whether to allow or deny the action because there is no corresponding rule for the action in the database. In such case it wants the user to decide. This requires to send a message to GUI to show the dialog and to receive the answer from it. This communication is usually implemented through the service component. The service of the firewall is sometimes used to ensure that the GUI is always available for the user.
Graphical user interface
The graphical user interface (GUI) is the user part of the firewall. It often implements a trayicon from which the administration of the firewall is available. Another important function of the GUI is to ask user for the decision of actions when the firewall is in the learning mode.
--------------------------------------------------------------------------------
Self-protection
This is rule no. 1 for all security products, not only for personal firewalls. No matter the perfection of other features, if the firewall is not able to secure itself it is useless. If a malicious activity is able to switch off, disable or destroy the personal firewall it is equivalent not to have any personal firewall at all. All parts of the firewall have to be protected including its processes, files, registry entries, drivers, services and other system resources and objects.
--------------------------------------------------------------------------------
Verification of own components
The verification of own components is very close to the above mentioned Self-protection. Firewalls are usually complex programs and they are often implemented in more than one module or component. In such case there are a few main modules that are executed by the operating system. During the startup or in the middle of run these modules loads other modules of the firewall. We say that the modules are loaded dynamically. It is necessary to check the integrity of all dynamically loaded modules. This implies that the integrity checker must be implemented in one of the main modules.
--------------------------------------------------------------------------------
Inbound and outbound protection
The description of inbound and outbound protection is given in the More about personal firewalls article in the Good and bad products paragraph. We have already mentioned that there exist firewalls that do not implement this kind of protection. However, the majority of personal firewalls do implement it.
--------------------------------------------------------------------------------
Process protection
Every privileged process must be protected against several dangerous actions. Firstly, no malicious application can terminate the process. Secondly, it must not be possible to modify its code or data. Thirdly, it must not be possible to execute any code in a context of any privileged process. This point also includes DLL injection.
--------------------------------------------------------------------------------
File and component protection
The protection of files is very close to Process protection. If a malicious code is able to replace files of privileged applications it is equivalent to modify their code flow when they run. There are two ways how to implement the protection of files. The first way (active protection) is to prevent write and delete access to files that belong to privileged applications. Because this can be hard to implement many firewall coders choose the second way - to check the integrity of modules (component protection). In this case the firewall allows malicious code to damage or replace files of privileged applications. If such application is about to run its modules are verified and the execution is stopped or reported to the user. The file protection is also needed for all system files.
--------------------------------------------------------------------------------
Driver protection
Windows operating systems trust its drivers. This mean that every code that is run by the driver is trusted and thus it is allowed to execute even protected processor's instruction and has potential access to all system resources. This is why it is necessary to implement a part of security software like personal firewall as a system driver. However, it is also why it is necessary to control loading of new drivers and to protect existing drivers. Malicious programs must not be able to install drivers or modify already loaded drivers.
--------------------------------------------------------------------------------
Service protection
Since a part of the firewall is usually implemented as a system service the protection of system services is also necessary. But it is not only the firewall component that has to be protected. To install a new service is easy way for malware how to persist in the system because system services can be set to run every system start. What is more, a malicious service can be dangerous also because it runs even if no user is logged on. Creation, deletion and control of system services must be protected actions.
--------------------------------------------------------------------------------
Registry protection
Windows registry contains a lot of important system information. Settings of system components can be changed using the registry. An incorrect modification of some registry objects can easily cause system to become unstable or unable to boot. There are many registry keys and values that should be protected against modifications of malicious applications.
--------------------------------------------------------------------------------
Protection of other system resources
There are also different system resources and objects in Windows operating systems. Some of them can be dangerous if they are controlled by malware. One of these objects is a well known section '\Device\PhysicalMemory' which can be used to gain the complete control of the system if it is not protected. The firewall must protect those objects that can be misused by malware.
--------------------------------------------------------------------------------
Parent process control
We already know that it is necessary to protect privileged processes. Probably the easiest way how to implement process protection is to control opening of processes and threads. However, if the process protection is implement in this way it is also important to implement Parent process control. Every process in the system has to be created by some other process - its parent. The parent is always given two handles when new it creates child process. These are handle to the process object and handle to its main thread. The given process handle is opened with a full access and thus the parent process can control its child completely. This is why the firewall must restrict the execution of privileged processes. Moreover, the parent process control should be implemented even if the firewall security design does not protect processes via control of opening of processes and threads. Some privileged processes can be misused to execute privilege action if they are run with specific command line arguments. Many firewalls do not distinguish between the execution of privileged and unprivileged processes. They restrict the process creation in general such that only those applications that were selected before are able to create child processes.
--------------------------------------------------------------------------------
Control of automatically started programs
The firewall should protect those places in the operating system that can be used by malware to persist in the system after the reboot. If we allow users to run new unknown applications then there is no chance to protect the system against executing malicious application. And users often download and install or run new applications. The firewall is able to restrict actions of malicious applications such that they are not able to damage the system. However, if the malware application persists in the system it can damage it later when a new security bug is discovered. This is why the firewall should control those applications that are run automatically e.g. after every system start or user logon.
--------------------------------------------------------------------------------
Sniffing protection
Spyware like keyloggers or packet sniffers are dangerous applications because they are made to steal the most sensitive data users can have - their passwords. But not only passwords are targets of these applications. Personal information, personal correspondence or business documents are also sensitive information that must be protected. The firewall has to protect sensitive data not only when they are complete in form of files but also when they are made or being transferred. Keyloggers can receive every key stroke user makes and thus assemble the whole information letter by letter. Packet sniffers are waiting for the messages to be transferred using some network interface and they make copies of sent messages. There are many ways how to implement spyware programs to collect sensitive data and all of them have to be protected by the firewall.
--------------------------------------------------------------------------------
Protection of system resources
Every system has limited resources. Windows workstations are able to deal with a few thousands of objects. This number is sufficient for every work of common users. However, if a malicious program creates thousands of threads the system become unusable and such an action cause Denial of service (DoS). The firewall should limit unprivileged applications to cause DoS. There should be a limit set for number of threads, open files, used memory and other system resources used by unprivileged applications.
--------------------------------------------------------------------------------
No ring3 hooks
The ring3 (or usermode) hooking is a technique that can be use to implement a personal firewall or its parts. However, ring3 hooks can be used only for special features and never for security critical features. A protection implemented by ring3 hooks can be easily bypassed by malicious applications. Ring3 hooks must not be used to restrict behaviour of unknown applications. They can be used very rarely to modify or control the behaviour of privileged applications that are guaranteed not to bypass ring3 hooks.
Published: 2006/07/01
The following article describes the design of the ideal Windows personal firewall from programmers point of view. First of all the ideal personal firewall is secure. So, this article is about secure design leaving other features like easy of use in the background. At first we say something about the common concept of personal firewalls and then we show important rules for the security design of personal firewall that should be respected during the development of Windows personal firewalls. During our analyses we examine whether those below mentioned rules, that are important for the security, are respected by tested products. In the following article we often use a term 'firewall' but we always mean 'Windows personal firewall'.
Contents:
Common concept
Self-protection
Verification of own components
Inbound and outbound protection
Process protection
File and component protection
Driver protection
Service protection
Registry protection
Protection of other system resources
Parent process control
Control of automatically started programs
Sniffing protection
Protection of system resources
No ring3 hooks
--------------------------------------------------------------------------------
Common concept
This paragraph describes common concept of Windows personal firewalls. It is not necessary to implement the firewall in a similar way to have it secure. Common personal firewall is implemented as three or four separate components.
Kernel driver
The first part is kernel driver. Its has two main functions and that is why it is sometimes implemented in two components rather than in one. The first function is a packet filter. Usually on the NDIS, TDI or both levels this driver checks every packet that comes in from the network or goes out to the network. This is also known as inbound and outbound connection protection. As mentioned in the More about personal firewalls article there exist some personal firewalls that do not implement neither inbound nor outbound connection protection. However, these products also have kernel drivers because of their second function. The second function is called sandbox. The most common methods of the sandbox implementation are SSDT hooks and SSDT GDI hooks. The driver of the firewall replaces some system functions with its own code that verifies the rights of calling application and either denies the action or passes the execution to original code. These methods allows the firewall to control all the possible dangerous activity of applications such as attempts to open files, processes, registry keys, modify firewall settings, automatically respond to its queries etc.
System service
There are special user mode processes called system services. These processes have special functions and behaviour in the system. They run under privileged system user rather than under common user account. This fact allows services to run independently of user and they run also when no user is logged in. The role of service in the personal firewall is to secure the communication between main components. The service receives messages from the GUI and from the kernel driver and forwards this messages to each other. For example if the firewall is in the learning mode, the driver code in hooked SSDT function may be unable to decide whether to allow or deny the action because there is no corresponding rule for the action in the database. In such case it wants the user to decide. This requires to send a message to GUI to show the dialog and to receive the answer from it. This communication is usually implemented through the service component. The service of the firewall is sometimes used to ensure that the GUI is always available for the user.
Graphical user interface
The graphical user interface (GUI) is the user part of the firewall. It often implements a trayicon from which the administration of the firewall is available. Another important function of the GUI is to ask user for the decision of actions when the firewall is in the learning mode.
--------------------------------------------------------------------------------
Self-protection
This is rule no. 1 for all security products, not only for personal firewalls. No matter the perfection of other features, if the firewall is not able to secure itself it is useless. If a malicious activity is able to switch off, disable or destroy the personal firewall it is equivalent not to have any personal firewall at all. All parts of the firewall have to be protected including its processes, files, registry entries, drivers, services and other system resources and objects.
--------------------------------------------------------------------------------
Verification of own components
The verification of own components is very close to the above mentioned Self-protection. Firewalls are usually complex programs and they are often implemented in more than one module or component. In such case there are a few main modules that are executed by the operating system. During the startup or in the middle of run these modules loads other modules of the firewall. We say that the modules are loaded dynamically. It is necessary to check the integrity of all dynamically loaded modules. This implies that the integrity checker must be implemented in one of the main modules.
--------------------------------------------------------------------------------
Inbound and outbound protection
The description of inbound and outbound protection is given in the More about personal firewalls article in the Good and bad products paragraph. We have already mentioned that there exist firewalls that do not implement this kind of protection. However, the majority of personal firewalls do implement it.
--------------------------------------------------------------------------------
Process protection
Every privileged process must be protected against several dangerous actions. Firstly, no malicious application can terminate the process. Secondly, it must not be possible to modify its code or data. Thirdly, it must not be possible to execute any code in a context of any privileged process. This point also includes DLL injection.
--------------------------------------------------------------------------------
File and component protection
The protection of files is very close to Process protection. If a malicious code is able to replace files of privileged applications it is equivalent to modify their code flow when they run. There are two ways how to implement the protection of files. The first way (active protection) is to prevent write and delete access to files that belong to privileged applications. Because this can be hard to implement many firewall coders choose the second way - to check the integrity of modules (component protection). In this case the firewall allows malicious code to damage or replace files of privileged applications. If such application is about to run its modules are verified and the execution is stopped or reported to the user. The file protection is also needed for all system files.
--------------------------------------------------------------------------------
Driver protection
Windows operating systems trust its drivers. This mean that every code that is run by the driver is trusted and thus it is allowed to execute even protected processor's instruction and has potential access to all system resources. This is why it is necessary to implement a part of security software like personal firewall as a system driver. However, it is also why it is necessary to control loading of new drivers and to protect existing drivers. Malicious programs must not be able to install drivers or modify already loaded drivers.
--------------------------------------------------------------------------------
Service protection
Since a part of the firewall is usually implemented as a system service the protection of system services is also necessary. But it is not only the firewall component that has to be protected. To install a new service is easy way for malware how to persist in the system because system services can be set to run every system start. What is more, a malicious service can be dangerous also because it runs even if no user is logged on. Creation, deletion and control of system services must be protected actions.
--------------------------------------------------------------------------------
Registry protection
Windows registry contains a lot of important system information. Settings of system components can be changed using the registry. An incorrect modification of some registry objects can easily cause system to become unstable or unable to boot. There are many registry keys and values that should be protected against modifications of malicious applications.
--------------------------------------------------------------------------------
Protection of other system resources
There are also different system resources and objects in Windows operating systems. Some of them can be dangerous if they are controlled by malware. One of these objects is a well known section '\Device\PhysicalMemory' which can be used to gain the complete control of the system if it is not protected. The firewall must protect those objects that can be misused by malware.
--------------------------------------------------------------------------------
Parent process control
We already know that it is necessary to protect privileged processes. Probably the easiest way how to implement process protection is to control opening of processes and threads. However, if the process protection is implement in this way it is also important to implement Parent process control. Every process in the system has to be created by some other process - its parent. The parent is always given two handles when new it creates child process. These are handle to the process object and handle to its main thread. The given process handle is opened with a full access and thus the parent process can control its child completely. This is why the firewall must restrict the execution of privileged processes. Moreover, the parent process control should be implemented even if the firewall security design does not protect processes via control of opening of processes and threads. Some privileged processes can be misused to execute privilege action if they are run with specific command line arguments. Many firewalls do not distinguish between the execution of privileged and unprivileged processes. They restrict the process creation in general such that only those applications that were selected before are able to create child processes.
--------------------------------------------------------------------------------
Control of automatically started programs
The firewall should protect those places in the operating system that can be used by malware to persist in the system after the reboot. If we allow users to run new unknown applications then there is no chance to protect the system against executing malicious application. And users often download and install or run new applications. The firewall is able to restrict actions of malicious applications such that they are not able to damage the system. However, if the malware application persists in the system it can damage it later when a new security bug is discovered. This is why the firewall should control those applications that are run automatically e.g. after every system start or user logon.
--------------------------------------------------------------------------------
Sniffing protection
Spyware like keyloggers or packet sniffers are dangerous applications because they are made to steal the most sensitive data users can have - their passwords. But not only passwords are targets of these applications. Personal information, personal correspondence or business documents are also sensitive information that must be protected. The firewall has to protect sensitive data not only when they are complete in form of files but also when they are made or being transferred. Keyloggers can receive every key stroke user makes and thus assemble the whole information letter by letter. Packet sniffers are waiting for the messages to be transferred using some network interface and they make copies of sent messages. There are many ways how to implement spyware programs to collect sensitive data and all of them have to be protected by the firewall.
--------------------------------------------------------------------------------
Protection of system resources
Every system has limited resources. Windows workstations are able to deal with a few thousands of objects. This number is sufficient for every work of common users. However, if a malicious program creates thousands of threads the system become unusable and such an action cause Denial of service (DoS). The firewall should limit unprivileged applications to cause DoS. There should be a limit set for number of threads, open files, used memory and other system resources used by unprivileged applications.
--------------------------------------------------------------------------------
No ring3 hooks
The ring3 (or usermode) hooking is a technique that can be use to implement a personal firewall or its parts. However, ring3 hooks can be used only for special features and never for security critical features. A protection implemented by ring3 hooks can be easily bypassed by malicious applications. Ring3 hooks must not be used to restrict behaviour of unknown applications. They can be used very rarely to modify or control the behaviour of privileged applications that are guaranteed not to bypass ring3 hooks.
how to make a corect ssdt hook
Plague in (security) software drivers
by Jakub Břečka & David Matoušek, members of Matousec – Transparent security Research team,
special thanks to Ondřej Vlček of ALWIL Software for corrections
Published: 2007/09/18
Last update: 2008/04/14 - BSODhook version 2.0.0 released, the information about it moved to a separate web page
During our security analyses of personal firewalls and other security-related software that uses SSDT hooking, we found out that many vendors simply do not implement the hooks in a proper way. This allows local Denial of Service by unprivileged users or even privilege escalations exploits to be created. 100% of tested personal firewalls that implement SSDT hooks do or did suffer from this vulnerability! This article reviews the results of our testing and describes how a proper SSDT hook handler should be implemented. We also introduce BSODhook – a handy tool for every developer that deals with SSDT hooks and a possible cure for the plague in today's Windows drivers world.
Contents:
Introduction
The bug
Structures with pointers
Other issues and exploitability
About BSODhook utility
Research results
Conclusion
--------------------------------------------------------------------------------
Introduction
Hooking kernel functions by modifying the System Service Descriptor Table (SSDT) is a very popular method of implementation of additional security features and is used frequently by personal firewalls and other security and low-level software. Although undocumented and despised by Microsoft, this technique can be implemented in a correct and stable way. However, many software vendors do not follow the rules and recommendations for kernel-mode code writing and many drivers that implement SSDT hooking do not properly validate the parameters of the hooking functions.
Microsoft's Common Driver Reliability Issues document describes the correct parameter checking and contains many important notes to problems related to writing Windows drivers. Many vendors of today's software do not bother to read such documents and their implementations are thus vulnerable, and making the stable and trustworthy Windows kernel unreliable.
Parameters to SSDT function handlers are passed directly from user-mode and therefore must be checked before they are used. This article shows some incorrect implementations of SSDT hooking functions and describes how a proper validity check should be performed on various parameter types. To understand the following text, you will need some knowledge of Windows NT architecture. We do not cover a proper implementation of SSDT hooking techniques here. We focus on parameter validation problems. Some related and interesting information can be also found in Microsoft's Memory Management: What Every Driver Writer Needs to Know document.
--------------------------------------------------------------------------------
The bug
For a demonstration of the bug, see the following code sample:
NTSTATUS HookNtOpenProcess(OUT PHANDLE ProcessHandle,IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,IN PCLIENT_ID ClientId OPTIONAL)
{
if (ClientId->UniqueProcess==ProtectedProcess)
return STATUS_ACCESS_DENIED;
...
}
Example 1: No parameter validation at all.
This code shows a hook handler for a Windows Native API function NtOpenProcess and it implements a simple security check that will deny all requests to open a protected process with a specified process ID. For the purpose of this article, it is not important how such a hook is set, which is also a task that is pretty complicated if it is supposed to be performed correctly, or what is done after the security check, which is also non-trivial.
Unfortunately, the implementation in Example 1 is incorrect, because this hook handler function receives the parameters exactly as they were sent from user-mode, and it must therefore check all parameters' validity before using them. If a similar hook was implemented in user-mode (e.g. to intercept kernel32.OpenProcess or ntdll.NtOpenProcess calls), the need for a parameter validation would not be so necessary, because an invalid memory read or write in user-mode cannot corrupt the system's stability nor can it be exploited to escalate privileges. Such a lack of parameter checking will only corrupt or crash the application that invoked the invalid API call.
In the kernel however, the situation is different. All parameters coming from user-mode must be checked for validity before they are used. This is especially related to pointers to various structures. To access structure contents, the pointer must be dereferenced, which is a risky operation that can result in one of the following:
A pointer to a valid memory is dereferenced – success.
The pointer is invalid and points into user-mode memory – a page fault will occur and its handler will raise a SEH exception of ACCESS_VIOLATION, which, if not handled, will crash the system.
The pointer is invalid and points into kernel-mode memory – this will not raise an exception, but instead a bugcheck (BSOD) will be invoked immediately.
SEH exceptions can be caught by enclosing the unsafe operations in try/except blocks. However, using try/except only is not enough, see the following code sample:
NTSTATUS HookNtOpenProcess(OUT PHANDLE ProcessHandle,IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,IN PCLIENT_ID ClientId OPTIONAL)
{
HANDLE ProcessId;
try
{
ProcessId=ClientId->UniqueProcess;
} except(EXCEPTION_EXECUTE_HANDLER)
{
return STATUS_INVALID_PARAMETER;
}
if (ProcessId==ProtectedProcess)
return STATUS_ACCESS_DENIED;
...
}
Example 2: Invalid parameter validation, only user-mode pointers are validated properly.
In this function, the parameter ClientId is properly validated only for user-mode pointers. If ClientId points into an invalid kernel memory, the dereference will cause a BSOD to occur. A correct approach is to use the ProbeForRead function:
NTSTATUS HookNtOpenProcess(OUT PHANDLE ProcessHandle,IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,IN PCLIENT_ID ClientId OPTIONAL)
{
HANDLE ProcessId;
if (ClientId!=NULL)
{
try
{
if (KeGetPreviousMode()!=KernelMode)
ProbeForRead(ClientId,sizeof(CLIENT_ID),1);
ProcessId=ClientId->UniqueProcess;
} except(EXCEPTION_EXECUTE_HANDLER)
{
return STATUS_INVALID_PARAMETER;
}
if (ProcessId==ProtectedProcess)
{
return STATUS_ACCESS_DENIED;
}
}
...
}
Example 3: Correct parameter validation for single-pointer structures like CLIENT_ID.
The call to ProbeForRead must be enclosed in the try/catch block as well as the access to the user supplied memory, because if the probing fails, an exception will be raised. You should also notice that ClientId is an OPTIONAL parameter. This means that it might not be present in a correct call. If it was NULL and we did not check this, the assignment to ProcessId would fail and we will return STATUS_INVALID_PARAMETER, which would disqualify some of correct NtOpenProcess calls.
See the following pseudo-code implementation of ProbeForRead:
VOID ProbeForRead(IN CONST VOID *Address,IN SIZE_T Length,IN ULONG Alignment)
{
// check for zero Length
if (Length==0)
return;
// check the alignment of Address
if (!CheckAlignment(Address,Alignment))
RaiseException(DATATYPE_MISALIGNMENT);
// check for an integer overflow
if (IsSumOverflow((ULONG)Address,Length))
RaiseException(ACCESS_VIOLATION);
// check if Address points into user-mode memory
if (((ULONG)Address+Length)>MmUserProbeAddress)
RaiseException(ACCESS_VIOLATION);
return;
}
Example 4: Internals of ProbeForRead in pseudo-code.
ProbeForRead checks whether the given pointer points into user-mode memory by comparing it to MmUserProbeAddress, which is the lowest invalid address for user-mode buffers. MmUserProbeAddress is typically set to 0x7FFF0000, or to 0xBFFF0000 on systems with 3GB user-space memory, activated by the /3GB switch in boot.ini.
It might seem odd, that the function does not perform any memory access to the desired memory area. But in fact, it is perfectly valid to access any user-mode memory, if the access is performed inside a try/except block. So, ProbeForRead only checks if the structure lies in the user-mode memory. This also means, that a function with a ProbeForRead call cannot be called from kernel-mode with a kernel memory pointer, because only user-mode addresses will pass the probe. This is why we use KeGetPreviousMode check, to allow kernel calls to pass without checking. You can find some information about it in Argument Validation in Windows NT part of Microsoft Windows NT: Design Goals article.
Note that you cannot use ProbeForRead or any similar function to perform a validity check on a kernel memory pointer. There is no easy mechanism that checks if an address in the kernel is valid or not. All kernel-mode pointers are trusted and there is no need to validate them – that is also the reason, why an invalid kernel memory access results in a BSOD and not a SEH exception. Simply, invalid kernel memory access is, unlike a user-mode memory access, a fatal error and should never occur in properly written drivers.
--------------------------------------------------------------------------------
Structures with pointers
All we discussed so far were single-pointer structures. However, some Native API functions require complex structures with additional pointers in them as parameters. See the following example of a hook handler for NtQueryValueKey:
NTSTATUS HookNtQueryValueKey(IN HANDLE KeyHandle,IN PUNICODE_STRING ValueName,
IN KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,OUT PVOID KeyValueInformation,
IN ULONG KeyValueInformationLength,OUT PULONG ResultLength)
{
if (_wcsicmp(ValueName->Buffer,L"ProtectedValue")==0)
return STATUS_ACCESS_DENIED;
...
}
Example 5: No validation for a PUNICODE_STRING parameter.
One of NtQueryValueKey function's parameters is a pointer to a UNICODE_STRING structure, which consist of three values: Length, MaximumLength and another pointer to an array of WCHARs. The code sample in Example 5 implements no parameter checking, so we know already that this is wrong.
If a function with a UNICODE_STRING is to be hooked properly, the hook handler must check the validity of the pointer to the Unicode string, before accessing the member values of this structure. Then the pointer to the array of wide characters must be validated. See the following example, which shows a correctly performed validity check:
NTSTATUS HookNtQueryValueKey(IN HANDLE KeyHandle,IN PUNICODE_STRING ValueName,
IN KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,OUT PVOID KeyValueInformation,
IN ULONG KeyValueInformationLength,OUT PULONG ResultLength)
{
UNICODE_STRING name;
WCHAR *buffer=NULL;
try
{
if (KeGetPreviousMode()!=KernelMode)
ProbeForRead(ValueName,sizeof(UNICODE_STRING),1);
RtlCopyMemory(&name,ValueName,sizeof(name));
if (name.Length==wcslen(L"ProtectedValue")*sizeof(WCHAR))
{
if (KeGetPreviousMode()!=KernelMode)
{
ProbeForRead(name.Buffer,name.Length,1);
buffer=(WCHAR*)ExAllocatePoolWithTag(NonPagedPool,name.Length,TAG);
if (!buffer) return STATUS_INSUFFICIENT_RESOURCES;
RtlCopyMemory(buffer,name.Buffer,name.Length);
} else buffer=name.Buffer;
if (_wcsnicmp(buffer,L"ProtectedValue",name.Length/sizeof(WCHAR))==0)
return STATUS_ACCESS_DENIED;
}
} except(EXCEPTION_EXECUTE_HANDLER)
{
return STATUS_INVALID_PARAMETER;
}
...
}
Example 6: Correct double-pointer structure (UNICODE_STRING) validation.
Note that we have to make a copy of ValueName before we can use its contents. If we did not make a copy and used its contents directly, another thread could change its contents after we checked its Length or after we probed its Buffer.
There are some other structures with additional pointers that appear as parameters in some Native API functions. All these pointers must be checked before they are dereferenced. See the following example, which checks the validity of a POBJECT_ATTRIBUTES parameter, which contains a pointer to a UNICODE_STRING structure. Therefore, a triple pointer check must be performed:
NTSTATUS HookNtCreateFile(OUT PHANDLE FileHandle,IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,OUT PIO_STATUS_BLOCK IoStatusBlock,
IN PLARGE_INTEGER AllocationSize OPTIONAL,IN ULONG FileAttributes,IN ULONG ShareAccess,
IN ULONG CreateDisposition,IN ULONG CreateOptions,IN PVOID EaBuffer OPTIONAL,
IN ULONG EaLength)
{
UNICODE_STRING name;
WCHAR *buffer=NULL;
try
{
if (KeGetPreviousMode()!=KernelMode)
ProbeForRead(ObjectAttributes,sizeof(OBJECT_ATTRIBUTES),1);
PUNICODE_STRING ObjectName=ObjectAttributes->ObjectName;
if (KeGetPreviousMode()!=KernelMode)
ProbeForRead(ObjectName,sizeof(UNICODE_STRING),1);
RtlCopyMemory(&name,ObjectName,sizeof(name));
if (name.Length==wcslen(L"ProtectedValue")*sizeof(WCHAR))
{
if (KeGetPreviousMode()!=KernelMode)
{
ProbeForRead(name.Buffer,name.Length,1);
buffer=(WCHAR*)ExAllocatePoolWithTag(NonPagedPool,name.Length,TAG);
if (!buffer) return STATUS_INSUFFICIENT_RESOURCES;
RtlCopyMemory(buffer,name.Buffer,name.Length);
} else buffer=name.Buffer;
if (_wcsnicmp(buffer,L"ProtectedValue",name.Length/sizeof(WCHAR))==0)
return STATUS_ACCESS_DENIED;
}
} except(EXCEPTION_EXECUTE_HANDLER)
{
return STATUS_INVALID_PARAMETER;
}
...
}
Example 7: Correct triple-pointer structure (OBJECT_ATTRIBUTES) validation.
--------------------------------------------------------------------------------
Other issues and exploitability
Even more necessary is to perform validation checks on output parameters. If a hook handler writes to the memory through a user-mode supplied pointer (which again might be wrapped in a structure), it must check that the whole write memory area is valid for writing, for example using ProbeForWrite. It will similarly produce an exception, if some part of the memory is not writable.
Note that there also is a possibility of invalidating a memory during the processing of the hook handler. Therefore, even if a user-mode memory address is validated, a driver still cannot consider it safe to read/write. The memory can be invalidated at any time, so every consequent read or write attempt must be enclosed in a try/except block.
If a driver communicates with user-mode using Windows I/O or any other mechanism, all user-mode addresses must be validated in exactly the same way (ProbeForRead and/or ProbeForWrite) as stated before. If the application logic treats such address as a structure with additional pointers, all of these must be checked too. Especially, this validation must be performed on user buffers supplied through METHOD_NEITHER I/O.
Generally, there is no common pattern for exploiting these bugs. An invalid memory read will only produce a BSOD. However, some special cases of missing ProbeForWrite validation can certainly be exploited and may lead to a privilege escalation or even a local root exploit. For example, a missing parameter validation on an OUT PHANDLE argument may, in some cases, be exploited to bypass system's security checks or modify kernel objects. The outcoming value of a newly opened handle can be predicted and if we set this parameter to point somewhere in the kernel, for example inside the kernel structures or a carefully selected address inside kernel code, we can alter the code flow and bypass access checks.
Since Windows XP, a memory write protection is keeping any driver from altering the kernel code, which effectively blocks these kinds of exploits. However, we can still overwrite any part of the kernel stack or kernel objects (for example modify current EPROCESS structure to gain privileges). In general, in case of incorrect OUT parameter validation implementation, we may have arbitrary kernel mode write possibility, which is usually enough to take over the whole machine.
--------------------------------------------------------------------------------
About BSODhook utility
We have developed a tool codenamed BSODhook that helps finding improper validation bugs in drivers that implement (not only) SSDT hooks. BSODhook (aka Kernel hooks probing tool) calls native functions with both valid and invalid parameters to produce a system crash (bugcheck). However, this tool comes with a kernel driver, which intercepts certain system functions to catch these bugchecks. Instead of crashing the system, an invalid memory access or other faulty behaviour that invoke the bugcheck will be caught, the calling thread will be terminated and the application will report that the tested API function is improperly validated. Moreover, BSODhook writes out the exact parameters of the function call that caused the crash. This allows vendors to find bugs in their drivers very quickly and efficiently.
In the second version, we have added support for SSDT GDI functions. We have also created a separate BSODhook web page and moved the information about this tool there.
You can download BSODhook right now and start probing. If you find some bugs in a software you use, which is not listed below, or if your Windows 2000 or XP kernel is not supported, please contact us. If your kernel is not supported, be sure to include a full information about your kernel version. If you are a vendor of a software that implements SSDT hooks and our tool helped you to improve it, we will be very glad if you tell us about it. Everyone is allowed to use BSODhook freely as is. There is no warranty or support for this product, but we will be glad to receive the feedback. There are also number of ways how to improve BSODhook itself, if you are an experienced Windows coder interested in doing so, feel free to contact us too.
by Jakub Břečka & David Matoušek, members of Matousec – Transparent security Research team,
special thanks to Ondřej Vlček of ALWIL Software for corrections
Published: 2007/09/18
Last update: 2008/04/14 - BSODhook version 2.0.0 released, the information about it moved to a separate web page
During our security analyses of personal firewalls and other security-related software that uses SSDT hooking, we found out that many vendors simply do not implement the hooks in a proper way. This allows local Denial of Service by unprivileged users or even privilege escalations exploits to be created. 100% of tested personal firewalls that implement SSDT hooks do or did suffer from this vulnerability! This article reviews the results of our testing and describes how a proper SSDT hook handler should be implemented. We also introduce BSODhook – a handy tool for every developer that deals with SSDT hooks and a possible cure for the plague in today's Windows drivers world.
Contents:
Introduction
The bug
Structures with pointers
Other issues and exploitability
About BSODhook utility
Research results
Conclusion
--------------------------------------------------------------------------------
Introduction
Hooking kernel functions by modifying the System Service Descriptor Table (SSDT) is a very popular method of implementation of additional security features and is used frequently by personal firewalls and other security and low-level software. Although undocumented and despised by Microsoft, this technique can be implemented in a correct and stable way. However, many software vendors do not follow the rules and recommendations for kernel-mode code writing and many drivers that implement SSDT hooking do not properly validate the parameters of the hooking functions.
Microsoft's Common Driver Reliability Issues document describes the correct parameter checking and contains many important notes to problems related to writing Windows drivers. Many vendors of today's software do not bother to read such documents and their implementations are thus vulnerable, and making the stable and trustworthy Windows kernel unreliable.
Parameters to SSDT function handlers are passed directly from user-mode and therefore must be checked before they are used. This article shows some incorrect implementations of SSDT hooking functions and describes how a proper validity check should be performed on various parameter types. To understand the following text, you will need some knowledge of Windows NT architecture. We do not cover a proper implementation of SSDT hooking techniques here. We focus on parameter validation problems. Some related and interesting information can be also found in Microsoft's Memory Management: What Every Driver Writer Needs to Know document.
--------------------------------------------------------------------------------
The bug
For a demonstration of the bug, see the following code sample:
NTSTATUS HookNtOpenProcess(OUT PHANDLE ProcessHandle,IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,IN PCLIENT_ID ClientId OPTIONAL)
{
if (ClientId->UniqueProcess==ProtectedProcess)
return STATUS_ACCESS_DENIED;
...
}
Example 1: No parameter validation at all.
This code shows a hook handler for a Windows Native API function NtOpenProcess and it implements a simple security check that will deny all requests to open a protected process with a specified process ID. For the purpose of this article, it is not important how such a hook is set, which is also a task that is pretty complicated if it is supposed to be performed correctly, or what is done after the security check, which is also non-trivial.
Unfortunately, the implementation in Example 1 is incorrect, because this hook handler function receives the parameters exactly as they were sent from user-mode, and it must therefore check all parameters' validity before using them. If a similar hook was implemented in user-mode (e.g. to intercept kernel32.OpenProcess or ntdll.NtOpenProcess calls), the need for a parameter validation would not be so necessary, because an invalid memory read or write in user-mode cannot corrupt the system's stability nor can it be exploited to escalate privileges. Such a lack of parameter checking will only corrupt or crash the application that invoked the invalid API call.
In the kernel however, the situation is different. All parameters coming from user-mode must be checked for validity before they are used. This is especially related to pointers to various structures. To access structure contents, the pointer must be dereferenced, which is a risky operation that can result in one of the following:
A pointer to a valid memory is dereferenced – success.
The pointer is invalid and points into user-mode memory – a page fault will occur and its handler will raise a SEH exception of ACCESS_VIOLATION, which, if not handled, will crash the system.
The pointer is invalid and points into kernel-mode memory – this will not raise an exception, but instead a bugcheck (BSOD) will be invoked immediately.
SEH exceptions can be caught by enclosing the unsafe operations in try/except blocks. However, using try/except only is not enough, see the following code sample:
NTSTATUS HookNtOpenProcess(OUT PHANDLE ProcessHandle,IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,IN PCLIENT_ID ClientId OPTIONAL)
{
HANDLE ProcessId;
try
{
ProcessId=ClientId->UniqueProcess;
} except(EXCEPTION_EXECUTE_HANDLER)
{
return STATUS_INVALID_PARAMETER;
}
if (ProcessId==ProtectedProcess)
return STATUS_ACCESS_DENIED;
...
}
Example 2: Invalid parameter validation, only user-mode pointers are validated properly.
In this function, the parameter ClientId is properly validated only for user-mode pointers. If ClientId points into an invalid kernel memory, the dereference will cause a BSOD to occur. A correct approach is to use the ProbeForRead function:
NTSTATUS HookNtOpenProcess(OUT PHANDLE ProcessHandle,IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,IN PCLIENT_ID ClientId OPTIONAL)
{
HANDLE ProcessId;
if (ClientId!=NULL)
{
try
{
if (KeGetPreviousMode()!=KernelMode)
ProbeForRead(ClientId,sizeof(CLIENT_ID),1);
ProcessId=ClientId->UniqueProcess;
} except(EXCEPTION_EXECUTE_HANDLER)
{
return STATUS_INVALID_PARAMETER;
}
if (ProcessId==ProtectedProcess)
{
return STATUS_ACCESS_DENIED;
}
}
...
}
Example 3: Correct parameter validation for single-pointer structures like CLIENT_ID.
The call to ProbeForRead must be enclosed in the try/catch block as well as the access to the user supplied memory, because if the probing fails, an exception will be raised. You should also notice that ClientId is an OPTIONAL parameter. This means that it might not be present in a correct call. If it was NULL and we did not check this, the assignment to ProcessId would fail and we will return STATUS_INVALID_PARAMETER, which would disqualify some of correct NtOpenProcess calls.
See the following pseudo-code implementation of ProbeForRead:
VOID ProbeForRead(IN CONST VOID *Address,IN SIZE_T Length,IN ULONG Alignment)
{
// check for zero Length
if (Length==0)
return;
// check the alignment of Address
if (!CheckAlignment(Address,Alignment))
RaiseException(DATATYPE_MISALIGNMENT);
// check for an integer overflow
if (IsSumOverflow((ULONG)Address,Length))
RaiseException(ACCESS_VIOLATION);
// check if Address points into user-mode memory
if (((ULONG)Address+Length)>MmUserProbeAddress)
RaiseException(ACCESS_VIOLATION);
return;
}
Example 4: Internals of ProbeForRead in pseudo-code.
ProbeForRead checks whether the given pointer points into user-mode memory by comparing it to MmUserProbeAddress, which is the lowest invalid address for user-mode buffers. MmUserProbeAddress is typically set to 0x7FFF0000, or to 0xBFFF0000 on systems with 3GB user-space memory, activated by the /3GB switch in boot.ini.
It might seem odd, that the function does not perform any memory access to the desired memory area. But in fact, it is perfectly valid to access any user-mode memory, if the access is performed inside a try/except block. So, ProbeForRead only checks if the structure lies in the user-mode memory. This also means, that a function with a ProbeForRead call cannot be called from kernel-mode with a kernel memory pointer, because only user-mode addresses will pass the probe. This is why we use KeGetPreviousMode check, to allow kernel calls to pass without checking. You can find some information about it in Argument Validation in Windows NT part of Microsoft Windows NT: Design Goals article.
Note that you cannot use ProbeForRead or any similar function to perform a validity check on a kernel memory pointer. There is no easy mechanism that checks if an address in the kernel is valid or not. All kernel-mode pointers are trusted and there is no need to validate them – that is also the reason, why an invalid kernel memory access results in a BSOD and not a SEH exception. Simply, invalid kernel memory access is, unlike a user-mode memory access, a fatal error and should never occur in properly written drivers.
--------------------------------------------------------------------------------
Structures with pointers
All we discussed so far were single-pointer structures. However, some Native API functions require complex structures with additional pointers in them as parameters. See the following example of a hook handler for NtQueryValueKey:
NTSTATUS HookNtQueryValueKey(IN HANDLE KeyHandle,IN PUNICODE_STRING ValueName,
IN KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,OUT PVOID KeyValueInformation,
IN ULONG KeyValueInformationLength,OUT PULONG ResultLength)
{
if (_wcsicmp(ValueName->Buffer,L"ProtectedValue")==0)
return STATUS_ACCESS_DENIED;
...
}
Example 5: No validation for a PUNICODE_STRING parameter.
One of NtQueryValueKey function's parameters is a pointer to a UNICODE_STRING structure, which consist of three values: Length, MaximumLength and another pointer to an array of WCHARs. The code sample in Example 5 implements no parameter checking, so we know already that this is wrong.
If a function with a UNICODE_STRING is to be hooked properly, the hook handler must check the validity of the pointer to the Unicode string, before accessing the member values of this structure. Then the pointer to the array of wide characters must be validated. See the following example, which shows a correctly performed validity check:
NTSTATUS HookNtQueryValueKey(IN HANDLE KeyHandle,IN PUNICODE_STRING ValueName,
IN KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,OUT PVOID KeyValueInformation,
IN ULONG KeyValueInformationLength,OUT PULONG ResultLength)
{
UNICODE_STRING name;
WCHAR *buffer=NULL;
try
{
if (KeGetPreviousMode()!=KernelMode)
ProbeForRead(ValueName,sizeof(UNICODE_STRING),1);
RtlCopyMemory(&name,ValueName,sizeof(name));
if (name.Length==wcslen(L"ProtectedValue")*sizeof(WCHAR))
{
if (KeGetPreviousMode()!=KernelMode)
{
ProbeForRead(name.Buffer,name.Length,1);
buffer=(WCHAR*)ExAllocatePoolWithTag(NonPagedPool,name.Length,TAG);
if (!buffer) return STATUS_INSUFFICIENT_RESOURCES;
RtlCopyMemory(buffer,name.Buffer,name.Length);
} else buffer=name.Buffer;
if (_wcsnicmp(buffer,L"ProtectedValue",name.Length/sizeof(WCHAR))==0)
return STATUS_ACCESS_DENIED;
}
} except(EXCEPTION_EXECUTE_HANDLER)
{
return STATUS_INVALID_PARAMETER;
}
...
}
Example 6: Correct double-pointer structure (UNICODE_STRING) validation.
Note that we have to make a copy of ValueName before we can use its contents. If we did not make a copy and used its contents directly, another thread could change its contents after we checked its Length or after we probed its Buffer.
There are some other structures with additional pointers that appear as parameters in some Native API functions. All these pointers must be checked before they are dereferenced. See the following example, which checks the validity of a POBJECT_ATTRIBUTES parameter, which contains a pointer to a UNICODE_STRING structure. Therefore, a triple pointer check must be performed:
NTSTATUS HookNtCreateFile(OUT PHANDLE FileHandle,IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,OUT PIO_STATUS_BLOCK IoStatusBlock,
IN PLARGE_INTEGER AllocationSize OPTIONAL,IN ULONG FileAttributes,IN ULONG ShareAccess,
IN ULONG CreateDisposition,IN ULONG CreateOptions,IN PVOID EaBuffer OPTIONAL,
IN ULONG EaLength)
{
UNICODE_STRING name;
WCHAR *buffer=NULL;
try
{
if (KeGetPreviousMode()!=KernelMode)
ProbeForRead(ObjectAttributes,sizeof(OBJECT_ATTRIBUTES),1);
PUNICODE_STRING ObjectName=ObjectAttributes->ObjectName;
if (KeGetPreviousMode()!=KernelMode)
ProbeForRead(ObjectName,sizeof(UNICODE_STRING),1);
RtlCopyMemory(&name,ObjectName,sizeof(name));
if (name.Length==wcslen(L"ProtectedValue")*sizeof(WCHAR))
{
if (KeGetPreviousMode()!=KernelMode)
{
ProbeForRead(name.Buffer,name.Length,1);
buffer=(WCHAR*)ExAllocatePoolWithTag(NonPagedPool,name.Length,TAG);
if (!buffer) return STATUS_INSUFFICIENT_RESOURCES;
RtlCopyMemory(buffer,name.Buffer,name.Length);
} else buffer=name.Buffer;
if (_wcsnicmp(buffer,L"ProtectedValue",name.Length/sizeof(WCHAR))==0)
return STATUS_ACCESS_DENIED;
}
} except(EXCEPTION_EXECUTE_HANDLER)
{
return STATUS_INVALID_PARAMETER;
}
...
}
Example 7: Correct triple-pointer structure (OBJECT_ATTRIBUTES) validation.
--------------------------------------------------------------------------------
Other issues and exploitability
Even more necessary is to perform validation checks on output parameters. If a hook handler writes to the memory through a user-mode supplied pointer (which again might be wrapped in a structure), it must check that the whole write memory area is valid for writing, for example using ProbeForWrite. It will similarly produce an exception, if some part of the memory is not writable.
Note that there also is a possibility of invalidating a memory during the processing of the hook handler. Therefore, even if a user-mode memory address is validated, a driver still cannot consider it safe to read/write. The memory can be invalidated at any time, so every consequent read or write attempt must be enclosed in a try/except block.
If a driver communicates with user-mode using Windows I/O or any other mechanism, all user-mode addresses must be validated in exactly the same way (ProbeForRead and/or ProbeForWrite) as stated before. If the application logic treats such address as a structure with additional pointers, all of these must be checked too. Especially, this validation must be performed on user buffers supplied through METHOD_NEITHER I/O.
Generally, there is no common pattern for exploiting these bugs. An invalid memory read will only produce a BSOD. However, some special cases of missing ProbeForWrite validation can certainly be exploited and may lead to a privilege escalation or even a local root exploit. For example, a missing parameter validation on an OUT PHANDLE argument may, in some cases, be exploited to bypass system's security checks or modify kernel objects. The outcoming value of a newly opened handle can be predicted and if we set this parameter to point somewhere in the kernel, for example inside the kernel structures or a carefully selected address inside kernel code, we can alter the code flow and bypass access checks.
Since Windows XP, a memory write protection is keeping any driver from altering the kernel code, which effectively blocks these kinds of exploits. However, we can still overwrite any part of the kernel stack or kernel objects (for example modify current EPROCESS structure to gain privileges). In general, in case of incorrect OUT parameter validation implementation, we may have arbitrary kernel mode write possibility, which is usually enough to take over the whole machine.
--------------------------------------------------------------------------------
About BSODhook utility
We have developed a tool codenamed BSODhook that helps finding improper validation bugs in drivers that implement (not only) SSDT hooks. BSODhook (aka Kernel hooks probing tool) calls native functions with both valid and invalid parameters to produce a system crash (bugcheck). However, this tool comes with a kernel driver, which intercepts certain system functions to catch these bugchecks. Instead of crashing the system, an invalid memory access or other faulty behaviour that invoke the bugcheck will be caught, the calling thread will be terminated and the application will report that the tested API function is improperly validated. Moreover, BSODhook writes out the exact parameters of the function call that caused the crash. This allows vendors to find bugs in their drivers very quickly and efficiently.
In the second version, we have added support for SSDT GDI functions. We have also created a separate BSODhook web page and moved the information about this tool there.
You can download BSODhook right now and start probing. If you find some bugs in a software you use, which is not listed below, or if your Windows 2000 or XP kernel is not supported, please contact us. If your kernel is not supported, be sure to include a full information about your kernel version. If you are a vendor of a software that implements SSDT hooks and our tool helped you to improve it, we will be very glad if you tell us about it. Everyone is allowed to use BSODhook freely as is. There is no warranty or support for this product, but we will be glad to receive the feedback. There are also number of ways how to improve BSODhook itself, if you are an experienced Windows coder interested in doing so, feel free to contact us too.
2008年9月16日星期二
微过滤器驱动开发指南
Windows文件系统过滤管理器之
0.译者序
我翻译此文出于对文件系统技术的兴趣。这就是新的文件系统过滤接口。其实也不算什么新的东西,微软开发了另一个“旧模型的”过滤驱动,称之为过滤管理器(Filter Manager)。从而提供了一系列新的接口来让你开发新的过滤器。确实这套接口变简单清晰了。你至少避免了包含无数个信息的IRP,避免了请求在各个部件中循环的发来发去,一个分发例程中处理无数中情况,一不小心系统崩溃。我不知道花了多少时间才弄明白一个简单的缓冲读请求从用户到过滤到文件系统和缓冲管理器,虚拟内存管理器之间的关系!现在你也许不需要再管他们了,仅仅做好自己的过滤工作就可以。
这套接口强大吗?能实现你想要的功能吗?你很快就发现你没有研究过sfilter就看不懂Minifilter,或者是还得从sfilter开始做起更灵活一点。微软就是这样,拿僵硬而且也不简单的东西来“简化”强大灵活但是设计上一团糟的东西,对于你来说是两者都必须学习,最后你的脑子被微软塞得满满的,不过没关系,我们已经习惯了.
此文的原文是《Filter Driver Development Guide》,出自微软的网站。我在以下这个地址下载得到此文:
http://download.microsoft.com/download/e/b/a/eba1050f-a31d-436b-9281-92cdfeae4b45/FilterDriverDeveloperGuide.doc
我尽量在翻译中使文章保持原貌。如果您认为此文无法理解,建议您首先阅读旧的文件过滤驱动的相关资料。我认为必须有文件系统和windows驱动的相关知识,才能阅读此文。
我未必总是使用规范的名词,但我总是使用最容易理解的名词。一些常用的可能不翻译,比如IRP,MDL,有驱动开发经验的人应该可以理解。另一些可能采用中文(英文)的方式。
一些解释如下:
例程(Routine):我不懂得例程和函数有什么不同。我认为例程就是函数。称为Routine而不是Function可能是为了避免其他c程序员理解得太容易。
接口(Api):编程开发接口,一个提供给你调用的函数。
流(Stream):如果你写过文件系统驱动,那么你一定知道FileObject,对你来说一个流就是一个FileObject。
文件(File):一个文件可能有多个流,因为可能多次打开,多个FileObject可能对应一个文件。
域(Field):一个数据结构中的一个数据成员。喜欢数据库的人可能称为字段。喜欢面向对象的称为数据成员。
透明(transparence):看不见,意味着也不需要管。不过请注意透明的反意词绝对不是不透明(opacity)。
不透明(opacity):不知道的。比如空指针。因为空指针指向的是什么,从空指针本身是了解不到的。所以称为不透明的指针。
回调(Callback)函数:一个由系统调用而且原则上你不能自己调的函数。
预操作(pre-operation)回调:如果打算过滤一个操作,那么这个回调出现在操作完成之前。
后操作(post-operation)回调:如果打算过滤一个操作,那么这个回调出现在操作完成之后。
1.概述
这个文档用于I/O管理器和基本文件系统之间的过滤驱动。文件系统可能是本地或者网络的。这个文档不涉及文件系统和存储设备之间的过滤驱动,比如FtDisk和DMIO.
我们将主要讨论一种新的文件系统过滤驱动模型,所谓的微过滤器(minfiter).
以前的文件系统过滤基于一个例子sfilter.使用IRP和设备对象进行过滤。我们现在称之为“旧过滤模型”
新的架构中一个关键的组件其实是一个旧过滤模型的文件系统过滤驱动,被称为“过滤管理器(Filter Manger)”.在未来,微软发行的操作系统将默认安装这个驱动。(译者注:现在,你得手工安装。)这个驱动通过提供一些库供微过滤器调用来管理所有的微过滤器。必要的头文件,库和二进制代码都在微过滤器IFSKit中。
为何要开发一个微文件系统过滤驱动?
.通过更少的工作量,得到更简单的,更可靠的过滤驱动.
.动态加载和卸载,绑定和解除绑定.
.在过滤栈中,绑定到一个合理确定的位置。
.上下文管理。快捷,干净,可靠的上下文管理,用于文件对象,流,文件,实例和卷.
.一组有用的调用.包括根据文件名寻找,高效存取,和用户态程序之间的通信,以及io排队.
.支持非回环I/O.这样,一个微过滤器发起的I/O请求可以轻松的只让栈中更下面的微过滤器以及文件系统看到了。
.仅仅过滤感兴趣的操作。不象旧过滤模型那样必须挂接每个操作入口以便把操作传递到下层.
2.术语
在过滤管理器架构中,定义了一些新的对象。为了搞清这些,这里将列出一些定义:
过滤器:在文件系统上执行一些过滤操作的一种驱动.
卷:在本地文件系统,这个对象指文件系统所管理的逻辑卷.对于网络重定向文件系统,指所有网络请求被重定向的目的。卷直接对应文件系统(无论本地或者网络)旧过滤模型中的设备对象(DEVICE_OBJECT).
实例:一个过滤器在一个卷上唯一的某层上生成的一个实例化对象。过滤管理器把所有的IO请求发到卷上的实例栈上。一个微过滤器在一个卷上可能不止一个实例.规范的例子是FileSpy.有时候把FileSpy的两个实例分别绑定在另一个过滤器的上边和下边。此时每个实例有一个私有的上下文.这个上下文包含IO操作的日志.可以用来比较一个过滤器上下的IO操作有什么不同。
文件:文件系统保存在一个磁盘上的可能包含若干个流的有名字的数据对象.
流:指一个文件中的物理数据流.
文件对象(FileObject):用来描述一个用户对一个文件中的一个物理数据流的一次打开。
回调数据(CallbackData):过滤管理器中的一种数据结构,包含了一个操作中的所有信息。对应于旧过滤模型中的IRP.
3.微过滤器安装
微过滤器可以通过一个INF文件安装。INF文件指出了这个微过滤器所支持的实例.实例的具体说明在第5节.每个实例有一组标志,还有一个唯一的数值固定了它在过滤栈中的位置。
INF文件中有一个表标明了每个实例的层级.这用来给文件系统过滤的开发商装载他们的微过滤器.有标记标明了这个微过滤器是否需要"自动的绑定".如果是,那么每个新的卷出现的时候,微过滤器都回收到一个通知.它可以在此绑定他们.绑定的时候,inf文件中的层级决定了绑定到什么层次上。
在微过滤器运行时,文件系统过滤开发商也可以在某个指定的层级上动态的生成一个实例,这可以使用FilterAttachAtAltitude()调用.这对于开发者来说可以用来进行测试和排除bug.
4.微过滤器注册
微过滤器是内核驱动。因此它必须导出一个名为DriverEntry的函数。在驱动加载的时候这个函数第一个被调用.很多微过滤器在DriverEntry()中调用FltRegisterFilter().
FilterRegisterFilter()需要传入一个参数。是一个FLT_REGISTRATION结构.包含了:一个卸载例程.实例通知回调,一组上下文回调指针,一组文件系统操作回调指针.一般情况下,微过滤过器只捕获一部分操作,因此文件系统操作回调指针可能并不多。
对于某一种操作,微过滤器可以指定一些附加的标记来指明它是否在所有的情况下都收到它们.比如,如果FLTFL_OPERATION_REGISTRATION_SKIP_PAGING_IO被指定了,微过滤器就不会收到任何此类IRP的paging I/O操作。
同样的,如果FLTFL_OPERATION_REGISTRATION_SKIP_CACHED_IO被指定了,那么只有这类操作的非缓冲请求能看见.(比如说,指定了IRP_MJ_READ类操作,那么所有的缓冲读就都不会被微过滤器捕获了。
5.开始过滤
当一个微过滤器注册自己,它就应该在某个时间调用函数FltStartFiltering()来开始过滤。并不一定要在DriverEntry中调用。不过大多数微过滤器可能是这样做的。
这个函数将激发必要的通知,导致微过滤器绑定到卷上然后开始过滤I/O操作。为此,过滤管理器会通过微过滤器的inf文件遍历它注册过的所有的实例。
每个示例都有一个层级。一个层级是一个唯一的字符串,(如"100.123456"),这个定义了微过滤器的这个实例在栈上的位置。商业版本的微过滤器层级将由微软公司来分配。
层级的数字越高,这个微过滤器绑定在栈上的位置就越高。一些示例层级提供给开发者用来实现微过滤器。这些是仅有的不会被分配的层级。层级有两个作用:一是确定两个微过滤器之间的顺序关系,尤其是有时得实现一些不用去考虑个别微过滤器什么时候加载的功能。
比如说,一个加密解密过滤器必须安装在一个防病毒的过滤的下边。否则,防病毒过滤器无法从已经加密的内容中发现病毒。另外就是提供了一个最小的测试矩阵,用来测试这些过滤驱动的互容性。如果这些驱动实例都是按一个指定的顺序在栈中的,那么测试的时候就不用再考虑排列各种不同的顺序了。
在inf文件中,一个实例和一个标记联系在一起。如果第1个位标记了,那么微过滤器不会在卷出现在系统中的时候得到通知。这样的实例应该通过过滤管理器编程接口来手工的绑定。如果第2位被设置了,即使手工的发送了一个绑定请求,微过滤器也不会收到通知来要求绑定一个实例到一个卷。
6.实例的通知
当一个实例生成的时候,一组回调函数提供来通知微过滤器。通过这些回调,微过滤器可以决定它的实例在什么时候绑定到卷上和从卷上解除了绑定。
6.1. 安装一个实例
回调例程InstatanceSetupCallback()在下列情况下被调用:
.当一个微过滤器加载的时候,每个存在的卷都会导致这个调用。
.当一个新的卷被mount.
.当FltAttachVolume被调用(内核模式)
.当FltAttachVolumeAtAltitude()被调用(内核模式)
.当FilterAttach()被调用(用户模式)
.当FilterAttachAtAltitude()被调用(用户模式)
在这个过程中,微过滤器决定是否在这个卷上生成实例。这个回调的原型如下:
typedef NTSTATUS
(*PFLT_INSTANCE_SETUP_CALLBACK) (
IN PCFLT_RELATED_OBJECTS FltObjects,
IN FLT_INSTANCE_SETUP_FLAGS Flags,
IN DEVICE_TYPE VolumeDeviceType,
IN FLT_FILESYSTEM_TYPE VolumeFilesystemType
);
FltObjects结构喊有指向微过滤器,卷,和实例的指针。这个实例指将要在InstanceSetupCallback()函数中生成的实例。Flags标记是什么操作导致激发了InstanceSetupCallback():
FLTFL_INSTANCE_SETUP_AUTOMATIC_ATTACHMENT:
这是一个微过滤器注册的时候,一个自动的绑定通知。过滤管理器为每个刚加载的微过滤器枚举所有的卷。如果是一个使用者明确的指定一个实例绑定到某一个卷,不会设置有这个标记。
FLTFL_INSTANCE_SETUP_MANUAL_ATTACHMENT:
通过调用FilterAttach()(用户态),或者是FilterAttachVolumeAtAltitude()(用户态),或者是FltAttachVolume()(内核态)所发起的一个手工的请求。
FLTFL_INSTANCE_SETUP_NEWLY_MOUNTED_VOLUME:
文件系统刚刚挂载(mount)了一个卷,所以呼叫InstanceSetupCallback()来通知微过滤器,如果它愿意可以生成实例来绑定这个卷。
在InstanceSetupCallback()中,微过滤器同时得到了卷设备类型(VolumeDeviceType)和卷文件系统类型(VolumeFilesytemType),用以判断这个卷是否过滤器所感兴趣的。同时,微过滤器可以调用FltGetVolumeProperties()来获取卷属性。通过FltSetInstanceContext()在实例上设置上下文。当然这是需要绑定的时候。它甚至可以在卷上打开或者关闭文件。
如果这个回调返回了成功,那么这个实例将绑定到卷上。如果返回了一个警告或者错误,那么不会绑定。
如果微过滤器没有指定InstanceSetup回调,那么,系统将认为用户总是返回了STATUS_SUCCESS,实例总是会生成并绑定。
6.2. 控制实例的销毁
InstanceQueryTeardown()回调仅仅在一个手工解除绑定的请求下被调用。以下操作可能导致:
FltDetachVolume() (内核模式)
FilterDetach()
(用户模式)
如果一个微过滤器没有提供这个回调,那么手工解除绑定是不被支持的。但是,卷的解挂载(dismount)和微过滤器的卸载还是允许的。
如果这个回调返回成功,那么过滤管理器开始销毁给出的实例。最后实例的InstanceTeardownStart()和InstanceTeardownComplete()会被调用。如果返回了错误或者警告,手工解除绑定会失败。推荐的错误代码有: STATUS_FLT_DO_NOT_DETACH,不过实际上你可以返回任何错误代码。
InstanceQueryTeardown()回调的原型是:
typedef NTSTATUS
(*PFLT_INSTANCE_QUERY_TEARDOWN_CALLBACK) (
IN PCFLT_RELATED_OBJECTS FltObjects,
IN FLT_INSTANCE_QUERY_TEARDOWN_FLAGS Flags
);
和InstanceSetupCallback()类似,FltObject指出了与这个销毁操作有关的微过滤器,卷和实例。
6.3. 实例解绑定的同步
如果InstanceTeardownStart()的时候已经决定要解除绑定,那么这个例程中必须做以下的事情:
(1)重设所有的未决的I/O操作(包括预操作和后操作)
(2)保证不会有新的I/O操作进入未决。
(3)对刚刚到达的操作开始最少的工作。
同时,应该做以下操作:
(1)关闭所有打开的文件。
(2)取消所有本过滤器发起的I/O请求。
(3)停止将新的工作任务排队。
然后微过滤器把控制权交还过滤管理器来继续它的销毁过程。当所有与这个实例相关的操作都排除干净或者完成了,InstanceTeardownComplete()会被调用。管理器保证此时所有此实例的存在的操作回调都完成了。这时微过滤器必须关闭所有这个实例打开的文件。
这两个回调的原型如下:
typedef VOID
(*PFLT_INSTANCE_TEARDOWN_CALLBACK) (
IN PCFLT_RELATED_OBJECTS FltObjects,
IN FLT_INSTANCE_TEARDOWN_FLAGS Reason
);
FltObjets中有微过滤器,卷和实例。Resson参数指明这次销毁的原因,可能是以下一些标记的组合:
FLTFL_INSTANCE_TEARDOWN_MANUAL:
这次销毁操作是一个手工的请求。(FilterDetach()或者 FltDetachVolume()).
FLTFL_INSTANCE_TEARDOWN_FILTER_UNLOAD:
这次销毁操作是因为微过滤器执行卸载或者是选择了把卸载请求失败掉导致的。
FLTFL_INSTANCE_TEARDOWN_MANDATORY_FILTER_UNLOAD:
这次销毁操作是一次强制卸载导致的。这种情况下不能把卸载请求失败掉。
FLTFL_INSTANCE_TEARDOWN_VOLUME_DISMOUNT: 这次销毁是一个卷被解挂载的结果。FLTFL_INSTANCE_TEARDOWN_INTERNAL_ERROR:
这次销毁是因为安装实例的时候的一个内部错误导致的,比如内存不足。
请注意没有返回值。InstanceTeardownStart()和InstanceTeardownComplete()都不能失败。过滤管理器保证这些例程都运行在Passive IRQL.
7.回调支持
7.1 回调数据(Callback data)
回调数据(Callback data)是过滤管理器用来描述I/O操作的新结构。类似旧过滤模型下的IRP.微过滤器通过这个结构和过滤管理器交互。不同的是,回调数据不像IRP那样管理一个栈结构。回调数据的管理都通过已经明确定义的过滤管理器接口。并且返回状态值给过滤管理器即可。
FLT_CALLBACK_DATA类型包含了微过滤器描述一个I/O操作所需要的所有的信息。下面继续详细讲解这个结构中的各个域来说明其中包含的信息:
Flags:提供这个操作的一些信息。一个或多个下面的标记可能被设置在Flags中:
FLTFL_CALLBACK_DATA_IRP_OPERATION: 这个回调数据描述一个IRP操作。
FLTFL_CALLBACK_DATA_FAST_IO_OPERATION:这个回调数据描述一个FastIO操作。
FLTFL_CALLBACK_DATA_FS_FILTER_OPERATION:这个回调描述一个文件系统过滤器操作。
FLTFL_CALLBACK_DATA_SYSTEM_BUFFER:这个操作所用的缓冲是一个系统分配的缓冲。
FLTFL_CALLBACK_DATA_GENERATED_IO:这个操作是由一个微过滤器发起的。
FLTFL_CALLBACK_DATA_REISSUED_IO:这个操作被一个当前实例之上的过滤器所重新发回给文件系统。
FLTFL_CALLBACK_DATA_DRAINING_IO:只有设置了后操作(Post-operation)回调的情况下,表明这是一个快速“排出”的I/O操作以便微过滤器的卸载。
FLTFL_CALLBACK_DATA_POST_OPERATION:只有设置了后操作(Post-operation)回调的情况下,表明着个I/O正在后操作中。
FLTFL_CALLBACK_DATA_DIRTY:当一个微过滤器已经改变了这个操作的一个或者多个可变参数的时候,设置这个参数。这个标记仅仅在Pre-operation过程中设置。微过滤器必须用FLT_SET_CALLBACK_DATA_DIRTY()和FLT_CLEAR_CALLBACK_DATA_DIRTY()来操作这个标记。
Thread: 发出这个操作的线程的地址。
Iopb: 指向这个操作的可变参数的指针。这个结构在后边详叙。
IoStatus:IO_STATUS_BLOCK结构返回操作最后的状态。如果一个微过滤器打算结束这个操作,那么必须先设置这个域,然后才能结束这个请求。对于传递给文件系统去的请求,在后操作过程(Post-operation)中有操作最终的状态。
TagData:仅仅在Create操作的后操作回调中有效。当一个操作的目标文件有一个重解析点(Reparse point)的时候设置这个位。
QueueLinks:一个链表入口结构。有时要把回调数据(Callback Data)放入工作队列中使用这个。
QueueContext[2]:一组空指针结构,用来传入附加的上下文到工作队列处理过程中。
FilterContext[4]:一组空指针结构,当回调数据进入了队列,微过滤器可以做任意使用。不依赖于过滤管理器的内部结构。
RequestorMode:这个操作的者的请求模式。
Iopb域所指的是一个FLT_IO_PARAMETER_BLOCK结构。包含了回调数据中可以修改的部分。对比IRP来说,这里相当于IRP的当前栈空间(current stack location)。微过滤器必须访问这个结构来得到每次预操作(pre-operation)和后操作(post-operation)回调的I/O参数。下面是一些更详细的细节:
IrpFlags:IRP中描述这个操作的一些标记。
MajorFunction:IRP主功能号。
MinorFunction:IRP辅功能号。
OperationFlags:即IO_STACK_LOCATION.Flags.
TargetFileObject:这个操作所影响到的目标文件。
TargetInstance:管理这个操作的实例。
Parameters:FLT_PARAMETERS是一个共用体。描述主功能号和辅功能号所指定的操作的具体参数。
除了在预操作回调中不能修改主功能号之外,微过滤器可以修改这个结构中其他的任何参数。如果参数改变,微过滤器应该调用FLT_SET_CALLBACK_DIRTY()来注明这个改变。更多详细的信息将在第8节中讲述。
微过滤器在同一个I/O操作的预回调和后回调中,总是会看到参数是一样的。即使下面的过滤器可能已经修改了这些参数。这是由过滤管理器保证的。但是虽然FLT_IO_PARAMTER_BLOCK的内容是一样的,在预操作和后操作中,这个结构的地址可能不一样。因此微过滤器不应该依赖这个地址。
回调数据结构包含IO_STATUS_BLOCK来记录这个操作的状态。过滤管理器会“尊重”这些改变而不会标记这些数据为脏(Dirty)。微过滤器如果打算在预操作回调中结束这个操作或者是后操作回调中撤消这个操作,都必须先设置这个IO_STATUS_BLOCK。
7.2 预操作回调(Pre-Operation Callbacks)
所有的预操作回调原型都是这样:
typedef FLT_PREOP_CALLBACK_STATUS
(*PFLT_PRE_OPERATION_CALLBACK) (
IN OUT PFLT_CALLBACK_DATA Data,
IN PCFLT_RELATED_OBJECTS FltObjects,
OUT PVOID *CompletionContext
);
所有的预操作回调都返回一个FLT_PRE_OPERATION_CALLBACK_STATUS.这个变量是如下定义的:
FLT_PREOP_SUCCESS_WITH_CALLBACK:这个操作成功了而且微过滤器需要后操作回调。
FLT_PREOP_SUCCESS_NO_CALLBACK:
操作成功了,但是不需要后操作回调。
FLT_PREOP_PENDING:
微过滤器将在未来某个时候结束这个操作(通过调用FltCompletePendedOperation())。微过滤器在返回这个值之前不需要做其他特殊的操作(比如IoMarkIrpPending())。如果这个状态返回了,这个I/O操作被过滤管理器挂起(栈中的下层驱动都不会收到预操作回调),直到FltCompletePendedPreOperation()被调用。
FLT_PREOP_COMPLETE:
微过滤结束了操作。这个微过滤器设置了Data->IoStatus.Status中的I/O状态。这个过滤器以下的微过滤器,旧模型过滤器和文件系统都不会看见这个I/O请求。而之上的微过滤器回看到这个请求以合适的状态完成。对于CLEANUP和CLOSE操作来说,微过滤器以一个失败状态结束这个操作是不允许的。因为这些操作不能失败。
FLT_PREOP_SYNCHRONIZE:
仅仅在非CREATE操作有效。(CREATE操作是自动同步的)。若返回此值微过滤器必须有一个后操作回调。这表明这个微过滤器希望这个操作在同一个线程里完成。也就是说后操作调出现的时候和预操作调用在同一个线程上下文里。这是由过滤管理器所保证的。不管下层的过滤器以及文件系统是挂起还是忽略这个I/O操作。这个状态必须小心使用。因为过滤管理器必须同步整个I/O,这可能影响整个系统的性能。
FLT_PREOP_DISALLOW_FAST_IO:
这个状态仅仅在旧模型下返回BOOLEAN的fastI/O的操作的情况下有效。这个状态表明不接受fastI/O请求,请发送IRP重试。
在预操作返回之前,过滤器可能修改I/O操作的参数。而且能修改的参数都集中在Data->Iopb中。当一个微过滤器修改了任何一个参数,它必须调用FLT_SET_CALLBACK_DATA_DIRTY(),否则,修改不会被承认,可能导致未知的错误。
对此还有两个例外。如果修改的是IoStatus,没有必要设置Dirty就会被过滤管理器所承认。
另一个例外是IRP_MJ_CREATE的后操作过程。如果一个碰到重解析点(reparse point),Data->TagData会指想一个重解析数据缓冲。如果微过滤器打算修改这个缓冲,它可以释放了这个缓冲然后重新分配一个(不能为空)。此时不用调用FLT_SET_CALLBACK_DATA_DIRTY().
7.3 后操作回调(Post-Operation Callbacks)
所有的后操作都有同样的原型:
typedef FLT_POSTOP_CALLBACK_STATUS
(*PFLT_POST_OPERATION_CALLBACK) (
IN OUT PFLT_CALLBACK_DATA Data,
IN PCFLT_RELATED_OBJECTS FltObjects,
IN PVOID CompletionContext,
IN FLT_POST_OPERATION_FLAGS Flags
);
Data->Flags中都设置了FLTFL_CALL_DATA_POST_OPERATION.
如果一个微过滤器在预操作回调中返回FLT_PREOP_SUCCESS_WITH_CALLBACK了,它肯定能收到这个同样操作的完成回调也就是后操作回调。后操作回调返回的类型为FLT_POSTOP_CALLBACK_STATUS.这个值是:
FLT_POSTOP_FINISHED_PROCESSING – 微过滤器已经完成了这个操作控制权应该还给请求的发起者.
FLT_POSTOP_STATUS_MORE_PROCESSING_REQUIRED – 微过滤器还没有完成这个请求,而且回在后边完成它,使用FltCompletePendedPostOperation().
与旧模型过滤驱动的完成例程不同,这个后操作回调执行在DPC中断级上。如果一个微过滤器需要完成一些在DPC上完成不了的工作,可以调用FltDoCompletionProcessingWhenSafe()。有必要的情况下(我们在DPC中断级时),会把工作插入一个工作线程。除非这个请求是不能被排队的(比如一个页面交换请求(paging I/O)).
对于打算在后操作回调中取消一个文件打开的微过滤器,FltCancelFileOpen()调用可以对指定的FileObject来一个清理和关闭的功能。微过滤器必须必须填写合适的错误代码,并且在后操作回调中返回FLT_POSTOP_FINISHED_PROCESSING.
当一个实例被卸除的时候,过滤管理器可能调用候后操作回调,但是此时操作还未真的完成。这时,标志FLTFL_POST_OPERATION_DRAINING会设置。此时提供了尽量少的信息。所以微过滤器应该清理所有的从预操作中传来的操作上下文,并返回FLT_POSTOP_FINISHED_PROCESSING.
操作的IRP_MJ(主功能码)中增加了一些附加的新数值来表示FastI/O中的一些没有IRP与之对应的操作。目的是把IRP操作和FastI/O操作可以采用同样的处理方式。这样通过一些标记就可以区分IRP操作,微过滤相关操作和FastI/O操作,而不用注册一些类似的回调函数了(比如读,写,锁定等操作的回调函数)。
有以下这些操作主功能码:
所有原有的IRP_MJ。
IRP_MJ_FAST_IO_CHECK_IF_POSSIBLE
IRP_MJ_NETWORK_QUERY_OPEN
IRP_MJ_MDL_READ
IRP_MJ_MDL_READ_COMPLETE
IRP_MJ_PREPARE_MDL_WRITE
IRP_MJ_MDL_WRITE_COMPLETE
IRP_MJ_VOLUME_MOUNT
IRP_MJ_VOLUME_DISMOUNT
IRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION
IRP_MJ_RELEASE_FOR_SECTION_SYNCHRONIZATION
IRP_MJ_ACQUIRE_FOR_MOD_WRITE
IRP_MJ_RELEASE_FOR_MOD_WRITE
IRP_MJ_ACQUIRE_FOR_CC_FLUSH
IRP_MJ_RELEASE_FOR_CC_FLUSH
关于主功能号的一些要注意的地方:
(1)IRP_MJ_CREATE的预操作回调中不能获得或者设置文件,流,或者流句柄这样的上下文。因为在预操作中,文件或者流还没有决定是否生成。
(2)IRP_MJ_CLOSE 的后操作中不能同样不能设置或者或者文件,流或者流句柄这样的上下问。因为在这个操作中,一些与之相关的系统内部结构可能已经释放了。
(3)IRP_MJ_CLEANUP和IRP_MJ_CLOSE 是永远不能失败的。他们可以挂起,传递或者成功结束。但是不能返回FLT_PREOP_COMPLETE并填一个错误码在IoStatus块中。
后操作回调不能失败,因为这个操作已经发生了。如果希望在后操作回调中让一个操作失败,那么你必须撤消已经成功的操作。比如IRP_MG_CREATE操作,过滤管理器提供了FltCancelFileOpen()来销毁已经打开的文件对象。但是过滤器还是有责任来重新整理由于Create操作而被覆盖掉的原来的文件的内容。
在后操作中对操作参数做的任何改变都不会被过滤管理器所承认。
8.操作回调数据参数
8.1 I/O参数块(I/O Parameter Block)
就像前面所提及的,回调数据(Callback Data)包含了两个关于I/O的重要的数据结构:
1)I/O状态块(I/O Status Block): Data->IoStatus用来返回它自己所结束的操作的状态(或者在后操作回调中可以读取下层驱动所完成的操作的状态)。
2)I/O参数块(I/O Parameter Block): Data->Iopb指向这个I/O操作中专为本微过滤器使用的数据。
本节深入讨论读取和改变这些参数的问题。
Iopb中的主功能号和辅功能号表示IRP/FastIo/FsFilter-主/辅功能,主功能号不能由过滤器修改。过滤管理器不支持。
TargetFileObject表示I/O操作的目标,流的文件对象。微过滤器可以修改它(必须调用FLT_SET_CALLBACK_DATA_DIRTY()),而且被过滤管理器承认。
TargetInstance参数与I/O从一个实例向另一个实例传递的意义不同,是表示当前的实例。过滤器可以改变这个指针。但是仅仅能是同一个层级,但绑定在其他的卷上的实例。同时,也必须调用FLT_SET_CALLBACK_DATA_DIRTY()。
微过滤器是不能修改TargetInstance指向同一个卷上的另一个实例的。举个例子来说,一个微过滤器在一个卷C:上有两个实例。一个在层级200称为实例C200,,另一个在层级100,称为实例C100,同时在D:上还有一个层级200的实例,称为实例D:200。现在假设在实例C200上IRP_MJ_READ的预操作回调被调用了,微过滤器可以把TrageInstance修改为实例D:200,但是不能修改为实例C:100。
这组织了任何微过滤器同一个卷的栈中非法的(不按固定顺序的)传递操作。
I/O参数块中含有一些与具体操作相关的参数。微过滤器必须用与操作配套的共用体结构来访问他们。对于IOCTL和FSCTL,具体的控制码也有不同的共用体结构。微过滤器必须检查具体的控制码来使用正确的共用体。
8.2 使用缓冲/MDL
在IRP的世界中,缓冲采用多种机制传递到驱动中。比如支持基本I/O的设备对象,缓冲是通过Irp->UserBuffer传入的,这对文件系统是最常见的。有些设备只支持缓冲I/O,那么I/O管理器传入的缓冲区Irp->AssociatedIrp.SystemBuffer.对于支持直接I/O的设备,缓冲是通过Irp->MdlAddress,一个被锁定的MDL。
但是可能有例外。一些IRP的栈空间的缓冲参数直接传入。可能指向核心内存或者是原始的用户空间内存。和设备对象对I/O操作的要求无关。这些缓冲不能传到硬件,因为他们无视设备对象所支持的I/O类型。
也有一些IRP总是缓冲I/O,比如IRP_MJ_QUERY/SET_INFORMATION.
对于微过滤器,缓冲总是通过适当的操作相关的共用体传入。没有一个通用的方法可以获得缓冲/MDL的地址来源。这种设计是为了减少栈空间之间缓冲导致的冲突。
在回调数据中,如果操作是缓冲型的,那么标记 FLTFL_CALLBACK_DATA_SYSTEM_BUFFER会被设置。如果是这样的,那么缓冲区在非分页的核心内存中。
如果这个标记没有设置,那么只能说明不是缓冲型。微过滤器有方法做进一步区分(见下一节)。缓冲还是可能来源于某个核心内存池。但是只要这个标记没有设置,过滤器就应该假设内存在原始用户空间中。这种情况下,如果缓冲并不是传入的用户缓冲,那么总是需要一个MDL来锁定一些页面,而且调用者必须获得一个系统空间地址来访问这些页面。这是一条规则,将在下节详叙之。
最后,对于某些微过滤器希望能够定位缓冲/长度/MDL来做一些最通用的操作的时候,FltDecodeParameters()提供用来做一个快速的查找并返回一个指向参数结构中的缓冲/长度/MDL。对于没有缓冲空间的操作,返回STATUS_INVALID_PARMETER.
NTSTATUS
FLTAPI
FltDecodeParameters(
IN PFLT_CALLBACK_DATA CallbackData,
OUT PMDL **MdlAddressPointer OPTIONAL,
OUT PVOID
**Buffer OPTIONAL,
OUT PULONG *Length OPTIONAL,
OUT LOCK_OPERATION *DesiredAcces OPTIONAL
);
DesiredAccess表示微过滤器可以使用的访问缓冲的方式。比如对于IRP_MJ_READ,可能指定IpWriteAccess,意味着这个缓冲区是可写的。对于IRP_MJ_WRITE,一般指定IoReadAccess表示微过滤器可以读取这个缓冲区但是不能修改它。这样一个应用使用一个仅仅有只读权限的页面来发起一个写请求也是合法的了。
对于希望缓冲空间被锁定的过滤器,应该了解:如果FLTFL_CALLBACK_DATA_SYSTEM_BUFFER表示设置了,那么可以假设这个缓冲区已经被锁定,可以安全的访问。
如果没有设置则可以调用FltLockUserBuffer()来锁定页面。这个调用可以保证页面被合适的方法锁定。如果成功了,它会设置与操作相关的参数部分的MdlAddress域为用来描述这些页面的MDL.
8.3 交换缓冲
一些微过滤器为了某些操作必须交换缓冲。考虑一个微过滤器实现加密算法,对一个非缓冲(non-cached)IRP_MJ_READ,它一般会希望把缓冲中的数据解密。同样的在写的时候,它希望把内容加密。考虑以下情况:内容无法在这个空间中加密。因为对于IRP_MJ_WRITE,这个微过滤器可能只有IoreadAccess权限。
因此微过滤器必须以他自己的有读写权限的缓冲区取代原来的缓冲区。加密了原缓冲区中的内容后写入新缓冲区后,再继续传递I/O请求。
为此,过滤管理器支持缓冲转换。有以下一些游戏规则必须遵守:
1. 改变了缓冲区的微过滤器必须有对应的后操作回调。这样缓冲能被过滤管理器自动的转换回来。
2. 如果改变的是一个标记有FLTFL_CALLBACK_DATA_SYSTEM_BUFFER标记的缓冲,必须保证新的缓冲是非分页内存。(也就是比如来自非分页内存池或者锁定的内存)。
3. 如果以上的标记没有设置,那么微过滤器必须按设备对象的要求来确定缓冲类型(可以在卷属性中查看DeviceObjectFlags等标记)。比如如果是支持直接I/O的,那么必须提供MDL等等。
4. 如果微过滤器使用非分页池中的缓冲来给一个没有设置FLTFL_CALLBACK_DATA_SYSTEM_BUFFER的操作,那么它也必须用MuBuildMdlForNonpagedPool()并把地址填写到MdlAddress域中。这是因为这么一来,下面的任何过滤器或者文件系统都不用再尝试去锁定非分页池(可以在构建的时候使用断言,但是对效率不利),如果提供了一个MDL,过滤器和文件系统总是可以通过MDL访问缓冲(可以获得一个系统内存地址来访问它)。
5. 替换一个缓冲的时候,微过滤器也必须换掉MDL(就是说缓冲和MDL要保持同步了)。对于通常的直接I/O异常,可以把MDL留空。
6. 微过滤器不应该释放旧的MDL和缓冲空间。
7. 不要尝试在后操作回调中替换掉旧的缓冲和MDL.过滤管理器自动执行这些操作。实际上微过滤器在后操作回调中的Iopb中见到缓冲空间和MDL是旧的(译者注:替换前的)。微过滤器必须自己在上下文中记录新的缓冲区。
8. 微过滤器应该释放自己分配的(和替换过的)缓冲。无论如何,如果有的话,过滤管理器会自动释放新缓冲的MDL。
9. 微过滤器不希望过滤管理器自动释放交换过的缓冲MDL可以调用FltRetainSwappedBufferMdl()。
10. 过滤器如果希望访问交换过的缓冲的MDL可以在后操作回调中使用FltGetSwappedBufferMdl()。既然一个更下层的过滤器或文件系统交换了新的缓冲空间进来,那么有可能生成了一个MDL。在后操作回调中微过滤器交换缓冲之前,过滤管理器保存了所有这样的的MDL。这个调用可以用来访问这些MDL.
9.上下文(Context)支持
所有的过滤器都必须在他们所操作的各种对象中记录一些他们自己的状态。让微过滤器在这些对象中拥有他们的上下文是过滤管理器的一个重要特点。
一个上下文是使用FltAllocateContext()分配一的一片动态内存区。这个调用传入需要的内存空间的大小并返回一个内存空间指针。系统会绑定一个内部的数据头用来跟踪这个指针所对应的上下文。
以下种类的对象支持上下文:
卷 - 一个已经挂载的设备。
实例 - 一个微过滤器对对一个卷的一次绑定。一个微过滤器可能对一个卷绑定多次。如果一个微过滤器对一个卷只能绑定一次,那么推荐用卷上下文来代替实例上下文,这样效率高多了。
文件 - 指关于一个文件的所有打开的流。一般这些上下文是不支持的。
流 - 文件上的一个单独的数据流。
流句柄 - 一个文件的一次打开,比如一个文件对象。
9.1 上下文注册
注册的时候,微过滤器定义它所想使用的上下文的类型,大小以及一个用来清理上下文的例程。微过滤器用一组FLT_CONTEXT_REGISTRATION结构来确定这些参数.
下面解释FLT_CONTEXT_REGISTRATION的细节:
ContextType:
注册的上下问的类型。
Flag:
表示这个上下文的一些特殊处理信息。当前的定义有:FLT_CONTEXT_REGISTRATION_NO_EXACT_SIZE_MATCH:默认的情况下,过滤管理器会比较一个给定的上下文的请求的长度和分配上下文的时候指定的数据长度。如果指定了这个标记,如果请求分配的内存大小少于等于注册的时候所指定的长度,过滤权利气回使用特殊指定的分配例程。当注册时候指定的大小为FLT_VARIABLE_SIZED_CONTEXT或者分配和释放例程都已经指定了的时候,这个标记被忽略。
CleanupContext:
当过滤管理器决定应该清理这个上下问的时候这个例程被调用。如果在上下文被释放之前没有什么需要清理的,这个可以设置为NULL.
Size:
这个上下文的字节大小。这用来允许过滤管理器使用内存池技术(如旁视列表)来让分配和释放更加有效率。如果使用了自己的分配和释放例程,这个域被忽略。
PoolTag:
分配内存的“池”上下文。这是使用ExAllocatePoolWithTag的时候用的。如果使用自己的分配和释放例程,这个域被忽略。
Allocate:
如果打算使用自己的分配例程,设置这个指针。如果打算依赖过滤管理器的内存池技术,那么这个应该设置为NULL.
Free:
如果想使用自己的释放例程,那么这个必须设置为非空。
如果一个微过滤器有相同类型的上下文但是长度不一,它可以为同类型的上下文注册不同的FLT_CONTEXT_REGISTRATION结构来利用过滤管理器的内存池技术。
9.2 上下文生成接口
下面是FltAllocateContext()的函数原型:
NTSTATUS
FLTAPI
FltAllocateContext (
IN PFLT_FILTER Filter,
IN FLT_CONTEXT_TYPE ContextType,
IN SIZE_T ContextSize,
IN POOL_TYPE PoolType,
OUT PFLT_CONTEXT *ReturnedContext
);
ContextType可以是以下的情况:FLT_VOLUME_CONTEXT , FLT_INSTANCE_CONTEXT, FLT_FILE_CONTEXT, FLT_STREAM_CONTEXT, 或者是FLT_STREAMHANDLE_CONTEXT.
下面是一组用来把一个上下文绑定到一个对象上的例程。请注意上下文的类型和对象类型必须是配套的。
NTSTATUS
FLTAPI
FltSetVolumeContext (
IN PFLT_VOLUME Volume,
IN FLT_SET_CONTEXT_OPERATION Operation,
IN PFLT_CONTEXT NewContext,
OUT PFLT_CONTEXT *OldContext OPTIONAL
);
NTSTATUS
FLTAPI
FltSetInstanceContext (
IN PFLT_INSTANCE Instance,
IN FLT_SET_CONTEXT_OPERATION Operation,
IN PFLT_CONTEXT NewContext,
OUT PFLT_CONTEXT *OldContext OPTIONAL
);
NTSTATUS
FLTAPI
FltSetFileContext (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
IN FLT_SET_CONTEXT_OPERATION Operation,
IN PFLT_CONTEXT NewContext,
OUT PFLT_CONTEXT *OldContext OPTIONAL
);
NTSTATUS
FLTAPI
FltSetStreamContext (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
IN FLT_SET_CONTEXT_OPERATION Operation,
IN PFLT_CONTEXT NewContext,
OUT PFLT_CONTEXT *OldContext OPTIONAL
);
NTSTATUS
FLTAPI
FltSetStreamHandleContext (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
IN FLT_SET_CONTEXT_OPERATION Operation,
IN PFLT_CONTEXT NewContext,
OUT PFLT_CONTEXT *OldContext OPTIONAL
);
设置一个上下文的时候有两种类型的操作可能发生。 它们是:
FLT_SET_CONTEXT_KEEP_IF_EXISTS:如果没有存在的上下文,那么将设置新的上下问。如果有存在的,那么新的上下文将不会设置上去,而且会返回一个错误代码。如果OldContext参数定义了,那么已经存在的上下文会返回。如果有,调用者必须释放返回的上下文。如果新的上下文没设置上去,那么调用者必须负责释放它。
FLT_SET_CONTEXT_REPLACE_IF_EXISTS: 即使旧的上下文存在,新的也回释放上去。如果OldContext定义了,那么被取代的上下文会返回。调用者必须自己释放它。如果没有定义,则旧的上下文会被自动释放。
当相关设备对象被系统释放,过滤管理器会在合适的时机调用微过滤器来清理上下文。微过滤器可能希望某个时候自己删除一个对象上的上下文。为此,微过滤器可以调用以下的一个例程来删除上下文。当然前提是它拥有这个上下文的指针。
VOID
FLTAPI
FltDeleteContext (
IN PFLT_CONTEXT Context
);
如果没有指针呢?它必须通过指定对象来删除上下文。可以使用以下这些例程中的某个:
NTSTATUS
FLTAPI
FltDeleteVolumeContext (
IN PFLT_FILTER Filter,
IN PFLT_VOLUME Volume,
OUT PFLT_CONTEXT *OldContext OPTIONAL
);
NTSTATUS
FLTAPI
FltDeleteInstanceContext (
IN PFLT_INSTANCE Instance,
OUT PFLT_CONTEXT *OldContext OPTIONAL
);
NTSTATUS
FLTAPI
FltDeleteFileContext (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
OUT PFLT_CONTEXT *OldContext OPTIONAL
);
NTSTATUS
FLTAPI
FltDeleteStreamContext (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
OUT PFLT_CONTEXT *OldContext OPTIONAL
);
NTSTATUS
FLTAPI
FltDeleteStreamHandleContext (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
OUT PFLT_CONTEXT *OldContext OPTIONAL
);
如果OldContext参数为空,那么过滤管理器会释放对这个上下文的所有的引用。否则,上下文会通过OldContext返回。微过滤器必须自己调用FltReleaseContext来释放它。
9.3 上下文获取(Retrieval)接口
下面的例程用来获取某个设备的相关上下文。使用完毕,调用者必须释放返回的上下文,释放使用FltReleaseContext().上下文不能在DPC中断级获取。所以如果一个后操作回调中希望得到一个上下文,那么必须从预操作中获得并传入。
NTSTATUS
FLTAPI
FltGetVolumeContext (
IN PFLT_FILTER Filter,
IN PFLT_VOLUME Volume,
OUT PFLT_CONTEXT *Context
);
NTSTATUS
FLTAPI
FltGetInstanceContext (
IN PFLT_INSTANCE Instance,
OUT PFLT_CONTEXT *Context
);
NTSTATUS
FLTAPI
FltGetFileContext (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
OUT PFLT_CONTEXT *Context
);
NTSTATUS
FLTAPI
FltGetStreamContext (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
OUT PFLT_CONTEXT *Context
);
NTSTATUS
FLTAPI
FltGetStreamHandleContext (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
OUT PFLT_CONTEXT *Context
);
当使用完毕,下面的一些例程用来释放获得的上下文。一般推荐不要在操作之间传递这些上下文指针。上下文的获取非常有效率,是专门设计用来在每个操作要使用的时候专门来获取上下文的。
VOID
FLTAPI
FltReleaseContext (
IN PFLT_CONTEXT Context
);
类似实例通知例程,每个操作的回调例程都收到一个FLT_RELATED_OBJECTS结构。这个结构包含所有这个操作相关的所有已知的的对象。为了简化上下文的获取,有一个类似的FLT_RELATED_CONTEXT可以一次获取。这个结构如下:
typedef struct _FLT_RELATED_CONTEXTS {
PFLT_CONTEXT VolumeContext;
PFLT_CONTEXT InstanceContext;
PFLT_CONTEXT FileContext;
PFLT_CONTEXT StreamContext;
PFLT_CONTEXT StreamHandleContext;
} FLT_RELATED_CONTEXTS, *PFLT_RELATED_CONTEXTS;
接下来两个例程用来依次获得多个例程,此外也有一次性释放。对于FltGetContexts()调用者指定(在DesiredContext参数中)需要的上下文。在内部,一次获得多个上下文比一个一个的获得它们效率高。当然,对于不需要的上下文最好是不要去获取它。FLT_ALL_CONTEXTS可以用来得到所有可用的上下文。
VOID
FltGetContexts (
IN PFLT_RELATED_OBJECTS FltObjects,
IN FLT_CONTEXT_TYPE DesiredContexts,
OUT PFLT_RELATED_CONTEXTS Contexts
);
VOID
FltReleaseContexts (
IN OUT PFLT_RELATED_CONTEXTS Contexts
);
9.4 上下文释放接口
当过滤管理器决定了一个上下文要被释放的时候,微过滤器的相关回调会被调用。这个回调例程应该清理任何上下文(包括清理分配的内存,释放资源等等)。通过返回,过滤管理器会释放传入的上下文结构。
每个类型的上下文都要有一个对应的清理例程。这些例程定义如下:
typedef VOID
(*PFLT_CONTEXT_CLEANUP_CALLBACK) (
IN PFLT_CONTEXT Context,
IN FLT_CONTEXT_TYPE ContextType
);
同一个清理例程可以注册给多个不同类型的上下文。
10.与用户态的通信
10.1 过滤器通信端口对象
为了实现安全的和支持多种不同类型通信方法,一个新的对象被引入:微过滤器通信端口(以下简称通信端口或者端口)。专门设计用来给核心态-用户态通信或者反过来。核心-核心通信现在不在支持。一个端口是一个有名字的NT对象。而且有一个安全的描述符号。
过滤管理器生成了一个新的对象类型,FilterConnectionPort来实现这个。过滤管理器在它的DriverEntry中生成这个新的对象类型,赶在了任何微过滤器加载之前。
只有核心模式的驱动才能生成一个通信端口,使用以下的调用:
NTSTATUS
FltCreateCommunicationPort(
IN PFLT_FILTER Filter,
OUT PHANDLE PortHandle,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PVOID ServerPortCookie OPTIONAL,
IN PFLT_CONNECT_NOTIFY ConnectNotifyCallback,
IN PFLT_DISCONNECT_NOTIFY DisconnectNotifyCallback,
IN PFLT_MESSAGE_NOTIFY MessageNotifyCallback,
IN ULONG MaxConnections
);
Filter是微过滤器的过滤器句柄。成功生成之后,端口的句柄在PortHandle参数中返回。
和其他的NT对象一样,ObjectAttributes参数定义了OBJECT_ATTRIBUTES结构来初始化要生成的端口对象的名字,对象属性和安全描述符等。
请注意属性中OBJ_KERNEL_HANDLE标记必须设置。因为通信端口只能是核心对象。
ServerPortCookie是一个上下文。微过滤器可以通过这个和端口联系在一起。这个上下文对过滤管理器是不透明的。所有的连接,中断通知,都会同过这个上下文才能传递给微过滤器。有些过滤器可能要生成一组通信端口,又想功用一个同志例程。那么可以通过这个上下文中保存的数据进行区分。
调用者还可以注册一些回调函数:
ConnectNotifyCallback():
当一个用户态进程尝试打开一个端口的时候,这个例程被调用。过滤器可以选择把这个请求失败掉。通知例程回手到一个关于此连接的句柄。每一个连接有唯一的一个句柄。ServerPortCookie 也会传入。微过可以填写ConnectionCookie为一个上下文。这个上下文会传到所有的用户态传来的消息以及连接中断例程中。
DisconnectNotifyCallback():
当一个端口被用户态关闭的时候会调用这个回调。(也就是打开计数到0的时候)。
MessageNotifyCallback():
任何时候手到一个消息都会调用这个。
MaxConnections指出了这个通信端口上允许的最大向外连接数。这没有默认值,必须设置得大于0。
并不能保证所有的对象名会生成在根名字空间。有可能过滤管理器把它们映射在\FileSystem\Filters目录下。不过即使如此,对微过滤器和用户态应用程序来说,这是透明的。当引用了一个通信断口的名字,那么所有的足见都应该使用同样的名字。例子Sacnner Minifiter中展示了这是怎么做的。
对应于新的对象类型,新的访问方式也被引入了。有新的访问方式如下:
FLT_PORT_CONNECT
FLT_PORT_ALL_ACCESS
这是一些访问类型。调用者可以设置这些来给使用者权限。用于构造安全描述符的时候,使用InitializeObjectAttributes()。
设置了FLT_PORT_CONNECT,那么我们的应用程序足可以连接这个端口并发送和接受消息。
微过滤器生成一个端口之后,端口就会开始侦听可能的连接。直到你使用ZwClose()将它关闭为止。
10.2 从用户态连接到通信端口
微过滤器的通信端口模型和旧模型的过滤器一样,是不对称的。核心态端生成,用户态端连接。有一个接口用来给用户态应用打开一个端口。当端口建立,ConnectNotify()例程被调用来通知微过滤器。
用户态下连接一个端口的编程接口原型如下:
HRESULT
FilterConnectCommunicationPort(
IN LPWSTR lpPortName,
IN DWORD dwOptions,
IN LPVOID lpContext,
IN WORD wSizeOfContext,
IN LPSECURITY_ATTRIBUTES lpSecurityAttributes,
OUT HANDLE *hPort
);
lpPortName是一个宽字符格式的字符串,指出要连接的端口的名字。这个名字应该和微过滤器生成这个端口的时候一样。名字可以以”\”开头表示在根路径中。过滤管理器会在合适的路径下打开他们并管理很多微过滤器通信端口。
dwOptions现在没有使用。
LpContext是一个指针。指向一个不透明的参数块。这个参数会被传入到ConnectNotify()中。比如可以用来鉴别请求生成端口的应用程序的版本。wSizeOfContext指出这个上下文的字节数。
LpSecurtityAttributes指出给用户端的安全权限。如果这个句柄是继承得到的。
如果这个调用不成功,合适的HRESULT会返回。
返回得到的句柄可以被管理或者被复制。这要通过一些编程接口。它也可以和一个I/O完成端口绑定。
当这个接口被调用,一个新的核心态的无名端口对象就生成了(类型是FilterCommunicationPort).这个对象用来表示这个连接。微过滤器的ConnectNotify()例程被调用,从而得到通知。而且也得到连接端口的句柄,用来从核心态发送消息。
如果调用者没有访问这个服务端口的权限,或者已经达到了最大连接数,那么调用会失败。
10.3 中断与通信端口的连接
当用户态程序调用CloseHandle()或者核心态调用ZwClose()来关闭连接句柄的时候,连接会中断。
只有用户态调用CloseHandle()的时候,微端口的DisconnectNotify()例程才会调用。
理想情况下,微过滤器应该总是在连接结束的时候,在DisconnectNotify()中关闭连接。如果一个微过滤器在其他地方关闭句柄,那么它必须用一些同步方法确保不会在DisconnectNotify()中关闭再次关闭它。
当一个连接在核心态或者用户态被中断了,以下情况发生:
1.
所有用户态的等待(通过FilterGetMessage)被清理掉,而且以STATUS_FLT_PORT_DISCONNECTED结束掉(被解释为win32错误码ERROR_DISCONNECTED)。
2. 所有核心的被阻塞的发送例程会以STATUS_FLT_PORT_DISCONNECTED结束阻塞。
3. 因为端口变无效了,所以不可能有其他的等待或者阻塞的情况出现。
微过滤器总是可以对服务端口的句柄调用ZwClose()来关闭服务端口。这并不会使已经建立的连接中断,但是会阻止新连接的建立。
10.4 卸载
微过滤器总是必须在FltUnregisterFilter()调用之前,在FilterUnload例程或者更早关闭服务端口。否则系统可能在卸载例程中被挂起。
即使在有一些连接打开的情况下(比如用户态一放已经打开了一些连接句柄),微过滤器也应该允许被卸载。这种情况下,过滤管理器会尝试强行终止这些连接。过滤管理器会调用DisconnectNotify()例程。微过滤器应该在这里关闭这些连接句柄以避免句柄的泄漏。
11.文件名处理
通过查找操作的参数或者询问文件系统,过滤管理器能得到对象的名字。因而过滤管理器提供一组调用来方便获取对象名。为了更高的效率,过滤管理器也暂存一些对象名。当很多过滤器经常查一个名字的时候,暂存这个名字所付出的代价是很值得的。
查询对象名的时候,过滤管理器返回一个FLT_FILE_NAME_INFORAMTION结构来避免数据拷贝。这些结构被根据过滤器的请求次数计数。只有通过过滤管理器的编程接口才能改变这些结构中的数据。下面有这些结构的详细信息。
11.1 从操作获得一个文件名
可以在当前操作的CallbackData->Iopb->TargetFileObject中得到文件名。这需要调用下面的例程:
NTSTATUS
FLTAPI
FltGetFileNameInformation (
IN PFLT_CALLBACK_DATA CallbackData,
IN FLT_FILE_NAME_FORMAT NameFormat,
IN FLT_FILE_NAME_QUERY_METHOD QueryMethod,
OUT PFLT_FILE_NAME_INFORMATION *FileNameInformation
);
CallbackData是这个操作的FLT_CALLBACK_DATA结构。现在假设过滤器想从这个操作中得到文件名。
名字的格式是以下三种:
FLT_FILE_NAME_NORMALIZED_FORMAT:请求全路径名字,包括卷名。所有的短名被扩展成长名。任何流名足见回去掉后边的“:$DATA”.如果这是一个目录名,且不是根目录,最后的“\”会被去掉。
FLT_FILE_NAME_OPENED_FORMAT:
包含全路径,包括卷名。但是这个名字和打开这个对象所用的名字相同。因此可能在路径中包含一些短名。
FLT_FILE_NAME_SHORT_FORMAT:
仅仅含有路径中最后一个元素的短名(Dos名),不会返回全路径。
QueryMethod应该是以下之一:
FLT_FILE_NAME_QUERY_DEFAULT:
搜索一个名字的时候,管理器会首先找暂存的名字。然后再询问文件系统。
FLT_FILE_NAME_QUERY_CACHE_ONLY:
仅仅在暂存中找。如果失败,返回STATUS_FLT_NAME_CACHE_MISS.
FLT_FILE_NAME_QUERY_FILE_SYSTEM_ONLY:仅仅询问文件系统,不会从暂存中去寻找这个名字。
名字在最后一个参数中返回,FileNameInformation.这个结构是一组共享缓冲的Unicode字符串。不同的字符串表明不名字中不同的部分.
typedef struct _FLT_FILE_NAME_INFORMATION {
USHORT Size;
FLT_FILE_NAME_FORMAT Format;
FLT_FILE_NAME_PARSED_FLAGS NamesParsed;
UNICODE_STRING Name;
UNICODE_STRING Volume;
UNICODE_STRING Share;
UNICODE_STRING Extension;
UNICODE_STRING Stream;
UNICODE_STRING FinalComponent;
UNICODE_STRING ParentDir;
} FLT_FILE_NAME_INFORMATION, *PFLT_FILE_NAME_INFORMATION;
当一个文件名信息结构从FltGetFileNameInforamtion()返回,name,Volume,Share(用于远程文件)会被解析出来。如果一个微过滤器需要其他的名字信息,它应该调用FltParseFileNameInformation().
运行在DPC或者更低级的中断级别的时候,微过滤器可以在IO过程中任何地方调用FltGetFileNameInformation().当它在一个可能导致死锁的情况下请求(比如处理分页交换的时候),如果在暂存中没有找到名字或者指定了必须从文件系统读取那么调用会失败。
即使没有回调数据(Callback Data)存在,仅仅知道文件对象(FileObject)的时候,微过滤器也可以调用FltGetFileNameInformationUnsafe()来获得一个文件名。不过过滤器必须知道当前向文件系统询问这个名字是安全的。这个调用不能像FltGetFileNameInformation()那样检查是否导致死活。微过滤器必须自己保证它。
当一个名字使用完毕,应该调用以下的例程来释放:
FLTAPI
FltReleaseFileNameInformation (
IN PFLT_FILE_NAME_INFORMATION FileNameInformation
);
过滤管理器的名字暂存对微过滤查找对象的名字来说效率足够了。名字暂存机制也管理了由于重新命名而无效的名字。维护一个名字暂存空间的复杂逻辑对微过滤器们而言是透明的。但是微过滤器对于无效的名字并不是完全不需要关系。当一个重命名发生了,过滤管理器清理所有的受影响的暂存文件名。但是微过滤器可能引用过一个过时的文件名。当这些引用全部被释放的时候,过时的文件名信息结构才会被真的释放掉。如果一个微过滤器请求询问一个已经被重新命名的对象,将尽可能的返回新的名字。
11.2 文件名的附加支持
过滤管理器也提供了一个编程接口来帮助微过滤器获得一个改名或生成硬连接的操作的目的名:
NTSTATUS
FltGetDestinationFileNameInformation (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
IN HANDLE RootDirectory OPTIONAL,
IN PWSTR FileName,
IN ULONG FileNameLength,
IN FLT_FILE_NAME_FORMAT NameFormat,
IN FLT_FILE_NAME_QUERY_METHOD QueryMethod,
OUT PFLT_FILE_NAME_INFORMATION *RetFileNameInformation
);
这个接口只能在IRP_MJ_SET_INFORMATION的预操作回调中调用,包括FileSetNameInformation和FileSetLinkInformation。调用者从这个操作中获得参数,这个调用会返回一个FLT_FILE_NAME_INFORMTION结构,包含了目标文件名在其中。与使用FltGetFileNameInformation()相同,这个调用返回的名字使用完毕之后,应该调用FltReleaseFileNameInformation()来释放。
命名隧道(Name Tunneling)是过滤器的另一个困绕之处。过滤管理器提供了一个编程接口来获得一个命名管道需要的新名字:
NTSTATUS
FltGetTunneledName (
IN PFLT_CALLBACK_DATA CallbackData,
IN PFLT_FILE_NAME_INFORMATION FileNameInformation,
OUT PFLT_FILE_NAME_INFORMATION *RetTunneledFileNameInformation
);
命名隧道仅仅影响获取文件名采用通常格式的微过滤器。如果一个微过滤器在它的一个预操作需要一个通常格式的文件名,这个预操作回调的来源是CREATE,改名,或生成硬连接的话,它应该在后操作回调中调用FltGetTunneledName()来确认这个名字是否被操作系统命名隧道所改过。如果命名隧道确实出现了,那么RetTunneledFileNameInformation中回返回一个新的文件名字信息结构。微过滤器必须用这个新的名字,并且处理完毕后必须用FltReleaseFileName()来释放它。
(译者注:什么是命名隧道(Name Tunneling)
长文件名出现之后,旧的16位应用程序随时可能破坏掉长文件名。为此出现了所谓的命名隧道概念。现在假设一个16位的程序比如文字处理程序把当前版本的文挡维护在一个临时文件中。当用户修改这个文件,原始的文件就被删除了,临时文件被改为原来的文件名。
如果原始文件有一个长文件名,但是临时文件仅仅有短文件名,那么当旧的文件被删除,名字也跟着丢失了。因此当命名隧道起作用的时候,文件系统记住了每个被删除的文件名一段时间(比如15秒),如果一个短文件名的文件生成了,刚好和被记忆的长文件名配套,那么短文件名自动改名为长文件名。这就是命名隧道概念。
想自己尝试一下:首先,在一个空文件夹中生成一个文件 longfilename.然后删除它。在生成一个文件longfi~1,然后输入dir /x,这时你发现,longfilename又出现了!)
11.3 名字提供接口
如果一个过滤器打算提供一个途径来改变名字空间,他必须注册三个附加的回调给过滤管理器。这些回调允许过滤器作为名字的提供者。过滤器将有责任对上层发来的FltGetFilenameInformation或者FltGetDestinationFileName返回FLT_FILE_NAME_INFORMATION结构,并填写其中名字的内容。而且这样的微过滤器还可以告知过滤管理器,他们所返回的名字要不要被暂存。
作为一个名字提供者,过滤器必须可以返回一个指定的文件对象(file object)的开放格式的名字。如果确定了通常名字格式,过滤管理器将重新遍历这个名字的所有部分并调用名字提供者的徽调来展开这些部分为一个通常格式的文件名。在展开一个路径的所有部分的过程中,过滤器的名字通常化例程可能被调用不止一次。因此这个过程中允许过滤器传入一个上下文。所有的过程结束后,如果调用返回了,过滤器会被要求清理这个上下文。
当一个过滤器作为名字提供者必须提供一个名字的时候,它的PFLT_GENERATE_FILE_NAME例程会被调用。你能在参数中得到文件对象,过滤器的实例,同时回调数据描述发生的操作。此外还有名字请求选项。那么过滤器必须在这中间生成名字:
typedef NTSTATUS
(*PFLT_GENERATE_FILE_NAME) (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
IN PFLT_CALLBACK_DATA CallbackData OPTIONAL,
IN ULONG NameOptions,
OUT PBOOLEAN CacheFileNameInformation,
OUT PFLT_NAME_CONTROL FileName
);
typedef NTSTATUS
(*PFLT_NORMALIZE_NAME_COMPONENT) (
IN PFLT_INSTANCE Instance,
IN CONST PUNICODE_STRING ParentDirectory,
IN USHORT VolumeNameLength,
IN CONST PUNICODE_STRING Component,
IN OUT PFILE_NAMES_INFORMATION ExpandComponentName,
IN ULONG ExpandComponentNameLength,
IN OUT PVOID *NormalizationContext
);
typedef VOID
(*PFLT_NORMALIZE_CONTEXT_CLEANUP) (
IN PVOID *NormalizationContext
);
如果一个过滤器希望让所有自己提供的名字的暂存失效,他可以调用以下的接口。在此之前它可能已经提供了一些FLT_FILE_NAME_INFORMATION结构,而且其他过滤器可能刚好还在使用。那么这些FLT_FILE_NAME_INFORMATION结构只有当引用数降到0(已经没有人使用了),才会自己释放掉。
NTSTATUS
FltPurgeFileNameInformationCache (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject OPTIONAL
);
为了确保这样一个过滤可以卸载,过滤管理器有责任管理这些过滤器提供的文件名的暂存和释放。
一些FltGetFileNameInformation()调用FltGetDestinationFileName()调用基于一些过滤器提供的名字。过滤管理器将负责初始化这些FLT_FILE_NAME_INFORMATION结构。
为了提高效率,过滤管理器往名字提供者的PFLT_GENERATE_FILE_NAME回调传入一个缓冲区。这个缓冲是一个FLT_NAME_CONTROL所包装的UNICODE_STRING.这个结构中包含一些公有的或者私有的信息。在一个过滤器试图填充这个缓冲之前,应该先检查缓冲是否足够的大:
NTSTATUS
FltCheckAndGrowNameControl (
IN OUT PFLT_NAME_CONTROL NameCtrl,
IN USHORT NewSize
);
如果这个调用返回STATUS_SUCCESS,说明FLT_NAME_CONTROL结构足够容纳从名字提供者返回的名字了。
12.过滤器自产生I/O
某些微过滤器需要执行他们自己的I/O请求。在卷的微过滤器栈中,只有此过滤器以下的过滤器才能收到这些I/O请求。比如,一个防毒软件,可能希望在打开一个文件之前先读一下这个文件。在新的微过滤模式下,一个微过滤器有两种方法生成自己的请求:使用生成例程,类似现在的其他例程。此外就是使用旧的ZwXxx例程。
主要的I/O生成例程如下:
NTSTATUS
FLTAPI
FltAllocateCallbackData (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject OPTIONAL,
OUT PFLT_CALLBACK_DATA *RetNewCallbackData
);
VOID
FLTAPI
FltPerformSynchronousIo (
IN PFLT_CALLBACK_DATA CallbackData
);
NTSTATUS
FLTAPI
FltPerformAsynchronousIo (
IN PFLT_CALLBACK_DATA CallbackData,
IN PFLT_COMPLETED_ASYNC_IO_CALLBACK CallbackRoutine,
IN PVOID CallbackContext
);
要使用这些例程,一个微过滤器可以首先调用FltAllocateCallbackData来分配一个 CallbackData.然后对应于不同的操作填写合适的参数。之后即可调用FltPerformSynchronousIo()或者是FltPerformAsynchronousIo()来实际发起I/O请求。参数Instance必须总是微过滤器发起此请求的实例。(译者注:不能由A实例分配而B实例来发起请求)。
此外过滤管理器导出了一些常用的实用例程,比如:
NTSTATUS
FLTAPI
FltCreateFile (
IN PFLT_FILTER Filter,
IN PFLT_INSTANCE Instance OPTIONAL,
OUT PHANDLE
FileHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN PLARGE_INTEGER AllocationSize OPTIONAL,
IN ULONG FileAttributes,
IN ULONG ShareAccess,
IN ULONG CreateDisposition,
IN ULONG CreateOptions,
IN PVOID EaBuffer OPTIONAL,
IN ULONG EaLength,
IN ULONG Flags
);
如果Instance参数被忽略(译者注:指定为空.),那么CREATE请求会被发送到微过滤器栈的顶端(这导致本微过滤器会回环的看到自己发送的请求。)除非绝对必要,这是不明智的。如果被滥用,很容易导致死锁和栈溢出.
如果指定了这个参数(应该总是你自己当前的instance),那么仅仅向下发起此请求。所有的前面的调用此api的微过滤器之上的微过滤器(包括自己)都不会收到此请求.
FileHandle参数返回一个供Zw*系列函数调用的文件句柄.如果先前指定的Instance不为空,那么保证关于这个文件句柄的其他未来的I/O操作(通过Zw接口,FltClose(),等),都只能被这个发起Instance下面的Instance看到。
FltReadFile()和FltWriteFile()发起的I/O读写请求也只有下层实例才能看见。用于我们只有FileObject而没有文件句柄的时候。这些例程类似以前旧的过滤模型中自己发送IRP的读写方式.
重要提示:过滤器不需要使用FltReadFile()/FltwriteFile()来对一个FltCreateFile返回的文件句柄发起一个I/O请求.对于这种情况,使用Zw*()接口就可以发送到合适的实例上去。
NTSTATUS
FLTAPI
FltReadFile (
IN PFLT_INSTANCE InitiatingInstance,
IN PFILE_OBJECT FileObject,
IN PLARGE_INTEGER ByteOffset OPTIONAL,
IN ULONG Length,
OUT PVOID Buffer,
IN FLT_IO_OPERATION_FLAGS Flags,
OUT PULONG BytesRead OPTIONAL,
IN PFLT_COMPLETED_ASYNC_IO_CALLBACK CallbackRoutine OPTIONAL,
IN PVOID CallbackContext OPTIONAL
);
NTSTATUS
FLTAPI
FltWriteFile (
IN PFLT_INSTANCE InitiatingInstance,
IN PFILE_OBJECT FileObject,
IN PLARGE_INTEGER ByteOffset OPTIONAL,
IN ULONG Length,
IN PVOID Buffer,
IN FLT_IO_OPERATION_FLAGS Flags,
OUT PULONG BytesWritten OPTIONAL,
IN PFLT_COMPLETED_ASYNC_IO_CALLBACK CallbackRoutine OPTIONAL,
IN PVOID CallbackContext OPTIONAL
);
微过滤器发出I/O可能为同步也可能为异步.当I/O被指定为异步,微过滤器提供一个回调例程.当系统完成了I/O请求回调例程会被调用.
13 卸载/注销/解除绑定的规则
解除绑定意味着一个微过滤实例将被销毁.这个微过滤器不在会因为任何对卷的操作而被调用(当然除非还有别的实例绑定在这个卷上).
卸载一个微过滤器意味着它的代码将不在存在于内存中。这往往在系统关闭的时候发生。或者是一个新版本的微过滤器在不关闭系统的情况下安装了(译者注:那么这时旧版本的微过滤器被卸载了。)
一个微过滤器可以自己把自己从一个卷解除绑定(调用FltDetachVolume),不过更通常的情况是在用户界面上解除的。一个微过滤器即使是有未完成的I/O请求的时候,依然可以解除绑定.此时,微过滤器的完成例程会被调用,而且所有的未完成的I/O操作都带有标记FLT_COMPLETION_DETACHED.当这些操作以后实际完成时,微过滤器就不会再收到这些完成回调了。
当一个微过滤器实例被解除绑定的时候,系统回调用这个微过滤器的所有的没有清除的上下文的释放例程,包括文件,流,流文件对象等和这个实例相关的对象。
14. 支持例程
除了已经讨论过的接口之外,过滤管理器提供了一组支持例程来帮助微过滤器完成他需要的工作。这里列出一部分附加的例程。请查阅过滤管理器IFS文档来获得这些例程的更多信息:
对象,名字转换:
FltGetFilterFromName()
FltGetVolumeFromName()
FltGetVolumeInstanceFromName()
卷,实例,设备对象转换历程:
FltGetVolumeFromInstance(), FltGetFilterFromInstance()
FltGetVolumeFromDeviceObject()
FltGetDeviceObject()
FltGetDiskDeviceObject()
获取对象信息例程:
FltGetVolumeProperties()
FltIsVolumeWriteable
FltQueryVolumeInformation(), FltSetVolumeInformation()
FltGetInstanceInformation()
FltGetFilterInformation()
枚举例程:
FltEnumerateFilters()
FltEnumerateVolumes()
FltEnumerateInstances()
FltEnumerateFilterInformation()
FltEnumerateInstanceInformationByFilter()
FltEnumerateInstanceInformationByVolume()
FltEnumerateVolumeInformation()
Oplock例程:
FltInitializeOplock()
FltUninitializeOplock()
FltOplockFsctrl()
FltCheckOplock()
FltOplockIsFastIoPossible()
FltCurrentBatchOplock()
目录改变通知例程:
(译者注:目录改变通知在文件系统中的意义是当目录改变后,通知操作系统,使操作系统可以在界面上做出一些改变,比如显示的目录结构变化等等,专门有一种IRP可以干这个事情。)
FltNotifyFilterChangeDirectory()
其他: FltGetRequestorProcess(), FltGetRequestorProcessId()
15.构建一个微过滤器
构建一个微过滤器应用所需要的所有的东西都可以在过滤管理器IFS开发包中找到。这包括:
1.用来构建一个微过滤器和一个使用了过滤管理器接口的用户态应用程序的完整的构建环境。
2.所有的在一台机器上安装过滤管理器组件用于开发的安装包。当然如果以后的windows版本内部包含了过滤管理器组件,那么就没有必要了。
3.微过滤器示例的代码。
所有的微过滤器包含头文件FltKernel.h,而且连接FltMgr.lib.而应用程序应该包含头文件FltUser.h,并连接库FltLib.lib。
0.译者序
我翻译此文出于对文件系统技术的兴趣。这就是新的文件系统过滤接口。其实也不算什么新的东西,微软开发了另一个“旧模型的”过滤驱动,称之为过滤管理器(Filter Manager)。从而提供了一系列新的接口来让你开发新的过滤器。确实这套接口变简单清晰了。你至少避免了包含无数个信息的IRP,避免了请求在各个部件中循环的发来发去,一个分发例程中处理无数中情况,一不小心系统崩溃。我不知道花了多少时间才弄明白一个简单的缓冲读请求从用户到过滤到文件系统和缓冲管理器,虚拟内存管理器之间的关系!现在你也许不需要再管他们了,仅仅做好自己的过滤工作就可以。
这套接口强大吗?能实现你想要的功能吗?你很快就发现你没有研究过sfilter就看不懂Minifilter,或者是还得从sfilter开始做起更灵活一点。微软就是这样,拿僵硬而且也不简单的东西来“简化”强大灵活但是设计上一团糟的东西,对于你来说是两者都必须学习,最后你的脑子被微软塞得满满的,不过没关系,我们已经习惯了.
此文的原文是《Filter Driver Development Guide》,出自微软的网站。我在以下这个地址下载得到此文:
http://download.microsoft.com/download/e/b/a/eba1050f-a31d-436b-9281-92cdfeae4b45/FilterDriverDeveloperGuide.doc
我尽量在翻译中使文章保持原貌。如果您认为此文无法理解,建议您首先阅读旧的文件过滤驱动的相关资料。我认为必须有文件系统和windows驱动的相关知识,才能阅读此文。
我未必总是使用规范的名词,但我总是使用最容易理解的名词。一些常用的可能不翻译,比如IRP,MDL,有驱动开发经验的人应该可以理解。另一些可能采用中文(英文)的方式。
一些解释如下:
例程(Routine):我不懂得例程和函数有什么不同。我认为例程就是函数。称为Routine而不是Function可能是为了避免其他c程序员理解得太容易。
接口(Api):编程开发接口,一个提供给你调用的函数。
流(Stream):如果你写过文件系统驱动,那么你一定知道FileObject,对你来说一个流就是一个FileObject。
文件(File):一个文件可能有多个流,因为可能多次打开,多个FileObject可能对应一个文件。
域(Field):一个数据结构中的一个数据成员。喜欢数据库的人可能称为字段。喜欢面向对象的称为数据成员。
透明(transparence):看不见,意味着也不需要管。不过请注意透明的反意词绝对不是不透明(opacity)。
不透明(opacity):不知道的。比如空指针。因为空指针指向的是什么,从空指针本身是了解不到的。所以称为不透明的指针。
回调(Callback)函数:一个由系统调用而且原则上你不能自己调的函数。
预操作(pre-operation)回调:如果打算过滤一个操作,那么这个回调出现在操作完成之前。
后操作(post-operation)回调:如果打算过滤一个操作,那么这个回调出现在操作完成之后。
1.概述
这个文档用于I/O管理器和基本文件系统之间的过滤驱动。文件系统可能是本地或者网络的。这个文档不涉及文件系统和存储设备之间的过滤驱动,比如FtDisk和DMIO.
我们将主要讨论一种新的文件系统过滤驱动模型,所谓的微过滤器(minfiter).
以前的文件系统过滤基于一个例子sfilter.使用IRP和设备对象进行过滤。我们现在称之为“旧过滤模型”
新的架构中一个关键的组件其实是一个旧过滤模型的文件系统过滤驱动,被称为“过滤管理器(Filter Manger)”.在未来,微软发行的操作系统将默认安装这个驱动。(译者注:现在,你得手工安装。)这个驱动通过提供一些库供微过滤器调用来管理所有的微过滤器。必要的头文件,库和二进制代码都在微过滤器IFSKit中。
为何要开发一个微文件系统过滤驱动?
.通过更少的工作量,得到更简单的,更可靠的过滤驱动.
.动态加载和卸载,绑定和解除绑定.
.在过滤栈中,绑定到一个合理确定的位置。
.上下文管理。快捷,干净,可靠的上下文管理,用于文件对象,流,文件,实例和卷.
.一组有用的调用.包括根据文件名寻找,高效存取,和用户态程序之间的通信,以及io排队.
.支持非回环I/O.这样,一个微过滤器发起的I/O请求可以轻松的只让栈中更下面的微过滤器以及文件系统看到了。
.仅仅过滤感兴趣的操作。不象旧过滤模型那样必须挂接每个操作入口以便把操作传递到下层.
2.术语
在过滤管理器架构中,定义了一些新的对象。为了搞清这些,这里将列出一些定义:
过滤器:在文件系统上执行一些过滤操作的一种驱动.
卷:在本地文件系统,这个对象指文件系统所管理的逻辑卷.对于网络重定向文件系统,指所有网络请求被重定向的目的。卷直接对应文件系统(无论本地或者网络)旧过滤模型中的设备对象(DEVICE_OBJECT).
实例:一个过滤器在一个卷上唯一的某层上生成的一个实例化对象。过滤管理器把所有的IO请求发到卷上的实例栈上。一个微过滤器在一个卷上可能不止一个实例.规范的例子是FileSpy.有时候把FileSpy的两个实例分别绑定在另一个过滤器的上边和下边。此时每个实例有一个私有的上下文.这个上下文包含IO操作的日志.可以用来比较一个过滤器上下的IO操作有什么不同。
文件:文件系统保存在一个磁盘上的可能包含若干个流的有名字的数据对象.
流:指一个文件中的物理数据流.
文件对象(FileObject):用来描述一个用户对一个文件中的一个物理数据流的一次打开。
回调数据(CallbackData):过滤管理器中的一种数据结构,包含了一个操作中的所有信息。对应于旧过滤模型中的IRP.
3.微过滤器安装
微过滤器可以通过一个INF文件安装。INF文件指出了这个微过滤器所支持的实例.实例的具体说明在第5节.每个实例有一组标志,还有一个唯一的数值固定了它在过滤栈中的位置。
INF文件中有一个表标明了每个实例的层级.这用来给文件系统过滤的开发商装载他们的微过滤器.有标记标明了这个微过滤器是否需要"自动的绑定".如果是,那么每个新的卷出现的时候,微过滤器都回收到一个通知.它可以在此绑定他们.绑定的时候,inf文件中的层级决定了绑定到什么层次上。
在微过滤器运行时,文件系统过滤开发商也可以在某个指定的层级上动态的生成一个实例,这可以使用FilterAttachAtAltitude()调用.这对于开发者来说可以用来进行测试和排除bug.
4.微过滤器注册
微过滤器是内核驱动。因此它必须导出一个名为DriverEntry的函数。在驱动加载的时候这个函数第一个被调用.很多微过滤器在DriverEntry()中调用FltRegisterFilter().
FilterRegisterFilter()需要传入一个参数。是一个FLT_REGISTRATION结构.包含了:一个卸载例程.实例通知回调,一组上下文回调指针,一组文件系统操作回调指针.一般情况下,微过滤过器只捕获一部分操作,因此文件系统操作回调指针可能并不多。
对于某一种操作,微过滤器可以指定一些附加的标记来指明它是否在所有的情况下都收到它们.比如,如果FLTFL_OPERATION_REGISTRATION_SKIP_PAGING_IO被指定了,微过滤器就不会收到任何此类IRP的paging I/O操作。
同样的,如果FLTFL_OPERATION_REGISTRATION_SKIP_CACHED_IO被指定了,那么只有这类操作的非缓冲请求能看见.(比如说,指定了IRP_MJ_READ类操作,那么所有的缓冲读就都不会被微过滤器捕获了。
5.开始过滤
当一个微过滤器注册自己,它就应该在某个时间调用函数FltStartFiltering()来开始过滤。并不一定要在DriverEntry中调用。不过大多数微过滤器可能是这样做的。
这个函数将激发必要的通知,导致微过滤器绑定到卷上然后开始过滤I/O操作。为此,过滤管理器会通过微过滤器的inf文件遍历它注册过的所有的实例。
每个示例都有一个层级。一个层级是一个唯一的字符串,(如"100.123456"),这个定义了微过滤器的这个实例在栈上的位置。商业版本的微过滤器层级将由微软公司来分配。
层级的数字越高,这个微过滤器绑定在栈上的位置就越高。一些示例层级提供给开发者用来实现微过滤器。这些是仅有的不会被分配的层级。层级有两个作用:一是确定两个微过滤器之间的顺序关系,尤其是有时得实现一些不用去考虑个别微过滤器什么时候加载的功能。
比如说,一个加密解密过滤器必须安装在一个防病毒的过滤的下边。否则,防病毒过滤器无法从已经加密的内容中发现病毒。另外就是提供了一个最小的测试矩阵,用来测试这些过滤驱动的互容性。如果这些驱动实例都是按一个指定的顺序在栈中的,那么测试的时候就不用再考虑排列各种不同的顺序了。
在inf文件中,一个实例和一个标记联系在一起。如果第1个位标记了,那么微过滤器不会在卷出现在系统中的时候得到通知。这样的实例应该通过过滤管理器编程接口来手工的绑定。如果第2位被设置了,即使手工的发送了一个绑定请求,微过滤器也不会收到通知来要求绑定一个实例到一个卷。
6.实例的通知
当一个实例生成的时候,一组回调函数提供来通知微过滤器。通过这些回调,微过滤器可以决定它的实例在什么时候绑定到卷上和从卷上解除了绑定。
6.1. 安装一个实例
回调例程InstatanceSetupCallback()在下列情况下被调用:
.当一个微过滤器加载的时候,每个存在的卷都会导致这个调用。
.当一个新的卷被mount.
.当FltAttachVolume被调用(内核模式)
.当FltAttachVolumeAtAltitude()被调用(内核模式)
.当FilterAttach()被调用(用户模式)
.当FilterAttachAtAltitude()被调用(用户模式)
在这个过程中,微过滤器决定是否在这个卷上生成实例。这个回调的原型如下:
typedef NTSTATUS
(*PFLT_INSTANCE_SETUP_CALLBACK) (
IN PCFLT_RELATED_OBJECTS FltObjects,
IN FLT_INSTANCE_SETUP_FLAGS Flags,
IN DEVICE_TYPE VolumeDeviceType,
IN FLT_FILESYSTEM_TYPE VolumeFilesystemType
);
FltObjects结构喊有指向微过滤器,卷,和实例的指针。这个实例指将要在InstanceSetupCallback()函数中生成的实例。Flags标记是什么操作导致激发了InstanceSetupCallback():
FLTFL_INSTANCE_SETUP_AUTOMATIC_ATTACHMENT:
这是一个微过滤器注册的时候,一个自动的绑定通知。过滤管理器为每个刚加载的微过滤器枚举所有的卷。如果是一个使用者明确的指定一个实例绑定到某一个卷,不会设置有这个标记。
FLTFL_INSTANCE_SETUP_MANUAL_ATTACHMENT:
通过调用FilterAttach()(用户态),或者是FilterAttachVolumeAtAltitude()(用户态),或者是FltAttachVolume()(内核态)所发起的一个手工的请求。
FLTFL_INSTANCE_SETUP_NEWLY_MOUNTED_VOLUME:
文件系统刚刚挂载(mount)了一个卷,所以呼叫InstanceSetupCallback()来通知微过滤器,如果它愿意可以生成实例来绑定这个卷。
在InstanceSetupCallback()中,微过滤器同时得到了卷设备类型(VolumeDeviceType)和卷文件系统类型(VolumeFilesytemType),用以判断这个卷是否过滤器所感兴趣的。同时,微过滤器可以调用FltGetVolumeProperties()来获取卷属性。通过FltSetInstanceContext()在实例上设置上下文。当然这是需要绑定的时候。它甚至可以在卷上打开或者关闭文件。
如果这个回调返回了成功,那么这个实例将绑定到卷上。如果返回了一个警告或者错误,那么不会绑定。
如果微过滤器没有指定InstanceSetup回调,那么,系统将认为用户总是返回了STATUS_SUCCESS,实例总是会生成并绑定。
6.2. 控制实例的销毁
InstanceQueryTeardown()回调仅仅在一个手工解除绑定的请求下被调用。以下操作可能导致:
FltDetachVolume() (内核模式)
FilterDetach()
(用户模式)
如果一个微过滤器没有提供这个回调,那么手工解除绑定是不被支持的。但是,卷的解挂载(dismount)和微过滤器的卸载还是允许的。
如果这个回调返回成功,那么过滤管理器开始销毁给出的实例。最后实例的InstanceTeardownStart()和InstanceTeardownComplete()会被调用。如果返回了错误或者警告,手工解除绑定会失败。推荐的错误代码有: STATUS_FLT_DO_NOT_DETACH,不过实际上你可以返回任何错误代码。
InstanceQueryTeardown()回调的原型是:
typedef NTSTATUS
(*PFLT_INSTANCE_QUERY_TEARDOWN_CALLBACK) (
IN PCFLT_RELATED_OBJECTS FltObjects,
IN FLT_INSTANCE_QUERY_TEARDOWN_FLAGS Flags
);
和InstanceSetupCallback()类似,FltObject指出了与这个销毁操作有关的微过滤器,卷和实例。
6.3. 实例解绑定的同步
如果InstanceTeardownStart()的时候已经决定要解除绑定,那么这个例程中必须做以下的事情:
(1)重设所有的未决的I/O操作(包括预操作和后操作)
(2)保证不会有新的I/O操作进入未决。
(3)对刚刚到达的操作开始最少的工作。
同时,应该做以下操作:
(1)关闭所有打开的文件。
(2)取消所有本过滤器发起的I/O请求。
(3)停止将新的工作任务排队。
然后微过滤器把控制权交还过滤管理器来继续它的销毁过程。当所有与这个实例相关的操作都排除干净或者完成了,InstanceTeardownComplete()会被调用。管理器保证此时所有此实例的存在的操作回调都完成了。这时微过滤器必须关闭所有这个实例打开的文件。
这两个回调的原型如下:
typedef VOID
(*PFLT_INSTANCE_TEARDOWN_CALLBACK) (
IN PCFLT_RELATED_OBJECTS FltObjects,
IN FLT_INSTANCE_TEARDOWN_FLAGS Reason
);
FltObjets中有微过滤器,卷和实例。Resson参数指明这次销毁的原因,可能是以下一些标记的组合:
FLTFL_INSTANCE_TEARDOWN_MANUAL:
这次销毁操作是一个手工的请求。(FilterDetach()或者 FltDetachVolume()).
FLTFL_INSTANCE_TEARDOWN_FILTER_UNLOAD:
这次销毁操作是因为微过滤器执行卸载或者是选择了把卸载请求失败掉导致的。
FLTFL_INSTANCE_TEARDOWN_MANDATORY_FILTER_UNLOAD:
这次销毁操作是一次强制卸载导致的。这种情况下不能把卸载请求失败掉。
FLTFL_INSTANCE_TEARDOWN_VOLUME_DISMOUNT: 这次销毁是一个卷被解挂载的结果。FLTFL_INSTANCE_TEARDOWN_INTERNAL_ERROR:
这次销毁是因为安装实例的时候的一个内部错误导致的,比如内存不足。
请注意没有返回值。InstanceTeardownStart()和InstanceTeardownComplete()都不能失败。过滤管理器保证这些例程都运行在Passive IRQL.
7.回调支持
7.1 回调数据(Callback data)
回调数据(Callback data)是过滤管理器用来描述I/O操作的新结构。类似旧过滤模型下的IRP.微过滤器通过这个结构和过滤管理器交互。不同的是,回调数据不像IRP那样管理一个栈结构。回调数据的管理都通过已经明确定义的过滤管理器接口。并且返回状态值给过滤管理器即可。
FLT_CALLBACK_DATA类型包含了微过滤器描述一个I/O操作所需要的所有的信息。下面继续详细讲解这个结构中的各个域来说明其中包含的信息:
Flags:提供这个操作的一些信息。一个或多个下面的标记可能被设置在Flags中:
FLTFL_CALLBACK_DATA_IRP_OPERATION: 这个回调数据描述一个IRP操作。
FLTFL_CALLBACK_DATA_FAST_IO_OPERATION:这个回调数据描述一个FastIO操作。
FLTFL_CALLBACK_DATA_FS_FILTER_OPERATION:这个回调描述一个文件系统过滤器操作。
FLTFL_CALLBACK_DATA_SYSTEM_BUFFER:这个操作所用的缓冲是一个系统分配的缓冲。
FLTFL_CALLBACK_DATA_GENERATED_IO:这个操作是由一个微过滤器发起的。
FLTFL_CALLBACK_DATA_REISSUED_IO:这个操作被一个当前实例之上的过滤器所重新发回给文件系统。
FLTFL_CALLBACK_DATA_DRAINING_IO:只有设置了后操作(Post-operation)回调的情况下,表明这是一个快速“排出”的I/O操作以便微过滤器的卸载。
FLTFL_CALLBACK_DATA_POST_OPERATION:只有设置了后操作(Post-operation)回调的情况下,表明着个I/O正在后操作中。
FLTFL_CALLBACK_DATA_DIRTY:当一个微过滤器已经改变了这个操作的一个或者多个可变参数的时候,设置这个参数。这个标记仅仅在Pre-operation过程中设置。微过滤器必须用FLT_SET_CALLBACK_DATA_DIRTY()和FLT_CLEAR_CALLBACK_DATA_DIRTY()来操作这个标记。
Thread: 发出这个操作的线程的地址。
Iopb: 指向这个操作的可变参数的指针。这个结构在后边详叙。
IoStatus:IO_STATUS_BLOCK结构返回操作最后的状态。如果一个微过滤器打算结束这个操作,那么必须先设置这个域,然后才能结束这个请求。对于传递给文件系统去的请求,在后操作过程(Post-operation)中有操作最终的状态。
TagData:仅仅在Create操作的后操作回调中有效。当一个操作的目标文件有一个重解析点(Reparse point)的时候设置这个位。
QueueLinks:一个链表入口结构。有时要把回调数据(Callback Data)放入工作队列中使用这个。
QueueContext[2]:一组空指针结构,用来传入附加的上下文到工作队列处理过程中。
FilterContext[4]:一组空指针结构,当回调数据进入了队列,微过滤器可以做任意使用。不依赖于过滤管理器的内部结构。
RequestorMode:这个操作的者的请求模式。
Iopb域所指的是一个FLT_IO_PARAMETER_BLOCK结构。包含了回调数据中可以修改的部分。对比IRP来说,这里相当于IRP的当前栈空间(current stack location)。微过滤器必须访问这个结构来得到每次预操作(pre-operation)和后操作(post-operation)回调的I/O参数。下面是一些更详细的细节:
IrpFlags:IRP中描述这个操作的一些标记。
MajorFunction:IRP主功能号。
MinorFunction:IRP辅功能号。
OperationFlags:即IO_STACK_LOCATION.Flags.
TargetFileObject:这个操作所影响到的目标文件。
TargetInstance:管理这个操作的实例。
Parameters:FLT_PARAMETERS是一个共用体。描述主功能号和辅功能号所指定的操作的具体参数。
除了在预操作回调中不能修改主功能号之外,微过滤器可以修改这个结构中其他的任何参数。如果参数改变,微过滤器应该调用FLT_SET_CALLBACK_DIRTY()来注明这个改变。更多详细的信息将在第8节中讲述。
微过滤器在同一个I/O操作的预回调和后回调中,总是会看到参数是一样的。即使下面的过滤器可能已经修改了这些参数。这是由过滤管理器保证的。但是虽然FLT_IO_PARAMTER_BLOCK的内容是一样的,在预操作和后操作中,这个结构的地址可能不一样。因此微过滤器不应该依赖这个地址。
回调数据结构包含IO_STATUS_BLOCK来记录这个操作的状态。过滤管理器会“尊重”这些改变而不会标记这些数据为脏(Dirty)。微过滤器如果打算在预操作回调中结束这个操作或者是后操作回调中撤消这个操作,都必须先设置这个IO_STATUS_BLOCK。
7.2 预操作回调(Pre-Operation Callbacks)
所有的预操作回调原型都是这样:
typedef FLT_PREOP_CALLBACK_STATUS
(*PFLT_PRE_OPERATION_CALLBACK) (
IN OUT PFLT_CALLBACK_DATA Data,
IN PCFLT_RELATED_OBJECTS FltObjects,
OUT PVOID *CompletionContext
);
所有的预操作回调都返回一个FLT_PRE_OPERATION_CALLBACK_STATUS.这个变量是如下定义的:
FLT_PREOP_SUCCESS_WITH_CALLBACK:这个操作成功了而且微过滤器需要后操作回调。
FLT_PREOP_SUCCESS_NO_CALLBACK:
操作成功了,但是不需要后操作回调。
FLT_PREOP_PENDING:
微过滤器将在未来某个时候结束这个操作(通过调用FltCompletePendedOperation())。微过滤器在返回这个值之前不需要做其他特殊的操作(比如IoMarkIrpPending())。如果这个状态返回了,这个I/O操作被过滤管理器挂起(栈中的下层驱动都不会收到预操作回调),直到FltCompletePendedPreOperation()被调用。
FLT_PREOP_COMPLETE:
微过滤结束了操作。这个微过滤器设置了Data->IoStatus.Status中的I/O状态。这个过滤器以下的微过滤器,旧模型过滤器和文件系统都不会看见这个I/O请求。而之上的微过滤器回看到这个请求以合适的状态完成。对于CLEANUP和CLOSE操作来说,微过滤器以一个失败状态结束这个操作是不允许的。因为这些操作不能失败。
FLT_PREOP_SYNCHRONIZE:
仅仅在非CREATE操作有效。(CREATE操作是自动同步的)。若返回此值微过滤器必须有一个后操作回调。这表明这个微过滤器希望这个操作在同一个线程里完成。也就是说后操作调出现的时候和预操作调用在同一个线程上下文里。这是由过滤管理器所保证的。不管下层的过滤器以及文件系统是挂起还是忽略这个I/O操作。这个状态必须小心使用。因为过滤管理器必须同步整个I/O,这可能影响整个系统的性能。
FLT_PREOP_DISALLOW_FAST_IO:
这个状态仅仅在旧模型下返回BOOLEAN的fastI/O的操作的情况下有效。这个状态表明不接受fastI/O请求,请发送IRP重试。
在预操作返回之前,过滤器可能修改I/O操作的参数。而且能修改的参数都集中在Data->Iopb中。当一个微过滤器修改了任何一个参数,它必须调用FLT_SET_CALLBACK_DATA_DIRTY(),否则,修改不会被承认,可能导致未知的错误。
对此还有两个例外。如果修改的是IoStatus,没有必要设置Dirty就会被过滤管理器所承认。
另一个例外是IRP_MJ_CREATE的后操作过程。如果一个碰到重解析点(reparse point),Data->TagData会指想一个重解析数据缓冲。如果微过滤器打算修改这个缓冲,它可以释放了这个缓冲然后重新分配一个(不能为空)。此时不用调用FLT_SET_CALLBACK_DATA_DIRTY().
7.3 后操作回调(Post-Operation Callbacks)
所有的后操作都有同样的原型:
typedef FLT_POSTOP_CALLBACK_STATUS
(*PFLT_POST_OPERATION_CALLBACK) (
IN OUT PFLT_CALLBACK_DATA Data,
IN PCFLT_RELATED_OBJECTS FltObjects,
IN PVOID CompletionContext,
IN FLT_POST_OPERATION_FLAGS Flags
);
Data->Flags中都设置了FLTFL_CALL_DATA_POST_OPERATION.
如果一个微过滤器在预操作回调中返回FLT_PREOP_SUCCESS_WITH_CALLBACK了,它肯定能收到这个同样操作的完成回调也就是后操作回调。后操作回调返回的类型为FLT_POSTOP_CALLBACK_STATUS.这个值是:
FLT_POSTOP_FINISHED_PROCESSING – 微过滤器已经完成了这个操作控制权应该还给请求的发起者.
FLT_POSTOP_STATUS_MORE_PROCESSING_REQUIRED – 微过滤器还没有完成这个请求,而且回在后边完成它,使用FltCompletePendedPostOperation().
与旧模型过滤驱动的完成例程不同,这个后操作回调执行在DPC中断级上。如果一个微过滤器需要完成一些在DPC上完成不了的工作,可以调用FltDoCompletionProcessingWhenSafe()。有必要的情况下(我们在DPC中断级时),会把工作插入一个工作线程。除非这个请求是不能被排队的(比如一个页面交换请求(paging I/O)).
对于打算在后操作回调中取消一个文件打开的微过滤器,FltCancelFileOpen()调用可以对指定的FileObject来一个清理和关闭的功能。微过滤器必须必须填写合适的错误代码,并且在后操作回调中返回FLT_POSTOP_FINISHED_PROCESSING.
当一个实例被卸除的时候,过滤管理器可能调用候后操作回调,但是此时操作还未真的完成。这时,标志FLTFL_POST_OPERATION_DRAINING会设置。此时提供了尽量少的信息。所以微过滤器应该清理所有的从预操作中传来的操作上下文,并返回FLT_POSTOP_FINISHED_PROCESSING.
操作的IRP_MJ(主功能码)中增加了一些附加的新数值来表示FastI/O中的一些没有IRP与之对应的操作。目的是把IRP操作和FastI/O操作可以采用同样的处理方式。这样通过一些标记就可以区分IRP操作,微过滤相关操作和FastI/O操作,而不用注册一些类似的回调函数了(比如读,写,锁定等操作的回调函数)。
有以下这些操作主功能码:
所有原有的IRP_MJ。
IRP_MJ_FAST_IO_CHECK_IF_POSSIBLE
IRP_MJ_NETWORK_QUERY_OPEN
IRP_MJ_MDL_READ
IRP_MJ_MDL_READ_COMPLETE
IRP_MJ_PREPARE_MDL_WRITE
IRP_MJ_MDL_WRITE_COMPLETE
IRP_MJ_VOLUME_MOUNT
IRP_MJ_VOLUME_DISMOUNT
IRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION
IRP_MJ_RELEASE_FOR_SECTION_SYNCHRONIZATION
IRP_MJ_ACQUIRE_FOR_MOD_WRITE
IRP_MJ_RELEASE_FOR_MOD_WRITE
IRP_MJ_ACQUIRE_FOR_CC_FLUSH
IRP_MJ_RELEASE_FOR_CC_FLUSH
关于主功能号的一些要注意的地方:
(1)IRP_MJ_CREATE的预操作回调中不能获得或者设置文件,流,或者流句柄这样的上下文。因为在预操作中,文件或者流还没有决定是否生成。
(2)IRP_MJ_CLOSE 的后操作中不能同样不能设置或者或者文件,流或者流句柄这样的上下问。因为在这个操作中,一些与之相关的系统内部结构可能已经释放了。
(3)IRP_MJ_CLEANUP和IRP_MJ_CLOSE 是永远不能失败的。他们可以挂起,传递或者成功结束。但是不能返回FLT_PREOP_COMPLETE并填一个错误码在IoStatus块中。
后操作回调不能失败,因为这个操作已经发生了。如果希望在后操作回调中让一个操作失败,那么你必须撤消已经成功的操作。比如IRP_MG_CREATE操作,过滤管理器提供了FltCancelFileOpen()来销毁已经打开的文件对象。但是过滤器还是有责任来重新整理由于Create操作而被覆盖掉的原来的文件的内容。
在后操作中对操作参数做的任何改变都不会被过滤管理器所承认。
8.操作回调数据参数
8.1 I/O参数块(I/O Parameter Block)
就像前面所提及的,回调数据(Callback Data)包含了两个关于I/O的重要的数据结构:
1)I/O状态块(I/O Status Block): Data->IoStatus用来返回它自己所结束的操作的状态(或者在后操作回调中可以读取下层驱动所完成的操作的状态)。
2)I/O参数块(I/O Parameter Block): Data->Iopb指向这个I/O操作中专为本微过滤器使用的数据。
本节深入讨论读取和改变这些参数的问题。
Iopb中的主功能号和辅功能号表示IRP/FastIo/FsFilter-主/辅功能,主功能号不能由过滤器修改。过滤管理器不支持。
TargetFileObject表示I/O操作的目标,流的文件对象。微过滤器可以修改它(必须调用FLT_SET_CALLBACK_DATA_DIRTY()),而且被过滤管理器承认。
TargetInstance参数与I/O从一个实例向另一个实例传递的意义不同,是表示当前的实例。过滤器可以改变这个指针。但是仅仅能是同一个层级,但绑定在其他的卷上的实例。同时,也必须调用FLT_SET_CALLBACK_DATA_DIRTY()。
微过滤器是不能修改TargetInstance指向同一个卷上的另一个实例的。举个例子来说,一个微过滤器在一个卷C:上有两个实例。一个在层级200称为实例C200,,另一个在层级100,称为实例C100,同时在D:上还有一个层级200的实例,称为实例D:200。现在假设在实例C200上IRP_MJ_READ的预操作回调被调用了,微过滤器可以把TrageInstance修改为实例D:200,但是不能修改为实例C:100。
这组织了任何微过滤器同一个卷的栈中非法的(不按固定顺序的)传递操作。
I/O参数块中含有一些与具体操作相关的参数。微过滤器必须用与操作配套的共用体结构来访问他们。对于IOCTL和FSCTL,具体的控制码也有不同的共用体结构。微过滤器必须检查具体的控制码来使用正确的共用体。
8.2 使用缓冲/MDL
在IRP的世界中,缓冲采用多种机制传递到驱动中。比如支持基本I/O的设备对象,缓冲是通过Irp->UserBuffer传入的,这对文件系统是最常见的。有些设备只支持缓冲I/O,那么I/O管理器传入的缓冲区Irp->AssociatedIrp.SystemBuffer.对于支持直接I/O的设备,缓冲是通过Irp->MdlAddress,一个被锁定的MDL。
但是可能有例外。一些IRP的栈空间的缓冲参数直接传入。可能指向核心内存或者是原始的用户空间内存。和设备对象对I/O操作的要求无关。这些缓冲不能传到硬件,因为他们无视设备对象所支持的I/O类型。
也有一些IRP总是缓冲I/O,比如IRP_MJ_QUERY/SET_INFORMATION.
对于微过滤器,缓冲总是通过适当的操作相关的共用体传入。没有一个通用的方法可以获得缓冲/MDL的地址来源。这种设计是为了减少栈空间之间缓冲导致的冲突。
在回调数据中,如果操作是缓冲型的,那么标记 FLTFL_CALLBACK_DATA_SYSTEM_BUFFER会被设置。如果是这样的,那么缓冲区在非分页的核心内存中。
如果这个标记没有设置,那么只能说明不是缓冲型。微过滤器有方法做进一步区分(见下一节)。缓冲还是可能来源于某个核心内存池。但是只要这个标记没有设置,过滤器就应该假设内存在原始用户空间中。这种情况下,如果缓冲并不是传入的用户缓冲,那么总是需要一个MDL来锁定一些页面,而且调用者必须获得一个系统空间地址来访问这些页面。这是一条规则,将在下节详叙之。
最后,对于某些微过滤器希望能够定位缓冲/长度/MDL来做一些最通用的操作的时候,FltDecodeParameters()提供用来做一个快速的查找并返回一个指向参数结构中的缓冲/长度/MDL。对于没有缓冲空间的操作,返回STATUS_INVALID_PARMETER.
NTSTATUS
FLTAPI
FltDecodeParameters(
IN PFLT_CALLBACK_DATA CallbackData,
OUT PMDL **MdlAddressPointer OPTIONAL,
OUT PVOID
**Buffer OPTIONAL,
OUT PULONG *Length OPTIONAL,
OUT LOCK_OPERATION *DesiredAcces OPTIONAL
);
DesiredAccess表示微过滤器可以使用的访问缓冲的方式。比如对于IRP_MJ_READ,可能指定IpWriteAccess,意味着这个缓冲区是可写的。对于IRP_MJ_WRITE,一般指定IoReadAccess表示微过滤器可以读取这个缓冲区但是不能修改它。这样一个应用使用一个仅仅有只读权限的页面来发起一个写请求也是合法的了。
对于希望缓冲空间被锁定的过滤器,应该了解:如果FLTFL_CALLBACK_DATA_SYSTEM_BUFFER表示设置了,那么可以假设这个缓冲区已经被锁定,可以安全的访问。
如果没有设置则可以调用FltLockUserBuffer()来锁定页面。这个调用可以保证页面被合适的方法锁定。如果成功了,它会设置与操作相关的参数部分的MdlAddress域为用来描述这些页面的MDL.
8.3 交换缓冲
一些微过滤器为了某些操作必须交换缓冲。考虑一个微过滤器实现加密算法,对一个非缓冲(non-cached)IRP_MJ_READ,它一般会希望把缓冲中的数据解密。同样的在写的时候,它希望把内容加密。考虑以下情况:内容无法在这个空间中加密。因为对于IRP_MJ_WRITE,这个微过滤器可能只有IoreadAccess权限。
因此微过滤器必须以他自己的有读写权限的缓冲区取代原来的缓冲区。加密了原缓冲区中的内容后写入新缓冲区后,再继续传递I/O请求。
为此,过滤管理器支持缓冲转换。有以下一些游戏规则必须遵守:
1. 改变了缓冲区的微过滤器必须有对应的后操作回调。这样缓冲能被过滤管理器自动的转换回来。
2. 如果改变的是一个标记有FLTFL_CALLBACK_DATA_SYSTEM_BUFFER标记的缓冲,必须保证新的缓冲是非分页内存。(也就是比如来自非分页内存池或者锁定的内存)。
3. 如果以上的标记没有设置,那么微过滤器必须按设备对象的要求来确定缓冲类型(可以在卷属性中查看DeviceObjectFlags等标记)。比如如果是支持直接I/O的,那么必须提供MDL等等。
4. 如果微过滤器使用非分页池中的缓冲来给一个没有设置FLTFL_CALLBACK_DATA_SYSTEM_BUFFER的操作,那么它也必须用MuBuildMdlForNonpagedPool()并把地址填写到MdlAddress域中。这是因为这么一来,下面的任何过滤器或者文件系统都不用再尝试去锁定非分页池(可以在构建的时候使用断言,但是对效率不利),如果提供了一个MDL,过滤器和文件系统总是可以通过MDL访问缓冲(可以获得一个系统内存地址来访问它)。
5. 替换一个缓冲的时候,微过滤器也必须换掉MDL(就是说缓冲和MDL要保持同步了)。对于通常的直接I/O异常,可以把MDL留空。
6. 微过滤器不应该释放旧的MDL和缓冲空间。
7. 不要尝试在后操作回调中替换掉旧的缓冲和MDL.过滤管理器自动执行这些操作。实际上微过滤器在后操作回调中的Iopb中见到缓冲空间和MDL是旧的(译者注:替换前的)。微过滤器必须自己在上下文中记录新的缓冲区。
8. 微过滤器应该释放自己分配的(和替换过的)缓冲。无论如何,如果有的话,过滤管理器会自动释放新缓冲的MDL。
9. 微过滤器不希望过滤管理器自动释放交换过的缓冲MDL可以调用FltRetainSwappedBufferMdl()。
10. 过滤器如果希望访问交换过的缓冲的MDL可以在后操作回调中使用FltGetSwappedBufferMdl()。既然一个更下层的过滤器或文件系统交换了新的缓冲空间进来,那么有可能生成了一个MDL。在后操作回调中微过滤器交换缓冲之前,过滤管理器保存了所有这样的的MDL。这个调用可以用来访问这些MDL.
9.上下文(Context)支持
所有的过滤器都必须在他们所操作的各种对象中记录一些他们自己的状态。让微过滤器在这些对象中拥有他们的上下文是过滤管理器的一个重要特点。
一个上下文是使用FltAllocateContext()分配一的一片动态内存区。这个调用传入需要的内存空间的大小并返回一个内存空间指针。系统会绑定一个内部的数据头用来跟踪这个指针所对应的上下文。
以下种类的对象支持上下文:
卷 - 一个已经挂载的设备。
实例 - 一个微过滤器对对一个卷的一次绑定。一个微过滤器可能对一个卷绑定多次。如果一个微过滤器对一个卷只能绑定一次,那么推荐用卷上下文来代替实例上下文,这样效率高多了。
文件 - 指关于一个文件的所有打开的流。一般这些上下文是不支持的。
流 - 文件上的一个单独的数据流。
流句柄 - 一个文件的一次打开,比如一个文件对象。
9.1 上下文注册
注册的时候,微过滤器定义它所想使用的上下文的类型,大小以及一个用来清理上下文的例程。微过滤器用一组FLT_CONTEXT_REGISTRATION结构来确定这些参数.
下面解释FLT_CONTEXT_REGISTRATION的细节:
ContextType:
注册的上下问的类型。
Flag:
表示这个上下文的一些特殊处理信息。当前的定义有:FLT_CONTEXT_REGISTRATION_NO_EXACT_SIZE_MATCH:默认的情况下,过滤管理器会比较一个给定的上下文的请求的长度和分配上下文的时候指定的数据长度。如果指定了这个标记,如果请求分配的内存大小少于等于注册的时候所指定的长度,过滤权利气回使用特殊指定的分配例程。当注册时候指定的大小为FLT_VARIABLE_SIZED_CONTEXT或者分配和释放例程都已经指定了的时候,这个标记被忽略。
CleanupContext:
当过滤管理器决定应该清理这个上下问的时候这个例程被调用。如果在上下文被释放之前没有什么需要清理的,这个可以设置为NULL.
Size:
这个上下文的字节大小。这用来允许过滤管理器使用内存池技术(如旁视列表)来让分配和释放更加有效率。如果使用了自己的分配和释放例程,这个域被忽略。
PoolTag:
分配内存的“池”上下文。这是使用ExAllocatePoolWithTag的时候用的。如果使用自己的分配和释放例程,这个域被忽略。
Allocate:
如果打算使用自己的分配例程,设置这个指针。如果打算依赖过滤管理器的内存池技术,那么这个应该设置为NULL.
Free:
如果想使用自己的释放例程,那么这个必须设置为非空。
如果一个微过滤器有相同类型的上下文但是长度不一,它可以为同类型的上下文注册不同的FLT_CONTEXT_REGISTRATION结构来利用过滤管理器的内存池技术。
9.2 上下文生成接口
下面是FltAllocateContext()的函数原型:
NTSTATUS
FLTAPI
FltAllocateContext (
IN PFLT_FILTER Filter,
IN FLT_CONTEXT_TYPE ContextType,
IN SIZE_T ContextSize,
IN POOL_TYPE PoolType,
OUT PFLT_CONTEXT *ReturnedContext
);
ContextType可以是以下的情况:FLT_VOLUME_CONTEXT , FLT_INSTANCE_CONTEXT, FLT_FILE_CONTEXT, FLT_STREAM_CONTEXT, 或者是FLT_STREAMHANDLE_CONTEXT.
下面是一组用来把一个上下文绑定到一个对象上的例程。请注意上下文的类型和对象类型必须是配套的。
NTSTATUS
FLTAPI
FltSetVolumeContext (
IN PFLT_VOLUME Volume,
IN FLT_SET_CONTEXT_OPERATION Operation,
IN PFLT_CONTEXT NewContext,
OUT PFLT_CONTEXT *OldContext OPTIONAL
);
NTSTATUS
FLTAPI
FltSetInstanceContext (
IN PFLT_INSTANCE Instance,
IN FLT_SET_CONTEXT_OPERATION Operation,
IN PFLT_CONTEXT NewContext,
OUT PFLT_CONTEXT *OldContext OPTIONAL
);
NTSTATUS
FLTAPI
FltSetFileContext (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
IN FLT_SET_CONTEXT_OPERATION Operation,
IN PFLT_CONTEXT NewContext,
OUT PFLT_CONTEXT *OldContext OPTIONAL
);
NTSTATUS
FLTAPI
FltSetStreamContext (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
IN FLT_SET_CONTEXT_OPERATION Operation,
IN PFLT_CONTEXT NewContext,
OUT PFLT_CONTEXT *OldContext OPTIONAL
);
NTSTATUS
FLTAPI
FltSetStreamHandleContext (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
IN FLT_SET_CONTEXT_OPERATION Operation,
IN PFLT_CONTEXT NewContext,
OUT PFLT_CONTEXT *OldContext OPTIONAL
);
设置一个上下文的时候有两种类型的操作可能发生。 它们是:
FLT_SET_CONTEXT_KEEP_IF_EXISTS:如果没有存在的上下文,那么将设置新的上下问。如果有存在的,那么新的上下文将不会设置上去,而且会返回一个错误代码。如果OldContext参数定义了,那么已经存在的上下文会返回。如果有,调用者必须释放返回的上下文。如果新的上下文没设置上去,那么调用者必须负责释放它。
FLT_SET_CONTEXT_REPLACE_IF_EXISTS: 即使旧的上下文存在,新的也回释放上去。如果OldContext定义了,那么被取代的上下文会返回。调用者必须自己释放它。如果没有定义,则旧的上下文会被自动释放。
当相关设备对象被系统释放,过滤管理器会在合适的时机调用微过滤器来清理上下文。微过滤器可能希望某个时候自己删除一个对象上的上下文。为此,微过滤器可以调用以下的一个例程来删除上下文。当然前提是它拥有这个上下文的指针。
VOID
FLTAPI
FltDeleteContext (
IN PFLT_CONTEXT Context
);
如果没有指针呢?它必须通过指定对象来删除上下文。可以使用以下这些例程中的某个:
NTSTATUS
FLTAPI
FltDeleteVolumeContext (
IN PFLT_FILTER Filter,
IN PFLT_VOLUME Volume,
OUT PFLT_CONTEXT *OldContext OPTIONAL
);
NTSTATUS
FLTAPI
FltDeleteInstanceContext (
IN PFLT_INSTANCE Instance,
OUT PFLT_CONTEXT *OldContext OPTIONAL
);
NTSTATUS
FLTAPI
FltDeleteFileContext (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
OUT PFLT_CONTEXT *OldContext OPTIONAL
);
NTSTATUS
FLTAPI
FltDeleteStreamContext (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
OUT PFLT_CONTEXT *OldContext OPTIONAL
);
NTSTATUS
FLTAPI
FltDeleteStreamHandleContext (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
OUT PFLT_CONTEXT *OldContext OPTIONAL
);
如果OldContext参数为空,那么过滤管理器会释放对这个上下文的所有的引用。否则,上下文会通过OldContext返回。微过滤器必须自己调用FltReleaseContext来释放它。
9.3 上下文获取(Retrieval)接口
下面的例程用来获取某个设备的相关上下文。使用完毕,调用者必须释放返回的上下文,释放使用FltReleaseContext().上下文不能在DPC中断级获取。所以如果一个后操作回调中希望得到一个上下文,那么必须从预操作中获得并传入。
NTSTATUS
FLTAPI
FltGetVolumeContext (
IN PFLT_FILTER Filter,
IN PFLT_VOLUME Volume,
OUT PFLT_CONTEXT *Context
);
NTSTATUS
FLTAPI
FltGetInstanceContext (
IN PFLT_INSTANCE Instance,
OUT PFLT_CONTEXT *Context
);
NTSTATUS
FLTAPI
FltGetFileContext (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
OUT PFLT_CONTEXT *Context
);
NTSTATUS
FLTAPI
FltGetStreamContext (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
OUT PFLT_CONTEXT *Context
);
NTSTATUS
FLTAPI
FltGetStreamHandleContext (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
OUT PFLT_CONTEXT *Context
);
当使用完毕,下面的一些例程用来释放获得的上下文。一般推荐不要在操作之间传递这些上下文指针。上下文的获取非常有效率,是专门设计用来在每个操作要使用的时候专门来获取上下文的。
VOID
FLTAPI
FltReleaseContext (
IN PFLT_CONTEXT Context
);
类似实例通知例程,每个操作的回调例程都收到一个FLT_RELATED_OBJECTS结构。这个结构包含所有这个操作相关的所有已知的的对象。为了简化上下文的获取,有一个类似的FLT_RELATED_CONTEXT可以一次获取。这个结构如下:
typedef struct _FLT_RELATED_CONTEXTS {
PFLT_CONTEXT VolumeContext;
PFLT_CONTEXT InstanceContext;
PFLT_CONTEXT FileContext;
PFLT_CONTEXT StreamContext;
PFLT_CONTEXT StreamHandleContext;
} FLT_RELATED_CONTEXTS, *PFLT_RELATED_CONTEXTS;
接下来两个例程用来依次获得多个例程,此外也有一次性释放。对于FltGetContexts()调用者指定(在DesiredContext参数中)需要的上下文。在内部,一次获得多个上下文比一个一个的获得它们效率高。当然,对于不需要的上下文最好是不要去获取它。FLT_ALL_CONTEXTS可以用来得到所有可用的上下文。
VOID
FltGetContexts (
IN PFLT_RELATED_OBJECTS FltObjects,
IN FLT_CONTEXT_TYPE DesiredContexts,
OUT PFLT_RELATED_CONTEXTS Contexts
);
VOID
FltReleaseContexts (
IN OUT PFLT_RELATED_CONTEXTS Contexts
);
9.4 上下文释放接口
当过滤管理器决定了一个上下文要被释放的时候,微过滤器的相关回调会被调用。这个回调例程应该清理任何上下文(包括清理分配的内存,释放资源等等)。通过返回,过滤管理器会释放传入的上下文结构。
每个类型的上下文都要有一个对应的清理例程。这些例程定义如下:
typedef VOID
(*PFLT_CONTEXT_CLEANUP_CALLBACK) (
IN PFLT_CONTEXT Context,
IN FLT_CONTEXT_TYPE ContextType
);
同一个清理例程可以注册给多个不同类型的上下文。
10.与用户态的通信
10.1 过滤器通信端口对象
为了实现安全的和支持多种不同类型通信方法,一个新的对象被引入:微过滤器通信端口(以下简称通信端口或者端口)。专门设计用来给核心态-用户态通信或者反过来。核心-核心通信现在不在支持。一个端口是一个有名字的NT对象。而且有一个安全的描述符号。
过滤管理器生成了一个新的对象类型,FilterConnectionPort来实现这个。过滤管理器在它的DriverEntry中生成这个新的对象类型,赶在了任何微过滤器加载之前。
只有核心模式的驱动才能生成一个通信端口,使用以下的调用:
NTSTATUS
FltCreateCommunicationPort(
IN PFLT_FILTER Filter,
OUT PHANDLE PortHandle,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PVOID ServerPortCookie OPTIONAL,
IN PFLT_CONNECT_NOTIFY ConnectNotifyCallback,
IN PFLT_DISCONNECT_NOTIFY DisconnectNotifyCallback,
IN PFLT_MESSAGE_NOTIFY MessageNotifyCallback,
IN ULONG MaxConnections
);
Filter是微过滤器的过滤器句柄。成功生成之后,端口的句柄在PortHandle参数中返回。
和其他的NT对象一样,ObjectAttributes参数定义了OBJECT_ATTRIBUTES结构来初始化要生成的端口对象的名字,对象属性和安全描述符等。
请注意属性中OBJ_KERNEL_HANDLE标记必须设置。因为通信端口只能是核心对象。
ServerPortCookie是一个上下文。微过滤器可以通过这个和端口联系在一起。这个上下文对过滤管理器是不透明的。所有的连接,中断通知,都会同过这个上下文才能传递给微过滤器。有些过滤器可能要生成一组通信端口,又想功用一个同志例程。那么可以通过这个上下文中保存的数据进行区分。
调用者还可以注册一些回调函数:
ConnectNotifyCallback():
当一个用户态进程尝试打开一个端口的时候,这个例程被调用。过滤器可以选择把这个请求失败掉。通知例程回手到一个关于此连接的句柄。每一个连接有唯一的一个句柄。ServerPortCookie 也会传入。微过可以填写ConnectionCookie为一个上下文。这个上下文会传到所有的用户态传来的消息以及连接中断例程中。
DisconnectNotifyCallback():
当一个端口被用户态关闭的时候会调用这个回调。(也就是打开计数到0的时候)。
MessageNotifyCallback():
任何时候手到一个消息都会调用这个。
MaxConnections指出了这个通信端口上允许的最大向外连接数。这没有默认值,必须设置得大于0。
并不能保证所有的对象名会生成在根名字空间。有可能过滤管理器把它们映射在\FileSystem\Filters目录下。不过即使如此,对微过滤器和用户态应用程序来说,这是透明的。当引用了一个通信断口的名字,那么所有的足见都应该使用同样的名字。例子Sacnner Minifiter中展示了这是怎么做的。
对应于新的对象类型,新的访问方式也被引入了。有新的访问方式如下:
FLT_PORT_CONNECT
FLT_PORT_ALL_ACCESS
这是一些访问类型。调用者可以设置这些来给使用者权限。用于构造安全描述符的时候,使用InitializeObjectAttributes()。
设置了FLT_PORT_CONNECT,那么我们的应用程序足可以连接这个端口并发送和接受消息。
微过滤器生成一个端口之后,端口就会开始侦听可能的连接。直到你使用ZwClose()将它关闭为止。
10.2 从用户态连接到通信端口
微过滤器的通信端口模型和旧模型的过滤器一样,是不对称的。核心态端生成,用户态端连接。有一个接口用来给用户态应用打开一个端口。当端口建立,ConnectNotify()例程被调用来通知微过滤器。
用户态下连接一个端口的编程接口原型如下:
HRESULT
FilterConnectCommunicationPort(
IN LPWSTR lpPortName,
IN DWORD dwOptions,
IN LPVOID lpContext,
IN WORD wSizeOfContext,
IN LPSECURITY_ATTRIBUTES lpSecurityAttributes,
OUT HANDLE *hPort
);
lpPortName是一个宽字符格式的字符串,指出要连接的端口的名字。这个名字应该和微过滤器生成这个端口的时候一样。名字可以以”\”开头表示在根路径中。过滤管理器会在合适的路径下打开他们并管理很多微过滤器通信端口。
dwOptions现在没有使用。
LpContext是一个指针。指向一个不透明的参数块。这个参数会被传入到ConnectNotify()中。比如可以用来鉴别请求生成端口的应用程序的版本。wSizeOfContext指出这个上下文的字节数。
LpSecurtityAttributes指出给用户端的安全权限。如果这个句柄是继承得到的。
如果这个调用不成功,合适的HRESULT会返回。
返回得到的句柄可以被管理或者被复制。这要通过一些编程接口。它也可以和一个I/O完成端口绑定。
当这个接口被调用,一个新的核心态的无名端口对象就生成了(类型是FilterCommunicationPort).这个对象用来表示这个连接。微过滤器的ConnectNotify()例程被调用,从而得到通知。而且也得到连接端口的句柄,用来从核心态发送消息。
如果调用者没有访问这个服务端口的权限,或者已经达到了最大连接数,那么调用会失败。
10.3 中断与通信端口的连接
当用户态程序调用CloseHandle()或者核心态调用ZwClose()来关闭连接句柄的时候,连接会中断。
只有用户态调用CloseHandle()的时候,微端口的DisconnectNotify()例程才会调用。
理想情况下,微过滤器应该总是在连接结束的时候,在DisconnectNotify()中关闭连接。如果一个微过滤器在其他地方关闭句柄,那么它必须用一些同步方法确保不会在DisconnectNotify()中关闭再次关闭它。
当一个连接在核心态或者用户态被中断了,以下情况发生:
1.
所有用户态的等待(通过FilterGetMessage)被清理掉,而且以STATUS_FLT_PORT_DISCONNECTED结束掉(被解释为win32错误码ERROR_DISCONNECTED)。
2. 所有核心的被阻塞的发送例程会以STATUS_FLT_PORT_DISCONNECTED结束阻塞。
3. 因为端口变无效了,所以不可能有其他的等待或者阻塞的情况出现。
微过滤器总是可以对服务端口的句柄调用ZwClose()来关闭服务端口。这并不会使已经建立的连接中断,但是会阻止新连接的建立。
10.4 卸载
微过滤器总是必须在FltUnregisterFilter()调用之前,在FilterUnload例程或者更早关闭服务端口。否则系统可能在卸载例程中被挂起。
即使在有一些连接打开的情况下(比如用户态一放已经打开了一些连接句柄),微过滤器也应该允许被卸载。这种情况下,过滤管理器会尝试强行终止这些连接。过滤管理器会调用DisconnectNotify()例程。微过滤器应该在这里关闭这些连接句柄以避免句柄的泄漏。
11.文件名处理
通过查找操作的参数或者询问文件系统,过滤管理器能得到对象的名字。因而过滤管理器提供一组调用来方便获取对象名。为了更高的效率,过滤管理器也暂存一些对象名。当很多过滤器经常查一个名字的时候,暂存这个名字所付出的代价是很值得的。
查询对象名的时候,过滤管理器返回一个FLT_FILE_NAME_INFORAMTION结构来避免数据拷贝。这些结构被根据过滤器的请求次数计数。只有通过过滤管理器的编程接口才能改变这些结构中的数据。下面有这些结构的详细信息。
11.1 从操作获得一个文件名
可以在当前操作的CallbackData->Iopb->TargetFileObject中得到文件名。这需要调用下面的例程:
NTSTATUS
FLTAPI
FltGetFileNameInformation (
IN PFLT_CALLBACK_DATA CallbackData,
IN FLT_FILE_NAME_FORMAT NameFormat,
IN FLT_FILE_NAME_QUERY_METHOD QueryMethod,
OUT PFLT_FILE_NAME_INFORMATION *FileNameInformation
);
CallbackData是这个操作的FLT_CALLBACK_DATA结构。现在假设过滤器想从这个操作中得到文件名。
名字的格式是以下三种:
FLT_FILE_NAME_NORMALIZED_FORMAT:请求全路径名字,包括卷名。所有的短名被扩展成长名。任何流名足见回去掉后边的“:$DATA”.如果这是一个目录名,且不是根目录,最后的“\”会被去掉。
FLT_FILE_NAME_OPENED_FORMAT:
包含全路径,包括卷名。但是这个名字和打开这个对象所用的名字相同。因此可能在路径中包含一些短名。
FLT_FILE_NAME_SHORT_FORMAT:
仅仅含有路径中最后一个元素的短名(Dos名),不会返回全路径。
QueryMethod应该是以下之一:
FLT_FILE_NAME_QUERY_DEFAULT:
搜索一个名字的时候,管理器会首先找暂存的名字。然后再询问文件系统。
FLT_FILE_NAME_QUERY_CACHE_ONLY:
仅仅在暂存中找。如果失败,返回STATUS_FLT_NAME_CACHE_MISS.
FLT_FILE_NAME_QUERY_FILE_SYSTEM_ONLY:仅仅询问文件系统,不会从暂存中去寻找这个名字。
名字在最后一个参数中返回,FileNameInformation.这个结构是一组共享缓冲的Unicode字符串。不同的字符串表明不名字中不同的部分.
typedef struct _FLT_FILE_NAME_INFORMATION {
USHORT Size;
FLT_FILE_NAME_FORMAT Format;
FLT_FILE_NAME_PARSED_FLAGS NamesParsed;
UNICODE_STRING Name;
UNICODE_STRING Volume;
UNICODE_STRING Share;
UNICODE_STRING Extension;
UNICODE_STRING Stream;
UNICODE_STRING FinalComponent;
UNICODE_STRING ParentDir;
} FLT_FILE_NAME_INFORMATION, *PFLT_FILE_NAME_INFORMATION;
当一个文件名信息结构从FltGetFileNameInforamtion()返回,name,Volume,Share(用于远程文件)会被解析出来。如果一个微过滤器需要其他的名字信息,它应该调用FltParseFileNameInformation().
运行在DPC或者更低级的中断级别的时候,微过滤器可以在IO过程中任何地方调用FltGetFileNameInformation().当它在一个可能导致死锁的情况下请求(比如处理分页交换的时候),如果在暂存中没有找到名字或者指定了必须从文件系统读取那么调用会失败。
即使没有回调数据(Callback Data)存在,仅仅知道文件对象(FileObject)的时候,微过滤器也可以调用FltGetFileNameInformationUnsafe()来获得一个文件名。不过过滤器必须知道当前向文件系统询问这个名字是安全的。这个调用不能像FltGetFileNameInformation()那样检查是否导致死活。微过滤器必须自己保证它。
当一个名字使用完毕,应该调用以下的例程来释放:
FLTAPI
FltReleaseFileNameInformation (
IN PFLT_FILE_NAME_INFORMATION FileNameInformation
);
过滤管理器的名字暂存对微过滤查找对象的名字来说效率足够了。名字暂存机制也管理了由于重新命名而无效的名字。维护一个名字暂存空间的复杂逻辑对微过滤器们而言是透明的。但是微过滤器对于无效的名字并不是完全不需要关系。当一个重命名发生了,过滤管理器清理所有的受影响的暂存文件名。但是微过滤器可能引用过一个过时的文件名。当这些引用全部被释放的时候,过时的文件名信息结构才会被真的释放掉。如果一个微过滤器请求询问一个已经被重新命名的对象,将尽可能的返回新的名字。
11.2 文件名的附加支持
过滤管理器也提供了一个编程接口来帮助微过滤器获得一个改名或生成硬连接的操作的目的名:
NTSTATUS
FltGetDestinationFileNameInformation (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
IN HANDLE RootDirectory OPTIONAL,
IN PWSTR FileName,
IN ULONG FileNameLength,
IN FLT_FILE_NAME_FORMAT NameFormat,
IN FLT_FILE_NAME_QUERY_METHOD QueryMethod,
OUT PFLT_FILE_NAME_INFORMATION *RetFileNameInformation
);
这个接口只能在IRP_MJ_SET_INFORMATION的预操作回调中调用,包括FileSetNameInformation和FileSetLinkInformation。调用者从这个操作中获得参数,这个调用会返回一个FLT_FILE_NAME_INFORMTION结构,包含了目标文件名在其中。与使用FltGetFileNameInformation()相同,这个调用返回的名字使用完毕之后,应该调用FltReleaseFileNameInformation()来释放。
命名隧道(Name Tunneling)是过滤器的另一个困绕之处。过滤管理器提供了一个编程接口来获得一个命名管道需要的新名字:
NTSTATUS
FltGetTunneledName (
IN PFLT_CALLBACK_DATA CallbackData,
IN PFLT_FILE_NAME_INFORMATION FileNameInformation,
OUT PFLT_FILE_NAME_INFORMATION *RetTunneledFileNameInformation
);
命名隧道仅仅影响获取文件名采用通常格式的微过滤器。如果一个微过滤器在它的一个预操作需要一个通常格式的文件名,这个预操作回调的来源是CREATE,改名,或生成硬连接的话,它应该在后操作回调中调用FltGetTunneledName()来确认这个名字是否被操作系统命名隧道所改过。如果命名隧道确实出现了,那么RetTunneledFileNameInformation中回返回一个新的文件名字信息结构。微过滤器必须用这个新的名字,并且处理完毕后必须用FltReleaseFileName()来释放它。
(译者注:什么是命名隧道(Name Tunneling)
长文件名出现之后,旧的16位应用程序随时可能破坏掉长文件名。为此出现了所谓的命名隧道概念。现在假设一个16位的程序比如文字处理程序把当前版本的文挡维护在一个临时文件中。当用户修改这个文件,原始的文件就被删除了,临时文件被改为原来的文件名。
如果原始文件有一个长文件名,但是临时文件仅仅有短文件名,那么当旧的文件被删除,名字也跟着丢失了。因此当命名隧道起作用的时候,文件系统记住了每个被删除的文件名一段时间(比如15秒),如果一个短文件名的文件生成了,刚好和被记忆的长文件名配套,那么短文件名自动改名为长文件名。这就是命名隧道概念。
想自己尝试一下:首先,在一个空文件夹中生成一个文件 longfilename.然后删除它。在生成一个文件longfi~1,然后输入dir /x,这时你发现,longfilename又出现了!)
11.3 名字提供接口
如果一个过滤器打算提供一个途径来改变名字空间,他必须注册三个附加的回调给过滤管理器。这些回调允许过滤器作为名字的提供者。过滤器将有责任对上层发来的FltGetFilenameInformation或者FltGetDestinationFileName返回FLT_FILE_NAME_INFORMATION结构,并填写其中名字的内容。而且这样的微过滤器还可以告知过滤管理器,他们所返回的名字要不要被暂存。
作为一个名字提供者,过滤器必须可以返回一个指定的文件对象(file object)的开放格式的名字。如果确定了通常名字格式,过滤管理器将重新遍历这个名字的所有部分并调用名字提供者的徽调来展开这些部分为一个通常格式的文件名。在展开一个路径的所有部分的过程中,过滤器的名字通常化例程可能被调用不止一次。因此这个过程中允许过滤器传入一个上下文。所有的过程结束后,如果调用返回了,过滤器会被要求清理这个上下文。
当一个过滤器作为名字提供者必须提供一个名字的时候,它的PFLT_GENERATE_FILE_NAME例程会被调用。你能在参数中得到文件对象,过滤器的实例,同时回调数据描述发生的操作。此外还有名字请求选项。那么过滤器必须在这中间生成名字:
typedef NTSTATUS
(*PFLT_GENERATE_FILE_NAME) (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject,
IN PFLT_CALLBACK_DATA CallbackData OPTIONAL,
IN ULONG NameOptions,
OUT PBOOLEAN CacheFileNameInformation,
OUT PFLT_NAME_CONTROL FileName
);
typedef NTSTATUS
(*PFLT_NORMALIZE_NAME_COMPONENT) (
IN PFLT_INSTANCE Instance,
IN CONST PUNICODE_STRING ParentDirectory,
IN USHORT VolumeNameLength,
IN CONST PUNICODE_STRING Component,
IN OUT PFILE_NAMES_INFORMATION ExpandComponentName,
IN ULONG ExpandComponentNameLength,
IN OUT PVOID *NormalizationContext
);
typedef VOID
(*PFLT_NORMALIZE_CONTEXT_CLEANUP) (
IN PVOID *NormalizationContext
);
如果一个过滤器希望让所有自己提供的名字的暂存失效,他可以调用以下的接口。在此之前它可能已经提供了一些FLT_FILE_NAME_INFORMATION结构,而且其他过滤器可能刚好还在使用。那么这些FLT_FILE_NAME_INFORMATION结构只有当引用数降到0(已经没有人使用了),才会自己释放掉。
NTSTATUS
FltPurgeFileNameInformationCache (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject OPTIONAL
);
为了确保这样一个过滤可以卸载,过滤管理器有责任管理这些过滤器提供的文件名的暂存和释放。
一些FltGetFileNameInformation()调用FltGetDestinationFileName()调用基于一些过滤器提供的名字。过滤管理器将负责初始化这些FLT_FILE_NAME_INFORMATION结构。
为了提高效率,过滤管理器往名字提供者的PFLT_GENERATE_FILE_NAME回调传入一个缓冲区。这个缓冲是一个FLT_NAME_CONTROL所包装的UNICODE_STRING.这个结构中包含一些公有的或者私有的信息。在一个过滤器试图填充这个缓冲之前,应该先检查缓冲是否足够的大:
NTSTATUS
FltCheckAndGrowNameControl (
IN OUT PFLT_NAME_CONTROL NameCtrl,
IN USHORT NewSize
);
如果这个调用返回STATUS_SUCCESS,说明FLT_NAME_CONTROL结构足够容纳从名字提供者返回的名字了。
12.过滤器自产生I/O
某些微过滤器需要执行他们自己的I/O请求。在卷的微过滤器栈中,只有此过滤器以下的过滤器才能收到这些I/O请求。比如,一个防毒软件,可能希望在打开一个文件之前先读一下这个文件。在新的微过滤模式下,一个微过滤器有两种方法生成自己的请求:使用生成例程,类似现在的其他例程。此外就是使用旧的ZwXxx例程。
主要的I/O生成例程如下:
NTSTATUS
FLTAPI
FltAllocateCallbackData (
IN PFLT_INSTANCE Instance,
IN PFILE_OBJECT FileObject OPTIONAL,
OUT PFLT_CALLBACK_DATA *RetNewCallbackData
);
VOID
FLTAPI
FltPerformSynchronousIo (
IN PFLT_CALLBACK_DATA CallbackData
);
NTSTATUS
FLTAPI
FltPerformAsynchronousIo (
IN PFLT_CALLBACK_DATA CallbackData,
IN PFLT_COMPLETED_ASYNC_IO_CALLBACK CallbackRoutine,
IN PVOID CallbackContext
);
要使用这些例程,一个微过滤器可以首先调用FltAllocateCallbackData来分配一个 CallbackData.然后对应于不同的操作填写合适的参数。之后即可调用FltPerformSynchronousIo()或者是FltPerformAsynchronousIo()来实际发起I/O请求。参数Instance必须总是微过滤器发起此请求的实例。(译者注:不能由A实例分配而B实例来发起请求)。
此外过滤管理器导出了一些常用的实用例程,比如:
NTSTATUS
FLTAPI
FltCreateFile (
IN PFLT_FILTER Filter,
IN PFLT_INSTANCE Instance OPTIONAL,
OUT PHANDLE
FileHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN PLARGE_INTEGER AllocationSize OPTIONAL,
IN ULONG FileAttributes,
IN ULONG ShareAccess,
IN ULONG CreateDisposition,
IN ULONG CreateOptions,
IN PVOID EaBuffer OPTIONAL,
IN ULONG EaLength,
IN ULONG Flags
);
如果Instance参数被忽略(译者注:指定为空.),那么CREATE请求会被发送到微过滤器栈的顶端(这导致本微过滤器会回环的看到自己发送的请求。)除非绝对必要,这是不明智的。如果被滥用,很容易导致死锁和栈溢出.
如果指定了这个参数(应该总是你自己当前的instance),那么仅仅向下发起此请求。所有的前面的调用此api的微过滤器之上的微过滤器(包括自己)都不会收到此请求.
FileHandle参数返回一个供Zw*系列函数调用的文件句柄.如果先前指定的Instance不为空,那么保证关于这个文件句柄的其他未来的I/O操作(通过Zw接口,FltClose(),等),都只能被这个发起Instance下面的Instance看到。
FltReadFile()和FltWriteFile()发起的I/O读写请求也只有下层实例才能看见。用于我们只有FileObject而没有文件句柄的时候。这些例程类似以前旧的过滤模型中自己发送IRP的读写方式.
重要提示:过滤器不需要使用FltReadFile()/FltwriteFile()来对一个FltCreateFile返回的文件句柄发起一个I/O请求.对于这种情况,使用Zw*()接口就可以发送到合适的实例上去。
NTSTATUS
FLTAPI
FltReadFile (
IN PFLT_INSTANCE InitiatingInstance,
IN PFILE_OBJECT FileObject,
IN PLARGE_INTEGER ByteOffset OPTIONAL,
IN ULONG Length,
OUT PVOID Buffer,
IN FLT_IO_OPERATION_FLAGS Flags,
OUT PULONG BytesRead OPTIONAL,
IN PFLT_COMPLETED_ASYNC_IO_CALLBACK CallbackRoutine OPTIONAL,
IN PVOID CallbackContext OPTIONAL
);
NTSTATUS
FLTAPI
FltWriteFile (
IN PFLT_INSTANCE InitiatingInstance,
IN PFILE_OBJECT FileObject,
IN PLARGE_INTEGER ByteOffset OPTIONAL,
IN ULONG Length,
IN PVOID Buffer,
IN FLT_IO_OPERATION_FLAGS Flags,
OUT PULONG BytesWritten OPTIONAL,
IN PFLT_COMPLETED_ASYNC_IO_CALLBACK CallbackRoutine OPTIONAL,
IN PVOID CallbackContext OPTIONAL
);
微过滤器发出I/O可能为同步也可能为异步.当I/O被指定为异步,微过滤器提供一个回调例程.当系统完成了I/O请求回调例程会被调用.
13 卸载/注销/解除绑定的规则
解除绑定意味着一个微过滤实例将被销毁.这个微过滤器不在会因为任何对卷的操作而被调用(当然除非还有别的实例绑定在这个卷上).
卸载一个微过滤器意味着它的代码将不在存在于内存中。这往往在系统关闭的时候发生。或者是一个新版本的微过滤器在不关闭系统的情况下安装了(译者注:那么这时旧版本的微过滤器被卸载了。)
一个微过滤器可以自己把自己从一个卷解除绑定(调用FltDetachVolume),不过更通常的情况是在用户界面上解除的。一个微过滤器即使是有未完成的I/O请求的时候,依然可以解除绑定.此时,微过滤器的完成例程会被调用,而且所有的未完成的I/O操作都带有标记FLT_COMPLETION_DETACHED.当这些操作以后实际完成时,微过滤器就不会再收到这些完成回调了。
当一个微过滤器实例被解除绑定的时候,系统回调用这个微过滤器的所有的没有清除的上下文的释放例程,包括文件,流,流文件对象等和这个实例相关的对象。
14. 支持例程
除了已经讨论过的接口之外,过滤管理器提供了一组支持例程来帮助微过滤器完成他需要的工作。这里列出一部分附加的例程。请查阅过滤管理器IFS文档来获得这些例程的更多信息:
对象,名字转换:
FltGetFilterFromName()
FltGetVolumeFromName()
FltGetVolumeInstanceFromName()
卷,实例,设备对象转换历程:
FltGetVolumeFromInstance(), FltGetFilterFromInstance()
FltGetVolumeFromDeviceObject()
FltGetDeviceObject()
FltGetDiskDeviceObject()
获取对象信息例程:
FltGetVolumeProperties()
FltIsVolumeWriteable
FltQueryVolumeInformation(), FltSetVolumeInformation()
FltGetInstanceInformation()
FltGetFilterInformation()
枚举例程:
FltEnumerateFilters()
FltEnumerateVolumes()
FltEnumerateInstances()
FltEnumerateFilterInformation()
FltEnumerateInstanceInformationByFilter()
FltEnumerateInstanceInformationByVolume()
FltEnumerateVolumeInformation()
Oplock例程:
FltInitializeOplock()
FltUninitializeOplock()
FltOplockFsctrl()
FltCheckOplock()
FltOplockIsFastIoPossible()
FltCurrentBatchOplock()
目录改变通知例程:
(译者注:目录改变通知在文件系统中的意义是当目录改变后,通知操作系统,使操作系统可以在界面上做出一些改变,比如显示的目录结构变化等等,专门有一种IRP可以干这个事情。)
FltNotifyFilterChangeDirectory()
其他: FltGetRequestorProcess(), FltGetRequestorProcessId()
15.构建一个微过滤器
构建一个微过滤器应用所需要的所有的东西都可以在过滤管理器IFS开发包中找到。这包括:
1.用来构建一个微过滤器和一个使用了过滤管理器接口的用户态应用程序的完整的构建环境。
2.所有的在一台机器上安装过滤管理器组件用于开发的安装包。当然如果以后的windows版本内部包含了过滤管理器组件,那么就没有必要了。
3.微过滤器示例的代码。
所有的微过滤器包含头文件FltKernel.h,而且连接FltMgr.lib.而应用程序应该包含头文件FltUser.h,并连接库FltLib.lib。
订阅:
博文 (Atom)