개요
Stage2 에서 원격 프로세스의 RW 메모리에 기록한 쉘코드를 ROP 를 이용하여 실행하는 과정을 알아봤습니다. 쉘 코드를 인젝션 하고 실행했으니 이제 그만 잊어버릴 법 한데 아직 문제가 남았습니다. 하이젝한 쓰레드의 원래 동작을 복원하지 않으면 어떤 형태의 충돌이 일어날지 알 수 없기에, 해당 쓰레드를 원상복구 시켜줘야 합니다.
전체 소스코드
3단계. Restoration
복구는 어떤 과정으로 이뤄질까요? 일단 현재 우리는 APC 컨텍스트 안에 있다고 생각해봅시다. 정상적인 APC 함수라면 OS는 APC 함수 완료시, 어떻게든 원래 실행흐름을 복구 시킬겁니다. 공격대상프로세스의 관점에서 APC들이 어떻게 분배되는지 살펴봅시다.
APC 함수(APC queue 에 대기중인 APC 쓰레드) 들을 분배하는 역할을 하는 함수는 ntdll!KiUserApcDispatcher
로 보입니다. (여기에서는 WaitForSingleObjectEx)
이 함수에선 세개의 CALL을 볼 수 있습니다. 첫번째것은 CFG1, 다음은 ECX(APC 함수), 마지막으로 Undocumented 함수인 ZwContinue()
입니다
ZwContinue()
는 CONTEXT 구조체2를 인자로 실행을 재개시킵니다. 커널은 APC 큐에 남아있는 APC가 있는지 확인하고, 최종적으로 실행을 원복시키기 전에 큐에 남은 APC들을 모두 분배합니다만, 요점은. 이를 무시할 수 있습니다.
ZwContinue()
에 전달되는 CONTEXT 구조체는 APC함수를 호출하기 전,(위에 말씀드린 CALL ECX 부분) EDI에 저장되어 있습니다. 우린 이 EDI의 값을 우리가 인젝션한 쉘코드의 도입부에서 가져올 수 있고, 쉘코드 마지막에서 ZwContinue 를 원본 EDI값과 함께 호출할 것입니다. 이로써 남은 APC큐를 무시하고 안전하게 실행을 복원할 수 있습니다.
__asm{
mov[ptContext], edi; // shellcode_entry() 초반 CONTEXT 백업(==EDI 백업)
}
여기서 주의할 점은 NtSetContextThread()
를 호출할 때 EDI값이 덮어쓰여지진 않는지 확실히 하는 것입니다. 이는 ContextFlags 를 세팅함으로써 쉽게 확인할 수 있습니다.(ContextFlags는 CONTEXT 구조체의 멤버입니다) CONTEXT_CONTROL 플래그를 세팅하면 NtSetContextThread()
를 호출할때 EBP,EIP,SEGCS,EFLAGS,ESP,SEGSS 를 제외한 다른 값은 수정되지 않도록 설정할 수 있습니다. 단, (CONTEXT.ContextFlags | CONTEXT_INTEGER == 0) 이 참일 때에만
이제 Win10 x86, x64 에 이르는 어떤 OS에서든 WriteProcessMemory()
와 CreateRemoteThread()
없이도 쉘코드를 인젝션 할 수 있습니다. POC 제작자의 GitHub에 컴파일된 POC가 있으니 확인해보셔도 좋을것입니다.