
이번에는 지난 시간과 비슷한 dll 인젝션 테크닉이지만 조금 차이가 있다. 바로 공략을 하는 위치인 Location이 다르다.
지난번 20장에서 했던 것이 IAT 영역에 기존 api 시작 주소에서 내 공격 api 주소로 바꿨다면,
이번에는 기존 API 코드 자체를 수정해버려서 기존 API가 내 악의적인 API를 호출해버리도록 하는 것이다.
API 시작 주소부터 명령어가 주우우욱 있을 거잖아. 거기서 처음 5바이트만 내가 수정하는 것이다. JMP (인젝션 dll 주소)라고 수정하면 된다. 어셈블리 말고 IA-32명령어로 하면 딱 5바이트야.
후킹 전/후의 호출 관계를 보면 이해가 쉽다.

procexp.exe가 우리의 타깃 프로세스이다. 이 프로세스는 IAT 영역에 ntdll.ZwQuerySystemInformation( ) API를 호출하려고 한다
그럼 결국 이 API의 코드 시작 부분을 CALL 하는 명령어를 실행해야겠지. 심플하다.
좀 지저분하게 적었지만 봐봐.
- stealth.dll이라는 dll을 인젝션 해둔 상태
- 타깃 프로세스가 IAT 영역에서 원래 있던 API를 호출한다.
- 하지만 그 API 첫 시작 주소에 명령어를 조작해뒀다. 이 명령어를 실행하면 인젝션한 stealth.dll로 제어가 넘어간다.
- stealth로 넘어갔으면 언훅을 한다. 아마 무한루프 방지를 위해 언훅하는 듯.
- 이런저런 실행을 하다가 다시 원래 부르려던 API를 호출한다.
- 원래 API에서의 실행 흐름 다 마치고 return 하면 다시 stealth.dll로 돌아간다.
- stealth로 돌아왔으면 다시 원래 API에 후킹을 걸어둔다. 앞으로 다시 쓸 수 있도록?
- stealth.dll로 실행 다 끝나면 다시 타깃 프로세스로 return 한다.
내가 궁금한 건 4~5에서 원래 API를 꼭 한번은 실행해야 하는 이유는 뭘까? 그냥 stealth에서 하고싶은 공격이나 악의적인 행동 다 한다음에 타깃 프로세스 코드 영역으로 리턴하면 되는거 아닌가?
정상 실행이 한번은 필요한건지가 궁금해.
ㄴAnswer : 이게 정상을 한번 꼭 실행한다 이런 의미가 아니더라구.
사실 이게 공격기법이잖아? 그럼 공격자는 몰래 실행하는 것이 중요하겠지. 사용자는 자기가 의도한 동작이 나타나면 아무 의심이 없겠지. 아무 의심을 안하게 하기 위해서 정상 api를 한번 실행시키는 것이라고 보면 된다.
우리가 이번에 후킹하려는 API는 ntdll.ZwQuerySystemInformation( )다. 굉장히 이름이 기네...
왜 이 API를 후킹하려는 것인지 그 이유는 바로 우리의 타깃 프로세스가 프로세스 탐지기에 감지되지 않게 만들고 싶기 때문이다.
프로세스를 은폐하는 방법은 다음과 같다.
타깃 이외의 모든 프로세스에서, 현재 시스템에서 실행 중인 프로세스 목록을 열어볼 때 타깃 프로세스만 안 보이게 한다.
사실 스텔스라는 이름에서 우리가 떠올리는 스텔스기랑 원리는 다르다. 자신을 직접 감지되지 않게 하는 스텔스 전투기랑은 반대로, 그냥 모든 레이더를 기능 고장나게 만들게 해서 감지되지 않게 하는 것이다.
"하지만 안 보였죠?" 이러는 꼴
유저 모드의 프로세스들이 프로세스를 검색하는 API는 크게 두가지 종류가 있다.
CreateToolhelp32Snapshot( )
HANDLE WINAPI CreateToolhelp32Snapshot( DWORD dwFlags, DWORD th32ProcessID );
EnumProcesses( )
BOOL EnumProcesses( DWORD* pProcessIds, DWORD cb, DWORD* pBytesReturned );
둘다 결국 내부적으로는 ntdll.ZwQuerySystemInformation( )을 호출한다. 결국 얘를 봐야겠지?
ZwQuerySystemInformation( )
NTSTATUS ZwQue rySysteminfo rmation( SYSTEM_INFORMATION_CLASS SysteminformationClass, PVOID Systeminformation, ULONG SystemlnformationLength, PULONG ReturnLength );
얘는 결국 실행 중인 모든 프로세스의 정보를 구조체 형태로 리스트를 빼오는 것이다. 그럼 은폐 하는 방식은 이 리스트에서 우리의 타깃 프로세스만 지우면 되겠네.
ZwQuerySystemInformation( )만 잘 후킹하면 다 숨길 수 있다.
프로세스의 은폐를 위해서 우리는 다음 과제를 생각해야 한다.
과제 1단계. 지금 실행되고 있는 타깃 이외의 모든 프로세스에서 ZwQuerySystemInformation( )를 후킹해야 한다.
과제 2단계. 앞으로 새로 실행되는 프로세스에서도 ZwQuerySystemInformation( )를 후킹해야 한다.
단, 1과 2를 자동으로 실행될 수 있게 해야한다.
이것을 만족하는 것이 바로 Global 후킹이다. 시스템 전체에 걸쳐서 후킹을 하는 것
첫번째로는 실행 중인 모든 프로세스에 dll 파일을 인젝션해서 ZwQuerySystemInformation를 후킹하는 것이다.
타깃 : notepad.exe를 은폐시키기
인젝터 : HideProc.exe -> 타깃 외에 실행중인 모든 프로세스에 Stealth.dll 인젝션
dll : Stealth.dll -> 인젝션된 프로세스에 ZwQuerySystemInformation를 후킹함.
커맨드는 이렇게 실행한다.
이렇게 하면 모든 프로세스에 stealth.dll이 인젝션 된다. 이때 시스템의 안정성을 위해서 PID가 0이나 4같은 시스템 프로세스들은 인젝션하지 않았다.
우리가 흔히 쓰는 유저모드의 탐지기 process explorer나 작업관리자 같은 것으로 보면 이제 notepad가 감지되지 않을 것이다.
여기에 노트패드가 안 보인다는 것이지. 메모장 창이 실제로 실행중인데도.
커맨드 옵션을 hide에서 다시 show로 바꾸면 다 언훅되면서 풀린다.

분석해야 할 게 우선 인젝터인 HookProc.exe도 봐야한다. 그리고 Stealth.dll도 봐야한다.
실행 중인 모든 프로세스에 내가 원하는 dll을 인젝션 및 이젝션 시킨다.
전체를 보면 이런 과정이다.
커맨드를 보여주고, hide 옵션이면 InjectAllProcess 함수를 실행한다.
show 옵션이면 Ejection Mode로 진입하면 된다.
dll을 inject 하는 InjectDll은 여타 본 것들과 비슷하다. InjectAllProcess를 보자.

우선 실행 중인 모든 프로세스의 핸들을 얻어와야 한다. 그걸 해주는 게 CreateToolhelp32Snapshot( ) API다. 이게 프로세스 리스트를 받아온다.
그 다음에 Process32First나 Process32Next는 리스트에서 PID를 하나씩 가져오는 거 아니겠어.
함수 정의를 하나씩 써보긴 하자...
CreateToolhelp32Snapshot( )
HANDLE WINAPI CreateToolhelp32Snapshot( __in DWORD dwFlags, __in DWORD th32ProcessID );
Process32First( )
BOOL WINAPI Process32First( __in HANDLE hSnapshot, __inout LPPR0CESSENTRY32 Ippe );
Process32Next( )
BOOL WINAPI Process32Next( __in HANDLE hSnapshot, __out LPPR0CESSENTRY32 Ippe );
저 pid가 100보다 작은 건 그냥 넘어가는 건 아까 말했지만 시스템 프로세스는 생략하기 위해서다. 시스템 프로세스는 PID가 0, 4, 8 뭐 이런 애들이라는데 윈도우 버전마다 조금씩 다른 듯.
이제 DLL로 넘어가자
실제 ZwQuerySystemInformation( ) 후킹을 담당하는 친구다.
DllMain부터 살펴보는 게 국룰 같았지만 이번엔 다른 것부터 살펴보네.

.SHARE라는 이름의 공유 메모리 섹션을 만든다.
그리고 g_szProcName이란 버퍼도 만든다.
그리고 외부로 export 되는 함수 SetProcName( )을 이용한다. 이 하무는 은폐하고 싶은 타깃 프로세스의 이름을 g_szProcName 버퍼에 저장한다. tcscpy 보이지?

여기보면 SetProcName은 위에 있던 HookProc.exe가 야무지게 호출한다.
이제 왜 공유메모리에 만드는지 알겠다. 모든 프로세스에서 타깃 프로세스 이름을 공유하기 위해서 접근할 수 있도록 버퍼를 공유메모리 섹션에 두는 것이다.
간단하다. 주석만 읽어도 이해는 되는 정도다.
API 후킹할 땐 hook_by_code() 함수로, 해제할 땐 unhook_by_code() 함수로 하는 게지.
알아둬야할 함수들을 미리 좀 하이라이터로 표시해뒀다.
코드 패치를 이용해서 API를 후킹하는 함수다. 실질적인 후킹을 얘가 하는 것이지.

파라미터 봐봐
szDllName : 후킹하려는 API가 속한 dll 파일 이름 in 해
szFuncName : 후킹하려는 API 이름 in 해줘
pfnNew : 공격자가 넣을 후킹 함수 주소 in 해
pOrgByte : 원본 5바이트 저장할 버퍼 out 해
핵심은 원본 API 코드 시작주소의 첫 5바이트를 JMP XXXXXXXX(injected dll 주소)로 가는 것이다.
왜 5바이트냐면 IA-32 OP 코드로는 E9 XX XX XX XX거든.
근데 이때 XXXXXXXX가 진짜 걍 RVA 적는게 아니다.
'JMP 명령어로부터 목적지까지의 거리'를 적어야 하므로 그 거리를 계산해야 한다.
3에서 8로 갈 때 점프 8이라고 하지 않잖아. 5만큼 점프라고 하지. 그런거야.
상대거리 = 목적지 주소 - (API 시작 주소 + 5)
왜 그런지 내가 직접 그려봤다.
결국 E9 XX XX XX XX 를 다 읽고나서야 점프를 할 수 있는거니까 저 5바이트만큼 상대거리가 더 있어야 한다는 것이지.
이때 XX XX XX XX 리틀 엔디언으로 적힌다.
목적지의 실제 주소로 이동하는 방법들이 있기야 하다. 근데 코드 길이가 늘어나.
- PUSH + RET 조합
PUSH 목적지 주소
RETN
또는
- MOV + JMP 조합
MOV EAX, 목적지 주소
JMP EAX
실제로 ZwQuerySystemInformation를 한번 보자.
7C93D92E | B8 AD00O00O | MOV EAX, 0AD
7C93D92E | E9 ED376C93 | JMP 10001120
10001120은 인젝션한 stealth.dll의 후킹함수인 NewZwQuerySystemInformation( ) 주소다.
지금 상대 거리가 936C37ED라고 되어있다.
10001120- (7C93D92E + 5) = 936C37ED
그냥 계산하면 -6C93C813 나오는데 이걸 2의 보수로 표현한게 936C37ED다.
-> 또는 (7C93D92E + 5) + 936C37ED 하면 110001120나오는데 맨앞에 1은 overflow니까 버령
후킹을 해제할 때 쓰는 함수다.
그냥 원래 잘 보관했던 pOrgByte를 API 시작주소에 다시 써주면 된다. memcpy를 이용하면 쓸 수 있지.
원래 기존 ZwQuerySystemInformation( )는 실행중인 모든 프로세스 리스트를 보여주는 것이다. 이걸 먼저 보자.
ZwQuerySystemInformation( )
NTSTATUS WINAPI ZwQuerySystemlnfo rmation( _in SYSTEM_INFORMATION_CLASS SystemlnfomationClass, _inout PVOID Systeminformation, _in U LONG SysteminformationLength, _out_opt PULONG ReturnLength );typedef struct _SYSTEM_PROCESS_INFORMATION { ULONG NextEntryOffset; ULONG NumberOfThreads; byte Reservedl[48]; PVOID Reserved?[3]; HANDLE UniqueProcessId; PVOID Reserved3; ULONG HandleCount; byte Reserved4[4]; PVOID Reserved5[11]; SIZE T PeakPagefileUsage; SIZET PrivatePageCount; LARGE_INTEGER Reserved6[6]; } SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;결국 SYSTEM_PROCESS_INFORMATION 구조체에 프로세스 정보들이 담겨있는 거잖아. 그럼 숨기고 싶은 타깃 프로세스의 리스트 멤버를 찾아서 연결을 끊으면 되지.
그걸 구현한게 NewZwQuerySystemInformation이다.


while문을 보자. SYSTEM_PROCESS_INFORMATION 구조체 연결 리스트를 검사하면서 프로세스 이름을 비교한다. 프로세스 이름은 유니코드 문자열로 적혀잇다.
이제 제대로 된 글로벌 후킹을 위해 앞으로 새로 실행되는 모든 프로세스들도 후킹을 하게 만들어야 한다. 그러기 위해선 한가지 API를 더 후킹해야 한다.
새로운 프로세스가 생성되려면 kernel32.CreateProcess( )를 사용해야 하는데 얘는 좀 불편하다.
아스키코드 버전과 유니코드 버전별로 api가 달라서 kernel32.CreateProcessA( )랑 kernel32.CreateProcessW( ) 각각 후킹해야 한다.
후킹할 함수는 원본 kernel32.CreateProcess( )를 호출하고 생성되는 자식 프로세스를 대상으로 후킹해야 한다. 자식이 생성되지 않는 짧은 시간동안 그럼 후킹되지 않은 채 실행될 수 있다.
뭐 이러이러한 문제들 때문에 더 간단하게 ntdll.ZwResumeThread( )라는 더 low level의 api를 후킹한다.
ntdll.ZwResumeThread( )
ZwResumeThread( IN HANDLE ThreadHandle, OUT PULONG SuspendCount OPTIONAL );
이건 프로세스가 생성된 다음 메인 스레드의 실행 직전에 호출되는 함수인데 결국 CreateProcess 내부에서 호출되는 것이다.
얘만 후킹하면 자식 프로세스 코드가 아직 하나도 실행되지 않아도 자식 프로세스를 대상으로 ZwQuerySystemInformation( ) API를 후킹할 수 있다.
뭐 단점이라면 얘는 undocumented API라서 안정성을 보장할 수 없다는 정도. 그래서 운영체제 패치하면 또 안될 수도 있다.
모든 프로세스에 새로운 stealth2.dll을 넣을 때 이번엔 글로벌 후킹을 해야하므로 이 dll을 system 폴더에 넣어주야 한다.
그래야 모든 프로세스가 공통적으로 인식할 수 있대.
타깃은 이번에도 여전히 notepad.exe로 고정해뒀다. stealth2.dll에 이번엔 리스트에서 지울 대상을 notepad.exe라고 아예 하드코딩해서 못박아뒀다.
그래서 커맨드에 타깃프로세스 파라미터가 없다.
그러면 몇번이고 process explorer를 실행한다고 해도 거기에 노트패드가 탐지되지 않는다. 작업관리자도 마찬가지고.
해제할 땐 옵션을 show로 바꾸면 되지.
stealth2.cpp엔 기존 스텔스 dll에서 타깃 프로세스를 그냥 notepad로 하드코딩했고, 글로벌 후킹을 위해서 kernel32.CreateProcessA( )랑 kernel32.CreateProcessW( ) 를 후킹하는 코드가 추가되었다.
-> 뭐야 ZwResumeThread 후킹한다면서..;;
결국 main은 얘인데 알아둬야 할 함수들을 체크해뒀지.

NewCreateProcessA( )나 NewCreateProcessW( )나 비슷하다.

여기서 핵심은 어떻게 자식 프로세스의 PID를 얻어내서 dll을 inject하느냐이다.
근데 ㄹㅇ이해 안되는 거.
stealth2.dll 자체가 inject 되어야 할 dll인데 왜 얘가 injectdll2( ) 함수를 갖고 있어?? dll을 넣어주는 injectdll( )함수는 HideProc2가 가져야 하는 거잖아.
-> 지금 stealth2.dll은 현재 실행중인 프로세스(부모 격)에 인젝션된다. 이건 HideProc2가 해준다.
실행중인 프로세스의 자식들에 stealth2.dll을 인젝션해주는건 stealth2 내부의 함수에서 처리하는 것
ㅇㅋㅇ키ㅣㅋ
암튼 이제 자식 프로세스으 pid를 어떻게 받는지 보자.

원래는 이렇게 현재 프로세스의 PID를 받았다.

하지만 지금은
original API인 CreateProcessA를 부르게 된다.
그때 lpProcessInformation이 담긴다.
lpProcessInformaiton-hProcess 에서 자식 프로세스의 핸들을 얻어낸다.
이렇게해서 글로벌 후킹을 하는 것이다.