주제
DLL Injection 기법은 다른 프로세스에 침투하는 가장 간단하고 강력한 방법입니다. DLL 인젝션의 원리와 인젝션 코드를 직접 작성하는 방법에 대해 알아봅시다.
사전 지식
본 글을 이해하기 위해 필요한 사전 지식입니다.
- ⭐ Windows 운영 체제가 관리하는 프로세스, 스레드, 메모리에 대한 이해
- DLL에 대한 기본적인 이해
(프로세스들이 DLL을 공유하는 메커니즘(메모리 관점에서의), DLL의 장단점, DLL 만드는 방법) - C++ 프로그래밍 언어
- Win32 API 프로그래밍
(MSDN에서 원하는 함수의 사용법을 읽고 이해할 수 있는 정도)
전체적인 과정
아래는 DLL 인젝션의 과정입니다. (블로그 글을 다 읽고 스스로 정리해 보세요!)
- 대상 프로세스의 핸들 구하기
- 대상 프로세스의 메모리에 영역 확보하기
- 확보한 메모리 영역에 인젝션 할 DLL 경로 써주기
- LoadLibraryW() 함수의 주소 구하기
- 대상 프로세스에 원격 스레드로 LoadLibraryW() 함수 실행하기
DLL Injection
DLL 인젝션을 통해 특정 프로세스가 특정 DLL을 로드하도록 강제할 수 있습니다.
DLL 라이브러리를 로드할 때 쓰이는 LoadLibrary() 함수를 대상 프로세스가 실행하도록 하여 DLL을 로드시키는 원리입니다.
LoadLibrary() 함수를 통해 로드된 DLL은, 대상 프로세스의 메모리에 대한 정당한 접근 권한을 가지기 때문에, DLL 인젝션을 통해 대상 프로세스 안에서 우리가 원하는 모든 작업을 할 수 있습니다. (대상 프로세스의 기능 개선 및 버그 패치, 대상 프로세스의 메모리 조작, 후킹 작업 등등)

DLL 인젝션 기법을 수행하는 방법은 세 가지가 있습니다.
- 원격 스레드 생성(CreateRemoteThread() API)
- 레지스트리 이용(AppInit_DLLs 값)
- 메시지 후킹(SetWindowsHookEx() API) - 윈도우 이벤트 메시지 후킹 기법(참고)
이번 글에서는 첫 번째 방법인 CreateRemoteThread() 함수를 이용하는 DLL 인젝션 기법에 대해 알아보겠습니다.
실습
DLL 인젝션 기법을 활용하여 우리의 영원한 희생자(?)인 메모장에 DLL을 인젝션 하고, 인젝션 된 DLL이 웹에서 파일을 다운로드하는 작업을 진행하도록 만들어보겠습니다.
소스 코드
먼저 우리가 인젝션할 DLL인 myhack.dll의 소스 코드인 myhack.cpp입니다.
#include <tchar.h>
#include <windows.h>
#include <urlmon.h>
#pragma comment(lib, "urlmon.lib")
#define DEF_FILE_URL _T("https://www.naver.com/index.html")
#define DEF_FILE_RETRIEVE_PATH _T("C:\\Users\\minmoong\\Desktop\\index.html")
DWORD WINAPI ThreadProc(LPVOID lParam);
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
// 인젝션되면 출력
OutputDebugString(_T("[myhack.dll] Injection succeed."));
// 파일 다운로드 작업은 별도의 스레드를 생성하여 진행합니다.
// 대상 프로세스의 실행 흐름을 방해하지 않기 위함입니다.
HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
CloseHandle(hThread);
break;
}
return TRUE;
}
DWORD WINAPI ThreadProc(LPVOID lParam) {
// 파일 다운로드
URLDownloadToFile(
NULL, // ActiveX가 아니면 NULL
DEF_FILE_URL, // 다운로드할 파일의 URL
DEF_FILE_RETRIEVE_PATH, // 다운로드받을 경로
0, // 예약된 인자. 무조건 0으로 두기
NULL // 콜백 함수
);
return 0;
}
위 코드를 간단히 설명하겠습니다.
LoadLibrary() 함수 호출에 의해 myhack.dll이 프로세스에 로드되면, DllMain() 함수가 자동으로 호출됩니다.
호출된 이유를 나타내는 ul_reason_for_call 인자가 존재하는데, 그 값이 DLL_PROCESS_ATTACH(프로세스에 load 되었을 때)이면, 다음 두 작업을 진행합니다.
1. 인젝션에 성공했다는 의미로 문자열을 출력 (OutputDebugString() 함수의 출력은 DebugView 프로그램에서 확인할 수 있습니다.)
2. 네이버의 index.html 파일을 바탕화면에 다운로드
myhack.dll은 네이버의 메인 페이지인 index.html 파일을 다운로드하는 작업을 진행합니다.
다음은 DLL을 대상 프로세스에 인젝션 해주는 프로그램인 InjectDll.exe의 소스코드, InjectDll.cpp입니다.
DLL 인젝션 기법의 하이라이트라고 할 수 있는 소스 코드입니다.
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
BOOL InjectDll(DWORD dwPid, LPCTSTR szDllPath);
int _tmain(int argc, TCHAR* argv[]) {
if (argc != 3) {
_tprintf(_T("Usage: %s [PID] [PATH TO DLL] \n"), argv[0]);
return 1;
}
if (InjectDll((DWORD)_tstol(argv[1]), argv[2])) {
_tprintf(_T("Inject to pid %s succeed. \n"), argv[1]);
return 1;
}
else {
_tprintf(_T("Inject to pid %s failed. \n"), argv[1]);
return 1;
}
return 0;
}
BOOL InjectDll(DWORD dwPid, LPCTSTR szDllPath) {
DWORD dwBufSize = (_tcslen(szDllPath) + 1) * sizeof(TCHAR);
// 1. 대상 프로세스의 핸들 구하기
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (hProcess == NULL) {
_tprintf(_T("OpenProcess failed. Error code: %d \n"), GetLastError());
return FALSE;
}
// 2. 대상 프로세스 메모리에 영역 확보하기
LPVOID pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);
// 3. 확보한 메모리 영역에 인젝션 할 DLL 경로 써주기
WriteProcessMemory(hProcess, pRemoteBuf, szDllPath, dwBufSize, NULL);
// 4. LoadLibraryW() 함수의 주소 구하기
HMODULE hMod = GetModuleHandle(_T("kernel32.dll"));
LPTHREAD_START_ROUTINE pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");
// 5. 대상 프로세스에 원격 스레드로 LoadLibraryW() 함수 실행하기
HANDLE hThread = CreateRemoteThread(
hProcess, // 대상 프로세스의 핸들
NULL, // 스레드 보안 속성
0, // 스택 크기 (0: 기본 크기)
pThreadProc, // 프로시저
pRemoteBuf, // 프로시저에 전달될 인자
0, // 플래그
NULL // 스레드의 아이디를 받기 위한 포인터
);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hProcess);
CloseHandle(hThread);
return TRUE;
}
코드가 다소 길고 복잡해 보일 수 있지만, DLL 인젝션의 과정을 이해한다면 어렵지 않습니다.
InjectDll() 함수에 이 소스 코드의 핵심이 들어있습니다. 단계별로 하나씩 설명해 보도록 하겠습니다.
모르는 함수가 있거나 더 자세히 알고 싶은 함수가 있다면 MSDN을 참고하시기 바랍니다.
1. 대상 프로세스의 핸들 구하기 (OpenProcess() 함수)
// 1. 대상 프로세스의 핸들 구하기
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (hProcess == NULL) {
_tprintf(_T("OpenProcess failed. Error code: %d \n"), GetLastError());
return FALSE;
}
프로그램을 실행하기 전에 작업 관리자로 인젝션 할 프로세스의 PID를 직접 알아내고, 프로그램을 실행할 때 PID를 인자로 넘겨줍니다. (_tmain() 함수 참고)
이 PID를 사용하여 PROCESS_ALL_ACCESS 권한으로 프로세스의 핸들을 얻어옵니다.
이 대상 프로세스의 핸들값은 다음 작업에 필요합니다.
1. 대상 프로세스 내부에 메모리 영역을 확보하고 그 메모리에 값을 쓸 때
2. CreateRemoteThread() 함수를 호출할 때
2. 대상 프로세스 메모리에 영역 확보하기 (VirtualAllocEx() 함수)
// 2. 대상 프로세스 메모리에 영역 확보하기
LPVOID pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);
대상 프로세스의 메모리에 영역을 확보합니다. 이 메모리 영역에 우리가 인젝션 할 DLL의 경로가 쓰일 것입니다.
아까 대상 프로세스가 LoadLibrary() 함수를 호출하도록 해서 DLL을 로드시키는 원리라고 했습니다.
LoadLibrary() 함수의 인자에 로드할 DLL의 경로를 넘겨줘야 하는데, 그 경로를 대상 프로세스의 메모리 영역에 쓰고 있는 것입니다.
3. 확보한 메모리 영역에 인젝션 할 DLL 경로 써주기 (WriteProcessMemory() 함수)
// 3. 확보한 메모리 영역에 인젝션 할 DLL 경로 써주기
WriteProcessMemory(hProcess, pRemoteBuf, szDllPath, dwBufSize, NULL);
위에서 확보한 메모리 영역에 DLL의 경로를 써줍니다. 이 경로도 PID와 마찬가지로 프로그램을 실행할 때 전달받은 인자입니다.
4. LoadLibraryW() 함수의 주소 구하기 (GetProcAddress() 함수)
// 4. LoadLibraryW() 함수의 주소 구하기
HMODULE hMod = GetModuleHandle(_T("kernel32.dll"));
LPTHREAD_START_ROUTINE pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");
이제 LoadLibraryW() 함수의 주소를 구합니다. (LoadLibraryW() 함수는 LoadLibrary() 함수의 유니코드 문자열 버전입니다.)
<여기서 잠깐!>
조금 이상한 점이 있습니다. 코드를 잘 보면 대상 프로세스에서 로드된 kernel32.dll의 LoadLibraryW() 함수의 주소를 구하는 것이 아니라, InjectDll.exe 프로세스에서 로드된 kernel32.dll의 LoadLibraryW() 함수의 주소를 구하고 있습니다.
"대상 프로세스에 있는 LoadLibraryW() 함수의 주소를 구해야 하지 않나요?"
상관없습니다. 왜냐하면 DLL은 프로세스끼리 공유를 하고 있기 때문에, kernel32.dll에서 export 하는 함수인 LoadLibraryW() 함수의 주소값은 A.exe 프로세스에서나, B.exe 프로세스에서나 동일하기 때문입니다.
따라서 대상 프로세스에 로드된 kernel32.dll에서 LoadLibraryW() 함수의 주소를 얻든, InjectDll.exe 프로세스에 로드된 kernel32.dll에서 LoadLibraryW() 함수의 주소를 얻든, 그 주소는 똑같습니다.
하지만 그렇게 대답을 해버리면 다음과 같은 질문도 가능하지요.
"DLL Relocation이 발생해서, A.exe 프로세스에 로드된 kernel32.dll의 주소와 B.exe 프로세스에 로드된 kernel32.dll의 주소가 다를 수도 있잖아요!"
날카로운 질문입니다. 다음과 같은 상황에 대한 얘기군요.

위 그림을 보면 A 프로세스에서 로드된 kernel32.dll의 주소와 B 프로세스에서 로드된 kernel32.dll의 주소가 다릅니다. DLL Relocation이 발생했기 때문이죠.
하지만 일반적으로는 이런 일은 일어나지 않습니다. Windows 운영체제에서 kernel32.dll은 프로세스마다 같은 주소에 로드됩니다.
실제로 마이크로소프트에서는 Windows의 핵심 DLL 파일들의 ImageBase들을 서로 겹치지 않게 정리해 두었습니다.
따라서 Windows의 핵심 DLL 파일들끼리는 DLL Relocation이 발생하지 않기 때문에 어느 프로세스에서든 로드된 주소가 같습니다.
5. 대상 프로세스에 원격 스레드로 LoadLibraryW() 함수 실행하기 (CreateRemoteThread() 함수)
// 5. 대상 프로세스에 원격 스레드로 LoadLibraryW() 함수 실행하기
HANDLE hThread = CreateRemoteThread(
hProcess, // 대상 프로세스의 핸들
NULL, // 스레드 보안 속성
0, // 스택 크기 (0: 기본 크기)
pThreadProc, // 프로시저
pRemoteBuf, // 프로시저에 전달될 인자
0, // 플래그
NULL // 스레드의 아이디를 받기 위한 포인터
);
WaitForSingleObject(hThread, INFINITE);
이제 CreateRemoteThread() 함수를 사용해 LoadLibraryW() 함수를 대상 프로세스에서 실행시킵니다. LoadLibraryW() 함수에 전달될 인자는 CreateRemoteThread() 함수의 인자로 전달해줘야 합니다.
pRemoteBuf 변수에 우리가 써놓은 DLL 경로 문자열의 주소값이 담겨 있으므로 이것을 인자로 전달합니다.
그러면 대상 프로세스에서 다음과 같이 실행될 것입니다.
LoadLibraryW("DLL의 경로");
해당 작업이 완료될 때까지 기다리기 위해 WaitForSingleObject() 함수를 사용해 줍니다.
그렇게 대상 프로세스에 DLL 인젝션이 완료됩니다.
<여기서 잠깐!>
아래와 같은 의문이 들 수도 있습니다.
"CreateRemoteThread() 함수를 통해 LoadLibrary() 함수를 실행하는데, 그럼 그냥 DLL의 경로를 문자열로 LoadLibrary()에 넘겨주면 되지, 왜 굳이 경로를 저장할 메모리 영역을 따로 파서 그 주소를 넘겨주는 거죠?"
아래와 같이 코딩해도 되지 않을까라는 질문입니다.
HANDLE hThread = CreateRemoteThread(
hProcess, // 대상 프로세스의 핸들
NULL, // 스레드 보안 속성
0, // 스택 크기 (0: 기본 크기)
pThreadProc, // 프로시저
_T("C:\\Users\\minmoong\\Desktop\\myhack.dll"), // 프로시저에 전달될 인자
0, // 플래그
NULL // 스레드의 아이디를 받기 위한 포인터
);
프로시저(예제에서는 LoadLibrary() 함수를 의미함)에 전달될 인자를 굳이 따로 메모리 영역을 파서 전달해야 하냐는 거죠.
위 코드에서 _T("DLL 주소") 문자열은 코드를 컴파일하면 결국 메모리 주소로 바뀌게 됩니다. 해당 메모리 주소에 문자열이 담기게 되는 것이고요. 그럼 그 메모리 주소를 인자로 전달하여 원격으로 LoadLibrary() 함수를 호출할 텐데, 대상 프로세스에서는 그 메모리 주소가 무엇인지 알지 못합니다. 대상 프로세스 입장에서는 다른 프로세스의 메모리 주소를 전달받았으니 말이죠.
따라서 대상 프로세스에서 DLL의 경로를 쓸 메모리 공간을 확보하고 그 주소를 LoadLibrary() 함수에 전달해야만 합니다. 그러면 대상 프로세스가 자신의 메모리 영역에 쓰인 DLL 경로를 참조하여 LoadLibrary() 함수를 정상적으로 실행합니다.
실행
이제 소스 코드를 컴파일하고 직접 실행해 보도록 하겠습니다.
DLL 인젝션에 성공했다면 myhack.dll에서 OutputDebugString() 함수로 출력하는 내용을 DebugView를 통해 확인할 수 있으며, 바탕화면에 네이버의 메인 페이지인 index.html이 다운로드되어 있을 것입니다.
우선 메모장을 실행하고, Process Explorer(또는 작업 관리자)로 메모장의 PID를 확인해 줍니다.

그리고 인젝션 할 DLL의 절대경로도 확인합니다.

그다음 cmd를 켜서 InjectDll.exe를 실행합니다. (PID와 DLL의 경로를 인자로 전달하기)

인젝션에 성공했다는 메시지가 뜹니다.
그럼 myhack.dll에서 OutputDebugString() 함수로 출력하는 내용을 확인해 보겠습니다.

내용이 잘 뜨는 것을 알 수 있습니다.
그러면 index.html도 다운로드가 되었는지 확인해 보겠습니다.


다운로드가 잘 된 것을 확인할 수 있습니다.
마무리
지금까지 DLL 인젝션 기법에 대해 알아보았습니다. DLL 인젝션 과정을 스스로 정리해 보세요. DLL 인젝션의 과정을 메모장에 적고, 각 과정마다 필요한 함수가 무엇이었는지 떠올려보세요. 함수의 구체적인 사용법까지 기억할 필요는 없습니다. MSDN을 참고하면 되니까요. 분명 큰 실력 향상이 따를 것입니다.
참고
- 리버싱 핵심 원리 도서 (저자 이승원)
- MSDN
- 다이어그램 - draw.io
'정보보안 > 리버싱' 카테고리의 다른 글
| 추억의 게임 렙업만이살길2 리버싱 일기 - 2편 (완) (4) | 2025.06.03 |
|---|---|
| 추억의 게임 렙업만이살길2 리버싱 일기 - 1편 (1) | 2025.06.01 |
| 윈도우 이벤트 메시지 후킹 기법 (0) | 2024.02.06 |
| 인라인 패치 기법 (2) | 2024.02.04 |
| PE 파일 분석을 위한 C++ 프로그램 제작 (0) | 2024.02.01 |