KillDisk 해체: BlackEnergy 파괴적 컴포넌트의 리버스 엔지니어링
BlackEnergy 위협에 대한 긴 소개는 생략하고 “ololo.exe”라는 악성코드 구성요소를 연구하는 것으로 곧바로 들어갑니다. KillDisk는 BlackEnergy 프레임워크의 모듈로, 데이터 파괴와 APT 작업 중 혼란 및 주의를 산만하게 하는 목적을 가지고 있습니다.
https://www.virustotal.com/en/file/11b7b8a7965b52ebb213b023b6772dd2c76c66893fc96a18a9a33c8cf125af80/analysis/오늘 분석에 사용된 주요 도구는 Process Monitor SysInternals 유틸리티의 일부로 Mark Russinovich가 제공하며 IDA Pro Disassembler입니다. 모든 작업은 Windows XP 운영 체제를 기반으로 한 가상 환경에서 수행됩니다. 테스트 VM의 간단한 초기 설정을 만들고, 머신을 켜고 “Before infection”이라는 스냅샷을 생성하는 것부터 시작합니다. 시작합시다!
연구의 주제와 관련된 모든 이벤트를 추적하기 위해 Process Monitor 을(를) 실행하고 화면에 표시되도록 합니다:그리고 필요한 프로세스를 추적하는지 확인합니다:
다음으로, 바이러스를 IDA Pro Disassembler 에 로드하고 다음과 같은 화면을 확인합니다.이는 WinMain 함수, 즉 메인 함수로, 이 분석을 시작해 그 함수가 첫 번째(그리고 유일한) 작업으로 수행하는 것이
sub_40E070
:
이라는 절차 호출임을 알 수 있습니다. 따라서 해당 절차로 곧바로 이동해 보겠습니다. 여기에서 메모리로 파일 확장자를 추출하는 책임을 진 절차들과 이들 바로 뒤에 흥미로운 것을 호출하는 절차인 sub_40E080
:
을 발견할 수 있습니다. 그 내용을 자세히 살펴보겠습니다:
그 내부에는 흥미로운 시스템 호출
CreateFile
이(가) 포함되어 있으며 몇 가지 절차 sub_40C390
and sub_40C400
이(가) 있습니다. 이제 가상 실험실의 스냅샷을 찍고 여정을 계속할 때입니다!
함수 호출을 검토하고 CreateFile에 중단점을 설정하고 연구 샘플을 실행합니다: BreakPoint on it and execute our study sample:
대부분의 컴파일러는 함수를 호출할 때 인수를 스택을 통해 전달하며, 우리가 C로 작성된 샘플을 다루고 있는 만큼, C 지시에 따라 함수를 호출할 때의 인수는 스택에 오른쪽에서 왼쪽으로 푸시됩니다. 스택 에서 오른쪽에서 왼쪽 으로 첫 번째 함수 인수가 마지막으로 스택에 푸시되어 스택의 맨 위에 있는 첫 번째 요소가 됩니다. [1].
There is a special register for working with the stack – ESP (Extended Stack Pointer). The ESP, by definition, always points to the top of the stack:
스택을 살펴봅시다:
스택에 푸시된 마지막 인수(맨 위에 있는 것)가 함수에서 수락한 첫 번째 인수입니다(
CreateFile
함수의 구문에 따르면) 이번 경우 그것은 파일 이름이 포함되어 있습니다 \.PHYSICALDRIVE0
, 따라서 목표는 첫 번째 물리 드라이브입니다.이제 우리는 스택에서 해당 값들을 가져와 함수에 전달된 다른 인수를 분석합니다:
몇 가지 결론:
- lpFileName – 열거나 생성 중인 파일의 이름(경로)을 포함하는 ASCIIZ 문자열 포인터(이미 확인했습니다);
- dwDesiredAccess – 파일에 대한 액세스 유형: 이 경우 값은
0C0000000h
으로, 이는GENERIC_READ+GENERIC_WRITE
or읽기-쓰기 액세스를 의미합니다
; - DwShareMode – 파일을 다른 프로세스와 공유할 수 있는 모드로, 이 파라미터는 다른 값을 가질 수 있으며 이 경우 00000003b 값은
FILE_SHARE_READ+FILE_SHARE_WRITE
또는다른 프로세스에서 파일을 읽고 쓰기 위해 열 수 있음
; - IpSecurityAttributes – 커널 파일 객체와 관련된 보안 설정을 정의하는 SecurityAttributes 구조체(file winbase.h)에 대한 포인터로, 보안이 설정되지 않은 경우
NULL
값이 사용됩니다; - dwCreationDistribution – 파일이 존재하거나 존재하지 않을 때 수행할 작업을 결정하는 역할을 하며, 이 경우의 값은
3
을 의미하며,OPEN_EXISTING
or존재할 경우 파일을 열고 존재하지 않을 경우 오류를 반환
; - DwFlagsAndAttributes – 플래그와 속성; 이 파라미터는 생성된 파일의 특성을 설정하는 데 사용되며 읽기 모드에서는 무시됩니다 0.
- hTemplateFile – 새 파일이 생성될 때만 사용되는 파라미터
0
. - 성공 시 함수는 새 파일 핸들러를 을 반환합니다
ЕАХ
그리고 함수가 실패하면NULL
가 기록됩니다ЕАХ
레지스터에.
좋습니다, CreateFile
을 호출한 후 EAX에 비제로 값이 표시됩니다. 이는 요청된 파일의 핸들러가 포함되었음을 의미합니다, 예를 들어 \.PHYSICALDRIVE0
:
핸들러를 얻었으니 다음 호출로 진행해 봅시다,
sub_40C390
:
이전과 마찬가지로 절차 내부를 살펴보면 또 다른 흥미로운 호출이 나타납니다:
함수 힌트:
- hDevice – 장치 설명자 (익숙한 친구를 알아봅니다)
\.PHYSICALDRIVE0
); - dwIoControlCode – 작업 코드. 이 경우 값은
0x70000h
으로, 이는이거나 디스크 기하학 정보(실린더 수, 미디어 유형, 실린더당 트랙, 트랙당 섹터, 섹터당 바이트)를 검색하는 것입니다;
or retrieval of the information regarding disks geometry (amount of cylinders, type of media, tracks on cylinder, sectors per track, bytes per sector); - lpInBuffer – 입력 데이터가 있는 버퍼. 입력 데이터는 필요하지 않으므로
NULL
로 설정됩니다; - nInBufferSize – 바이트 단위의 입력 버퍼 크기이며, 우리의 경우
0
; - lpOutBuffer – 출력 버퍼에 대한 포인터. 그 유형은 dwIoControlCode 파라미터로 설정됩니다
0x0011FCA8h
; - nOutBufferSize – 바이트 단위의 출력 버퍼 크기
0x18h
(24); - lpBytesReturned – 출력에 기록된 바이트 수를 포함하는 변수에 대한 포인터
0x0011FCA4h
; - lpOverlapped – 구조체 OVERLAPPED 에 대한 포인터;
- 이 호출이 처리되면 버퍼를 검사합니다(인수로 전달된 주소에서 시작하여 lpOutBuffer 정확히
0x0011FCA8h
):
- 주소
0x0011FCA4h
에 예정대로 출력 버퍼에 작성된 바이트 수가 반환되었습니다(초록색으로 표시됨). 요청한 대로 24개의 기호를 얻었습니다. - 주소
0x0011FCA8h
첫 번째 물리 디스크의 기하 정보를 얻었습니다 (\.PHYSICALDRIVE0
):- 실린더 수 –
0x519h
(1305) - 미디어 유형 –
0x0Ch
(12) 고정 하드 디스크를 의미합니다. - 실린더 당 트랙 –
0x0FFh
(255) - 트랙 당 섹터 –
0x3Fh
(63) - 섹터 당 바이트 –
0x200
(512)
- 실린더 수 –
이제 세 번째 절차로 이동해 봅시다, sub_40C400
:
절차 내부에서 두 가지 호출이 즉시 우리의 주의를 끌었습니다, 특히
SetFilePointerEx
및WriteFile
:
- hfile – 우리가 친숙한 장치 설명자
\.PHYSICALDRIVE0
; - liDustanceToMove – 쓰기를 시작할 초기 위치
0
; - lpNewFilePointer – 파일에서 새 위치 포인터를 검색하는 변수에 대한 포인터
0x0011FCA8h
. 매개변수가 EMPTY (NULL)일 경우 새 포인터는 파일에서 반환되지 않습니다; - dwMoveMethod – 바이트 단위의 입력 버퍼 크기입니다. 저희의 경우
0x200h
(512) – 섹터 내 바이트 수입니다;
이제 우리의 “짐승”이 WriteFile
함수를 사용하여 이미 직접적이고 신뢰할 수 있는 접근을 수립한 파일을 0으로 채웁니다.
함수 힌트:
A WriteFile 함수는 파일 또는 I/O 장치에 데이터를 작성하고, 파일에 지정된 위치에서 시작하여 작성합니다. 이 함수는 동기식 및 비동기 작업 모두를 위해 설계되었습니다.
함수 인수 목록:좋습니다, 흥미로운 것을 놓치지 않기 위해 대시보드를 살펴보고 모든 것을 확인합니다. 스택:
모든 작업 준비 완료:
- 1 – 함수 호출에 대기 중
WriteFile
- 2 – Process Monitor 가 우리의 “짐승”의 근육 저마다의 움직임을 신경질적으로 기다리고 있습니다
- 3 – 레지스터에서 스택으로 인수가 푸시되었습니다
- 4 – 그리고, 스택 자체
- Hex View-EBP 창에서 두 번째 함수 인수(데이터 버퍼)가 주로 사용되는 지역에 전용 사각형을 표시했습니다. 제 말을 믿으셔도 됩니다 – 그것은 정확히
0x200h
(512
)의 0으로 가득 차 있습니다.
명령어를 F8 키로 실행하고 변화를 관찰합니다:기대했던 대로 Process Monitor 의 예리한 상황 보고는 적중했고 0 위치에서 512바이트 쓰기를 기록했습니다.
그 후 절차는 종료되고 다시 호출되지만 이번에는 다른 값으로:이번에는 이전에 파란색 프레임으로 표시한 인수 스택이 특히 0x200에 잘 드러났습니다
This merry-go-round goes on until the check of 0x40C865h
주소에서 확인한 EBX
레지스터 값이 로 표시되는 것은
(255 회
):
확실히 하기 위해 “파괴”의 악순환에 이은 명령어에 중단점을 설정합니다:
총
256
의 재작성 작업을 받습니다:기본적으로 종료하며, “짐승”은 수행 중인 중요 파일 핸들를 만지는 함수
CloseHandle
함수를 호출합니다.
함수 힌트:
함수 인수:
스택 준비의 전통적인 모습:
그리고 ESI 레지스터에 무엇이 있습니까?
기대했던 대로 우리의 익숙한 설명자가
0x44h
형태로 함수를 위해 전송되었습니다.
우리는 함수에서 EAX 레지스터에 반환된 값을 봅니다: 함수가 성공적으로 끝날 경우 반환된 값은
비널
입니다.
함수가 오류로 끝날 경우 반환된 값은 NULL입니다.
이번 조치도 우리의 주의 깊은 눈을 피해가지 않았습니다 Process Monitor:
모든 것이 정상으로 진행되었습니다 (시스템 드라이브의 가장 중요한 부분을 파괴하는 것을 정상이라고 할 수 있다면) ) 그리고 파일이 닫혔습니다.
계속해 봅시다:좀더 편안하게 인식할 수 있도록 함수를
Eraser
로 이름을 바꿨습니다. 성공적으로 데이터를 PhysicalDrive0
에서 파괴한 후, ESI
레지스터에 위치한 카운터가 1
)에 의해 증가되고, 값 0x0Ah
(10
와 검토되어 새로운 값을 사용하여 재작성 작업을 시작합니다:다음과 같이 10에 도달할 때까지 계속됩니다.
이와 같은 방식으로 이 “짐승”은 모든 물리 드라이브를 탐색하며 처음 512 * 255 = 128kb
를 삭제합니다(이는 “짐승”이 디스크의 기하를 알고 얻은 바이트 수에 따라 다릅니다).
이번에는 함수 CreateFileW
의 결과값이 다음과 같을 것입니다: 이는 우리 “즉석 실험실”에 두 번째 물리 디스크(또는 0 이상의 시퀀스 번호를 가진 어떤 종류의 디스크도) 존재하지 않는다는 것을 의미하며, 이 주기가 재작성할 아무것도 없습니다.
새롭게 얻은 지식을 바탕으로 우리가 방금 익힌 파괴적인 기능을 비활성화 시켜 보고, 코드를 수정하여 데이터 재작성을 우회할 수 있도록 해봅시다:
절차를 기억합시다
sub_40E080
이 포함된 CreateFileW
함수 호출을 우리는 0xFFFFFFFFh
값이 CreateFileW
함수 완료 후 나타나는 경우 , 물리 드라이브가 존재하지 않는 경우에만 값이 나타납니다 주소에 위치한 조건 분기 명령어를 무조건적인 것으로 변경합시다:
address to an unconditional one:
IDA Pro 멋진 도구이기 때문에 우리는 명령을 작성하기만 하면 됩니다.
우리가 고려해야 할 유일한 것은 명령의 크기입니다. 조건부 시퀀스로 사용되는 바이트 수를 확인해 봅시다:
우리가 볼 수 있듯이 조건부 분기 명령은
6
바이트와 같고, 무조건적 시퀀스는 5
바이트에 불과하므로 동등하게 하기 위해 다른 명령인 NOP를 추가해야 합니다:다음 명령은 이전에 무엇을 포함하고 있었든지 상관없이 NOP로 바꿉니다 (때로는 그곳에 0이 있는 것을 봅니다):
명령 주소가 다음 명령의 주소를 기준으로 동등하게 되었는지 이중 확인합니다:
이 조작 후 절차는 간단해 보일 것입니다: 입장하고 나와서 빈 루프를 만듭니다 (프로그램의 다른 부분이 이에 연결될 수 있으므로 이런 상태로 둡니다).Process Monitor에 의해 발견된 최종 증거에 따르면 분해된 KillDisk의 실행 결과 물리적 드라이브가 손상되지 않았습니다:
악성 코드는 디스크에 대한 정보를 읽고, 이를 분석한 후에 다음 분석할 비즈니스를 계속 진행했습니다.
출처:
[1] – Hacker Disassembling Uncovered. Kris Kaspersky, 2009.
Andrii Bezverkhyi 원본에서 번역 | CEO SOC Prime