DLL ( Dynamic Link Library )
DLL은 동적 링크 라이브러리(Dynamic Link Library)의 약자로 일반적으로 확장자가 DLL인 파일이다. 라이브러리라는 말에서 알 수 있듯이 다른 프로그램에서 이용하는 함수들을 모아둔 것이다. 하지만 표준 C 라이브러리 같은 일반 라이브러리의 파일과는 구조나 사용법이 다소 다르다. 일반 라이브러리는 소스코드를 컴파일한 결과로 생성되는 객체 파일(.OBJ)을 그대로 모아둔 것이다. 링커는 이 중에서 필요한 함수가 포함된 객체 파일을 꺼내서 실행 파일에 결합하는 '정적 링크' 방식이다.
아래 그림은 정적 링크를 나타내는 것으로 C/C++ 프로그램의 소스 코드를 기계어 코드로 변환하는 컴파일 단계를 거치게 된다. 여기서 C/C++에는 수많은 표준함수들이 존재하고 있는데 이들은 표준 라이브러리 파일 안에 어셈블리 코드의 형태로 담겨 있다. 소스 코드는 하나 이상 존재할 수가 있는데, 링크 단계는 이 여러 개의 소스 파일들이 하나의 실행 파일로 구성해낸다. 이때 각각의 소스파일에서 호출한 표준 함수들을 표준 라이브러리에서 가져와 실행파일에 붙여준다. 이러한 과정을 링크 과정이라 한다. 그리고 이러한 방식이 바로 '정적 링크'이다.
하지만 많은 표준 함수를 사용할수록 프로그램의 크기가 커지게 되며, 똑같은 함수를 사용한다고 하더라도 이러한 정적 링크 방식은 각 프로그램마다 링크 과정에서 라이브러리를 가져와 프로그램 안에 저장하기 때문에 이는 비효율적이라 할 수 있다.
정적 링크 방식과는 다르게 DLL은 '동적 링크'에 방식으로, 이는 링크 시에 실행 파일에 결합되는 것이 아니라 프로그램 실행 시에 DLL도 함께 프로그램의 메모리 공간으로 읽어와 호출될 주소 등을 적절하게 바꾸는 것을 말한다. 일단 읽어온 DLL 함수는 프로그램 내부 함수처럼 호출할 수 있다. DLL은 실행 파일과 다른 파일이므로 필요한 시점에 메모리로 읽어오고 불필요하면 메모리에서 내릴 수 있다.
이러한 방식으로 인해 DLL은 여러 장점을 갖게 된다. 우선 여러 프로그램에서 동시에 사용할 수 있다는 것이다. 정적 링크 방식은 자신이 가진 코드를 자기 혼자만 사용하지만, 동적 링크 방식은 하나의 DLL로 존재하여 다른 프로그램에게 라이브러리를 제공해준다. 실행 파일은 DLL에 있는 함수를 Import하게 되는 것이고, DLL은 실행파일에게 함수를 Export 해주게 되는 것이다.
* Export Name Table Offset Data Ordinal Value ============================================== 0x47C4 0xD9E1 0x01 HEEnterWindowLoop 0x47C8 0xD9F3 0x02 HEEnterWindowLoopInNewThread 0x47CC 0xDA10 0x03 HESpecifySettings |
단, 함수나 변수가 실행 파일 안에 포함되지 않았기 때문에, 사용하고자 하는 함수나 변수를 컴파일러나 링커에게 알려주어야 한다. 만약 Export에 대한 정보가 없다면 실행파일은 DLL의 함수를 Import 할 수 없게 된다. 그러므로 위와 같이 Export 하는 함수에 대한 정보가 DLL에 기록되어 있어야 한다.
DLL Binding
EXE 파일은 사용하고자 하는 DLL의 함수를 메모리에 같은 메모리상에 올리게 되는데 이때 IAT에는 실제 사용하고자 함수들의 주소가 오게 된다. 다시 말해, 파일에서 IAT는 실제 함수의 주소를 가리키고 있지 않다. 왜냐하면 사용하고자 하는 함수의 주소를 아직 알 수 없기 때문이다. 하지만 메모리에 올라오면서 PE 로더는 IAT에 사용하고자 하는 함수의 실제 주소를 올려주므로 우리는 아무런 의심 없이 사용할 수 있다. 하지만 이러한 작업은 프로그램의 초기화 시간을 지연시키므로 MS는 이러한 제약을 피할 수 있도록 하나의 기능을 제공한다. 바로 IAT에 함수의 주소를 기록하는 작업을 미리 수행하여 로딩 시의 속도 향상을 도모하도록 한다. 이 과정을 바로 DLL 바인딩이라고 하며, 바인드 된 실행 파일의 IAT는 실제 함수의 주소를 가리키고 있게 된다.
* Import Name Table Offset Data Name ============================================== 0xA3C 0x307C GetDriveTypeA 0xA40 0x308C ExitProcess --------------------------------------KERNEL32.dll 0xA48 0x309A MessageBoxA --------------------------------------USER32.dll * Import Address Table Offset RVA Data Value ============================================== 0xA50 0x3050 0x307C GetDriveTypeA 0xA54 0x3054 0x308C ExitProcess --------------------------------------KERNEL32.dll 0xA5C 0x305C 0x309A MessageBoxA --------------------------------------USER32.dll |
우선 위의 표는 일반적인 파일의 Import Section 정보로 INT와 IAT가 같은 곳을 가리키고 있는 것을 확인할 수 있다. INT의 첫 번째 함수인 GetDriveTypeA인 0x307C의 이름의 위치를 IAT에서도 똑같이 가리키고 있다. 메모리에 올라오면서 IAT에 실제 주소가 기록되어 변경된다.
* Import Name Table Offset Data Name ============================================== 0xA3C 0x307C GetDriveTypeA 0xA40 0x308C ExitProcess --------------------------------------KERNEL32.dll 0xA48 0x309A MessageBoxA --------------------------------------USER32.dll * Import Address Table Offset RVA Data Value ============================================== 0xA50 0x3050 0x6B826A00 0xA54 0x3054 0x6B827B30 --------------------------------------KERNEL32.dll 0xA5C 0x305C 0x6BAFFF90 --------------------------------------USER32.dll |
위의 표는 같은 파일에 바인딩을 실시한 후의 모습으로 INT는 기존과 동일한 모습을 하고 있지만 IAT의 경우 Data에 실제 함수의 주소가 위치한 것을 확인할 수 있다. 이처럼 바인딩을 하게 되면 실제 함수의 주소를 가지게 되는 것을 알 수 있다.
DLL Relocation
DLL 재배치에 대하여 알아보기 전에 먼저 ImageBase에 대하여 알아보자. ImageBase란 PE 구조에서 해당 PE 파일이 PE 로더에 의해 메모리에 로드될 때 로드시키고자 하는 메모리의 주소가 된다. 보통 EXE 파일의 경우 0x4000000이며 DLL 파일의 경우 0x10000000이다.
typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; …. DWORD AddressOfEntryPoint; …. DWORD ImageBase; …. |
하지만 위에서 말한 것과 같이 하나의 EXE 파일은 여러 라이브러리를 필요로 하는 경우가 일반적이기 때문에, 여러 DLL을 메모리에 올리고자 한다. 이 경우 DLL들의 ImageBase가 중첩된다면 하나의 메모리 주소에 여러 DLL이 존재할 수 없으므로 사용할 수 없게 된다. 다행히 DLL 재배치를 통해 원하는 ImageBase에 이미 다른 DLL이 올라와 있다면, 다른 주소에 맵핑될 수가 있다.
하지만 이러한 재배치 작업이 일어나면 PE 로더는 부차적인 작업을 수행해야 한다. DLL의 주소를 바꾸어 올리는 것뿐만 아니라, 해당 DLL의 Code Section의 일부 내용을 수정해야만 한다. 이에 대해 아래의 표를 보자. 아래의 표는 Relocation Section의 내용으로 하단 두 줄에 RVA가 있는 것을 확인할 수 있다. 일반적으로 이 주소가 가리키는 부분은 0x????????과 같은 4 Bytes의 주소를 나타내는 것으로 PE 구조에 기록되어 있는 ImageBase에 맞게 주소가 설정되어 있다. 하지만 ImageBase와 다른 곳에 로드되면 이 주소들은 ImageBase에 로드된 다른 DLL을 가리키게 되는 문제가 발생하므로 값을 수정해주어야 한다.
[+] Relocation Section -------------------- Base Address : 0x1000 Size of Block : 0x1069 (Num : 0x830) Type Value : 0x5708 --- RVA : 0x1708 (Offset : 0xB08) Type Value : 0x106C --- RVA : 0x106C (Offset : 0x46C) |
다행히 이러한 주소 값의 수정을 사용자가 직접 하나하나 하는 것이 아니라 PE 로더가 재배치 섹션을 확인하여 알아서 수정해준다. 하지만 여기서 몇 가지 문제점이 존재하게 된다. 우선 재배치 정보가 가리키고 있는 값들은 대개 어떤 주소에 관한 값으로, 이러한 값들이 대개 코드 섹션에 위치하고 있다는 것이다. 따라서 이 값을 PE 로더가 수정하기 위해선 해당 섹션에 Write 속성을 추가한 뒤 수정을 하고, 수정을 마치면 다시 원래의 속성으로 되돌려야 한다는 것이다. 이에 더해 위 예에서는 2개의 값만 나타냈지만, 실제로는 더 많은 경우가 많기 때문에 PE 로더는 그 많은 주소의 값들을 직접 찾아 수정해주어야 한다. 만약 하나의 EXE 파일에 여러 DLL에 대하여 이러한 작업을 수행해야 한다면, 프로그램을 실행하기 위한 초기화 시간이 길어질 수 있다.
DLL Delay Loading & DLL Forwarding
DLL Delay Loading
상기의 이유들로 초기화 시간이 길어질 수 있다는 것에 대하여 알 수 있었다. 사실 하드웨어의 성능이 상향된 요즘은 별 상관이 없지만, 윈도우는 이러한 초기화 시간을 줄이기 위한 또 다른 방안을 구비해놓았다. 바로 DLL 지연 로딩으로, 단어에서와 같이 DLL을 프로그램 실행 시에 로드하는 것이 아니라 지연하여 로딩하는 것이다.
지연 로딩은 암시적 로딩에서의 간편함과 명시적 로딩에서의 유연함, 이 두 장점을 취하고자 하는 방식으로 EXE 작성에서 DLL 링크 시에는 암시적인 방식으로, 실제 런타임에서 사용 할 때는 명시적인 방식으로 작동하도록 한 것이다. 쉽게 말해 프로그램을 실행 시에 메모리에 매핑되는 것이 아니라 해당 DLL의 Export 함수들 중 하나가 최초로 실행될 때 그 시점에 해당 DLL을 로드해서 가상 주소 공간에 매핑한다는 것이다.
DLL Forwarding
DLL의 Export Function Forwarding이란 Export하고자 하는 함수를, 그 기능을 대신하는 다른 DLL 내에 정의된 함수의 호출로 대체하는 것이다. 글로 설명하는 것보다는 직접 코드를 확인 것이 더 좋으므로 일반적인 경우의 DLL의 Export 함수의 주소를 확인해보자. 아래의 표와 같이 Export하는 함수의 주소로 이동을 하면 해당 함수의 내용이 존재하고 있는 것을 확인할 수 있다. 즉, 자신의 DLL 안에 해당 코드를 그대로 잘 가지고 있는 것이다.
text:5F923F4B mov edi, edi .text:5F923F4D push ebp .text:5F923F4E mov ebp, esp .text:5F923F50 sub esp, 1Ch .text:5F923F53 mov eax, [ebp+arg_0] .text:5F923F56 push ebx .text:5F923F57 push esi .text:5F923F58 push edi ; struct CApplnMgr * .text:5F923F59 mov edi, [ebp+arg_4] .text:5F923F5C mov [ebp+var_C], eax |
이번에는 DLL Forwarding이 적용된 DLL의 내용을 확인해보자. 위 표와는 다른 DLL이기는 하지만 Export 하는 함수의 주소로 이동하여 확인해보면 심히 코드가 짧다는 것을 알 수 있다. Export 하고자 하는 함수의 이름 "LpkEditControl"과 함께 0x1000261C를 호출하는 것을 확인할 수 있다.
.text:10002BC8 MemCode_LpkEditControl proc near ; DATA XREF: .rdata:off_1001E148 .text:10002BC8 ; .data:LpkEditControl .text:10002BC8 push offset aLpkeditcontr_0 ; "LpkEditControl" .text:10002BCD call sub_1000261C .text:10002BD2 jmp dword ptr [eax] |
0x1000261C에는 다시 아래와 같은 내용이 있으며, 10002634를 호출한 다음, 이전에 인자로 전달 받았던 Export하고자 하는 함수의 이름과 함께 GetProcAddress를 통해 주소를 구하고자 하는 것이다.
.text:1000261C call sub_10002634 .text:10002621 push [esp+lpProcName] ; lpProcName : LpkEditControl .text:10002625 push hModule ; hModule : lpk.dll .text:1000262B call ds:GetProcAddress .text:10002631 retn 4 |
해당 10002634를 따라가다 보면 시스템 디렉터리의 경로를 구한 뒤, 해당 Export 함수를 가진 대상(포워딩 대상)을 LoadLibarary API를 통해 로드하는 것을 확인할 수 있다. 이를 통해 해당 DLL이 로드되고 위의 과정에서와 같이 GetProcAddress를 통해 해당 함수가 로드되는 것이다.
.text:1000265B push esi ; uSize .text:1000265C push eax ; lpBuffer .text:1000265D call ds:GetSystemDirectoryA .text:10002663 lea eax, [ebp+Buffer] .text:10002669 push offset String2 ; "\\lpk.dll" .text:1000266E push eax ; lpString1 .text:1000266F call ds:lstrcatA .text:10002675 cmp hModule, 0 .text:1000267C pop esi .text:1000267D jnz short loc_10002691 .text:1000267F lea eax, [ebp+Buffer] .text:10002685 push eax ; lpLibFileName .text:10002686 call ds:LoadLibraryA ; lpk.dll |
DLL 포워딩이 그렇다면 어느 곳에 사용될 수 있을까? 필자는 악성코드에 관심이 많으므로 악성코드를 대상으로 설명하겠다. 악성코드는 문서의 형태로 존재할 수도 있고 실행파일의 형태로 존재할 수도 있다. 하지만 이 글의 취지에 맞게 DLL로 구성된 경우 악성코드 제작자는 매우 유용하게 DLL 포워딩을 사용할 수 있다. 악성코드 제작자 그 누구도 자신이 만든 애지중지한 파일이 누가 보아도 '악성'으로 보이고 싶지는 않을 것이다. 그렇기에 정상 파일인 것처럼 위장을 하게 되는데, DLL의 경우 실제 Export 하는 함수의 내용을 구현할 수 있어야 한다는 것이다. 물론 실제 DLL의 내용을 Ctrl+C/Ctrl+V를 통해 사용할 수는 있겠지만 이는 결코 좋은 방법이 아니다.
이 기능을 사용하면 해당 포워딩 설정을 실제 DLL 파일로 해놓으면 너무나 쉽게 정상적인 기능을 모두 구현할 수 있게 된다. 결국 정상적인 기능을 수행하면서, DllMain()에는 자신이 원하는 기능을 수행하도록 하면 이는 완벽한 위장이 된다. 물론 실제 시스템 DLL이 먼저 로드되는 상황이 발생되면 안되므로 DLL 로딩 순서를 변경하여 실행파일과 같은 디렉터리에 있는 악성 DLL을 먼저 로드하도록 하여 이를 로드시키면 모든 것은 끝이 난다.
Conclusion
단순하게 DLL을 이론만 공부했던 때에는 위의 기능들이 어떻게 사용되는지 별로 관심이 없었다. 하지만 실제로 악성 DLL을 분석해보고 난 뒤 너무나 부족한 지식을 가지고 있다고 다시 한 번 느끼게 되어 이렇게 기억하고자 정리해보았다. 이외에도 분석을 통해 어떠한 부분이 부족한지 더 자세히 알게 되어 너무나 유익하였다.
약간의 후기를 공유하자면 이론으로 알고 있던 내용이 어떻게 코드나 어셈블리어로 적용되는지 반드시 확인을 해보자. 이론으로는 자세히 알고 있더라도 그 부분을 무심코 지나칠 수 있다는 것을 뼈저리게 느끼게 되었다.
Reference
http://www.sck.pe.kr/c-cgi/whatisdll.htm
Windows 시스템 실행파일의 구조와 원리 [한빛미디어]
'Reversing > Theory' 카테고리의 다른 글
Assembly로 보는 코드, strcmp 문자열 비교 (0) | 2016.08.08 |
---|---|
WFP 무력화 (0) | 2016.06.21 |
PE구조의 이해 (0) | 2016.05.04 |
윈도우 후킹 원리 [PDF] (1) | 2016.04.23 |
윈도우 후킹 원리 (3) - Kernel [SSDT] (0) | 2016.04.23 |