Windows DLL 이름 확인 방식 - 검색 순서와 SxS

· 업데이트: · · Windows, DLL, 로더, 보안, Windows 개발

수정 이력(7건, 최종 수정 2026년 09월 03일)

이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.

permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
DLL 이름 확인의 선행 규칙, 검색 공간을 좁히는 API 사용법, 로드 원본 확인 절차를 그림으로도 따라갈 수 있도록 Mermaid 그림을 23점 추가했습니다(본문 500〜750자당 1그림 규약에 맞춘 것입니다). 기존 선행 규칙 평가 그림에도 캡션을 달고, 그림 번호를 연속으로 다시 매겼습니다. 본문 문장은 바꾸지 않았습니다.
글 맨 앞에 「이 글의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
외부 리뷰(1283건)에 대한 대응으로 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참조하십시오.
C# 플러그인 로드 예에서, 전용 폴더에 둔 DLL을 `ApplicationDirectory`와 `System32` 검색 플래그로 집으려 하던 것을 고쳤습니다. 전자는 exe가 있는 폴더, 후자는 OS 폴더를 가리키므로 플러그인 폴더는 보지 않습니다. 전체 경로로 지정하는 형태로 바꾸고, `DllImportSearchPath`에는 임의의 폴더를 가리키는 값이 없다는 점과 의존 DLL까지는 고정되지 않는다는 점을 보완했습니다.
최소 코드 예를 C/C++와 C#으로 추가했습니다(`SetDefaultDllDirectories`부터 `RemoveDllDirectory`까지를 이어서 다루고, C#은 특성 선언뿐 아니라 실제 호출까지 보여 줍니다). 어느 DLL이 어디서 읽혔는지를 확인하는 장을 새로 만들고, 맨 앞에 용어 표, 검색 순서를 12행 표로 정리했습니다.
일본어 기사인데 영어로 남아 있던 「관련 글」「참고 링크」 제목을 일본어로 고치고, 관련 글 링크 문구를 링크 대상의 현재 제목에 맞췄습니다.
최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635216)

이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.

Go Komura (2026). 「Windows DLL 이름 확인 방식 - 검색 순서와 SxS」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635216 https://comcomponent.com/ko/blog/windows-dll-name-resolution/

DOI(최신 버전)
10.5281/zenodo.21635216
DOI(이 버전)
10.5281/zenodo.21635217

Windows에서 네이티브 DLL을 다루다 보면, 꽤 자주 이런 혼란이 생깁니다.

  • LoadLibrary("foo.dll")라고 썼을 때 실제로 어디를 보러 가는가
  • 실행 파일과 같은 폴더에 두었는데 왜 다른 DLL이 로드되는가
  • System32가 우선인가, 앱 폴더가 우선인가
  • manifest나 API set, Known DLLs는 어느 단계에서 적용되는가
  • SetDllDirectoryAddDllDirectory를 쓰면 무엇이 달라지는가
  • DLL planting 공격이나 DLL hijacking은 무엇을 하면 일어나기 쉬운가

이 주제는 검색 순서를 한 줄로 외우는 것만으로는 실무에 도움이 되지 않습니다. 실제로는 Windows 로더는 파일 시스템을 순서대로 훑기 전에, 몇 가지 특별 규칙을 먼저 평가합니다.

검색 순서 암기만으로는 부족한 이유DLL 이름 확인은 검색 순서를 한 줄로 외우는 것만으로는 실무에 도움이 되지 않으며, Windows 로더는 파일 시스템을 순서대로 찾기 전에 몇 가지 특별 규칙을 먼저 평가한다는 것을 나타내는 그림.검색 순서를 한 줄로 외운다실무에서는 도움이 되지 않는다Windows 로더의 실제먼저 특별 규칙을 평가한다그다음에 파일 시스템을 순서대로 찾는다

그림 1: 이름 확인은 폴더 탐색 전에, 선행하는 특별 규칙부터 시작합니다.

이 글에서는 Windows의 DLL 이름 확인을 unpackaged app과 packaged app의 차이, Known DLLs, loaded-module list, API set, side-by-side manifest, LoadLibraryEx 계열 API의 영향까지 포함해 실무용으로 정리합니다. 내용은 2026년 3월 시점의 Microsoft Learn 공개 정보를 전제로 합니다.123456789

이 글에서 쓰는 용어

이후 설명 없이 나오는 말을 한 줄씩 먼저 정리합니다. 자세한 내용은 본문 각 장에서 다룹니다.

용어 한 줄로
packaged app / unpackaged app MSIX 같은 패키지로 배포·설치되는 앱인지, 예전처럼 폴더에 실행 파일을 두는 앱인지. 검색 순서의 정의 자체가 다릅니다 (3장·4장)
safe DLL search mode 기본으로 켜진 설정이며, current folder를 검색 순서 뒤로 옮깁니다. 레지스트리 값 SafeDllSearchMode를 0으로 하면 꺼집니다1
DLL redirection 실행 파일과 같은 위치에 앱이름.exe.local이라는 표시를 두면, 로더가 실행 파일 폴더를 먼저 보게 되는 구조. .local, DotLocal이라고도 합니다 (7장)7
SxS (side-by-side) 같은 DLL의 여러 버전을 공존시키고, 어느 버전에 묶을지를 manifest에 적어 지정하는 구조. side-by-side의 약어이며, 나란히 두는 방식이라고 설명하기도 합니다 (7장)9
loaded-module list 같은 모듈 이름의 DLL이 이미 그 프로세스 메모리에 로드되어 있는지를 시스템이 확인하는 구조. 어느 폴더에서 읽혔는지와 관계없이, 이미 로드되어 있으면 그것이 쓰입니다 (5.1)1
Known DLLs Windows가 그 버전에서 알려진 것으로 보는 DLL 목록. 해당하는 DLL은 시스템 쪽 복사본이 쓰입니다 (5.2)1
API set api-ms-win-... 같은 계약 이름. 구현하는 물리 DLL을 숨기는 가상 alias입니다 (6장)3
package dependency graph 앱 패키지 본체와, 패키지 매니페스트의 Dependencies 섹션에 PackageDependency로 적힌 의존 패키지의 집합. 매니페스트에 적힌 순서대로 검색됩니다1

1. 먼저 결론

실무용 결론만 먼저 나열합니다.

  • Windows의 DLL 이름 확인은 먼저 파일 시스템을 검색하는 것이 아닙니다. DLL redirection, API set, SxS manifest, loaded-module list, Known DLLs 같은 요소가 검색 순서의 앞에 들어갑니다.1
  • unpackaged app에서 safe DLL search mode가 켜진 표준 형태에서는 애플리케이션 폴더가 위쪽에 있지만, 그 앞에 위의 특별 규칙이 평가됩니다.1
  • DLL을 전체 경로로 로드해도 그 DLL의 의존 DLL까지 자동으로 같은 전체 경로에 고정되지는 않습니다. 의존 DLL은 모듈 이름만으로 검색되는 취급이라, 다른 위치에서 확인될 수 있습니다.1
  • Known DLLs는 OS가 알려진 특정 DLL을 시스템 쪽 복사본에 묶는 구조이며, 보통의 앱 쪽 배치로 덮어쓰는 이야기가 아닙니다.1
  • API set은 실제 DLL 이름 그 자체가 아니라 구현 DLL을 숨기는 가상 alias입니다. api-ms-win-... 같은 이름을 보고 일반 DLL 검색과 같은 감각으로 생각하면 오해하기 쉽습니다.3
  • SetDllDirectory는 검색 순서를 바꿀 뿐 아니라 safe DLL search mode를 사실상 끄는 동작이 있어, 가볍게 쓰면 보안 면에서 역효과가 날 수 있습니다.1
  • 실무에서는 전체 경로 지정, SetDefaultDllDirectories, AddDllDirectory, LoadLibraryExLOAD_LIBRARY_SEARCH_* 플래그를 조합해 검색 범위를 명시적으로 좁히는 편이 안전합니다.4562

정리하면, Windows의 DLL 이름 확인은 어느 폴더가 몇 번째인가만이 아니라, 이름을 무엇으로 확인할지에 대한 선행 규칙이 있는가와 API로 검색 공간을 어떻게 바꿨는가로 정해진다고 보는 것이 실무적입니다.

DLL 이름 확인을 정하는 세 요소Windows의 DLL 이름 확인은 어느 폴더가 몇 번째인가라는 폴더 순서만이 아니라, 이름을 무엇으로 확인할지라는 선행 규칙과 API로 검색 공간을 어떻게 바꿨는지라는 세 요소의 겹침으로 정해진다는 것을 나타내는 그림.이름의 선행 규칙실제로 로드되는 DLL폴더의 검색 순서API로 바꾼 검색 공간redirection·Known DLLsLoadLibraryEx의 플래그

그림 2: 확인 결과는 선행 규칙·폴더 순서·API 세 겹으로 정해집니다.

이 글의 지식 맵

이 글은 Windows의 DLL 이름 해석이 단순한 폴더 탐색 순서가 아니라, DLL redirection·API set·SxS 매니페스트·loaded-module list·Known DLLs라는 앞단계 규칙이 파일 시스템 탐색보다 먼저 평가되는 구조임을 정리합니다. packaged app과 unpackaged app에서는 검색 순서의 정의 자체가 다르며, current folder의 위치는 safe DLL search mode의 사용 여부에 따라 달라집니다. SetDllDirectory는 safe DLL search mode를 사실상 끄기 때문에, SetDefaultDllDirectories와 LoadLibraryEx의 검색 플래그, AddDllDirectory를 함께 써서 검색 공간을 좁히는 것이 DLL 하이재킹 완화로 이어집니다. 전체 경로를 지정해도 의존 DLL까지는 자동으로 고정되지 않으며, 실제 로드 위치는 Process Monitor나 ListDLLs로 확인합니다.

Windows DLL 이름 해석의 지식 맵DLL redirection·API set·SxS 매니페스트·loaded-module list·Known DLLs·package dependency graph가 파일 시스템 탐색보다 앞단계에서 평가된다는 점, packaged app과 unpackaged app에서 검색 순서의 정의 자체가 다르다는 점, SetDllDirectory가 safe DLL search mode를 약화하는 한편 SetDefaultDllDirectories와 LoadLibraryEx의 검색 플래그가 검색 공간을 좁혀 DLL 하이재킹을 완화한다는 점, 실제 로드 위치를 Process Monitor 등으로 확인하는 수단의 관계를 보여주는 그림이용한다이용한다이용한다이용한다이용한다이용한다에서 구성할 수 있다양립하지 않는다이용한다양립하지 않는다완화한다전제로 한다이용한다사용은 비권장권장되는 대응완화한다원인이 될 수 있다완화한다사용은 비권장이용한다에서 확인할 수 있다에서 확인할 수 있다에서 확인할 수 있다DLL 검색 순서DLL 하이재킹(DLL preloading 공격)DLL 리디렉션(.local)API setSxS 매니페스트 리디렉션loaded-module listKnown DLLspackage dependency graphcurrent folder 검색safe DLL search modepackaged app패키지되지 않은 앱(unpackaged app)SetDllDirectorySetDefaultDllDirectoriesAddDllDirectoryLoadLibraryExDLL 검색 공간 축소경로 없는 이름에 의한 LoadLibrary전체 경로 지정 DLL 로드종속 DLL 해석DllImportSearchPath(.NET)Process Monitor(procmon.exe)ListDLLstasklist /m

그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 23건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle

2. DLL 이름 확인은 폴더 탐색 앞에 선행 규칙이 있다

Microsoft Learn의 DLL search order 설명에서는, DLL을 로드할 때 먼저 다음 요소가 검색 순서의 일부로 다뤄집니다.1

  1. DLL redirection
  2. API sets
  3. SxS manifest redirection
  4. loaded-module list
  5. Known DLLs

그다음에 app folder, System32, Windows 폴더, PATH 같은 파일 시스템 탐색으로 들어갑니다.1

여기를 놓치면, 예를 들어 앱 폴더보다 먼저 무언가가 정해지는 것이 이상하다고 느끼지만, Windows 로더 설명으로는 오히려 그쪽이 본류입니다.

DLL 이름을 확인하고 싶다DLL redirectionAPI setSxS manifest redirectionloaded-module listKnown DLLs파일 시스템상의 검색 순서실제로 로드되는 DLL이 정해진다

그림 3: 선행 규칙이 순서대로 평가되고, 어느 것에도 해당하지 않을 때만 파일 시스템 검색으로 들어갑니다.

3. unpackaged app의 표준 검색 순서

이른바 평범한 데스크톱 앱에서 DLL을 전체 경로 없이 로드하는 경우, Microsoft Learn에는 unpackaged app의 표준 검색 순서가 설명되어 있습니다. safe DLL search mode가 켜진 기본 상태에서는 다음 순서입니다.1

# 찾는 위치 종류 보충
1 DLL redirection 선행 규칙 앱이름.exe.local이 있는지 (7장)
2 API sets 선행 규칙 계약 이름에서 구현 DLL로 (6장)
3 SxS manifest redirection 선행 규칙 manifest에 의한 binding (7장)
4 loaded-module list 선행 규칙 같은 이름 모듈이 이미 로드되어 있는지 (5.1)
5 Known DLLs 선행 규칙 알려진 DLL이면 시스템 쪽 복사본 (5.2)
6 package dependency graph 선행 규칙 Windows 11 버전 21H2 이후. 매니페스트에 적힌 순서대로 검색됩니다
7 애플리케이션이 로드된 폴더 파일 시스템 여기부터가 실제 폴더 탐색입니다
8 시스템 폴더 파일 시스템 보통은 %SystemRoot%\System32. GetSystemDirectory로 얻을 수 있는 위치입니다
9 16-bit system folder 파일 시스템 16비트 시절의 System 폴더. 경로를 얻는 함수는 없지만 검색은 됩니다. 요즘 앱에서 의식할 일은 거의 없고, 순서 항목으로는 남아 있다고만 알면 충분합니다
10 Windows 폴더 파일 시스템 GetWindowsDirectory로 얻을 수 있는 위치입니다
11 current folder 파일 시스템 safe DLL search mode가 꺼져 있으면 여기가 8번 위치까지 올라갑니다
12 PATH에 나열된 디렉터리 파일 시스템 App Paths 레지스트리 키의 앱별 경로는 포함되지 않습니다

이 표는 암기용이 아닙니다. 자기 사례가 어느 단계에서 정해지는지를 맞히기 위한 표입니다. 1~6에서 정해진 문제를 7 이후의 폴더 배치로 고치려 하면 무엇을 해도 달라지지 않습니다.

검색 순서 표의 올바른 쓰임검색 순서 표는 암기용이 아니라 자기 사례가 어느 단계에서 정해지는지를 맞히기 위한 것이며, 선행 규칙에서 정해진 문제를 폴더 배치로 고치려 해도 아무것도 달라지지 않는다는 것을 나타내는 그림.선행 규칙(1~6)파일 시스템(7~12)증상: 의도하지 않은 DLL이 읽힌다어느 단계에서 정해졌는가폴더 배치를 바꿔도 아무것도 달라지지 않는다배치나 경로 정리가 통하는 영역

그림 4: 고치기 전에, 그 문제가 어느 단계에서 정해졌는지를 맞힙니다.

실무에서 특히 중요한 것은 다음 세 가지입니다.

  • current folder는 기본에서는 꽤 뒤쪽입니다. safe DLL search mode 때문에 current folder를 앞으로 빼기 어렵게 되어 있습니다.1
  • 다만 뒤에 있다고 안전하다는 뜻은 아닙니다. 공격자가 통제할 수 있는 디렉터리가 검색 대상에 남아 있는 시점에서, DLL preloading의 여지는 남습니다.2
  • Windows 11 21H2 이후에는 unpackaged app의 검색 설명에도 package dependency graph가 들어갑니다. 오래된 설명만 기억하고 있으면 놓치기 쉬운 차이입니다.1
current folder의 위치와 안전성safe DLL search mode가 켜진 기본에서는 current folder가 검색 순서에서 꽤 뒤로 밀리지만, 뒤에 있다고 안전하다는 뜻은 아니며, 공격자가 통제할 수 있는 디렉터리가 검색 대상에 남아 있는 시점에서 DLL preloading의 여지는 남는다는 것을 나타내는 그림.safe DLL search mode가 켜져 있다(기본)current folder는 꽤 뒤로 밀린다뒤에 있다고 안전하다는 뜻은 아니다공격자가 통제할 수 있는 곳이 검색 대상에 남으면 여지는 남는다

그림 5: current folder의 위치는 내려가 있지만, 그것만으로는 방어가 되지 않습니다.

4. packaged app과 unpackaged app은 같지 않다

Microsoft Learn에는 packaged app에 대해 별도의 검색 순서가 정의되어 있습니다. packaged app에서는 package dependency graph가 더 앞에서 적용되고, 검색의 사고방식 자체가 조금 다릅니다.1

이 차이를 놓치면 MSIX화나 Windows App SDK 도입 후에 이런 혼란이 생깁니다.

  • 개발 중의 unpackaged 실행에서는 찾아지는 DLL이, 운영 package에서는 찾아지지 않는다
  • package manifest에 의한 의존 관계와 예전 방식의 PATH 의존이 섞여 재현 조건이 바뀐다
  • Windows의 DLL 검색 순서는 이렇다고 단일 표만으로 설명해 버려, packaged app의 동작 차이를 빠뜨린다

글이나 설계 리뷰에서는 packaged app 이야기인지, unpackaged app 이야기인지를 먼저 나누는 편이 안전합니다.1

packaged와 unpackaged를 먼저 나눈다packaged app에는 별도의 검색 순서가 정의되어 있고 package dependency graph가 더 앞에서 적용되므로, 개발 중의 unpackaged 실행에서는 찾아지는 DLL이 운영 package에서는 찾아지지 않는 혼란이 생기며, 설계 리뷰에서는 어느 쪽 이야기인지를 먼저 나누는 편이 안전하다는 것을 나타내는 그림.unpackaged apppackaged app어느 쪽 앱 이야기인가3장의 표준 검색 순서별도의 검색 순서 (dependency graph가 앞)MSIX화 후에 개발 때와 동작이 달라지는 혼란의 원인

그림 6: 검색 순서는 표 한 장이 아니라, packaged인지 unpackaged인지로 먼저 갈립니다.

5. Known DLLs와 loaded-module list는 무엇을 하는가

DLL 확인에서 직관에 어긋나기 쉬운 것이 loaded-module listKnown DLLs입니다.

5.1 loaded-module list

Microsoft Learn에는 같은 모듈 이름의 DLL이 이미 메모리에 로드되어 있는지를 시스템이 확인할 수 있다고 설명되어 있습니다.1

즉 파일 시스템 검색 전에,

  • 그 DLL 이름은 이미 로드되어 있지 않은가
  • 그 결과, 지금부터 찾으러 갈 필요 자체가 있는가

라는 판정이 들어갑니다.

그래서 조사 중에 이 프로세스에서는 다른 폴더의 같은 이름 DLL이 먼저 로드되어 있었다는 사실을 빠뜨리면, 재현 조건을 잘못 읽습니다.

loaded-module list의 판정파일 시스템 검색 전에 같은 모듈 이름의 DLL이 이미 그 프로세스 메모리에 로드되어 있는지가 확인되고, 로드되어 있으면 어느 폴더에서 읽혔는지와 관계없이 그것이 쓰이므로 찾으러 갈 필요 자체가 없어진다는 것을 나타내는 그림.로드됨미로드DLL 이름 확인 요청같은 이름 모듈이 로드되어 있는가그 모듈을 쓴다 (폴더는 관계없다)이어지는 검색으로 진행다른 폴더의 같은 이름 DLL이 먼저 읽혀 있으면 길을 잃는다

그림 7: 이미 로드된 같은 이름 모듈이 있으면, 새로 찾으러 가지 않습니다.

5.2 Known DLLs

Known DLLs는 Windows가 그 버전에서 알려진 것으로 보는 DLL 목록이며, HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\KnownDLLs에서 확인할 수 있습니다. 해당하는 DLL이면 시스템은 그 알려진 DLL의 복사본을 씁니다.1

여기서 중요한 것은 Known DLLs는 일반 앱이 같은 이름 DLL을 앱 폴더에 두면 이긴다는 종류의 이야기가 아니다라는 점입니다. System32와의 단순한 선착 다툼으로 이해하면 동작을 오해합니다.

Known DLLs의 역할Known DLLs는 Windows가 그 버전에서 알려진 것으로 보는 DLL 목록이며, 해당하는 DLL이면 시스템 쪽 복사본이 쓰이므로, 일반 앱이 같은 이름 DLL을 앱 폴더에 두면 이긴다는 종류의 이야기가 아니고 System32와의 단순한 선착 다툼으로 이해하면 오해한다는 것을 나타내는 그림.해당해당하지 않음확인하고 싶은 DLL 이름Known DLLs에 해당하는가시스템 쪽 복사본이 쓰인다이어지는 검색으로 진행앱 폴더 배치로 덮어쓸 수 있는 이야기가 아니다

그림 8: 알려진 DLL은 시스템 쪽 복사본에 묶이며, 선착 다툼이 되지 않습니다.

6. API set은 실제 DLL 이름이 아니라 계약 이름

api-ms-win-core-... 같은 이름을 보면, 그 DLL 파일을 어디서 찾는지로 생각하기 쉽습니다. 그러나 Microsoft Learn에서는 API set을 물리 DLL에 대한 가상 alias이며, 구현과 계약을 분리하는 구조라고 설명합니다.3

즉,

  • API set 이름 = 그대로 물리 DLL 파일 이름
  • API set의 확인 = 일반 DLL과 같은 파일 탐색

이라고 생각하는 것은 부정확합니다.

API set의 사고방식을 넣어 두면,

  • Windows 버전이나 디바이스 종류에 따라 구현 DLL 이름이 달라도 맞춰진다
  • 호출 측은 어느 호스트 DLL이 구현하는지를 고정으로 알 필요가 없다

는 설명이 쉬워집니다.3

API set은 계약 이름api-ms-win- 같은 이름은 물리 DLL 파일 이름이 아니라 구현 DLL을 숨기는 가상 alias이며, 계약과 구현을 분리함으로써 Windows 버전이나 디바이스 종류에 따라 구현 DLL 이름이 달라도 맞춰지고, 호출 측은 어느 호스트 DLL이 구현하는지를 고정으로 알 필요가 없다는 것을 나타내는 그림.api-ms-win-…라는 계약 이름가상 alias로 확인된다구현하는 물리 DLL은 숨겨져 있다버전이나 디바이스에 따라 구현이 달라도 맞춰진다일반 파일 탐색과 같은 감각은 부정확하다

그림 9: API set은 찾을 파일 이름이 아니라, 구현을 숨기는 계약 이름입니다.

7. manifest와 side-by-side (SxS)는 DLL versioning 문제에 대한 다른 해법

DLL redirection이나 SxS manifest는 단순한 검색 순서의 잔기술이 아니라, DLL versioning 충돌을 피하기 위한 구조로 설명됩니다.789

Microsoft Learn의 정리는 다음과 같습니다.

  • manifest는 side-by-side assembly나 isolated application을 기술하는 XML
  • side-by-side assembly는 이름 붙이기, binding, versioning, deployment의 단위
  • manifest에 적힌 의존 관계로, 어느 버전에 bind할지를 로더가 판단한다

89

manifest와 side-by-side의 관계manifest는 side-by-side assembly나 isolated application을 기술하는 XML이고, side-by-side assembly는 이름 붙이기·binding·versioning·deployment의 단위이며, manifest에 적힌 의존 관계로 어느 버전에 bind할지를 로더가 판단한다는, DLL versioning 충돌을 피하는 구조임을 나타내는 그림.manifest(XML)의존하는 side-by-side assembly와 버전을 기술로더가 어느 버전에 bind할지 판단같은 DLL의 여러 버전을 공존시킬 수 있다

그림 10: SxS는 검색 순서의 잔기술이 아니라, versioning 충돌에 대한 구조로서의 다른 해법입니다.

그래서 실무에서는 다음 셋을 나눠 생각해야 합니다.

  • 단지 private DLL을 app folder에 두는 이야기
  • .local 같은 DLL redirection을 쓰는 이야기
  • manifest에 의한 side-by-side binding을 쓰는 이야기

모두 DLL 확인에 영향을 준다는 점에서는 가깝지만, 설계 의도는 같지 않습니다. Microsoft Learn은 기존 앱을 손대지 않고 해결하려면 DLL redirection, 새로 만드는 앱이면 side-by-side 컴포넌트라는 쓰임새를 안내합니다.7

세 수단의 쓰임새private DLL을 app folder에 두는 이야기, .local 같은 DLL redirection을 쓰는 이야기, manifest에 의한 side-by-side binding을 쓰는 이야기는 모두 DLL 확인에 영향을 주지만 설계 의도는 같지 않으며, 기존 앱을 손대지 않고 해결하려면 DLL redirection, 새로 만드는 앱이면 side-by-side라는 쓰임새가 안내된다는 것을 나타내는 그림.어느 수단의 이야기인가private DLL을 app folder에 둔다DLL redirection (.local)SxS manifest binding기존 앱을 손대지 않고 고치고 싶을 때새로 만드는 앱의 의존 관리

그림 11: 비슷해 보이는 세 수단은 설계 의도로 나눠 씁니다.

7.1 .local은 구체적으로 무엇을 하는가

한 줄로 흘리기 쉬우므로, unpackaged app에서의 동작을 나눠 적습니다.7

  • 두는 것: 리디렉션 파일 이름은 실행파일이름.local입니다. Editor.exe라면 Editor.exe.local을 실행 파일과 같은 폴더에 둡니다. 로드시키고 싶은 DLL도 같은 폴더에 둡니다
  • 내용: 파일 내용은 무시됩니다. 존재 자체가, DLL을 로드할 때 실행 파일 폴더를 먼저 보게 하는 신호가 됩니다
  • 적용 범위: 전체 경로 지정 로드에도, 모듈 이름만의 로드에도 적용됩니다. LoadLibraryLoadLibraryEx에 넘긴 경로와 관계없이, 실행 파일 폴더에 같은 이름 DLL이 있으면 그쪽이 읽힙니다. COM처럼 등록처가 하나뿐인 장면을 구하기 위한 사양입니다
  • 없을 때: 실행 파일 폴더에 없으면 보통의 검색 순서로 돌아갑니다
  • 폴더 형식: Editor.exe.local이라는 폴더를 만들고 그 안에 DLL을 두는 형태로도 동작합니다
  • 머신 전체에서 켜기: HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution OptionsDevOverrideEnable이라는 DWORD 값을 만들어 1로 두고 재시작합니다. 이렇게 설정하면 앱이 application manifest를 가지고 있어도 .local에 의한 리디렉션이 적용됩니다
  • packaged app의 경우: 두는 위치가 바뀌고, <패키지 설치 위치>\microsoft.system.package.metadata\application.local\을 보러 갑니다

여기서 하나, 빠뜨리면 조사가 길을 잃는 부작용이 있습니다. DLL redirection을 쓰고 있고, 앱이 검색 순서상의 드라이브나 디렉터리 전부에 접근하지 못하는 경우, LoadLibrary는 접근이 거부된 시점에서 검색을 중단합니다. DLL redirection을 쓰지 않으면, 접근할 수 없는 디렉터리는 건너뛰고 검색이 이어집니다.7 .local을 둔 환경에서만 그 너머에 있어야 할 DLL을 찾지 못한다고 하면, 이 차이를 의심하십시오.

.local의 동작실행파일이름.local이라는 표시가 실행 파일과 같은 폴더에 있으면 내용과 관계없이 존재 자체가 신호가 되어, 전체 경로 지정 로드에도 적용되어 실행 파일 폴더가 먼저 보이고, 없으면 보통의 검색 순서로 돌아가며, 접근 거부로 검색이 중단되는 부작용이 있다는 것을 나타내는 그림.있다없다실행파일이름.local을 둔다실행 파일 쪽을 먼저 본다같은 이름 DLL이 있는가전체 경로여도 그쪽을 읽는다보통의 검색 순서로 돌아간다부작용: 거부로 검색 중단

그림 12: .local은 존재 자체가 신호이며, 전체 경로 지정 로드에까지 적용됩니다.

8. LoadLibraryEx, SetDllDirectory, AddDllDirectory로 무엇이 달라지는가

8.1 SetDllDirectory

SetDllDirectory는 검색 순서를 바꾸지만, Microsoft Learn에는 safe DLL search mode를 사실상 끈다고 명시되어 있습니다.1

즉 앱 전용 폴더를 하나 더하고 싶었을 뿐인데, 결과적으로 current folder 취급을 포함한 검색 공간이 바뀝니다.

게다가 부모 프로세스에서 SetDllDirectory를 호출하면, 그 영향이 자식 프로세스 쪽 표준 검색 순서에도 갈 수 있습니다.1

그래서 실무에서는 SetDllDirectory를 대충 상시 쓰기보다,

  • SetDefaultDllDirectories
  • AddDllDirectory
  • LoadLibraryExLOAD_LIBRARY_SEARCH_*

쪽으로 맞추는 편이 안전합니다.456

SetDllDirectory의 함정SetDllDirectory는 앱 전용 폴더를 하나 더하고 싶었을 뿐인데 safe DLL search mode를 사실상 끄고 검색 공간 전체를 바꾸며, 부모 프로세스에서 호출하면 자식 프로세스 쪽 검색 순서에도 영향이 갈 수 있으므로 SetDefaultDllDirectories나 AddDllDirectory, LoadLibraryEx의 검색 플래그로 맞추는 편이 안전하다는 것을 나타내는 그림.SetDllDirectory로 폴더를 하나 더한다safe DLL search mode가 사실상 꺼진다current folder를 포함한 검색 공간 전체가 바뀐다자식 프로세스의 검색 순서에 영향이 갈 수 있다대신 SetDefaultDllDirectories 계열 API로 맞춘다

그림 13: 폴더 하나만 더하려는 의도가, 검색 공간 전체를 약화시킵니다.

8.2 AddDllDirectory

AddDllDirectory로 추가한 경로는 LOAD_LIBRARY_SEARCH_USER_DIRS와 조합해 씁니다. Microsoft Learn에서는 여러 개를 추가했을 때의 검색 순서가 미규정입니다.15

그래서,

  • 디렉터리를 여러 개 추가했다
  • 그 탐색 순서까지 엄밀히 기대했다

는 설계는 피하는 편이 좋습니다.

AddDllDirectory의 쓰임과 주의AddDllDirectory로 추가한 경로는 LOAD_LIBRARY_SEARCH_USER_DIRS와 조합해 쓰는 것이며, 여러 개를 추가했을 때의 검색 순서는 미규정이므로 여러 디렉터리를 추가하고 그 탐색 순서까지 엄밀히 기대하는 설계는 피해야 한다는 것을 나타내는 그림.AddDllDirectory로 경로를 추가LOAD_LIBRARY_SEARCH_ USER_DIRS와 세트로 적용여러 개 추가 시의 순서는 미규정탐색 순서에 의존하는 설계는 피한다

그림 14: 추가한 사용자 디렉터리 사이의 순서는 사양상 믿을 수 없습니다.

8.3 SetDefaultDllDirectories

SetDefaultDllDirectories는 표준 DLL search path에서 취약해지기 쉬운 디렉터리를 빼고, 검색 대상을 한정하기 위한 API로 설명됩니다.4

특히 잡아 두고 싶은 성질은 다음 셋입니다.

  • 프로세스 단위로 적용된다
  • 호출 후에는 프로세스 수명 동안 계속된다
  • 한 번 설정한 표준 검색 경로를, 그대로 원래 표준 형태로 되돌릴 수는 없다

보안 면을 생각한다면, 시작 직후에 안전한 쪽 검색 공간으로 맞추는 설계를 취하기 쉬운 API입니다.4

SetDefaultDllDirectories의 성질SetDefaultDllDirectories는 표준 DLL 검색 경로에서 취약해지기 쉬운 디렉터리를 빼고 검색 대상을 한정하는 API이며, 프로세스 단위로 적용되고 호출 후에는 프로세스 수명 동안 계속되며 한 번 설정한 검색 경로를 원래 표준 형태로 되돌릴 수는 없으므로 시작 직후에 안전한 쪽 검색 공간으로 맞추는 설계를 취하기 쉽다는 것을 나타내는 그림.시작 직후에 SetDefaultDllDirectories취약해지기 쉬운 곳을 검색 대상에서 뺀다프로세스 단위로 수명 동안 계속 적용원래 표준 형태로 되돌릴 수는 없다

그림 15: 시작 직후에 한 번 호출해, 프로세스 전체를 안전한 쪽으로 맞추기 위한 API입니다.

8.4 LoadLibraryEx

LoadLibraryExLOAD_WITH_ALTERED_SEARCH_PATHLOAD_LIBRARY_SEARCH_* 플래그로 검색 동작을 바꿀 수 있습니다.61

실무적으로는,

  • 의존 DLL을 포함해, 로드 원본 DLL의 폴더도 탐색 대상에 넣고 싶다
  • 앱 폴더, System32, 명시적으로 추가한 사용자 디렉터리만으로 좁히고 싶다

같은 요구에 맞추기 쉬운 API입니다.

쓰기 전에 잡아 두고 싶은 제약은 네 가지입니다.6

제약 내용
두 번째 인수 hFile은 미래를 위한 예약이며, 반드시 NULL을 넘깁니다
병용 불가 LOAD_WITH_ALTERED_SEARCH_PATH어느 LOAD_LIBRARY_SEARCH_* 플래그와도 조합할 수 없습니다
전체 경로 필수 LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR을 쓰려면 첫 번째 인수는 전체 경로여야 합니다
여러 개 지정 시 순서 LOAD_LIBRARY_SEARCH_DLL_LOAD_DIRLOAD_LIBRARY_SEARCH_APPLICATION_DIRLOAD_LIBRARY_SEARCH_USER_DIRSLOAD_LIBRARY_SEARCH_SYSTEM32 순으로 찾습니다. 다만 USER_DIRS 안에서의 순서는 미규정입니다

LOAD_LIBRARY_SEARCH_* 플래그를 하나라도 지정하면 표준 검색 경로는 전혀 쓰이지 않습니다. 즉 LOAD_LIBRARY_SEARCH_SYSTEM32만 넘기면 앱 폴더는 찾지 않습니다. 좁힌다는 것은 그런 뜻입니다.

검색 플래그를 지정하면 표준 경로는 쓰이지 않는다LOAD_LIBRARY_SEARCH_ 계열 플래그를 하나라도 지정하면 표준 검색 경로는 전혀 쓰이지 않으며, 예를 들어 LOAD_LIBRARY_SEARCH_SYSTEM32만 넘기면 앱 폴더는 찾지 않는다는, 좁힌다는 말의 뜻을 나타내는 그림.지정하지 않음하나라도 지정LOAD_LIBRARY_SEARCH_계열 플래그를 지정했는가표준 검색 경로가 쓰인다표준 검색 경로는 전혀 쓰이지 않는다지정한 플래그의 범위만 탐색된다SYSTEM32만이면 앱 폴더는 찾지 않는다

그림 16: 플래그는 더하는 것이 아니라, 그 범위만으로 바꾸는 지정입니다.

8.5 최소 코드 예 (C/C++)

여기까지의 API를 하나로 모으면 다음 형태가 됩니다. SetDefaultDllDirectoriesLOAD_LIBRARY_SEARCH_*Windows 8 이후 API이므로, 헤더의 대상 버전을 명시해야 합니다.46

/* cl /W4 loader.c  (Visual Studio 2022 + Windows SDK 10)
 * SetDefaultDllDirectories / AddDllDirectory / LOAD_LIBRARY_SEARCH_* 는
 * Windows 8 이후. Windows 7도 대상으로 하려면 KB2533623이 전제가 되며,
 * GetProcAddress로 Kernel32.dll에서 실행 시 가져오는 형태가 필요합니다. */
#define _WIN32_WINNT 0x0602   /* Windows 8 */
#include <windows.h>
#include <stdio.h>

int wmain(void)
{
    /* 1. 프로세스 기본 검색 공간을 안전한 쪽으로 맞춘다.
     *    current folder와 PATH를 검색 대상에서 빼는 것이 목적입니다.
     *    LOAD_LIBRARY_SEARCH_DEFAULT_DIRS 는
     *    APPLICATION_DIR + SYSTEM32 + USER_DIRS 의 조합입니다.
     *    USER_DIRS 를 넣어 두지 않으면, 절차 2의 AddDllDirectory 는
     *    프로세스 기본에는 반영되지 않습니다. */
    if (!SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)) {
        wprintf(L"SetDefaultDllDirectories failed: %lu\n", GetLastError());
        return 1;
    }

    /* 2. 자체 플러그인 폴더만 명시적으로 더한다.
     *    AddDllDirectory 에 넘기는 것은 절대 경로여야 합니다. */
    const wchar_t *pluginDir = L"C:\\MyApp\\plugins";
    DLL_DIRECTORY_COOKIE cookie = AddDllDirectory(pluginDir);
    if (cookie == NULL) {
        wprintf(L"AddDllDirectory failed: %lu\n", GetLastError());
        return 1;
    }

    /* 3. 로드한다.
     *    두 번째 인수 hFile 은 예약되어 있으므로 반드시 NULL.
     *    여기서 플래그를 넘기면, 절차 1의 프로세스 기본이 아니라
     *    이 플래그 조합만 쓰입니다.
     *    APPLICATION_DIR 을 빼 두었으므로 앱 폴더는 찾지 않습니다. */
    HMODULE h = LoadLibraryExW(
        L"foo.dll",
        NULL,
        LOAD_LIBRARY_SEARCH_USER_DIRS | LOAD_LIBRARY_SEARCH_SYSTEM32);
    if (h == NULL) {
        wprintf(L"LoadLibraryExW failed: %lu\n", GetLastError());
        RemoveDllDirectory(cookie);
        return 1;
    }

    /* 4. 어디서 읽혔는지를 반드시 확인한다.
     *    조사에서는 읽혔는지보다 어디서 읽혔는지가 중요합니다. */
    {
        wchar_t loadedPath[MAX_PATH];
        DWORD cap = (DWORD)(sizeof(loadedPath) / sizeof(loadedPath[0]));
        DWORD len = GetModuleFileNameW(h, loadedPath, cap);
        if (len > 0 && len < cap) {
            wprintf(L"loaded from: %s\n", loadedPath);
        } else {
            wprintf(L"GetModuleFileNameW failed: %lu\n", GetLastError());
        }
    }

    FreeLibrary(h);
    RemoveDllDirectory(cookie);
    return 0;
}

이 코드에서 잡는 점은 네 가지입니다.

  1. SetDefaultDllDirectories를 먼저 호출한다. 한 번 호출하면 프로세스 수명 동안 계속 적용되고, 표준 검색 경로로 되돌릴 수는 없습니다4
  2. AddDllDirectoryLOAD_LIBRARY_SEARCH_USER_DIRS와 세트로 비로소 의미가 있다. SetDefaultDllDirectoriesUSER_DIRS를 넣지 않은 경우, 추가한 디렉터리는 LOAD_LIBRARY_SEARCH_USER_DIRS를 지정한 LoadLibraryEx 호출에서만 쓰입니다5
  3. 뒷정리를 한다. AddDllDirectory가 반환한 cookie는 RemoveDllDirectory에 넘겨 뺄 수 있습니다5
  4. 읽힌 위치를 출력한다. GetModuleFileNameW 한 줄이 있는지에 따라, 장애 시 조사 시간이 달라집니다

참고로 foo.dll이 의존하는 DLL도 이 LOAD_LIBRARY_SEARCH_* 범위에서 찾습니다. foo.dll 자신의 폴더를 의존 DLL 탐색처로 넣고 싶으면, 첫 번째 인수를 전체 경로로 한 뒤 LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR을 더합니다.

최소 코드 예의 흐름SetDefaultDllDirectories로 프로세스 기본 검색 공간을 안전한 쪽으로 맞추고, AddDllDirectory로 플러그인 폴더를 명시적으로 더하고, LoadLibraryEx로 검색 플래그를 지정해 로드하고, GetModuleFileNameW로 어디서 읽혔는지를 확인한 뒤 마지막으로 RemoveDllDirectory로 뒷정리하는 흐름을 나타내는 그림.1. SetDefaultDllDirectories로 기본을 안전한 쪽으로2. AddDllDirectory로 허용 폴더를 더한다3. LoadLibraryEx로 플래그 지정 로드4. GetModuleFileNameW로 로드 원본을 확인5. RemoveDllDirectory로 뒷정리

그림 17: 코드 예의 골격은 좁히기 → 더하기 → 로드 → 확인 → 뒷정리의 다섯 수입니다.

8.6 C#에서 쓰는 경우

.NET에서도 같은 사고방식을 쓸 수 있습니다. P/Invoke의 검색 경로는 DefaultDllImportSearchPaths 특성으로, 명시 로드는 NativeLibrary.Load로 제어합니다.

// .NET 8 / C# 12
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;

// 어셈블리 안의 모든 P/Invoke에 대해, 기본 검색처를 System32로 한정한다.
// 제약: 절대 경로를 지정한 P/Invoke에는 이 특성이 적용되지 않습니다.
//       또한 Windows 이외 플랫폼에서는 효과가 없습니다.
[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.System32)]

internal static class Program
{
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern uint GetTickCount();

    private static void Main()
    {
        // 특성은 선언만으로는 아무 일도 일어나지 않습니다. 호출해야 비로소 적용됩니다.
        Console.WriteLine($"GetTickCount = {GetTickCount()}");

        // 플러그인은 전용 폴더에 두어 있다. 여기서
        //   NativeLibrary.Load("foo.dll", asm, ApplicationDirectory | System32)
        // 라고 쓰면 안 됩니다. ApplicationDirectory가 가리키는 것은 exe가 있는 폴더,
        // System32가 가리키는 것은 OS 폴더이며, 어느 쪽도 플러그인 폴더를 보지 않습니다.
        // 로드에 실패하거나, 우연히 exe 옆에 있던 다른 foo.dll을 집습니다.
        // 두는 위치를 아는 것은 전체 경로로 지정하는 것이 확실합니다.
        string pluginDir = Path.Combine(AppContext.BaseDirectory, "plugins");
        string pluginPath = Path.GetFullPath(Path.Combine(pluginDir, "foo.dll"));
        if (!File.Exists(pluginPath))
        {
            throw new FileNotFoundException("플러그인을 찾을 수 없습니다.", pluginPath);
        }

        // 경로를 넘기는 오버로드는 그 파일을 직접 읽습니다(검색하지 않습니다).
        IntPtr handle = NativeLibrary.Load(pluginPath);

        try
        {
            // 어디서 읽혔는지를 확인한다.
            foreach (ProcessModule module in Process.GetCurrentProcess().Modules)
            {
                if (string.Equals(module.ModuleName, "foo.dll", StringComparison.OrdinalIgnoreCase))
                {
                    Console.WriteLine($"loaded from: {module.FileName}");
                }
            }
        }
        finally
        {
            NativeLibrary.Free(handle);
        }
    }
}

DllImportSearchPath의 값은 LOAD_LIBRARY_SEARCH_* 플래그와 대응합니다. 그래서 C# 쪽에서 System32만이라고 쓴 경우에도 8.4의 제약이 그대로 적용됩니다. 애플리케이션 디렉터리는 찾지 않습니다.

여기서 주의할 것은, DllImportSearchPath에는 임의의 폴더를 가리키는 값이 없다는 점입니다. ApplicationDirectory는 exe가 있는 폴더, System32는 OS 폴더를 가리키며, 플러그인용으로 스스로 정한 폴더를 나타내는 값은 없습니다. 그래서 전용 폴더에 둔 것을 검색 플래그로 집으려 해도 닿지 않고, 위처럼 전체 경로로 지정하게 됩니다.

참고로 전체 경로로 읽어도 9장과 같이 foo.dll이 의존하는 DLL까지는 고정되지 않습니다. 플러그인이 독자 의존 DLL을 데려온다면, AddDllDirectory로 그 폴더를 더한 뒤 LOAD_LIBRARY_SEARCH_USER_DIRS를 적용하거나, 의존 관계까지 한 폴더에 모아 LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR을 쓰는 C 쪽 작법이 그대로 필요합니다.

C#에서 전용 폴더의 DLL을 읽는 사고방식DllImportSearchPath에는 임의의 폴더를 가리키는 값이 없고, ApplicationDirectory는 exe가 있는 폴더, System32는 OS 폴더를 가리키므로 전용 폴더에 둔 플러그인은 검색 플래그로는 닿지 않고 전체 경로로 지정해 직접 읽게 되며, 의존 DLL 제어는 C 쪽 작법이 그대로 필요하다는 것을 나타내는 그림.임의의 폴더를 가리키는 값은 없다확실한 방법플러그인을 전용 폴더에 둔다검색 플래그로 집을 수 있는가플래그로는 닿지 않는다전체 경로를 조립해 NativeLibrary.Load로 직접 읽는다의존 DLL 제어는 C 쪽 작법이 필요하다

그림 18: .NET에서도, 스스로 정한 폴더는 전체 경로 지정으로 읽는 것이 확실합니다.

9. 전체 경로로 지정해도 의존 DLL까지는 고정되지 않는다

이 주제에서 실무상 꽤 중요한데 놓치기 쉬운 지점입니다. Microsoft Learn에는 첫 DLL을 전체 경로로 로드해도, 그 DLL의 의존 DLL은 모듈 이름만으로 검색된다고 설명되어 있습니다.14

즉,

  • C:\\MyApp\\plugins\\foo.dll을 명시 로드했다
  • 그래서 foo.dll이 의존하는 bar.dll도 같은 폴더에서 반드시 가져온다

는 것은 아닙니다.

이 오해가 있으면,

  • 개발 환경에서는 된다
  • 배포처에서는 다른 bar.dll이 확인된다
  • 의존 DLL 충돌이 재현 환경에 의존하게 된다

는, 조금 까다로운 장애가 됩니다.

전체 경로 지정이어도 의존 DLL은 고정되지 않는다첫 DLL을 전체 경로로 로드해도 그 DLL이 의존하는 DLL은 모듈 이름만으로 검색되는 취급이라 다른 위치에서 확인될 수 있고, 개발 환경에서는 되는데 배포처에서는 다른 의존 DLL이 확인되는 환경 의존 장애가 된다는 것을 나타내는 그림.foo.dll을 전체 경로로 로드한다foo.dll 자체는 지정대로 읽힌다의존하는 bar.dll은 모듈 이름만으로 검색환경에 따라 다른 위치에서 확인될 수 있다개발에서는 되는데 배포처에서 깨지는 환경 의존 장애

그림 19: 전체 경로로 고정할 수 있는 것은 첫 하나뿐이고, 의존은 별도 취급입니다.

10. DLL preloading / hijacking을 피하려면

Microsoft Learn의 DLL security에서는, 전체 경로 없는 동적 로드와 공격자가 통제할 수 있는 검색 대상 디렉터리의 조합이 DLL preloading attack이나 binary planting attack으로 이어진다고 설명합니다.2

실무에서는 이 기본 형태에 맞추면 생각하기 쉽습니다.

  • LoadLibrary("foo.dll")처럼 이름만으로 하는 로드를 줄인다
  • 필요하면 전체 경로를 쓴다
  • 프로세스 기본 검색 경로를 SetDefaultDllDirectories로 좁힌다
  • 명시적으로 허용한 디렉터리만 AddDllDirectory로 추가한다
  • LOAD_LIBRARY_SEARCH_SYSTEM32, LOAD_LIBRARY_SEARCH_APPLICATION_DIR, LOAD_LIBRARY_SEARCH_USER_DIRS 등을 써서 탐색 범위를 명시한다
  • current folder나 섣부른 PATH 의존을 피한다

특히 관리자 권한으로 도는 프로세스가 모호한 검색 경로를 가진 상황은 위험합니다. Microsoft Learn에도, 악성 DLL이 로드되면 그 DLL은 그 프로세스 권한으로 실행된다고 설명되어 있습니다.2

DLL preloading 공격이 성립하는 조합전체 경로 없는 동적 로드와 공격자가 통제할 수 있는 검색 대상 디렉터리의 조합이 DLL preloading attack으로 이어지고, 악성 DLL이 로드되면 그 프로세스 권한으로 실행되므로 이름만으로 하는 로드를 줄이고 검색 공간을 좁히는 것이 대책임을 나타내는 그림.전체 경로 없는 동적 로드DLL preloading의 성립 조건이 갖춰진다공격자가 통제할 수 있는 검색 대상 디렉터리악성 DLL이 프로세스 권한으로 실행된다대책: 이름만 쓰는 일을 줄이고 검색 공간을 좁힌다

그림 20: 공격은 모호한 이름과 통제할 수 있는 장소의 곱으로 성립합니다.

11. 어느 DLL이 어디서 읽혔는지를 확인한다

여기까지는 사양 이야기였습니다. 실제 조사에서는 추측하지 않고 확인하는 편이 빠릅니다. 목적별로 네 가지를 듭니다.

알고 싶은 것 쓰는 것 보는 곳
어디를 찾아, 어디서 발견했는가 Process Monitor10 파일 접근 기록
지금 무엇이 로드되어 있는가 Process Explorer, tasklist /m11 프로세스의 모듈 목록
어떤 DLL을 어느 프로세스가 잡고 있는가 ListDLLs12 프로세스 횡단 목록
디버그 중에 실제로 읽힌 경로 Visual Studio의 모듈 창 디버그 > 창 > 모듈7

11.1 Process Monitor로 탐색 발자국을 따라간다

정보량이 가장 많은 것이 이것입니다. 절차는 다음과 같습니다.10

  1. Process Monitor를 관리자로 시작해 기록을 시작한다
  2. Filter > Filter…를 열고, Process Name is에 대상 exe 이름을 넣어 추가한다
  3. 같은 화면에서 Path ends with .dll을 추가한다
  4. 대상 앱을 시작하고, 문제가 난 지점에서 기록을 멈춘다

이때 보는 것은 Result 열입니다. 로더는 검색 순서를 따라 위에서부터 차례로 열려고 하므로, 찾지 못한 위치에는 NAME NOT FOUND, 실제로 연 위치에는 SUCCESS가 늘어섭니다. 즉 NAME NOT FOUND가 이어진 마지막 다음 행이, 실제로 채택된 경로가 됩니다.

3장의 표와 맞춰 보면, 그 사례가 어느 단계에서 정해졌는지를 알 수 있습니다. 선행 규칙(표의 1~6)에서 정해진 경우에는, 애초에 파일을 찾으러 간 기록이 나오지 않습니다. 이것도 중요한 단서입니다.

Process Monitor 기록 읽는 법로더는 검색 순서를 따라 위에서부터 차례로 열려고 하므로 찾지 못한 위치에는 NAME NOT FOUND, 실제로 연 위치에는 SUCCESS가 늘어서고, NAME NOT FOUND가 이어진 마지막 다음 행이 실제로 채택된 경로가 되며, 선행 규칙에서 정해진 경우에는 파일을 찾으러 간 기록 자체가 나오지 않는다는 것을 나타내는 그림.Result 열을 위에서부터 읽는다NAME NOT FOUND의 나열 =찾아서 없었던 위치그다음 SUCCESS가 실제로 채택된 경로기록이 나오지 않으면 선행 규칙에서 정해진 것이다

그림 21: 발자국은 Result 열에 남고, 기록이 없다는 것 자체도 단서가 됩니다.

11.2 로드된 모듈을 나열한다

이미 시작된 프로세스에 대해, 무엇이 어디서 읽히고 있는지만 보려면 추가 도구 없이도 확인할 수 있습니다.

tasklist /m foo.dll

이 명령은 지정한 모듈을 로드한 프로세스의 이름과 PID를 나열합니다.11 어느 프로세스가 대상인지를 좁힌 뒤, Process Explorer의 아래 창을 DLL 표시로 바꾸면 그 프로세스가 읽는 DLL의 전체 경로까지 확인할 수 있습니다.

Sysinternals의 ListDLLs를 쓰면 같은 일을 명령줄에서, 게다가 프로세스 횡단으로 확인할 수 있습니다.12

5.1의 loaded-module list가 적용되고 있는지는 이 방법으로만 알 수 있습니다. 같은 이름 DLL이 다른 폴더에서 먼저 로드되어 있으면, 그 프로세스에서는 새로 찾으러 가지 않기 때문입니다.

로드된 모듈 확인 절차tasklist /m으로 지정 모듈을 로드한 프로세스를 나열해 대상을 좁히고, Process Explorer의 DLL 표시나 ListDLLs로 로드된 DLL의 전체 경로까지 확인하는 절차이며, loaded-module list가 적용되고 있는지는 이 방법으로만 알 수 있다는 것을 나타내는 그림.tasklist /m foo.dll로 잡고 있는 프로세스를 좁힌다Process Explorer나 ListDLLs로 전체 경로를 본다어디서 읽혔는가가 사실로 확정된다loaded-module list의 영향은 여기서만 보인다

그림 22: 지금 읽히고 있는 것은 추측이 아니라 모듈 목록으로 확정합니다.

12. 실무에서의 판단 체크리스트

Windows에서 DLL 로드 설계를 리뷰할 때는, 최소한 다음을 확인하면 사고가 줄어듭니다.

  1. 그 앱은 packaged app인가 unpackaged app인가
  2. 어느 DLL이 정적 링크 유래이고, 어느 것이 동적 로드인가
  3. 전체 경로 지정인가, 모듈 이름만인가
  4. SetDllDirectory를 쓰지 않았는가
  5. SetDefaultDllDirectoriesLOAD_LIBRARY_SEARCH_*를 쓸 수 있는 구성인가
  6. AddDllDirectory를 여러 개 쓰면서, 순서 의존을 암묵적으로 기대하지 않았는가
  7. manifest / SxS / private DLL / redirection 중 무엇으로 의존 관계를 관리하는가
  8. current folder나 PATH에 보안상 약한 전제가 없는가
  9. 의존 DLL이 다른 환경에서 다른 위치에서 확인되지 않는가
  10. 증상이 난 환경에서, 실제로 어디서 읽혔는지를 확인했는가 (11장)

이 10가지를 나눠 확인하면, DLL을 찾지 못함, 다른 DLL이 읽힘, 운영에서만 시작되지 않음, 취약점 리뷰에서 멈춤 같은 문제를 꽤 앞에서 막을 수 있습니다.

리뷰에서 확인하는 흐름DLL 로드 설계 리뷰에서는 packaged인지 unpackaged인지라는 앱 형태, 전체 경로인지 모듈 이름인지라는 로드 방식, SetDllDirectory 유무나 검색 플래그 같은 API 사용, manifest나 의존 DLL 관리, 그리고 증상이 난 환경에서 실제로 어디서 읽혔는지의 확인이라는 흐름으로 보면 사고가 줄어든다는 것을 나타내는 그림.앱 형태를 확인 (packaged / unpackaged)로드 방식을 확인 (전체 경로인가 이름만인가)API 사용을 확인 (SetDllDirectory·플래그)의존 관리를 확인 (manifest / SxS / PATH)현장에서 로드 원본을 확인한다(11장)

그림 23: 체크리스트는 형태 → 로드 → API → 의존 → 현장 확인 순으로 소화할 수 있습니다.

13. 정리

Windows에서 DLL 이름 확인은 단순한 폴더 탐색 순서가 아닙니다. 실제로는 DLL redirection, API set, SxS manifest, loaded-module list, Known DLLs, 그리고 API 호출로 바뀌는 검색 공간이 겹쳐 정해집니다.134

실무에서 가장 중요한 것은 다음 여섯 가지입니다.

  • 검색 순서를 표 한 장으로 외우지 않는다. 표는 자기 사례가 어느 단계에서 정해졌는지를 맞히기 위해 쓴다
  • packaged / unpackaged를 나눈다
  • 전체 경로 지정이어도 의존 DLL은 별도 취급이 될 수 있음을 이해한다
  • SetDllDirectory를 가볍게 쓰지 않는다
  • 안전한 쪽으로 맞추려면 SetDefaultDllDirectoriesLoadLibraryEx의 검색 플래그를 쓴다
  • 추측으로 끝내지 않고, Process Monitor 등으로 실제로 읽힌 경로를 확인한다

DLL 이름 확인은 시작 장애, 환경 차이, 보안 문제가 한꺼번에 드러나기 쉬운 지점입니다. 그래서 Windows가 어떤 순서로 찾는가뿐 아니라, Windows가 애초에 무엇을 이름 확인의 전제로 다루는가까지 이해해 둘 가치가 있습니다.

이 글의 결론DLL 이름 확인은 시작 장애·환경 차이·보안 문제가 한꺼번에 드러나기 쉬운 지점이며, 어떤 순서로 찾는가뿐 아니라 Windows가 애초에 무엇을 이름 확인의 전제로 다루는가까지 이해해 둘 가치가 있다는 것을 나타내는 그림.어떤 순서로 찾는가 (폴더의 순서)둘 다 이해해야 비로소 설명이 된다무엇을 전제로 다루는가 (선행 규칙과 검색 공간)시작 장애·환경 차이· 보안을 한꺼번에 막을 수 있다

그림 24: 순서 암기가 아니라, 전제의 이해까지가 DLL 이름 확인의 실무 지식입니다.

관련 글

참고 링크

  1. Microsoft Learn: Dynamic-link library search order
  2. Microsoft Learn: Dynamic-Link Library Security
  3. Microsoft Learn: Windows API sets
  4. Microsoft Learn: SetDefaultDllDirectories function
  5. Microsoft Learn: AddDllDirectory function
  6. Microsoft Learn: LoadLibraryEx function
  7. Microsoft Learn: Dynamic-link library redirection
  8. Microsoft Learn: Manifests
  9. Microsoft Learn: About Side-by-Side Assemblies
  10. Microsoft Learn: Process Monitor
  11. Microsoft Learn: ListDLLs
  12. Microsoft Learn: tasklist
  1. Microsoft Learn, Dynamic-link library search order, accessed March 24, 2026  2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

  2. Microsoft Learn, Dynamic-Link Library Security, accessed March 24, 2026  2 3 4 5

  3. Microsoft Learn, Windows API sets, accessed March 24, 2026  2 3 4 5 6

  4. Microsoft Learn, SetDefaultDllDirectories function, accessed March 24, 2026  2 3 4 5 6 7 8 9

  5. Microsoft Learn, AddDllDirectory function, accessed March 24, 2026  2 3 4 5 6

  6. Microsoft Learn, LoadLibraryEx function, accessed March 24, 2026  2 3 4 5 6

  7. Microsoft Learn, Dynamic-link library redirection, accessed March 24, 2026  2 3 4 5 6 7

  8. Microsoft Learn, Manifests, accessed March 24, 2026  2 3

  9. Microsoft Learn, About Side-by-Side Assemblies, accessed March 24, 2026  2 3 4

  10. Microsoft Learn, Process Monitor  2

  11. Microsoft Learn, tasklist  2

  12. Microsoft Learn, ListDLLs  2

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

Windows 앱 개발

DLL 배치나 로드 방법은 Windows 앱의 시작 여부, 배포 형태, 문제 발생 시 재현성에 직결되므로, Windows 앱 개발 맥락에서 다룰 가치가 있습니다.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

LoadLibrary에 DLL 이름을 지정하면 Windows는 어떤 순서로 찾나요?
파일 시스템을 순서대로 찾기 전에 선행 규칙이 평가됩니다. 구체적으로는 DLL redirection, API set, SxS manifest redirection, loaded-module list, Known DLLs가 먼저 적용되고, 그다음에 애플리케이션 폴더, System32, Windows 폴더, current folder, PATH 같은 파일 시스템 탐색으로 들어갑니다. safe DLL search mode가 켜진 기본 상태에서는 current folder는 꽤 뒤로 밀립니다. 또한 packaged app과 unpackaged app은 검색 순서의 사고방식 자체가 다릅니다.
DLL을 전체 경로로 로드하면 의존 DLL도 같은 폴더에서 읽히나요?
반드시 그렇지는 않습니다. 첫 DLL을 전체 경로로 로드해도, 그 DLL이 의존하는 DLL은 모듈 이름만으로 검색되는 취급이라 다른 위치에서 확인될 수 있습니다. 이 오해가 있으면 개발 환경에서는 되는데 배포처에서는 다른 의존 DLL이 잡혀, 환경에 따라 재현이 어려운 장애가 됩니다. 의존 DLL까지 제어하려면 LoadLibraryEx의 LOAD_LIBRARY_SEARCH_* 플래그 등으로 검색 공간을 명시합니다.
SetDllDirectory를 쓰면 안 되는 이유는 무엇인가요?
검색 순서만 바꾸는 것이 아니라, safe DLL search mode를 사실상 끄는 동작이 있기 때문입니다. 앱 전용 폴더를 하나 더하고 싶었을 뿐인데 current folder 취급을 포함한 검색 공간 전체가 바뀌어, 보안 면에서 역효과가 날 수 있습니다. 게다가 부모 프로세스에서 호출하면 자식 프로세스 쪽 검색 순서에도 영향이 갈 수 있습니다. 대신 SetDefaultDllDirectories, AddDllDirectory, LoadLibraryEx의 검색 플래그로 맞추는 편이 안전합니다.
DLL 하이재킹(DLL preloading 공격)을 막으려면 어떻게 해야 하나요?
전체 경로 없는 동적 로드와, 공격자가 통제할 수 있는 검색 대상 디렉터리의 조합이 공격으로 이어지므로 둘 다 줄입니다. 구체적으로는 이름만으로 하는 LoadLibrary를 줄이고, 필요하면 전체 경로를 쓰며, SetDefaultDllDirectories로 프로세스 기본 검색 경로를 좁히고, 허용한 디렉터리만 AddDllDirectory로 추가하며, current folder나 섣부른 PATH 의존을 피합니다. 특히 관리자 권한으로 도는 프로세스가 모호한 검색 경로를 가진 상황은 위험하고, 악성 DLL이 그 프로세스 권한으로 실행됩니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기