리버스 엔지니어링 바이블


1. API 후킹 기본

단순히 API의 엔트리 포인트를 jmp 문으로 바꾸면 후킹 탐지에 쉽게 무력화 가능



ws2_32.dll의 send 함수


해당 함수는 위와 같이 mov edi, edi로 시작하며 일반적으로 API 후킹을 하게 되면 아래와 같이 해당 엔트리 포인트를 jmp 문으로 바꾸게됨


API 후킹 이후엔 위와 같이 엔트리 포인트 5바이트만 수정됨


이러한 방법으로는 다른 애플리케이션과 충돌이 가능..



그래서 이렇게 코드 중간에 점프 코드를 삽입하는 방법을 사용하면 코드 후킹 탐지도 피할 수 있고 다른 애플리케이션과 충돌도 피할 수 있음


*사진은 예시를 위해 임의의 명령어와 주소를 삽입함



2. DLL 인젝션


코드 후킹을 위해선 DLL 인젝션이 선행되어야 함.


대부분 흔히 널려있는 DLL 인젝션 툴들은 백신에 악성코드로 진단되기 때문에 직접 만들어서 구현해보는 것도 좋음


2.1. CreateRemoteThread 

 DLL 인젝션의 핵심 원리는 CreateRemoteThread API임

 해당 API는 타 프로세스의 메모리에 스레드를 생성할 수 있는 기능을 지원함

 일반적으로 DLL 로딩은 다음과 같은 코드를 사용함



 위 코드는 C:\Windows\system32\ws2_32.dll을 메모리에 로딩하는 코드임

LoadLibrary() 를 호출해 DLL 파일의 경로를 전달하면, 그 프로세스 메모리에 위 DLL이 로딩됨

CreateRemoteThread() 호출하고 해당 프로세스에서 LoadLibrary() 실행, 인자로 들어가는 DLL 파일의 전체 경로만 지정하면 DLL 인젝션 가능


구현은 다음과 같은 과정으로 가능함



void InjectionDll(DWORD pid, LPCSTR dll)
cs

위와 같이 타겟이 될 프로세스의 pid와 인젝션 할 dll을 두 번째 인자로 받는 구조로 선언
 

HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
cs

타겟 프로세스의 핸들이 필요하므로 OpenProcess()로 pid를 지정한 후 핸들 hProcess를 얻음


LPVOID lpAddr = VirtualAllocEx(hProcess, NULL, strlen(dll)+1, MEM_COMMIT, PAGE_READWRITE);
cs

타겟 프로세스에 LoadLibrary()로 넘겨줄 dll의 주소를 써 넣기 위해 메모리를 할당해줌
* VirtualAlloc은 자신의 프로세스에 할당, VirtualAllocEx는 타 프로세스에 메모리 할당
크기는 dll 경로 문자열이 들어갈 공간이면 충분하므로 Null 문자를 위한 strlen(dll)+1 로 설정
위 함수의 반환 값인 lpAddr에는 DLL의 전체 경로가 담긴 메모리 번지가 되고 이를  LoadLibrary()와  CreateRemoteThread() 에 전달함



WriteProcessMemory(hProccess, lpAddr, dll, strlen(dll)+1NULL);
cs

할당해준 메모리에 dll의 전체 경로를 써줌



LPTHREAD_START_ROUTINE pfnLoadLibraryA =
 (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");

cs


  CreateRemoteThread() 에 사용할  LoadLibrary()의 주소를  GetProcAddress()를 이용하여 구해옴



HANDLE hThread = CreateRemoteThread(hProcess, NULL0, pfnLoadLibraryA, lpAddr, 0NULL);
cs


  CreateRemoteThread() 를 호출하면 타겟 프로세스에서는   LoadLibrary() 가 호출되며 인젝션할 DLL를 로드한다




3. 코드 후킹 

 3.1 후킹 기본

 실제 Lws_32.dll()  파일의 send()  API는 다음과 같이 후킹할 수 있다.


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
void InitHooking1()
{
    typedef void (WINAPI *Tsend)(SOCKET, const char *intint);
    Tsend fnsend;
    TCHAR szWs2_32Dll[MAX_PATH];
    GetSystemDirectory(szWs2_32Dll, MAX_PATH);
    _tcscat(szWs2_32Dll, _T("\\ws2_32.dll"));
    HMODULE hMod = LoadLibrary(szWs2_32Dll);
    fnsend = (Tsend)GetProcAddress(hMod, "send");
 
 // 엔트리부터 0x16 바이트 떨어진 지점을 대상 주소로 설정
    LPVOID lpTargetAdr = (LPVOID)((DWORD)fnsend + 0x16); 
    TRACEB(_T("lpTargetAdr: %x"), lpTargetAdr);
    DWORD dwOldProtect = 0;
    VirtualProtect(lpTargetAdr, 6, PAGE_READWRITE, &dwOldProtect);
    
    // 기초 설명을 위한 하드코딩
    //719E4C3D           E9 BEC391C1     jmp 33301000
    //719E4C42           90              nop
    *((LPBYTE)lpTargetAdr + 0= 0xE9;
    *((LPBYTE)lpTargetAdr + 1= 0xBE;
    *((LPBYTE)lpTargetAdr + 2= 0xC3;
    *((LPBYTE)lpTargetAdr + 3= 0x91;
    *((LPBYTE)lpTargetAdr + 4= 0xC1;
    *((LPBYTE)lpTargetAdr + 5= 0x90;
}
 
cs


 이를 통해 send() 함수가 호출될 경우 0x33301000에 있는 코드를 실행하게 할 수 있음


 0x33301000에 필요한 DLL를 로드하면 원하는 DLL를 실행시킬 수 있음


#pragma comment(linker, "/base:0x33300000 /fixed")
cs


위와 같은 코드를 소스코드의 위쪽에 배치하면 0x33300000번지가 ImageBase가 됨



그러나 위와 같이 주소를 하드코딩하는 방식은 DLL의 주소가 그 주소에 로딩되지 못하면 무의미함

주소가 변경됐을 때도 주소를 찾아 연결시킬 수 있는 방법이 필요함


위의 코드에서 20~26번째 라인을 다음과 같이 변경하여 동적 주소를 구할 수 있음



1
2
3
    *((LPBYTE)lpTargetAdr + 0= 0xE9;
    DWORD dwBufferAdr = (DWORD)HookSend - (DWORD)fnsend - 0x16 - 5;    
    *((LPDWORD)((LPBYTE)lpTargetAdr + 1)) = dwBufferAdr;
cs



 먼저 타겟 jmp 명령어를 의미하는 0xE9는 타겟 주소에 넣어줌

 주소부분 계산은 삽입할 주소의 위치와 jmp 대상이 되는 주소의 차이와 5바이트의 op code를 고려하여 계산해주면 됨

 

 719E4C3D E9 BEC391C1     jmp 33301000


 앞서 이 명령어의 operand도 이렇게 계산됨


0x33301000 - 0x719E4C3D - 5 = 0xFFFFFFFFC191C3BE

 이를 4바이트 주소인 DWORD로 타입 캐스팅 하면 앞의 0xFFFFFFFF는 제거되고 0xC191C3BE만 남고

 이를 리틀엔디안 방식으로 하면 명령어의 operand 부분인 BEC391C1이 됨


 마지막에는 ((LPBYTE)(lpTargetAdr + 1))  의 위치를 LPDWORD 타입으로 4바이트 묶어서 계산된 주소를 넣어줌



3.2 Send 후킹

 

 send 함수의 프로토 타입을 참고하여 버퍼를 출력하는 코드를 작성할 수 있음


1
2
3
4
5
6
int send(
    __in SOCKET s,
    __in const char *buf,
    __in int len,
    __in int flags
);

cs


 2번째 인자로 전송할 버퍼를 전달받고, 버퍼의 길이를 3번째 인자로 전달받음


 이를 참고하여 버퍼를 OutputDebugString을 통해 출력하는 코드를 다음과 같이 어셈블리 코드로 작성

 

1
2
3
4
5
6
7
8
9
10
11
__declspec(naked) HookSend()
{
    __asm
    {
        mov eax, [ebp+0xC]
        push eax
        call ds:OutputDebugString
        mov eax, 0x719E4C43
        jmp eax
    }
}
cs

 

 [ebp+0xC]에는 함수로 전달되는 두 번째 인자가 존재하니까, 그걸 eax에 넣어주고 OutputDebugString 함수의 인자로 전달하기 위해 스택에 넣음


 그리고 원래 코드로 돌아오기 위한 점프문 배치하고 이 함수를 0x33301000 번지에 올라가게 하면 됨



 그러나 단순히 이렇게 해버리면, send 함수를 통해 나가는 모든 패킷을 모니터링하게됨...


 send  함수를 호출한 부모 함수를 식별해서 어느 함수에서 호출한 send인지 출력 가능

 또한 모니터링 하고자 하는 패킷만 모니터링 할 수 있음


 부모 함수를 식별하기 위한 방법은 send 함수의 return 주소인 [ebp + 0x4]를 참조하면 식별이 가능함



 먼저, 어셈블리 코드에서 사용할 ebx, eax, edx 레지스터가 망가지지 않게 스택에 넣어두고, 각각 레지스터에 메시지의 길이, 메시지 내용, return 주소를 넣음 


1
2
3
4
5
6
push ebx
push eax
push edx
mov ebx, [ebp+0x10] // 패킷 길이
mov ebx, [ebp+0xC] // 패킷 버퍼
mov ebx, [ebp+0x4] // return 주소
cs

 이렇게 원래의 레지스터에 있는 값을 저장하고 레지스터를 마음대로 쓴 이후 다시 pop을 통해 복원해줌

 다음과 같은 코드로 send 함수를 호출한 부모 함수의 주소와, 버퍼를 문자열과 덤프 형식으로 출력이 가능함
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
void PseudoFunc(int retaddr, char *buf, int len)
{
    if (len != 1 && len > 0)
    {
        //TRACEB("[%x] send : [%d]", retaddr, len);
 
        int len_temp = 0;
        int len_temp2 = 0;
        char szMsg1[MAX_PATH] = {0,};
        char szMsg2[MAX_PATH] = {0,};
        for (int i=0; i<len; ++i)
        {
            if (MAX_PATH < i)
                break;

            // null 바이트는 왜 "_"로 바꾸지...
            if (buf[len_temp] == '\0')
                len_temp += sprintf(szMsg1 + len_temp, "_");
            else
                len_temp += sprintf(szMsg1 + len_temp, "%c", buf[i]);
            
            // 200 바이트 까지 버퍼를 Hex dump
            if (len_temp2 < 200)
                len_temp2 += sprintf(szMsg2 + len_temp2, "%02X "*(buf + i));
        }
        
        TRACEB("[%x] send : [%d] %s", retaddr, len, szMsg1);
        TRACEB("     dump : %s", szMsg2);
    }
}
cs

 
 이처럼 작성한 함수를 호출하는 어셈블리 코드를 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__declspec(naked) HookSend()
{
    __asm
    {
        push ebx
        push eax
        push edx
        mov ebx, [ebp+0x10]
        mov eax, [ebp+0xC]
        mov edx, [ebp+0x4]
        push ebx
        push eax
        push edx
        call lpPseudoFunc
        add esp, 0xc
        pop edx
        pop eax
        pop ebx
        mov eax, 0x719E4C43
        jmp eax
    }
}
cs
 15번째 줄의 add esp, 0xc는 함수 호출 과정에서 늘어난 스택 크기를 조정해주기 위함

 







 

 

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

우회 기법  (0) 2019.03.20
FSC_Level1 풀이  (0) 2019.03.20
Petya Ransomware 분석  (0) 2019.02.19
PE 헤더  (0) 2019.02.18
2. C 문법과 디스어셈블리  (0) 2019.02.14

13. 우회 방법

API Hooking을 통한 우회
  • API를 이용한 디버깅 탐지 기법들은 API Hooking을 이용하여 해당 API를 무력화 하는 방법으로 우회 가능

  • OllyDBG의 OllyAdvanced라는 안티 디버깅 플러그인이 대부분의 무력화를 지원함

    • FindWindow

      : FindWindow()를 Hooking -> Debuger process의 윈도우 핸들을 가리킬 경우 Null을 return

    • IsSoftIceLoaded

      : CreateFile() Hooking -> Softice 드라이버일 경우 null 또는 INVALID_HANDLE 리턴

    • CheckRemoteDebuggerPresent

      : CheckRemoteDebuggerPresent Hooking -> 호출 시 강제로 false 리턴





      함수 호출부를 아래와 같이 수정하여 강제로 0을 반환하도록 함

      xor eax, eax

      ret

    • Debug Oject Handle

      : NtQueryInformationProcess() API를 같은 방법으로 수정

      : 두 번째 인자가 0x1e (ProcessDebugObjectHandle)일 경우에 대해서만 반환하도록

    • NtQueryObject

      : NtQueryObject에서 NTSTATUS 실패를 리턴

      • NtQueryObject() 를 호출할 경우 모든 오브젝트 핸들을 얻어올 수 있음, 이 중 DebugObject가 포함됐을 경우 탐지하는 기법

    • NoDebugInherit

      : NtQueryInformationProcess() API를 같은 방법으로 수정

    • NtSetInformationThread

      : NtSetInformationThread() API를 같은 방법브로 수정

    • 프로세스 체크

      : Process32Next()API를 이용, 프로세스를 찾지 못하도록 수정

    • 버전 체크

      : GetFileVersionInfoSize() 를 실패하도록 수정

    • 부모 프로세스 체크

      : Process32Next()API를 이용, 프로세스를 찾지 못하도록 수정

      : NtQueryInformationProcess() 도 변조

    • SeDebugPrivilege 권한 체크

      : CsrGetProcessId() 를 변조, OpenProcess(), NtQueryObject()

    • 키보드 입력 봉쇄

      : BlockInput() 변조

    • OutputDebugString

      : OutputDebugString() 함수를 변조, 바로 반환하도록 설정해도 가능

    • WinDBG 검출

      : FindWindow() 무력화

    • CloseHandle()을 이용한 안티 리버싱

      : CloseHandle() 을 후킹, 정상 핸들을 Close하는 것을 걸러야 하는데 이 과정이 복잡

      : OllyAdvanced에서도CloseHandle()을 필터링하는 기능이 존재

      : Ignore and skip C0000008h (inv Handle) 기능 활성화

    • 타이머를 이용한 방법

      : GetTickCount() API를 후킹해 시간을 속임, rdtsc 를 사용하면 Win32 API 가 아니기 때문에 APi 후킹으로는 안됨, OllyAdvanced 에서 관련 기능을 제공함

      1. Anti-RDTSC 기능을 Enable

      2. 해당 기능을 사용하면 아래와 같이 커널 드라이버가 설치됨    


3. IDT 후킹을 이용한 rdtsc 우회 기법

4. rdtsc : 시스템 부팅 후 현재 실행 시점까지를 알려주는 명령어, CPU 사이클을 읽어서 계산함

5. CR4 레지스터의 TSD(Time Stamp Disable) 비트를 1로 설정할 경우 rdtsc가 호출될 때마다 GP(General Protection) 예외가 발생

6. 해당 예외를 처리하는 핸들러가 GPF(General Protection Fault)이며 0xD 인테럽트를 사용
7. 즉, rdtsc를 호출하면 0xD번 인테럽트가 호출됨
8. 0xD 인테럽트를 후킹하여 해당 인테럽트가 호출되면 그것이 rdtsc에 의해 호출되었는지 확인, rdtsc일 경우 시간 값 조작을 통해 우회

      그냥 opcode를 찾아 패치하는 방법도 있음

      1. rdtsc를 호출하는 명령어 (0x0F31)를 모두 찾아 0x90(nop)으로 수정함

    • 디버그 레지스터

      : ???

    • API Hook을 이용한 디버깅 감지

      : 디버거가 프로세스를 attach 할 때 주로 사용하는 DebugActiveProcess() 를 후킹해서 attach 를 못 하게 하는 기법으로, 후킹된 해당 DebugActiveProcess() 를 다시 후킹해서 원복시킴

    • Self Debugging

      : 부모 프로세스에서 WaitForDebugEvent() API를 호출함, 해당 API에 DebugActiveProcessStop() 함수를 삽입하여 부모 프로세스가 디버거를 놓아버리게 함

OllyDBG 옵션을 이용한 우회
  • int3을 이용한 디버거 감지

    : OllyDBG 의 메뉴에서 Option - Debugging Option 으로 가서 Exception 탭에서 Ignore memory access violations in KERNEL32 을 켜고 INT3 break 옵션과 Single-step break 옵션을 켜준다

    : 디버거 인테럽트가 발생해도 자동으로 예외 처리 핸들러를 호출하여 디버거 사용이 가능

  • SetUnhandledExceptionFilter

    : 예외 처리 관련 안티 디버깅에 대한 우회는 해당 옵션으로 대부분 우회 가능

플래그 수정으로 우회
  • PEB를 이용한 방법

    : PEBBeginDebugged 플래그 값을 0 으로 패치

  • 리모트 디버깅 감지

    : KD_DEBugGER_NOT_PRESENT 는 커널 전역 변수, 해당 변수를 TRUE 로 패치해버림

  • 0xCC 자체를 탐지

    : 코드 상의 0xCC 를 찾아서 패치


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

코드 후킹  (0) 2019.03.21
FSC_Level1 풀이  (0) 2019.03.20
Petya Ransomware 분석  (0) 2019.02.19
PE 헤더  (0) 2019.02.18
2. C 문법과 디스어셈블리  (0) 2019.02.14
  1. 사전 정보 수집
  • 해당 바이너리를 실행할 경우 위와 같이 키를 입력하라는 메시지가 출력된다.
  • 바이너리가 요구하는 키를 맞힐 경우 플래그가 출력되는 형식이라고 추측할 수 있다.
  • PEiD를 이용하여 확인해본 결과 해당 바이너리는 PEiD로는 알 수 없는 방법으로 패킹되었거나 패킹되지 않았음을 확인할 수 있다.
  • 패커가 사용되었는지 확실히 하기 위해 IAT를 봐서 추가적으로 확인할 수 있다.
  • 패커는 대부분 IAT를 망가뜨리기 때문에 PEView를 이용하여 해당 바이너리의 IAT를 확인했을 때 Import한 함수의 개수가 적으면 패킹이 되었을 것이라 의심해볼 수 있다.
  • PEView를 이용하여 해당 바이너리의 IAT를 확인해본다.

  • 해당 바이너리가 두 개의 dll로부터 충분히 많은 Import 함수가 존재하므로 패킹되지 않은 바이너리임을 알 수 있다.
  • 추가적으로 해당 바이너리의 Image Base는 일반적인 EXE 파일이 가지는 0x400000 번지가 아닌 0x69000000이었다. 따라서 해당 주소부터 분석을 진행한다.
  1. 분석
  • 먼저 실패할 경우 출력되는 메시지부터 백트레이싱을 수행한다.
  • 참조된 문자열들을 검색해본 결과 0x69001152번지에 해당 문자열이 있는 것을 확인했다.

  • 해당 위치의 윗부분에 문자열을 입력받고 특정 문자열과 비교를 하는 stricmp 함수를 발견할 수 있었다.
  • 해당 함수에 bp를 걸고 실행 시 어떤 문자열이 오는지 확인해본다.

  • 비교 대상 문자열은 "Asm07REC"이다.
  • 해당 문자열을 입력하면 다음과 같이 플래그가 출력된다

 




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

코드 후킹  (0) 2019.03.21
우회 기법  (0) 2019.03.20
Petya Ransomware 분석  (0) 2019.02.19
PE 헤더  (0) 2019.02.18
2. C 문법과 디스어셈블리  (0) 2019.02.14

1. Petya?

2016년 3월 발견된 랜섬웨어로 디스크의 MBR 영역을 변조하여 PC의 부팅이 불가능하도록 하고 부팅 시 Ascii 코드로 작성된 해골 화면을 띄우며 결제를 유도하는 랜섬웨어다. MBR의 0번 섹터부터 56번 섹터 사이만 접근하여 조작하기 때문에 다른 부팅 디스크를 통해 부팅하여 MBR 영역만 복원해주면 정상적으로 사용이 가능하다.



2. Analysis

  2-1. File information


 윈도우 운영체제에서 실행되는 PE 형식의 파일이며 해당 파일에 대한 해시 정보는 위의 표와 같다


 분석에 앞서 Virustotal 사이트에서 제공하는 기본적인 정보에 대해서 파악하면 다음과 같다.


 

  

  해당 PE 파일은 5개의 섹션으로 이루어졌으며 각 섹션의 엔트로피 값을 통해 섹션의 암호화 여부를 확인할 수 있다

 엔트로피를 통한 패킹 및 암호화 여부는 다음 논문에서 확인할 수 있었다.

 

Using Entropy Analysis to Find Encrypted and Packed Malware / J. Hamrock and R. Lyda / IBM in IEEE Security & Privacy / 2007. 


발췌한 엔트로피를 통한 패킹 및 암호화 여부에 대해 파악할 수 있는 자료이다.

해당 자료를 참고하면 엔트로피 값이 7 이상일 경우 암호화된 데이터로 분류하며 따라서 엔트로피 값이 7.19로 나타난 text 섹션은 암호화된 코드가 삽입되었다고 판단할 수 있다

백신 제품들이 API 호출 탐지 기반 등 고도화된 탐지 기법을 도입함에 따라 해당 기법들에 대해 우회하기 위한 방안으로 프로그램이 실행될 때 암호화된 코드 데이터를 복호화 하여 실행하는 백신 우회 기법을 사용한 것으로 의심해볼 수 있으며 좀 더 자세히 알아보기 위해 동적 분석을 진행해본다.


  2-2. Dynamic Analysis

Petya를 동적 분석하기 위한 환경으로 VMware 가상화 도구를 사용했으며, 프로그램 역분석 도구로는 IDA Pro 6.6 도구를 사용했다. 먼저 Petya를 실행시킨 다음 컴퓨터가 강제종료 된 상황에서 볼 수 있는 화면이다.

 


Petya 실행 후 OS가 재부팅 되면 위와 같이 디스크를 확인하는 과정이 진행된다

위 과정이 끝나면 다음과 같은 랜섬노트가 화면에 출력된다.






 해골 문양이 출력된 이후에 이어서 위와 같은 랜섬노트가 화면에 출력된다

사용자의 디스크가 암호화 됐으며 이를 복구하기 위해선 Tor Browser를 이용하여 가상화폐로 결제를 유도하는 화면이며 부팅을 할 수 없는 상황이다

다음은 Petya 실행 전후의 MBR 영역이다.


    

 Petya 실행 전 정상적인 MBR()과 실행 후의 MBR()


위 사진과 같이 Petya를 실행 전후로 MBR이 변경된 것을 확인할 수 있다

다음으로 동적 분석을 수행하기 전에 MBR이 무엇인지, PC의 부팅과 어떤 관련이 있는지 간략하게 정리한다.



  2-3. MBR (Master Boot Record)?

파티션이 여러 개인 다중 파티션의 경우 각 파티션의 첫 번째 섹터에 존재하는 BR(Boot Record)만으로는 부팅 파티션을 결정할 수 없다

MBR은 파티션 테이블을 참조하여 부팅이 가능한 파티션을 찾고 코드를 실행하여 해당 파티션으로 부팅이 되도록 하는 코드가 있는 메모리 영역이다

저장매체의 첫 번째 섹터에 위치하는 512 bytes의 영역으로 구조는 다음과 같다



PC가 부팅되면 디스크의 첫 번째 섹터를 호출하며 부트 코드가 실행된다

부트 코드는 파티션 테이블에서 부팅이 가능한 파티션을 찾아 해당 파티션의 부트 섹터를 호출해주는 역할을 하며, 파티션 테이블에는 각 파티션들에 대한 정보가 있으며 SignatrueMBR 디스크 영역이 가지는 고유의 바이트(0x55AA)이다.

 



각 파티션 테이블은 위와 같은 구조를 가지며 각 바이트들이 뜻하는 의미는 다음과 같다.

-       Boot flag(1byte)  : 해당 파티션이 부팅이 가능한 파티션인지 여부를 나타냄

(0x80  : 부팅 가능, 0x00 : 부팅 불가능)

 

-       CHS Start(3bytes) : 해당 파티션의 시작 주소를 의미함

 

-       Part Type(1byte) : 해당 파티션의 종류를 의미함

(0x0B : CHS 모드 FAT32, 0x0C : LBA 모드의 FAT32, 0x07 : NTFS )

 

-       CHS End(3bytes) : CHS 방식의 종료 주소

-       LBA Start(4bytes) : LBA 시작 주소, 파티션 시작 섹터 번호

-       Size in Sector(4bytes) : 파티션의 섹터 수

 

MBR 방식을 사용하는 시스템에서는 MBR의 부트 코드는 부팅에 필수적인 코드이며 따라서 Petya와 같이 MBR의 영역의 데이터의 무결성을 해치는 공격이 발생할 경우 사용자의 PC가 부팅이 가능한 파티션을 찾지 못해 부팅을 할 수 없는 상황이 발생할 수 있다

다음으로는 Petya가 어떤 과정으로 MBR을 암호화 하는지 파악하기 위해 IDA Pro disassembly 도구를 이용하여 정적 분석을 시행한다.


  2-4. Static Analysis?

Petya를 정적분석하기 위한 환경으로 IDA Pro 6.6 + Hex-ray Decompiler plugin을 이용하여 역분석을 진행한다

먼저 Petya는 실행 과정에서 특정 데이터 영역을 복호화 하여 코드 영역에 쓰며 해당 코드 영역을 실행시키는 형태로 실행된다

복호화 하여 실행시키는 코드는 MZ 헤더 형식의 실행 파일이었으며 다음 사진은 프로그램 실행 도중에 MZ 헤더로 시작하는 코드를 복호화 하여 코드 영역에 올리는 사진이다.



왼쪽 사진은 프로그램을 실행하기 전의 코드이며 해당 프로그램을 실행할 경우 0041AE72 ~ 0041B358 까지의 코드를 반복하며 0041B35E MZ 헤더로 시작하는 코드를 삽입한다

Petya가 수행하는 암호화 루틴은 해당 코드에서 수행되며 실행 도중에 메모리 덤프를 통해 해당 MZ 헤더의 실행 파일을 얻을 수 있다

Petya가 수행하는 암호화 루틴을 분석하기 위해 해당 DLL을 확보하여 IDA에 올려서 이후 분석을 진행한다



분석 대상 MZ 실행 파일을 IDA로 올리고 DllEntryPoint에서 호출하는 몇 가지의 함수들을 타고 들어가보면 위와 같은 코드를 확인할 수 있다

CreateFileA 함수를 통해 운영체제가 설치된 물리 디스크의 핸들을 받아오며 만약 핸들을 얻지 못하면 프로그램은 종료된다

(line32) 핸들을 얻은 후 프로그램은 MBR이 있는 디스크의 0번지부터 0x7200번지까지 암호화 루틴에 들어간다

다음은 첫 번째로 실행되는 암호화 과정이다.

 


위 코드는 0x200번지부터 512byte씩 블록 단위로 읽고 0x37 xor 연산을 수행한 뒤에 같은 영역에 다시 쓰는 과정이다

이 과정을 0x22번 수행하므로 0x200부터 0x4400 까지는 원본의 데이터와 0x37xor 연산을 한 결과가 저장된다

위 사진에서 보이는 ReadFromFile 함수와 WriteToFile 함수는 함수들의 실행 코드를 분석하여 분석의 편의를 위해 임의로 붙인 이름이다

다음은 각 함수들의 모습이다.



임의로 이름을 붙인 ReadFromFile 함수는 첫 번째 인자로 파일의 이름을 받고, 두 번째 인자로는 파일로부터 읽은 데이터를 저장할 버퍼를 전달받는다

세 번째 인자와 네 번째 인자는 파일 포인터를 계산하는데 사용되며 결과적으로 해당 함수가 호출되면 첫 번째 인자로 전달받은 파일이름에 해당하는 파일을 열고 3번 째 인자로 전달받은 a3512를 곱한 지점부터 512바이트를 읽어서 두 번째 인자로 전달받은 버퍼에 저장한다.



위 함수에서 세 번째 인자로 전달받은 값을 ecx 레지스터에 저장한 후에 shift left9차례 수행하여 setFilePointerEx 함수의 인자로 전달한다. 결과적으로 파일 포인터는 세 번째 인자의 512배에 해당하는 지점을 가리키게 된다.



WriteToFile 함수는 첫 번째 인자로 전달받은 파일의 이름에 해당하는 파일을 열어서 세 번째 인자로 파일 포인터를 계산, 두 번째 인자로 전달받은 버퍼에 있는 데이터를 512바이트 쓰는 함수이다

파일의 포인터 계산하는 방법은 위의 ReadFromFile 함수와 같다. Petya가 디스크로부터 데이터를 읽고 암호화 한 데이터를 쓰는 일련의 과정들은 모두 위의 두 함수를 통해서 진행된다.



디스크의 0x200 ~ 0x4400 영역을 암호화 한 이후에 오는 코드는 0x00번지부터 0x200번지까지 MBR 영역을 읽은 후에 암호화 하는 과정이다

v47 변수에 해당 암호화 코드는 저장되며 이 v47 변수는 프로그램이 종료하기 다른 디스크 영역에 쓰이게 된다.



이후에 호출되는 함수에서는 두 변수에 악성 데이터를 쓰는 루틴이다.

 v51 변수에 문자 ‘7’512 bytes를 쓰며, v50에는 함수로 이루어진 암호화 루틴을 수행하며 랜섬노트에 출력되는 URL 텍스트 중 일부를 저장한다

해당 함수는 의미 있는 암호화 루틴이 아니므로 설명 및 분석을 생략한다.



앞에서 설명한 랜섬노트 일부 텍스트가 담긴 v50 변수를 0x6C00에 쓰고 ‘7’문자로 이루어진 512 bytes의 변수 v510x6E00에 쓰여진다

그 다음에 쓰여지는 데이터가 중요한데, v47 변수는 앞에서 0x00~0x200 MBR 영역을 암호화 한 데이터가 저장된 변수이다

여기까지 Petya가 수행하는 암호화 루틴이며 이를 정리하면 다음과 같다.




실제로 Petya로 암호화된 가상머신의 디스크를 분석 프로그램을 이용하여 열어본 화면이다

랜섬노트로 암호화된 0x4400~0x6E00 까지의 영역 중 일부이며 해골 문양의 일부와 랜섬노트에 출력되는 텍스트들을 확인할 수 있었다.



암호화 루틴이 끝난 후에 호출되는 함수이다

해당 함수 내에서 GetCurrentProcess 함수를 통해 현재 프로세스의 핸들을 얻고 OpenProcessToken, LookupPrivilegeValueA, AdjustTokenPrivileges 함수들을 호출하여 시스템 종료 권한을 얻으며 권한을 상승한다

그 이후에 “NTDLL.DLL”을 로드하고 NtRaiseHardError를 통해 PC를 강제 종료한다.


 

  2-5. Decryption

분석한 Petya 랜섬웨어가 사용한 암호화 루틴은 블록암호 또는 비대칭키 암호와 같이 복잡한 암호화 알고리즘을 사용하지도 않았으며 키가 1 바이트인 스트림 암호라고 볼 수 있다

해당 키는 역분석을 통해 간단히 찾을 수 있었고 암호화 한 MBR 데이터를 숨겨둔 위치 또한 분석을 통해 얻을 수 있었다

따라서 암호화 된 MBR 영역의 hex 값을 복호화 하는 코드를 작성하여 암호화 전의 데이터와 비교해봤다.



MBR 영역의 데이터는 0x37 XOR 연산을 통해 암호화 됐으며 해당 데이터를 0x37과 다시 XOR 연산을 해주면 원본의 데이터를 확보할 수 있다

위와 같은 코드를 작성하여 암호화 된 데이터만 추출하여 해당 코드를 실행시키고 나온 복호화 데이터를 비교하면 다음과 같았다.


    

위 사진에서 왼쪽은 원본의 MBR 데이터이고 오른쪽의 사진은 암호화 된 MBR 데이터를 복호화 한 결과이다. 암호화 과정이 간단했던 만큼 쉽게 복호화 한 데이터를 얻을 수 있었다.






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

우회 기법  (0) 2019.03.20
FSC_Level1 풀이  (0) 2019.03.20
PE 헤더  (0) 2019.02.18
2. C 문법과 디스어셈블리  (0) 2019.02.14
1. IA-32 assembly  (0) 2019.02.14

1. PE 헤더 (PE Header)?

  Portable Executable File Format의 약자로 이식 가능한 다른 곳에서도 실행이 가능한 포맷이다.


2. PE 파일 생성 과정

 

- 소스코드(cpp)를 컴파일 하면 어셈블리 코드가 만들어지고 어셈블러에 의해 어셈블리 코드들은 오브젝트 코드로 만들어짐

- 이렇게 만들어진 오브젝트 코드들은 정적으로 라이브러리 코드를 모두 포함하여 만들어지지 않는 이상 동적 링킹을 거치게 된다.

- 동적 라이브러리나 각종 리소스 데이터와 Import, Export 테이블을 처리할 수 있는 정보를 어딘가에 적어둠

- 윈도우는 exe 파일을 만들 때 약속된 규약에 따라 정보를 기입하고 이렇게 만들어진 파일이 PE 파일이다.

- EXE 또는 DLL을 실행하면 개발자가 만든 코드가 실행되기 전에 PE 정보부터 읽어와서 바이너리를 메모리에 올리기 위한 각종 데이터를 설정하는 작업을 함



3. PE 파일 구조

- 소스코드(cpp)를 컴파일 하면 어셈블리 코드가 만들어지고 어셈블러에 의해 어셈블리 코드들은 오브젝트 코드로 만들어짐

- PE 헤더는 여러 구조체로 이루어져 있으며 PE viewer를 이용하여 직접 구조체 살펴볼 수 있음 (eg : VX PE-Viewer)
- VX PE-Viewer는 현재 2.0 버전까지 배포됐으나 2.0 버전에서는 런타임 에러가 발생하여 실행이 불가능했음, 따라서 1.1 버전으로 분석을 진행함


  3-1. IMAGE_DOS_HEADER
   - 크기는 0x3D 바이트이며 가장 먼저 위치하여 PE 파일의 맨 처음 부분에 해당한다. Hex 에디터를 이용하여 PE 파일을 열어도 해당 구조체를 가장 먼저 찾아볼 수 있다.         - 가장 첫 번째 필드인 (e_magic)과 마지막 필드(e_lfanew)가 중요하다
 

    


     

   - e_magic 필드는 PE 파일의 시그니처로 최초 개발자의 이름을 딴 "MZ"로 시작한다. 위에 그림에서도 확인해볼 수 있다.

   - e_lfanew 필드는 다음 구조체인 IMAGE_NT_HEADER의 구조체 위치를 알아내는 데 사용되는 값

  

 3-2. IMAGE_NT_HEADER

   - IMAGE_DOS_HEADER의 마지막 필드인 e_lfanew 필드를 이용하여 다음과 같이 주소를 알아낼 수 있음

    

     

  

    - 다음과 같이 구성되어 있으며 Signatrue는 "PE\0\0"을 나타내는 4바이트 값(50 45 00 00)이다.

    - 요즘 윈도우에서는 해당 자리에 50 45 00 00이 아닌 다른 값이 자리하게되면 해당 파일은 윈도우에서 실행되지 않음,

      과거엔 이를 확인하지 않아 바이러스나 악성코드에 의한 감염 표식용으로 사용됨


    



    



  3-3. IMAGE_FILE_HEADER

   - IMAGE_DOS_HEADER의 마지막 필드인 e_lfanew 필드를 이용하여 다음과 같이 주소를 알아낼 수 있음

 

    

   - Machine

     : 어떤 CPU에서 이 파일이 실행될 수 있는지를 알려줌

   - NumberOfSections 

     : 섹션이 몇 개 있는지를 알려줌 (.text, .data 등)

       일반적으로 비주얼 스튜디오에서 MFC로 별다른 옵션 변경 없이 빌드한 경우 .text, .rdata, .data, .rsrc로 4개의 섹션이 존재함

       패킹이나 프로텍팅 등의 이유로 섹션 수가 증가하면 이 값도 같이 증가함

   - TimeDateStamp

     : 해당 파일이 빌드된 날짜 정보

   - SizeOfOptionalHeader

     : IMAGE_OPTIONAL_HEADER32의 구조체 크기, 해당 구조체는 PE를 로딩하기 위한 중요한 정보를 갖고 있는데, 이 구조체는 운영체제마다

       크기가 다를 수 있기 때문에 PE 로더에서는 SizeOfOptionalHeader값을 먼저 확인한 뒤 IMAGE_OPTIONAL_HEADER32 구조체의 크기를 처리함 

   - Characteristics

     : 현재 파일이 어떤 형식인지 알려주며 DLL인지 EXE인지 구분하는 용도로 활용이 가능하다.

    



  3-4. IMAGE_OPTIONAL_HEADER
    - PE 구조체 중 중요한 값을 가장 많이 담긴 헤더

    


    - MAGIC

     : 시그니처, 32비트의 경우 0x10B가 들어오며 64비트의 경우 0x20B가 된다.

    - SizeOfCode

     : 코드 양의 전체 크기를 가리킴, 실제 개발자가 만든 코드의 양이 해당 필드에 들어가며, 바이러스나 악성코드는 이 필드를 읽어서 

       악성 코드를 복제할 위치를 계산하기도 하며, 보안 솔루션에서 코드 섹션의 무결성 검사를 위한 크기도 해당 필드를 통해 정함 

    - MajorLinkerVersion, MinorLinkerVersion

     : 어떤 버전의 컴파일러로 빌드했는지를 알려줌 (비주얼 스튜디오 6.0일 경우 6.0이 자리함)

    - ImageBase

     : 해당 파일이 실행될 경우 실제 가상 메모리에 올라가는 번지를 가리킴, EXE 파일의 경우 번지 지정을 별도로 해주지 않는 이상 0x400000에 올라감.

       DLL의 경우 기본 이미지 베이스 주소가 0x10000000 번지로 정해져 있지만 재배치 속성에 따라 재할당되기도 함

    - AddressOfEntryPoint

     : 실제 파일이 메모리에서 실행되는 시작 지점을 말함. 디버거를 통해 파일을 실행했을 때 디버거는 첫 실행 지점을 이곳과 ImageBase를 합산한 위치에 지정해서 멈춤

    - BaseOfCode

     : 실제 코드가 실행되는 번지, 코드 영역의 시작 주소는 ImageBase에 BaseOfCode를 더한 위치이다. 기본적으로 0x1000의 값을 가짐

    - SectionAlignment, FileAlignment

     : 각 섹션을 정렬하기 위한 저장 단위, 보통 0x1000이 지정되며 섹션의 크기가 0x1000이하일 경우에도 패딩을 통해 0x1000을 채움

    - SizeOfImage

     : EXE나 DLL이 메모리에 로딩됐을 때의 전체 크기, 로더가 해당 필드를 참조하여 메모리 공간을 할당함

    - SizeOfHeaders

     : PE 헤더의 크기를 알려주는 필드

    - Subsystem

     : 해당 프로그램이 GUI(0x02)인지 CLI(0x03)인지를 알려줌

    - DataDirectory

     : IMAGE_DATA_DIRECTORY의 구조체로서, VirtualAddress와 Size라는 필드가 포함돼있음

       Export 디렉토리나 Import 디렉토리, 리소스 디렉토리, IAT 등 각각의 가상 주소와 크기를 이 필드를 통해 알 수 있음

      

      


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

우회 기법  (0) 2019.03.20
FSC_Level1 풀이  (0) 2019.03.20
Petya Ransomware 분석  (0) 2019.02.19
2. C 문법과 디스어셈블리  (0) 2019.02.14
1. IA-32 assembly  (0) 2019.02.14

1. 함수 호출 규약

  - 함수가 호출될 때 인자를 전달하거나 함수의 결과를 반환하는 방법에 대한 규약

  - 대표적으로 __cdecl, __stdcall, __fastcall, __thiscall 네 가지 존재

  - 분석 과정에서 해당 함수의 호출 규약을 판단하여 전달하는 인자 및 반환 값을 식별하는 것이 중요


  1-1) __cdecl


    
    - main 함수에서 __cdecl 호출 규약을 사용한 sum 함수를 호출하는 부분인 call calling.00401000 

    - 해당 함수 호출 이후 add esp, 8과 같이 스택을 보정해주는 명령어가 존재할 경우 __cdecl 규약

    - 또한 해당 스택의 크기를 이용하여 파라미터의 개수까지 확인이 가능, 파라미터는 4바이트씩 계산되므로 위 함수에서는 총 2개의 파라미터가 있음을 파악 가능


  1-2) __stdcall


    

    - 함수가 반환하는 ret 명령어에 오퍼랜드로 8이라는 상수가 온다

    - __cdecl 규약은 함수 반환 후에 해당 함수를 호출한 부분에서 스택을 조정하지만 

       __stdcall 규약을 사용할 경우 호출된 함수 내에서 반환할 때 스택을 조정함, 해당 값을 통해서도 파라미터의 개수를 추측이 가능함


     - Win32 API는 __stdcall 호출 규약을 사용함. MessageBoxA() 함수를 살펴보면 다음과 같음.

    

      해당 함수의 반환 부분에서 총 0x10 만큼의 스택을 재조정해주는 것을 확인할 수 있음

      따라서 해당 함수는 총 4바이트 크기의 파라미터를 4개 (0x10) 전달받을 것이라고 추측이 가능하며 실제 함수를 확인해보면 다음과 같이 일치함을 확인할 수 있음


    




  
1-3) __fastcall

    

    

    -  sub esp, 0xC 명령어로 스택 공간을 확보하고 edx, ecx 레지스터에 있는 값을 이용한다.
    - 함수의 호출부를 봐도 함수를 호출하기 직전 파라미터를 edx, ecx 레지스터에 넣는다.
    - __fastcall 호출 규약은 파라미터가 2개 이하일 경우에 사용이 가능하며 파라미터를 레지스터를 이용하여 전달한다. (빠름)

  1-4) __thiscall


    

    -  c++에서 객체의 멤버에 접근하기 위한 this 포인터(객체에 대한 포인터)를 edx레지스터에 전달한다.



2. if 조건문


    -  C 코드의 조건문이 디스어셈블되면 다음과 같은 형태를 가진다.



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

우회 기법  (0) 2019.03.20
FSC_Level1 풀이  (0) 2019.03.20
Petya Ransomware 분석  (0) 2019.02.19
PE 헤더  (0) 2019.02.18
1. IA-32 assembly  (0) 2019.02.14

1. 레지스터 (Register)


  - 프로세서가 사용하는 작은 메모리 공간


  1-1 ) EAX (Accumulator)

   - 산술 계산에 사용되며 함수의 반환 값을 저장


  1-2 ) EDX (Data)

   - 산술 계산에 사용되지만 EAX와 같이 함수의 반환 값을 저장하지는 않음


  1-3 ) ECX (Counter)

   - 주로 반복문의 반복 횟수를 카운팅하는데 사용


  1-4 ) EBX (Base)

    - Base 값 지정, 주로 index 계산의 Base로 사용


  1-5 ) ESI (Source)

    - 문자열 또는 각종 반복 데이터를 처리하거나 메모리를 옮길 때 Source 주소 지정을 위해 사용


  1-6 ) EDI (Destination)

    - 문자열 또는 각종 반복 데이터를 처리하거나 메모리를 옮길 때 Destination 주소 지정을 위해 사용


  1-7 ) al & ah (Low & High)

    - 8비트 레지스터

    

   




2. 어셈블리 (Assembly)


  2-1 ) PUSH & POP

   - 스택에 값을 넣는 것을 PUSH, 스택의 값을 가져오는 것이 POP (PUSHAD, POPAD : 모든 레지스터를 PUSH, POP)


  2-2 ) MOV

   - 값을 넣어주는 명령어 (ex : MOV  eax, 1 )


  2-3 ) LEA

   - 주소를 가져와 넣어주는 명령어


  ※ MOV와 LEA의 차이


   



  2-4 ) ADD

   - src에서 dest로 값을 더하는 명령어


  2-5 ) SUB

   - src에서 dest로 값을 빼는 명령어


  2-6 ) INT

   - 인테럽트를 발생시키는 명령어. 뒤에 오는 오퍼랜드 값에 해당하는 인테럽트를 발생시킴. 

      대표적으로 INT 3 명령어가 존재하며 0xCC opcode를 가진 DebugBreak()가 있다. 


  2-7 ) CALL

   - 함수를 호출하는 명령어. Call 뒤의 오퍼랜드로 주소가 오게되고 해당 주소를 호출한 뒤 작업이 끝나서 Ret 명령어를 만나면 Call 다음 주소로 되돌아옴


  2-8 ) INC & DEC

   - 오퍼랜드에 1을 더하거나 뺀다.


  2-9 ) AND & OR & XOR

   - dest와 src를 연산한다. XOR eax, eax 와 같은 구문은 eax를 초기화하는데 사용


  2-10 ) NOP

   - 아무것도 수행하지 않고 다음 명령어를 수행하는 명령어


  2-11 ) CMP & JMP

   - 비교 및 점프 명령어




3. 스택 (Stack)


  - 프로세서가 사용하는 작은 메모리 공간, 함수의 반환 주소, 파라미터와 함수 내에서 선언하는 지역 변수가 해당 메모리에 위치함

 

  3-1 ) 지역 변수


   


   - push  ebp 

     : 이전 함수의 stack base pointer를 저장함 (호출된 함수의 스택 공간을 사용하기 위함)

   - mov  ebp, esp

     : 이전 함수의 esp를 현재 함수의 stack base pointer로 옮김  

   - sub  esp, 50h

     : 현재 함수의 stack pointer를 50 감소시킴 (0x50 크기의 지역 변수 공간 할당)  


   -> 지역변수가 4바이트 크기라 가정하면 ebp - 4 : 첫 번째 지역변수, ebp - 8은 두 번째 지역변수가 됨

  3-2 ) 함수의 호출과 파라미터

  

   


   - 위와 같이 3개의 인자를 전달받는 함수가 있을 경우 마지막 인자부터 역순으로 스택에 push되고 함수가 호출됨

   - 따라서 함수 호출 후 ebp에는 이전 스택의 base pointer, ebp + 4에는 함수의 리턴 주소가  위치하고 그 위로는 함수에 전달된 인자가 위치함

   - ebp + 0x8  : 첫 번째 파라미터

   - ebp + 0xC  : 두 번째 파라미터

   - ebp + 0x10 : 세 번째 파라미터


  3-3 ) 함수의 리턴 주소

   - 함수가 호출되면 전달된 인자들이 스택에 쌓이고 그 이후에 함수의 리턴 주소가 위치함

   




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

우회 기법  (0) 2019.03.20
FSC_Level1 풀이  (0) 2019.03.20
Petya Ransomware 분석  (0) 2019.02.19
PE 헤더  (0) 2019.02.18
2. C 문법과 디스어셈블리  (0) 2019.02.14

+ Recent posts