[Reversing]DLL injection ( injector 스켈레톤 코드 분석 )
오늘 포스팅은 DLL injection 기법에서 injector.c 코드의 스켈레톤 코드를 분석하고 공부해보겠습니다.
IAT후킹때도 그렇고 inline후킹에서도 dll injection 기법을 사용하여 우리가 짠 후킹 코드를 프로그램에 집어넣어서 후킹을 시도했었습니다.
dll injection에서 들어가는 후킹 코드는 바뀌어도 인젝터의 형식은 거의 바뀌지 않기 때문에 inline후킹 실습에 앞서 살펴보겠습니다.
인젝터 코드의 흐름은 먼저 다음과 같습니다.
① CreateProcess로 일시 정지된 상태로 실행
② VirtualAllocEx로 DLL 경로 저장할 공간 확보
③ WriteProcessMemory로 경로 전달
④ GetProcAddress로 LoadLibraryA 주소 구함
⑤ CreateRemoteThread로 대상 프로세스에 DLL 삽입
⑥ ResumeThread로 정상 실행
스켈레톤 코드는 다음과 같습니다.
#include <windows.h>
#include <stdio.h>
int main(int argc, char *argv[]) {
if (argc != 3) {
printf("사용법: %s <대상 실행파일 경로> <삽입할 DLL 경로>\n", argv[0]);
return 1;
}
const char *targetExe = argv[1];
const char *dllPath = argv[2];
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
if (!CreateProcessA(
targetExe, NULL, NULL, NULL, FALSE,
CREATE_SUSPENDED, NULL, NULL,
&si, &pi)) {
printf("CreateProcess 실패: %lu\n", GetLastError());
return 1;
}
LPVOID remoteStr = VirtualAllocEx(pi.hProcess, NULL, MAX_PATH, MEM_COMMIT, PAGE_READWRITE);
if (!remoteStr) {
printf("VirtualAllocEx 실패\n");
TerminateProcess(pi.hProcess, 1);
return 1;
}
WriteProcessMemory(pi.hProcess, remoteStr, dllPath, strlen(dllPath) + 1, NULL);
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
LPVOID loadLibraryAddr = (LPVOID)GetProcAddress(hKernel32, "LoadLibraryA");
HANDLE hThread = CreateRemoteThread(
pi.hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)loadLibraryAddr,
remoteStr, 0, NULL
);
if (!hThread) {
printf("CreateRemoteThread 실패\n");
TerminateProcess(pi.hProcess, 1);
return 1;
}
WaitForSingleObject(hThread, INFINITE);
ResumeThread(pi.hThread);
CloseHandle(hThread);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
printf("DLL 삽입 완료\n");
return 0;
}
가장 처음 부분입니다.
int main(int argc, char *argv[]){
if (argc != 3){
printf("사용법 : %s <대상 실행파일 경로><삽입할 dll 경로\n"
, argv[0])
return 1;
}
const char *targetExe = argv[1];
const char *dllpath = argc[2];
인자가 3개가 아닐경우 함수를 종료합니다.
또한 인자로 받은 타깃프로세스와 dll경로를 문자열 포인터 형식으로 받은 뒤 const char*로 선언합니다.
STARTUPINFO si = {{sizeof(si)};
PROCESS_INFORMATION pi;
STARTUPINFO와 PROCESS_INFORMATION은 windows.h에 정의된 자료형이며 CreateProcess를 쓸 때 필수적입니다.

STARTUPINFO 구조체이고 구조체의 크기인 si만 설정을 해주었습니다.(*나머지는 다 0으로 초기화)
주로 프로세스 생성 시 설정할 정보들이 담깁니다.

PROCESS_INFORMATION은 CreateProcess가 실행된 뒤 정보들이 채워집니다.
생성된 프로세스와 핸들, PID 등의 정보를 담고 있습니다
if (!CreateProcessA(targetExe,NULL, NULL, NULL, FALSE,
CREATE_SUSPENDED, NULL, NULL, &si, &pi)){
printf("CreateProcess 실패 : %lu\n", GetLastErropr())
return 1;
}
다음 코드입니다.
해당 부분은 아까 봤던 순서중 ① CreateProcess로 일시 정지된 상태로 실행 부분입니다.


첫번째 인자에 Target.exe가 들어갔고 나머지는 NULL, si,pi가 들어갔습니다 .특히 신경써야되는 부분은 CREATE_SUSPENDED입니다. 해당 인자값이 없으면 프로세스가 정지가 안된상태로 실행되는데, 그럼 후킹 대상함수들이 후킹하기 전 호출 될 수 있다는 문제점이 발생합니다.
따라서 안정적인 DLL인젝션을 위해 꼭 필요한 부분입니다.
다음 코드입니다.
LPVOID remoteStr = VirtualAllocEx(pi.hProcess, NULL,MAX_PATH,
MEM_COMMIT, PAGE_READWRITE)
if(!remoteStr){
printf("VirtualAllocEx 실패 \n");
TerminateProcess(pi.hProcess, 1);
return 1;
}
(*LPVOID형식은 VOID 형식과 동일합니다)
dll경로 문자열을 넣기 위해 프로세스 메모리 공간을 확보하는 ② VirtualAllocEx로 DLL 경로 저장할 공간 확보하는 부분입니다.
VirtualAlloc은 저번에 inline 스켈레톤 코드에서 나왔었는데 Ex가 추가로 붙은 형태입니다. Ex는 external process의 약자입니다.

VirtualAllocEx는 다음과 같은 형식입니다.
여기서 dwSize값으로 MAX_PATH값이 들어갔는데, MAX_PATH는 windows.h에서 정의된값으로 260바이트의 공간입니다.
실패시 TerminateProcess로 깔끔하게 예외처리를 끝냅니다.
WriteProcessMemory(pi.hProcess, remoteStr, dllpath, strlen(dllpath)+1, NULL);
③ WriteProcessMemory로 경로 전달부분을 수행하는 코드입니다.

pi.hProcess의 remoteStr에 dllpath의내용을 strlen(dllpath)+1(NULL문자)만큼 작성합니다.
타겟 프로세스에서 우리가 인젝션하고자 하는 dll 의 주소를 실행하기 위해 필요합니다.
HMODULE hKernal32 = GetModuleHandleA("kernel32.dll");
LPVOID loadLibraryAddr = (LPVOID)GetProcAddress(hKernel32, "LoadLibraryA");
④ GetProcAddress로 LoadLibraryA 주소 구함에 해당하는 코드입니다
1. 현재 인젝터 프로세스에서 kernel32.dll 이라는 모듈의 베이스 주소를 구하고
2. 위에서 구한 kernel32.dll의 핸들(hkernel32)에서 LoadLibrary함수의 실제 주소를 가져옵니다.

HMODULE은 window에서 제공하는 자료형입니다. void*와 비슷하지만 의미상 로드된 모듈의 베이스주소를 가리킬 때 사용합니다.
GetProcessAddress(hKernel32, "LoadLibrary"에서는 kernel32.dll의 Export Table을 검색해서 LoadLibrary의 정확한 주소를 구합니다.
이런 과정을 거치는 이유는 타겟 프로세스에 dll을 로드하려면 그 프로세스가 직접 LoadLibraryA를 호출해야하기 때문입니다.
HANDLE hThread = CreateRemoteThread(
pi.hProcess,NULL, 0,
(LPTHREAD_START_ROUTINE)loadLibraryAddr,
remoteStr,0,NULL
);
⑤ CreateRemoteThread로 대상 프로세스에 DLL 삽입하는 코드입니다.

(LPTHREAD_START_ROUTINE)도 함수 포인터 타입입니다. 새로 만들 쓰레드의 진입점 함수(시작위치)를 지정할 때 사용합니다.
해당 코드는 타겟 프로세스에서 LoadLibraryA를 통해 경로 문자열의 DLL을 호출 시키는 목적을 가집니다. DLL 인젝션에서 제일 핵심코드입니다..!
if(!hThread){
printf("CreateRemoteThread 실패\n");
TerminateProcess(pi.hProcess, 1);
return 1;
}
예외처리 코드입니다.
WaitForSingleObject(hThread, INFINITE);
우리가 생성한 원격 쓰레드(hThread)가 종료될 때까지 무한정 대기합니다.
ResumeThread(pi.hThread)
⑥ ResumeThread로 정상 실행하는 코드입니다.
저희가 아까 메인쓰레드를 CreateProcessA로 실행할 때 일시정지된 상태(*CREATE_SUSPENDED)로 설정했기 때문에 ResumeThread로 프로세스를 실행시킵니다.
CloseHandle(hThread);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
인젝션이 끝난 후 자원 정리를 하는 단계입니다.
CloseHandle()로 핸들을 닫아주지 않으면 성능이 저하될 수 있습니다.
이상으로 인젝터 코드에 대한 분석을 마치겠습니다.
주석 달린 전체 코드입니다.
#include <windows.h>
#include <stdio.h>
int main(int argc, char *argv[]){
if (argc != 3){
printf("사용법 : %s <대상 실행파일 경로><삽입할 dll 경로\n"
, argv[0])
return 1;
}
//인자로 받은 내용을 const char*로 선언
const char *targetExe = argv[1];
const char *dllpath = argv[2];
//STARTUPINFO에서 si.cb 제외한 인자 0으로 초기화
STARTUPINFO si = {sizeof(si)};
//PROCESS_INFORMATION 자료형 초기화
PROCESS_INFORMATION pi;
//CreateProcessA을 일시정지된 상태로 실행(CREATE_SUSPENDED)
if (!CreateProcessA(targetExe,NULL, NULL, NULL, FALSE,
CREATE_SUSPENDED, NULL, NULL, &si, &pi)){
printf("CreateProcess 실패 : %lu\n", GetLastErropr())
return 1; //실패할경우 종료
}
//공간 동적 할당, MAX_PATH는 windows.h에서 정한 260바이트
LPVOID remoteStr = VirtualAllocEx(pi.hProcess, NULL,MAX_PATH,
MEM_COMMIT, PAGE_READWRITE)
//물리 메모리만 할당, 읽기쓰기권한 허용
if(!remoteStr){
printf("VirtualAllocEx 실패 \n");
//실패시 프로세스 종료(깔끔한 예외처리)
TerminateProcess(pi.hProcess, 1);
return 1;
}
//타겟 프로세스의 remoteStr에 dllpath의 내요을 strlen(dllpath)+1 (NULL문자추가) 만큼 넣음
WriteProcessMemory(pi.hProcess, remoteStr,dllpath,strlen(dllpath)+1,NULL);
//kernel32.dll의 베이스 주소를 구함
HMODULE hKernal32 = GetModuleHandleA("kernel32.dll");
//구한 베이스 주소에서 LoadLibraryA의 주소를 가져옴
LPVOID loadLibraryAddr = (LPVOID)GetProcAddress(hKernel32, "LoadLibraryA");
//타겟 프로세스에서 dll을 로드하려면 그 프로세스가 직접 LoadLibrary를 호출해야하기 때문
//CreateRemoteThread를 통해 LoadLibrary호출, 인자는 remoteStr
HANDLE hThread = CreateRemoteThread(
pi.hProcess,NULL, 0,
(LPTHREAD_START_ROUTINE)loadLibraryAddr,
remoteStr,0,NULL
);
//예외처리
if(!hThread){
printf("CreateRemoteThread 실패\n");
TerminateProcess(pi.hProcess, 1);
return 1;
}
//hThread가 종료될때까지 무한 대기
WaitForSingleObject(hThread, INFINITE);
//멈춰놨던 매인 쓰레드 실행
ResumeThread(pi.hThread)
//핸들 정리(자원 정리)
CloseHandle(hThread);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}
이런류의 코드 분석을 많이 했더니 이제는 거부감이 많이 사라진 것 같습니다..
해당 코드를 exe파일로 컴파일해보겠습니다.
저번에 iat후킹할 때 만들어둔 파일들로 한번 잘 되는지 실행해보겠습니다. 저희가 만든 c코드에는 인자를 입력하는 부분이 없기 때문에 cmd로 인자를 넣어서 실행해줘야합니다.
.\injector.exe (exe파일 경로) (dll주소)
이렇게 명령어를 내려주시면 됩니다.

근데 한번 실행하고나니까 악성프로그램이라고 생각해서 그런가 바로 삭제되었습니다..ㅋㅋ 신기하네요.

어쨋든 잘 실행되는것도 확인을 했으니 오늘 포스팅은 여기서 마치도록 하겠습니다.
다음포스팅은 inline후킹 실습을 마저 해보도록 하겠습니다. 개인 사설로 poirier vs holloway 3차전 리뷰나 해보고싶네요.
이상으로 포스팅을 마치겠습니다. 읽어주셔거 감사합니다
