이름만 같으면 그 파일을 믿는다 — DLL 하이재킹이 계속 반복되는 이유
이름만 같으면 그 파일을 믿는다 — DLL 하이재킹이 계속 반복되는 이유
분류: Security · 커널·OS · DLL 하이재킹(권한상승/지속성) [정보성]
덱. 윈도우 프로그램은 자신이 불러올 라이브러리(DLL)를 이름만으로 찾고, 그 이름이 어느 경로에서 왔는지는 크게 따지지 않습니다. 보안 연구자 Elliot(elliotonsecurity.com)은 이 오래된 약점을 다시 들여다보며, 악성 DLL을 몰래 끼워 넣는 기법(DLL 하이재킹) 자체가 아니라 그 다음 단계 — 끼워 넣은 뒤 코드를 “안전하게” 실행하는 방법 — 를 새로 정리했습니다. 이 원리를 이해하면 신뢰된 실행파일이라는 말이 실제로 무엇을 검증하고 무엇을 검증하지 않는지가 보입니다.
리드
윈도우가 프로그램 실행 시 DLL을 찾는 방식은 정해진 순서(검색 순서, search order)를 따라 여러 폴더를 차례로 뒤지는 것입니다. 이 검색 순서에 프로그램의 실행 폴더, 현재 작업 디렉터리, 시스템 폴더, PATH 환경변수에 등록된 경로들이 포함됩니다. 문제는 이 검색 과정이 “이 이름의 파일이 진짜 그 라이브러리가 맞는가”를 확인하지 않고, “이 이름의 파일이 먼저 발견됐는가”만 본다는 점입니다. 공격자가 검색 순서상 더 앞쪽에 같은 이름의 가짜 DLL을 놓아두면, 정상 서명된 프로그램이 스스로 악성 코드를 불러와 실행하게 됩니다. 이런 종류의 결함은 새로운 것이 아니지만, “끼워 넣은 다음 뭘 할 것인가” 쪽은 의외로 덜 다뤄진 영역이었습니다.
01 개념 30초 — DLL 하이재킹과 그 함정
DLL 하이재킹은 정상 프로그램이 특정 DLL을 검색 순서에 따라 찾다가, 공격자가 놓아둔 가짜 DLL을 먼저 발견해 로드하게 만드는 기법입니다. DLL이 로드되는 순간 DllMain이라는 초기화 함수가 자동으로 호출되는데, 여기가 공격자 코드의 첫 실행 지점이 됩니다. 그런데 이 DllMain은 “로더 락(Loader Lock)“이라는 동시성 보호 장치(다른 스레드가 라이브러리 로딩 작업을 동시에 건드리지 못하게 막는 잠금장치) 아래에서 실행됩니다. 마이크로소프트는 이 상태에서 LoadLibrary 호출, 스레드 동기화, 레지스트리 함수, 프로세스 생성 등 상당수의 기본 API를 쓰지 말라고 공식 문서에서 경고합니다. 어기면 그 자리에서 멈추거나(교착상태, deadlock) 프로그램이 죽습니다.
02 빈틈 — “안전하게” 코드를 실행할 방법이 마땅치 않았다
그동안 이 문제를 우회하는 방법은 크게 두 갈래였습니다. 하나는 메모리 보호 속성을 바꿔(읽기/쓰기 전용 메모리를 실행 가능하게 전환) 자체 코드를 심는 방식인데, 이런 메모리 보호 변경 자체가 백신·EDR이 흔히 감시하는 지표라 쉽게 걸립니다. 다른 하나는 함수 포인터나 반환 주소 같은 값을 직접 조작하는 방식인데, 이는 인텔의 CET(Control-flow Enforcement Technology, 반환 주소가 변조되지 않았는지 하드웨어 수준에서 대조하는 최신 보호 기능) 같은 차세대 보호 장치가 정확히 이런 조작을 막기 위해 설계됐습니다. 즉 기존 우회법들은 갈수록 탐지되기 쉽거나 조만간 무력화될 운명이었습니다. 연구자는 이 두 가지를 모두 건드리지 않는, “데이터만 다루는” 우회법을 찾는 데 집중했습니다.
03 트릭 — 잠금장치 자체를 이해하고 다루기
연구는 윈도우가 내장한 프로그램 중 하나(Windows Defender의 오프라인 검사 도구)를 대상으로 진행됐습니다. 이 프로그램은 DLL을 실행 파일 시작 시점에 정적으로 불러오고, 그 DLL의 내보내기 함수(export)를 하나도 호출하지 않은 채 조건이 안 맞으면 즉시 종료해버리는 — 연구자 표현으로 “최악의 케이스”였습니다. 이런 대상에서 통하는 기법이라면 대부분의 다른 상황에서도 통할 가능성이 높다는 논리였습니다.
접근 방식은 단계적으로 발전했습니다. 초기에는 새 스레드를 만들어 우선순위를 조정하고 메인 스레드를 잠깐 멈춘 뒤 로더 락이 풀리는 타이밍을 노리는 “경쟁(race)” 방식을 시도했지만, 메인 스레드가 하필 메모리 할당 관련 내부 잠금을 쥐고 있는 순간에 멈추면 교착상태가 나는 근본적 한계가 있어 완전한 성공률에 이르지 못했습니다.
다음으로 표준 C 함수인 atexit(프로그램 종료 시 지정한 함수를 실행해주는 기능)을 이용하는 방법을 시도했으나, 이 역시 로더 락 아래에서 실행된다는 걸 확인하게 됩니다. 다만 조사 과정에서 흥미로운 사실이 드러났습니다 — 윈도우에는 사실상 두 갈래의 C 런타임(오래된 msvcrt.dll과 최신 UCRT)이 공존하고, 이 중 하나의 특정 내부 구현 경로로 atexit을 등록하면 로더 락 밖에서 실행된다는 것이었습니다. 다만 이 방법은 대상 프로그램이 그 특정 런타임과 연결돼 있어야만 통하는 조건부 기법이었습니다.
가장 완성도 높은 접근은 로더 락이라는 잠금장치 자체가 어떻게 구현돼 있는지를 리버스 엔지니어링(역공학)으로 이해한 뒤, DllMain 안에서 이 잠금 상태를 직접 풀어버리는 것이었습니다. 잠금을 관리하는 내부 상태 변수의 위치를 문서화되지 않은 win32 내부 함수들의 어셈블리 코드를 뒤져 알아낸 뒤, 필요한 만큼 잠금을 해제하고 원하는 코드를 실행하고, 다시 원래 순서대로 잠금을 복원하는 방식입니다. 이 방식은 반복 테스트에서 매번 성공했다고 보고됐습니다. 다만 이는 마이크로소프트가 공식적으로 보장하지 않는 내부 구현 세부에 의존하는 방식이라, 윈도우 버전이 바뀌면 깨질 수 있는 접근입니다.
위 기법들의 정확한 동작 방식·내부 함수명·주소 계산 방식 등은 원문에 상세히 기술돼 있으나, 이 글에서는 개념 수준으로만 다룹니다. 실제 동작하는 코드나 프록시 DLL 구현은 싣지 않습니다(개념용 단순화, 실제 코드 아님).
04 왜 반복되는가 — 신뢰의 기준이 “이름”에 머물러 있다
이 연구가 보여주는 근본적인 지점은, 하이재킹의 시작점이 결국 **“이 프로그램이 자신이 불러오는 파일의 정체를 충분히 검증하지 않는다”**는 것입니다. 검색 순서라는 편의 기능(개발자가 DLL 경로를 일일이 지정하지 않아도 되게 해주는)이, 그 경로 어딘가에 공격자가 쓸 수 있는 폴더(사용자 프로필, PATH에 등록된 사용자 쓰기 가능 경로 등)가 끼어 있는 순간 신뢰 경계가 됩니다. 이건 저희가 다른 글에서 다룬 “한 계층에서의 확인 상태를 다른 계층이 그대로 믿어버리는” 문제와 같은 뿌리입니다 — 여기서는 “이 이름의 파일이 검색 경로 어딘가에 존재한다”는 사실 하나가 “이 파일이 신뢰할 만한 라이브러리다”라는 결론으로 아무 검증 없이 이어집니다. 게다가 정적 탐지(코드 실행 없이 파일만 보고 판단)와 동적 탐지(실행 중 행동을 보고 판단) 양쪽 모두 이 종류의 공격에 대해 한계를 갖습니다 — 정적으로는 정상 코드처럼 보이고, 동적으로는 로더 락이라는 짧은 시간 창 안에서 벌어지는 일이라 놓치기 쉽습니다.
05 남는 교훈 — 방어는 결국 “출처”를 보는 쪽으로
원문이 제시하는 방어책은 결국 파일의 이름이 아니라 출처와 서명을 보는 쪽으로 수렴합니다. 첫째, DLL의 내보내기 함수 목록이 정상 마이크로소프트 서명 DLL과 겹치면서도 서명이 없다면 하이재킹 의심 대상으로 봐야 합니다(로더가 필요한 내보내기 함수가 없으면 애초에 DllMain도 실행되지 않고 조기에 실패하기 때문에, 이 검사가 유효합니다). 둘째, 사용자 쓰기 권한이 있는 폴더(사용자 프로필, 특정 PATH 경로)에서 로드되는 DLL은 더 엄격히 감시해야 합니다. 셋째, 윈도우가 이미 제공하는 Set-ProcessMitigation -Enable MicrosoftSignedOnly 같은 정책으로 특정 프로그램이 마이크로소프트 서명 DLL만 로드하도록 강제할 수 있습니다. 근본적으로는 코드 실행 시점의 행위를 탐지하기보다, 애초에 “가짜 DLL이 그 자리에 놓일 수 없게” 만드는 예방이 가장 확실한 방어라는 게 이 분야의 오래된 결론이고, 이번 연구도 같은 지점에서 끝맺습니다.
References / 참고자료
- Elliot (elliotonsecurity.com), “Perfect DLL Hijacking” — 이 글이 재구성한 원 분석. 로더 락의 내부 동작 원리와 이를 우회하는 세 갈래 기법, 탐지·완화 방안을 다룸. https://elliotonsecurity.com/perfect-dll-hijacking/
이 글은 위 원 분석을 우리 관점에서 다시 쓰고 짧은 논평을 더한 정보성 초안입니다. 특정 문장·코드를 그대로 옮기지 않았으며, 작동하는 재현 절차나 실제 DLL 프록시 코드는 싣지 않았습니다. ⚠️미확인 표시가 없어도, 발행 전 독립 팩트체크에서 사실관계는 재확인됩니다.