개요
앞서 Stage1 에서는 AtomBoming 인젝션 기법을 위한 사전작업으로 원격 프로세스에 쉘코드를 쓰는 방법을 알아봤습니다. 쉘코드를 쓰긴 했지만 아직 실행을 시키진 못한 상태이죠. 이 글에선 AtomBominb Stage2 Execution 에 대해 알아보겠습니다.
전체 소스코드
2단계. Execution
물론 코드를 실행시키기 위해선 위해선 RWE 메모리가 필요합니다. 일반적으로 메모리 할당과 접근권한 설정을 함께 하기 위해선 VirtualAloocEx()
함수를 이용하지만 여기선 조금 다른 방법을 사용합니다. 먼저 1단계에서 GlobalGetAtomName()
함수를 이용해 쓴 쉘코드는 DEP로 인해, 실행할 수 없는 상태임을 다시 한번 강조하겠습니다. 그럼 DEP 를 어떻게 우회할 수 있을까요? ROP가 그것입니다.
우린 1단계를 통해 대상 프로세스의 RW 메모리(RW code cave)에 버퍼를 저장할 수 있음을 확인했습니다. 이후 맞춤 설계된 ROP chain1 을 통해 RWE 메모리를 할당하고, 버퍼를 복사하고, 실행할 것입니다.
임시로 쓸만한 RW code cave를 찾는 것은 그리 문제될 게 없습니다. 여기에선 KernelBase2의 data 섹션 이후 미사용 메모리공간을 쓰도록 하겠습니다.
소스코드
ROP 체인
ROP 체인의 목표는 아래 세 가지입니다.
- RWE 메모리 할당
- 쉘코드를 RW 메모리에서 신규 할당된 RWE 메모리로 복사
- 신규 할당된 RWE 메모리 영역 실행
함수 호출 상황에 따른 스택 프레임을 머릿속에 그릴 수 있다면 이 부분을 훨씬 쉽게 이해하실 수 있습니다. ROP chain 에 본격적으로 들어가기에 앞서 1단계에 나온 방법을 통해 아래와 같이 스택의 용도로 쓸 임의의 메모리영역 0x30000000 을 설정했습니다.
1. RWE 메모리 할당
우린 RWE 메모리를 할당해야 합니다. 보통 VirtualAllocEx()
가 떠오르실 겁니다. 하지만 이 함수는 신규 할당된 메모리의 포인터를 오직 EAX로만 리턴한다는 문제가 있습니다. ROP 체인 내에서 레지스터에 있는 값을 찾고 다음 단계로 전달하기란 그리 쉽지 않은 일입니다.
LPVOID WINAPI VirtualAllocEx( // 리턴(EAX 레지스터) 을 통해 할당된 메모리 포인터 반환
_In_ HANDLE hProcess,
_In_opt_ LPVOID lpAddress,
_In_ SIZE_T dwSize,
_In_ DWORD flAllocationType,
_In_ DWORD flProtect
);
그럼 어떤 함수를 사용해야 쉽게 ROP 체인을 만들 수 있을까요? ZxAllocateVirtualMemory()
가 바로 그것입니다. ZwAllocateVitualMemory()
는 신규 할당된 RWE 메모리를 OUPUT parameter로 전달합니다. RWE 메모리 포인터가 스택에 저장되기에 효과적으로 다음 ROP 체인 내에서 주소를 전달할 수 있습니다.
NTSTATUS ZwAllocateVirtualMemory(
_In_ HANDLE ProcessHandle,
_Inout_ PVOID *BaseAddress, // 할당된 메모리 포인터
_In_ ULONG_PTR ZeroBits,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG AllocationType,
_In_ ULONG Protect
);
이 단계에서 주의 깊게 봐야 할 부분에 표시해뒀습니다. ZwAllocateVirtualMemory()
함수는 0x30000004 부터 0x30000018 의 값을 인자로 사용하고, 함수가 종료되면 0x30000000 에 저장된 주소로 리턴 할 것입니다. 그리고 그 리턴 할 주소에는 2. 쉘코드 복사에서 사용할 함수의 주소, memcpy()
가 들어있습니다.
소스코드
2. 쉘코드 복사
다음 해야 할 일은, 할당된 RWE 메모리로 쉘코드 버퍼를 복사하는 일입니다. memcpy()
혹은 RtlMoveMemory()
정도를 생각할 수 있습니다. 보통 이런 형태의 ROP 체인을 만들 때 처음에 시도해 볼법한 것은 ___stdcall
3 호출규약을 따르는 RtlMoveMemory()
입니다만, 이 경우에는 ZwAllocateVirtualMemory()
를 호출하고 얻은 스택에 있는 메모리포인터를 보존해야 하기 때문에 callee가 스택을 정리해버린다면 이 주소를 POP 해버릴 것입니다.
따라서 이번엔 memcpy()
를 사용할 것입니다. memcpy()
는 ___cdecl
4 호출규약을 따르기에 리턴 후에도 스택의 데이터에 접근할 수 있습니다.
memcpy()
의 첫 번째 인자는 ZwAllocateVirtualMemory()
가 할당해준 RWE 메모리주소이자, memcpy()
가 쉘코드 복사에 사용할 목적지 주소입니다. 또한 우리가 최종적으로 호출하고자 하는 주소이기도 합니다. 이어서 두 번째 인자는 memcpy()
가 복사할 소스가 되겠죠. 이곳엔 1단계에서 준비한 쉘코드가 담겨있을것입니다.
3. 신규 할당된 RWE 메모리 실행
여기까지 우린 RWE 메모리를 할당하고, memcpy()
를 통해 쉘코드를 복사했습니다. 이제 RWE 메로리에 복사된 쉘코드 주소로 리턴을 할 차례입니다. 하지만 여기서 문제가 있습니다. memcpy()
가 리턴시 참고하는 값은 0x3000001C 인데 RWE 로 복사된 쉘코드는 4바이트 떨어진 0x30000020 이란 점입니다. memcpy()
의 첫번째 인자이기 때문이죠. 그러므로 여기에 접근하기 위해 간단한 RET 가젯5 을 추가할 것입니다. 이를통해 memcpy()
로 부터 리턴한 EIP는 다시 RET을 만나게 될 것이고, 최종적으로 RWE 메모리를 실행하게 될 것입니다.
ROP 체인에 접근하기
앞서 말했다시피 APC는 3개의 인자를 받습니다. 하지만 위에서 알아본 바로는 우린 11개의 인자가 필요합니다. 이를 위해 우린 스택 주소를 조율(pivot) 할 필요가 있습니다. 여기에는 NtSetContextThread()
를 사용할 것입니다.
NTSYSAPI NTSTATUS NTAPI NtSetContextThread(
_In_ HANDLE hThread,
_In_ const CONTEXT *lpContext
);
이 함수는 hThread의 컨텍스트를 설정할 수 있습니다. 대상 프로세스에서 이 함수를 호출하게 만든다면, ESP 레지스터를 수정하여 우리가 임의의 RW 메모리에 써둔 데이터(ROP체인)를 스택처럼 사용하게 만들 수 있습니다. 물론 EIP 레지스터는 ZwAllocateVirtaulMemory()
의 주소로 설정해야겠죠.
이제 대상프로세스가 이 함수 NtSetContextThread()
를 실행하게 만드는 방법을 찾아야 합니다. 이 함수는 두 개의 인자를 받습니다. APC는 세 개의 인자를 받기에 사용할 수 없습니다. APC를 그대로 사용한다면, 리턴 할 때 스택에 오류가 발생할 것입니다.
하지만 이렇게 생각해보겠습니다. 우리가 NtSetContextThread()
를 호출해 EIP를 바꾸는 순간, 해당 쓰레드에서 원래 실행중이던 함수는 절대 return 될 일이 없을 것입니다. 그 이유는 해당 쓰레드는 Context가 set 된 순간, NtSetContextThread()
가 호출되었는지, 어디로 다시 돌아가야할지 알 방법이 없기 때문입니다. 따라서 세 개의 인자를 받는 APC를 그대로 사용해도 괜찮습니다.
마무리
2단계를 통해 우리는 원하는데로 원격프로세스에 쉘코드를 호출할 수 있습니다. 하지만 안타깝게도, 원격 프로세스의 쓰레드 하나는 실행흐름이 꼬여버려서 다신 살릴 수 없겠군요. 절대 return 되지 않을테니까요. 만약 원격 프로세스의 실행흐름에 매우 치명적인 쓰레드 였다면 어땠을까요? 사용자는 경고창을 보고 즉시 뭔가 이상하다는 사실을 알아챌 것입니다.
이거서 3단계 Restoration 에서는 통해 원래 프로세스의 실행흐름을 복구하는 법을 알아보도록 하겠습니다.
계속…
1 Comment