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 를 비교한 것으로 해시 값이 동일한 것을 확인할 수 있다

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