다른 프로세스에 침투해 뭔가를 하려 할 때 흔히 쓰이는 것이 dll injection이죠.

(게임)핵에 많이 쓰입니다. 또는 원하는 작업을 기존 다른 프로세스에 숨어서 하려 할때도 쓰이죠.

지금은 어떤지 몰라도, 스타 맵핵만 보더라도 보통 load.exe와 xxx.dll로 구성돼 있고,

load.exe가 broodwar 프로세스를 찾아서 그곳에 맵핵 dll을 injection하는 형태가 대부분이었습니다.

이 포스트에선 dll injection을 user 모드에서 막아보겠습니다.

(기술적으로 전혀 어렵지 않으므로 스크롤압박도 넘겨봅시다^^)

일단 dll injection에 대해 알아야겠죠?





windows(아마도98이후)는 가상메모리를 지원하는 운영체제입니다.

즉 프로세스는 32bit, 4GB의 메모리를 혼자 쓰고 있다고 가정해도 됩니다.

다시말하면 starcraft.exe가 0x400000 ~ 0x600000 의 메모리를 쓸 때

동시에 다른 프로세스인 iexplore.exe도 0x400000 ~ 0x600000을 쓸 수 있고, 아무 문제 없다는 뜻입니다.

(궁금하시면 가상메모리, 세그멘테이션등에 대해 찾아보시면 됩니다.)

원칙적으로 프로세스는 다른 프로세스 메모리에 액세스 할 수 없습니다.
불가능하다기보다, 다른 프로세스를 "걱정하지 않고 활동 할 수 있게" 되어있습니다.

안정성때문이란 뜻이죠.

호랑이 담배먹던 시절엔 프로세스가 메모리를 액세스 할때 혹시 다른 프로세스가 쓰고있는 영역이 아닌지 걱정해야 할 때가 있었습니다.

이건 제가 태어나기 전 이야기이므로 중요치않습니다.
(라는건 뻥이고, 저나 다른 사람들이 겜위자드나 pctools로 삼국지, 은영전4 능력치 패치하던 시절 이야기입니다.)

아무튼 지금은 starcraft.exe의 미니맵의 안개(fog)가 껴서 안보이는 부분을 관리하는 메모리에

단순히 memcpy로 덮어쓸 순 없는 상황입니다.

위에 말했지만 이건 '보안'때문이라기보다 '안정성'때문입니다.

즉 os는 "원한다면" 다른 프로세스의 메모리에 write해줄 수 있게 해줍니다. api를 통해서 말이죠.

아래처럼요.

사용자 삽입 이미지



이렇게 다른 프로세스에 write도 가능하고, 심지어 다른 프로세스에 allocation(메모리할당)도 가능합니다.

아, 그리고 다른 프로세스에 thread를 생성하는 일도 가능하죠.

이것들을 조합해서 현재 널리 쓰이는, 다른 프로세스에 기본적인 dll 로드가 가능합니다.

jeffrey richter씨가 선보인 방식으로 말이죠.



여기서 잠깐!






그럼 너무도 흔해진 dll injection 코드의 개념은 이렇습니다.

대상 프로세스의 빈 메모리에 다음 코드를 write합니다.

LoadLibraryA( "mydll.dll" );

그리고 그곳에 thread를 생성합니다.

그럼 타겟 프로세스는 mydll.dll을 로드하게 되고 그 시점에서 내 dll은

대상 프로세스 내에서 실행되며 dllmain에서 원하는 짓거리를 맘껏 할 수 있습니다.


코드의 자세한 내용은 다른 많은 포스트를 보시면 될 것 같습니다. 굳이 여기서 injection 코드까지 설명할 필욘 없겠죠?


dll injection을 정리해보면

1. 타겟 프로세스에 memory를 alloc한다.
2. alloc한 메모리에 LoadLibrary()코드와 이에 필요한 문자열, 함수 주소등을 write한다.
3. 새로운 thread를 write한 코드에 생성한다.  



이게 dll injection 과정입니다.

사용자 삽입 이미지


사용자 삽입 이미지

미스터 앤더슨,
원하든 원치않든, 내 dll을 로드하게 될거요.

사용자 삽입 이미지

FreeLibrary()당한 bug.dll?





ProcessControl에 넣어놓은 기능으로, dll injection 을 해보겠습니다.
(ProcessControl.dll은 제가 기존에 포스팅(http://jumpzero.tistory.com/3)한 process관련 기능함수들을 만들어둔 dll입니다.)

계산기를 띄우고 여기에 testloaded.dll을 inject 해보죠.

testloaded.dll은 테스트용으로 만든 dll로, 단순히 DllMain에서 MessageBoxA를 띄웁니다.

그리고 testloader.exe는 ProcessControl.dll의 injection 기능을 이용, dll injection을 실행해주는 껍데기구요.


사용법은?

사용자 삽입 이미지

라고 하는군요. 그냥 testloader를 실행하면 안내 메시지가 나오게 해놨습니다.


그럼 계산기에 이 testloaded.dll을 injection하면 어떻게 될까요?

계산기에서 MessageBox를 띄우게 될겁니다.^^

자 해보죠.

계산기를 띄우고

사용자 삽입 이미지


콘솔에서 testloader.exe와 다른 dll들이 위치한데로 가서 아래 커맨드를 실행합니다.

c:\..>testloader.exe calc.exe "c:\documents and settings\....\testloaded.dll" processcontrol.dll
( inject 되는 dll은 full path 를 써주거나, 대상 프로세스의 상대path를 써줘야합니다.
  LoadLibraryA하는 놈은 대상 프로세스이므로, 대상 프로세스의 환경에서 로드가 되게 해야하기 때문이죠 )


사용자 삽입 이미지


잘 되네요.







서론이 길었네요.

본론으로 들어가서, 그럼 어느 프로세스를 보호하기 위해, 이걸 막으려면 어떤 방법이 있을까요?


첫번째로, 커널에서 막으면 됩니다.

ring0는, (커널모드, 이하 r0) 수퍼맨입니다.

injection 과정에서 쓰이는 api중 하나(또는 조합)를 r0에서 막으면 끝납니다.

ㅇVirtualAllocEx   -> NtAllocateVirtualMemory
ㅇWriteProcessMemory  -> NtWriteProcessMemory
ㅇCreateRemoteThread  -> NtCreateThread


위를 후킹해서 보호하려는 프로세스에 다른 프로세스가 이짓을 하려 할 때 막으면 됩니다.

쉽죠?



두번째로, user모드에서 막는걸 이 포스트에서 보여주려고 합니다.

내 프로세스에 누가 dll을 로드시켰다면? 이 과정에서 내가 알아챌 수 있는 방법은 뭐가 있을까요?

여러가지가 있지만 제가 보여줄 코드에서는 다음 방법으로 감지했습니다.

ㅇ새로운 thread가 생성되면 로드 된 모든 dll에 DLL_THREAD_ATTACH notification이 옵니다.

ㅇthread의 시작 위치는 (시스템별로) 모든 프로세스에게 항상 같습니다.


DLL_THREAD_ATTACH가 왔을 때 내가 생성한 thread인지 검사하고,

thread의 시작 위치를 후킹해서, 내가 생성한 thread인지 검사하면

injection을 막을 수 있습니다.

둘중 하나만 사용해도 충분히 막습니다.

그렇지만 둘다 구현해보겠습니다.


단순히 막기만 하면 안되고, 내가 생성한 thread 인지 봐야겠죠?

그래서 CreateRemoteThread를 앞뒤로 후킹해야합니다.


CreateRemoteThread 시작부분을 스티븐후킹박사.
CreateRemoteThread 뒷부분도 후킹.

ring3에서 thread가 시작되는 부분을 후킹.(kernel32의 특정 위치가 됩니다.)


CreateRemoteThread 시작부분에 설치된 훅루틴에선 Thread의 start address를 구하고 저장합니다.

hThread = CreateRemoteThread( hProcess,
        NULL,
        0,
        (LPTHREAD_START_ROUTINE)funcThread, ← 이 주소를 저장합니다.
        NULL,
        CREATE_SUSPENDED,
        &dwThreadId );

문제는 보호하려는 프로세스가 자체적으로 생성한 thread는 허용시켜줘야하기 때문에, hProcess가 -1이면 통과시켜줍니다.

(CreateThread는 내부적으로 CreateRemoteThread를 wrapping 하고 있습니다.)


그리고 CreateRemoteThread의 뒷부분에 설치된 훅루틴에선 마지막 인자인 threadid를 구해서 저장합니다.

(커널이 전달해주는 output 인자이므로, 뒷부분을 따로 후킹할 수 밖에 없습니다.)


여기서 구한 thread의 start address는 dllmain에서 DLL_THREAD_ATTACH가 왔을 때 threadid 와 비교해서 injection된 thread인지 검사합니다.

그리고 thread의 시작위치에선, register에 start address가 저장돼 와서, 결국 start address 로 가게 되는데,

이를 후킹해서 저장된 threadid와 비교, injection 된 놈인지 검사하게 됩니다.



즉 누군가가 이 프로세스에 dll을 injection하려 하면,

dllmain에 DLL_THREAD_ATTACH notify가 오고,

여기서 threadid를 저장된 threadid와 비교해서,

일치하는게 없으면 ExitThread()해버리는거죠.



코딩은 끝났습니다.

(코드에 대한 설명은 따로 않겠습니다. 아래 소스를 보세요.)

AntiInjection이라고 프로젝트를 명명해보죠.

자 잘 되나 볼까요?


 



아래 프로그램은 HelloWorld를 띄우는 기본 win32 프로그램이고, 시작할 때 완성된 AntiInjection.dll 을 로드합니다.

마찬가지로 AntiInjection.dll이 ProcessControl.dll을 사용하므로, AntiInjection.dll과 ProcessControl.dll이 같이 있어야 합니다.


사용자 삽입 이미지


세상아 안녕?

winmain에 간단히 loadlibrary 코드를 넣어놨으므로 이미 AntiInjection은 설치돼있습니다.

이 프로세스에 위에서 했듯 ProcessControl을 이용해 dll을 inject해보죠.

c:\..>testloader.exe wintest.exe "c:\documents and settings\....\testloaded.dll" processcontrol.dll

사용자 삽입 이미지


조용하네요. (제 머신에선)잘 됩니다.

소스를 받으셔서, 직접 디버깅 해보세요. visual studio에선 win32 dll project로 하셔야 하고,

running executable을 wintest.exe로 하시면 됩니다.

그리고 dllmain에 bp를 걸고 testloader.exe로 inject해보세요. 잘 됩니다.^^



후킹거는 코드는 소스 보시면 됩니다.

(대충만든 런타임패치라, windows 버전에 따라 후킹이 안될지도 모릅니다;; )


위에 언급된 바이너리와 , 소스코드 첨부합니다.
(which are under terms of the GNU General Public Licence)




p.s. AntiInjection에서 thread의 시작 주소를 얻기 위해 메모장 프로세스를 생성합니다. 프로세스 생성 후 그 프로세스에 스레드를 생성하고, CREATE_THREAD_DEBUG_EVENT 디버그이벤트를 받아서 시작 위치를 구하죠. 근데 집어넣은 스레드때문에 프로세스가 죽질 않네요. 실행할 수도 없고 해서... 차일드프로세스 notepad가 생기더라도 놀라지 마시길 ^^


p.s.2 코딩은 하루걸렸는데 포스팅이 며칠 걸리네요 ㅡㅡ;; 블로깅 하시는 분들 정말 대단합니다!