Kali-KM_Security Study

 개요

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

 

암호학

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

기밀성

무결성

가용성

부인 봉쇄


대칭키

대칭키 암호 방식에서는 암호화에 사용되는 암호화키와 복호화에 사용되는 복호화키가 동일하다는 특징이 있다. 쉽게 아래 그림과 같이 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
암호학 기초 개념  (2) 2016.11.23
Memory Detection(메모리 진단)  (0) 2016.09.26
Assembly로 보는 코드, strcmp 문자열 비교  (0) 2016.08.08
WFP 무력화  (0) 2016.06.21

Comment +2

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
Memory Detection(메모리 진단)  (0) 2016.09.26
Assembly로 보는 코드, strcmp 문자열 비교  (0) 2016.08.08
WFP 무력화  (0) 2016.06.21
DLL이란?  (3) 2016.05.29

Comment +0

메모리에서 특정 코드로 시작하는 스레드를 찾는 코드이다. 주로 코드 인젝션을 통해 다른 프로세스에 코드가 인젝션 되었는지 확인할 때 사용하면 된다.

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




Comment +0