Clop Sample을 다운 받은 후 IDA로 확인했을때의 모습이다. 바이너리가 호출하는 함수의 개수가 적은 것으로 보아 바로 악성행위를 분석할 수 있을 것 같지 않다. Line115에서 for문을 500000번을 도는데 이는 단순히 동적 분석을 방해하는 fake코드 인 것 같다. 해당 루프를 탈출하면 Line140의 조건문으로 들어가게 된다.
조건문안에는 쓰이지 않는 값들을 변수에 할당해주며 마지막에 401000()을 호출한다.
401000()에서는 VirtualAlloc을 통해 메모리를 할당하게 되는데 빨간 박스 부분에서 할당한 메모리에 값을 쓰게된다. 여기서 Line 176, 183, 185가 연산에 쓰이는 핵심 코드이며 나머지는 쓰레기 코드이다.
위 과정에서 입력한 내용을 Line232에서 호출하는 것으로 보아, 다음 단계로 넘어가기 위해 Shellcode를 쓰고 호출하는 것을 예상할 수 있다. 내용을 입력할 때 사용한 코드를 아래와 같이 python으로 똑같이 구현하여 쉘코드를 추출할 수 있었다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
import struct
defROL4(x,n): x = (x << n) | (x >> (32-n)) return x & 0xFFFFFFFF
withopen('clop','rb') as f: f.seek(0x933C) shellcode = f.read(1380*4) decode = b''
for i inrange(0,1380): decode += struct.pack('<I',((0x4559 ^ ROL4(0x4559 ^ struct.unpack('<I',shellcode[i*4:(i+1)*4])[0],9)) - i) & 0xFFFFFFFF)
withopen('decoded.bin','wb') as file: file.write(decode)
Step 2
Step 1에서 추출한 Shellcode를 IDA로 확인한 모습니다. TerminateThread, Virtual..와 같은 문자열이 있는걸 확인할 수 있고 이는 성공적으로 두번째 단계에 들어왔음을 의미한다.
Step 1에서 인자로 받아온 Kernel32와 GetProcAddress를 통해 필요한 함수 주소를 알아온다. 성공적으로 모든 함수의 주소를 알아 왔다면 VirtualQuery 함수를 호출한다.
VirtualQuery함수는 프로세스의 특정 메모리의 정보, 권한을 얻어온다. 여기서 lpAddress의 인자 값으로 retaddr이 들어갔는데 이 주소는 Step 1에서 Shellcode 호출 후 다음으로 실행될 주소이다. 이는 이전 Step 의 바이너리의 ImageBase주소의 정보를 얻기 위함으로 예상된다.
Line265 269에서 args[2],args[4]만큼의 크기를 VirtualAlloc한다. 여기서 args는 Step 1에서 Shellcode를 호출 할 때의 인자값들로 아래 그림을 참고하면 args[2]는 3번째 인자인 0x19d48 args[4]는 5번째 인자인 0x20200임을 알 수 있다.
각각 크기로 VirtualAlloc을 한 후, 반복문을 통해 0x19d48만큼 할당한 메모리에 값을 쓴다. 이 과정에서는 Step 1에서 Shellcode를 추출할 때 보였던 코드가 있으며, 이는 다음 단계로 넘어가는 Shellcode를 추출하는 것임을 다시 한 번 예상할 수 있다. Shellcode를 추출 한 후 aplib_decompress을 호출한다. 이때 인자는 0x19d48만큼 할당한 메모리와 0x20200만큼 할당한 메모리가 되는데 추출 한 Shellcode를 decompress하여 해당 내용을 더 큰 메모리에 쓰는 역할을 한다.
다음 단계로 넘어가기 위해 Step 1과 동일하게 해당 코드를 python으로 구현하였고, 이 때 decompress하는 과정은 아래 모듈을 사용했다.
defgetbit(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
# first byte verbatim self.destination += self.source.read(1)
# main decompression loop whilenot done: if self.getbit(): if self.getbit(): if self.getbit(): offs = 0 for _ inrange(4): offs = (offs << 1) + self.getbit()
if offs: self.destination.append(self.destination[-offs]) else: self.destination.append(0)
if data.startswith(b'AP32') andlen(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 isnotNoneand packed_size != len(data): raise RuntimeError('Packed data size is incorrect') if packed_crc isnotNoneand packed_crc != crc32(data): raise RuntimeError('Packed data checksum is incorrect')
result = APLib(data, strict=strict).depack()
if strict: if orig_size isnotNoneand orig_size != len(result): raise RuntimeError('Unpacked data size is incorrect') if orig_crc isnotNoneand orig_crc != crc32(result): raise RuntimeError('Unpacked data checksum is incorrect')
return result
defmain(): # 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'
defROL4(x,n): x = (x << n) | (x >> (32-n)) return x & 0xFFFFFFFF
withopen("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 inrange(len(result ) // 4): decode += struct.pack('<I',((0x0AE0 ^ ROL4(0x0AE0 ^ struct.unpack('<I',result[i*4:(i+1)*4])[0],9)) - i) & 0xFFFFFFFF)
withopen("decompress","wb") as f: f.write(decompress(decode))
Step 3
세 번째 단계로 들어왔다. 여기에는 악성코드에서 쓰일법한 함수들이 많이 있는 것으로 보아 실제 악성 행위를 하는 코드가 있을 것이다.
특정 조건을 만족하면 CreateThread함수를 호출한다. 이 때 Thread가 수행하는 함수는 악성 행위를 할 가능성이 높다.
WNetOpenEum,WNetEenumResource함수 호출로 현재 연결되어있는 네트워크 정보를 알아온다. 그리고 특정 조건이 만족되면 다시 한 번 CreateThread함수를 호출한다.
Crypt로 시작하는 함수들이 보인다. 암호화 관련 코드인 것 같다. pubKey 문자열을 복사하는데 해당 내용은 아래와 같다.
CryptStringToBinaryA : 포맷된 문자열을 바이트 배열로 변환
CryptDecodeObjectEx : 변환된 바이트 배열을 구조체 변수로 디코딩
CryptAcquireContextW : 특정 cryptographic service provider(CSP)에서 원하는 키 컨테이너의 핸들값을 가져옴
CryptImportPublicKeyInfo : pubkey의 핸들값을 가져옴
위에서 얻은 Key, 구조체, 핸들 값을 가지고 4014b0()을 호출한다.
4014b0()에서는 암호화할 path를 확인한다. 조건문에서 compare_hash_file의 반환 값에 따라 LABEL_37로 이동한다.
compare_hash_file함수에서는 특정 hash값과 path에 대한 hash값을 비교하여 return 값을 반환한다. 이 는 모든 Windows의 파일을 암호화하게 된다면 정상적인 작동이 어렵기에 Windows 동작에 필요한 특정 파일들을 암호화에서 제외하는 작업이다.
LABLE_37은 4014b0을 다시 호출하는 것을 확인 할 수 있는데 이는 해당 path의 하위 폴더를 대상으로 재귀적인 호출을 통해 암호화에서 제외하는 작업으로 판단된다.
그러고 나서 조건문을 만나는데, 파일 속성이 디렉토리가 아닐 경우, 파일이름이 .. , . 가 아닐 경우, 파일 이름이 README_README.txt가 아닐 경우와 추가적으로
특정 파일의 hash값과 일치 하지 않을 경우 (특정 파일이 아닌경우)
파일의 확장자가 특정 값이 아닐 경우 (특정 확장자가 아닌경우)에 조건문을 실행하게 된다. 여기서 제외되는 확장자는 아래와 같다.
- .CI0P : 과거 암호화 파일 확장자
- .OCX : ActiveX 파일
- .DLL : 동적 라이브러리
- .EXE : 실행 파일
- .SYS : 드라이버 파일
- .LNK : 바로가기 파일
- .ICO : 아이콘 파일
- .INI : 설정파일
- .MSI : Installer 파일
- .CHM : 도움말 파일
- .HLF
- .LNG : 언어팩 파일
- .TTF : 폰트 파일
- .CMD : 배치 파일
- .BAT : 배치 파일
- .CLLP : 현재 랜섬웨어 암호화 파일
조건을 만족하게 되면 앞에서 받아온 암호화 관련 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)을 사용한 것이다.
암호화에 쓰일 rc4_key를 만든 후 암호화 대상 파일의 이름과 같은 이름의 .clip확장자의 파일을 만든다. 그리고 clip^_-문자열과 rc4_key값을 encrypt_key함수를 통해 RSA로 암호화한 후 해당 파일에 적는다. (rc4_key를 encrpyt하는 과정)