{"id":110963,"date":"2025-03-13T07:00:00","date_gmt":"2025-03-13T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=110963"},"modified":"2025-03-13T16:07:06","modified_gmt":"2025-03-13T23:07:06","slug":"20250313-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20250313-00\/?p=110963","title":{"rendered":"Making sure that a DLL loads only from your application directory"},"content":{"rendered":"<p>A customer distributed a program and included its supporting DLLs in the same directory, because <a title=\"In Windows, the directory is the application bundle\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20110620-00\/?p=10393\"> the application directory is the application bundle<\/a>.<\/p>\n<p>They worried about the case that the user deletes one of the supporting DLLs, and then when the program tries to load that DLL, a rogue copy somewhere else on the <tt>PATH<\/tt> gets loaded instead. They want to reject loading the DLL from anywhere other than the application directory.<\/p>\n<p>You can accomplish this by explicitly calling <code>Load\u00adLibrary\u00adEx<\/code> with the <code>LOAD_<wbr \/>LIBRARY_<wbr \/>SEARCH_<wbr \/>APPLICATION_<wbr \/>DIR<\/code> flag, which says that the function should look only in the application directory for the DLL. If it&#8217;s not there, it gives up without searching any other directories. After you load the library, you can use <code>Get\u00adProc\u00adAddress<\/code> to get the functions.<\/p>\n<p>Unfortunately, this is rather cumbersome since you have to switch from implicit loading to explicit loading, so you don&#8217;t get the convenience of import libraries.<\/p>\n<p>You might think that you can get the convenience back by using <a title=\"How can I try to escape the disease-ridden hot-tubs known as the TEMP and Downloads directories?\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20230328-00\/?p=107978\"> the <tt>\/DEPENDENTLOADFLAG<\/tt> linker option<\/a> with the value <code>0x200<\/code> (the numeric value of <code>LOAD_<wbr \/>LIBRARY_<wbr \/>SEARCH_<wbr \/>APPLICATION_<wbr \/>DIR<\/code>), but the problem is that the dependent load flag applies to <i>all<\/i> DLLs loaded via import tables, and that includes <tt>kernel32<\/tt> and other DLLs you probably wanted to load from the <tt>system32<\/tt> directory.<\/p>\n<p>Now, the <tt>system32<\/tt> directory is writable only by administrators, so we could consider that a &#8220;safe&#8221; directory, because if somebody attacks that directory, they have already taken over the system. Therefore, you could use the <tt>\/DEPENDENTLOADFLAG<\/tt> linker option with the value <code>0xA00<\/code>, which is the numeric value of <code>LOAD_<wbr \/>LIBRARY_<wbr \/>SEARCH_<wbr \/>APPLICATION_<wbr \/>DIR |\nLOAD_<wbr \/>LIBRARY_<wbr \/>SEARCH_<wbr \/>SYSTEM32<\/code>. Alternatively, you could use the value <code>0x1000<\/code>, which is the numeric value of <code>LOAD_<wbr \/>LIBRARY_<wbr \/>SEARCH_<wbr \/>DEFAULT_<wbr \/>DIRS<\/code>, which includes the application directory, the <tt>system32<\/tt> directory, and any directories added by <code>Add\u00adDll\u00adDirectory<\/code> and <code>Set\u00adDll\u00adDirectory<\/code>.<\/p>\n<p>But wait, what is the issue we are trying to defend against? The stated scenario is &#8220;The user deletes a DLL from the application directory.&#8221; In that case, the user already has write permission into the application directory, so instead of deleting the DLL, they can just replace it with a malicious DLL. Restricting the load to the application directory does not prevent a malicious DLL from being loaded.<\/p>\n<p>But maybe your goal is not to create a security boundary but just to contain the scope of an error. If the user accidentally deletes the DLL from the application directory, at least prevent somebody else from injecting a DLL into the process by planting a DLL on the path.<\/p>\n<p>Now, the directories on the path fall into two categories. You have the directories on the global path, and the directories that are specific to a single user. If an attacker can plant a DLL into a directory on the global path, then that means that they have gained write permission onto the global path. To do this without administrator privileges requires that <a title=\"It rather involved being on the other side of this airtight hatchway: Planting files onto a custom PATH\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20200420-00\/?p=103685\"> the global path contain a directory writable by non-administrators<\/a>, which is an insecure configuration, so we are in the case of <a title=\"It rather involved being on the other side of this airtight hatchway: If they can inject code, then they can run code\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20100114-00\/?p=15273\"> creating an insecure system and then being surprised that it is insecure<\/a>. Instead of planting a rogue DLL on the path, the attacker could just plant, say, a rogue <code>notepad.exe<\/code>, and steal all your attempts to run notepad.<\/p>\n<p>The other case is that the directory under attack is a directory on the per-user path. The user chose to add that directory, and if they added a directory that is writable by non-administrators other than the current user, they have once again created an insecure system because they have granted non-administrators the ability to inject things into their path.<\/p>\n<p>The only attacks against rogue DLLs on the path assume that the system has already been compromised. So this issue is not about protecting a secure system but rather trying to protect from an already-compromised system.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>You can ask for it as an option, but think about what you&#8217;re actually protecting against.<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[25],"class_list":["post-110963","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>You can ask for it as an option, but think about what you&#8217;re actually protecting against.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/110963","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=110963"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/110963\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=110963"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=110963"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=110963"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}