발자취
악성코드 Pre-Lab6. 스텔스 프로세스 본문
본 실습은 리버싱 핵심원리 교재의 33장에 해당하는 실습이다.
악성코드 프로세스가 시스템 상에서 돌아가는 것을 타겟이 모르도록 은폐하는 실습이다.
사용코드는 다음과 같다.
https://github.com/reversecore/book
(소스코드\04_API_Hooking\33_스텔스_프로세스)
본 게시글에서는 실습을 총 두 번 진행한다.
첫번째 실습의 문제점을 개선하여 두번째 실습을 진행하고, 두번째 실습의 문제점을 파악해 해결 방안을 알아볼 것이다.
1. 첫번째 실습
사용 코드: HideProc.cpp, stealth.cpp
목표: notepad.exe 프로세스 은폐
1. 코드 수정

1. HideProc.cpp - [83, 132] InjectDll 함수와 EjectDll 함수 안에서 사용된 WaitForSingleObject 함수의 파라미터인 INFINITE를 1000으로 변경해준다. (백그라운드 프로세스가 무한정 기다리지 않게 하기 위해서)

2. stealth.cpp - [127번 라인 밑]에 'pPrev = 0;' 코드 추가

두 코드 모두 Debug 속성을 Debug 모드, 유니코드 속성으로 변경해준 뒤 빌드한다.
2. 실습 시작

HideProc.exe와 stealth.dll를 같은 폴더에 둔다.

우선 아무것도 하지 않은 상태에서 notepad.exe와 procexp.exe를 실행하여 notepad.exe가 잘 돌아가고 있다는 것을 확인한다.

[Ctrl+Shift+Esc]를 눌러 작업 관리자를 실행해보면 메모장이 돌아가고 있는 것을 확인할 수 있다.
이제 본격적으로 메모장 프로세스를 은폐해보도록 할 것이다.

PowerShell을 '관리자로 실행'한다.

cd 명령어를 통해 [HideProc.exe가 있는 경로]로 이동한다.
./HideProc.exe -hide [은폐할 프로세스] [stealth.dll의 전체 경로]를 입력하여 실행하면, 프로세스 은폐가 된다.
'failed' 문자열이 잔뜩 떴는데, 이건 다른 작업이 실패한 것이고 실제로 은폐작업은 제대로 되었다.

procexp.exe를 실행해본 뒤, 'notepad'를 검색했을 때, 아무것도 뜨지 않는다.
검색하지 않고 procexp.exe 리스트를 살펴봐도 'notepad.exe'가 뜨지 않는다. 제대로 은폐된 것이다.

작업 관리자를 실행해보아도 '메모장'이 없는 것을 확인할 수 있다.

그러나 이 방법에는 문제점이 있다.
작업 관리자와 procexp를 종료했다가 다시 실행해보면 'notepad'가 리스트에 다시 뜬다.
완전한 은폐라고 할 수 없기 때문에 우리는 두 번째 실습을 통해 이 문제점을 해결해볼 것이다.
2. 두번째 실습 - 글로벌 후킹
사용 코드: HideProc2.cpp, stealth2.cpp
목표: notepad.exe 프로세스 영구 은폐
실습 진행 전 스냅샷을 찍을 것을 권장한다.
코드의 내용은 첫번째 실습과 동일하게 바꿔줘야 한다.
1. 실습 시작

HideProc.exe와 stealth2.dll을 같은 폴더에 둔다.

역시 마찬가지로 PowerShell을 '관리자로 실행'한다.

아까와 마찬가지로 cd 명령어를 통해 [HideProc.exe가 있는 경로]로 이동한다.
cp 명령어로 stealth2.dll을 C:\Windows\System32 파일로 옮겨준다
./HideProc2.exe -hide [stealth2.dll]를 입력하여 실행하면, 프로세스 은폐가 된다.

작업 관리자와 procexp를 실행해본 결과, notepad.exe가 켜져있는데도 불구하고 리스트에 뜨지 않는 것을 확인할 수 있었다.
첫번째, 두번째 실습은 모두
Unhook → call original API → stealth 기능 주입 → hook
이 과정을 계속 반복하고 있다. (아래에서 코드 분석할 때 자세히 볼 것)
이 방식을 사용하는 것은 성능 저하 및 멀티스레드 충돌 등을 일으킬 수 있다. (Unhook과 Hook을 계속 반복하기 때문에)
따라서, 핫패치 방식을 사용하는 것이 좋다.
3. 핫패치 방식 사용
핫패치 방식을 사용하여 스텔스 프로세스 실습을 진행하지는 않을 것이고, 간단한 개념 설명과 코드 설명만 진행할 것이다.
우선 일반적인 API 시작 코드 형태는 아래와 같다.
NOP
NOP
NOP
NOP
NOP
MOV EDI, EDI
위와 같은 형태로 시작하는 함수들에 한해 핫패치를 적용할 수 있다.
본 실습에서 사용하는 코드에서도 CreateProcess 함수는 위와 같은 형태로 시작되어 핫패치가 적용 가능하고, ZwQuerySystemInformation 함수는 그렇지 못해서 적용할 수 없다.
아무튼, 위와 같은 형태로 시작하는 함수에서 핫패치를 적용하는 원리는 다음과 같다.
위에 적은 어셈블리 코드는 사실상 아무 내용을 갖지 않는다. 따라서 아무 내용을 갖지 않는 함수의 맨 첫 부분에 공격자가 다른 내용을 패치하여 원하는 작업을 하도록 만드는 것이다.
위 부분에 새롭게 패치할 내용은 아래와 같다.
A: Jmp B
A+5: Jmp short A
B: 실제 후킹 작업을 수행하는 함수
이렇게 패치해주면 이전과 다르게 hook, unhook을 반복할 필요가 없어진다.
2. 코드 분석
1. HideProc.cpp

[192] LoadLibrary를 통해서 세번째 인자, 즉 stealth.dll을 가져온다.
[195] 로드한 라이브러리(=stealth.dll) 안에서 GetProcAddress를 통해서 "SetProcName"이라는 함수의 주소를 가져온다.
[196] 우리가 숨기고자 하는 프로세스 이름(=notepad.exe)을 SetProcName()의 인자로 넣고 함수를 호출한다.
[202] InjectAllProcess() 함수를 통해서 실제적으로 하이딩 작업에 들어간다.

[149] CreateToolhelp32Snapshot()을 통해 전체 프로세스를 찾아서
[160] dwPID가 특정 값보다 큰 프로세스에만 인젝션한다.

[68] OpenProcess: 프로세스 열기
[72] VirtualAllocEx: 메모리 할당
[75] WriteProcessMemory: 메모리에 내용 적기
[81] CreateRemoteThread: 실행해줌
=> 이전 실습에서 봤듯, 인젝션하는 과정
2. stealth.cpp

[221] SetProcName 함수: HideProc.cpp에서 인자로 받은 값(=notepad.exe)을 g_szProcName에 복사함

DllMain 함수: API Hooking이라는 테크닉을 이용해서 하이딩함.
[192] 지금 동작중인 exe 파일(=HideProc.exe)는 제외하고 모든 프로세스를 대상으로 후킹함
[194] 현재 실행중인 프로세스의 경로를 szCurProc에 넣어줌
[203] stealth.dll이 HideProc.exe에 ATTACH 되면 hook_by_code가 실행된다.

hook_by_code() 함수: 이 함수의 3번째 인자가 NewZwQuerySystemInformation() 함수이다.
[55] szDllName은 ntdll.dll이다. szFuncName은 ZwQuerySystemInformation 함수이다.
[63] ZwQuerySystemInformation()이 ntdll.dll에 정의되어 있다. 그래서 그 dll에서 그 함수에 대한 주소를 가져옴. 이게 오리지널 주소이고, 후킹(변조) 대상 API 주소이다.
[67] 0xE9는 점프이다. -> 즉 후킹이 되었다는 뜻이다.
[71] VirtualProtect(): 호출하는 프로세스의 가상 주소 영역에서 커밋(=설정)된 페이지 영역의 보호(=권한) 변경. 즉, ZwQuerySystemInformation 함수부터 5바이트를 읽고, "쓰고", 실행할 수 있도록 권한을 바꿔줌 (쓰기 권한 추가)
[78] pfnOrg는 ZwQuerySystemInformation, pfnNew는 NewZwQuerySystemInformation.
[79] 새 주소에서 원래 주소만큼 빼고 사이즈만큼도 빼줘서 새롭게 주소를 계산한다. 점프 뒤에 오는 주소를 dwAddress로 넣어주는 것. 결과적으로는 pBuf가 기계어 코드를 갖게 되는 것이다
[82] ZwQuerySystemInformation 함수의 시작 코드의 첫 다섯바이트를 "E9 XXXX"라는 기계어 코드를 바꾼 것. NewZwQuerySystemInformation 함수를 코드 상에서 명확하게 호출하는 방법이 아니라, 기존 ZwQuerySystemInformation 함수의 시작 주소에 NewZwQuerySystemInformation로 가는 점프 기계어 코드를 덮어씌워준 것임!
→ 이 함수는 후킹을 위한 사전 준비작업임. 실행의 흐름을 바꿔줌. New ZwQuerySystemInformation로 가도록.. 실질적으로 하이딩 작업은 NewZwQuerySystemInformation에서 한다. [119]
---
ZwQuerySystemInformation 함수
실행중인 모든 프로세스의 정보를 링크드 리스트 형태로 얻는 함수
→ 여기서 우리가 숨기고자 하는 프로세스 정보를 리스트에서 제거해주면 됨


NewZwQuerySystemInformation 함수
[130] 은폐 작업 전, 후킹 해제. 이걸 해줘야 은폐 후 후킹을 할 수 있음.
call unhook → 스텔스 기능(은폐 기능) → call hook
즉, Z wQuerySystemInformation 함수 앞부분에 점프하도록 넣었던 걸 다시 원상복구한 다음에 스텔스 기능을 넣고 그 다음에 다시 ZwQuerySystemInformation 함수 앞에 점프를 넣어주는 방식이다.
**정리**
- ZwQuerySystemInformation를 NewZwQuerySystemInformation로 가도록 점프하게 바꿔준 것: 후킹
- NewZwQuerySystemInformation를 실행: 하이딩 작업
- 전체 프로세스를 보여주는 프로그램(예: procexp.exe)가 실행됐을 때 ZwQuerySystemInformation가 호출된다. 그리고 이 함수가 호출되어야만 이렇게 은폐 작업이 이루어질 수 있다 (그렇기 때문에 unhook과 hook을 반복하는 사태가 발생한 것)

[100] ntdll.dll에서 ZwQuerySystemInformation의 주소 가져와서 pByte로 지정
[104] 만약 첫번째 바이트가 0xE9(점프)라면 후킹을 해제
[108] 후킹할 때 점프 코드 썼던 부분에 다시 덮어써줘야 하니까 다시 write 권한을 부여
[111] [75]에서 원본 백업했던 걸로 덮어씌워주기
[130]에서 만약 언후킹 하지 않았다면 [132]에서 ZwQuerySystemInformation를 호출할 때, 제대로 동작하지 않음. 왜냐하면 이미 원래 코드가 점프 어쩌구 내용으로 후킹되었기 때문에... 코드가 달라졌기 때문에...
핵심
1. 모든 프로세스를 보여주는 기능(=ZwQuerySystemInformation)을 가진 프로그램을 실행했을 때만 은폐 작업이 이뤄짐
2. 스텔스 기능을 넣기 위해 언후킹 해야 함. 은폐 기능을 넣기 위해서는 원래 버전 ZwQuerySystemInformation에서 넣어줘야만 정상 동작함.
왜냐? 그걸 얻기 위해서는 ZwQuerySystemInformation가 호출되어야 하는데, 변형이 되어 있다면 제대로 호출되지 않을 수 있으니까.
3. stealth2.cpp
추가적으로 실행되는 프로세스에도 적용되도록 글로벌 후킹 방식 사용

[358] hook_by_code를 호출하기 위해 각 dll 정보와 함수 정보를 넣어주고 있음.
원래는 zwQuerySystemInformation()만 후킹해줬었는데, 지금 여기서는 CreateProcessA와 CreateProcessW도 같이 후킹해주고 있음. 프로세스 생성될 때 호출되는 함수들임.
CreateProcessA -> NewCreateProcessA
CreateProcessW -> NewCreateProcessW로 실행의 흐름이 바뀜.

[254] NewCreateProcessA() 함수
[271] 우선 언후킹부터 한다. (호출해야하니까)
[274] 함수를 호출한다
[288] InjectDll2를 호출한다. 생성된 자식 프로세스에 stealth2.dll을 인젝션 시키는 것.
CreateProcessA가 호출되어야 하는데 NewCreateProcessA가 호출되면서, 새롭게 생성된 프로세스에 스텔스 기능을 갖는 dll을 주입함. 그러면 그 dll이 ATTACH돼서 은폐 기능을 하게 됨.
4. stealth3.cpp
핫패치 방식 사용

[399] CreateProcessA, CreateProcessW에는 hook_by_hotpatch()를 사용하나, zw에는 hotpatch 적용할 수 없어서 hook_by_code를 사용하고 있음. 이렇게 핫패치가 적용되는 함수도 있고 안되는 함수도 있음. 원래 있던 문제를 완전 없애진 못하지만 축소시킴.

[97] hook_by_hotpatch
[107] EB는 Jmp short임. 이 바이트 코드를 보고 핫패치가 되었는지 확인함.
3. 생각해보기
1. 첫번째 실습 방식대로 notepad.exe를 은폐할 경우에 발생할 수 있는 문제점은?
후킹 대상의 프로세스들이 기본적으로 모든 프로세스들을 후킹해야만 은폐가 가능한 것이 문제이다.
실습에서는 procexp.exe나 작업 관리자만 사용되었으나, 어떤 상황에서는 ZwQuerySystemInformation 함수가 다른 프로세스에서도 호출될 수 있다. 만약 이렇게 예상치 못한 프로세스에서 ZwQuerySystemInformation를 호출하는데 이 프로세스가 후킹되어 있지 않은 상태라면? 완전한 은폐가 되지 않을 것이다. 따라서 모든 프로세스들에서 은폐가 되어야 하기 때문에 모든 프로세스들을 후킹해야 한다.
그리고 새롭게 생성된 프로세스에는 후킹이 적용되지 않는 문제점 또한 존재한다.
그렇기 때문에 글로벌 후킹을 해서 추가적으로 생성되는 모든 프로세스에 적용되게 만들어야 한다.
2. 두번째 실습 방식은 첫번째 실습 방식의 문제점을 어떻게 해결하였는가?
글로벌 API 후킹 방식을 사용하였다.
CreateProcess 관련된 부분이 나올 때마다 전부 후킹하여 프로세스가 생성될 때 자동으로 스텔스 관련된 dll이 인젝션되어 은폐 기능이 들어가도록 만들었다.
그러나 이 방식은 언후킹과 후킹을 매번 반복해서 성능이 저하되고 멀티스레드 등의 충돌 문제가 생길 수 있다는 문제가 있다.
(그래서 세번째 실습에서 핫패치 방식으로 성능 저하 문제를 해결하였다.)

스텔스 프로세스 실습 끝!
'3-2 > 악성코드' 카테고리의 다른 글
| 악성코드 탐지 시스템 실습 02 - 별도 파일로 저장된 VirusDB (0) | 2024.01.18 |
|---|---|
| 악성코드 탐지 시스템 실습 01 - 해시값 하드코딩, 파이썬 파일에 포함된 DB (0) | 2024.01.18 |
| 악성코드 Pre-Lab5. 계산기 후킹 (0) | 2024.01.09 |
| 악성코드 Pre-Lab4. Code Injection (0) | 2024.01.09 |
| 악성코드 Pre-Lab3. PE 패치를 이용한 DLL 로딩 (0) | 2023.12.16 |